From a773d4314111d5b11e9ba27249a14a996c301c18 Mon Sep 17 00:00:00 2001 From: Indra Gunawan Date: Thu, 15 Oct 2026 19:39:09 +0700 Subject: [PATCH] feat: form fields, select wrapper, appearance settings, no-auth sign out, inline styles removed Co-Authored-By: Claude Sonnet 5.5 --- internal/http/router.go | 1 + internal/http/static/app.css | 188 +++++++------------- internal/http/templates.go | 4 + internal/http/templates/about.html | 29 ++-- internal/http/templates/apps.html | 2 +- internal/http/templates/apps_add.html | 198 +++++++++++----------- internal/http/templates/error.html | 6 +- internal/http/templates/export.html | 48 ++---- internal/http/templates/import.html | 41 ++--- internal/http/templates/layout.html | 3 +- internal/http/templates/login.html | 8 +- internal/http/templates/servers_add.html | 37 ++-- internal/http/templates/servers_edit.html | 42 ++--- internal/http/templates/settings.html | 92 +++++++--- internal/http/templates/ui_icon.html | 2 +- 15 files changed, 327 insertions(+), 374 deletions(-) diff --git a/internal/http/router.go b/internal/http/router.go index 00b4c54..18c5497 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -18,6 +18,7 @@ import ( func NewRouter(svc *server.Service, appSvc *app.Service, render RenderFunc, serverListAdapter app.ServerRepo, cfgUser, cfgPass, apiToken, sshKeyDir string, webhookSecretFn func() string, settingsHandler *settings.Handler, backupHandler *backup.Handler, version string, devMock, trustProxy bool) *chi.Mux { assetVersion = version + noAuth = cfgPass == "" r := chi.NewRouter() r.Use(chimw.Logger) diff --git a/internal/http/static/app.css b/internal/http/static/app.css index 7217699..689eb1e 100644 --- a/internal/http/static/app.css +++ b/internal/http/static/app.css @@ -329,7 +329,6 @@ body:not(.login-page) { padding-bottom: calc(56px + env(safe-area-inset-bottom)) /* Grid System */ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 1em; } form .grid { gap: 1.2em; margin-bottom: 0.9em; } -.grid-fill > .card { display: flex; flex-direction: column; } .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1em; } /* Buttons */ @@ -528,18 +527,6 @@ progress.bar-danger::-moz-progress-bar { background: var(--danger); } .rows td[data-label]::before { content: attr(data-label); display: block; font-size: 11px; font-weight: 800; text-transform: uppercase; color: var(--muted); } .col-sm-hide { display: none !important; } } - -/* Tables */ -.table-wrap { - width: 100%; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - border: 1px solid var(--line); - border-radius: 8px; - background: var(--surface); - box-shadow: var(--shadow); - margin-bottom: 1em; -} table { width: 100%; border-collapse: collapse; text-align: left; } thead th { padding: 0.65em 0.85em; @@ -557,39 +544,54 @@ tbody tr:hover td { background: var(--accent-soft); } table a { font-weight: 500; color: var(--accent); } table a:hover { color: var(--ink); text-decoration: underline; } -/* Form elements */ +/* Form elements (DESIGN-SYSTEM §4, §5): 16px controls, 44px high, radius 12, border `control` */ input, textarea, select { font-family: inherit; - font-size: 0.92em; - padding: 0.55em 0.8em; + font-size: 16px; + min-height: 44px; + padding: 0 12px; border-radius: 12px; - border: 1px solid var(--line); - background: var(--bg); + border: 1px solid var(--control); + background: var(--surface); color: var(--ink); width: 100%; outline: none; - transition: border-color 0.15s ease, box-shadow 0.15s ease; -} -input:focus, textarea:focus, select:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-soft); -} -input[type=radio], input[type=checkbox] { width: auto; cursor: pointer; } -input[type=radio] { margin: 0 0.6em 0 0; vertical-align: middle; } -input[type=checkbox] { margin: 0 0.5em 0 0; vertical-align: middle; } -textarea { resize: vertical; min-height: 110px; line-height: 1.5; } -label { font-size: 0.85em; font-weight: 500; color: var(--ink2); margin-bottom: 0.5em; display: block; } -fieldset { border: none; padding: 0; margin: 0 0 1.5em; } -legend { - font-size: 0.8em; - color: var(--ink2); - margin-bottom: 0.5em; - text-transform: uppercase; - letter-spacing: 0.04em; - font-weight: 600; -} -small { font-size: 0.82em; color: var(--muted); display: block; margin-top: 0.35em; } -.grid label { margin-bottom: 0; } + transition: border-color 0.15s ease; +} +textarea { padding: 10px 12px; resize: vertical; min-height: 96px; line-height: 1.5; } +textarea.mono, input.mono { font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 13px; } +input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 3px solid var(--accent); outline-offset: 1px; border-color: var(--accent); } +input[type=radio], input[type=checkbox] { width: 20px; height: 20px; min-height: 0; padding: 0; accent-color: var(--accent-fill); cursor: pointer; flex-shrink: 0; } +input[type=file] { padding: 8px 12px; } +input[readonly] { background: var(--sunk); } +.field, label { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; font-size: 15px; color: var(--ink); } +.field-label { font-size: 12.8px; font-weight: 600; color: var(--ink2); } +.field-hint, small { font-size: 12px; color: var(--muted); display: block; margin-top: 0; } +.field-error { font-size: 12.8px; color: var(--danger-text); } +.field-error[hidden] { display: none; } +.check { flex-direction: row; align-items: flex-start; gap: 8px; cursor: pointer; font-size: 15px; } +.select-wrap { position: relative; display: block; } +.select-wrap select { appearance: none; -webkit-appearance: none; padding-right: 40px; } +.select-wrap .icon { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: var(--muted); pointer-events: none; } +fieldset { border: none; padding: 0; margin: 0 0 16px; } +legend { font-size: 12.8px; color: var(--ink2); margin-bottom: 8px; font-weight: 600; } +details { margin-bottom: 12px; } +summary { cursor: pointer; font-size: 13px; font-weight: 600; color: var(--accent); padding: 8px 0; } +.card-sub { font-size: 12.8px; color: var(--muted); margin: -8px 0 12px; } +.form-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; } +@media (min-width: 700px) { .form-actions { display: flex; } } +.row-grid { display: grid; gap: 8px; margin-bottom: 8px; align-items: start; } +.row-grid > * { min-width: 0; } +.row-domain { grid-template-columns: minmax(0, 1fr) 44px; } +.row-env { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) 40px 40px; } +.row-file { grid-template-columns: minmax(0, 1fr) auto 40px; } +.row-grid input, .row-grid textarea { margin: 0; } +.row-env textarea { min-height: 44px; } +.inline-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; } +.opt-group { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; } +.opt-group legend { margin-bottom: 4px; } +.secret-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } +.secret-row input { flex: 1 1 200px; min-width: 0; font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 13px; } .kpi-grid { grid-template-columns: repeat(2, 1fr); } @media (min-width: 700px) { .kpi-grid { grid-template-columns: repeat(4, 1fr); } } @@ -640,11 +642,6 @@ small { font-size: 0.82em; color: var(--muted); display: block; margin-top: 0.35 .breadcrumb { display: flex; align-items: center; gap: 0.4em; font-size: 0.88em; color: var(--ink2); margin-bottom: 1em; } .breadcrumb a { color: var(--ink2); font-weight: 500; } .breadcrumb a:hover { color: var(--accent); } -.breadcrumb span.sep { color: var(--muted); font-size: 0.8em; } - -/* Button Group Layout */ -.btn-group { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.6em; margin: 1.2em 0; } -.btn-group-left, .btn-group-right { display: flex; gap: 0.5em; align-items: center; flex-wrap: wrap; } /* Code blocks & Log viewer */ pre, code { font-size: 0.88em; } @@ -657,9 +654,6 @@ pre { line-height: 1.5; box-shadow: var(--shadow); } - -.log-container { margin: 1.2em 0; } -.log-toolbar { display: flex; gap: 0.4em; margin-bottom: 0.6em; } .log-content { background: var(--term-bg); border: 1px solid var(--line); @@ -683,7 +677,6 @@ pre { gap: 0.6em; } .secret-row code { flex: 1; font-size: 0.85em; } -.secret-row .secret-val { color: var(--ink2); font-size: 0.85em; } .secret-row input { width: auto; min-width: 160px; padding: 0.4em 0.6em; font-size: 0.85em; } /* Empty state */ @@ -745,11 +738,6 @@ pre { border-bottom: 1px solid var(--line); background: var(--term-bar); } -.console-dots { display: flex; gap: 0.35rem; } -.console-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; } -.console-dot-red { background: var(--danger); } -.console-dot-yellow { background: var(--warn); } -.console-dot-green { background: var(--ok); } #terminal { padding: 0.4em; } /* Gauges & Live Charts */ @@ -757,18 +745,9 @@ pre { .stats-header h3 { margin: 0; } .stats-gauges { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.8em 1.5em; margin-bottom: 0.85em; } -.stats-gauges--resources { grid-template-columns: 0.7fr 1.65fr 0.65fr 1.1fr 1.1fr; } .stat-gauge { min-width: 0; } .stat-gauge-label { font-size: 0.7em; color: var(--ink2); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; } -.stat-gauge-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 0.2em; } -.stat-gauge-row { display: flex; align-items: center; gap: 0.4em; } -.stat-gauge-bar { width: 100%; height: 7px; background: var(--line); border-radius: 4px; overflow: hidden; } -.stat-gauge-fill { height: 100%; border-radius: 4px; transition: width 0.5s ease; } -.stat-gauge-cpu { background: var(--ink2); } -.stat-gauge-mem { background: var(--ok); } -.stat-gauge-disk { background: var(--accent); } .stat-gauge-value { font-size: 0.85em; font-weight: 600; font-family: 'JetBrains Mono', monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.stat-gauge-detail { font-size: 0.8em; color: var(--ink2); font-weight: 400; } .stat-gauge-net { font-size: 0.85em; font-weight: 400; color: var(--ink); line-height: 1.4; word-break: break-word; font-family: 'JetBrains Mono', monospace; } .stats-divider { border: none; border-top: 1px solid var(--line); margin: 1.2em 0 1em 0; } @@ -800,70 +779,17 @@ pre { .grid:not(.kpi-grid) { grid-template-columns: 1fr; } .grid-2 { grid-template-columns: 1fr; } .stats-gauges { grid-template-columns: 1fr 1fr; } - .btn-group { flex-direction: column; align-items: stretch; } - .btn-group-left, .btn-group-right { width: 100%; } - .btn-group-left > *, .btn-group-right > * { flex: 1 1 auto; } } /* Login page (body.login-page) */ -.login-page { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - padding: 1em; -} -.login-card { - background: var(--surface); - border: 1px solid var(--line); - border-radius: 12px; - padding: 2.2em 2em; - width: 360px; - max-width: 100%; - box-shadow: var(--shadow); -} -.brand-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.4rem; } -.brand-icon { - width: 30px; - height: 30px; - border-radius: 8px; - background: linear-gradient(135deg, var(--accent-fill), var(--chart)); - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--on-accent); - font-size: 0.95em; - font-weight: 800; - box-shadow: var(--shadow); -} -.login-card h1 { font-size: 1.35em; font-weight: 700; margin: 0; letter-spacing: -0.02em; } -.login-card p { color: var(--ink2); margin: 0.2em 0 1.5em; font-size: 0.9em; } -.login-card .error { - background: var(--danger-bg); - color: var(--danger-text); - border: 1px solid color-mix(in srgb, var(--danger) 25%, transparent); - border-radius: 6px; - padding: 0.6em 0.8em; - font-size: 0.85em; - font-weight: 500; - margin-bottom: 1.2em; -} -.login-card label { font-size: 0.85em; font-weight: 500; color: var(--ink2); display: block; margin-bottom: 0.4em; } -.login-card input { - font-family: inherit; - font-size: 0.92em; - padding: 0.6em 0.85em; - border-radius: 6px; - border: 1px solid var(--control); - background: var(--bg); - color: var(--ink); - width: 100%; - outline: none; - margin-bottom: 1.2em; - transition: border-color 0.15s ease, box-shadow 0.15s ease; -} -.login-card input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } -.login-card .btn-primary { width: 100%; text-align: center; justify-content: center; } +.login-page { display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 16px; } +.login-card { background: var(--surface); border: 1px solid var(--line); border-radius: 16px; padding: 28px 24px; width: 360px; max-width: 100%; box-shadow: var(--shadow); } +.brand-header { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; } +.brand-icon { width: 32px; height: 32px; border-radius: 8px; background: linear-gradient(135deg, var(--accent-fill), var(--chart)); display: inline-flex; align-items: center; justify-content: center; color: var(--on-accent); font-weight: 800; } +.login-card h1 { font-size: 20px; font-weight: 800; margin: 0; } +.login-card p { color: var(--ink2); margin: 0 0 20px; font-size: 14px; } +.login-card .alert { margin-bottom: 16px; } +.login-card .btn-primary { width: 100%; } .login-page .theme-btn { position: fixed; top: 12px; right: 12px; } /* Detail pages (DESIGN-SYSTEM §6) */ @@ -955,3 +881,17 @@ pre.code { margin: 0; padding: 12px; background: var(--sunk); border: 1px solid .rows-apps td.c-name { grid-column: 1; } .rows-apps td.c-act { grid-column: 1 / -1; } } + +/* Forms: segmented radios, page actions */ +.seg-form { margin-bottom: 16px; } +.seg-radio { position: relative; display: block; margin: 0; flex-direction: row; } +.seg-radio input { position: absolute; opacity: 0; inset: 0; width: 100%; height: 100%; min-height: 0; cursor: pointer; } +.seg-radio span { display: flex; align-items: center; min-height: 32px; padding: 0 16px; border-radius: 10px; font-size: 12.8px; font-weight: 600; color: var(--ink2); } +.seg-radio input:checked + span { background: var(--surface); color: var(--ink); box-shadow: var(--shadow); } +.seg-radio input:focus-visible + span { outline: 3px solid var(--accent); outline-offset: 1px; } +@media (max-width: 699px) { .seg-radio span { min-height: 44px; } } +.group-title { font-size: 12.8px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink2); margin: 16px 0 4px; } +.page-actions { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 16px; } +/* iOS zooms inputs below 16px: keep mono fields at 16px on phones */ +@media (max-width: 699px) { input.mono, textarea.mono, .secret-row input { font-size: 16px; } } +.row-domain > input:only-child { grid-column: 1 / -1; } diff --git a/internal/http/templates.go b/internal/http/templates.go index e487a4a..48d4a62 100644 --- a/internal/http/templates.go +++ b/internal/http/templates.go @@ -246,6 +246,9 @@ func GetDevMock(r *http.Request) bool { // assetVersion busts the browser cache of /static/app.css on upgrade; set by NewRouter. var assetVersion string +// noAuth hides the Sign out button when no admin password is configured; set by NewRouter. +var noAuth bool + // navSection maps the request path to the active main-nav item. func navSection(r *http.Request) string { p := "/" + strings.TrimPrefix(strings.TrimPrefix(r.URL.Path, strings.TrimSuffix(web.GetBasePath(r), "/")), "/") @@ -275,6 +278,7 @@ func Render(w http.ResponseWriter, r *http.Request, status int, name string, dat m["DevMock"] = GetDevMock(r) m["AssetVersion"] = assetVersion m["Nav"] = navSection(r) + m["NoAuth"] = noAuth } var buf bytes.Buffer diff --git a/internal/http/templates/about.html b/internal/http/templates/about.html index bd8a17f..9ed7898 100644 --- a/internal/http/templates/about.html +++ b/internal/http/templates/about.html @@ -19,26 +19,25 @@

What is Dockify?

Built with Go, HTMX, SQLite, and a fully custom CSS interface — no JavaScript frameworks, no build steps.

-
+

Update

-

- Current: v{{.Version}}
- Latest: - -

-
- - +
+
Current
v{{.Version}}
+
Latest
-
+
+
+ +
- +
-
+

Sponsor & Donation

Dockify is open source and free to use. If you find it useful, consider supporting the project via GitHub Sponsors.

Sponsor on GitHub
-

Version {{.Version}}

{{template "ui_confirm" (dict "ID" "update-confirm" "Title" "Update Dockify now?" "Body" "The server restarts to apply the update and is unavailable for a short time." "Label" "Update")}}