diff --git a/CHANGELOG.md b/CHANGELOG.md index 175a8a5..42d7ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,20 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version still pre-2.0 and may only shrink; CI also rejects a commit that changes a guard together with the implementation, or brings back a template from history. The kit and route tests stay skipped until their stages land. +- panel-redesign (stage 0): the component kit. `panel.css` is the mockups' + stylesheet rule for rule; `components.html` holds every partial with its + typed input in `components.go`; the new `layout.html` is the shell (brick + navbar with the wordmark, perforated edge, sibling strip, user menu, + footer) and the signed-out screen; `GET /server/components` (global role) + renders every partial in every state. `@kit` is off the ratchet: the kit + is held to the design contract from here on. ### Changed +- The old panel is untouched on screen but steps aside in the tree: its + stylesheet is `legacy.css` and its layout `layout_legacy.html` until the + last old page is restyled. The 2.0 menu paths in the new shell do not + exist before stage 1; only the kit page uses that shell so far. - panel-redesign is `agreed` (owner, 2026-10-09); work starts with stage 0, the component kit. diff --git a/docs/assets/panel-redesign/evidence/components.png b/docs/assets/panel-redesign/evidence/components.png new file mode 100644 index 0000000..67cf2c4 Binary files /dev/null and b/docs/assets/panel-redesign/evidence/components.png differ diff --git a/docs/plans/panel-redesign.md b/docs/plans/panel-redesign.md index 4675cc3..c8d005d 100644 --- a/docs/plans/panel-redesign.md +++ b/docs/plans/panel-redesign.md @@ -499,9 +499,9 @@ only when both are done. Every stage-2 step ends with the evidence of - [x] Vendor Bulma 1.0.4 and the icon subset (MIT) under `/static`, NOTICE and development.md rows, pinned checksum — **Haiku** - [x] Guard tests from § Enforcement and `TestRoutesFollowNavigation`, red against today's panel where expected, with the `legacyPages` ratchet listing every page; `design-first` CI step including the ratchet and the history-blob check (§ Against resurrected pages) — **Opus** -- [ ] `panel.css` from `panel/theme.css` + `shared/brand.css`; `components.html` partials with typed inputs in `components.go`; `status_tag`, `copy_field`, `wbr_at` — **Sonnet** -- [ ] `layout.html`: navbar, perforated edge, sibling strip, user menu, footer, visibility flags; the signed-out shell — **Sonnet** -- [ ] `GET /server/components` behind the global role rendering every partial from fixtures; kit acceptance (§ Component kit) with evidence — **Sonnet**, reviewed by **Opus** +- [x] `panel.css` from `panel/theme.css` + `shared/brand.css`; `components.html` partials with typed inputs in `components.go`; `status_tag`, `copy_field`, `wbr_at` — **Sonnet** +- [x] `layout.html`: navbar, perforated edge, sibling strip, user menu, footer, visibility flags; the signed-out shell — **Sonnet** +- [x] `GET /server/components` behind the global role rendering every partial from fixtures; kit acceptance (§ Component kit) with evidence — **Sonnet**, reviewed by **Opus** **Stage 1 — data and routes** (no template work; parallel with stage 0) diff --git a/docs/roadmap.md b/docs/roadmap.md index e92eed6..050cfb9 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -39,7 +39,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md). | panel-notifications | E-mail notifications about important events | candidate | — | — | | delivery-log-storage | Keep a message's `mail.log` lines with its send-log row | candidate | — | — | | password-reset | Password reset by e-mail | candidate | — | — | -| panel-redesign | Panel redesign (accepted mockups, design contract) | agreed | 7/27 | [plans/panel-redesign.md](plans/panel-redesign.md) | +| panel-redesign | Panel redesign (accepted mockups, design contract) | agreed | 10/27 | [plans/panel-redesign.md](plans/panel-redesign.md) | | schema-squash | Squash SQLite migrations into a 2.x baseline | **2.x** | — | — | **Recommended order** (not binding; owner, 2026-10-09: feature order follows diff --git a/internal/web/handlers/authz_test.go b/internal/web/handlers/authz_test.go index 393395f..6dfe8d7 100644 --- a/internal/web/handlers/authz_test.go +++ b/internal/web/handlers/authz_test.go @@ -24,6 +24,8 @@ type route struct { // administrator is answered 404 rather than 403 so the panel does not confirm // that the page exists (security.md). var globalOnlyRoutes = []route{ + {"GET", "/server/components", func(h *Handlers) http.HandlerFunc { return h.HandleComponents }, nil}, + {"GET", "/users", func(h *Handlers) http.HandlerFunc { return h.HandleUsers }, nil}, {"GET", "/users/new", func(h *Handlers) http.HandlerFunc { return h.HandleUserNew }, nil}, {"POST", "/users/new", func(h *Handlers) http.HandlerFunc { return h.HandleUserNew }, nil}, diff --git a/internal/web/handlers/handlers_components.go b/internal/web/handlers/handlers_components.go new file mode 100644 index 0000000..c19a89a --- /dev/null +++ b/internal/web/handlers/handlers_components.go @@ -0,0 +1,24 @@ +package handlers + +import ( + "net/http" + + "github.com/mixeme/selfpost/internal/web/auth" + "github.com/mixeme/selfpost/internal/web/view" +) + +// HandleComponents serves the kit page: every partial of the panel's component +// kit in every state, rendered from fixtures (view.KitPage). It is the reference +// a reviewer holds a restyled page against, and it is part of Server, so only +// the global role sees it — a domain administrator is answered 404, as on every +// other Server page. +func (h *Handlers) HandleComponents(w http.ResponseWriter, r *http.Request) { + p, ok := h.requireGlobal(w, r) + if !ok { + return + } + page := view.KitPage() + page.User = auth.CurrentUser(r) + page.IsGlobal = p.IsGlobal() + h.view.Render(w, http.StatusOK, "components", page) +} diff --git a/internal/web/handlers/handlers_components_test.go b/internal/web/handlers/handlers_components_test.go new file mode 100644 index 0000000..8dadd3e --- /dev/null +++ b/internal/web/handlers/handlers_components_test.go @@ -0,0 +1,56 @@ +package handlers + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +var componentsRoute = route{"GET", "/server/components", func(h *Handlers) http.HandlerFunc { return h.HandleComponents }, nil} + +// The kit page is part of Server, so the rule of the subtree holds for it: a +// domain administrator, and a request with no principal at all, get 404 and not +// a hint that the page exists (security.md). +func TestComponentsPageIsGlobalOnly(t *testing.T) { + h, domains := serverWithTwoDomains(t) + + p := domainAdmin(t, h.store, "kit-reader", domains["first.example.ru"].ID) + if rec := call(h, componentsRoute, p); rec.Code != http.StatusNotFound { + t.Errorf("GET /server/components as a domain administrator = %d, want 404:\n%s", rec.Code, rec.Body.String()) + } + + rec := httptest.NewRecorder() + h.HandleComponents(rec, httptest.NewRequest(http.MethodGet, "/server/components", nil)) + if rec.Code != http.StatusNotFound { + t.Errorf("GET /server/components with no principal = %d, want 404", rec.Code) + } +} + +// For the global role it renders the kit: the shell, with the Server group of +// the menu marked, and the page's own partials. +func TestComponentsPageRendersTheKitForTheGlobalRole(t *testing.T) { + h, _ := serverWithTwoDomains(t) + + rec := call(h, componentsRoute, globalPrincipal) + if rec.Code != http.StatusOK { + t.Fatalf("GET /server/components as a global administrator = %d, want 200:\n%s", rec.Code, rec.Body.String()) + } + out := rec.Body.String() + for _, want := range []string{ + `

Components

`, + `href="/static/panel.css"`, + `