From d3278061980bf0aa2ad150f8bb9398aed46e76a6 Mon Sep 17 00:00:00 2001 From: timbogdanov Date: Tue, 18 Aug 2026 20:18:47 -0500 Subject: [PATCH] Restore the gold accent, and write down the states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/09-design-system.md specified Aseprite's gold accent with "blue only for links". The stylesheet used #2962ff and verify-ui.py asserted "accent is no longer gold". Code and checker agreed; the document whose whole purpose is preventing drift was the thing that had drifted, and --link (#6e9adb) had become a second blue sitting eight hue-degrees from the accent while doing an unrelated job. Gold wins. Chrome belongs to the audience, which lives in Aseprite; OpenFront fidelity is carried by the format, the codec and the preview, not by the accent. verify-ui.py now fails on #2962ff, so the round trip is not repeated. Four status messages were writing colour as inline hex — style="color:#e5674b" — beside a --danger token that already held that job. They now use the tokens, and the checker rejects inline colour in that position. States were the part of the system nobody had written down, which is why they drifted: the document specified tokens, radius, density and layout, and said nothing about focus, disabled, hover, pressed, success or error. All eight are specified now and six are mechanically checked. Three other claims in the document were false and are corrected: the default interface scale is 1.5x rather than 2x, the preview holds a column of its own, and Tab collapses three columns rather than two. The gutters between them are drag-to-resize, which went unmentioned entirely. The icon's duotone band moves to the same gold. It draws only the ink in colour now — giving the band its own ground introduced a third grey between paper and ink and muddied the tile at small sizes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UCrKKTD2mwHx8Fm1EnJiQ3 --- app/patternfront.html | 14 ++++---- build/icon.png | Bin 14754 -> 14745 bytes docs/09-design-system.md | 72 ++++++++++++++++++++++++++++++++------- tools/gen-icon.py | 12 ++++--- tools/verify-ui.py | 29 ++++++++++++++-- 5 files changed, 99 insertions(+), 28 deletions(-) diff --git a/app/patternfront.html b/app/patternfront.html index d0facaf..f7db28a 100644 --- a/app/patternfront.html +++ b/app/patternfront.html @@ -27,9 +27,9 @@ --text:#c0c0c0; /* text */ --dim:#7d7d7d; /* tab_normal_text */ --mute:#636d79; /* status_bar_text */ - --accent:#2962ff; /* OpenFront's own blue, src/core/.../Colors.ts */ - --accent-hi:#4b8fff; - --accent-ink:#ffffff; + --accent:#e1b85f; /* selected */ + --accent-hi:#eec877; /* accent hover */ + --accent-ink:#41444a; /* selected_text */ --link:#6e9adb; /* link_text */ --danger:#c75a68; /* flag_active */ --ok:#64c864; /* select_box_grid */ @@ -1774,17 +1774,17 @@ ' map tiles. Repeats every '+(doc.w<OF_MAXW||doc.h>OF_MAXH){ $('ofData').textContent='—'; - $('ofStat').innerHTML='Canvas is '+doc.w+'×'+doc.h+ + $('ofStat').innerHTML='Canvas is '+doc.w+'×'+doc.h+ '. OpenFront allows at most '+OF_MAXW+'×'+OF_MAXH+'.'; return null;} const s=encodeOF(doc.w,doc.h,scale,to1bit()); $('ofData').textContent=s; $('ofStat').innerHTML=s.length<=OF_MAXB64 - ?'Valid · '+s.length+' / '+OF_MAXB64+' characters' - :'Too long — '+s.length+' / '+OF_MAXB64+''; + ?'Valid · '+s.length+' / '+OF_MAXB64+' characters' + :'Too long — '+s.length+' / '+OF_MAXB64+''; return s;} function copyText(t,ok){ - const done=()=>{$('ofStat').innerHTML=''+ok+'';}; + const done=()=>{$('ofStat').innerHTML=''+ok+'';}; const fb=()=>{const ta=document.createElement('textarea');ta.value=t; document.body.appendChild(ta);ta.select(); try{document.execCommand('copy');done();} diff --git a/build/icon.png b/build/icon.png index 23619194f30b606344b4f7a4bf3e00fafae2c0a6..5546518e9c9299e05e91aa5a8a7d272ef17d3be1 100644 GIT binary patch literal 14745 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 diff --git a/docs/09-design-system.md b/docs/09-design-system.md index bbe67a5..a826456 100644 --- a/docs/09-design-system.md +++ b/docs/09-design-system.md @@ -34,6 +34,14 @@ Aseprite's own id is kept against each value so the mapping stays auditable. Note Aseprite's dark accent is **gold**, not blue. Blue is reserved for links. +This one was reversed once and reversed back. A revision moved `--accent` to +OpenFront's own blue (`#2962ff`) on the reasoning that the tool is for OpenFront. +The result was two blues eight hue-degrees apart doing unrelated jobs — selection +and links — and a design document that disagreed with both the stylesheet and the +checker. Chrome belongs to the audience, which lives in Aseprite; OpenFront fidelity +is carried by the format, the codec and the map-scale preview, not by the accent. +`tools/verify-ui.py` now fails on `#2962ff` so the round trip is not repeated. + ## Hard rules - **Radius 0–2px.** Nothing rounder, anywhere. @@ -51,7 +59,8 @@ Two deliberate exceptions, both checked for: ## Interface scale Aseprite's native density is very tight and it ships a **UI Scale** preference for -exactly that reason, defaulting to 2× on high-DPI displays. PatternFront does the same. +exactly that reason, defaulting to 2× on high-DPI displays. PatternFront carries the +same preference at a lower default, for the reason given below. Every length in the stylesheet is `rem`, and `html{font-size:calc(10px * var(--ui))}` drives the lot. So `1.1rem` reads as "11px at 1×" and one token resizes the whole @@ -59,18 +68,21 @@ interface. The artwork view scales too — `Z() = zoom * uiScale` — so the app a single piece rather than the chrome growing around a fixed canvas. The control sits in the top bar (1× · 1.5× · 2× · 2.5× · 3×) and persists to -`localStorage`. **Default is 2×.** +`localStorage`. **Default is 1.5×** — 2× was the original default and crowded the +canvas once the preview took a column of its own. + +Columns are left / preview / right. | Scale | Bars | Buttons | Font | Rail | Columns | |---|---|---|---|---|---| -| 1× | 24px | 18px | 11px | 26px | 152 / 190 | -| 1.5× | 36px | 27px | 16px | 39px | 228 / 285 | -| **2×** | **48px** | **36px** | **22px** | **52px** | **304 / 380** | -| 2.5× | 60px | 45px | 28px | 65px | 380 / 475 | -| 3× | 72px | 54px | 33px | 78px | 456 / 570 | +| 1× | 24px | 18px | 11px | 26px | 152 / 240 / 190 | +| **1.5×** | **36px** | **27px** | **16px** | **39px** | **228 / 360 / 285** | +| 2× | 48px | 36px | 22px | 52px | 304 / 480 / 380 | +| 2.5× | 60px | 45px | 28px | 65px | 380 / 600 / 475 | +| 3× | 72px | 54px | 33px | 78px | 456 / 720 / 570 | -Above 2× the side columns start to crowd the canvas on a 1440px window — `Tab` collapses -both and is the intended companion to the larger scales. +Above 1.5× the columns start to crowd the canvas on a 1440px window — `Tab` collapses +them and is the intended companion to the larger scales. ## Density @@ -90,16 +102,50 @@ Base values, scaled from Aseprite's own `dimensions` block, quoted at 1×. ``` top bar 24px ├ rail 26px · tools, always visible -├ left column 152px · colour, palette, canvas, options +├ left column 152px · colour, duotone, stamps, palette ├ stage · dominant, the only thing that grows -└ right column 190px · preview, layers +├ preview column 240px · preview and its controls +└ right column 190px · layers dock 24px · frames + status on one line ``` -`Tab` collapses both columns (`body.zen`) for a near-fullscreen canvas. -One continuous docked column per side — no floating cards, no gaps, no rounding; +The preview holds its own column rather than stacking under the layers panel. It is +the surface a pattern is actually judged on — it is the only place the duotone +appears at all — and sharing a column made it the first thing squeezed. + +`Tab` collapses all three columns and their gutters (`body.zen`) for a near-fullscreen +canvas. Each boundary is a drag handle (`.gut`) that resizes the column it sits against +and resets on double-click; the widths live in `--wL` / `--wP` / `--wR` so a drag and a +scale change move the same numbers. + +One continuous docked column per region — no floating cards, no gaps, no rounding; sections separated by 1px rules. +## States + +Tokens and density were specified from the start; states were not, and drifted into +inline hex. Every one below is a token, and `tools/verify-ui.py` fails on a literal +colour used where one of these belongs. + +| State | Treatment | Token | +|---|---|---| +| Rest | `--raised` fill, `--edge-dim` border | — | +| Hover | fill and border both go to `--edge` | `--edge` | +| Active / pressed | fill drops to `--well` | `--well` | +| Selected / on | `--accent` fill, `--accent-ink` text | `--accent`, `--accent-ink` | +| Focus (keyboard) | 1px `--accent` outline, no offset | `--accent` | +| Disabled | text to `--well`, default cursor, no hover | `--well` | +| Success | text only, never a fill | `--ok` | +| Error | text only, never a fill | `--danger` | + +Two rules that are easy to get wrong: + +- **Status is text, not chrome.** A valid or over-length pattern says so in `--ok` or + `--danger` text. It never tints a panel, because a coloured panel competes with the + artwork, which is the only thing in the window whose colour carries meaning. +- **Focus is not hover.** Hover is a fill change; focus is an outline. A control that + showed focus by filling would be indistinguishable from selection. + ## Anti-patterns Every one of these was tried and rejected: diff --git a/tools/gen-icon.py b/tools/gen-icon.py index 1bb7e60..930a6fd 100644 --- a/tools/gen-icon.py +++ b/tools/gen-icon.py @@ -54,8 +54,10 @@ # The app's own chrome, so the icon and the window agree. PAPER = (0x20, 0x21, 0x25) # --well, the editor's background INK = (0xE6, 0xE6, 0xE8) # the editor's foreground -DUO_A = (0x29, 0x62, 0xFF) # the accent, standing in for a duotone primary -DUO_B = (0xF1, 0x7A, 0x3C) # its warm complement, the secondary +ACCENT = (0xE1, 0xB8, 0x5F) # --accent, the same gold the app selects with +# Inside the band only the *ink* changes colour; the ground stays the same dark as +# everywhere else. Giving the band its own ground introduced a third grey that sat +# between paper and ink and made the whole tile muddy at small sizes. # A diagonal, which is one of the patterns the editor ships, at the coarsest # step that still reads as motion. Two cells of ink, two of paper, shifted one @@ -133,10 +135,10 @@ def pixel(x: int, y: int): cy = min(GRID - 1, max(0, int((y - inset) // cell))) lit = ART[cy][cx] == "#" - if in_band(cx, cy): - rgb = DUO_B if lit else DUO_A + if lit: + rgb = ACCENT if in_band(cx, cy) else INK else: - rgb = INK if lit else PAPER + rgb = PAPER return (rgb[0], rgb[1], rgb[2], round(a * 255)) diff --git a/tools/verify-ui.py b/tools/verify-ui.py index f249e42..046a3c5 100644 --- a/tools/verify-ui.py +++ b/tools/verify-ui.py @@ -37,7 +37,9 @@ "--text": "#c0c0c0", # text "--dim": "#7d7d7d", # tab_normal_text "--mute": "#636d79", # status_bar_text - "--accent": "#2962ff", # OpenFront's own blue + "--accent": "#e1b85f", # selected + "--accent-hi": "#eec877", # accent hover + "--accent-ink": "#41444a", # selected_text "--link": "#6e9adb", # link_text "--danger": "#c75a68", # flag_active } @@ -45,7 +47,7 @@ # The accent is exempt: Aseprite's own `selected` is a bright gold, and # selection is meant to be loud. The rule is about chrome, not highlights. MAX_SURFACE_LUMA = 0.30 -ACCENT_FAMILY = {"2962ff", "4b8fff"} +ACCENT_FAMILY = {"e1b85f", "eec877"} fails: list[str] = [] @@ -200,7 +202,28 @@ def main() -> int: chk("picking a pair yields exactly two colours", "doc.palette=[{r:0,g:0,b:0,a:0},hexRGB(prim),hexRGB(sec)]" in js) - chk("accent is no longer gold", "#e1b85f" not in css) + # Aseprite's dark accent is gold and blue is reserved for links, which is what + # keeps the two distinguishable. An earlier revision moved the accent to + # OpenFront's blue (#2962ff), leaving two near-identical blues doing different + # jobs. The tool's chrome belongs to the audience that lives in Aseprite; + # OpenFront fidelity is carried by the format and the preview, not the chrome. + chk("accent is gold, not OpenFront blue", + "#2962ff" not in css and "#4b8fff" not in css) + + # States were the part of the system nobody wrote down, so they drifted into + # inline hex: four `style="color:#e5674b"` strings sat beside a --danger token + # that already held that job. See docs/09-design-system.md, "States". + print("\n=== states ===") + chk("status colour comes from tokens, never inline hex", + not re.search(r'style="color:#[0-9a-fA-F]', js), ) + chk("focus is an outline in the accent, not a fill", + "outline:0.1rem solid var(--accent)" in css) + chk("hover changes fill and border to --edge", + ".b:hover:not(:disabled){background:var(--edge);border-color:var(--edge)}" in css) + chk("pressed drops to --well", ".b:active:not(:disabled){background:var(--well)}" in css) + chk("disabled dims to --well", ".b:disabled{color:var(--well)" in css) + chk("selected is accent fill with accent ink", + 'background:var(--accent);color:var(--accent-ink)' in css) print("\n=== preview & colour input ===") chk("preview starts fitted and tiled", "pvScale=0,pvTile=true" in js)