diff --git a/mockups/canvases/README.md b/mockups/canvases/README.md index cb2d3696..ae9a6094 100644 --- a/mockups/canvases/README.md +++ b/mockups/canvases/README.md @@ -61,9 +61,9 @@ and `templates/` use `page(title, body, extra_css="")`. Copy whichever matches your source. `apple-wallet` is the folder built from more than one source: two modules, one per Figma file, imported by its `gen.py`. -`00-welcome` is the one board here that is not phone-shaped. Its `gen.py` -writes 2153 x 819 into `layout.json` as `w`/`h`, which is how any board -declares a box other than the default 478 x 980. +Two boards here are not phone-shaped. `00-welcome` writes 2153 x 819 into +`layout.json` as `w`/`h` and `dun-web` writes 1954 x 982, which is how any +board declares a box other than the default 478 x 980. ## Examples @@ -308,6 +308,23 @@ declares a box other than the default 478 x 980. of the captures' own clocks, badges or iOS 26 glyphs carried over. And it records p2, where two stacked 20% scrims land exactly on 255 x .8 x .8. The nine `ref-*` boards are gitignored, so a fresh clone has 14. +- `dun-web/`: the run to read when the source is a web page rather than a + phone. One 1954 x 982 landscape board, the eight-card bento feature section + of Dun's marketing page, in 8 boards across three rows: a colour and radius + board, a type and metrics board, four evidence boards for 94 tokens, the + replica, and the capture under it. The + capture is 1x, so design px and capture px are the same number and no + measurement passes through a scale. Mean absolute delta is 4.86 levels (of + 255) over the whole frame and 3.19-9.21 per card, the spread tracking how + much of a card is small type. Its `README.md` records the two findings that + cost the most: a faded row's tile does **not** fade with its contents (the + tile holds .78 while the contents go to .04, so the tile is painted behind + an opacity wrapper), and the estimator that separates the two, which only + works when both windows sit at the same x because that is what cancels the + cards' right-edge fade. It also records the dark pills, whose plane fit + extrapolates below black, and the card radius that `refkit bbox` reports as + 28 where a circle fit over the corner says 36. The one `ref-*` board is + gitignored, so a fresh clone has 7. - `templates/`: the starting point, not a finished board. The four boards every run produces (design tokens, evidence, one phone screen, one parked reference) with placeholder values, generated from one list of tokens so diff --git a/mockups/canvases/dun-web/00-design-tokens.html b/mockups/canvases/dun-web/00-design-tokens.html new file mode 100644 index 00000000..f7e33635 --- /dev/null +++ b/mockups/canvases/dun-web/00-design-tokens.html @@ -0,0 +1,155 @@ + + + + +Dun - Design Tokens + + + +

Dun — web feature section

Measured off one 1954 × 982 capture at 1×, so every number here is also a design px. The faces and the geometry are on the next board, the evidence for every value on the ones after it.

Colour

--d-bg#F2F2F2
--d-c-ws#D8E2E8
--d-c-proj#F9D2B9
--d-c-team#E1E6C4
--d-c-docs#F6C8CE
--d-c-comm#D3ECE8
--d-c-subt#F9E9B5
--d-c-task#DDDAED
--d-c-bulk#ECEFBF
--d-white#FFFFFF
--d-panel-top#F5F9FA
--d-panel-bot#EBF2F2
--d-chip#F6F6F6
--d-dark#000000
--d-dark-2#4E4E4E
--d-box#3A3B38
--d-kbd#2B2B2B
--d-tip#444746
--d-pill-grey#EEEEEE
--d-plus#E6EAEB
--d-tile-fadergba(255,255,255,.78)
--d-ghostrgba(255,255,255,.42)
--d-ringrgba(255,255,255,.75)
--d-hairline#ECECEC
--d-tree#E8D7A4
--d-i-ws#1F4256
--d-i-proj#724324
--d-i-team#203E14
--d-i-docs#5A2D4F
--d-i-comm#215C5C
--d-i-subt#51430D
--d-i-task#5C476A
--d-i-bulk#727358
--d-ink#050505
--d-ink-2#656765
--d-ink-3#B2B2B2
--d-ink-4#D1D1D1
--d-ink-dim#9BA0A0
--d-ink-inv#FFFFFF
--d-doc-ink#2A2A2A
--d-doc-head#343434
--d-doc-body#777876
--d-blue#3B82F6
--d-amber#FBBF24
--d-orange#F59E0B
--d-fuchsia#D946EF
--d-green#22C55E
--d-red#F87171
--d-folder#FFDE77
--d-cur-blue#3092FE
--d-cur-green#1BB944

Radius

36px
24px
14px
20px
18px
8px
8px
16px
999px
+ + diff --git a/mockups/canvases/dun-web/00b-type.html b/mockups/canvases/dun-web/00b-type.html new file mode 100644 index 00000000..40baa37f --- /dev/null +++ b/mockups/canvases/dun-web/00b-type.html @@ -0,0 +1,155 @@ + + + + +Dun - Type and metrics + + + +

Type and metrics

One composite font: per role. The hand face is a substitution and sets narrower than the reference’s, so its size is the one fitted by rendering rather than read off the capture.

Type

Everything starts--d-t-label · italic 700 25px/25px
Everything starts--d-t-body · 400 18px/28px
Everything starts--d-t-pill · 400 19px/24px
Everything starts--d-t-row · 600 16px/20px
Everything starts--d-t-row-id · 400 16px/20px
Everything starts--d-t-chip · 400 13px/16px
Everything starts--d-t-btn · 600 16px/20px
Everything starts--d-t-btn-s · 600 13px/17px
Everything starts--d-t-kbd · 500 11px/14px
Everything starts--d-t-tip · 600 21px/26px
Everything starts--d-t-cursor · 600 18px/22px
Everything starts--d-t-doc-t · 500 18px/22px
Everything starts--d-t-doc-h · 500 11px/14px
Everything starts--d-t-doc-b · 400 8px/11px
Everything starts--d-t-comm-n · 600 13px/16px
Everything starts--d-t-comm-b · 400 12px/16px

Metrics

--d-w: 1954px
--d-h: 982px
--d-pad: 27px
--d-label-top: 31px
--d-body-top: 65px
--d-gap: 21px
--d-row-pitch: 85px
--d-bulk-pitch: 66px
--d-o-text: .82
--d-o-bulk-2: .94
--d-o-bulk-3: .55
--d-o-bulk-4: .04
--d-rot-pill: .56deg
--d-rot-ghost: -1.04deg
--d-o-subt-4: .93
--d-o-subt-5: .38
+ + diff --git a/mockups/canvases/dun-web/00c-evidence.html b/mockups/canvases/dun-web/00c-evidence.html new file mode 100644 index 00000000..d7ecc45d --- /dev/null +++ b/mockups/canvases/dun-web/00c-evidence.html @@ -0,0 +1,155 @@ + + + + +Dun - Evidence 1/4 + + + +

Evidence 1/4

One row per token. A token with no evidence is a guess. The replayable half is probes.json.

--d-font-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Helvetica Neue",Helvetica,Arial,sans-serifrefkit font on “Everything”: no call (SF Pro Rounded .665, SF Pro .629). Real face is outside the macOS set; repo platform stack, width ratio in README
--d-font-hand"SignPainter","Bradley Hand","Brush Script MT",cursiverefkit font on the “Workspace” label: all candidates weak (Futura .383, Brush Script .341). SignPainter closest on scratch/labelcmp2.png
--d-bg#F2F2F2flat-fill census of the page ground, 100% flat
--d-c-ws#D8E2E8flat-fill census inside the Workspace card
--d-c-proj#F9D2B9flat-fill census inside the Projects card
--d-c-team#E1E6C4flat-fill census inside the Teams card
--d-c-docs#F6C8CEflat-fill census inside the Docs card
--d-c-comm#D3ECE8flat-fill census inside the Comments card
--d-c-subt#F9E9B5flat-fill census inside the Subtasks card
--d-c-task#DDDAEDflat-fill census inside the Tasks card
--d-c-bulk#ECEFBFflat-fill census inside the Bulk actions card
--d-white#FFFFFFflat-fill census inside a Teams pill, 2859 px
--d-panel-top#F5F9FArefkit scan col 560, Workspace panel at its top edge
--d-panel-bot#EBF2F2refkit scan col 560, same panel at the card’s bottom
--d-chip#F6F6F6refkit scan row 812, the Tasks row’s Research chip; the Docs segmented field solves #F7F7F7, one level away, so it is the same token and not two
--d-dark#000000the Save-doc and Mark-as-done pills are a gradient, not a fill: a plane fit over each core, evaluated at the gradient line’s two ends, gives Mark-as-done −5.1 → 78.5 on a 10.7° axis and Save doc −2.1 → 78.2 on 13.0°. Both dark ends extrapolate below zero, so the ramp starts at black: starting the gradient line 6% before the box instead closes the last 0.2 levels of the four gradient-only bands and is not worth a stop
--d-dark-2#4E4E4Ethe same two fits’ light end, 78.5 and 78.2 of 255. The first pass read #434343 by averaging the cores instead of fitting the plane, which flattened the ramp from both directions at once
--d-box#3A3B38mode of a Bulk checkbox core, 40 px
--d-kbd#2B2B2Bmode of the Ctrl and Enter key chips on the Bulk toolbar
--d-tip#444746mode of the Dun tooltip core, 357 px
--d-pill-grey#EEEEEEmode of the ThisUX tooltip core, 347 px
--d-plus#E6EAEBmode of the add-workspace circle core, 1599 px
--d-tile-fadergba(255,255,255,.78)the deepest faded row on Bulk and on Subtasks keeps its tile long after its ink has gone: Bulk row 3’s tile solves .77 and the last Subtasks row’s .78 in green and .77 in blue, against contents at .55 and .38. So the tile alpha rides on the background and the opacity goes on a wrapper inside it
+ + diff --git a/mockups/canvases/dun-web/00d-evidence.html b/mockups/canvases/dun-web/00d-evidence.html new file mode 100644 index 00000000..ba7a3d6a --- /dev/null +++ b/mockups/canvases/dun-web/00d-evidence.html @@ -0,0 +1,155 @@ + + + + +Dun - Evidence 2/4 + + + +

Evidence 2/4

One row per token. A token with no evidence is a guess. The replayable half is probes.json.

--d-ghostrgba(255,255,255,.42)the stacked card behind the Tasks row reads #EBEAF3 over --d-c-task (#DDDAED): alpha .41 on red and .43 on green. Blue has 18 levels of headroom against red’s 34 and solves .33, which is the noise floor, not a third colour
--d-ringrgba(255,255,255,.75)1-2px near-white rim just outside every card fill (#F9FEFF/#FFFFFF); the ground returns to #F2F2F2 at once, so there is no dark drop shadow to solve
--d-hairline#ECECECrefkit scan row 527, the toolbar button outline
--d-tree#E8D7A4row scan at y614: the subtask connector is 1px wide and a tint of --d-c-subt, not a grey; a #D6D6D6 mask finds the DUN-NN text instead of the tree
--d-i-ws#1F4256ink core, darkest 2%, Workspace label and body (they match)
--d-i-proj#724324ink core, Projects label and body
--d-i-team#203E14ink core, Teams label and body
--d-i-docs#5A2D4Fink core, Docs label and body
--d-i-comm#215C5Cink core, Comments label and body
--d-i-subt#51430Dink core, Subtasks label and body
--d-i-task#5C476Aink core, Tasks label and body
--d-i-bulk#727358ink core, Bulk label and lines 1-2; line 3 is the same ink at --d-o-text
--d-ink#050505ink core of a row title (Develop MVP, Competitor Analysis Research)
--d-ink-2#656765ink core of the commenter name and the Research chip label
--d-ink-3#B2B2B2ink core of a DUN-NN row id, two cards
--d-ink-4#D1D1D1ink core of the 2h timestamp
--d-ink-dim#9BA0A0ink core of the ThisUX (#9DA09F) and Fli.so (#A0A5A5) tooltip labels and of the add-workspace plus glyph (#919596, only 19 px of ink)
--d-ink-inv#FFFFFFink core on the dark pills and the cursor labels
--d-doc-ink#2A2A2Aink core of the doc title, Product Requirement Doc
--d-doc-head#343434ink core of a doc section heading
--d-doc-body#777876ink core of a doc paragraph
--d-blue#3B82F61.5px stroke; darkest px #488ED6, nearest Tailwind step to its hue
--d-amber#FBBF24filled half-circle, darkest px #FFBF28 = Tailwind amber-400
--d-orange#F59E0B1.5px stroke, terminal icon; darkest px #CB9744
--d-fuchsia#D946EF1.5px stroke, megaphone; darkest px #C435B0
--d-green#22C55E1.5px stroke, briefcase; darkest px #46AD66
--d-red#F871711.5px stroke, warning triangle and the Research dot; darkest px #D4796C
--d-folder#FFDE77mode of the Projects folder core, 8842 px
--d-cur-blue#3092FEmode of the James cursor core, 429 px
--d-cur-green#1BB944mode of the You cursor core, 334 px
+ + diff --git a/mockups/canvases/dun-web/00e-evidence.html b/mockups/canvases/dun-web/00e-evidence.html new file mode 100644 index 00000000..bac40a46 --- /dev/null +++ b/mockups/canvases/dun-web/00e-evidence.html @@ -0,0 +1,156 @@ + + + + +Dun - Evidence 3/4 + + + +

Evidence 3/4

One row per token. A token with no evidence is a guess. The replayable half is probes.json.

--d-r-card36pxcircle fit to where the fill starts on each row of the Tasks card’s bottom-left corner: r 39.5 at rms 0.6, against 30.0 for my own 28px corner, so the fit runs 2 levels of threshold wide and the reference is 37.5. A sweep of the 32 card corners bottoms out at 36, and the same sweep confirms r-panel, r-tile and r-cur where they already stood. A bbox on one corner crop read 28, which is the corner’s chord, not its radius
--d-r-panel24pxthe Workspace and Docs panels’ one visible corner
--d-r-tip14pxthe Dun tooltip corner, and the Docs toolbar’s
--d-r-tile20pxthe Comments tile corner
--d-r-row18pxsolved on a Subtasks row, the Tasks pill and the Bulk toolbar; the Tasks pill is 55px tall, so 18 is a real radius and not h/2
--d-r-box8pxa Bulk checkbox and a toolbar button corner
--d-r-chip8pxthe Research chip corner
--d-r-cur16pxthe James and You label pills, 46 and 47px tall, + so 16 is a real radius and not a full pill
--d-r-pill999pxsolved ≥ h/2 on the four Teams pills, so they are full pills
--d-t-labelitalic 700 25px/25px var(--d-font-hand)ink top is card + 31 on all eight cards. Size is calibrated, not solved: SignPainter is a substitution, so its 21px ink was measured against all eight reference labels (81/64/44/39/79/69/41/96 px wide) and each asks for 23.7-26.4
--d-t-body400 18px/28px var(--d-font)body cap 13px → 18.2; line 1 ink 402px wide on Workspace → 17-18px. Line 1 ink top is card + 65 and the pitch is 28 on all eight cards
--d-t-pill400 19px/24px var(--d-font)“Engineering” cap 14px → 19.6; ink 95px wide → 18px
--d-t-row600 16px/20px var(--d-font)“Competitor Analysis Research” cap 12px → 16.8; ink 212px wide → 14px. The widest disagreement on the board: SF Pro Semibold sets it +14%
--d-t-row-id400 16px/20px var(--d-font)“DUN-20” cap 12px → 16.8; ink 58px wide → 15px
--d-t-chip400 13px/16px var(--d-font)“Research” cap 10px → 14; ink 55px wide → 12-13px
--d-t-btn600 16px/20px var(--d-font)“3 Selected” cap 12px → 16.8, ink 75px → 14px; “Mark as done” ink 121px → 18px. White-on-dark ink measures dilated and dark-on-white eroded, so the two straddle 16
--d-t-btn-s600 13px/17px var(--d-font)“Save doc” cap 10px → 14, ink 57px → 12px. The Docs toolbar is a smaller instance of the Bulk one (252 × 51 vs 528 × 56)
--d-t-kbd500 11px/14px var(--d-font)“Ctrl” ink is 18 × 8 on the Docs plate and 17 × 9 on the Bulk one — the same size within a pixel, so one token, not two
--d-t-tip600 21px/26px var(--d-font)cap 16/16/15px and ink 38/72/48px wide on Dun / ThisUX / Fli.so; 21px satisfies both on all three, the cleanest agreement on the board
--d-t-cursor600 18px/22px var(--d-font)“James” cap 14px → 19.6, ink 58px → 18px; “You” cap 13px → 18.2
+ + diff --git a/mockups/canvases/dun-web/00f-evidence.html b/mockups/canvases/dun-web/00f-evidence.html new file mode 100644 index 00000000..a0a83445 --- /dev/null +++ b/mockups/canvases/dun-web/00f-evidence.html @@ -0,0 +1,155 @@ + + + + +Dun - Evidence 4/4 + + + +

Evidence 4/4

One row per token. A token with no evidence is a guess. The replayable half is probes.json.

--d-t-doc-t500 18px/22px var(--d-font)“Product Requirement Doc” ink is 201 × 18, and cap + descender is .9386em, so 18px. Weight from ink mass: the reference carries 1.125× this string at 15px/700, where the same coverage sum on type that already matches runs 1.00-1.23, which puts it near 500 rather than 700
--d-t-doc-h500 11px/14px var(--d-font)“Overview” ink 50px and “Description” 60px, both 11px; 700 sets Description 3px over and carries 1.1× the reference's ink mass
--d-t-doc-b400 8px/11px var(--d-font)the first doc paragraph is 222px of ink → 8px, and 8px ascender-to-descender → 8px; the measured pitch inside a paragraph is 11
--d-t-comm-n600 13px/16px var(--d-font)“Damia” cap 10px → 14, ink 34px → 10px; 13 splits them
--d-t-comm-b400 12px/16px var(--d-font)“The font size feels too small. Maybe we” cap 9px → 12.6, ink 220px → 11-12px; the measured line pitch is 16
--d-w1954pxthe capture’s own width; the board is the section, 1x
--d-h982pxthe capture’s own height
--d-pad27pxrefkit bands: body ink at card left + 27 on all eight cards
--d-label-top31pxrefkit bands: label ink top, card + 30 to + 33
--d-body-top65pxrefkit bands: body line 1 ink top, card + 65 to + 67
--d-gap21pxcolumn gap in band 1: 595→616, 1136→1157, 1497→1518
--d-row-pitch85pxrefkit bands pitch on the five Subtasks rows
--d-bulk-pitch66pxrefkit bands pitch on the four Bulk actions rows
--d-o-text.82Bulk’s third body line reads #878A6B where lines 1-2 read #727358: the same ink at .82 over #ECEFBF solves 136, the measured 135
--d-o-bulk-2.94both row lists fade toward the bottom of their card, and a row’s own opacity comes off the contrast between its title ink and the tile right above that ink, divided by row 1’s: a ratio that cancels the right-edge fade, which a fill sample does not. Bulk row 2 reads .935 on all three channels
--d-o-bulk-3.55Bulk row 3, .544/.540/.559. Its checkbox solves .57 instead, because it sits 3px above the toolbar inside the white ringing along that edge
--d-o-bulk-4.04Bulk row 4, .040/.037/.040, and its tile is 2 levels off the card fill in blue and level with it in red and green
--d-rot-pill.56degthe Tasks tile is the one rotated box on the board: its top edge runs 774.70 at x 160 to 779.55 at x 700 and its bottom 831.53 to 836.33, two independent fits on +0.0097 rad. Every other tile, panel and toolbar here is flat to a third of a pixel
--d-rot-ghost-1.04degthe stack behind it turns the other way: its one visible edge is 859.25 at x 200 and 850.88 at x 660, −0.0182 rad
--d-o-subt-4.93Subtasks row 4, .928 off its row id at x 1684. Its title reads .839 instead, and the difference is the fade: the titles on rows 3 and 4 are indented to x 1761 where the row ids and row 1 are not, and a ratio only cancels the fade where both windows sit at the same x. Row 3 reads .99, so it is not faded at all
--d-o-subt-5.38the last Subtasks row, .381/.389/.416 off its id and .33 off its title. The two cards do not share one ramp: this row is 56px off its card’s bottom and Bulk’s fourth is 46, and Bulk’s has all but gone
+ + diff --git a/mockups/canvases/dun-web/01-feature-grid.html b/mockups/canvases/dun-web/01-feature-grid.html new file mode 100644 index 00000000..bd6f6411 --- /dev/null +++ b/mockups/canvases/dun-web/01-feature-grid.html @@ -0,0 +1,139 @@ + + + + +Dun - Feature section + + + +
Workspace
Everything starts with a clean workspace. Nothing
cluttered, nothing distracting.
logo-dunlogo-thisuxlogo-fliso
Dun
ThisUX
Fli.so
cur-james
James
cur-you
You
Projects
Create projects to keep things
organized. Big or small, work
how you like
proj-art
Teams
Your team’s in too. Everyone has their own space, work
together & stay in sync
Design
Engineering
Marketing
Sales
Docs
Docs, right next to your tasks. So
no hopping between apps
Product Requirement Doc
Overview
A simple todo app to add, manage, and complete daily tasks.
Description
The app helps people stay organized by letting them quickly jot down tasks,
group them by projects, mark them done, and set reminders.
It’s fast, minimal, and easy to use — works well solo or with a small team. Think
of it like a digital sticky note with superpowers.
Why
Most task apps are too heavy or too confusing for regular folks. People just
want to get stuff out of their heads and come back to it when they’re ready.
We’re solving this because staying organized shouldn’t feel like a job by
KPIs
them via Twitter, Product Hunt, and design communities with a simple
promise: “Get things done without the fluff.”
User adds at least 1 task per day for 7 straight days
At least 40% of tasks marked “done”
30% users use more than one list/project
nk B2C. We’ll targ
Save doc
Ctrl
Comments
Need to discuss? Just comment on
a task, it all stay in one place
avatar-damia
Damia
2h
The font size feels too small. Maybe we
could bump it up a bit for readability
Subtasks
Big task? Break it down and tackle
bit by bit with subtasks
DUN-20
Develop MVP
DUN-21
Setup everything
DUN-22
Setup Database
DUN-23
Create GitHub
DUN-24
Implement Auth
Tasks
Adding tasks is simple, instant and feels as quick as writing on a sticky note
DUN-20
Competitor Analysis Research
Research
avatar-task
Bulk actions
And when there’s a lot to handle at once, you
can select tasks and apply changes in bulk,
all at once
DUN-31
Core features
DUN-32
Create task
DUN-33
Edit/Delete task
DUN-34
Change status
3 Selected
Mark as done
Ctrl
+ + diff --git a/mockups/canvases/dun-web/README.md b/mockups/canvases/dun-web/README.md new file mode 100644 index 00000000..353cc75d --- /dev/null +++ b/mockups/canvases/dun-web/README.md @@ -0,0 +1,264 @@ +# Dun web: the marketing page's feature section + +A `clone-prototype` run over one landscape capture: the eight-card bento grid +on [Dun](https://dun.so)'s marketing page, where each pastel card carries a +handwritten label, a paragraph, and a piece of the product's own UI. One +replica board, six foundation boards, and the capture parked under it. + +Open it with `?canvas=dun-web`. + +| file | what it is | +|---|---| +| `00-design-tokens.html` | The contract. 94 tokens: two faces, 22 surfaces, 3 lines, 17 inks, 9 accents, 9 radii, 16 type shorthands, 16 metrics. Inlined byte-identically into every board. The sheet holds the colours and the radii. | +| `00b-type.html` | The 16 type shorthands set as specimens, and the 16 metrics. On its own board because the swatches and the specimens together overflow one 478 × 980 shape by 573px. | +| `00c`–`00f-evidence.html` | The measurement behind every one of the 94, four boards because the rows do not fit one shape either. `gen.py` fills an evidence board to a height rather than to a row count, and splits the table evenly across the boards it needs, so the last one is not a stub. | +| `01-feature-grid.html` | The replica, 1954 × 982, its own `w`/`h` in `layout.json`. | +| `ref-01-feature-grid.html` | The capture, same size, directly under it. Not checked in — see the last section. | +| `gen.py` | Emits all of the above plus `layout.json` and `icon.png`. Edit this, never the HTML. | +| `probes.json` | 90 probes: the machine half of the evidence table, replayed against the render by `refkit batch`. | +| `crops.json`, `assets/art/` | The eight pieces of the capture that are pixels rather than CSS. | +| `assets/icons/` | 17 line icons, inlined by `gen.py` so the canvas inspector can name each one. | + +The capture is **1×** — a web page screenshot, not a device one — so capture px +and design px are the same number. There is no scale constant in `gen.py`, no +`--pt` on any probe, and `crops.json` holds plain page coordinates. + +```bash +python3 mockups/canvases/dun-web/gen.py +``` + +regenerates the folder byte-identically. It needs either `assets/refs/` +absent or Pillow present: `cut()` and `app_icon()` import PIL, and with the +capture in place system `python3` stops at `ModuleNotFoundError: No module +named 'PIL'`. There is deliberately no fallback for it. + +## How close it lands + +Mean absolute delta against the capture, all three channels, no crop and no +alignment shift — `refkit blend` scores `dy 0` best, so the two are registered +as generated: + +| region | box | Δ | +|---|---|---| +| whole frame | 1954 × 982 | **4.86** | +| Projects | 616,24 521 × 281 | 3.19 | +| Workspace | 75,24 520 × 582 | 3.60 | +| Tasks | 75,634 700 × 290 | 4.60 | +| Bulk actions | 797,634 700 × 290 | 7.45 | +| Subtasks | 1518,328 340 × 596 | 7.03 | +| Teams | 616,326 521 × 280 | 6.96 | +| Docs | 1157,24 340 × 582 | 8.99 | +| Comments | 1518,24 340 × 283 | 9.21 | + +The spread is type density, not geometry. A Δ-block map puts every remaining +hot spot on a glyph: Docs is four headings and 19 lines of 8px body text in a +340px column, and Comments is two lines of 12px text across an otherwise empty +card, so on those two the substituted face is most of what a pixel can +disagree about. +Workspace and Projects, whose content is art and one paragraph, land at 3.2–3.6 +with the same tokens. + +## Substitutions and what they cost + +**The body face is not identified.** `refkit font` on "Everything", the +largest string on the board, returns **no call** — SF Pro Rounded .665 against +SF Pro .629, and neither is close enough to promote. The real face is outside +the macOS candidate set. The board ships the repo's platform stack and the bill +arrives as width: across the 16 type probes the capture's ink width over mine +runs **0.895 to 1.167** (mine 12% wide on "Damia", 14% narrow on "Ctrl"), +median 0.99. + +**Sizes are solved from cap height, not from width.** Every `--x-t-*` size +comes from the measured cap height at SF Pro's .714 cap ratio, then +cross-checked against the string's ink width; where the two disagree the cap +height wins, because the stand-in is not the real face. `--x-t-row` is the +widest disagreement: "Competitor Analysis Research" measures a 12px cap +(→ 16.8px) and 212px of ink (→ 14px), and it ships at 16px semibold. A width +that then overruns a measured container widens the container; the type is never +shrunk to make a width hold. + +**The hand labels are SignPainter.** `refkit font` on "Workspace" rates every +candidate weak (Futura .383, Brush Script .341), so this is a look-alike rather +than a match. Its size is calibrated, not solved: SignPainter's ink was +measured against all eight reference labels (81/64/44/39/79/69/41/96 px wide) +and they ask for 23.7–26.4px, so the token is 25px. + +## Four things a reader would otherwise take for measurement + +**The row lists fade, and the tile does not fade with its contents.** Both +Subtasks and Bulk dim toward the bottom of their card, and the first model — +one opacity per row — is wrong: it moved Subtasks from 7.29 to 7.71. The tile +under a row keeps its white long after the row's ink has gone. Bulk row 3's +tile solves .77 against contents at .55; the last Subtasks row's solves .78 in +green and .77 in blue against contents at .38. So the tile is painted behind at +a fixed `--x-tile-fade: rgba(255,255,255,.78)` and the opacity goes on a +wrapper inside it. A checkbox belongs to the contents, not the tile: row 3's +solves .57, with the contents. + +The estimator behind the five per-row numbers is worth writing down, because +two obvious versions of it are silently wrong. For each row, ground = the +median of the tile just above the glyphs, ink = the darkest 4% of the title +band, and α = (ground − ink) / (ground₁ − ink₁) **measured in the same x window +as row 1**. The right-edge fade cancels exactly in that ratio. Grounding on the +card's token colour instead does not cancel it (row 5 then reads .8 by tile and +.26 by ink), and comparing windows at different x folds the fade back in — the +Subtasks titles on rows 3 and 4 are indented to x 1761 where row 1's ends at +1783, which is what made two undimmed rows read .92 and .84. Validated against +my own render: .70 → 0.713, .11 → 0.114, 1.0 → 1.0. + +**The dark pills are a gradient rebuilt from a fit, and its dark end is not a +colour.** A least-squares plane over each pill's core, evaluated at the CSS +gradient line's two ends, gives Mark-as-done −5.1 → 78.5 on a 10.7° axis and +Save doc −2.1 → 78.2 on 13.0°. Both dark ends extrapolate **below black**, so +`--x-dark` is `#000000` and `--x-dark-2` is `#4E4E4E` — the ramp the capture +shows is steeper than any sRGB gradient over that box can be. Starting the +gradient line 6% before the box closes the last 0.2 levels of the four +gradient-only bands and is not worth a stop. The earlier `#0B0B0B`/`#434343` +pair came from averaging the cores rather than fitting the plane, which +flattened the ramp from both ends at once and cost 2.5 levels on those bands. + +**The card radius is 36px, and one command reports 28.** A `refkit bbox` on a +corner crop reads the corner's chord, not its radius. The number that holds is +a circle fit to where the fill starts on each row of the Tasks card's +bottom-left corner: r 39.5 at rms 0.6, against 30.0 fitted the same way to my +own 28px corner, so the fit runs 2 levels of threshold wide and the reference is +37.5. A sweep over all 32 card corners bottoms out at 36 (2.798 against 3.790 +at 28), and the same sweep confirms `r-panel`, `r-tile` and `r-cur` where they +already stood. + +**Two rotated tiles on Tasks, whose contents do not turn with them.** The +foreground pill is `.56deg` (top edge 774.70 at x 160 → 779.55 at x 700, bottom +831.53 → 836.33, two independent fits on +0.0097 rad) and the stack behind it +`-1.04deg` (859.25 at x 200 → 850.88 at x 660). Every other tile, panel and +toolbar on the board is flat to a third of a pixel. The row's icon, id, title +and chip are set straight inside the rotated box, because that is what the +capture shows. + +The right-edge fades on Subtasks and Bulk are card-coloured gradients over the +rows (transparent → `--x-c-subt` from card-local 246 to 521, and → +`--x-c-bulk` from 611 to 826), not opacity on the rows themselves, which is why +a row's own alpha has to be measured in a window the fade treats equally. + +## What the source itself shows, and what its file format invented + +Text hidden behind a toolbar is **not** reconstructed. The Docs panel's last +line runs under the floating toolbar and comes out as "nk B2C. We'll targ", +written as that tail alone and positioned so its ink starts where the +capture's does. The words in front of it are not recoverable from the pixels +and inventing them would be inventing a fact. The same holds for the Bulk row +the toolbar crosses. + +Four things in the capture are webp artifacts and are deliberately **not** +reproduced: + +- faint grid lines across the Comments card, +- a white glow along the Bulk actions rows, +- a white ring around both cursor label pills, +- ringing above and to the right of the Bulk toolbar's edge, which also biases + any probe placed near it. The "row 4 tile at .11" reading that sent the fade + model wrong came from a window at x 1360–1404 sitting inside that ringing; + the clean window at x 1402–1492 gives .03, which matches its ink at .039. + +**The capture's icons are soft.** At 12× every line glyph carries a visible +glow, and a 1.5px coloured stroke in a soft 1× capture never reaches its own +colour: the five stroke accents read 35 to 95 levels light in `refkit batch` +(`blue` 65, `orange` 60, `fuchsia` 94, `green` 35, `red` 55) and those rows are +expected to differ. What puts the whole set on the Tailwind palette is the one +*filled* glyph, the amber half-circle, whose darkest pixel is `#FFBF28` — Tailwind +amber-400 to the level, against the token's `#FBBF24` (Δ4). The two cursor +arrows are crops, so `cur-blue` and `cur-green` land at Δ2. + +Two icons differ from the capture on purpose: + +- the **briefcase** carries a small clasp stub in the capture that lucide's + `briefcase` does not have; the replica is lucide's handle-and-rounded-rect + and the clasp is not reproduced, +- the **megaphone** is authored on lucide's 24-grid rather than taken from it. + lucide's `megaphone` is a different shape; the capture's is a horn with a + hinged handle under it and a speaker arc at its right. + +## Reading probes.json + +`refkit batch probes.json --against scratch/shots` after a shoot. 55 colour +probes at mean Δmax 7.9, almost all 0–3; 29 box probes at mean |dw| 2.07 and +|dh| 0.93. The rows that do not close, and why: + +- the five stroke accents, above. +- `dark` Δ11. A flat census inside a ramp reports whichever level happens to + repeat most, so it carries ~10 levels of slop; the window's means are 14.6 + against 19.0, and those 4.4 are the part of the reference's ramp that runs + below black. +- `doc-body` Δ28, the 8px body text: an ink percentile over the thinnest type + on the board is a glyph-coverage measurement, not a colour one. +- `o-bulk-3` Δ12 and `o-subt-5` Δ13, ink cores under a faded row, same reason. +- `row-pitch` and `bulk-pitch` print **differs**. `bands` collapses to its + first band, and the substituted face's ink band is 5px taller on Subtasks + and 2px on Bulk. The pitch, which is what the token says, matches: 85 and 66. + +Four scans were narrowed after the first pass, because `batch` compares a +scan's **largest** colour step and a range holding two comparable edges flips +which one that is between the capture and the render. `ring` now takes the +Workspace card's right edge alone (588–602), `gap` the Projects card's left +edge (608–622), `hairline` the X button's left outline (928–942) and `h` the +Tasks card's bottom (900–940). All four then agree to a pixel. A probe window +is wrong far more often than a measurement is, which is why every entry carries +its own sanity note: `panel-bot` had to move twice to get clear of both the You +pill and the 36px card corner, and it read `#1BB944` — the cursor green — until +it did. + +## Assets: all cropped, none generated + +Eight boxes in `crops.json`, cut from the capture by `cut()` and placed back by +`art()` at the same numbers, so an asset cannot drift from where it was +measured: + +- the three workspace marks (60px discs on the panel's own near-white ramp, so + each square crop carries two levels of that ramp back with it), +- the Projects folder and the three note cards fanned behind it — one crop, not + four, because the cards overlap and the fan angles are not measurable off + 169px of art, +- two photographs, the Damia and task avatars: a drawn stand-in for a face is + the substitution a reader always notices, +- the two collaborator arrows. Their pills are CSS, but an arrow is four sharp + vertices and at 1× every one of them is antialiased below any threshold — + fitting the edges put the James apex 4px above where the ink starts. Each box + is the ink grown to its ringing fringe, so the crop carries the one pixel of + overshoot the capture has and the render must not invent. + +Nothing here is generated artwork. `icon.png` is the one place the folder +upscales rather than crops: the Dun mark exists in the capture only at 60px, so +the sticker is a 4.27× LANCZOS upscale of the crop under a supersampled circle +alpha (the crop is the mark's own bbox — the disc spans x 0..59 across its +middle rows and 21..38 at the top). It is soft at 256 and there is no sharper +source; redrawing the loop would make the sticker the one thing in this folder +that is not the reference's own pixels. + +## Two defects that produced no error message + +- **`preserveAspectRatio`.** A square `viewBox` on a non-square span binds one + axis only under the default `xMidYMid meet`, so the 16 × 4 ellipsis rendered + at a quarter size and a size loop would have converged on the wrong axis + forever. Fixed by measuring each icon's own ink box on the 24-grid + (`GRID_INK`, read off a 10× render of the whole set) and setting + `preserveAspectRatio="none"` with both axes given. +- **Pillow's variable-font axis order.** `SFNS.ttf`'s axes are + `[Width, Optical Size, GRAD, Weight]`, and Optical Size must equal the pixel + size. `set_variation_by_axes([wght])` — or the same values in the wrong + order — returns absurd, quantized advances with no complaint, which silently + corrupts every width measurement taken through it. + +## The reference board is not checked in + +Phase 5 parks the capture in `ref-01-feature-grid.html` and `layout.json` keeps +a third row for it, so the reference sits column-for-column under the replica. +The file itself is gitignored: it embeds a third-party page capture this repo +does not redistribute. Put your own capture at `assets/refs/01.png` and re-run +`gen.py` to rebuild it; without it the generator emits everything else and +leaves that board alone. + +## Attribution + +Dun is a product of its owner and the name and logo are its trademarks. This +board is an unaffiliated design study, kept as a record of the measurement +workflow. It is not a Dun product, not endorsed by Dun, and the replica HTML is +not meant to be shipped as a user-facing interface. diff --git a/mockups/canvases/dun-web/assets/art/avatar-damia.png b/mockups/canvases/dun-web/assets/art/avatar-damia.png new file mode 100644 index 00000000..61aa25f4 Binary files /dev/null and b/mockups/canvases/dun-web/assets/art/avatar-damia.png differ diff --git a/mockups/canvases/dun-web/assets/art/avatar-task.png b/mockups/canvases/dun-web/assets/art/avatar-task.png new file mode 100644 index 00000000..8612617e Binary files /dev/null and b/mockups/canvases/dun-web/assets/art/avatar-task.png differ diff --git a/mockups/canvases/dun-web/assets/art/cur-james.png b/mockups/canvases/dun-web/assets/art/cur-james.png new file mode 100644 index 00000000..99e76b90 Binary files /dev/null and b/mockups/canvases/dun-web/assets/art/cur-james.png differ diff --git a/mockups/canvases/dun-web/assets/art/cur-you.png b/mockups/canvases/dun-web/assets/art/cur-you.png new file mode 100644 index 00000000..327511ee Binary files /dev/null and b/mockups/canvases/dun-web/assets/art/cur-you.png differ diff --git a/mockups/canvases/dun-web/assets/art/logo-dun.png b/mockups/canvases/dun-web/assets/art/logo-dun.png new file mode 100644 index 00000000..1626ae5f Binary files /dev/null and b/mockups/canvases/dun-web/assets/art/logo-dun.png differ diff --git a/mockups/canvases/dun-web/assets/art/logo-fliso.png b/mockups/canvases/dun-web/assets/art/logo-fliso.png new file mode 100644 index 00000000..b15af2a4 Binary files /dev/null and b/mockups/canvases/dun-web/assets/art/logo-fliso.png differ diff --git a/mockups/canvases/dun-web/assets/art/logo-thisux.png b/mockups/canvases/dun-web/assets/art/logo-thisux.png new file mode 100644 index 00000000..0764077e Binary files /dev/null and b/mockups/canvases/dun-web/assets/art/logo-thisux.png differ diff --git a/mockups/canvases/dun-web/assets/art/proj-art.png b/mockups/canvases/dun-web/assets/art/proj-art.png new file mode 100644 index 00000000..e7070bc1 Binary files /dev/null and b/mockups/canvases/dun-web/assets/art/proj-art.png differ diff --git a/mockups/canvases/dun-web/assets/icons/archive-check.svg b/mockups/canvases/dun-web/assets/icons/archive-check.svg new file mode 100644 index 00000000..f9ef0c80 --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/archive-check.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/assets/icons/briefcase.svg b/mockups/canvases/dun-web/assets/icons/briefcase.svg new file mode 100644 index 00000000..13ebd184 --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/briefcase.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/assets/icons/check.svg b/mockups/canvases/dun-web/assets/icons/check.svg new file mode 100644 index 00000000..e67327d8 --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/assets/icons/circle-check.svg b/mockups/canvases/dun-web/assets/icons/circle-check.svg new file mode 100644 index 00000000..e7a4d845 --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/circle-check.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/assets/icons/circle-empty.svg b/mockups/canvases/dun-web/assets/icons/circle-empty.svg new file mode 100644 index 00000000..0a847ac6 --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/circle-empty.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/assets/icons/circle-half.svg b/mockups/canvases/dun-web/assets/icons/circle-half.svg new file mode 100644 index 00000000..cba4e696 --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/circle-half.svg @@ -0,0 +1 @@ + diff --git a/mockups/canvases/dun-web/assets/icons/corner-down-left.svg b/mockups/canvases/dun-web/assets/icons/corner-down-left.svg new file mode 100644 index 00000000..43973904 --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/corner-down-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/assets/icons/ellipsis.svg b/mockups/canvases/dun-web/assets/icons/ellipsis.svg new file mode 100644 index 00000000..8558adbb --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/ellipsis.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/assets/icons/eye.svg b/mockups/canvases/dun-web/assets/icons/eye.svg new file mode 100644 index 00000000..cb732317 --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/eye.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/assets/icons/megaphone.svg b/mockups/canvases/dun-web/assets/icons/megaphone.svg new file mode 100644 index 00000000..214599e8 --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/megaphone.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/assets/icons/pencil.svg b/mockups/canvases/dun-web/assets/icons/pencil.svg new file mode 100644 index 00000000..4ca2fdf8 --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/pencil.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/assets/icons/plus.svg b/mockups/canvases/dun-web/assets/icons/plus.svg new file mode 100644 index 00000000..d9c1200d --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/assets/icons/square-pen.svg b/mockups/canvases/dun-web/assets/icons/square-pen.svg new file mode 100644 index 00000000..e8b59641 --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/square-pen.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/assets/icons/square-terminal.svg b/mockups/canvases/dun-web/assets/icons/square-terminal.svg new file mode 100644 index 00000000..e5fbdc71 --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/square-terminal.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/assets/icons/trash.svg b/mockups/canvases/dun-web/assets/icons/trash.svg new file mode 100644 index 00000000..f49e05fe --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/trash.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/assets/icons/triangle-alert.svg b/mockups/canvases/dun-web/assets/icons/triangle-alert.svg new file mode 100644 index 00000000..1e17d77d --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/triangle-alert.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/assets/icons/x.svg b/mockups/canvases/dun-web/assets/icons/x.svg new file mode 100644 index 00000000..bfc06fc0 --- /dev/null +++ b/mockups/canvases/dun-web/assets/icons/x.svg @@ -0,0 +1,3 @@ + + + diff --git a/mockups/canvases/dun-web/crops.json b/mockups/canvases/dun-web/crops.json new file mode 100644 index 00000000..396f17f2 --- /dev/null +++ b/mockups/canvases/dun-web/crops.json @@ -0,0 +1,19 @@ +{ + "_": "Art cut from assets/refs/01.png at measured page boxes, [ref, x0, y0, x1, y1]. The capture is 1x, so these are design px too. gen.py's cut() writes assets/art/.png and art() places each at the same numbers, so an asset cannot drift from where it was measured. Everything else on the board -- cards, pills, panels, toolbars, type, line icons -- is CSS or inline SVG.", + + "_logos": "The three workspace marks in the Workspace panel. 60px circles on the panel's own near-white ramp, so each square crop carries 2 levels of that ramp with it and lands back on the same place.", + "logo-dun": ["01", 325, 233, 385, 293], + "logo-thisux": ["01", 325, 316, 385, 376], + "logo-fliso": ["01", 325, 400, 385, 460], + + "_proj": "The Projects folder and the three note cards fanned behind it. One crop, not four: the cards overlap the folder and each other, and the fan angles are not measurable off a 169px piece of art.", + "proj-art": ["01", 928, 49, 1103, 222], + + "_faces": "Two photographs. Nothing else on the board is a photograph, and a drawn stand-in for a face is the one substitution a reader always notices.", + "avatar-damia": ["01", 1557, 195, 1580, 218], + "avatar-task": ["01", 690, 798, 711, 819], + + "_cursors": "The two collaborator arrows. Their pills are CSS -- box and radius measure to a pixel -- but an arrow is a four-point polygon with four sharp vertices, and at 1x every one of them is antialiased below any threshold: fitting the edges off this capture put the James apex 4px above where the ink starts. The box is the arrow ink grown to the ringing fringe around it, so the crop also carries the one pixel of cyan overshoot the capture has and a render must not invent. Both boxes clip the pill the arrow points out of, so the art is drawn first and the pill lands back on top of it.", + "cur-james": ["01", 185, 272, 236, 318], + "cur-you": ["01", 439, 531, 489, 576] +} diff --git a/mockups/canvases/dun-web/gen.py b/mockups/canvases/dun-web/gen.py new file mode 100644 index 00000000..c1634edb --- /dev/null +++ b/mockups/canvases/dun-web/gen.py @@ -0,0 +1,938 @@ +"""Emit mockups/canvases/dun-web/ from one 1954 x 982 capture. + +The feature section of Dun's marketing page: eight pastel cards in a bento +grid, each with a handwritten label, a paragraph, and a piece of the product's +own UI inside it. One landscape artboard, not a phone, so `layout.json` gives +the board its own `w`/`h`. + +The capture is 1× -- it is a web page screenshot, not a device one -- so +capture px and design px are the same number and every measurement below goes +straight into the CSS with no scale in between. That is why there is no SCALE +constant here and why `crops.json` boxes are plain page coordinates. + +Chrome is CSS: cards, pills, panels, toolbars, type. Three things are not. +Photographs, the three brand marks and the two cursor arrows are cropped out +of the capture at their measured boxes (`crops.json` -> `cut()` -> `assets/art/.png` -> `art()`), +because a crop scores 0 against the reference by construction and a redrawn +one does not. Line icons are inline SVG from `assets/icons/`, authored on +lucide's 24 x 24 grid and placed at the measured ink box grown 2px a side, +which is lucide's own padding. + + python3 mockups/canvases/dun-web/gen.py + +Artboards are output. Edit this file, never the HTML. +""" +import base64 +import json +import re +from pathlib import Path + +OUT = Path(__file__).resolve().parent +REFS_DIR = OUT / "assets" / "refs" # third-party capture, not committed +ART_DIR = OUT / "assets" / "art" +ICON_DIR = OUT / "assets" / "icons" +CROPS = {k: v for k, v in json.loads((OUT / "crops.json").read_text()).items() + if not k.startswith("_")} + +NAME = "Dun" +PAGE_NAME = "(example) " + NAME + " Web" +P = "d" + +# ---------------------------------------------------------------- tokens ---- +# (group, name, value, evidence). Phase 2 order: font, then surface -> line -> +# ink -> accent, then radii, type, metrics. +TOKENS = [ + ("Font", "font", + '-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display",' + '"Helvetica Neue",Helvetica,Arial,sans-serif', + "refkit font on “Everything”: no call (SF Pro Rounded .665, SF Pro .629). " + "Real face is outside the macOS set; repo platform stack, width ratio in README"), + ("Font", "font-hand", + '"SignPainter","Bradley Hand","Brush Script MT",cursive', + "refkit font on the “Workspace” label: all candidates weak (Futura .383, " + "Brush Script .341). SignPainter closest on scratch/labelcmp2.png"), + + ("Surface", "bg", "#F2F2F2", "flat-fill census of the page ground, 100% flat"), + ("Surface", "c-ws", "#D8E2E8", "flat-fill census inside the Workspace card"), + ("Surface", "c-proj", "#F9D2B9", "flat-fill census inside the Projects card"), + ("Surface", "c-team", "#E1E6C4", "flat-fill census inside the Teams card"), + ("Surface", "c-docs", "#F6C8CE", "flat-fill census inside the Docs card"), + ("Surface", "c-comm", "#D3ECE8", "flat-fill census inside the Comments card"), + ("Surface", "c-subt", "#F9E9B5", "flat-fill census inside the Subtasks card"), + ("Surface", "c-task", "#DDDAED", "flat-fill census inside the Tasks card"), + ("Surface", "c-bulk", "#ECEFBF", "flat-fill census inside the Bulk actions card"), + ("Surface", "white", "#FFFFFF", "flat-fill census inside a Teams pill, 2859 px"), + ("Surface", "panel-top", "#F5F9FA", "refkit scan col 560, Workspace panel at its top edge"), + ("Surface", "panel-bot", "#EBF2F2", "refkit scan col 560, same panel at the card’s bottom"), + ("Surface", "chip", "#F6F6F6", + "refkit scan row 812, the Tasks row’s Research chip; the Docs segmented field " + "solves #F7F7F7, one level away, so it is the same token and not two"), + ("Surface", "dark", "#000000", + "the Save-doc and Mark-as-done pills are a gradient, not a fill: a plane fit over " + "each core, evaluated at the gradient line’s two ends, gives Mark-as-done " + "−5.1 → 78.5 on a 10.7° axis and Save doc −2.1 → 78.2 on " + "13.0°. Both dark ends extrapolate below zero, so the ramp starts at black: " + "starting the gradient line 6% before the box instead closes the last 0.2 levels " + "of the four gradient-only bands and is not worth a stop"), + ("Surface", "dark-2", "#4E4E4E", + "the same two fits’ light end, 78.5 and 78.2 of 255. The first pass read " + "#434343 by averaging the cores instead of fitting the plane, which flattened " + "the ramp from both directions at once"), + ("Surface", "box", "#3A3B38", "mode of a Bulk checkbox core, 40 px"), + ("Surface", "kbd", "#2B2B2B", "mode of the Ctrl and Enter key chips on the Bulk toolbar"), + ("Surface", "tip", "#444746", "mode of the Dun tooltip core, 357 px"), + ("Surface", "pill-grey", "#EEEEEE", "mode of the ThisUX tooltip core, 347 px"), + ("Surface", "plus", "#E6EAEB", "mode of the add-workspace circle core, 1599 px"), + ("Surface", "tile-fade", "rgba(255,255,255,.78)", + "the deepest faded row on Bulk and on Subtasks keeps its tile long after its " + "ink has gone: Bulk row 3’s tile solves .77 and the last Subtasks row’s " + ".78 in green and .77 in blue, against contents at .55 and .38. So the tile " + "alpha rides on the background and the opacity goes on a wrapper inside it"), + ("Surface", "ghost", "rgba(255,255,255,.42)", + "the stacked card behind the Tasks row reads #EBEAF3 over --x-c-task " + "(#DDDAED): alpha .41 on red and .43 on green. Blue has 18 levels of " + "headroom against red’s 34 and solves .33, which is the noise floor, " + "not a third colour"), + + ("Line", "ring", "rgba(255,255,255,.75)", + "1-2px near-white rim just outside every card fill (#F9FEFF/#FFFFFF); the ground " + "returns to #F2F2F2 at once, so there is no dark drop shadow to solve"), + ("Line", "hairline", "#ECECEC", "refkit scan row 527, the toolbar button outline"), + ("Line", "tree", "#E8D7A4", + "row scan at y614: the subtask connector is 1px wide and a tint of --x-c-subt, not a grey; a #D6D6D6 mask finds the DUN-NN text instead of the tree"), + + ("Ink", "i-ws", "#1F4256", "ink core, darkest 2%, Workspace label and body (they match)"), + ("Ink", "i-proj", "#724324", "ink core, Projects label and body"), + ("Ink", "i-team", "#203E14", "ink core, Teams label and body"), + ("Ink", "i-docs", "#5A2D4F", "ink core, Docs label and body"), + ("Ink", "i-comm", "#215C5C", "ink core, Comments label and body"), + ("Ink", "i-subt", "#51430D", "ink core, Subtasks label and body"), + ("Ink", "i-task", "#5C476A", "ink core, Tasks label and body"), + ("Ink", "i-bulk", "#727358", "ink core, Bulk label and lines 1-2; line 3 is the same ink at --x-o-text"), + ("Ink", "ink", "#050505", "ink core of a row title (Develop MVP, Competitor Analysis Research)"), + ("Ink", "ink-2", "#656765", "ink core of the commenter name and the Research chip label"), + ("Ink", "ink-3", "#B2B2B2", "ink core of a DUN-NN row id, two cards"), + ("Ink", "ink-4", "#D1D1D1", "ink core of the 2h timestamp"), + ("Ink", "ink-dim","#9BA0A0", + "ink core of the ThisUX (#9DA09F) and Fli.so (#A0A5A5) tooltip labels and of the " + "add-workspace plus glyph (#919596, only 19 px of ink)"), + ("Ink", "ink-inv","#FFFFFF", "ink core on the dark pills and the cursor labels"), + ("Ink", "doc-ink","#2A2A2A", "ink core of the doc title, Product Requirement Doc"), + ("Ink", "doc-head","#343434","ink core of a doc section heading"), + ("Ink", "doc-body","#777876","ink core of a doc paragraph"), + + ("Accent", "blue", "#3B82F6", "1.5px stroke; darkest px #488ED6, nearest Tailwind step to its hue"), + ("Accent", "amber", "#FBBF24", "filled half-circle, darkest px #FFBF28 = Tailwind amber-400"), + ("Accent", "orange", "#F59E0B", "1.5px stroke, terminal icon; darkest px #CB9744"), + ("Accent", "fuchsia", "#D946EF", "1.5px stroke, megaphone; darkest px #C435B0"), + ("Accent", "green", "#22C55E", "1.5px stroke, briefcase; darkest px #46AD66"), + ("Accent", "red", "#F87171", "1.5px stroke, warning triangle and the Research dot; darkest px #D4796C"), + ("Accent", "folder", "#FFDE77", "mode of the Projects folder core, 8842 px"), + ("Accent", "cur-blue","#3092FE", "mode of the James cursor core, 429 px"), + ("Accent", "cur-green","#1BB944","mode of the You cursor core, 334 px"), + + ("Radius", "r-card", "36px", + "circle fit to where the fill starts on each row of the Tasks card’s " + "bottom-left corner: r 39.5 at rms 0.6, against 30.0 for my own 28px corner, " + "so the fit runs 2 levels of threshold wide and the reference is 37.5. A sweep " + "of the 32 card corners bottoms out at 36, and the same sweep confirms r-panel, " + "r-tile and r-cur where they already stood. A bbox on one corner crop read 28, " + "which is the corner’s chord, not its radius"), + ("Radius", "r-panel", "24px", "the Workspace and Docs panels’ one visible corner"), + ("Radius", "r-tip", "14px", "the Dun tooltip corner, and the Docs toolbar’s"), + ("Radius", "r-tile", "20px", "the Comments tile corner"), + ("Radius", "r-row", "18px", "solved on a Subtasks row, the Tasks pill and the Bulk toolbar; " + "the Tasks pill is 55px tall, so 18 is a real radius and not h/2"), + ("Radius", "r-box", "8px", "a Bulk checkbox and a toolbar button corner"), + ("Radius", "r-chip", "8px", "the Research chip corner"), + ("Radius", "r-cur", "16px", "the James and You label pills, 46 and 47px tall,\n so 16 is a real radius and not a full pill"), + ("Radius", "r-pill", "999px", "solved ≥ h/2 on the four Teams pills, so they are full pills"), + + # Sizes are solved from the measured **cap height** at SF Pro's .714 cap ratio, + # then cross-checked against the string's measured ink width. Where the two + # disagree the cap height wins, because the stand-in is not the real face: + # SF Pro sets the same cap 4-19% wide here (see README). A width that then + # overruns a measured container widens the container, never the type. + ("Type", "t-label", "italic 700 25px/25px var(--x-font-hand)", + "ink top is card + 31 on all eight cards. Size is calibrated, not solved: " + "SignPainter is a substitution, so its 21px ink was measured against all eight " + "reference labels (81/64/44/39/79/69/41/96 px wide) and each asks for 23.7-26.4"), + ("Type", "t-body", "400 18px/28px var(--x-font)", + "body cap 13px → 18.2; line 1 ink 402px wide on Workspace → 17-18px. " + "Line 1 ink top is card + 65 and the pitch is 28 on all eight cards"), + ("Type", "t-pill", "400 19px/24px var(--x-font)", + "“Engineering” cap 14px → 19.6; ink 95px wide → 18px"), + ("Type", "t-row", "600 16px/20px var(--x-font)", + "“Competitor Analysis Research” cap 12px → 16.8; ink 212px wide " + "→ 14px. The widest disagreement on the board: SF Pro Semibold sets it +14%"), + ("Type", "t-row-id", "400 16px/20px var(--x-font)", + "“DUN-20” cap 12px → 16.8; ink 58px wide → 15px"), + ("Type", "t-chip", "400 13px/16px var(--x-font)", + "“Research” cap 10px → 14; ink 55px wide → 12-13px"), + ("Type", "t-btn", "600 16px/20px var(--x-font)", + "“3 Selected” cap 12px → 16.8, ink 75px → 14px; " + "“Mark as done” ink 121px → 18px. White-on-dark ink measures " + "dilated and dark-on-white eroded, so the two straddle 16"), + ("Type", "t-btn-s", "600 13px/17px var(--x-font)", + "“Save doc” cap 10px → 14, ink 57px → 12px. The Docs " + "toolbar is a smaller instance of the Bulk one (252 × 51 vs 528 × 56)"), + ("Type", "t-kbd", "500 11px/14px var(--x-font)", + "“Ctrl” ink is 18 × 8 on the Docs plate and 17 × 9 on the " + "Bulk one — the same size within a pixel, so one token, not two"), + ("Type", "t-tip", "600 21px/26px var(--x-font)", + "cap 16/16/15px and ink 38/72/48px wide on Dun / ThisUX / Fli.so; 21px satisfies " + "both on all three, the cleanest agreement on the board"), + ("Type", "t-cursor", "600 18px/22px var(--x-font)", + "“James” cap 14px → 19.6, ink 58px → 18px; “You” " + "cap 13px → 18.2"), + ("Type", "t-doc-t", "500 18px/22px var(--x-font)", + "“Product Requirement Doc” ink is 201 × 18, and cap + descender " + "is .9386em, so 18px. Weight from ink mass: the reference carries 1.125× " + "this string at 15px/700, where the same coverage sum on type that already " + "matches runs 1.00-1.23, which puts it near 500 rather than 700"), + ("Type", "t-doc-h", "500 11px/14px var(--x-font)", + "“Overview” ink 50px and “Description” 60px, both 11px; " + "700 sets Description 3px over and carries 1.1× the reference's ink mass"), + ("Type", "t-doc-b", "400 8px/11px var(--x-font)", + "the first doc paragraph is 222px of ink → 8px, and 8px ascender-to-descender " + "→ 8px; the measured pitch inside a paragraph is 11"), + ("Type", "t-comm-n", "600 13px/16px var(--x-font)", + "“Damia” cap 10px → 14, ink 34px → 10px; 13 splits them"), + ("Type", "t-comm-b", "400 12px/16px var(--x-font)", + "“The font size feels too small. Maybe we” cap 9px → 12.6, ink " + "220px → 11-12px; the measured line pitch is 16"), + + ("Metrics", "w", "1954px", "the capture’s own width; the board is the section, 1x"), + ("Metrics", "h", "982px", "the capture’s own height"), + ("Metrics", "pad", "27px", "refkit bands: body ink at card left + 27 on all eight cards"), + ("Metrics", "label-top", "31px", "refkit bands: label ink top, card + 30 to + 33"), + ("Metrics", "body-top", "65px", "refkit bands: body line 1 ink top, card + 65 to + 67"), + ("Metrics", "gap", "21px", "column gap in band 1: 595→616, 1136→1157, 1497→1518"), + ("Metrics", "row-pitch", "85px", "refkit bands pitch on the five Subtasks rows"), + ("Metrics", "bulk-pitch", "66px", "refkit bands pitch on the four Bulk actions rows"), + ("Metrics", "o-text", ".82", + "Bulk’s third body line reads #878A6B where lines 1-2 read #727358: the same " + "ink at .82 over #ECEFBF solves 136, the measured 135"), + ("Metrics", "o-bulk-2", ".94", + "both row lists fade toward the bottom of their card, and a row’s own " + "opacity comes off the contrast between its title ink and the tile right above " + "that ink, divided by row 1’s: a ratio that cancels the right-edge fade, " + "which a fill sample does not. Bulk row 2 reads .935 on all three channels"), + ("Metrics", "o-bulk-3", ".55", + "Bulk row 3, .544/.540/.559. Its checkbox solves .57 instead, because it sits " + "3px above the toolbar inside the white ringing along that edge"), + ("Metrics", "o-bulk-4", ".04", + "Bulk row 4, .040/.037/.040, and its tile is 2 levels off the card fill in " + "blue and level with it in red and green"), + ("Metrics", "rot-pill", ".56deg", + "the Tasks tile is the one rotated box on the board: its top edge runs " + "774.70 at x 160 to 779.55 at x 700 and its bottom 831.53 to 836.33, two " + "independent fits on +0.0097 rad. Every other tile, panel and toolbar here " + "is flat to a third of a pixel"), + ("Metrics", "rot-ghost", "-1.04deg", + "the stack behind it turns the other way: its one visible edge is 859.25 at " + "x 200 and 850.88 at x 660, −0.0182 rad"), + ("Metrics", "o-subt-4", ".93", + "Subtasks row 4, .928 off its row id at x 1684. Its title reads .839 instead, " + "and the difference is the fade: the titles on rows 3 and 4 are indented to " + "x 1761 where the row ids and row 1 are not, and a ratio only cancels the " + "fade where both windows sit at the same x. Row 3 reads .99, so it is not " + "faded at all"), + ("Metrics", "o-subt-5", ".38", + "the last Subtasks row, .381/.389/.416 off its id and .33 off its title. The " + "two cards do not share one ramp: this row is 56px off its card’s bottom " + "and Bulk’s fourth is 46, and Bulk’s has all but gone"), +] + + +def _root(): + """One :root block, byte-identical in every board. No `}` inside it: + refkit tokens reads it with a non-greedy regex.""" + out, seen = [":root{"], None + for group, name, value, _ in TOKENS: + if group != seen: + out.append("" if seen else None) + out.append(" /* %s */" % group) + seen = group + out.append(" --x-%s:%s;" % (name, value)) + return "\n".join(x for x in out if x is not None) + "\n}" + + +TOKENS_CSS = _root() + +# ------------------------------------------------------------------ art ---- +def cut(): + """Refresh assets/art/ from assets/refs/ at the boxes in crops.json.""" + if not REFS_DIR.exists(): + return + from PIL import Image # noqa: local dep + ART_DIR.mkdir(parents=True, exist_ok=True) + src, n = {}, 0 + for cid, (ref, x0, y0, x1, y1) in CROPS.items(): + f = REFS_DIR / (ref + ".png") + if not f.exists(): + continue + if ref not in src: + src[ref] = Image.open(f).convert("RGB") + src[ref].crop((x0, y0, x1, y1)).save(ART_DIR / (cid + ".png"), optimize=True) + n += 1 + print("%-24s %6d crops" % ("assets/art/", n)) + + +def app_icon(): + """icon.png, the sticker the welcome card wears: the Dun mark, which the + capture holds at 60px and nowhere larger. The crop is the mark's own bbox + -- the disc spans x 0..59 across its middle rows and 21..38 at the top, so + it is a circle of r 30 at (29.5, 29.5) -- so the alpha is that circle + scaled to 128, supersampled 4x, and the mark itself is a 4.27x LANCZOS + upscale. It is soft at 256 and there is no sharper source; the alternative + was redrawing the loop, which would make the sticker the one thing in this + folder that is not the reference's pixels. Keeps the committed file when + Pillow is missing. + """ + if not (ART_DIR / "logo-dun.png").exists(): + return + try: + from PIL import Image, PngImagePlugin # noqa: local dep + import numpy as np + except ImportError: + print("icon.png kept: Pillow not installed") + return + N, ss = 256, 4 + yy, xx = np.mgrid[0:N * ss, 0:N * ss] + .5 + disc = ((xx - N * ss / 2) ** 2 + (yy - N * ss / 2) ** 2 <= (N * ss / 2) ** 2) + mask = disc.reshape(N, ss, N, ss).mean((1, 3)) + im = Image.open(ART_DIR / "logo-dun.png").convert("RGB").resize((N, N), Image.LANCZOS) + im.putalpha(Image.fromarray(np.round(mask * 255).astype(np.uint8))) + # Every icon.png in mockups/canvases states its provenance in a PNG + # Source chunk; this one's is a crop of the capture rather than store art. + meta = PngImagePlugin.PngInfo() + meta.add_text("Source", "assets/art/logo-dun.png, crops.json logo-dun " + "(page 325,233 60 x 60), LANCZOS to 256") + im.save(OUT / "icon.png", pnginfo=meta) + print("%-24s %6s" % ("icon.png", "%dx%d" % im.size)) + + +def _uri(cid): + f = ART_DIR / (cid + ".png") + return ("data:image/png;base64," + base64.b64encode(f.read_bytes()).decode() + if f.exists() else "") + + +def art(cid, ox, oy, extra=""): + """One , placed at the box it was measured from, relative to the card + it belongs to: every card is a positioned box at its own page offset + (ox, oy), so the crop's page box minus that offset is where it goes.""" + _, x0, y0, x1, y1 = CROPS[cid] + return ('%s' + % (cid, _uri(cid), x0 - ox, y0 - oy, x1 - x0, y1 - y0, extra)) + + +# Every icon's ink box on its own 24 x 24 grid, measured by rendering the whole +# set at 10x (scratch/icongrid.html) and reading each cell's ink. Lucide's +# nominal 2..22 inset is not what a stroked path covers -- several of these +# reach 1..23, and the ellipsis covers 18 x 4 -- so the viewBox is set to the +# measured box and the span to the measured px box. Scale is then 1:1 on both +# axes with nothing to converge. The default xMidYMid meet binds one axis only, +# which is how a 16 x 4 ellipsis renders at a quarter of its size and a size +# loop never notices. +GRID_INK = { + "archive-check": (1, 2, 22, 20), + "briefcase": (1, 1, 22, 21), + "check": (3, 5, 18, 13), + "circle-check": (1, 1, 22, 22), + "circle-empty": (1, 1, 22, 22), + "circle-half": (0, 0, 24, 24), + "corner-down-left": (3, 3, 18, 18), + "ellipsis": (3, 10, 18, 4), + "eye": (1, 3.5, 22, 17), + "megaphone": (2, 2.5, 19.5, 19.3), + "pencil": (1, 0, 22.1, 23), + "plus": (4, 4, 16, 16), + "square-pen": (2, 0.9, 21.1, 21.1), + "square-terminal": (2, 2, 20, 20), + "trash": (2, 1, 20, 22), + "triangle-alert": (1, 2, 22, 20), + "x": (5, 5, 14, 14), +} + + +def icon(name, x, y, w, colour="currentColor", h=None): + """Inline assets/icons/.svg at a measured ink box, in card + coordinates. (x, y, w, h) is the ink, not the 24-grid.""" + svg = (ICON_DIR / (name + ".svg")).read_text().strip().replace("\n", "") + return (svg + .replace('viewBox="0 0 24 24"', + 'viewBox="%g %g %g %g"' % GRID_INK[name], 1) + .replace("\n\n\n\n' + '%s\n\n\n\n%s\n\n\n' + % (title, TOKENS_CSS, BASE, extra_css, body)) + return html.replace("--x-", "--%s-" % P) + + +def write(name, html): + (OUT / (name + ".html")).write_text(html) + print("%-24s %6d bytes" % (name, len(html))) + + +# --------------------------------------------------- foundations boards ---- +SHEET = """body{color:var(--x-ink)} +.sheet{width:478px;height:980px;padding:20px;overflow:hidden} +h1{font:600 17px/22px var(--x-font);margin-bottom:2px} +header p{font:400 11px/15px var(--x-font);color:var(--x-ink-2);margin-bottom:14px} +h2{font:600 9px/12px var(--x-font);letter-spacing:.8px;text-transform:uppercase; + color:var(--x-ink-2);margin:12px 0 5px} +.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:6px} +.sw .chip{height:22px;border-radius:6px;border:1px solid var(--x-hairline)} +.sw b{display:block;margin-top:3px;font:600 8.5px/11px ui-monospace,Menlo,monospace} +.sw i{display:block;font:400 8px/11px ui-monospace,Menlo,monospace; + color:var(--x-ink-2);font-style:normal;word-break:break-all} +.rad{display:flex;flex-wrap:wrap;gap:9px} +.rb{flex:none;width:44px;height:26px;background:var(--x-c-ws); + border:1px solid var(--x-hairline)} +.rad em{display:block;margin-top:2px;font:400 8.5px/11px var(--x-font); + color:var(--x-ink-2);font-style:normal;text-align:center} +.tr{display:flex;align-items:baseline;justify-content:space-between;gap:10px; + padding-bottom:2px;border-bottom:1px solid var(--x-hairline)} +.tr span{white-space:nowrap;overflow:hidden} +.tr em{font:400 8px/11px ui-monospace,Menlo,monospace;color:var(--x-ink-2); + font-style:normal;white-space:nowrap;flex:none} +.met{font:400 9px/13px ui-monospace,Menlo,monospace;color:var(--x-ink-2)} +table.ev{width:100%;border-collapse:collapse} +table.ev td{vertical-align:top;padding:2.5px 6px 2.5px 0; + border-bottom:1px solid var(--x-hairline);font:400 8.5px/11px var(--x-font)} +td.t,td.v{font-family:ui-monospace,Menlo,monospace} +td.t{color:var(--x-i-ws);white-space:nowrap} +td.v{color:var(--x-ink-2);max-width:132px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +td.e{color:var(--x-doc-body)}""" + + +def _of(group): + return [t for t in TOKENS if t[0] == group] + + +def token_board(): + swatches = "".join( + '
' + '--x-%s%s
' % (n, n, v) + for g in ("Surface", "Line", "Ink", "Accent") for _, n, v, _ in _of(g)) + radii = "".join( + '
%s
' % (v, v) + for _, n, v, _ in _of("Radius")) + return page(NAME + " - Design Tokens", + '

%s — web feature section

' + '

Measured off one 1954 × 982 capture at 1×, so every ' + 'number here is also a design px. The faces and the geometry are on ' + 'the next board, the evidence for every value on the ones after ' + 'it.

' + '

Colour

%s
' + '

Radius

%s
' + % (NAME, swatches, radii), SHEET) + + +# 51 swatches and 16 specimens do not fit one 980px sheet: the two together +# overflow it by 573px, which the iframe would have clipped in silence. +def type_board(): + type_ = "".join( + '
Everything starts' + '--x-%s · %s
' % (n, n, v.split(" var")[0]) + for _, n, v, _ in _of("Type")) + met = "
".join("--x-%s: %s" % (n, v) for _, n, v, _ in _of("Metrics")) + return page(NAME + " - Type and metrics", + '

Type and metrics

' + '

One composite font: per role. The hand face is a ' + 'substitution and sets narrower than the reference’s, so its ' + 'size is the one fitted by rendering rather than read off the ' + 'capture.

' + '

Type

%s' + '

Metrics

%s
' + % (type_, met), SHEET) + + +# An evidence page is filled to a height, not to a row count. This folder's +# prose runs from 20 to 300 characters, so a fixed 40 rows a board overflowed +# two boards by 204 and 270px while leaving the third half empty. The evidence +# column is 222px wide, which holds about 52 characters of 8.5px SF; a row is +# 11px a line plus 6 of padding and rule. The sheet leaves 900px of table under +# its header and the split fills 880 of it, because the estimate rounds a row +# that wraps one word past the line down rather than up. +EV_H, EV_CH = 880, 52 + + +def _rendered_len(s): + """Characters as the browser draws them: `’` is one glyph, not seven.""" + return len(re.sub(r"&[#0-9a-zA-Z]+;", "x", s)) + + +def evidence_boards(): + """The evidence table, split across as many boards as it needs. It is the + deliverable of Phase 1: trim the board count, never the rows.""" + rows = [(t, 6 + 11 * -(-_rendered_len(t[3]) // EV_CH)) for t in TOKENS] + total = sum(h for _, h in rows) + n = -(-total // EV_H) # boards the table needs... + pages, used = [[]], 0 + for t, h in rows: + if used + h > total / n and len(pages) < n: # ...filled evenly, so the + pages.append([]) # last one is not a stub + used = 0 + pages[-1].append(t) + used += h + for i, chunk in enumerate(pages): + rows = "".join( + '--x-%s%s%s' + % (n, v, e) for _, n, v, e in chunk) + of = " %d/%d" % (i + 1, len(pages)) if len(pages) > 1 else "" + yield page(NAME + " - Evidence" + of, + '

Evidence%s

' + '

One row per token. A token with no evidence is a guess. ' + 'The replayable half is probes.json.

' + '
%s
' % (of, rows), SHEET) + + +def foundation_boards(): + """(file, html) for the Foundations row, lettered in order after 00.""" + boards = [("design-tokens", token_board()), ("type", type_board())] + boards += [("evidence", html) for html in evidence_boards()] + return [("00%s-%s" % ("bcdefgh"[i - 1] if i else "", slug), html) + for i, (slug, html) in enumerate(boards)] + + +# ----------------------------------------------------- the feature section ---- +# Text is positioned by its measured *ink top*, because that is what a capture +# can be measured for: a line box's top is invisible. SF Pro's ascent is +# .9552em and its descent .2246em, so the content box is 1.1798em centred in +# the line box, and the cap top sits .714em below the ascent line. cap_top() +# turns a token into that offset once, and tx() subtracts it. +ASC, DESC, CAP = 0.9552, 0.2246, 0.714 +_VAL = {n: v for _, n, v, _ in TOKENS} + +# The hand face is a substitution, so its own ascent and cap ratio are not SF +# Pro's and cap_top() cannot place it. This is the residual measured in Phase 4 +# against the eight labels' ink tops: the reference sets its caps 30-33px below +# each card's top depending on the letter, SignPainter sets the same eight at +# 29-30, and +3 is the shift with the smallest total error. It leaves the two +# round caps -- Docs' D and Comments' C -- 2-3px low, which is the face rather +# than the placement and cannot be fixed by moving the line. +LABEL_DY = 3.0 + + +def cap_top(tok): + fs, lh = (float(n[:-2]) for n in + _VAL[tok].split(" var")[0].split()[-1].split("/")) + return (lh - (ASC + DESC) * fs) / 2 + (ASC - CAP) * fs + + +def tx(tok, x, y, text, colour=None, extra=""): + """A line (or a
-joined block) whose first line's cap top lands on y.""" + return ('
%s
' + % (x, y - cap_top(tok), tok, + ";color:var(--x-%s)" % colour if colour else "", extra, text)) + + +def box(x, y, w, h, css=""): + return ('
' + % (x, y, w, h, css)) + + +def tile(x, y, w, css=""): + """One 55px white row tile, left open for its contents. Every row on + Subtasks, Bulk and Tasks is one of these.""" + return box(x, y, w, 55, css).replace('class="b"', 'class="b tile"') + + +GRID = """.board{position:relative;width:var(--x-w);height:var(--x-h);overflow:hidden} +.c{position:absolute;overflow:hidden;border-radius:var(--x-r-card); + box-shadow:0 0 0 1.5px var(--x-ring)} +.b,.t,.a,.i{position:absolute} +/* Every line's break is a measured fact, so
is the only one allowed: the + reference runs its doc lines straight off the panel and clips them. */ +.t{white-space:pre} +.a,.i{display:block} +.ctr{display:flex;align-items:center;justify-content:center} +.tile{background:var(--x-white);border-radius:var(--x-r-row);height:55px} +.plate{background:var(--x-kbd);border-radius:6px;color:var(--x-ink-inv); + font:var(--x-t-kbd)} +.obtn{background:var(--x-white);border:1px solid var(--x-hairline);border-radius:10px}""" + +# key, page x/y/w/h, fill token, ink token, label, label ink top, body ink top, +# body lines. Every card shares one rhythm -- label ink at card + 31, body line +# 1 at card + 65, pitch 28, left pad 27 -- except Projects, whose text is +# bottom-aligned under its illustration. +CARDS = [ + ("ws", 75, 24, 520, 582, "c-ws", "i-ws", "Workspace", 31, 65, + ("Everything starts with a clean workspace. Nothing", + "cluttered, nothing distracting.")), + ("proj", 616, 24, 521, 281, "c-proj", "i-proj", "Projects", 143, 177, + ("Create projects to keep things", "organized. Big or small, work", + "how you like")), + ("team", 616, 326, 521, 280, "c-team", "i-team", "Teams", 31, 65, + ("Your team’s in too. Everyone has their own space, work", + "together & stay in sync")), + ("docs", 1157, 24, 340, 582, "c-docs", "i-docs", "Docs", 31, 65, + ("Docs, right next to your tasks. So", "no hopping between apps")), + ("comm", 1518, 24, 340, 283, "c-comm", "i-comm", "Comments", 31, 65, + ("Need to discuss? Just comment on", "a task, it all stay in one place")), + ("subt", 1518, 328, 340, 596, "c-subt", "i-subt", "Subtasks", 31, 65, + ("Big task? Break it down and tackle", "bit by bit with subtasks")), + ("task", 75, 634, 700, 290, "c-task", "i-task", "Tasks", 31, 65, + ("Adding tasks is simple, instant and feels as quick as writing on a sticky note",)), + ("bulk", 797, 634, 700, 290, "c-bulk", "i-bulk", "Bulk actions", 31, 65, + ("And when there’s a lot to handle at once, you", + "can select tasks and apply changes in bulk,", + "all at once")), +] + + +def workspace(): + """Three brand marks stacked on a panel, each with its hover tooltip, and + two collaborator cursors. The panel is declared 340 x 460 inside a 520 x + 582 card so its right and bottom corners fall outside and come out square, + which is what the capture shows.""" + o = [box(214, 175, 340, 460, + ";border-radius:var(--x-r-panel);background:linear-gradient(180deg," + "var(--x-panel-top) 0,var(--x-panel-bot) 407px)") + "
"] + o += [art(cid, 75, 24) for cid in ("logo-dun", "logo-thisux", "logo-fliso")] + o.append(box(249, 460, 60, 60, + ";border-radius:var(--x-r-pill);background:var(--x-plus)") + "") + o.append(icon("plus", 263, 475, 31, "var(--x-ink-dim)")) + # Only the dark tooltip has a pointer. A column profile 6px left of either + # grey one is a smooth 244 -> 237 antialias ramp with no notch in it; the + # dark one's is a 7px triangle whose dark rows grow 2, 4, 6, 8, 10, 12, 16. + o.append(box(331, 230.5, 0, 0, + ";border-top:8px solid transparent;border-bottom:8px solid transparent" + ";border-right:7px solid var(--x-tip)") + "") + for x, y, w, h, bg, ink, s in ( + (338, 217, 73, 42, "tip", "ink-inv", "Dun"), + (338, 301, 101, 42, "pill-grey", "ink-dim", "ThisUX"), + (338, 384, 81, 41, "pill-grey", "ink-dim", "Fli.so")): + o.append(box(x, y, w, h, ";border-radius:var(--x-r-tip);background:var(--x-%s)" + ";font:var(--x-t-tip);color:var(--x-%s)" % (bg, ink)) + .replace('class="b"', 'class="b ctr"') + s + "") + # Neither pill has a white ring. The one bright pixel around each is webp + # chroma overshoot -- #CEFFFF, whose red is *below* the card fill's -- and a + # real 2px ring reads #FFFFFF twice over. The arrows are crops for the reason + # in crops.json, and each is drawn before its pill because the crop box + # clips the pill's corner. + for cid, px, py, pw, ph, col, lab in ( + ("cur-james", 32, 222, 89, 46, "cur-blue", "James"), + ("cur-you", 405, 535, 62, 47, "cur-green", "You")): + o.append(art(cid, 75, 24)) + o.append(box(px, py, pw, ph, ";border-radius:var(--x-r-cur)" + ";background:var(--x-%s);font:var(--x-t-cursor)" + ";color:var(--x-ink-inv)" % col) + .replace('class="b"', 'class="b ctr"') + lab + "") + return "".join(o) + + +# left, top, w, h | icon name, ink x/y/w/h, colour | label box x / ink y, label +# The label x is the text box, not the ink: the reference's four labels start +# their ink at card 152 / 296 / 200 / 361, and SF Pro at 19px carries a 2px left +# side bearing on D, E and M and 1px on S, so the box goes that much earlier. +# Nowhere else on the board does the bearing round to more than a pixel. +TEAM_PILLS = ( + (99, 138, 127, 52, "square-pen", 113, 154, 21, 21, "blue", 150, 157, "Design"), + (242, 138, 176, 52, "square-terminal", 256, 152, 22, 22, "orange", 294, 158, "Engineering"), + (147, 205, 152, 52, "megaphone", 161, 221, 21, 18, "fuchsia", 198, 224, "Marketing"), + (315, 205, 108, 52, "briefcase", 329, 220, 21, 22, "green", 360, 224, "Sales"), +) + + +def teams(): + o = [] + for x, y, w, h, ic, ix, iy, iw, ih, col, lx, ly, lab in TEAM_PILLS: + o.append(box(x, y, w, h, ";border-radius:var(--x-r-pill)" + ";background:var(--x-white)") + "") + o.append(icon(ic, ix, iy, iw, "var(--x-%s)" % col, ih)) + o.append(tx("t-pill", lx, ly, lab, "ink")) + return "".join(o) + + +# ink top (card px), token, text. The doc runs off the bottom of the card and +# under the toolbar, so the last lines are what is legible and no more: the +# heading between the KPI bullets and the closing paragraph is entirely behind +# the toolbar, and inventing it would be inventing a fact. +DOC = [ + (191, "t-doc-t", "doc-ink", "Product Requirement Doc"), + (226, "t-doc-h", "doc-head", "Overview"), + (250, "t-doc-b", "doc-body", + "A simple todo app to add, manage, and complete daily tasks."), + (272, "t-doc-h", "doc-head", "Description"), + (296, "t-doc-b", "doc-body", + "The app helps people stay organized by letting them quickly jot down tasks,
" + "group them by projects, mark them done, and set reminders."), + (328, "t-doc-b", "doc-body", + "It’s fast, minimal, and easy to use — works well solo or with a " + "small team. Think
of it like a digital sticky note with superpowers."), + (364, "t-doc-h", "doc-head", "Why"), + (388, "t-doc-b", "doc-body", + "Most task apps are too heavy or too confusing for regular folks. People just
" + "want to get stuff out of their heads and come back to it when they’re ready."), + (421, "t-doc-b", "doc-body", + "We’re solving this because staying organized shouldn’t feel like a job by"), + (446, "t-doc-h", "doc-head", "KPIs"), + (551, "t-doc-b", "doc-body", + "them via Twitter, Product Hunt, and design communities with a simple
" + "promise: “Get things done without the fluff.”"), +] +BULLETS = ("User adds at least 1 task per day for 7 straight days", + "At least 40% of tasks marked “done”", + "30% users use more than one list/project") + + +def docs(): + """A product doc scrolled under a floating toolbar. The panel is declared + past the card's right and bottom edges for the same reason Workspace's is, + and the text lines are declared at their full length and clipped there -- + which is how the capture reads, mid-sentence at the card edge.""" + o = [box(54, 168, 340, 460, ";border-radius:var(--x-r-panel)" + ";background:var(--x-white)") + ""] + o += [tx(tok, 73, y, s, col) for y, tok, col, s in DOC] + for i, s in enumerate(BULLETS): + o.append(box(77, 473 + 11 * i, 4, 4, + ";border-radius:var(--x-r-pill);background:var(--x-doc-body)") + + "") + o.append(tx("t-doc-b", 86, 470 + 11 * i, s, "doc-body")) + # The line whose head is behind the toolbar is written as the tail alone, + # positioned so its ink starts where the capture's does: the toolbar covers + # the first glyph either way, and the words in front of "nk B2C" are not + # recoverable from the pixels. + o.append(tx("t-doc-b", 278, 539, "nk B2C. We’ll targ", "doc-body")) + o.append(box(54, 168, 340, 600, ";background:linear-gradient(180deg,transparent " + "268px,var(--x-c-docs) 578px)") + "") + o.append(box(29, 498, 252, 51, ";border-radius:var(--x-r-tip)" + ";background:var(--x-white)") + "") + o.append(box(37, 506, 72, 34, ";border-radius:10px;background:var(--x-chip)") + "") + o.append(box(39, 508, 33, 30, ";border-radius:var(--x-r-box)" + ";background:var(--x-white)") + "") + o.append(icon("pencil", 46, 513, 19, "var(--x-ink-2)", 20)) + o.append(icon("eye", 80, 515, 19, "var(--x-ink-2)", 16)) + o.append(box(121, 506, 152, 34, ";border-radius:17px;background:linear-gradient(" + "12deg,var(--x-dark),var(--x-dark-2))") + "") + o.append(tx("t-btn-s", 138, 518, "Save doc", "ink-inv")) + o.append(box(209, 512, 27, 23, "").replace('class="b"', 'class="b ctr plate"') + + "Ctrl") + o.append(box(242, 512, 21, 23, "").replace('class="b"', 'class="b plate"') + "") + o.append(icon("corner-down-left", 249, 520, 9, "var(--x-ink-inv)", 6)) + return "".join(o) + + +def comments(): + o = [box(26, 156, 286, 82, ";border-radius:var(--x-r-tile)" + ";background:var(--x-white)") + "", + art("avatar-damia", 1518, 24, ";border-radius:var(--x-r-pill)"), + tx("t-comm-n", 70, 174, "Damia", "ink-2"), + tx("t-comm-n", 286, 175, "2h", "ink-4"), + tx("t-comm-b", 69, 194, + "The font size feels too small. Maybe we
" + "could bump it up a bit for readability", "ink")] + return "".join(o) + + +# top, left, icon name, icon colour, id, title, contents opacity token, and +# whether the tile under those contents holds --x-tile-fade instead of fading +# with them. The rows are indented by their depth and run off the card's right edge, which is why they +# are declared wider than the card: 37 + 400 is past 340 on every one of them. +SUBT_ROWS = ( + (171, 37, "circle-half", "amber", "DUN-20", "Develop MVP", None, False), + (258, 79, "circle-check", "blue", "DUN-21", "Setup everything", None, False), + (343, 120, "circle-check", "blue", "DUN-22", "Setup Database", None, False), + (427, 120, "circle-check", "blue", "DUN-23", "Create GitHub", "o-subt-4", False), + (512, 79, "circle-empty", "ink-3", "DUN-24", "Implement Auth", "o-subt-5", True), +) +# x, y0, y1 verticals and y, x0, x1 stubs of the 1px connector tree, measured +# as a tint of the card fill rather than a grey. Vertical A hangs off row 1 and +# feeds rows 2 and 5; vertical B hangs off row 2 and feeds rows 3 and 4. +TREE_V = ((50, 228, 539), (91, 313, 458)) +TREE_H = ((285, 50, 76), (539, 50, 76), (374, 91, 117), (458, 91, 117)) + + +def subtasks(): + o = [] + for x, y0, y1 in TREE_V: + o.append(box(x, y0, 1, y1 - y0, ";background:var(--x-tree)") + "") + for y, x0, x1 in TREE_H: + o.append(box(x0, y, x1 - x0, 1, ";background:var(--x-tree)") + "") + for y, x, ic, col, rid, title, fade, tile_fade in SUBT_ROWS: + dim = ";opacity:var(--x-%s)" % fade if fade else "" + o.append((tile(x, y, 400, ";background:var(--x-tile-fade)") + "" + + box(x, y, 400, 55, dim) if tile_fade + else tile(x, y, 400, dim)) + + icon(ic, 14, 18, 20, "var(--x-%s)" % col) + + tx("t-row-id", 46, 23, rid, "ink-3") + + tx("t-row", 123, 22, title, "ink") + "") + o.append(box(0, 0, 560, 596, ";background:linear-gradient(90deg,transparent 246px," + "var(--x-c-subt) 521px)") + "") + return "".join(o) + + +def tasks(): + """One task row with the next peeking out from under it, and the only two + rotated boxes on the board -- see --x-rot-pill and --x-rot-ghost, which is + the card's own joke about writing on a sticky note. + + The contents do not turn with the tile. The status glyph's ink top is 794 + at x 143 and the warning triangle's is 798 at x 654, which is the flat + model at both ends; a rotation about the tile's centre would put them 2.5px + high and 2.1px low respectively. So the tile is its own box behind an + unrotated content layer, and the tile's own numbers come off its four + edges: 126.5 -> 723.5 by 777.2 -> 834.3 around a centre at (425, 805.7), + which is 597 x 57 rather than the 598 x 55 the rows on the other cards use. + """ + return "".join([ + tile(81.3, 166.2, 536, ";background:var(--x-ghost)" + ";transform:rotate(var(--x-rot-ghost))") + "", + tile(51.5, 143.2, 597, ";height:57px" + ";transform:rotate(var(--x-rot-pill))") + "", + box(52, 141, 598, 55, "") + + icon("circle-half", 15, 19, 19, "var(--x-amber)") + + tx("t-row-id", 47, 24, "DUN-20", "ink-3") + + tx("t-row", 123, 24, "Competitor Analysis Research", "ink") + + box(424, 17, 89, 31, ";border-radius:var(--x-r-chip)" + ";background:var(--x-chip)") + "" + + box(433, 30, 5, 5, ";border-radius:var(--x-r-pill)" + ";background:var(--x-red)") + "" + + tx("t-chip", 448, 27, "Research", "ink-2") + + icon("triangle-alert", 528, 23, 21, "var(--x-red)", 20) + + art("avatar-task", 127, 775, ";border-radius:var(--x-r-pill)") + + "", + ]) + + +# top, icon name, icon colour, id, title, contents opacity token, tile-fade +BULK_ROWS = ( + (25, "circle-half", "amber", "DUN-31", "Core features", None, False), + (91, "circle-check", "blue", "DUN-32", "Create task", "o-bulk-2", False), + (157, "circle-check", "blue", "DUN-33", "Edit/Delete task", "o-bulk-3", True), + (223, "circle-empty", "ink-3", "DUN-34", "Change status", "o-bulk-4", False), +) + + +def bulk(): + """Four rows with their checkboxes ticked and the bulk toolbar over them. + A checkbox belongs to its row's contents, not to its tile: row 3's solves + .57 against contents at .55 and a tile at .78, so the checkbox and the row + go in one opacity wrapper and the tile is painted behind it.""" + o = [] + for y, ic, col, rid, title, fade, tile_fade in BULK_ROWS: + o.append((tile(483, y, 260, ";background:var(--x-tile-fade)") + "" + if tile_fade else "") + + box(0, 0, 700, 290, ";opacity:var(--x-%s)" % fade if fade else "") + + box(443, y + 17, 23, 23, ";border-radius:var(--x-r-box)" + ";background:var(--x-box)") + + icon("check", 7, 8, 9, "var(--x-white)", 7) + "" + + (box(483, y, 260, 55, "") if tile_fade else tile(483, y, 260)) + + icon(ic, 14, 18, 20, "var(--x-%s)" % col) + + tx("t-row-id", 46, 22, rid, "ink-3") + + tx("t-row", 122, 22, title, "ink") + "") + o.append(box(0, 0, 860, 290, ";background:linear-gradient(90deg,transparent 611px," + "var(--x-c-bulk) 826px)") + "") + o.append(box(31, 200, 528, 56, ";border-radius:var(--x-r-row)" + ";background:var(--x-white)") + "") + o.append(tx("t-btn", 49, 222, "3 Selected", "ink")) + # Four outlined buttons, not three: the X gets one too, set apart by a 35px + # gap where the other three sit 9-10px apart. Their glyphs all solve to + # --x-ink-2 -- the X and the ellipsis reach #646464 in their cores, and the + # archive and trash only read #8A8A8A because a 1.5px stroke never does. + for bx, bw, ic, ix, iy, iw, ih in ( + (136, 38, "x", 148, 222, 12, 12), + (209, 40, "ellipsis", 221, 226, 16, 4), + (257, 39, "archive-check", 267, 220, 18, 16), + (304, 39, "trash", 316, 219, 16, 17)): + o.append(box(bx, 208, bw, 39, "").replace('class="b"', 'class="b obtn"') + "") + o.append(icon(ic, ix, iy, iw, "var(--x-ink-2)", ih)) + o.append(box(352, 209, 199, 38, ";border-radius:19px;background:linear-gradient(" + "12deg,var(--x-dark),var(--x-dark-2))") + "") + o.append(tx("t-btn", 371, 223, "Mark as done", "ink-inv")) + o.append(box(481, 216, 31, 25, "").replace('class="b"', 'class="b ctr plate"') + + "Ctrl") + o.append(box(518, 216, 25, 25, "").replace('class="b"', 'class="b plate"') + "") + o.append(icon("corner-down-left", 525, 222, 10, "var(--x-ink-inv)", 7)) + return "".join(o) + + +INNER = {"ws": workspace, "proj": lambda: art("proj-art", 616, 24), "team": teams, + "docs": docs, "comm": comments, "subt": subtasks, "task": tasks, + "bulk": bulk} + + +def section(): + cards = [] + for key, x, y, w, h, fill, ink, label, lt, bt, lines in CARDS: + body = tx("t-body", 27, bt, "
".join(lines)) if key != "bulk" else ( + tx("t-body", 27, bt, "
".join(lines[:2])) + + tx("t-body", 27, bt + 56, lines[2], None, ";opacity:var(--x-o-text)")) + cards.append( + '
%s%s%s
' + % (x, y, w, h, fill, ink, + tx("t-label", 27, lt + LABEL_DY, label), body, INNER[key]())) + return page(NAME + " - Feature section", + '
%s
' % "".join(cards), GRID) + + +# ------------------------------------------------------------- reference ---- +def reference(): + """Phase 5: the capture itself, parked as a board so the row under the + mockup is the source of truth rather than a memory of it. Third-party + pixels, so this board is regenerated and never committed.""" + f = REFS_DIR / "01.png" + if not f.exists(): + return None + uri = "data:image/png;base64," + base64.b64encode(f.read_bytes()).decode() + return page(NAME + " - reference 01", + 'dun.app feature section' % uri, + "body{background:none}") + + +# ---------------------------------------------------------------- layout ---- +# The welcome card fits its coverBox into one phone case by cover-crop +# (max of the two scales), so a landscape box would come back as a 453px +# slice through the middle of the grid. The box is instead the Comments and +# Subtasks column at the case's own 393:852, which crops nothing. +FOUNDATIONS = foundation_boards() + +LAYOUT = { + "name": PAGE_NAME, + "order": 40, + "cover": "01-feature-grid", + "coverBox": [1480, 24, 415, 900], + "status": "live", + "rows": [ + {"title": "Foundations", "files": [name for name, _ in FOUNDATIONS]}, + {"title": "Boards", "numbered": True, + "files": [{"file": "01-feature-grid", "label": "Feature section", + "w": 1954, "h": 982}]}, + {"title": "Source of truth: captures", "numbered": True, + "files": [{"file": "ref-01-feature-grid", "label": "Feature section", + "w": 1954, "h": 982}]}, + ], +} + +if __name__ == "__main__": + cut() + app_icon() + for name, html in FOUNDATIONS: + write(name, html) + write("01-feature-grid", section()) + ref = reference() + if ref: + write("ref-01-feature-grid", ref) + (OUT / "layout.json").write_text(json.dumps(LAYOUT, indent=2) + "\n") + print("%-24s %6d rows" % ("layout.json", len(LAYOUT["rows"]))) diff --git a/mockups/canvases/dun-web/icon.png b/mockups/canvases/dun-web/icon.png new file mode 100644 index 00000000..236beee6 Binary files /dev/null and b/mockups/canvases/dun-web/icon.png differ diff --git a/mockups/canvases/dun-web/layout.json b/mockups/canvases/dun-web/layout.json new file mode 100644 index 00000000..4b584c9f --- /dev/null +++ b/mockups/canvases/dun-web/layout.json @@ -0,0 +1,49 @@ +{ + "name": "(example) Dun Web", + "order": 40, + "cover": "01-feature-grid", + "coverBox": [ + 1480, + 24, + 415, + 900 + ], + "status": "live", + "rows": [ + { + "title": "Foundations", + "files": [ + "00-design-tokens", + "00b-type", + "00c-evidence", + "00d-evidence", + "00e-evidence", + "00f-evidence" + ] + }, + { + "title": "Boards", + "numbered": true, + "files": [ + { + "file": "01-feature-grid", + "label": "Feature section", + "w": 1954, + "h": 982 + } + ] + }, + { + "title": "Source of truth: captures", + "numbered": true, + "files": [ + { + "file": "ref-01-feature-grid", + "label": "Feature section", + "w": 1954, + "h": 982 + } + ] + } + ] +} diff --git a/mockups/canvases/dun-web/probes.json b/mockups/canvases/dun-web/probes.json new file mode 100644 index 00000000..6023c0e2 --- /dev/null +++ b/mockups/canvases/dun-web/probes.json @@ -0,0 +1,1263 @@ +[ + { + "id": "bg", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 598, + 100, + 613, + 500 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "the 21px gutter between the Workspace and Projects cards (595 -> 616), clear of both rings: page ground and nothing else", + "_head": "Phase 1's measurements, replayable. The capture is 1x -- design px == capture px, 1954 x 982 -- so no probe carries a --pt and `refkit batch probes.json` takes none either. --against wants a directory holding the render at the same size, which is what `refkit shoot 01-feature-grid.html -o scratch/shots --w 1954 --h 982 --scale 1` writes; every probe names 01-feature-grid.png there. A `sample` prints its flat-fill census first, so an ink probe says \"only\": \"ink\" and a small-element probe says \"only\": \"all\" (the mode) -- without that batch compares two backgrounds and reports a perfect 0. `bright` flips both: on a sample it takes the lightest percentile as the ink, on a bbox it matches pixels above the threshold instead of below. --x-rot-pill and --x-rot-ghost have no probe: a rotation of half a degree is a sub-pixel edge fit per column (README has the numbers), and no bbox threshold can see it -- `r-row` returns the tile's bounding box instead. --x-font and --x-font-hand have none either: `refkit font` takes a string rather than a box and batch has no way to pass one, so the face evidence is the verdict quoted in the token table and scratch/labelcmp2.png. Eight boxes here are crops rather than renders (see crops.json), so their probes compare the capture with its own pixels and read 0 by construction: proj-art's folder, the two cursor pills' arrows and the two avatars." + }, + { + "id": "c-ws", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 450, + 40, + 580, + 80 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "Workspace card top right: the label ink ends at x 185 and body line 1 starts at y 89, so this window holds only fill" + }, + { + "id": "c-proj", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 650, + 50, + 880, + 120 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "Projects card above its bottom-aligned text (label ink top 167) and left of the illustration crop (starts x 928)" + }, + { + "id": "c-team", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1000, + 336, + 1130, + 380 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "Teams card top right: right of the label (ends x 700), above body line 1 (y 391) and above the first pill row (y 464)" + }, + { + "id": "c-docs", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1300, + 40, + 1480, + 80 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "Docs card top right: the label is 39px wide at x 1184 and the panel starts at y 192" + }, + { + "id": "c-comm", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1540, + 275, + 1830, + 300 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "Comments card below the comment tile (ends y 262) and above the card edge at 307" + }, + { + "id": "c-subt", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1545, + 445, + 1840, + 490 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "Subtasks card between body line 2 (ink ends y 441) and row 1 (starts y 499). The window below the last row reads #FAE9B8 instead: the bottom of every card carries a few levels of webp mush, and this one is 91.7% one colour" + }, + { + "id": "c-task", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 120, + 875, + 740, + 915 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "Tasks card below the stacked pill (ends y 859) and above the card edge at 924" + }, + { + "id": "c-bulk", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 824, + 790, + 1200, + 828 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "Bulk card between body line 3 (ink ends y 772) and the toolbar (starts y 834), left of the rows (x 1240). Below the toolbar the same census reads #E6EBBC, 5 levels off, which is the bottom-edge mush again" + }, + { + "id": "white", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 900, + 470, + 1020, + 478 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "inside the Engineering pill (858,464 -> 1034,516), above its label ink (y 484) and right of its icon (ends x 894)" + }, + { + "id": "panel-top", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 520, + 203, + 590, + 212 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "the Workspace panel's top band (panel top y 199), right of the Dun tooltip (ends x 486) and above everything on it. x 520 rather than 440 so the pair of panel probes cut the same column of the ramp" + }, + { + "id": "panel-bot", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 548, + 588, + 580, + 596 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "the same panel 10px off the card's bottom edge (606), in the one window down there that is clear of both the You pill (480,559 -> 544,604) and the card's own 36px corner (which reaches x 584 by y 596). At x 440 or 520 the census catches the pill, #1BB944 over a third of it; here the ramp reads 245,249,250 at the top and 236,243,245" + }, + { + "id": "chip", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 565, + 795, + 635, + 801 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "the Research chip (551,792 -> 640,823) above its dot (y 805) and its label ink (y 802)" + }, + { + "id": "dark", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1190, + 872, + 1240, + 879 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "Mark as done, the gradient's dark end: below the button label's ink (ends y 872) and left of the Ctrl plate (starts x 1278). A flat census inside a ramp reports whichever level happens to repeat most, so this one carries about ten levels of slop either way: the window's mean is 14.6 against my 19.0, and the 4.4 between them is the part of the reference's ramp that runs below black and a CSS gradient cannot" + }, + { + "id": "dark-2", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1190, + 845, + 1240, + 852 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "the same button's light end: above the label ink (starts y 857) and inside the pill, which runs 843 -> 881" + }, + { + "id": "box", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1240, + 676, + 1263, + 699 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "the row-1 Bulk checkbox, exactly its 23 x 23 box; the white tick is 9 x 7 inside it, so the mode is the fill" + }, + { + "id": "kbd", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1280, + 852, + 1307, + 873 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "the Bulk toolbar's Ctrl plate (1278,850 -> 1309,875), inset 2px so the mode cannot pick up the white pill behind it" + }, + { + "id": "tip", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 416, + 274, + 483, + 280 + ], + "only": "all", + "mine": "01-feature-grid.png", + "_": "the Dun tooltip, the band below its label ink (ends y 272) and above its bottom edge (283). The core has a left-to-right ramp, 64,66,65 to 69,72,71, so the mode of the whole pill flips with the window while its median is exactly the token; this strip's mode is 68,71,70" + }, + { + "id": "pill-grey", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 416, + 328, + 425, + 364 + ], + "only": "all", + "mine": "01-feature-grid.png", + "_": "the ThisUX tooltip's left strip, inside the pill (413,325 -> 514,367) and left of its label ink (starts x 428). It reads #ECEEEE against a neutral token: the residual is the same red-below-green cast the cursor pills show, which is webp chroma, not a tint" + }, + { + "id": "plus", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 326, + 486, + 382, + 542 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "the add-workspace circle (324,484, 60 x 60) inset 2px; its plus glyph is 19px of ink" + }, + { + "id": "ghost", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 220, + 838, + 640, + 854 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "the stacked card behind the Tasks pill, in the band below the front tile (ends y 830) and above its own bottom edge (859)" + }, + { + "id": "ring", + "img": "assets/refs/01.png", + "cmd": "scan", + "axis": "row", + "at": 130, + "range": [ + 588, + 602 + ], + "mine": "01-feature-grid.png", + "_": "left to right across the Workspace card's right edge at a row that crosses card fill on both sides: fill to 595, the near-white rim, then the page ground. Two rings and one ground across the 21px gutter is what says the gap is a gap and not a shadow. The window stops at 602 because `batch` collapses a scan to its largest colour step, and a range wide enough to hold the Projects card's left edge at 616 holds two comparable steps, so which one is largest flips between the capture and the render" + }, + { + "id": "hairline", + "img": "assets/refs/01.png", + "cmd": "scan", + "axis": "row", + "at": 875, + "range": [ + 928, + 942 + ], + "mine": "01-feature-grid.png", + "_": "across the left outline of the X button on the Bulk toolbar (933,842 -> 971,881): toolbar white, the 1px outline at 934, then button white. Row 861 crosses the X glyph (856 -> 868) and row 845 the button's rounded corner, where the same cut reads #F8F8F8. Stopping at 942 leaves the button's right outline at 970 out, for the same largest-step reason as `ring`" + }, + { + "id": "tree", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1568, + 600, + 1569, + 700 + ], + "only": "all", + "mine": "01-feature-grid.png", + "_": "1px wide by design: the Subtasks connector's first vertical is at x 1568 and runs y 556 -> 867, and this window is 100px of it clear of every horizontal stub (285/374/458/539 in card px)" + }, + { + "id": "i-ws", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 100, + 86, + 500, + 105 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "Workspace body line 1, ink top 89, the full 402px of it and no label" + }, + { + "id": "i-proj", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 643, + 198, + 900, + 232 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "Projects body lines 1-2 (ink top 201, pitch 28), left of the illustration" + }, + { + "id": "i-team", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 643, + 388, + 1110, + 412 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "Teams body line 1 (ink top 391); the pills start at y 464" + }, + { + "id": "i-docs", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1184, + 86, + 1470, + 106 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "Docs body line 1 (ink top 89); the panel starts at y 192" + }, + { + "id": "i-comm", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1545, + 86, + 1825, + 106 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "Comments body line 1 (ink top 89); the tile starts at y 180" + }, + { + "id": "i-subt", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1545, + 390, + 1825, + 410 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "Subtasks body line 1 (ink top 393); the first row starts at y 499" + }, + { + "id": "i-task", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 102, + 696, + 740, + 716 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "the Tasks card's single body line (ink top 699); the pill starts at y 775" + }, + { + "id": "i-bulk", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 824, + 696, + 1180, + 716 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "Bulk body line 1 only. Line 3 is the same ink at --x-o-text and is probed separately, so this window stops well above it (y 755)" + }, + { + "id": "ink", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 248, + 796, + 470, + 818 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "Competitor Analysis Research on the Tasks pill, ink left 250, clear of the DUN-20 id (ends x 232) and the Research chip (starts 551)" + }, + { + "id": "ink-2", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1586, + 199, + 1626, + 212 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "Damia, the commenter name: 34px of ink at x 1588, left of the 2h timestamp (1804) and above the comment body (y 218)" + }, + { + "id": "ink-3", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 173, + 801, + 234, + 815 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "DUN-20 on the Tasks pill: 58px of ink at x 174, right of the status icon (ends x 161) and left of the title (250)" + }, + { + "id": "ink-4", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1802, + 200, + 1824, + 213 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "the 2h timestamp, the palest ink on the board; 18px wide, so the window is barely wider than the string" + }, + { + "id": "ink-dim", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 425, + 334, + 500, + 356 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "the ThisUX tooltip label, centred in a 101px pill at x 413; the same solve on Fli.so gives #A0A5A5 and on the plus glyph #919596" + }, + { + "id": "ink-inv", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1166, + 856, + 1292, + 874 + ], + "only": "ink", + "bright": true, + "mine": "01-feature-grid.png", + "_": "Mark as done: white ink on the dark pill, so --bright takes the lightest percentile; the Ctrl plate starts at x 1278 and is also white ink, which is why this stops at 1292 rather than the pill's 1348" + }, + { + "id": "doc-ink", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1229, + 216, + 1433, + 237 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "Product Requirement Doc, the doc title: 201 x 18 of ink at 1230,216, above the Overview heading (y 250)" + }, + { + "id": "doc-head", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1229, + 252, + 1282, + 264 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "Overview: 50px of ink, the first of the four section headings" + }, + { + "id": "doc-body", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1229, + 275, + 1453, + 285 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "the first doc paragraph, 222px of ink on one line, between Overview (ends y 264) and Description (starts 274 -> probe stops at 285)" + }, + { + "id": "blue", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1611, + 604, + 1631, + 624 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "the circle-check on Subtasks row 2, exactly its 20 x 20 ink box on white. A 1.5px stroke has no flat core, so this is the darkest percentile, not a mode -- and it reads #608AB5 against a token of #3B82F6, because the icons in the capture are soft (their glow is visible at 12x) and a stroke that soft never reaches its own colour while the render's does. The five stroke accents all read 35-95 levels light here for that reason; --x-amber below is the one filled glyph on the board, it reaches #FFBF28 in the capture, and that is what puts the whole set on Tailwind rather than on these washed readings" + }, + { + "id": "amber", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1569, + 517, + 1589, + 537 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "the half-filled circle on Subtasks row 1: the only filled status glyph on the board, which is why it is the one that reads a true value" + }, + { + "id": "orange", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 872, + 478, + 894, + 500 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "the square-terminal on the Engineering pill, its 22 x 22 ink box; the capture reads #CB9744 on it, the stroke wash described under --x-blue" + }, + { + "id": "fuchsia", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 777, + 547, + 798, + 565 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "the megaphone on the Marketing pill, 21 x 18: the one icon on the board that is not lucide's (see README); the capture reads #C435B0 on it, the stroke wash described under --x-blue" + }, + { + "id": "green", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 945, + 546, + 966, + 568 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "the briefcase on the Sales pill, 21 x 22; the capture reads #46AD66 on it, the stroke wash described under --x-blue" + }, + { + "id": "red", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 655, + 798, + 676, + 818 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "the warning triangle at the right of the Tasks pill, 21 x 20; the Research chip's 5px dot solves the same hue on far less ink; the capture reads #D4796C on it, the stroke wash described under --x-blue" + }, + { + "id": "folder", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 980, + 120, + 1060, + 180 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "inside the Projects folder, which is part of the proj-art crop: 8842px of flat fill, so ref and render are the same pixels by construction" + }, + { + "id": "cur-blue", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 120, + 250, + 185, + 256 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "the James pill's top band (pill 107,246 -> 196,292), above its label ink (y 258). The arrow is a crop; the pill is CSS" + }, + { + "id": "cur-green", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 490, + 563, + 535, + 569 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "the You pill's top band (pill 480,559 -> 542,606), above its label ink" + }, + { + "id": "r-card", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 70, + 20, + 600, + 612 + ], + "dark": 232, + "mine": "01-feature-grid.png", + "_": "the Workspace card against the page ground: 20 levels of contrast, so the threshold sits between them. Expect 75,24 520 x 582. 235 is above the ground's own luminance of 242 in name only -- the fill reads 226 and the rim 250, so the threshold has to clear the rim, not the fill" + }, + { + "id": "r-panel", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 284, + 194, + 400, + 230 + ], + "bright": 232, + "mine": "01-feature-grid.png", + "_": "the Workspace panel's top left corner, brighter than the card it sits on; the window stops above the Dun mark (y 233)" + }, + { + "id": "r-tip", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 405, + 235, + 495, + 290 + ], + "dark": 150, + "mine": "01-feature-grid.png", + "_": "the Dun tooltip, the darkest thing on that panel. Expect 413,241 73 x 42; its 7px pointer is at x 331 in card px, outside this window" + }, + { + "id": "r-tile", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 1535, + 172, + 1840, + 270 + ], + "bright": 236, + "mine": "01-feature-grid.png", + "_": "the Comments tile, white on #D3ECE8. Expect 1544,180 286 x 82" + }, + { + "id": "r-row", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 120, + 770, + 735, + 845 + ], + "bright": 250, + "mine": "01-feature-grid.png", + "_": "the Tasks pill, white on #DDDAED. Expect 127,775 598 x 57: the tile is rotated .56deg, so a threshold box is its bounding box and reads 2px taller than the tile. A 248 threshold instead runs the box down into the ghost tile behind it" + }, + { + "id": "r-box", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 1233, + 669, + 1270, + 706 + ], + "dark": 120, + "mine": "01-feature-grid.png", + "_": "the row-1 Bulk checkbox, the darkest 23px on that card" + }, + { + "id": "r-chip", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 545, + 786, + 648, + 829 + ], + "dark": 250, + "mine": "01-feature-grid.png", + "_": "the Research chip, #F6F6F6 -- on the white pill, not the card, so it is the darker of the two and --bright selects the whole pill. Expect 551,793 89 x 29" + }, + { + "id": "r-cur", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 100, + 240, + 200, + 270 + ], + "dark": 180, + "mine": "01-feature-grid.png", + "_": "the James label pill's top band. Expect 107,246 89 x 24. The full pill is 107,246 -> 196,292, but the arrow crop next to it runs to x 236, so a window tall enough to hold the pill also holds the arrow and reads 13px too wide" + }, + { + "id": "r-pill", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 850, + 458, + 1042, + 522 + ], + "bright": 242, + "mine": "01-feature-grid.png", + "_": "the Engineering pill. Expect 858,464 176 x 52, and a corner solve at or past h/2 is what makes it a full pill rather than a 26px radius" + }, + { + "id": "t-label", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 98, + 48, + 200, + 86 + ], + "dark": 180, + "mine": "01-feature-grid.png", + "_": "the Workspace hand label: ink top 55 (card + 31) and 81px wide. This is the box the 25px size was calibrated against, one of eight" + }, + { + "id": "t-body", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 100, + 84, + 510, + 108 + ], + "dark": 180, + "mine": "01-feature-grid.png", + "_": "Workspace body line 1: 402px of ink at 18px, ink top 89" + }, + { + "id": "t-pill", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 905, + 480, + 1010, + 505 + ], + "dark": 150, + "mine": "01-feature-grid.png", + "_": "Engineering on its pill: 95px of ink, cap 14px. Right of the icon (ends x 894) and inside the pill (ends 1034)" + }, + { + "id": "t-row", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 245, + 793, + 470, + 820 + ], + "dark": 150, + "mine": "01-feature-grid.png", + "_": "Competitor Analysis Research: 212px of ink and a 12px cap, the widest disagreement on the board between the cap solve and the width solve" + }, + { + "id": "t-row-id", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 170, + 797, + 240, + 818 + ], + "dark": 200, + "mine": "01-feature-grid.png", + "_": "DUN-20 at --x-ink-3 (#B2B2B2, luminance 178), so the threshold has to be 200 rather than the 150 the black titles take" + }, + { + "id": "t-chip", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 572, + 798, + 632, + 815 + ], + "dark": 180, + "mine": "01-feature-grid.png", + "_": "Research inside its chip: 55px of ink at --x-ink-2 on #F6F6F6, right of the 5px dot (ends x 565)" + }, + { + "id": "t-btn", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 843, + 853, + 928, + 876 + ], + "dark": 150, + "mine": "01-feature-grid.png", + "_": "3 Selected on the Bulk toolbar: 75px of dark ink on white, left of the X button (starts x 933)" + }, + { + "id": "t-btn-s", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 1290, + 538, + 1360, + 562 + ], + "bright": 150, + "mine": "01-feature-grid.png", + "_": "Save doc: white ink on the dark pill, so --bright. Left of the Ctrl plate (starts x 1366), which is also white ink" + }, + { + "id": "t-kbd", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 1280, + 852, + 1309, + 874 + ], + "bright": 120, + "mine": "01-feature-grid.png", + "_": "Ctrl on the Bulk plate: 17 x 9 of white ink inside a 31 x 25 chip. The Docs plate's is 18 x 8, the same within a pixel, which is why there is one token and not two" + }, + { + "id": "t-tip", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 420, + 248, + 480, + 278 + ], + "bright": 150, + "mine": "01-feature-grid.png", + "_": "Dun in its tooltip: 38px of white ink centred in a 73px pill" + }, + { + "id": "t-cursor", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 112, + 252, + 192, + 288 + ], + "bright": 200, + "mine": "01-feature-grid.png", + "_": "James on its pill: 58px of white ink, cap 14px. The threshold is 200 because the pill under it is already luminance 129" + }, + { + "id": "t-doc-t", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 1226, + 212, + 1440, + 240 + ], + "dark": 150, + "mine": "01-feature-grid.png", + "_": "the doc title's ink box, the measurement the 18px size and the 500 weight both come from" + }, + { + "id": "t-doc-h", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 1226, + 248, + 1290, + 268 + ], + "dark": 150, + "mine": "01-feature-grid.png", + "_": "Overview: 50px of ink. Description, the other heading, is 60px, and 700 sets that one 3px over" + }, + { + "id": "t-doc-b", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 1226, + 272, + 1460, + 288 + ], + "dark": 180, + "mine": "01-feature-grid.png", + "_": "the first doc paragraph's one line: 222px of ink at --x-doc-body (#777876), the smallest type on the board" + }, + { + "id": "t-comm-n", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 1584, + 196, + 1630, + 214 + ], + "dark": 180, + "mine": "01-feature-grid.png", + "_": "Damia: 34px of ink at --x-ink-2, left of the timestamp" + }, + { + "id": "t-comm-b", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 1584, + 215, + 1815, + 232 + ], + "dark": 150, + "mine": "01-feature-grid.png", + "_": "the comment's first line: 220px of ink, cap 9px, the pair that put this token at 12px rather than 11" + }, + { + "id": "w", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 0, + 0, + 1954, + 982 + ], + "dark": 232, + "mine": "01-feature-grid.png", + "_": "the whole board: the union of the eight cards against the ground. Expect 75,24 1783 x 900 -- the section is not centred in its capture, 96px of margin on the right against 75 on the left, which is why the board is the capture's own 1954 and not a symmetric crop" + }, + { + "id": "h", + "img": "assets/refs/01.png", + "cmd": "scan", + "axis": "col", + "at": 90, + "range": [ + 900, + 940 + ], + "tol": 12, + "mine": "01-feature-grid.png", + "_": "down through the bottom of the Tasks card in the 28px of left margin it keeps (its content starts at x 127): fill, then ground. The step lands at 917 rather than the card's own 924 because x 90 is 15px into a 36px corner, and that is where --x-r-card was fitted. The full-height cut this probe used to take, (0, 982), holds four comparable steps" + }, + { + "id": "pad", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 1180, + 84, + 1480, + 108 + ], + "dark": 180, + "mine": "01-feature-grid.png", + "_": "Docs body line 1: its ink left should read 1184, the card's 1157 plus 27. The same 27 holds on all eight cards" + }, + { + "id": "label-top", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 1540, + 352, + 1620, + 382 + ], + "dark": 180, + "mine": "01-feature-grid.png", + "_": "the Subtasks hand label: ink top should read 359, the card's 328 plus 31. Across the eight cards the reference runs + 30 to + 33 and the spread is the letter, not the layout" + }, + { + "id": "body-top", + "img": "assets/refs/01.png", + "cmd": "bbox", + "box": [ + 1540, + 84, + 1830, + 108 + ], + "dark": 180, + "mine": "01-feature-grid.png", + "_": "Comments body line 1: ink top should read 89, the card's 24 plus 65" + }, + { + "id": "gap", + "img": "assets/refs/01.png", + "cmd": "scan", + "axis": "row", + "at": 130, + "range": [ + 608, + 622 + ], + "mine": "01-feature-grid.png", + "_": "the other side of the same gutter: page ground to 615, the Projects card's rim, then its fill. 616 - 595 = 21, the gutter, and `ring` holds the other edge. Row 300 crosses the Workspace panel rather than the card fill and reads #F1F6F7 on the left" + }, + { + "id": "row-pitch", + "img": "assets/refs/01.png", + "cmd": "bands", + "box": [ + 1770, + 490, + 1850, + 930 + ], + "axis": "rows", + "thr": 200, + "mine": "01-feature-grid.png", + "_": "the five Subtasks row titles, in a column all five reach into (row 1 ends at x 1783, rows 3 and 4 start at 1761): five bands at a pitch of 85" + }, + { + "id": "bulk-pitch", + "img": "assets/refs/01.png", + "cmd": "bands", + "box": [ + 1402, + 650, + 1470, + 850 + ], + "axis": "rows", + "thr": 215, + "mine": "01-feature-grid.png", + "_": "the Bulk row titles, rows 1-3 only, in a column the titles alone reach into: row 4 sits at --x-o-bulk-4 and is 3 levels off the card fill, below any threshold that does not also take the fill. Three bands, pitch 66" + }, + { + "id": "o-text", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 824, + 752, + 1100, + 772 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "Bulk body line 3, the faded one: the same ink as lines 1-2 (probed as i-bulk) and it should read about 13 levels lighter" + }, + { + "id": "tile-fade", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1650, + 845, + 1740, + 855 + ], + "only": "flat", + "mine": "01-feature-grid.png", + "_": "the last Subtasks row's tile, above its own ink (y 861) and left of where the right-edge fade begins (1764): --x-tile-fade over the card, which is the tile that stays while its contents go" + }, + { + "id": "o-bulk-2", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1402, + 746, + 1492, + 763 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "Create task, Bulk row 2's title, against Core features on row 1 at the same x: the contrast ratio is .935 on all three channels" + }, + { + "id": "o-bulk-3", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1402, + 812, + 1492, + 829 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "Edit/Delete task, row 3, at .544/.540/.559 of row 1" + }, + { + "id": "o-bulk-4", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1402, + 878, + 1492, + 895 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "Change status, row 4, at .040/.037/.040 -- 3 levels of ink, which is why this row needs an ink probe and not a threshold" + }, + { + "id": "o-subt-4", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1684, + 776, + 1750, + 791 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "DUN-23, the row id on Subtasks row 4, at .928 of row 1's. The id rather than the title: the titles on rows 3 and 4 are indented to x 1761, inside that card's fade, and the ids are not" + }, + { + "id": "o-subt-5", + "img": "assets/refs/01.png", + "cmd": "sample", + "box": [ + 1643, + 861, + 1709, + 876 + ], + "only": "ink", + "mine": "01-feature-grid.png", + "_": "DUN-24 on the last Subtasks row, at .381/.389/.416 of row 1's" + } +]