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 2361919..5546518 100644 Binary files a/build/icon.png and b/build/icon.png differ 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)