efficient-easel 0.0.0 → 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -0
- package/README.md +723 -4
- package/dist/autosave/index.d.ts +101 -0
- package/dist/autosave/index.js +1 -0
- package/dist/blocks/index.d.ts +295 -0
- package/dist/blocks/index.js +0 -0
- package/dist/buckets-1vRlQceY.d.ts +13 -0
- package/dist/canvas/index.d.ts +194 -0
- package/dist/canvas/index.js +1 -0
- package/dist/chrome/chrome.css +570 -0
- package/dist/chrome/index.d.ts +219 -0
- package/dist/chrome/index.js +21 -0
- package/dist/chunk-2LUSK32B.js +1 -0
- package/dist/chunk-2OAZYSWJ.js +1 -0
- package/dist/chunk-67VZRQLG.js +13 -0
- package/dist/chunk-6JDII7N3.js +1 -0
- package/dist/chunk-6OPSJGAI.js +1 -0
- package/dist/chunk-6WY6ZXJG.js +1 -0
- package/dist/chunk-7A7O2JWQ.js +1 -0
- package/dist/chunk-BZ5ASXOL.js +1 -0
- package/dist/chunk-CERTFXCV.js +13 -0
- package/dist/chunk-EFTTSIMK.js +1 -0
- package/dist/chunk-FI23ALVJ.js +1 -0
- package/dist/chunk-FJE4IDBQ.js +1 -0
- package/dist/chunk-GGTPNGFJ.js +1 -0
- package/dist/chunk-H4B72XCJ.js +1 -0
- package/dist/chunk-H6DDIBQ4.js +1 -0
- package/dist/chunk-KDIVA6WD.js +1 -0
- package/dist/chunk-LPRQDDQO.js +1 -0
- package/dist/chunk-MCLBHENY.js +1 -0
- package/dist/chunk-MFIRYGHW.js +1 -0
- package/dist/chunk-NKB3KVAB.js +1 -0
- package/dist/chunk-O2NLQBFX.js +1 -0
- package/dist/chunk-OGZFDJ6G.js +1 -0
- package/dist/chunk-OWBO4WKA.js +0 -0
- package/dist/chunk-QB3BZIRA.js +5 -0
- package/dist/chunk-RE4LP46F.js +1 -0
- package/dist/chunk-STFMZJKU.js +14 -0
- package/dist/chunk-SUO5627P.js +1 -0
- package/dist/chunk-TDA4HH54.js +1 -0
- package/dist/chunk-WK3AFPXY.js +1 -0
- package/dist/chunk-YIMRTIBN.js +3 -0
- package/dist/chunk-YNZZYHA4.js +450 -0
- package/dist/chunk-ZAOM6PFH.js +1 -0
- package/dist/chunk-ZNCEOSXO.js +1 -0
- package/dist/copy/copy.css +282 -0
- package/dist/copy/index.d.ts +336 -0
- package/dist/copy/index.js +10 -0
- package/dist/data/index.d.ts +208 -0
- package/dist/data/index.js +1 -0
- package/dist/drag/drag.css +360 -0
- package/dist/drag/index.d.ts +379 -0
- package/dist/drag/index.js +18 -0
- package/dist/edit-lock/index.d.ts +354 -0
- package/dist/edit-lock/index.js +1 -0
- package/dist/editor/editor.css +58 -0
- package/dist/editor/index.d.ts +361 -0
- package/dist/editor/index.js +9 -0
- package/dist/fields/fields.css +207 -0
- package/dist/fields/index.d.ts +273 -0
- package/dist/fields/index.js +46 -0
- package/dist/gold/gold.css +196 -0
- package/dist/gold/index.d.ts +280 -0
- package/dist/gold/index.js +1 -0
- package/dist/icons/index.d.ts +474 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -0
- package/dist/markup/index.d.ts +452 -0
- package/dist/markup/index.js +1 -0
- package/dist/menu/index.d.ts +49 -0
- package/dist/menu/index.js +15 -0
- package/dist/menu/menu.css +254 -0
- package/dist/node-ops/index.d.ts +91 -0
- package/dist/node-ops/index.js +1 -0
- package/dist/panel/index.d.ts +1088 -0
- package/dist/panel/index.js +16 -0
- package/dist/panel/panel.css +1460 -0
- package/dist/pick/index.d.ts +153 -0
- package/dist/pick/index.js +14 -0
- package/dist/pick/pick.css +235 -0
- package/dist/plan-DtNw2VuI.d.ts +88 -0
- package/dist/rail/index.d.ts +145 -0
- package/dist/rail/index.js +1 -0
- package/dist/rail/rail.css +175 -0
- package/dist/render/index.d.ts +117 -0
- package/dist/render/index.js +5 -0
- package/dist/resolve-DpH8YQOA.d.ts +57 -0
- package/dist/shell/index.d.ts +159 -0
- package/dist/shell/index.js +1 -0
- package/dist/shell/shell.css +122 -0
- package/dist/strings/catalogue.d.ts +141 -0
- package/dist/strings/catalogue.js +1 -0
- package/dist/strings/index.d.ts +56 -0
- package/dist/strings/index.js +2 -0
- package/dist/style-clipboard/index.d.ts +184 -0
- package/dist/style-clipboard/index.js +1 -0
- package/dist/tree/index.d.ts +173 -0
- package/dist/tree/index.js +1 -0
- package/dist/tree/tree.css +618 -0
- package/dist/types-CEqUFH2j.d.ts +129 -0
- package/dist/types-CuNiSnX6.d.ts +204 -0
- package/dist/types-DNrn_w4V.d.ts +88 -0
- package/dist/types-DziKCwsN.d.ts +105 -0
- package/dist/types-_Vayx8ov.d.ts +153 -0
- package/dist/types-pGBXevBU.d.ts +131 -0
- package/dist/useNodeOps-6IAej9jy.d.ts +68 -0
- package/dist/watch-DmasQA61.d.ts +60 -0
- package/dist/wordFit-9B9nqyf3.d.ts +35 -0
- package/dist/words-B2YLaKEr.d.ts +30 -0
- package/dist/words-B87GIFWH.d.ts +86 -0
- package/dist/words-BK-npta-.d.ts +27 -0
- package/dist/words-CD3WADyb.d.ts +51 -0
- package/dist/words-CsRWXkY4.d.ts +129 -0
- package/dist/words-D9ChvbNC.d.ts +52 -0
- package/dist/words-DOhF-4D4.d.ts +35 -0
- package/dist/words-DagJdwIA.d.ts +78 -0
- package/dist/words-DfxEp_RH.d.ts +142 -0
- package/package.json +202 -3
|
@@ -0,0 +1,1460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* efficient-easel/panel.css — the settings-panel grammar.
|
|
3
|
+
*
|
|
4
|
+
* Values come from the shell token layer (`efficient-easel/shell.css`), never from
|
|
5
|
+
* literals: this file is the storefront editor's approved panel design expressed
|
|
6
|
+
* once, so both editors read the same rules. Light only — neither editor has a
|
|
7
|
+
* dark mode.
|
|
8
|
+
*
|
|
9
|
+
* Two details are deliberate and were decided by measurement, not taste:
|
|
10
|
+
* · rows sit 38px apart while a label sits 11px from its own control, so a
|
|
11
|
+
* setting reads as ONE thing with air around it. That inside gap is the
|
|
12
|
+
* ONLY thing saying which label owns which control, and the two are a
|
|
13
|
+
* RATIO rather than a pair of numbers: measured at 28 and 7 (card 8802.d,
|
|
14
|
+
* approved from mockups/8802e-settings-spacing-mockup.html) and re-scaled
|
|
15
|
+
* together to 38 and 11 (card 66301), which keeps the gap between settings
|
|
16
|
+
* 3.5x the gap inside one;
|
|
17
|
+
* · the label is small and grey, the value is larger and dark. Before that
|
|
18
|
+
* contrast existed, a merchant could not tell a name from a number.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/* ── the row floor: one setting, and nothing falls below it (card 66203) ───── */
|
|
22
|
+
|
|
23
|
+
/* Lewis, with a screenshot of the Site styles Text tab: the rows are too tight
|
|
24
|
+
top to bottom, and he wants ONE global setting keeping every settings row at
|
|
25
|
+
least 10px from the next.
|
|
26
|
+
|
|
27
|
+
THIS IS THAT SETTING, and it is the only place the number lives. Before it,
|
|
28
|
+
the panel's row rhythm was six hand-written numbers in five rules with no
|
|
29
|
+
token behind any of them: there was nowhere to change it once, and no floor
|
|
30
|
+
under any of it.
|
|
31
|
+
|
|
32
|
+
ON `.es-editor`, because the panel HAS no root of its own. Puck lays the
|
|
33
|
+
inspector out as a flat sibling list, so the primitives render no wrapper —
|
|
34
|
+
and `.es-editor` is the chrome root both editors already put on their own
|
|
35
|
+
chrome and the root every colour in this sheet is already read from. A host
|
|
36
|
+
overrides the whole panel's rhythm by setting this one var on, or above, that
|
|
37
|
+
root. Declaring it HERE rather than in `shell.css` follows `rail.css`, which
|
|
38
|
+
owns its own `--es-rail-*`: the sheet that owns the rules owns its tokens, and
|
|
39
|
+
`shell.css` stays a verbatim mirror of the storefront editor's `--sf-*` block.
|
|
40
|
+
|
|
41
|
+
HOW A LARGER RHYTHM KEEPS THE FLOOR. Three of the gaps below are deliberately
|
|
42
|
+
bigger than 10 and were decided by measurement, not by taste (card 8802.d).
|
|
43
|
+
They are written `max(var(--es-row-gap), <rhythm>)` — never as a bare number
|
|
44
|
+
beside the token, which would look governed and would not be, and never as
|
|
45
|
+
`calc()` off it, which would claim an arithmetic relationship that does not
|
|
46
|
+
exist: 28 was measured, it is not "10 plus 18". The max() is what makes this a
|
|
47
|
+
FLOOR rather than a default — raise the token past a rhythm and that row rises
|
|
48
|
+
with it instead of quietly staying tighter than everything around it.
|
|
49
|
+
|
|
50
|
+
NOW 16, AND 10 WAS WORTH NOTHING ON SCREEN (card 66206). 66203 shipped the
|
|
51
|
+
token at 10 and Lewis could see no change at all, which is not a bug in it —
|
|
52
|
+
it is what a floor of 10 was always going to do here. Four of the six gaps
|
|
53
|
+
(36, 28, 16, 14) were already above it, and of the two that were not, one is
|
|
54
|
+
the dead `.es-group + .es-group` rule below. So 10 could only ever reach the
|
|
55
|
+
drill list, and there it landed on the number that list already had.
|
|
56
|
+
|
|
57
|
+
16 is Lewis's own, picked on 2026-08-28 once that was shown to him, and it is
|
|
58
|
+
the first value that moves anything a merchant looks at. MEASURED in Chromium
|
|
59
|
+
against these stylesheets (`tests/fixtures/panel-row-spacing.html`):
|
|
60
|
+
|
|
61
|
+
one drill row to the next, word to word 23.40px → 35.40px
|
|
62
|
+
a group head to its first setting 14px → 16px
|
|
63
|
+
one setting to the next 28px unchanged
|
|
64
|
+
a one-line boolean row 16px unchanged
|
|
65
|
+
a heading, after the row above it 36px unchanged
|
|
66
|
+
|
|
67
|
+
The three measured rhythms do not move because 16 is still under them, which
|
|
68
|
+
is the floor doing exactly what it was built to do.
|
|
69
|
+
|
|
70
|
+
NOW 20, AND THE RHYTHMS MOVED WITH IT (card 66301). Lewis, 2026-08-31: the
|
|
71
|
+
vertical spacing is too tight, open it up, roughly 10px more. Asked whether
|
|
72
|
+
the tightest gap should take the full 10 as well he handed the numbers over —
|
|
73
|
+
"you decide, whatever feels comfortable" — so the six values below are this
|
|
74
|
+
desk's, made under that.
|
|
75
|
+
|
|
76
|
+
NOT A FLAT +10 ON EVERY TOKEN, which is what was first asked for. A flat rise
|
|
77
|
+
takes the widest gap against the tightest from 36:7 to 46:17 — 5.1x down to
|
|
78
|
+
2.7x — so it opens the panel and FLATTENS it at the same time, and "these
|
|
79
|
+
belong together" stops reading differently from "these are separate". The six
|
|
80
|
+
values keep it at 4.4x while every seam still grows visibly. */
|
|
81
|
+
/* ── the rhythms: the numbers under the floor, given names (card 66228) ────── */
|
|
82
|
+
|
|
83
|
+
/* THE FLOOR ABOVE IS NOT THE RHYTHM, and handing a consumer the floor was the
|
|
84
|
+
whole problem. `--es-row-gap` answers 20 for all five gaps below alike; what
|
|
85
|
+
the storefront's Layers tree needs in order to "follow the shell's spacing"
|
|
86
|
+
is the RHYTHM — 38 between two settings against 11 inside one. Under the floor
|
|
87
|
+
those were bare literals, so the only way to follow them was to COPY them
|
|
88
|
+
into `efficient-shop/components/storefront-editor.css`: a second source of
|
|
89
|
+
truth for numbers this sheet was already the source of.
|
|
90
|
+
|
|
91
|
+
THE VALUES DID NOT CHANGE WHEN THEY WERE NAMED. Every one was the number
|
|
92
|
+
that was there before, moved and named; (g) asserted each still resolved to
|
|
93
|
+
what it resolved to, and (a) still read the scale off the sheet. Card 66301
|
|
94
|
+
is the first card to turn them, and it turned all six at once — which is what
|
|
95
|
+
naming them was for.
|
|
96
|
+
|
|
97
|
+
NAMES, NOT ARITHMETIC, for the reason this sheet already gives one rule down:
|
|
98
|
+
38 was measured and then re-scaled (cards 8802.d, 66301), it is not "20 plus
|
|
99
|
+
18" and never was "16 plus 12". `calc()` off the floor
|
|
100
|
+
would claim a relationship nobody decided; a name states the value and the
|
|
101
|
+
job it does.
|
|
102
|
+
|
|
103
|
+
WHAT A CONSUMER IS ACTUALLY BUYING is the pair at the end. `--es-rhythm-between`
|
|
104
|
+
and `--es-rhythm-within` are the two halves of the one rule that has to
|
|
105
|
+
survive being re-scaled: the gap INSIDE a thing stays plainly smaller than
|
|
106
|
+
the gap BETWEEN things, because that inside gap is the ONLY thing saying
|
|
107
|
+
which label owns which control. Card 66301 re-scaled all six and is the proof
|
|
108
|
+
the pair travels: 28:7 became 38:11, both up, the ratio held. A tree at 38px a
|
|
109
|
+
row would scroll forever, so a consumer is expected to scale these — and (g)
|
|
110
|
+
is what stops the two from drifting together while it does. */
|
|
111
|
+
/* ⛔ CUSTOM PROPERTIES ONLY IN THIS BLOCK. Never a paint property.
|
|
112
|
+
|
|
113
|
+
`.es-editor` is not this package's own element — it is the class each editor
|
|
114
|
+
puts on ITS OWN chrome root, and the campaign designer puts it on a root that
|
|
115
|
+
lives INSIDE the admin portal's DOM (card 66024). A custom property here is
|
|
116
|
+
inert until something reads it; a `background`, a `padding`, a `font` —
|
|
117
|
+
anything that PAINTS — applies to that root the moment this sheet is
|
|
118
|
+
imported, in every editor that imports it.
|
|
119
|
+
|
|
120
|
+
Audited 2026-09-02 across every sheet here that opens an `.es-editor` block:
|
|
121
|
+
shell.css 67/67 custom, panel.css 11/11, menu.css 7/7 — zero paint in all
|
|
122
|
+
three. ⚠️ Nothing enforces it. If you are adding a declaration here, you are
|
|
123
|
+
the check. */
|
|
124
|
+
.es-editor {
|
|
125
|
+
/* THE FLOOR (card 66206) — no row-to-row gap on this panel resolves below it. */
|
|
126
|
+
--es-row-gap: 20px;
|
|
127
|
+
|
|
128
|
+
/* THE RHYTHMS. All five are declared in THIS rule and not a second
|
|
129
|
+
`.es-editor` block: the panel has ONE root, and a second block would put
|
|
130
|
+
half the panel's spacing somewhere a reader — and the spec, which reads the
|
|
131
|
+
root by selector — would not look. */
|
|
132
|
+
/* between one group heading and whatever is above it */
|
|
133
|
+
--es-rhythm-group: 48px;
|
|
134
|
+
/* between a group heading and the first setting under it — closer, because
|
|
135
|
+
that setting belongs to that heading */
|
|
136
|
+
--es-rhythm-group-first: 24px;
|
|
137
|
+
/* BETWEEN two settings */
|
|
138
|
+
--es-rhythm-between: 38px;
|
|
139
|
+
/* between two one-line rows, which are one line tall and need less air */
|
|
140
|
+
--es-rhythm-between-tight: 26px;
|
|
141
|
+
/* WITHIN one setting: its label to its own control */
|
|
142
|
+
--es-rhythm-within: 11px;
|
|
143
|
+
|
|
144
|
+
/* ── THE CHIP'S OWN METRICS (card 66248) ───────────────────────────────────
|
|
145
|
+
|
|
146
|
+
These five were literals inside the chip rules until a SECOND component
|
|
147
|
+
had to wear the same chip. They are named for one reason, and it is not
|
|
148
|
+
tidiness: the segmented control's fit thresholds are ARITHMETIC OVER THEM.
|
|
149
|
+
|
|
150
|
+
Its words drop where the words stop fitting, and where that is depends on
|
|
151
|
+
the room the chrome takes before any word is drawn — two chip paddings, a
|
|
152
|
+
glyph, the gap after it, the gaps between chips and the track's own
|
|
153
|
+
padding. Card 66138 has already moved one of these once (the track's side
|
|
154
|
+
padding, 14px → 3px). A threshold holding its own copy of that number would
|
|
155
|
+
have gone stale that day and said nothing. Read from here, it moves with
|
|
156
|
+
the chip.
|
|
157
|
+
|
|
158
|
+
They are DECLARED lengths and not measurements: the chip is drawn from
|
|
159
|
+
these, so they are the source, not a reading of one. The one thing that
|
|
160
|
+
cannot be named here is the width of a WORD — see `.es-segs` below. */
|
|
161
|
+
--es-chip-pad-x: 8px;
|
|
162
|
+
--es-chip-icon: 16px;
|
|
163
|
+
--es-chip-icon-gap: 6px;
|
|
164
|
+
--es-track-pad: 3px;
|
|
165
|
+
--es-track-gap: 3px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* ── the tab strip: a segmented control ────────────────────────────────────── */
|
|
169
|
+
|
|
170
|
+
/* The strip's own container context (card 8810), and the reason `SettingsTabs`
|
|
171
|
+
renders a wrapper at all.
|
|
172
|
+
|
|
173
|
+
The drop rules further down turn on the width of the PANEL, and `@container`
|
|
174
|
+
is the only thing that can ask for it. A media query asks the WINDOW, and the
|
|
175
|
+
window is the wrong box: the inspector is a column the merchant drags, so a
|
|
176
|
+
220px panel on a 2560px screen is the ordinary case, not an edge one — a
|
|
177
|
+
media query would answer for that panel with the width of the screen behind
|
|
178
|
+
it.
|
|
179
|
+
|
|
180
|
+
The context lives HERE rather than in a host's own panel rule so the shell is
|
|
181
|
+
self-contained: a host that never declares `container-type` would silently
|
|
182
|
+
pin the strip to one state with nothing to see and nothing to fail.
|
|
183
|
+
|
|
184
|
+
`.es-rail-fit` is the top-bar rail's column (`TopBarRail`), which reads the
|
|
185
|
+
word-fit rule below: its container is the left panel's own width. */
|
|
186
|
+
.es-tabs-fit,
|
|
187
|
+
.es-segs-fit,
|
|
188
|
+
.es-rail-fit {
|
|
189
|
+
container-type: inline-size;
|
|
190
|
+
flex: none;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* …EXCEPT IN THE ONE SLOT THIS PACKAGE HANDS CONSUMERS (card 66516).
|
|
194
|
+
|
|
195
|
+
`SettingsField` stacks its control into `.es-field-ctl`, and that slot is
|
|
196
|
+
`display: flex`. So `flex: none` above stops meaning "do not stretch" and
|
|
197
|
+
starts meaning "size to your own content" — and `container-type:
|
|
198
|
+
inline-size` is size containment on that axis, so this wrapper HAS no
|
|
199
|
+
content width to size to. The strip comes out at ZERO.
|
|
200
|
+
|
|
201
|
+
⚠️ NEITHER DECLARATION ABOVE IS WRONG, WHICH IS WHY THIS IS AN EXCEPTION AND
|
|
202
|
+
NOT AN EDIT. The containment is what lets `100cqi` mean the strip's own
|
|
203
|
+
width for the word-drop arithmetic further down; `flex: none` is what keeps
|
|
204
|
+
the strip from stretching in the contexts it was written for. They collide
|
|
205
|
+
only here.
|
|
206
|
+
|
|
207
|
+
MEASURED, `tests/fixtures/field-slot-basis.html`, Chromium, before this rule:
|
|
208
|
+
|
|
209
|
+
panel field content strip in .es-field-ctl strip in a block row
|
|
210
|
+
320px 320px 0px 320px
|
|
211
|
+
220px 220px 0px 220px
|
|
212
|
+
|
|
213
|
+
The block-row column is the control: it is where every one of these strips
|
|
214
|
+
lived until now — a consumer's own block row, where `flex: none` is inert
|
|
215
|
+
and the box fills the line. That is why this looked right for a year and
|
|
216
|
+
why it broke for the first consumer that took this package's own advice.
|
|
217
|
+
|
|
218
|
+
⚠️ FOUND BY A PERSON LOOKING, NOT BY A GUARD. Card 66508 shipped 1198 green
|
|
219
|
+
checks over this: every button present, `aria-pressed` correct, exactly one
|
|
220
|
+
chip active, the glyph in its slot, `tsc` clean, the PR MERGEABLE. All of
|
|
221
|
+
that is true of a control 0px wide. Our checks ask whether markup EXISTS;
|
|
222
|
+
this one asks whether it OCCUPIES SPACE, which is the gap that let it ship.
|
|
223
|
+
|
|
224
|
+
`flex: 1 1 auto` rather than a percentage width: the slot is a flex row that
|
|
225
|
+
may hold a value readout or a second control beside the strip, and a
|
|
226
|
+
percentage would overflow it the day it does. `min-width: 0` because a flex
|
|
227
|
+
item's automatic minimum is its content size, and the containment above
|
|
228
|
+
makes that meaningless here — without it the strip cannot shrink below the
|
|
229
|
+
basis the parent gives it.
|
|
230
|
+
|
|
231
|
+
BOTH `-fit` WRAPPERS, though only the segmented one has a live consumer
|
|
232
|
+
today. Measured in the shop at the time of writing: 1845 custom fields
|
|
233
|
+
rendered, 68 draw a tab strip, 0 of them inside `.es-field-ctl`. But
|
|
234
|
+
`PanelTabs` already draws a strip AMONG the fields and is one refactor from
|
|
235
|
+
the slot, and the two wrappers share the declaration that causes this — a
|
|
236
|
+
rule that fixed one and left the other would be a trap wearing a fix. */
|
|
237
|
+
.es-field-ctl > .es-tabs-fit,
|
|
238
|
+
.es-field-ctl > .es-segs-fit {
|
|
239
|
+
flex: 1 1 auto;
|
|
240
|
+
min-width: 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* THE TRACK (card 66138). Lewis, looking at a real settings panel: the tab he
|
|
244
|
+
is NOT on does not invite a click — it does not even look like a button. He
|
|
245
|
+
was shown six treatments in `mockups/settings-tab-strip-options.html` and
|
|
246
|
+
picked B, the segmented control.
|
|
247
|
+
|
|
248
|
+
This REPLACES the underline strip, and with it card 8811's accent-tint ground
|
|
249
|
+
(see `.es-tab.is-active` below). The grey track is the part doing the work
|
|
250
|
+
Lewis asked for: it is what makes every segment look pressable, where a mark
|
|
251
|
+
under the chosen tab only ever made the chosen tab louder.
|
|
252
|
+
|
|
253
|
+
FULL WIDTH, no side inset. The old strip carried `padding: 0 14px` because
|
|
254
|
+
underline tabs must start inset from the panel edge. A track is a FILLED row,
|
|
255
|
+
and every other filled row here — `.es-group-head` at `width: 100%` — runs
|
|
256
|
+
the full width of the body's content box; a track inset 14px further than the
|
|
257
|
+
rows beneath it is visibly out of line. It is also worth 28px of room, which
|
|
258
|
+
is the difference between three segments fitting the shop's default panel and
|
|
259
|
+
not.
|
|
260
|
+
|
|
261
|
+
NOWRAP, unlike the strip it replaces. A segmented control is one row by
|
|
262
|
+
definition — a second row of track under the first is not a wrapped control,
|
|
263
|
+
it is two controls. The guarantee that no bucket is ever out of reach is
|
|
264
|
+
re-made out of different parts: segments that share the leftover space, and
|
|
265
|
+
the drop rules below. */
|
|
266
|
+
.es-tabs,
|
|
267
|
+
.es-segs {
|
|
268
|
+
display: flex;
|
|
269
|
+
flex-wrap: nowrap;
|
|
270
|
+
gap: var(--es-track-gap);
|
|
271
|
+
padding: var(--es-track-pad);
|
|
272
|
+
background: var(--es-track-bg);
|
|
273
|
+
border-radius: var(--es-row-radius);
|
|
274
|
+
flex: none;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* A SEGMENT TAKES ITS OWN WIDTH, THEN SHARES WHAT IS LEFT.
|
|
278
|
+
|
|
279
|
+
`1 1 auto` and not `1 1 0`: equal segments were measured and are too
|
|
280
|
+
expensive. Every equal segment takes the width of the WIDEST label, and
|
|
281
|
+
"Content" is 85.36px at 12px/600, so three equal segments need a 267px track
|
|
282
|
+
and four need 355px — against the 281px the shop's default panel gives. With
|
|
283
|
+
natural widths the track still reaches both edges, and "Style" is not forced
|
|
284
|
+
to be as wide as "Content" to get there.
|
|
285
|
+
|
|
286
|
+
`box-sizing` for the same reason the switch sets it: this file assumes no
|
|
287
|
+
reset of the host's. */
|
|
288
|
+
.es-tab,
|
|
289
|
+
.es-seg {
|
|
290
|
+
box-sizing: border-box;
|
|
291
|
+
flex: 1 1 auto;
|
|
292
|
+
border: 0;
|
|
293
|
+
background: transparent;
|
|
294
|
+
border-radius: 6px;
|
|
295
|
+
padding: 6px var(--es-chip-pad-x);
|
|
296
|
+
font: inherit;
|
|
297
|
+
font-size: 12px;
|
|
298
|
+
font-weight: 600;
|
|
299
|
+
color: var(--es-text-secondary);
|
|
300
|
+
white-space: nowrap;
|
|
301
|
+
cursor: pointer;
|
|
302
|
+
display: inline-flex;
|
|
303
|
+
align-items: center;
|
|
304
|
+
justify-content: center;
|
|
305
|
+
gap: var(--es-chip-icon-gap);
|
|
306
|
+
transition: background 0.12s, color 0.12s, box-shadow 0.12s;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* A quiet segment is READABLE, not faint. It was `--es-muted`, which on the new
|
|
310
|
+
grey track sits at about 4.3:1 and misses AA; this is about 8.9:1. The track
|
|
311
|
+
is what says "pressable" now, so the label no longer has to whisper to stay
|
|
312
|
+
out of the chosen tab's way. */
|
|
313
|
+
.es-tab:hover:not(.is-active),
|
|
314
|
+
.es-seg:hover:not(.is-active) { color: var(--es-text); }
|
|
315
|
+
|
|
316
|
+
/* Keyboard focus, themed rather than left to the UA. Same ring as the switch,
|
|
317
|
+
so the panel answers a keyboard the one way. */
|
|
318
|
+
.es-tab:focus-visible,
|
|
319
|
+
.es-seg:focus-visible {
|
|
320
|
+
outline: none;
|
|
321
|
+
box-shadow: 0 0 0 3px var(--es-accent-ring);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/* THE CHOSEN CHIP IS THE DARKEST THING IN THE ROW (card 66302).
|
|
325
|
+
|
|
326
|
+
IT WAS A RAISED WHITE CHIP, AND THAT WAS NOT A MISTAKE. Card 66138 put white
|
|
327
|
+
on the grey track with a soft lift, and Lewis picked that look himself from
|
|
328
|
+
six treatments. He picked it BY EYE, AT A WIDTH WHERE THE WORDS WERE STILL
|
|
329
|
+
THERE — which is the whole reason this is a revision and not a correction.
|
|
330
|
+
|
|
331
|
+
WHAT CHANGED IS THE CONDITION, TWICE.
|
|
332
|
+
|
|
333
|
+
First the words went. A four-tab strip is already wordless below 200px, and
|
|
334
|
+
the panel's drag band bottoms out at 181px, so that state is live in the
|
|
335
|
+
editor today; card 66246 then made the shop's Weight row show one word of
|
|
336
|
+
four at the DEFAULT panel and none at the narrow end. A chip that used to sit
|
|
337
|
+
beside a word became the only thing saying which option is set.
|
|
338
|
+
|
|
339
|
+
Then the second mark went, and nobody noticed. Card 66248 moved the strip
|
|
340
|
+
into this package and the quiet label darkened from the muted grey to
|
|
341
|
+
`--es-text-secondary`. That darkening is right on its own terms — it is card
|
|
342
|
+
66138's AA fix, 4.39:1 to 9.37:1 on the track — but it also cut the
|
|
343
|
+
chosen-vs-quiet GLYPH contrast from 3.67:1 to 1.72:1. The strip lost the one
|
|
344
|
+
state cue that was already above 3:1, and the change was described as
|
|
345
|
+
cosmetic when it merged.
|
|
346
|
+
|
|
347
|
+
So the raised white chip was left carrying the whole message alone, at
|
|
348
|
+
1.10:1 against its own track, where WCAG 1.4.11 asks 3:1 of anything
|
|
349
|
+
identifying a control's STATE.
|
|
350
|
+
|
|
351
|
+
WHY DARK AND NOT A RING. Four treatments were drawn and measured for Lewis;
|
|
352
|
+
two of the routes were closed by arithmetic before he saw them (darkening the
|
|
353
|
+
track drops the quiet label to 3.40:1 and fails AA; pushing the glyph delta
|
|
354
|
+
only reaches 3:1 by putting that label back where 66138 moved it FROM). Of
|
|
355
|
+
what was left, this is the one that survives the DISABLED state:
|
|
356
|
+
`.es-segs[data-disabled]` fades the strip to 0.5 over the panel, and a ring
|
|
357
|
+
at 4.39:1 composites down to 1.89:1 while this holds 3.24:1. A cue that only
|
|
358
|
+
works while the control is live is a cue with a hole in it.
|
|
359
|
+
|
|
360
|
+
⚠️ THE LIFT STAYS, AND IT IS NO LONGER THE MESSAGE. `--es-raised-shadow`
|
|
361
|
+
still seats the chip on the track. What it must not go back to being is the
|
|
362
|
+
thing telling the two states apart — a shadow is not a colour, so no
|
|
363
|
+
contrast ratio can score it, and that is exactly how this got to 1.10:1
|
|
364
|
+
without any guard noticing. `tests/panel-chip-contrast.test.ts` pins the
|
|
365
|
+
RATIO and not this colour, so a better answer is free to replace it. */
|
|
366
|
+
.es-tab.is-active,
|
|
367
|
+
.es-seg.is-active {
|
|
368
|
+
background: var(--es-text);
|
|
369
|
+
color: var(--es-on-accent);
|
|
370
|
+
box-shadow: var(--es-raised-shadow);
|
|
371
|
+
border-radius: 6px;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* Focused AND chosen: keep the lift, or the chip drops flat the moment a
|
|
375
|
+
keyboard reaches it. */
|
|
376
|
+
.es-tab.is-active:focus-visible,
|
|
377
|
+
.es-seg.is-active:focus-visible {
|
|
378
|
+
box-shadow: var(--es-raised-shadow), 0 0 0 3px var(--es-accent-ring);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.es-tab-badge {
|
|
382
|
+
min-width: 16px;
|
|
383
|
+
height: 16px;
|
|
384
|
+
padding: 0 5px;
|
|
385
|
+
border-radius: 999px;
|
|
386
|
+
background: var(--es-accent);
|
|
387
|
+
color: var(--es-on-accent);
|
|
388
|
+
font-size: 10px;
|
|
389
|
+
font-weight: 700;
|
|
390
|
+
display: inline-flex;
|
|
391
|
+
align-items: center;
|
|
392
|
+
justify-content: center;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* The glyph in the chip: 16px of drawing, the same size a field's icon takes,
|
|
396
|
+
standing in an 18px box.
|
|
397
|
+
|
|
398
|
+
DRAWN AT EVERY WIDTH, unlike the strip this replaces, where the glyph only
|
|
399
|
+
appeared once the words started failing. In a segmented control the glyph is
|
|
400
|
+
part of the chip — it is what the chip still says when its word has gone, and
|
|
401
|
+
mockup B draws it in both states.
|
|
402
|
+
|
|
403
|
+
The 18 is not a second opinion about the glyph — it is the WORD's own line
|
|
404
|
+
box (12px/600 in `--es-font` lays out an 18px line). Give the box that height
|
|
405
|
+
and a chip is the same height whether it is showing a word, a glyph, or both,
|
|
406
|
+
so the track keeps one height as the merchant drags the panel past a
|
|
407
|
+
threshold. It is also what keeps a badge from moving anything: the badge is
|
|
408
|
+
16px, so it is never the tallest thing in the chip.
|
|
409
|
+
|
|
410
|
+
`flex: none` because a glyph that shrinks is a squeezed segment in a smaller
|
|
411
|
+
box. */
|
|
412
|
+
.es-tab-icon,
|
|
413
|
+
.es-seg-icon {
|
|
414
|
+
display: inline-flex;
|
|
415
|
+
width: var(--es-chip-icon);
|
|
416
|
+
height: 18px;
|
|
417
|
+
flex: none;
|
|
418
|
+
align-items: center;
|
|
419
|
+
justify-content: center;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.es-tab-icon svg,
|
|
423
|
+
.es-seg-icon svg { width: var(--es-chip-icon); height: var(--es-chip-icon); display: block; }
|
|
424
|
+
|
|
425
|
+
/* ── where the word stops fitting ──────────────────────────────────────────── */
|
|
426
|
+
|
|
427
|
+
/* MEASURED in Chromium, `tests/fixtures/tabs-segmented-fit.html`, against these
|
|
428
|
+
* stylesheets and the shop's four buckets Content / Media / Style / Layout, by
|
|
429
|
+
* EDGE GEOMETRY — the right edge of the last segment against the right content
|
|
430
|
+
* edge of the track. Not `scrollWidth`: a flex button reports a `scrollWidth`
|
|
431
|
+
* equal to its `clientWidth` while its own content is drawn outside it, so it
|
|
432
|
+
* says "fits" about a strip that visibly does not.
|
|
433
|
+
*
|
|
434
|
+
* Smallest container width with nothing drawn past the track edge:
|
|
435
|
+
*
|
|
436
|
+
* tabs words glyph + active word glyphs only
|
|
437
|
+
* 2 168px 126px 73px
|
|
438
|
+
* 3 239px 161px 108px
|
|
439
|
+
* 4 320px 196px 143px
|
|
440
|
+
*
|
|
441
|
+
* THE GLYPHS-ONLY COLUMN SAID 120px FOR BOTH 2 AND 3 TABS UNTIL CARD 66248,
|
|
442
|
+
* and it was wrong twice over. `tabs-segmented-fit.html` searched upward from
|
|
443
|
+
* 110px, so it could never have reported anything smaller than 110 whatever the
|
|
444
|
+
* truth was — and 120 is not even that, so a second hand had been on it. The
|
|
445
|
+
* truth is arithmetic and there is nothing to measure: a chip with no word is a
|
|
446
|
+
* glyph in its padding, so the strip is `n × 32px` plus `(n − 1)` gaps plus the
|
|
447
|
+
* track's own padding — 73, 108, 143. The 4-tab cell was right by luck of being
|
|
448
|
+
* above the floor. Re-measured with the search starting at 40px, all three
|
|
449
|
+
* agree with the arithmetic to the pixel; `(d)` now pins them to it, so the
|
|
450
|
+
* cell cannot drift from the sheet again.
|
|
451
|
+
*
|
|
452
|
+
* The drag band gives containers from 181px (a 220px panel, the minimum) to
|
|
453
|
+
* 281px (a 320px panel, the default — the panel minus 28px of body padding
|
|
454
|
+
* minus the 11px reserved scrollbar gutter). Read against it:
|
|
455
|
+
*
|
|
456
|
+
* · TWO tabs never drop a word. 168px is under the 181px floor, so both words
|
|
457
|
+
* survive the whole band — and this is the case Lewis is looking at.
|
|
458
|
+
* · THREE tabs carry their words down to 239px.
|
|
459
|
+
* · FOUR tabs need 320px, which the band never reaches, so a four-bucket
|
|
460
|
+
* section is glyph-plus-one-word at the default panel, and below 196px even
|
|
461
|
+
* that one word stops fitting.
|
|
462
|
+
*
|
|
463
|
+
* WHY THE RULES ARE KEYED ON THE TAB COUNT. With natural widths, whether the
|
|
464
|
+
* words fit is a question about the SUM of the labels, so the answer moves with
|
|
465
|
+
* how many buckets a section has. A width-only rule would have to answer for
|
|
466
|
+
* the worst case — four tabs — and would then strip the word off a TWO-tab
|
|
467
|
+
* strip at the default panel width. That is the exact bug this card exists to
|
|
468
|
+
* remove, so it cannot be the fix for it.
|
|
469
|
+
*
|
|
470
|
+
* Only `.has-icon` is ever touched. A tab with no glyph has nothing to fall
|
|
471
|
+
* back to, so it keeps its word at every width — a button holding neither word
|
|
472
|
+
* nor glyph is not a narrower segment, it is an empty one. */
|
|
473
|
+
|
|
474
|
+
/* THREE OR MORE, under 240px: the words on the tabs he is not reading go. The
|
|
475
|
+
NAME does not go with them — it is still on the button as `title` and
|
|
476
|
+
`aria-label`, in every state. */
|
|
477
|
+
@container (width < 240px) {
|
|
478
|
+
.es-tabs:has(.es-tab:nth-child(3)) .es-tab.has-icon:not(.is-active) .es-tab-word { display: none; }
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/* FOUR OR MORE: a fourth label is another 78px of track, so the same words stop
|
|
482
|
+
fitting a long way sooner. */
|
|
483
|
+
@container (width < 320px) {
|
|
484
|
+
.es-tabs:has(.es-tab:nth-child(4)) .es-tab.has-icon:not(.is-active) .es-tab-word { display: none; }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/* FOUR OR MORE at the narrow end: the last word goes as well, and the strip is
|
|
488
|
+
four glyphs on a track. It reads — the track and the chips are what say
|
|
489
|
+
"these are buttons", which is more than the bare glyphs it replaces ever
|
|
490
|
+
said. */
|
|
491
|
+
@container (width < 200px) {
|
|
492
|
+
.es-tabs:has(.es-tab:nth-child(4)) .es-tab.has-icon.is-active .es-tab-word { display: none; }
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/* ── the segmented control: the same chip, a different promise ─────────────── */
|
|
496
|
+
|
|
497
|
+
/* WHY A SECOND COMPONENT AND NOT A SECOND CALLER OF `SettingsTabs` (card 66248).
|
|
498
|
+
`SettingsTabs` is a TABLIST, and `role="tab"` is the promise that pressing a
|
|
499
|
+
chip switches a panel. The rows this control is for switch nothing: Weight,
|
|
500
|
+
Case and Alignment WRITE A VALUE and stay where they are. Pointing the tab
|
|
501
|
+
strip at them would make three controls that are correct today start
|
|
502
|
+
announcing "tab, 1 of 4, selected" over nothing. Everything a merchant SEES
|
|
503
|
+
is shared above — one chip, one track, one raised-chip mark — and only the
|
|
504
|
+
promise differs. It is the split this sheet already made between
|
|
505
|
+
`.es-group-head` and `button.es-group-head`.
|
|
506
|
+
|
|
507
|
+
── WHY THE @container TIERS ABOVE ARE NOT INHERITED ────────────────────────
|
|
508
|
+
|
|
509
|
+
Those three tiers are keyed on the tab COUNT, and that works for the strip
|
|
510
|
+
they were measured for because the shell KNOWS its four words: Content /
|
|
511
|
+
Media / Style / Layout. It cannot work here, and the reason is arithmetic,
|
|
512
|
+
not taste. MEASURED in Chromium, `tests/fixtures/segmented-fit.html`, the
|
|
513
|
+
width at which each row's words stop fitting:
|
|
514
|
+
|
|
515
|
+
row options words fit from
|
|
516
|
+
Case Normal / Uppercase / Lowercase 3 296px
|
|
517
|
+
Alignment Left / Centre / Right 3 221px
|
|
518
|
+
|
|
519
|
+
Two three-option rows, 75px apart. One threshold at three options is wrong
|
|
520
|
+
for one of them whichever number is picked — and the panel drags between
|
|
521
|
+
181px and 281px, so the wrong one is wrong ON SCREEN: at the default 281px
|
|
522
|
+
panel Alignment's words fit and Case's do not.
|
|
523
|
+
|
|
524
|
+
Whether the words fit is a question about the SUM OF THE LABELS, which is
|
|
525
|
+
what this sheet already says out loud above, and a count is only ever a
|
|
526
|
+
stand-in for it. The stand-in holds while one strip's words are known and
|
|
527
|
+
breaks the moment a second strip brings its own.
|
|
528
|
+
|
|
529
|
+
── SO THE THRESHOLD IS COMPUTED, PER STRIP ─────────────────────────────────
|
|
530
|
+
|
|
531
|
+
Everything in that sum except the words is the CHIP'S, and named on the
|
|
532
|
+
panel root above. Only the words belong to the consumer. So the strip
|
|
533
|
+
declares its own two measurements and this sheet adds the chrome:
|
|
534
|
+
|
|
535
|
+
words-from = the track + every chip with its glyph, its gap and its word
|
|
536
|
+
chosen-from = the track + every chip's glyph, plus one word — the widest,
|
|
537
|
+
because the merchant may be on any option and the strip may
|
|
538
|
+
not overflow on the one that costs most
|
|
539
|
+
|
|
540
|
+
WHY NOT `@container` HERE. A container query condition takes a literal
|
|
541
|
+
length; it cannot read a custom property, so it cannot ask a question whose
|
|
542
|
+
answer differs per strip. Container query UNITS can: `100cqi` is the panel's
|
|
543
|
+
own width, in a place `calc()` can use it. So the comparison is done in the
|
|
544
|
+
property instead of in the query — subtract the threshold from the width and
|
|
545
|
+
scale hard. Above the threshold that is a large positive length and the word
|
|
546
|
+
is unlimited; below it the result is negative, and `max-width` clamps a
|
|
547
|
+
negative to zero, which is the word gone. Same two tiers as the strip above,
|
|
548
|
+
measured continuously instead of at three fixed steps.
|
|
549
|
+
|
|
550
|
+
WHY NOT ESTIMATE THE WORDS FROM THEIR LETTERS, and skip the measuring. The
|
|
551
|
+
chip's font is `--es-font`, which resolves to SF on a Mac, Segoe UI on
|
|
552
|
+
Windows and something else again elsewhere — one shipped table of letter
|
|
553
|
+
widths would be wrong on two platforms out of three. Measured against these
|
|
554
|
+
labels the guess is out by up to 7% either way, which is 12px on a 170px sum:
|
|
555
|
+
enough to drop a word that fits, or keep one that does not.
|
|
556
|
+
|
|
557
|
+
WHY NOT MEASURE IT AT RUNTIME. That is a `ResizeObserver` and an effect, and
|
|
558
|
+
this module is a hook-free leaf a server component can import. The host could
|
|
559
|
+
do it, but then every consumer writes its own and they drift — which is the
|
|
560
|
+
thing this package exists to stop. */
|
|
561
|
+
|
|
562
|
+
/* ── WORD FIT: ONE RULE FOR EVERY STRIP WHOSE WORDS ARE THE HOST'S ─────────
|
|
563
|
+
|
|
564
|
+
Two strips read it: this segmented control, and the top-bar rail
|
|
565
|
+
(`efficient-easel/chrome` `TopBarRail`, owner 2026-09-18: "the rule must be the
|
|
566
|
+
same as the top tabs of the right settings panel"). Both have the tab strip's
|
|
567
|
+
THREE states — every word; the glyphs plus the chosen word; glyphs only —
|
|
568
|
+
and both have words the shell has never seen, so both need the computed
|
|
569
|
+
threshold above rather than the tab strip's count-keyed tiers.
|
|
570
|
+
|
|
571
|
+
It is written ONCE, here, with neutral names. Each strip declares its own
|
|
572
|
+
chrome as four inputs, and the component hands in the three measured ones
|
|
573
|
+
(`wordFitVars`, `src/fit/wordFit.ts`):
|
|
574
|
+
|
|
575
|
+
--es-fit-chip one button with its glyph and no word
|
|
576
|
+
--es-fit-word-gap glyph to word
|
|
577
|
+
--es-fit-gap between two buttons
|
|
578
|
+
--es-fit-ends the strip's own inset at EACH end
|
|
579
|
+
--es-fit-count · --es-fit-labels · --es-fit-widest measured
|
|
580
|
+
|
|
581
|
+
words-from = 2 × ends + (count − 1) × gap + count × (chip + word-gap) + labels
|
|
582
|
+
chosen-from = 2 × ends + (count − 1) × gap + count × chip + word-gap + widest
|
|
583
|
+
|
|
584
|
+
NO MEASUREMENT, NO THRESHOLD. `is-measured` is the component's own answer to
|
|
585
|
+
whether it was given a `fit`, so the arithmetic below is reached by a strip
|
|
586
|
+
that has one and never by a strip that does not. What an unmeasured strip
|
|
587
|
+
does is the strip's own call: a segmented control keeps every word (there is
|
|
588
|
+
nowhere honest to drop one), a top-bar rail stays icon-only (always fits). */
|
|
589
|
+
.es-segs {
|
|
590
|
+
--es-fit-chip: calc(2 * var(--es-chip-pad-x) + var(--es-chip-icon));
|
|
591
|
+
--es-fit-word-gap: var(--es-chip-icon-gap);
|
|
592
|
+
--es-fit-gap: var(--es-track-gap);
|
|
593
|
+
--es-fit-ends: var(--es-track-pad);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.es-segs.is-measured,
|
|
597
|
+
.es-rail-row.is-measured {
|
|
598
|
+
--es-fit-strip: calc(2 * var(--es-fit-ends) + (var(--es-fit-count) - 1) * var(--es-fit-gap));
|
|
599
|
+
|
|
600
|
+
--es-fit-words-from: calc(
|
|
601
|
+
var(--es-fit-strip) +
|
|
602
|
+
var(--es-fit-count) * (var(--es-fit-chip) + var(--es-fit-word-gap)) +
|
|
603
|
+
var(--es-fit-labels)
|
|
604
|
+
);
|
|
605
|
+
--es-fit-chosen-from: calc(
|
|
606
|
+
var(--es-fit-strip) +
|
|
607
|
+
var(--es-fit-count) * var(--es-fit-chip) +
|
|
608
|
+
var(--es-fit-word-gap) +
|
|
609
|
+
var(--es-fit-widest)
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/* THE ROOM LEFT OVER, once this strip's own words are paid for. Positive and
|
|
614
|
+
large while they fit, negative below — and the two properties that read it
|
|
615
|
+
both clamp a negative to zero, which is what makes it a switch rather than a
|
|
616
|
+
width. The 10000 is what makes the change a step and not a fade: without it
|
|
617
|
+
the last pixel before the threshold would draw a one-pixel word.
|
|
618
|
+
|
|
619
|
+
`100cqi` stays a unit until a word USES it, so it is the width of the
|
|
620
|
+
container around that word — the strip's own `-fit` wrapper. */
|
|
621
|
+
.es-segs,
|
|
622
|
+
.es-rail-row.is-measured {
|
|
623
|
+
--es-fit-room-all: calc((100cqi - var(--es-fit-words-from, 0px)) * 10000);
|
|
624
|
+
--es-fit-room-chosen: calc((100cqi - var(--es-fit-chosen-from, 0px)) * 10000);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/* The word is CLIPPED to nothing rather than `display: none`, because the
|
|
628
|
+
switch has to be a length for `max-width` to clamp it. Nothing is lost by
|
|
629
|
+
it: the label is on the button as `title` and `aria-label` in every state, so
|
|
630
|
+
the name a screen reader reads never depended on the span being drawn. */
|
|
631
|
+
.es-seg-word,
|
|
632
|
+
.es-rail-row.is-measured .es-rail-word {
|
|
633
|
+
overflow: hidden;
|
|
634
|
+
max-width: var(--es-fit-room-all);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/* THE CHOSEN SEGMENT KEEPS ITS WORD LONGER — the second tier, and the same one
|
|
638
|
+
the tab strip drops to. A track of bare glyphs still reads, because the track
|
|
639
|
+
and the chips are what say "these are buttons"; a track that also still says
|
|
640
|
+
which value is set reads better, and it costs one word. On the rail "chosen"
|
|
641
|
+
is the button whose panel is open. */
|
|
642
|
+
.es-seg.is-active .es-seg-word,
|
|
643
|
+
.es-rail-row.is-measured .es-rail-btn.is-active .es-rail-word { max-width: var(--es-fit-room-chosen); }
|
|
644
|
+
|
|
645
|
+
/* THE GAP GOES WITH THE WORD IT SEPARATES. The chip's `gap` is a flex gap, and
|
|
646
|
+
a flex gap does not care that the word is now zero wide — it would leave 6px
|
|
647
|
+
of nothing between the glyph and the panel edge and put every glyphs-only
|
|
648
|
+
strip 6px per chip over its real width. So the segment spends the gap as the
|
|
649
|
+
word's OWN margin and switches it with the same room. The tab strip is
|
|
650
|
+
untouched and keeps the flex gap, which is right there: its word is
|
|
651
|
+
`display: none`, so the gap goes with the element, and a tab also has a badge
|
|
652
|
+
on the other side of that word for the gap to separate. */
|
|
653
|
+
.es-seg { gap: 0; }
|
|
654
|
+
.es-seg.has-icon .es-seg-word,
|
|
655
|
+
.es-rail-row.is-measured .es-rail-word { margin-left: clamp(0px, var(--es-fit-room-all), var(--es-fit-word-gap)); }
|
|
656
|
+
.es-seg.has-icon.is-active .es-seg-word,
|
|
657
|
+
.es-rail-row.is-measured .es-rail-btn.is-active .es-rail-word { margin-left: clamp(0px, var(--es-fit-room-chosen), var(--es-fit-word-gap)); }
|
|
658
|
+
|
|
659
|
+
/* ── a segment out of force (card 66303; the RULE is card 66216's) ──────────
|
|
660
|
+
|
|
661
|
+
The setting still EXISTS and still shows the merchant's saved answer. Another
|
|
662
|
+
setting has simply taken the decision away — the Header's "Text colour at the
|
|
663
|
+
top" under a pinned Background, which cannot be transparent. DIMMED, NEVER
|
|
664
|
+
HIDDEN: a control that disappears reads as a feature that was removed, and
|
|
665
|
+
leaves the merchant no way to see what to change to get it back.
|
|
666
|
+
|
|
667
|
+
THE STATE IS ON THE BUTTONS, and `SettingsSegmented` renders it as their own
|
|
668
|
+
`disabled` rather than as a fade a host paints over them. A faded parent
|
|
669
|
+
leaves every segment focusable and announcing as ENABLED, which is the defect
|
|
670
|
+
the 2026-08-06 audit and card 66034 closed; it is also why the prop had to
|
|
671
|
+
come here at all, since nothing outside this component owns those buttons.
|
|
672
|
+
|
|
673
|
+
`--es-disabled` is the shell's existing token for exactly this state, already
|
|
674
|
+
worn by the drill row's quiet glyph. Nothing here is a literal, and nothing
|
|
675
|
+
here removes anything: the track keeps its border and the pressed chip keeps
|
|
676
|
+
its lift, so the strip still READS as a strip with an answer in it. */
|
|
677
|
+
.es-seg:disabled { cursor: not-allowed; color: var(--es-disabled); }
|
|
678
|
+
|
|
679
|
+
/* The chosen one stays the loudest thing on the track. It is still the
|
|
680
|
+
merchant's answer — a pressed chip faded to the same grey as the ones beside
|
|
681
|
+
it stops saying which answer that was.
|
|
682
|
+
|
|
683
|
+
⚠️ THIS COLOUR BELONGS TO THE CHIP'S GROUND, NOT TO THE TRACK (card 66302).
|
|
684
|
+
It was `--es-text-secondary`, which was right while the chip was white:
|
|
685
|
+
10.31:1 on it. The moment the chip went dark that same rule drew
|
|
686
|
+
`--es-text-secondary` on `--es-text` — 1.72:1, an unreadable label on a
|
|
687
|
+
control that had just been made MORE visible. Nothing about the rule changed; what it sat on moved underneath
|
|
688
|
+
it. So it reads `--es-on-accent` for the same reason the enabled chip does,
|
|
689
|
+
and the strip says which option is out of force through the track's fade and
|
|
690
|
+
its quiet siblings rather than by dimming the one word still worth reading. */
|
|
691
|
+
.es-seg:disabled.is-active { color: var(--es-on-accent); }
|
|
692
|
+
|
|
693
|
+
/* And none of them answers the pointer any more: the hover rule above would
|
|
694
|
+
otherwise keep lighting a segment that cannot be chosen. */
|
|
695
|
+
.es-seg:disabled:hover:not(.is-active) { color: var(--es-disabled); }
|
|
696
|
+
|
|
697
|
+
/* THE WHOLE CONTROL, when every option went at once — one statement instead of
|
|
698
|
+
three identically faded chips. The track is the part that says "these buttons
|
|
699
|
+
are one control", so it is the right place to say the control is not in force.
|
|
700
|
+
A single option out of force leaves the track alone, because the strip still
|
|
701
|
+
is. */
|
|
702
|
+
.es-segs[data-disabled] { opacity: 0.5; cursor: not-allowed; }
|
|
703
|
+
|
|
704
|
+
/* ── a group: the soft pill row that folds ─────────────────────────────────── */
|
|
705
|
+
|
|
706
|
+
/* WAS 6px, AND THE 6 NEVER APPLIED (card 66203). `.es-group` is a plain div, so
|
|
707
|
+
this margin is adjoining `.es-group-head`'s own 36px below and the two
|
|
708
|
+
COLLAPSE to the larger — measured in Chromium against these stylesheets, two
|
|
709
|
+
stacked groups sit 36.00px apart, not 6. The rule was dead as written.
|
|
710
|
+
|
|
711
|
+
It is kept, at the floor, because it is a landmine rather than a non-issue: a
|
|
712
|
+
host that gives `.es-group` a border or padding through `className` stops the
|
|
713
|
+
collapsing, and then the 6 is real and is the tightest gap in the panel. At
|
|
714
|
+
`var(--es-row-gap)` the worst case is the floor instead. */
|
|
715
|
+
.es-group + .es-group { margin-top: var(--es-row-gap); }
|
|
716
|
+
|
|
717
|
+
/* ONE pill, two elements. `SettingsGroup` renders a <button> that folds;
|
|
718
|
+
`SettingsGroupHead` renders a heading that folds nothing, because Puck's
|
|
719
|
+
inspector is a flat sibling list and a heading there cannot wrap the rows it
|
|
720
|
+
heads. Everything a merchant SEES is shared here; the press — the pointer
|
|
721
|
+
and the hover tint — is scoped to the button below, so the heading never
|
|
722
|
+
offers a press it cannot honour.
|
|
723
|
+
|
|
724
|
+
`box-sizing` for the same reason the switch sets it: this file assumes no
|
|
725
|
+
reset of the host's, and `width: 100%` plus 22px of padding would otherwise
|
|
726
|
+
run the pill past the panel. */
|
|
727
|
+
.es-group-head {
|
|
728
|
+
box-sizing: border-box;
|
|
729
|
+
width: 100%;
|
|
730
|
+
display: flex;
|
|
731
|
+
align-items: center;
|
|
732
|
+
gap: 8px;
|
|
733
|
+
border: 0;
|
|
734
|
+
text-align: left;
|
|
735
|
+
font: inherit;
|
|
736
|
+
font-size: 12px;
|
|
737
|
+
font-weight: 600;
|
|
738
|
+
color: var(--es-text);
|
|
739
|
+
background: var(--es-hover);
|
|
740
|
+
border-radius: var(--es-row-radius);
|
|
741
|
+
padding: 8px 11px;
|
|
742
|
+
/* A heading needs more air above it than the settings it heads have between
|
|
743
|
+
them (38px), or it reads as one more row. Floored on `--es-row-gap` so it
|
|
744
|
+
rises with the rest of the panel (card 66203). */
|
|
745
|
+
margin: max(var(--es-row-gap), var(--es-rhythm-group)) 0 0;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/* THE QUIET TIER (card 66300) — a heading one rung under the pill.
|
|
749
|
+
NO GROUND AND NO RULE, which is the whole point of it. The pill separates
|
|
750
|
+
itself from the settings around it with a fill; this separates itself with
|
|
751
|
+
case and colour, so the two can sit in one panel without competing. And a
|
|
752
|
+
heading takes no hairline under it: a rule there separates a heading from the
|
|
753
|
+
rows it heads, which is the opposite of what a heading is for. The storefront
|
|
754
|
+
ruled BOTH its tiers that way and the owner's words on 2026-08-31 were
|
|
755
|
+
"these lines, I still find them annoying to look at".
|
|
756
|
+
|
|
757
|
+
THE TYPE IS NOT INVENTED HERE. It is `.sf-field-group` and `.sf-group-label`
|
|
758
|
+
from `efficient-shop`, which are the same look to the decimal, hand-rolled in
|
|
759
|
+
two panels of one editor. Owning it here is what stops a third copy.
|
|
760
|
+
|
|
761
|
+
FLUSH, not inset. `.es-field` has no horizontal padding, so a heading that
|
|
762
|
+
started 11px in would sit to the RIGHT of the rows it heads and read as those
|
|
763
|
+
rows being outdented from it. The pill's text is inset only because a pill has
|
|
764
|
+
padding; this has no such excuse.
|
|
765
|
+
|
|
766
|
+
ONE NOTCH UNDER THE PILL IN AIR AS WELL AS IN TYPE. `--es-rhythm-between`
|
|
767
|
+
rather than the pill's `--es-rhythm-group`, so the tiers stay in order when a
|
|
768
|
+
reader is skimming rather than reading. Written as NAMES: every one of these
|
|
769
|
+
tokens grew between 0.20.0 and 0.23.0 (group 36->48, between 28->38,
|
|
770
|
+
group-first 14->24) and the ladder survived untouched because it reads names.
|
|
771
|
+
Pinned numbers would have crossed. */
|
|
772
|
+
.es-group-sub {
|
|
773
|
+
box-sizing: border-box;
|
|
774
|
+
font-size: 10.5px;
|
|
775
|
+
font-weight: 600;
|
|
776
|
+
letter-spacing: 0.07em;
|
|
777
|
+
text-transform: uppercase;
|
|
778
|
+
color: var(--es-muted);
|
|
779
|
+
margin: max(var(--es-row-gap), var(--es-rhythm-between)) 0
|
|
780
|
+
max(var(--es-row-gap), var(--es-rhythm-group-first));
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/* Directly under a pill it belongs TO that pill, so it does not take a second
|
|
784
|
+
helping of the air the pill already put above itself. */
|
|
785
|
+
.es-group-head + .es-group-sub { margin-top: max(var(--es-row-gap), var(--es-rhythm-group-first)); }
|
|
786
|
+
|
|
787
|
+
button.es-group-head {
|
|
788
|
+
cursor: pointer;
|
|
789
|
+
transition: background 0.12s;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
button.es-group-head:hover { background: var(--es-chip-bg); }
|
|
793
|
+
|
|
794
|
+
.es-group-count {
|
|
795
|
+
min-width: 16px;
|
|
796
|
+
height: 16px;
|
|
797
|
+
padding: 0 5px;
|
|
798
|
+
border-radius: 999px;
|
|
799
|
+
background: var(--es-accent);
|
|
800
|
+
color: var(--es-on-accent);
|
|
801
|
+
font-size: 10px;
|
|
802
|
+
font-weight: 700;
|
|
803
|
+
display: inline-flex;
|
|
804
|
+
align-items: center;
|
|
805
|
+
justify-content: center;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/* The sign is drawn from aria-expanded so the open state has ONE home. */
|
|
809
|
+
.es-group-sign {
|
|
810
|
+
margin-left: auto;
|
|
811
|
+
width: 16px;
|
|
812
|
+
text-align: center;
|
|
813
|
+
color: var(--es-muted);
|
|
814
|
+
font-size: 14px;
|
|
815
|
+
line-height: 1;
|
|
816
|
+
flex: none;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
.es-group-head[aria-expanded='true'] .es-group-sign::before { content: '−'; }
|
|
820
|
+
.es-group-head[aria-expanded='false'] .es-group-sign::before { content: '+'; }
|
|
821
|
+
|
|
822
|
+
/* `hidden` on the body is the fold; this only stops a flex/grid parent from
|
|
823
|
+
overriding the attribute's own display:none. */
|
|
824
|
+
.es-group-body[hidden] { display: none; }
|
|
825
|
+
/* The first setting under a head sits CLOSER than the settings below it do to
|
|
826
|
+
each other — it belongs to that head, and 38px would cut it loose.
|
|
827
|
+
|
|
828
|
+
AND IT IS LIVE FOR THE FIRST TIME (card 66301). It declared 14 against a 16px
|
|
829
|
+
floor, so it has never once been 14 on screen — the floor answered every
|
|
830
|
+
time, and card 66206 measured that very move (14 → 16) and wrote it down as
|
|
831
|
+
the floor working, which it was. At 24 over a 20px floor the declared number
|
|
832
|
+
is finally the visible one, so this seam grows 16 → 24 rather than the 14 →
|
|
833
|
+
24 the file appears to say. `tests/panel-layout.test.ts` (h) is what turns a
|
|
834
|
+
floored rhythm into a failure instead of a footnote. */
|
|
835
|
+
.es-group-body > .es-field:first-child { margin-top: max(var(--es-row-gap), var(--es-rhythm-group-first)); }
|
|
836
|
+
|
|
837
|
+
/* ── one setting ───────────────────────────────────────────────────────────── */
|
|
838
|
+
|
|
839
|
+
/* 38 IS NOT A THIRD OPINION ABOUT THE FLOOR. A setting is TWO lines — a label
|
|
840
|
+
row and its control — held together by the 11px between them, and that 11px
|
|
841
|
+
is the only thing saying which label owns which control. The gap between
|
|
842
|
+
settings has to be plainly larger than the gap inside one or the panel stops
|
|
843
|
+
being a list of settings and becomes a list of loose parts. The pair was
|
|
844
|
+
measured at 28 and 7 and approved from mockups (card 8802.d) — 20 was tried
|
|
845
|
+
there and was too tight — and re-scaled together to 38 and 11 (card 66301),
|
|
846
|
+
which is the same rule with more air, not a new one.
|
|
847
|
+
|
|
848
|
+
Floored, not replaced, and not `calc()`: 38 was measured and then re-scaled,
|
|
849
|
+
it is not "20 plus 18", so arithmetic off the token would claim a
|
|
850
|
+
relationship nobody decided. */
|
|
851
|
+
.es-field { margin-top: max(var(--es-row-gap), var(--es-rhythm-between)); }
|
|
852
|
+
|
|
853
|
+
.es-field-head {
|
|
854
|
+
display: flex;
|
|
855
|
+
align-items: center;
|
|
856
|
+
gap: 8px;
|
|
857
|
+
/* THE ONE THAT SAYS WHICH LABEL OWNS WHICH CONTROL. Named so a consumer can
|
|
858
|
+
read it (card 66228) — it is half of the rule, and the half that is easiest
|
|
859
|
+
to lose when someone re-scales the other half. */
|
|
860
|
+
margin-bottom: var(--es-rhythm-within);
|
|
861
|
+
min-height: 16px;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.es-field-name {
|
|
865
|
+
display: inline-flex;
|
|
866
|
+
align-items: center;
|
|
867
|
+
gap: 8px;
|
|
868
|
+
min-width: 0;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
.es-field-icon {
|
|
872
|
+
width: 16px;
|
|
873
|
+
height: 16px;
|
|
874
|
+
flex: none;
|
|
875
|
+
color: var(--es-icon);
|
|
876
|
+
display: inline-flex;
|
|
877
|
+
align-items: center;
|
|
878
|
+
justify-content: center;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.es-field-icon svg { width: 16px; height: 16px; }
|
|
882
|
+
|
|
883
|
+
.es-field-label {
|
|
884
|
+
font-size: 11px;
|
|
885
|
+
font-weight: 600;
|
|
886
|
+
color: var(--es-muted);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.es-field.is-changed .es-field-label { color: var(--es-accent-text); }
|
|
890
|
+
|
|
891
|
+
.es-field-value {
|
|
892
|
+
margin-left: auto;
|
|
893
|
+
font-size: 12px;
|
|
894
|
+
color: var(--es-text);
|
|
895
|
+
font-variant-numeric: tabular-nums;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.es-field-ctl {
|
|
899
|
+
display: flex;
|
|
900
|
+
align-items: center;
|
|
901
|
+
gap: 8px;
|
|
902
|
+
min-width: 0;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
.es-field-hint {
|
|
906
|
+
margin: 7px 0 0;
|
|
907
|
+
padding-left: 24px;
|
|
908
|
+
font-size: 11px;
|
|
909
|
+
line-height: 1.45;
|
|
910
|
+
color: var(--es-muted);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/* A boolean row is ONE line (card 8801, Option A). The switch rides the label
|
|
914
|
+
line in the `action` slot, so there is no control row under it — and with no
|
|
915
|
+
control row, the label line's 7px bottom margin would be a gap to nothing.
|
|
916
|
+
|
|
917
|
+
A one-line row is one line tall, so it needs less air above it than a
|
|
918
|
+
two-line setting does — 26, not 38 — and it is floored on the token for the
|
|
919
|
+
same reason everything else here is (card 66203).
|
|
920
|
+
|
|
921
|
+
IT TIED WITH THE FLOOR UNTIL NOW (card 66301). At 16 against a 16px floor
|
|
922
|
+
nothing was wrong on screen, but the sheet could not tell you which of the
|
|
923
|
+
two numbers you were reading, and moving either one alone would have moved
|
|
924
|
+
nothing. 26 over 20 puts it plainly on its own number. */
|
|
925
|
+
.es-field.is-inline { margin-top: max(var(--es-row-gap), var(--es-rhythm-between-tight)); }
|
|
926
|
+
.es-field.is-inline .es-field-head { margin-bottom: 0; }
|
|
927
|
+
|
|
928
|
+
/* ── the switch: one yes-or-no shape, both editors ─────────────────────────── */
|
|
929
|
+
|
|
930
|
+
.es-switch {
|
|
931
|
+
appearance: none;
|
|
932
|
+
-webkit-appearance: none;
|
|
933
|
+
box-sizing: border-box;
|
|
934
|
+
margin: 0;
|
|
935
|
+
padding: 0;
|
|
936
|
+
border: 0;
|
|
937
|
+
flex: none;
|
|
938
|
+
position: relative;
|
|
939
|
+
width: 34px;
|
|
940
|
+
height: 20px;
|
|
941
|
+
border-radius: 999px;
|
|
942
|
+
background: var(--es-toggle-off);
|
|
943
|
+
cursor: pointer;
|
|
944
|
+
transition: background 0.15s ease;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/* The knob. `box-sizing` is load-bearing, not tidiness: the locked knob below
|
|
948
|
+
takes a 1px border, and under content-box that border would grow it to 18px
|
|
949
|
+
and eat the 2px gap the whole shape is built on. */
|
|
950
|
+
.es-switch::after {
|
|
951
|
+
content: '';
|
|
952
|
+
box-sizing: border-box;
|
|
953
|
+
position: absolute;
|
|
954
|
+
top: 2px;
|
|
955
|
+
left: 2px;
|
|
956
|
+
width: 16px;
|
|
957
|
+
height: 16px;
|
|
958
|
+
border-radius: 50%;
|
|
959
|
+
background: var(--es-knob);
|
|
960
|
+
/* The knob has to lift off the track, and the shadow is a plain token on
|
|
961
|
+
purpose. A softer mix reads slightly better, but if color-mix() ever fails
|
|
962
|
+
to parse the WHOLE declaration drops and the knob goes flat with no
|
|
963
|
+
warning — not a risk worth carrying in a leaf both editors render
|
|
964
|
+
thousands of times for the sake of a 1px shadow. */
|
|
965
|
+
box-shadow: 0 1px 2px var(--es-input-border);
|
|
966
|
+
transition: transform 0.15s cubic-bezier(0.2, 0.8, 0.3, 1);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/* On is the accent — the same blue as an active tab, so the panel keeps ONE
|
|
970
|
+
accent. The travel is 14px: 34 track − 16 knob − 2 gap − 2 gap. */
|
|
971
|
+
.es-switch[aria-checked='true'] { background: var(--es-accent); }
|
|
972
|
+
.es-switch[aria-checked='true']::after { transform: translateX(14px); }
|
|
973
|
+
|
|
974
|
+
.es-switch:hover:not(:disabled) { filter: brightness(0.96); }
|
|
975
|
+
|
|
976
|
+
.es-switch:focus-visible {
|
|
977
|
+
outline: none;
|
|
978
|
+
box-shadow: 0 0 0 3px var(--es-accent-ring);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/* Held down: the knob stretches the way a physical one would.
|
|
982
|
+
|
|
983
|
+
ON, it must stretch LEFT. The knob is 16px at translateX(14px), so growing it
|
|
984
|
+
to 19px in place would put its right edge at 2 + 14 + 19 = 35px inside a 34px
|
|
985
|
+
track and poke it past the round end. Pulling the travel back to 11px lands
|
|
986
|
+
the right edge on 32 and keeps the 2px gap on both sides. */
|
|
987
|
+
.es-switch:active:not(:disabled)::after { width: 19px; }
|
|
988
|
+
.es-switch[aria-checked='true']:active:not(:disabled)::after { transform: translateX(11px); }
|
|
989
|
+
|
|
990
|
+
/* Locked. Still readable as on or off, but flat and grey, and the knob trades
|
|
991
|
+
its shadow for a hairline so it does not look pressable. */
|
|
992
|
+
.es-switch:disabled { background: var(--es-divider); cursor: not-allowed; }
|
|
993
|
+
.es-switch:disabled[aria-checked='true'] { background: var(--es-input-border); }
|
|
994
|
+
.es-switch:disabled::after { box-shadow: none; border: 1px solid var(--es-divider); }
|
|
995
|
+
|
|
996
|
+
/* ── a drill-in row: name, preview, chevron (card 8819) ────────────────────── */
|
|
997
|
+
|
|
998
|
+
/* The row a section lists a text part as. Pressing it drills the inspector down
|
|
999
|
+
a level to that part's own editor.
|
|
1000
|
+
|
|
1001
|
+
A FULL-WIDTH LIST ROW, not the raised pill a group's head is. These stack into
|
|
1002
|
+
a list at the bottom of the section panel: flat rows, a quiet hover tint, no
|
|
1003
|
+
chip elevation.
|
|
1004
|
+
|
|
1005
|
+
⚠️ THEY ARE SEPARATED BY AIR, NOT BY INK (card 66350). Up to 0.29.0 a hairline
|
|
1006
|
+
was drawn between adjacent rows, `.es-drill + .es-drill`. The owner asked for
|
|
1007
|
+
the settings panel's divider lines to go, repeatedly; card 66334 removed every
|
|
1008
|
+
one the storefront itself draws, and this was the largest source left that the
|
|
1009
|
+
shop could not reach, because it lives here. Sixteen painted lines in one
|
|
1010
|
+
editor, measured. The shop overrode it locally in the meantime, which made
|
|
1011
|
+
this package and its consumer two sources of truth for one line — this
|
|
1012
|
+
deletion is what lets that override go.
|
|
1013
|
+
|
|
1014
|
+
The air between rows does not change with it. It was never the hairline's; it
|
|
1015
|
+
is each row's own padding, on its own side of the seam.
|
|
1016
|
+
|
|
1017
|
+
`box-sizing` for the same reason the switch and the group head set it: this
|
|
1018
|
+
file assumes no reset of the host's, and `width: 100%` plus the padding would
|
|
1019
|
+
otherwise run the row past the panel. */
|
|
1020
|
+
.es-drill {
|
|
1021
|
+
box-sizing: border-box;
|
|
1022
|
+
width: 100%;
|
|
1023
|
+
display: flex;
|
|
1024
|
+
align-items: center;
|
|
1025
|
+
gap: 10px;
|
|
1026
|
+
border: 0;
|
|
1027
|
+
text-align: left;
|
|
1028
|
+
font: inherit;
|
|
1029
|
+
background: transparent;
|
|
1030
|
+
/* THE DRILL LIST SPENDS ITS ROW GAP HERE, AS PADDING, AND THAT IS DELIBERATE
|
|
1031
|
+
(card 66203). The rows' boxes touch and nothing between them adds a margin,
|
|
1032
|
+
so the air a merchant sees is each row's own vertical padding, on its own
|
|
1033
|
+
side of the seam. That was true when a hairline sat on the seam and it is
|
|
1034
|
+
true now that none does — card 66350 removed the INK and moved no spacing,
|
|
1035
|
+
so every number below still stands as its card recorded it. MEASURED in
|
|
1036
|
+
Chromium against these stylesheets (`tests/fixtures/panel-row-spacing.html`):
|
|
1037
|
+
|
|
1038
|
+
token above the seam below it word to word recorded by
|
|
1039
|
+
(each row taken while the
|
|
1040
|
+
hairline was still drawn)
|
|
1041
|
+
10px 11.40px 12.00px 23.40px card 66203
|
|
1042
|
+
16px 17.40px 18.00px 35.40px card 66206
|
|
1043
|
+
16px 17.39px 17.00px 34.39px card 66301
|
|
1044
|
+
20px 21.39px 21.00px 42.39px card 66301
|
|
1045
|
+
|
|
1046
|
+
TWO ROWS FOR 16px, AND THEY DISAGREE BY A PIXEL. 66301's pair was taken in
|
|
1047
|
+
one run of Chromium 124 (zenika/alpine-chrome) so its two rows compare; the
|
|
1048
|
+
earlier rows are left as the cards recorded them rather than overwritten,
|
|
1049
|
+
because the difference is the engine and the font it had, not this sheet.
|
|
1050
|
+
Read a row against another row from the same card, never across cards.
|
|
1051
|
+
|
|
1052
|
+
THIS SEAM MOVES AT TWICE THE TOKEN'S RATE, and it is the only one that
|
|
1053
|
+
does: both rows spend the gap, so +4 on the dial buys +8 of air. Under the
|
|
1054
|
+
floor alone it was also the ONLY place raising the token moved a measured
|
|
1055
|
+
distance rather than a declared one — every other seam was held by a bigger
|
|
1056
|
+
rhythm — which is why it is the row Lewis was looking at when he asked for
|
|
1057
|
+
card 66206. Card 66301 moves the rhythms too, so it is no longer alone.
|
|
1058
|
+
|
|
1059
|
+
A MARGIN WOULD STILL BE THE WRONG FIX, and the reason outlived the line it
|
|
1060
|
+
was first written about. Adjacent vertical margins COLLAPSE, so spending the
|
|
1061
|
+
gap that way makes the floor something two rows negotiate rather than
|
|
1062
|
+
something each row owns on its own side. So the gap stays padding — it just reads the token now, which is the whole point: before
|
|
1063
|
+
this, raising the panel's rhythm moved every other row and left the drill
|
|
1064
|
+
list exactly where it was. */
|
|
1065
|
+
padding: var(--es-row-gap) 11px;
|
|
1066
|
+
cursor: pointer;
|
|
1067
|
+
transition: background 0.12s;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
.es-drill:hover { background: var(--es-hover); }
|
|
1071
|
+
|
|
1072
|
+
/* An INSET ring, unlike the switch's and the tab's outset one: a full-width row
|
|
1073
|
+
has neighbours hard against its edges, and an outset ring would be clipped by
|
|
1074
|
+
the row above and below it. */
|
|
1075
|
+
.es-drill:focus-visible {
|
|
1076
|
+
outline: none;
|
|
1077
|
+
box-shadow: inset 0 0 0 2px var(--es-accent-ring);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/* The two lines, stacked, and allowed to shrink so the preview can ellipsise
|
|
1081
|
+
instead of shoving the chevron off the row. */
|
|
1082
|
+
.es-drill-text {
|
|
1083
|
+
display: flex;
|
|
1084
|
+
flex-direction: column;
|
|
1085
|
+
gap: 2px;
|
|
1086
|
+
min-width: 0;
|
|
1087
|
+
flex: 1;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.es-drill-name {
|
|
1091
|
+
font-size: 12px;
|
|
1092
|
+
font-weight: 600;
|
|
1093
|
+
color: var(--es-text-strong);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/* The preview is one line, clipped. It never wraps: a part's copy can be a
|
|
1097
|
+
paragraph, and a row that grows to fit it stops being a row. */
|
|
1098
|
+
.es-drill-preview {
|
|
1099
|
+
font-size: 11px;
|
|
1100
|
+
color: var(--es-muted);
|
|
1101
|
+
white-space: nowrap;
|
|
1102
|
+
overflow: hidden;
|
|
1103
|
+
text-overflow: ellipsis;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/* A part with no place on the page — an identifier the eye never draws. Quiet
|
|
1107
|
+
and italic, so it reads as a note about the part rather than as its copy. */
|
|
1108
|
+
.es-drill-preview.is-offpage {
|
|
1109
|
+
font-style: italic;
|
|
1110
|
+
color: var(--es-disabled);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/* The status glyph (the "hidden on the page" eye). Not `aria-hidden` here — the
|
|
1114
|
+
host owns its meaning and names it; this only places it. */
|
|
1115
|
+
.es-drill-status {
|
|
1116
|
+
flex: none;
|
|
1117
|
+
display: inline-flex;
|
|
1118
|
+
align-items: center;
|
|
1119
|
+
color: var(--es-muted);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/* The chevron is drawn from CSS `content`, the way the group sign is: it is pure
|
|
1123
|
+
direction, carries no text of its own, and stays out of the accessible name. */
|
|
1124
|
+
.es-drill-chev {
|
|
1125
|
+
flex: none;
|
|
1126
|
+
width: 16px;
|
|
1127
|
+
text-align: center;
|
|
1128
|
+
color: var(--es-muted);
|
|
1129
|
+
font-size: 16px;
|
|
1130
|
+
line-height: 1;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
.es-drill-chev::before { content: '\203A'; }
|
|
1134
|
+
|
|
1135
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1136
|
+
.es-tab,
|
|
1137
|
+
.es-seg,
|
|
1138
|
+
/* `button.` and not `.es-group-head`: the transition it cancels is declared
|
|
1139
|
+
on `button.es-group-head`, and a plainer selector would lose to it. */
|
|
1140
|
+
button.es-group-head,
|
|
1141
|
+
.es-drill,
|
|
1142
|
+
.es-switch,
|
|
1143
|
+
.es-switch::after { transition: none; }
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/* ── SettingsPlacement — the 3×3 position grid ─────────────────────────────
|
|
1147
|
+
One control for two questions a merchant asks the same way: which part of a
|
|
1148
|
+
cropped picture stays in view, and where on the thing behind it a block sits.
|
|
1149
|
+
Both are one of nine positions, and both want the corner PRESSED.
|
|
1150
|
+
|
|
1151
|
+
Promoted out of efficient-shop (`.sf-focal`) the day its own rule fired —
|
|
1152
|
+
card 66189 wrote "the day a second section wants it, that is the card that
|
|
1153
|
+
promotes it"; 66214 declined it early and said why; the sections that hold
|
|
1154
|
+
boxes take it past six callers. The values below are the shop's, token for
|
|
1155
|
+
token, so every existing caller draws exactly what it drew: the only change
|
|
1156
|
+
is that `--sf-*` aliases have become the `--es-*` names they always aliased. */
|
|
1157
|
+
.es-placement {
|
|
1158
|
+
display: grid;
|
|
1159
|
+
grid-template-columns: repeat(3, 1fr);
|
|
1160
|
+
grid-template-rows: repeat(3, 1fr);
|
|
1161
|
+
gap: 4px;
|
|
1162
|
+
width: 96px;
|
|
1163
|
+
height: 96px;
|
|
1164
|
+
padding: 4px;
|
|
1165
|
+
background: var(--es-track-bg);
|
|
1166
|
+
border: 1px solid var(--es-input-border);
|
|
1167
|
+
border-radius: 9px;
|
|
1168
|
+
}
|
|
1169
|
+
.es-placement-cell {
|
|
1170
|
+
border: none;
|
|
1171
|
+
background: transparent;
|
|
1172
|
+
border-radius: 6px;
|
|
1173
|
+
cursor: pointer;
|
|
1174
|
+
padding: 0;
|
|
1175
|
+
display: flex;
|
|
1176
|
+
align-items: center;
|
|
1177
|
+
justify-content: center;
|
|
1178
|
+
transition: background 0.12s;
|
|
1179
|
+
}
|
|
1180
|
+
.es-placement-cell:hover { background: var(--es-hover); }
|
|
1181
|
+
/* The dot is the whole of the cell's state: a lit cell grows it and paints it
|
|
1182
|
+
the accent. Size and colour move together so the chosen square reads at a
|
|
1183
|
+
glance and still reads for someone who cannot separate the two colours. */
|
|
1184
|
+
.es-placement-dot {
|
|
1185
|
+
width: 7px;
|
|
1186
|
+
height: 7px;
|
|
1187
|
+
border-radius: 50%;
|
|
1188
|
+
background: var(--es-muted);
|
|
1189
|
+
opacity: 0.5;
|
|
1190
|
+
transition: transform 0.12s, background 0.12s, opacity 0.12s;
|
|
1191
|
+
}
|
|
1192
|
+
.es-placement-cell:hover .es-placement-dot { opacity: 0.85; }
|
|
1193
|
+
/* ⚠️ THE CHOSEN CELL IS THE PANEL'S CHIP, NOT THE SHOP'S TINT — and that is a
|
|
1194
|
+
VISIBLE change to the control the shop ships today. `.sf-focal` marked its
|
|
1195
|
+
chosen square with `--sf-accent-tint` and a bigger accent dot; that is card
|
|
1196
|
+
8811's answer, which cards 66138 and 66302 replaced everywhere else in this
|
|
1197
|
+
panel. The strip tells the chosen chip apart by being the DARKEST thing in
|
|
1198
|
+
the row on a grey track, and a nine-square asks the identical question —
|
|
1199
|
+
which one am I on — so it gets the identical answer. Two answers to one
|
|
1200
|
+
question, one panel apart, is the thing this package exists to prevent, and
|
|
1201
|
+
`panel-layout.test.ts` bans the tint token package-wide to keep it that way.
|
|
1202
|
+
The dot inverts with the ground, or it disappears into it. */
|
|
1203
|
+
.es-placement-cell.is-active {
|
|
1204
|
+
background: var(--es-text);
|
|
1205
|
+
box-shadow: var(--es-raised-shadow);
|
|
1206
|
+
}
|
|
1207
|
+
.es-placement-cell.is-active .es-placement-dot {
|
|
1208
|
+
background: var(--es-on-accent);
|
|
1209
|
+
opacity: 1;
|
|
1210
|
+
transform: scale(1.5);
|
|
1211
|
+
}
|
|
1212
|
+
/* A position the caller has closed. It keeps its square — nine that became
|
|
1213
|
+
eight would read as a broken control — and stops answering the pointer; the
|
|
1214
|
+
reason is on the button's `title`. */
|
|
1215
|
+
.es-placement-cell.is-off { cursor: default; }
|
|
1216
|
+
.es-placement-cell.is-off:hover { background: transparent; }
|
|
1217
|
+
.es-placement-cell.is-off .es-placement-dot { opacity: 0.22; }
|
|
1218
|
+
/* A cell that draws a GLYPH instead of its dot — the Scrolling Bar's
|
|
1219
|
+
`Scroll direction`, where four live cells mean a direction of travel and a
|
|
1220
|
+
dot cannot say which way. It follows the cell through every state the dot
|
|
1221
|
+
does, or the chosen square goes dark and the arrow on it stays muted and
|
|
1222
|
+
disappears into the ground it is drawn on.
|
|
1223
|
+
13px, sized against the CELL and not against the settings column: a cell is
|
|
1224
|
+
26.67px across (96px box, 3×3, gap 4, padding 4) and the dot it stands in for
|
|
1225
|
+
is 7px, so 13px reads as a direction without crowding the 6px cell radius. */
|
|
1226
|
+
.es-placement-icon {
|
|
1227
|
+
display: flex;
|
|
1228
|
+
align-items: center;
|
|
1229
|
+
justify-content: center;
|
|
1230
|
+
color: var(--es-muted);
|
|
1231
|
+
opacity: 0.5;
|
|
1232
|
+
transition: color 0.12s, opacity 0.12s;
|
|
1233
|
+
}
|
|
1234
|
+
.es-placement-icon svg { width: 13px; height: 13px; display: block; }
|
|
1235
|
+
.es-placement-cell:hover .es-placement-icon { opacity: 0.85; }
|
|
1236
|
+
/* ⚠️ IT DOES NOT GROW THE WAY THE DOT DOES. The dot scales to 1.5 on the chosen
|
|
1237
|
+
cell because a dot has no other way to say "chosen"; an arrow says its whole
|
|
1238
|
+
meaning by its shape, and growing it would crowd the square and make it read
|
|
1239
|
+
heavier than its three siblings. Colour alone marks it, on the inverted
|
|
1240
|
+
ground the cell already paints. */
|
|
1241
|
+
.es-placement-cell.is-active .es-placement-icon {
|
|
1242
|
+
color: var(--es-on-accent);
|
|
1243
|
+
opacity: 1;
|
|
1244
|
+
}
|
|
1245
|
+
.es-placement-cell.is-off .es-placement-icon { opacity: 0.22; }
|
|
1246
|
+
/* Keyboard focus is visible, like every other control in this panel. */
|
|
1247
|
+
.es-placement-cell:focus-visible {
|
|
1248
|
+
outline: 2px solid var(--es-accent-ring);
|
|
1249
|
+
outline-offset: 1px;
|
|
1250
|
+
}
|
|
1251
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1252
|
+
.es-placement-cell,
|
|
1253
|
+
.es-placement-icon,
|
|
1254
|
+
.es-placement-dot { transition: none; }
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/* ── SettingsCorners — four corners of a box, any number of them lit ─────────
|
|
1258
|
+
The sibling of `.es-placement` above and deliberately built from its parts:
|
|
1259
|
+
the same track, the same chip on the chosen cell, the same focus ring. A
|
|
1260
|
+
merchant who has met the nine-square should not have to learn this one.
|
|
1261
|
+
|
|
1262
|
+
TWO COLUMNS, NOT THREE. The grid is 2×2 and each cell sits in the corner it
|
|
1263
|
+
stands for, so the control has the shape of the thing it edits. */
|
|
1264
|
+
.es-corners {
|
|
1265
|
+
position: relative;
|
|
1266
|
+
display: grid;
|
|
1267
|
+
grid-template-columns: repeat(2, 1fr);
|
|
1268
|
+
grid-template-rows: repeat(2, 1fr);
|
|
1269
|
+
gap: 4px;
|
|
1270
|
+
width: 96px;
|
|
1271
|
+
height: 96px;
|
|
1272
|
+
padding: 4px;
|
|
1273
|
+
background: var(--es-track-bg);
|
|
1274
|
+
border-radius: 8px;
|
|
1275
|
+
}
|
|
1276
|
+
/* The BOX the four corners belong to. Inset so the cells sit ON its corners
|
|
1277
|
+
rather than beside it, dashed so it reads as the outline of the thing being
|
|
1278
|
+
edited rather than as a fifth control. See the note in SettingsCorners.tsx:
|
|
1279
|
+
without this the cells are four switches, not one box. */
|
|
1280
|
+
.es-corners-frame {
|
|
1281
|
+
position: absolute;
|
|
1282
|
+
inset: 22px;
|
|
1283
|
+
border: 1.5px dashed var(--es-muted);
|
|
1284
|
+
opacity: 0.45;
|
|
1285
|
+
pointer-events: none;
|
|
1286
|
+
}
|
|
1287
|
+
.es-corners-cell {
|
|
1288
|
+
position: relative;
|
|
1289
|
+
border: none;
|
|
1290
|
+
background: transparent;
|
|
1291
|
+
border-radius: 6px;
|
|
1292
|
+
cursor: pointer;
|
|
1293
|
+
padding: 0;
|
|
1294
|
+
display: flex;
|
|
1295
|
+
align-items: center;
|
|
1296
|
+
justify-content: center;
|
|
1297
|
+
}
|
|
1298
|
+
.es-corners-cell:hover { background: var(--es-hover); }
|
|
1299
|
+
/* THE MARK IS THE ANSWER, drawn rather than labelled. Each cell holds a square
|
|
1300
|
+
whose MATCHING corner is rounded when the corner is on and square when it is
|
|
1301
|
+
off, so the state is carried by SHAPE as well as by fill — which is what
|
|
1302
|
+
keeps it readable for someone who cannot separate the accent from the
|
|
1303
|
+
ground. The per-corner radius is set by the four rules below. */
|
|
1304
|
+
.es-corners-mark {
|
|
1305
|
+
width: 18px;
|
|
1306
|
+
height: 18px;
|
|
1307
|
+
background: var(--es-muted);
|
|
1308
|
+
opacity: 0.5;
|
|
1309
|
+
transition: border-radius 0.12s, background 0.12s, opacity 0.12s;
|
|
1310
|
+
}
|
|
1311
|
+
.es-corners-cell:hover .es-corners-mark { opacity: 0.85; }
|
|
1312
|
+
/* ⚠️ THE FOUR RULES ARE WRITTEN OUT, ONE PER CORNER, and must stay written out.
|
|
1313
|
+
The corner a cell stands for is in its class name (`es-corners-tl`), and a
|
|
1314
|
+
radius built by interpolating that name is a string no stylesheet contains —
|
|
1315
|
+
the same trap the shop documents on its own card wells. Four rules is the
|
|
1316
|
+
whole cost of never having that bug. */
|
|
1317
|
+
.es-corners-cell.is-on .es-corners-mark {
|
|
1318
|
+
background: var(--es-accent);
|
|
1319
|
+
opacity: 1;
|
|
1320
|
+
}
|
|
1321
|
+
.es-corners-tl.is-on .es-corners-mark { border-radius: 8px 0 0 0; }
|
|
1322
|
+
.es-corners-tr.is-on .es-corners-mark { border-radius: 0 8px 0 0; }
|
|
1323
|
+
.es-corners-br.is-on .es-corners-mark { border-radius: 0 0 8px 0; }
|
|
1324
|
+
.es-corners-bl.is-on .es-corners-mark { border-radius: 0 0 0 8px; }
|
|
1325
|
+
/* A corner the caller has closed. It keeps its cell — four that became three
|
|
1326
|
+
would read as a broken control — and stops answering the pointer; the reason
|
|
1327
|
+
is on the button's `title`. */
|
|
1328
|
+
.es-corners-cell.is-off { cursor: default; }
|
|
1329
|
+
.es-corners-cell.is-off:hover { background: transparent; }
|
|
1330
|
+
.es-corners-cell.is-off .es-corners-mark { opacity: 0.22; }
|
|
1331
|
+
/* Keyboard focus is visible, like every other control in this panel. */
|
|
1332
|
+
.es-corners-cell:focus-visible {
|
|
1333
|
+
outline: 2px solid var(--es-accent-ring);
|
|
1334
|
+
outline-offset: 1px;
|
|
1335
|
+
}
|
|
1336
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1337
|
+
.es-corners-mark { transition: none; }
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/* ── THE ACTION BUTTON (`[ADD] A Panel Button That Does One Thing`) ─────────────────────────────────────────
|
|
1341
|
+
The full-width press under a group's rows that COMMITS — "Apply to all
|
|
1342
|
+
cards". Measured against `.es-drill`, the panel's other full-width row, and
|
|
1343
|
+
NOT against the shop's `.sf-add-card`: this is a settings row like the rest
|
|
1344
|
+
of the panel, so it takes the panel's row width and the panel's radius.
|
|
1345
|
+
|
|
1346
|
+
IT LOOKS LIKE A BUTTON AND NOT LIKE A ROW, deliberately. The drill is flat
|
|
1347
|
+
with a hover tint, because a list of doorways is a list. This one is a single
|
|
1348
|
+
press that writes, so it carries a border and a field ground: a merchant
|
|
1349
|
+
should be able to tell at a glance that the thing at the bottom of the group
|
|
1350
|
+
is not the twelfth setting.
|
|
1351
|
+
|
|
1352
|
+
NO TOP RULE. The air above it is the separator — the same discipline card
|
|
1353
|
+
66350 applied to the drill list, and `panel-no-divider-lines.test.ts` is what
|
|
1354
|
+
keeps it. */
|
|
1355
|
+
.es-action-wrap {
|
|
1356
|
+
/* IT BELONGS TO THE ROWS ABOVE IT, so it sits closer than one setting sits to
|
|
1357
|
+
the next (38) and plainly apart from the last row. The floor rides
|
|
1358
|
+
underneath as `max()`, like every other row-to-row seam on this panel. */
|
|
1359
|
+
margin-top: max(var(--es-row-gap), var(--es-rhythm-between-tight));
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
.es-action {
|
|
1363
|
+
box-sizing: border-box;
|
|
1364
|
+
width: 100%;
|
|
1365
|
+
display: flex;
|
|
1366
|
+
align-items: center;
|
|
1367
|
+
justify-content: center;
|
|
1368
|
+
gap: 8px;
|
|
1369
|
+
padding: 10px 11px;
|
|
1370
|
+
border: 1px solid var(--es-btn-border);
|
|
1371
|
+
border-radius: var(--es-row-radius);
|
|
1372
|
+
background: var(--es-field-bg);
|
|
1373
|
+
color: var(--es-btn-text);
|
|
1374
|
+
font: inherit;
|
|
1375
|
+
font-size: 12px;
|
|
1376
|
+
font-weight: 600;
|
|
1377
|
+
cursor: pointer;
|
|
1378
|
+
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
.es-action:hover:not(:disabled) {
|
|
1382
|
+
background: var(--es-hover);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/* An INSET ring, for the reason the drill gives: a full-width row has the
|
|
1386
|
+
panel's rows hard against its edges, and an outset ring would be clipped. */
|
|
1387
|
+
.es-action:focus-visible {
|
|
1388
|
+
outline: none;
|
|
1389
|
+
box-shadow: inset 0 0 0 2px var(--es-accent-ring);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/* UNAVAILABLE, AND STILL THERE. Dimmed rather than hidden — a control that
|
|
1393
|
+
vanishes leaves a merchant hunting for something they remember, and takes the
|
|
1394
|
+
reason away with it. The reason is the note below, which a screen reader
|
|
1395
|
+
reaches through `aria-describedby` whether the button answers a pointer or
|
|
1396
|
+
not; a `title` here would reach neither. */
|
|
1397
|
+
.es-action:disabled {
|
|
1398
|
+
cursor: default;
|
|
1399
|
+
color: var(--es-disabled);
|
|
1400
|
+
background: var(--es-track-bg);
|
|
1401
|
+
border-color: var(--es-line);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/* THE PRESS HAS LANDED.
|
|
1405
|
+
|
|
1406
|
+
THE WORD IS THE STATE, and the colour agrees with it — the button says
|
|
1407
|
+
"Applied to 12 cards" and the live region announces the same string, so
|
|
1408
|
+
nothing here is carried by colour alone.
|
|
1409
|
+
|
|
1410
|
+
⚠️ NOT A TINTED GROUND. `--es-accent-tint` is card 8811's retired look and
|
|
1411
|
+
this sheet no longer draws it anywhere; `panel-layout.test.ts` sweeps the
|
|
1412
|
+
whole file for it. So the confirmation is drawn the way the rest of the panel
|
|
1413
|
+
draws a positive: the text and the edge move, the ground does not. */
|
|
1414
|
+
.es-action.is-done {
|
|
1415
|
+
color: var(--es-success);
|
|
1416
|
+
border-color: var(--es-success);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
.es-action-icon {
|
|
1420
|
+
flex: none;
|
|
1421
|
+
display: inline-flex;
|
|
1422
|
+
align-items: center;
|
|
1423
|
+
color: currentColor;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
.es-action-label {
|
|
1427
|
+
min-width: 0;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
/* Why the press is unavailable, or what it is about to rewrite. Quiet, under
|
|
1431
|
+
the button, and wrapped — this is a clause, not a name, the same split
|
|
1432
|
+
`.es-field-hint` makes. */
|
|
1433
|
+
.es-action-note {
|
|
1434
|
+
margin: 6px 0 0;
|
|
1435
|
+
font-size: 11px;
|
|
1436
|
+
line-height: 1.4;
|
|
1437
|
+
color: var(--es-muted);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/* THE ANNOUNCEMENT, and nothing on screen. The button already shows the state;
|
|
1441
|
+
this exists so a merchant who cannot see the button HEARS that eleven cards
|
|
1442
|
+
they were not looking at have changed. Taken off screen the standard way —
|
|
1443
|
+
`display: none` and `visibility: hidden` are both skipped by screen readers,
|
|
1444
|
+
which would leave this rendering nothing at all. */
|
|
1445
|
+
.es-action-live {
|
|
1446
|
+
position: absolute;
|
|
1447
|
+
width: 1px;
|
|
1448
|
+
height: 1px;
|
|
1449
|
+
margin: -1px;
|
|
1450
|
+
padding: 0;
|
|
1451
|
+
overflow: hidden;
|
|
1452
|
+
clip: rect(0 0 0 0);
|
|
1453
|
+
clip-path: inset(50%);
|
|
1454
|
+
white-space: nowrap;
|
|
1455
|
+
border: 0;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1459
|
+
.es-action { transition: none; }
|
|
1460
|
+
}
|