From 49b4040ae71f825597f2dde4d8ca43350e6c4ff6 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 21:30:22 -0300 Subject: [PATCH 1/2] layout: a thing with its own action is a panel; panels tile at width_panel_min A section of a page stays a heading and its rows. A thing with its own identity and its single action - a network policy, a server, an image - is a panel: a hairline edge at radius, its header on surface, its body on the page, no shadow. Panels tile across the 1600 px column, none narrower than width_panel_min (--spin-width-panel-min), and inside the column a table has fixed tracks: on the network policies page a port stood 1000 px from its destination. The dashboard gets --surface for the panel's header, and the laboratory a Panels specimen. Co-Authored-By: Claude Opus 5.5 --- cmd/palette/main.go | 1 + cmd/palette/outputs.go | 6 ++++++ dist/dashboard.css | 3 +++ dist/tokens.css | 1 + dist/tokens.json | 3 ++- index.html | 31 ++++++++++++++++++++++++++++--- palette.toml | 11 +++++++++++ 7 files changed, 52 insertions(+), 4 deletions(-) diff --git a/cmd/palette/main.go b/cmd/palette/main.go index 871f895..9c16ced 100644 --- a/cmd/palette/main.go +++ b/cmd/palette/main.go @@ -99,6 +99,7 @@ type palette struct { TargetTouch int `toml:"target_touch"` Phone, Tablet, Desktop int WidthPage int `toml:"width_page"` + WidthPanelMin int `toml:"width_panel_min"` } // A control's geometry: the heights of a button or an input, the icon in it, and the gap // between an icon and its word. diff --git a/cmd/palette/outputs.go b/cmd/palette/outputs.go index 83c2836..7ca58e0 100644 --- a/cmd/palette/outputs.go +++ b/cmd/palette/outputs.go @@ -118,6 +118,7 @@ func tokensCSS(p palette, dark, light map[string]rgb) string { fmt.Fprintf(&b, "\t--spin-control-sm: %dpx;\n\t--spin-control-md: %dpx;\n\t--spin-control-lg: %dpx;\n", p.Controls.Sm, p.Controls.Md, p.Controls.Lg) fmt.Fprintf(&b, "\t--spin-control-icon: %dpx;\n\t--spin-control-gap: %dpx;\n", p.Controls.Icon, p.Controls.Gap) fmt.Fprintf(&b, "\t--spin-width-page: %dpx;\n", p.Layout.WidthPage) + fmt.Fprintf(&b, "\t--spin-width-panel-min: %dpx;\n", p.Layout.WidthPanelMin) fmt.Fprintf(&b, "\t--spin-duration-fast: %dms;\n\t--spin-duration: %dms;\n", p.Motion.Fast, p.Motion.Base) fmt.Fprintf(&b, "\t--spin-duration-slow: %dms;\n\t--spin-duration-draw: %dms;\n", p.Motion.Slow, p.Motion.Draw) fmt.Fprintf(&b, "\t--spin-ease: %s;\n", p.Motion.Ease) @@ -221,6 +222,9 @@ func dashboardCSS(p palette, dark, light map[string]rgb) string { // by its own lightness rather than by a shadow; white in light. {"popover", hsl(c["overlay"])}, {"popover-foreground", hsl(c["text"])}, {"muted", hsl(c["raised"])}, {"muted-foreground", hsl(c["muted"])}, + // A panel's header: the surface, a step off the page in either theme, so a thing's name + // and its action read as one band without a second border saying so. + {"surface", hsl(c["surface"])}, {"border", hsl(c["border"])}, {"input", hsl(c["border_strong"])}, {"ring", hsl(c["accent"])}, {"primary", hsl(c["accent"])}, {"primary-foreground", hsl(c["on_accent"])}, // The accent as text - a link, an action written as words: accent_text, held at 4.5:1 on @@ -264,6 +268,8 @@ func dashboardCSS(p palette, dark, light map[string]rgb) string { fmt.Fprintf(&b, "\t--spin-control-icon: %dpx;\n\t--spin-control-gap: %dpx;\n", p.Controls.Icon, p.Controls.Gap) // How wide a page may grow (the dashboard's page-width.ts reads it). fmt.Fprintf(&b, "\t--spin-width-page: %dpx;\n", p.Layout.WidthPage) + // The least a panel is as panels tile across the column (layout.width_panel_min). + fmt.Fprintf(&b, "\t--spin-width-panel-min: %dpx;\n", p.Layout.WidthPanelMin) // The dim behind a dialog: the dark page, in either theme, at the palette's scrim - a neutral // dim, not the mark's navy, which tinted everything behind a dialog blue. dim := dark["page"] diff --git a/dist/dashboard.css b/dist/dashboard.css index 85b5ecc..94a9ccb 100644 --- a/dist/dashboard.css +++ b/dist/dashboard.css @@ -8,6 +8,7 @@ --popover-foreground: 220.9 39.3% 11.0%; --muted: 216.0 33.3% 91.2%; --muted-foreground: 219.4 16.3% 38.4%; + --surface: 217.5 40.0% 96.1%; --border: 216.0 20.4% 80.8%; --input: 221.1 14.8% 53.5%; --ring: 200.4 88.4% 33.9%; @@ -45,6 +46,7 @@ --popover-foreground: 215.0 31.6% 92.5%; --muted: 221.8 34.7% 18.6%; --muted-foreground: 218.0 17.4% 66.3%; + --surface: 220.8 36.2% 13.5%; --border: 221.1 26.8% 27.8%; --input: 221.0 16.1% 50.0%; --ring: 201.6 73.9% 40.6%; @@ -81,6 +83,7 @@ --spin-control-icon: 16px; --spin-control-gap: 6px; --spin-width-page: 1600px; + --spin-width-panel-min: 560px; --scrim: rgb(14 20 33 / 0.64); --terminal-background: #0A0C10; --terminal-foreground: #E6EBF2; diff --git a/dist/tokens.css b/dist/tokens.css index b7b9848..8be2bae 100644 --- a/dist/tokens.css +++ b/dist/tokens.css @@ -118,6 +118,7 @@ --spin-control-icon: 16px; --spin-control-gap: 6px; --spin-width-page: 1600px; + --spin-width-panel-min: 560px; --spin-duration-fast: 120ms; --spin-duration: 200ms; --spin-duration-slow: 400ms; diff --git a/dist/tokens.json b/dist/tokens.json index 203369d..e42a788 100644 --- a/dist/tokens.json +++ b/dist/tokens.json @@ -47,7 +47,8 @@ "Phone": 390, "Tablet": 768, "Desktop": 1280, - "WidthPage": 1600 + "WidthPage": 1600, + "WidthPanelMin": 560 }, "light": { "accent": "#0A6FA3", diff --git a/index.html b/index.html index 6b92f72..fe980e6 100644 --- a/index.html +++ b/index.html @@ -197,7 +197,15 @@ .state::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: currentColor; } .state.ok { color: var(--spin-success); } .state.warn { color: var(--spin-warning); } .state.err { color: var(--spin-error); } .state.idle { color: var(--spin-muted); } .state.warn::before { border-radius: 1px; transform: rotate(45deg); } .state.err::before { border-radius: 0; } -.panel { background: var(--spin-raised); border: 1px solid var(--spin-border); border-radius: var(--spin-radius); padding: 1rem; } +.panels { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--spin-width-panel-min)), 1fr)); gap: 1rem; } +.panel { background: var(--spin-page); border: 1px solid var(--spin-border); border-radius: var(--spin-radius); overflow: hidden; } +.panel header { display: flex; align-items: center; gap: .6rem; padding: .5rem .75rem; background: var(--spin-surface); border-bottom: 1px solid var(--spin-border); font-size: 13px; } +.panel header b { font: 600 14px var(--spin-font-mono); } +.panel header .grow { flex: 1; } +.panel .grid { table-layout: fixed; } +.panel .grid td, .panel .grid th { white-space: normal; } +.panel .grid tr:last-child td { border-bottom: 0; } +.panel .grid .id { color: var(--spin-text); } .dialog { background: var(--spin-overlay); border: 1px solid var(--spin-border); border-radius: var(--spin-radius-lg); padding: 1.25rem; max-width: 26rem; box-shadow: var(--spin-shadow-overlay); } .toast { display: inline-flex; gap: .6rem; align-items: center; background: var(--spin-overlay); border: 1px solid var(--spin-border); border-radius: var(--spin-radius); padding: .6rem .9rem; box-shadow: var(--spin-shadow-overlay); } .row { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-start; margin-top: 1rem; } @@ -250,7 +258,7 @@

Spinbox brand system

@@ -402,6 +410,23 @@

Sizes

Headers in the muted sans, never uppercase; ids in the mono; figures right-aligned in tabular digits (--spin-numeric); agent-1 under the pointer, ci-job selected. On a phone a table scrolls inside its frame, the page does not.

+
+ Panels +
+

A section of a page is a heading and its rows; a thing with its own identity and its single action is a panel. A hairline edge at --spin-radius, its header - the name in the mono, its state, the one action and ⋯ - on surface, its body on the page, no shadow: it does not float. Panels tile across the column, none narrower than --spin-width-panel-min; inside one, a table has fixed tracks, so a column is at one x in every panel. A list of many is a table, never a panel per row.

+
+
+
webpending
+
DestinationsPorts
Any address80/tcp, 443/tcp
github.com443/tcp
+
+
+
pingenforced on 2 runners
+
DestinationsPorts
Any addressping
+
+
+
+
+
Compositions
@@ -411,7 +436,7 @@

Marketing

Documentation

A Starlight page: one column at the reading measure, the sidebar at --spin-sidebar, headings in the mono. A claim links to the row of the status page that proves it.

Application

-

The dashboard, read for diagnosis. Every page is one centred column --spin-width-page wide, so its edges stay where the page before had them; an explorer takes the viewport. A page is a header - the title, its state, its context and its one primary action on one row - and sections of rows between hairlines; the name beside the mark is --spin-wordmark, which the catalog audit holds the sidebar to.

+

The dashboard, read for diagnosis. Every page is one centred column --spin-width-page wide, so its edges stay where the page before had them; an explorer takes the viewport. A page is a header - the title, its state, its context and its one primary action on one row - and sections of rows between hairlines; a thing with its own identity and its single action is a panel, and panels tile across the column; the name beside the mark is --spin-wordmark, which the catalog audit holds the sidebar to.

Actions

One primary action per view, solid in the accent and in words alone: New workspace on a list, Open terminal on a workspace. A row's actions are quiet: its most likely one as a secondary button (the raised fill, a hairline edge, no icon) and the rest behind ⋯. An icon goes alone, with a tooltip, only where the toolbar has no room for words; never an icon and a word on every row. What destroys is in the destructive fill, in a confirmation, after Cancel.

Elevation

diff --git a/palette.toml b/palette.toml index 6d6a2a8..1e7b9a9 100644 --- a/palette.toml +++ b/palette.toml @@ -170,6 +170,17 @@ desktop = 1280 # (2026-11-08). An explorer - logs, traces - takes the viewport. The dashboard's page-width.ts # reads this. width_page = 1600 # px +# What a page is made of, inside that column. A section of a page is a heading, a hairline and +# its rows, on the page's own plane: a border around every section made the console a grid of +# cards (the October review). A thing with its own identity and its single action - a network +# policy, a server, an image - is a panel: a hairline edge at `radius`, its header (name, state, +# the action and its menu) on `surface`, its body on the page, no shadow, since it does not +# float. A list of many is a table, never a panel per row. Panels tile across the column, each +# at least this wide: at 1600 px two rules tables side by side, where one stretched to the +# column's width put a port 1000 px from its destination (2026-11-08). In the column a table has +# fixed tracks, so a column is at one x in every row and every panel, and a label and its value +# sit together, never split across the width. +width_panel_min = 560 # px # Controls: a button's or an input's height, small (a row's action, a toolbar), medium (a form, a # page's header) and large (a dialog's footer, the site); the icon in it and the gap to its word. From ad26088fe7111a5e42221f3424adaed8fca02184 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 21:31:07 -0300 Subject: [PATCH 2/2] layout: the panel's examples are things the dashboard draws as one Co-Authored-By: Claude Opus 5.5 --- palette.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/palette.toml b/palette.toml index 1e7b9a9..89116ed 100644 --- a/palette.toml +++ b/palette.toml @@ -173,7 +173,7 @@ width_page = 1600 # px # What a page is made of, inside that column. A section of a page is a heading, a hairline and # its rows, on the page's own plane: a border around every section made the console a grid of # cards (the October review). A thing with its own identity and its single action - a network -# policy, a server, an image - is a panel: a hairline edge at `radius`, its header (name, state, +# policy, a resource preset, a forge's app - is a panel: a hairline edge at `radius`, its header (name, state, # the action and its menu) on `surface`, its body on the page, no shadow, since it does not # float. A list of many is a table, never a panel per row. Panels tile across the column, each # at least this wide: at 1600 px two rules tables side by side, where one stretched to the