feat: Sprint 0 — GoFiber + HTMX + TailwindCSS v4 scaffolding
- Go module: git.tukangketik.id/swanadiva/hostkeeper/v2 - GoFiber v2 server with static, template engine, middleware - HTMX + Alpine.js (downloaded from CDN) - TailwindCSS v4 with custom theme (Lumina System colors) - Custom CSS: dot-grid, glassmorphism, animations, toggle, modal - Layout template (sidebar nav + header + main) - Dashboard page with host list, search, filter, view toggle - Health endpoint: GET /api/health
This commit is contained in:
@@ -41,3 +41,7 @@ Thumbs.db
|
|||||||
# Build output
|
# Build output
|
||||||
dist/hostkeeper
|
dist/hostkeeper
|
||||||
v1/hostkeeper
|
v1/hostkeeper
|
||||||
|
|
||||||
|
# Node dependencies (for TailwindCSS build)
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
module git.tukangketik.id/swanadiva/hostkeeper/v2
|
||||||
|
|
||||||
|
go 1.26.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.14
|
||||||
|
github.com/gofiber/template/html/v2 v2.1.3
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
|
github.com/gofiber/template v1.8.3 // indirect
|
||||||
|
github.com/gofiber/utils v1.1.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.14 h1:Of3L+9qVFaQNwPlcmEdl5IIodHz8BSE0j37R7rWu4pE=
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.14/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||||
|
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
|
||||||
|
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
|
||||||
|
github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
|
||||||
|
github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
|
||||||
|
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
|
||||||
|
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||||
|
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||||
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import "github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
func Dashboard(c *fiber.Ctx) error {
|
||||||
|
return c.Render("index", fiber.Map{
|
||||||
|
"View": "hosts",
|
||||||
|
"NavItems": []fiber.Map{
|
||||||
|
{"ID": "hosts", "Label": "Hosts", "Icon": "server"},
|
||||||
|
{"ID": "terminal", "Label": "Terminal", "Icon": "terminal"},
|
||||||
|
{"ID": "sftp", "Label": "SFTP", "Icon": "folder-sync"},
|
||||||
|
{"ID": "snippets", "Label": "Snippets", "Icon": "code2"},
|
||||||
|
{"ID": "keychain", "Label": "Keychain", "Icon": "key-round"},
|
||||||
|
{"ID": "settings", "Label": "Settings", "Icon": "settings2"},
|
||||||
|
},
|
||||||
|
"Title": "Hosts",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import "github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
func Health(c *fiber.Ctx) error {
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"status": "ok",
|
||||||
|
"app": "hostkeeper-v2",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import "github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
func ViewData() fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||||
|
"github.com/gofiber/template/html/v2"
|
||||||
|
|
||||||
|
"git.tukangketik.id/swanadiva/hostkeeper/v2/internal/handler"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
engine := html.New("./views", ".html")
|
||||||
|
engine.Reload(true)
|
||||||
|
engine.AddFunc("svg", func(name string) string {
|
||||||
|
return "" // TODO: inline SVG icons
|
||||||
|
})
|
||||||
|
|
||||||
|
app := fiber.New(fiber.Config{
|
||||||
|
Views: engine,
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Use(logger.New())
|
||||||
|
app.Use(recover.New())
|
||||||
|
app.Use(compress.New())
|
||||||
|
app.Static("/static", "./static")
|
||||||
|
|
||||||
|
app.Get("/", handler.Dashboard)
|
||||||
|
app.Get("/hosts", handler.Dashboard)
|
||||||
|
app.Get("/api/health", handler.Health)
|
||||||
|
|
||||||
|
log.Fatal(app.Listen(":1947"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/cli": "^4.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
/* Lumina System — Custom CSS */
|
||||||
|
|
||||||
|
/* Dot grid background */
|
||||||
|
.dot-grid {
|
||||||
|
background-image: radial-gradient(#e2e8f0 1.5px, transparent 1.5px);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glass panels */
|
||||||
|
.glass-panel {
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-sidebar {
|
||||||
|
background: rgba(255, 255, 255, 0.75);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fade-in {
|
||||||
|
from { opacity: 0; transform: translateY(8px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scale-up {
|
||||||
|
from { opacity: 0; transform: scale(0.95); }
|
||||||
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-in {
|
||||||
|
from { opacity: 0; transform: translateX(20px); }
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cursor-blink {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fade-in 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-scale-up {
|
||||||
|
animation: scale-up 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in {
|
||||||
|
animation: slide-in 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-cursor {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 18px;
|
||||||
|
background: currentColor;
|
||||||
|
animation: cursor-blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #c2c6d8;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a0a4b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal backdrop */
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||||
|
animation: scale-up 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast */
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
background: #191c1e;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 100;
|
||||||
|
animation: slide-in 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle switch */
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
width: 44px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #c2c6d8;
|
||||||
|
transition: background 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch.active {
|
||||||
|
background: #0050cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-knob {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch.active .toggle-knob {
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File table */
|
||||||
|
.file-table-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 80px 120px;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-table-row:hover {
|
||||||
|
background: #f1f4f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Strength badges */
|
||||||
|
.strength-badge-secure {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
background: #006e2f/15;
|
||||||
|
color: #006e2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-badge-moderate {
|
||||||
|
background: #7e23cc/15;
|
||||||
|
color: #7e23cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-badge-weak {
|
||||||
|
background: #b3261e/15;
|
||||||
|
color: #b3261e;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-primary: #0050cb;
|
||||||
|
--color-on-primary: #ffffff;
|
||||||
|
--color-primary-container: #d6e3ff;
|
||||||
|
--color-on-primary-container: #001638;
|
||||||
|
--color-secondary: #006e2f;
|
||||||
|
--color-on-secondary: #ffffff;
|
||||||
|
--color-secondary-container: #6bff8f;
|
||||||
|
--color-on-secondary-container: #00210b;
|
||||||
|
--color-tertiary: #7e23cc;
|
||||||
|
--color-on-tertiary: #ffffff;
|
||||||
|
--color-tertiary-container: #f0daff;
|
||||||
|
--color-on-tertiary-container: #290055;
|
||||||
|
--color-error: #b3261e;
|
||||||
|
--color-on-error: #ffffff;
|
||||||
|
--color-error-container: #f9dedc;
|
||||||
|
--color-on-error-container: #410e0b;
|
||||||
|
--color-surface: #f7f9fb;
|
||||||
|
--color-on-surface: #191c1e;
|
||||||
|
--color-surface-container-low: #f0f2f5;
|
||||||
|
--color-surface-container: #eaecf0;
|
||||||
|
--color-surface-container-high: #e2e5e9;
|
||||||
|
--color-on-surface-variant: #424656;
|
||||||
|
--color-outline: #727686;
|
||||||
|
--color-outline-variant: #c2c6d8;
|
||||||
|
--color-inverse-surface: #2d3133;
|
||||||
|
--color-inverse-on-surface: #f0f1f3;
|
||||||
|
|
||||||
|
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
||||||
|
}
|
||||||
@@ -0,0 +1,848 @@
|
|||||||
|
/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
|
||||||
|
@layer properties;
|
||||||
|
@layer theme, base, components, utilities;
|
||||||
|
@layer theme {
|
||||||
|
:root, :host {
|
||||||
|
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
||||||
|
--color-white: #fff;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
--container-md: 28rem;
|
||||||
|
--container-7xl: 80rem;
|
||||||
|
--text-xs: 0.75rem;
|
||||||
|
--text-xs--line-height: calc(1 / 0.75);
|
||||||
|
--text-sm: 0.875rem;
|
||||||
|
--text-sm--line-height: calc(1.25 / 0.875);
|
||||||
|
--text-lg: 1.125rem;
|
||||||
|
--text-lg--line-height: calc(1.75 / 1.125);
|
||||||
|
--text-2xl: 1.5rem;
|
||||||
|
--text-2xl--line-height: calc(2 / 1.5);
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-semibold: 600;
|
||||||
|
--font-weight-bold: 700;
|
||||||
|
--tracking-wide: 0.025em;
|
||||||
|
--radius-lg: 0.5rem;
|
||||||
|
--radius-xl: 0.75rem;
|
||||||
|
--blur-md: 12px;
|
||||||
|
--default-transition-duration: 150ms;
|
||||||
|
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
--default-font-family: var(--font-sans);
|
||||||
|
--default-mono-font-family: var(--font-mono);
|
||||||
|
--color-primary: #0050cb;
|
||||||
|
--color-secondary: #006e2f;
|
||||||
|
--color-tertiary: #7e23cc;
|
||||||
|
--color-surface: #f7f9fb;
|
||||||
|
--color-on-surface: #191c1e;
|
||||||
|
--color-surface-container-low: #f0f2f5;
|
||||||
|
--color-on-surface-variant: #424656;
|
||||||
|
--color-outline-variant: #c2c6d8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@layer base {
|
||||||
|
*, ::after, ::before, ::backdrop, ::file-selector-button {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0 solid;
|
||||||
|
}
|
||||||
|
html, :host {
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
tab-size: 4;
|
||||||
|
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
|
||||||
|
font-feature-settings: var(--default-font-feature-settings, normal);
|
||||||
|
font-variation-settings: var(--default-font-variation-settings, normal);
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
height: 0;
|
||||||
|
color: inherit;
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
abbr:where([title]) {
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
-webkit-text-decoration: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
b, strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
code, kbd, samp, pre {
|
||||||
|
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
|
||||||
|
font-feature-settings: var(--default-mono-font-feature-settings, normal);
|
||||||
|
font-variation-settings: var(--default-mono-font-variation-settings, normal);
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
sub, sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
text-indent: 0;
|
||||||
|
border-color: inherit;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
:-moz-focusring {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
ol, ul, menu {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
img, svg, video, canvas, audio, iframe, embed, object {
|
||||||
|
display: block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
img, video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
button, input, select, optgroup, textarea, ::file-selector-button {
|
||||||
|
font: inherit;
|
||||||
|
font-feature-settings: inherit;
|
||||||
|
font-variation-settings: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
color: inherit;
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
:where(select:is([multiple], [size])) optgroup {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
:where(select:is([multiple], [size])) optgroup option {
|
||||||
|
padding-inline-start: 20px;
|
||||||
|
}
|
||||||
|
::file-selector-button {
|
||||||
|
margin-inline-end: 4px;
|
||||||
|
}
|
||||||
|
::placeholder {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
|
||||||
|
::placeholder {
|
||||||
|
color: currentcolor;
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
color: color-mix(in oklab, currentcolor 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
::-webkit-date-and-time-value {
|
||||||
|
min-height: 1lh;
|
||||||
|
text-align: inherit;
|
||||||
|
}
|
||||||
|
::-webkit-datetime-edit {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
::-webkit-datetime-edit-fields-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
|
||||||
|
padding-block: 0;
|
||||||
|
}
|
||||||
|
::-webkit-calendar-picker-indicator {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
:-moz-ui-invalid {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
|
||||||
|
appearance: button;
|
||||||
|
}
|
||||||
|
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
[hidden]:where(:not([hidden="until-found"])) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@layer utilities {
|
||||||
|
.collapse {
|
||||||
|
visibility: collapse;
|
||||||
|
}
|
||||||
|
.visible {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
.relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.mx-auto {
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
.mb-1 {
|
||||||
|
margin-bottom: var(--spacing);
|
||||||
|
}
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.mb-6 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.inline {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.h-4 {
|
||||||
|
height: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.h-5 {
|
||||||
|
height: calc(var(--spacing) * 5);
|
||||||
|
}
|
||||||
|
.h-8 {
|
||||||
|
height: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
|
.h-14 {
|
||||||
|
height: calc(var(--spacing) * 14);
|
||||||
|
}
|
||||||
|
.min-h-screen {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.w-4 {
|
||||||
|
width: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.w-5 {
|
||||||
|
width: calc(var(--spacing) * 5);
|
||||||
|
}
|
||||||
|
.w-8 {
|
||||||
|
width: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
|
.w-\[260px\] {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.max-w-7xl {
|
||||||
|
max-width: var(--container-7xl);
|
||||||
|
}
|
||||||
|
.max-w-md {
|
||||||
|
max-width: var(--container-md);
|
||||||
|
}
|
||||||
|
.min-w-0 {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.flex-1 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.shrink-0 {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.transform {
|
||||||
|
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
||||||
|
}
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.resize {
|
||||||
|
resize: both;
|
||||||
|
}
|
||||||
|
.grid-cols-1 {
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
.grid-cols-3 {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.gap-2 {
|
||||||
|
gap: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.gap-3 {
|
||||||
|
gap: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
|
.gap-4 {
|
||||||
|
gap: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.space-y-0\.5 {
|
||||||
|
:where(& > :not(:last-child)) {
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
margin-block-start: calc(calc(var(--spacing) * 0.5) * var(--tw-space-y-reverse));
|
||||||
|
margin-block-end: calc(calc(var(--spacing) * 0.5) * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.overflow-auto {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.overflow-hidden {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.rounded {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
.rounded-full {
|
||||||
|
border-radius: calc(infinity * 1px);
|
||||||
|
}
|
||||||
|
.rounded-lg {
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
}
|
||||||
|
.rounded-xl {
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
}
|
||||||
|
.border {
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
.border-2 {
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
.border-t {
|
||||||
|
border-top-style: var(--tw-border-style);
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
.border-r {
|
||||||
|
border-right-style: var(--tw-border-style);
|
||||||
|
border-right-width: 1px;
|
||||||
|
}
|
||||||
|
.border-b {
|
||||||
|
border-bottom-style: var(--tw-border-style);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
.border-dashed {
|
||||||
|
--tw-border-style: dashed;
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
.border-outline-variant\/20 {
|
||||||
|
border-color: color-mix(in srgb, #c2c6d8 20%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
border-color: color-mix(in oklab, var(--color-outline-variant) 20%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.border-outline-variant\/30 {
|
||||||
|
border-color: color-mix(in srgb, #c2c6d8 30%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
border-color: color-mix(in oklab, var(--color-outline-variant) 30%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.border-outline-variant\/50 {
|
||||||
|
border-color: color-mix(in srgb, #c2c6d8 50%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
border-color: color-mix(in oklab, var(--color-outline-variant) 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bg-primary {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
.bg-primary\/10 {
|
||||||
|
background-color: color-mix(in srgb, #0050cb 10%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--color-primary) 10%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bg-primary\/20 {
|
||||||
|
background-color: color-mix(in srgb, #0050cb 20%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--color-primary) 20%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bg-secondary\/20 {
|
||||||
|
background-color: color-mix(in srgb, #006e2f 20%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--color-secondary) 20%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bg-surface {
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
}
|
||||||
|
.bg-tertiary\/20 {
|
||||||
|
background-color: color-mix(in srgb, #7e23cc 20%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--color-tertiary) 20%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bg-white {
|
||||||
|
background-color: var(--color-white);
|
||||||
|
}
|
||||||
|
.bg-white\/70 {
|
||||||
|
background-color: color-mix(in srgb, #fff 70%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
background-color: color-mix(in oklab, var(--color-white) 70%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.p-0\.5 {
|
||||||
|
padding: calc(var(--spacing) * 0.5);
|
||||||
|
}
|
||||||
|
.p-1\.5 {
|
||||||
|
padding: calc(var(--spacing) * 1.5);
|
||||||
|
}
|
||||||
|
.p-4 {
|
||||||
|
padding: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.p-6 {
|
||||||
|
padding: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
|
.p-8 {
|
||||||
|
padding: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
|
.px-3 {
|
||||||
|
padding-inline: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
|
.px-6 {
|
||||||
|
padding-inline: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
|
.py-2 {
|
||||||
|
padding-block: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.py-4 {
|
||||||
|
padding-block: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.pr-3 {
|
||||||
|
padding-right: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
|
.pl-9 {
|
||||||
|
padding-left: calc(var(--spacing) * 9);
|
||||||
|
}
|
||||||
|
.text-2xl {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
||||||
|
}
|
||||||
|
.text-lg {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
line-height: var(--tw-leading, var(--text-lg--line-height));
|
||||||
|
}
|
||||||
|
.text-sm {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||||
|
}
|
||||||
|
.text-xs {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
line-height: var(--tw-leading, var(--text-xs--line-height));
|
||||||
|
}
|
||||||
|
.font-bold {
|
||||||
|
--tw-font-weight: var(--font-weight-bold);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
.font-medium {
|
||||||
|
--tw-font-weight: var(--font-weight-medium);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
.font-semibold {
|
||||||
|
--tw-font-weight: var(--font-weight-semibold);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
.tracking-wide {
|
||||||
|
--tw-tracking: var(--tracking-wide);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
}
|
||||||
|
.text-on-surface {
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
}
|
||||||
|
.text-on-surface-variant {
|
||||||
|
color: var(--color-on-surface-variant);
|
||||||
|
}
|
||||||
|
.text-on-surface-variant\/50 {
|
||||||
|
color: color-mix(in srgb, #424656 50%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
color: color-mix(in oklab, var(--color-on-surface-variant) 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.text-primary {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
.text-secondary {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
.text-tertiary {
|
||||||
|
color: var(--color-tertiary);
|
||||||
|
}
|
||||||
|
.text-white {
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
.uppercase {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.antialiased {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
.placeholder-on-surface-variant {
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--color-on-surface-variant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filter {
|
||||||
|
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||||
|
}
|
||||||
|
.backdrop-blur-md {
|
||||||
|
--tw-backdrop-blur: blur(var(--blur-md));
|
||||||
|
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||||
|
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||||
|
}
|
||||||
|
.transition {
|
||||||
|
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||||
|
}
|
||||||
|
.transition-colors {
|
||||||
|
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||||
|
}
|
||||||
|
.hover\:border-primary\/30 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
border-color: color-mix(in srgb, #0050cb 30%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
border-color: color-mix(in oklab, var(--color-primary) 30%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hover\:bg-surface-container-low {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--color-surface-container-low);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hover\:text-on-surface {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hover\:text-primary\/50 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
color: color-mix(in srgb, #0050cb 50%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
color: color-mix(in oklab, var(--color-primary) 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus\:border-primary\/50 {
|
||||||
|
&:focus {
|
||||||
|
border-color: color-mix(in srgb, #0050cb 50%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
border-color: color-mix(in oklab, var(--color-primary) 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus\:ring-2 {
|
||||||
|
&:focus {
|
||||||
|
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||||
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus\:ring-primary\/30 {
|
||||||
|
&:focus {
|
||||||
|
--tw-ring-color: color-mix(in srgb, #0050cb 30%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
--tw-ring-color: color-mix(in oklab, var(--color-primary) 30%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus\:outline-none {
|
||||||
|
&:focus {
|
||||||
|
--tw-outline-style: none;
|
||||||
|
outline-style: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sm\:grid-cols-2 {
|
||||||
|
@media (width >= 40rem) {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.lg\:grid-cols-3 {
|
||||||
|
@media (width >= 64rem) {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.xl\:grid-cols-4 {
|
||||||
|
@media (width >= 80rem) {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@property --tw-rotate-x {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-rotate-y {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-rotate-z {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-skew-x {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-skew-y {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-space-y-reverse {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-border-style {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: solid;
|
||||||
|
}
|
||||||
|
@property --tw-font-weight {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-tracking {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-blur {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-brightness {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-contrast {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-grayscale {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-hue-rotate {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-invert {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-opacity {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-saturate {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-sepia {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-drop-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-drop-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-drop-shadow-alpha {
|
||||||
|
syntax: "<percentage>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 100%;
|
||||||
|
}
|
||||||
|
@property --tw-drop-shadow-size {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-blur {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-brightness {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-contrast {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-grayscale {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-hue-rotate {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-invert {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-opacity {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-saturate {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-sepia {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-shadow-alpha {
|
||||||
|
syntax: "<percentage>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 100%;
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow-alpha {
|
||||||
|
syntax: "<percentage>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 100%;
|
||||||
|
}
|
||||||
|
@property --tw-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-ring-inset {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-width {
|
||||||
|
syntax: "<length>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0px;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: #fff;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@layer properties {
|
||||||
|
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||||
|
*, ::before, ::after, ::backdrop {
|
||||||
|
--tw-rotate-x: initial;
|
||||||
|
--tw-rotate-y: initial;
|
||||||
|
--tw-rotate-z: initial;
|
||||||
|
--tw-skew-x: initial;
|
||||||
|
--tw-skew-y: initial;
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
--tw-border-style: solid;
|
||||||
|
--tw-font-weight: initial;
|
||||||
|
--tw-tracking: initial;
|
||||||
|
--tw-blur: initial;
|
||||||
|
--tw-brightness: initial;
|
||||||
|
--tw-contrast: initial;
|
||||||
|
--tw-grayscale: initial;
|
||||||
|
--tw-hue-rotate: initial;
|
||||||
|
--tw-invert: initial;
|
||||||
|
--tw-opacity: initial;
|
||||||
|
--tw-saturate: initial;
|
||||||
|
--tw-sepia: initial;
|
||||||
|
--tw-drop-shadow: initial;
|
||||||
|
--tw-drop-shadow-color: initial;
|
||||||
|
--tw-drop-shadow-alpha: 100%;
|
||||||
|
--tw-drop-shadow-size: initial;
|
||||||
|
--tw-backdrop-blur: initial;
|
||||||
|
--tw-backdrop-brightness: initial;
|
||||||
|
--tw-backdrop-contrast: initial;
|
||||||
|
--tw-backdrop-grayscale: initial;
|
||||||
|
--tw-backdrop-hue-rotate: initial;
|
||||||
|
--tw-backdrop-invert: initial;
|
||||||
|
--tw-backdrop-opacity: initial;
|
||||||
|
--tw-backdrop-saturate: initial;
|
||||||
|
--tw-backdrop-sepia: initial;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-color: initial;
|
||||||
|
--tw-shadow-alpha: 100%;
|
||||||
|
--tw-inset-shadow: 0 0 #0000;
|
||||||
|
--tw-inset-shadow-color: initial;
|
||||||
|
--tw-inset-shadow-alpha: 100%;
|
||||||
|
--tw-ring-color: initial;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-inset-ring-color: initial;
|
||||||
|
--tw-inset-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-inset: initial;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+5
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,68 @@
|
|||||||
|
{{template "layout" .}}
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
|
<div id="dashboard-page" class="max-w-7xl mx-auto">
|
||||||
|
<div class="grid grid-cols-3 gap-4 mb-6">
|
||||||
|
<div class="rounded-xl bg-white border border-outline-variant/30 p-4">
|
||||||
|
<div class="flex items-center gap-2 text-primary mb-1">
|
||||||
|
<span class="w-5 h-5 bg-primary/20 rounded flex items-center justify-center text-xs font-bold">⚡</span>
|
||||||
|
<span class="text-xs font-medium text-on-surface-variant uppercase tracking-wide">Active Connections</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-2xl font-bold text-on-surface">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl bg-white border border-outline-variant/30 p-4">
|
||||||
|
<div class="flex items-center gap-2 text-secondary mb-1">
|
||||||
|
<span class="w-5 h-5 bg-secondary/20 rounded flex items-center justify-center text-xs font-bold">✓</span>
|
||||||
|
<span class="text-xs font-medium text-on-surface-variant uppercase tracking-wide">Transfers Today</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-2xl font-bold text-on-surface">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl bg-white border border-outline-variant/30 p-4">
|
||||||
|
<div class="flex items-center gap-2 text-tertiary mb-1">
|
||||||
|
<span class="w-5 h-5 bg-tertiary/20 rounded flex items-center justify-center text-xs font-bold">#</span>
|
||||||
|
<span class="text-xs font-medium text-on-surface-variant uppercase tracking-wide">Saved Hosts</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-2xl font-bold text-on-surface">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3 mb-6" x-data="{ view: 'grid' }">
|
||||||
|
<div class="relative flex-1 max-w-md">
|
||||||
|
<input type="search" name="q" placeholder="Search hosts..."
|
||||||
|
hx-get="/hosts?q=..." hx-trigger="keyup delay:200ms"
|
||||||
|
hx-target="#host-list"
|
||||||
|
class="w-full pl-9 pr-3 py-2 rounded-lg border border-outline-variant/50
|
||||||
|
bg-white text-sm text-on-surface placeholder-on-surface-variant
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50">
|
||||||
|
</div>
|
||||||
|
<select name="filter"
|
||||||
|
hx-get="/hosts?filter={value}" hx-target="#host-list"
|
||||||
|
class="px-3 py-2 rounded-lg border border-outline-variant/50 bg-white text-sm text-on-surface
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-primary/30">
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="offline">Offline</option>
|
||||||
|
</select>
|
||||||
|
<div class="flex items-center bg-white rounded-lg border border-outline-variant/50 p-0.5">
|
||||||
|
<button @click="view = 'grid'"
|
||||||
|
:class="view === 'grid' ? 'bg-primary/10 text-primary' : 'text-on-surface-variant hover:text-on-surface'"
|
||||||
|
class="p-1.5 rounded transition-colors">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button @click="view = 'list'"
|
||||||
|
:class="view === 'list' ? 'bg-primary/10 text-primary' : 'text-on-surface-variant hover:text-on-surface'"
|
||||||
|
class="p-1.5 rounded transition-colors">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="host-list" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||||
|
<div class="rounded-xl border-2 border-dashed border-outline-variant/30 p-8 flex flex-col items-center justify-center text-on-surface-variant/50 hover:border-primary/30 hover:text-primary/50 transition-colors cursor-pointer"
|
||||||
|
@click="console.log('add host')">
|
||||||
|
<svg class="w-8 h-8 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||||
|
<span class="text-sm font-medium">Add New Host</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Hostkeeper V2</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="/static/css/output.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/custom.css">
|
||||||
|
<script src="/static/js/htmx.min.js"></script>
|
||||||
|
<script src="/static/js/alpine.min.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-surface text-on-surface antialiased dot-grid min-h-screen">
|
||||||
|
<div class="flex min-h-screen">
|
||||||
|
{{template "nav" .}}
|
||||||
|
<div class="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<header class="h-14 border-b border-outline-variant/30 flex items-center px-6 bg-white/70 backdrop-blur-md">
|
||||||
|
<h1 class="text-lg font-semibold text-on-surface">{{.Title}}</h1>
|
||||||
|
</header>
|
||||||
|
<main id="main-content" class="flex-1 overflow-auto p-6">
|
||||||
|
{{block "content" .}}{{end}}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{{define "nav"}}
|
||||||
|
<nav class="w-[260px] min-h-screen bg-white/70 backdrop-blur-md border-r border-outline-variant/30 flex flex-col shrink-0">
|
||||||
|
<div class="p-4 border-b border-outline-variant/20">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 rounded-lg bg-primary flex items-center justify-center text-white text-sm font-bold">K</div>
|
||||||
|
<span class="font-bold text-lg text-on-surface">Hostkeeper</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 px-3 py-4 space-y-0.5">
|
||||||
|
{{range .NavItems}}
|
||||||
|
<a href="/{{.ID}}"
|
||||||
|
hx-get="/{{.ID}}" hx-target="#main-content" hx-push-url="true"
|
||||||
|
class="flex items-center gap-3 px-3 py-2 rounded-lg transition-colors text-sm
|
||||||
|
{{if eq $.View .ID}}bg-primary/10 text-primary font-medium
|
||||||
|
{{else}}text-on-surface-variant hover:bg-surface-container-low hover:text-on-surface{{end}}">
|
||||||
|
<span class="w-5 h-5 flex items-center justify-center">{{svg .Icon}}</span>
|
||||||
|
<span>{{.Label}}</span>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="p-4 border-t border-outline-variant/20">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-tertiary/20 flex items-center justify-center text-tertiary text-sm font-semibold">U</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-medium text-on-surface truncate">User</p>
|
||||||
|
<p class="text-xs text-on-surface-variant truncate">Local</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{{end}}
|
||||||
Reference in New Issue
Block a user