editor-shell 0.21.0 → 0.23.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/dist/panel/index.d.ts +125 -14
- package/dist/panel/index.js +8 -3
- package/dist/panel/index.js.map +1 -1
- package/dist/panel/panel.css +123 -40
- package/package.json +1 -1
package/dist/panel/index.d.ts
CHANGED
|
@@ -97,16 +97,51 @@ interface SettingsSegmentedFit {
|
|
|
97
97
|
*/
|
|
98
98
|
widestLabelPx: number;
|
|
99
99
|
}
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
/**
|
|
101
|
+
* WHAT AN OPTION IS CALLED, and the one rule that governs it: EVERY option has a
|
|
102
|
+
* name, and the name is a string (card 66303).
|
|
103
|
+
*
|
|
104
|
+
* `label` is spent three ways — the visible mark, `title`, and `aria-label` — so
|
|
105
|
+
* it cannot simply be widened to a `ReactNode` and left there: a node reaches
|
|
106
|
+
* the two attributes as `[object Object]`, which is a tooltip that says nothing
|
|
107
|
+
* and a screen reader that says worse. Nor can it be split into `icon` + `label`
|
|
108
|
+
* and the word left to fall away at a threshold: both spans render, so a strip
|
|
109
|
+
* whose mark IS its label would draw the mark AND the word beside it.
|
|
110
|
+
*
|
|
111
|
+
* So the two cases are told apart in the type, and each carries its own name:
|
|
112
|
+
*
|
|
113
|
+
* a WORDED option `{ label: 'Left' }` — the word is the name. `title` is
|
|
114
|
+
* optional and almost never given; this is every option that
|
|
115
|
+
* exists today and none of them changes.
|
|
116
|
+
* a DRAWN option `{ label: <AlignGlyph/>, title: 'Left' }` — the mark cannot
|
|
117
|
+
* be read aloud, so `title` is REQUIRED and is the name.
|
|
118
|
+
*
|
|
119
|
+
* The part panel's Alignment row is the case that forced it: three alignments
|
|
120
|
+
* have no honest three-letter word between them, so the label is the rule
|
|
121
|
+
* itself. Case is the same shape one step milder — Aa / AA / aa are letterforms,
|
|
122
|
+
* not the words Normal / Uppercase / Lowercase that name them. Passing `title`
|
|
123
|
+
* beside a WORDED label is allowed and overrides the word, which is what a
|
|
124
|
+
* strip whose words are abbreviations wants.
|
|
125
|
+
*/
|
|
126
|
+
type SettingsSegmentedLabel = {
|
|
127
|
+
label: string;
|
|
128
|
+
title?: string;
|
|
129
|
+
} | {
|
|
130
|
+
label: ReactNode;
|
|
131
|
+
title: string;
|
|
132
|
+
};
|
|
133
|
+
interface SettingsSegmentedOptionBase<V = string> {
|
|
104
134
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
135
|
+
* The VALUE this option writes, as `value` and `onChange` name it.
|
|
136
|
+
*
|
|
137
|
+
* `V` and not `string` since card 66303. A weight is a NUMBER — the storefront
|
|
138
|
+
* part panel's row is `[400, 500, 600, 700]` — and "Image side" is a BOOLEAN
|
|
139
|
+
* on every published page across every tenant, so giving those rows the house
|
|
140
|
+
* strip by storing "left"/"right" would be a migration of live customer data
|
|
141
|
+
* rather than a control change. `V` defaults to `string`, so every existing
|
|
142
|
+
* declaration means exactly what it did.
|
|
108
143
|
*/
|
|
109
|
-
|
|
144
|
+
id: V;
|
|
110
145
|
/**
|
|
111
146
|
* A 16px glyph from `editor-shell/icons`, so the option can spend less width
|
|
112
147
|
* than its word needs when the panel is dragged narrow.
|
|
@@ -115,9 +150,38 @@ interface SettingsSegmentedOption {
|
|
|
115
150
|
* has nothing to fall back to, so it keeps its word at every width. Pass
|
|
116
151
|
* glyphs for ALL the options or none of them — half words and half glyphs
|
|
117
152
|
* reads as a rendering fault, not as a decision.
|
|
153
|
+
*
|
|
154
|
+
* NOT the same thing as a DRAWN label. An `icon` is a glyph BESIDE the word,
|
|
155
|
+
* and the word is what it falls back from; a drawn label replaces the word
|
|
156
|
+
* altogether and is named by its `title`. An option wanting the second one
|
|
157
|
+
* puts the mark in `label`, not here.
|
|
118
158
|
*/
|
|
119
159
|
icon?: ReactNode;
|
|
160
|
+
/**
|
|
161
|
+
* This choice exists but cannot be taken right now.
|
|
162
|
+
*
|
|
163
|
+
* Renders the button's own `disabled`, not a fade over it, and that is the
|
|
164
|
+
* whole point of it living here: this component owns the `<button>`, so a host
|
|
165
|
+
* dimming a parent instead leaves every segment focusable and announcing as
|
|
166
|
+
* ENABLED — the exact defect the 2026-08-06 audit and card 66034 closed.
|
|
167
|
+
*
|
|
168
|
+
* Dimmed, NEVER hidden, and the pressed segment still reads as pressed: the
|
|
169
|
+
* merchant's saved answer is still their answer, it simply is not in force.
|
|
170
|
+
* Defaults absent, so nothing an existing caller renders moves.
|
|
171
|
+
*
|
|
172
|
+
* NO CONSUMER TODAY, and it is carried on purpose rather than by oversight —
|
|
173
|
+
* so the next reader does not find it unused and tidy it away. The case it is
|
|
174
|
+
* for is one row away: card 66246 draws Weight as four letterform glyphs, and
|
|
175
|
+
* a merchant's typeface may only carry two real weights (measured there —
|
|
176
|
+
* Helvetica, Georgia, Verdana and Impact render Regular and Medium as the same
|
|
177
|
+
* letter, and Semibold and Bold as the same letter). That is a choice the
|
|
178
|
+
* strip CAN offer and the font cannot honour, which is this field and not the
|
|
179
|
+
* strip-level one beside it.
|
|
180
|
+
*/
|
|
181
|
+
disabled?: boolean;
|
|
120
182
|
}
|
|
183
|
+
/** One option in a segmented control. */
|
|
184
|
+
type SettingsSegmentedOption<V = string> = SettingsSegmentedOptionBase<V> & SettingsSegmentedLabel;
|
|
121
185
|
/**
|
|
122
186
|
* A SEGMENTED CONTROL — the tab strip's chip, with the other promise.
|
|
123
187
|
*
|
|
@@ -135,11 +199,24 @@ interface SettingsSegmentedOption {
|
|
|
135
199
|
* plain tab-stopped buttons would promise a keyboard that is not there, which
|
|
136
200
|
* is the same defect as `role="tab"` over no panel.
|
|
137
201
|
*/
|
|
138
|
-
interface SettingsSegmentedProps {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
202
|
+
interface SettingsSegmentedProps<V = string> {
|
|
203
|
+
/**
|
|
204
|
+
* `readonly` so a panel may declare its option table `as const` and keep the
|
|
205
|
+
* literal value types — which is how every one of them is already written.
|
|
206
|
+
* A widening only: a mutable array still binds.
|
|
207
|
+
*/
|
|
208
|
+
options: readonly SettingsSegmentedOption<V>[];
|
|
209
|
+
/**
|
|
210
|
+
* The `id` of the option currently set.
|
|
211
|
+
*
|
|
212
|
+
* A value matching NO option presses nothing, and that is a state a caller
|
|
213
|
+
* relies on rather than an edge: the part panel hands `""` when the selection
|
|
214
|
+
* is mixed across several parts. The match is an EQUALITY and never a
|
|
215
|
+
* truthiness test — `false` is a real answer ("image left"), and `value ||
|
|
216
|
+
* fallback` collapses it into the other one.
|
|
217
|
+
*/
|
|
218
|
+
value: V;
|
|
219
|
+
onChange: (next: V) => void;
|
|
143
220
|
/**
|
|
144
221
|
* What the group is called — "Weight", "Alignment". Required: a group of
|
|
145
222
|
* pressed buttons with no name is a set a screen reader cannot introduce.
|
|
@@ -152,6 +229,22 @@ interface SettingsSegmentedProps {
|
|
|
152
229
|
* this too, or the glyphs have nothing to fall back at.
|
|
153
230
|
*/
|
|
154
231
|
fit?: SettingsSegmentedFit;
|
|
232
|
+
/**
|
|
233
|
+
* The whole strip exists but cannot apply right now, because another setting
|
|
234
|
+
* has taken the decision away — the Header's "Text colour at the top" under a
|
|
235
|
+
* pinned Background (card 66216).
|
|
236
|
+
*
|
|
237
|
+
* Every segment goes out of force AND the track says so, so the control goes
|
|
238
|
+
* quiet as one thing rather than as three identically faded chips. Per-option
|
|
239
|
+
* `disabled` is the other half of this: use that for a choice the strip cannot
|
|
240
|
+
* offer, and this for a strip the panel cannot offer.
|
|
241
|
+
*
|
|
242
|
+
* Dimmed, never hidden — a control that disappears reads as a feature that was
|
|
243
|
+
* removed, and leaves the merchant no way to see what to change to get it
|
|
244
|
+
* back. Defaults false, so every existing caller renders byte for byte as it
|
|
245
|
+
* did.
|
|
246
|
+
*/
|
|
247
|
+
disabled?: boolean;
|
|
155
248
|
className?: string;
|
|
156
249
|
}
|
|
157
250
|
interface SettingsGroupProps {
|
|
@@ -406,8 +499,26 @@ declare function SettingsTabs({ items, activeId, onSelect, ariaLabel, className,
|
|
|
406
499
|
*
|
|
407
500
|
* Without `fit` nothing drops — an unmeasured strip keeps every word at every
|
|
408
501
|
* width, which is exactly what a strip with no glyphs already does.
|
|
502
|
+
*
|
|
503
|
+
* ── WHAT CARD 66303 WIDENED, AND WHY IT HAD TO BE HERE ───────────────────────
|
|
504
|
+
*
|
|
505
|
+
* `efficient-shop` draws its own `Segmented` beside this one and could not stop:
|
|
506
|
+
* four things twelve live call sites need were missing here. Generic `V`
|
|
507
|
+
* (weights are numbers, "image side" is a boolean on every published page), a
|
|
508
|
+
* DRAWN label with its own `title`, and `disabled` — on one option or on the
|
|
509
|
+
* strip.
|
|
510
|
+
*
|
|
511
|
+
* The last two could not have been bridged by a wrapper the shop writes, which
|
|
512
|
+
* is the reason they are props and not a consumer's problem. This component
|
|
513
|
+
* renders the `<button>`s, so nothing outside it can disable them; dimming a
|
|
514
|
+
* parent instead leaves them focusable and announcing as ENABLED, which is the
|
|
515
|
+
* regression the 2026-08-06 audit and card 66034 closed. And `label` is spent as
|
|
516
|
+
* `title` and `aria-label` as well as the visible mark, so a node in it reaches
|
|
517
|
+
* both attributes as `[object Object]` — while splitting it into `icon` +
|
|
518
|
+
* `label` renders BOTH spans, and Alignment would draw the rule and the word
|
|
519
|
+
* "Left" beside it.
|
|
409
520
|
*/
|
|
410
|
-
declare function SettingsSegmented({ options, value, onChange, ariaLabel, fit, className, }: SettingsSegmentedProps): react.JSX.Element;
|
|
521
|
+
declare function SettingsSegmented<V = string>({ options, value, onChange, ariaLabel, fit, disabled, className, }: SettingsSegmentedProps<V>): react.JSX.Element;
|
|
411
522
|
|
|
412
523
|
/**
|
|
413
524
|
* A group of settings under a soft pill row that folds with − / +.
|
package/dist/panel/index.js
CHANGED
|
@@ -53,6 +53,7 @@ function SettingsSegmented({
|
|
|
53
53
|
onChange,
|
|
54
54
|
ariaLabel,
|
|
55
55
|
fit,
|
|
56
|
+
disabled = false,
|
|
56
57
|
className
|
|
57
58
|
}) {
|
|
58
59
|
const measured = fit && {
|
|
@@ -68,18 +69,22 @@ function SettingsSegmented({
|
|
|
68
69
|
{
|
|
69
70
|
role: "group",
|
|
70
71
|
"aria-label": ariaLabel,
|
|
72
|
+
"data-disabled": disabled || void 0,
|
|
71
73
|
className: `es-segs${measured ? " is-measured" : ""}${className ? ` ${className}` : ""}`,
|
|
72
74
|
style: measured,
|
|
73
75
|
children: options.map((option) => {
|
|
74
76
|
const pressed = option.id === value;
|
|
77
|
+
const name = option.title ?? (typeof option.label === "string" ? option.label : void 0);
|
|
78
|
+
const off = disabled || option.disabled === true;
|
|
75
79
|
const cls = `es-seg${option.icon ? " has-icon" : ""}${pressed ? " is-active" : ""}`;
|
|
76
80
|
return /* @__PURE__ */ jsxs(
|
|
77
81
|
"button",
|
|
78
82
|
{
|
|
79
83
|
type: "button",
|
|
80
|
-
title:
|
|
81
|
-
"aria-label":
|
|
84
|
+
title: name,
|
|
85
|
+
"aria-label": name,
|
|
82
86
|
"aria-pressed": pressed,
|
|
87
|
+
disabled: off,
|
|
83
88
|
className: cls,
|
|
84
89
|
onClick: () => onChange(option.id),
|
|
85
90
|
children: [
|
|
@@ -87,7 +92,7 @@ function SettingsSegmented({
|
|
|
87
92
|
/* @__PURE__ */ jsx("span", { className: "es-seg-word", children: option.label })
|
|
88
93
|
]
|
|
89
94
|
},
|
|
90
|
-
option.id
|
|
95
|
+
String(option.id)
|
|
91
96
|
);
|
|
92
97
|
})
|
|
93
98
|
}
|
package/dist/panel/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/panel/SettingsTabs.tsx","../../src/panel/SettingsSegmented.tsx","../../src/panel/SettingsGroup.tsx","../../src/panel/SettingsGroupHead.tsx","../../src/panel/SettingsSwitch.tsx","../../src/panel/SettingsField.tsx","../../src/panel/SettingsDrill.tsx"],"names":["jsx","jsxs"],"mappings":"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","file":"index.js","sourcesContent":["import type { SettingsTabsProps } from './types';\n\n/**\n * The tab strip under the panel head.\n *\n * It replaces the old stack of fold headers a merchant had to open and hunt\n * through: the buckets a section actually has become tabs, in one fixed order,\n * so the panel keeps the same shape on every section. A section shows only the\n * tabs it has — pass three items and three tabs render.\n *\n * Controlled: the host decides `activeId`. No hooks, no browser globals.\n *\n * A tab can be WIRED to what it shows (`panelId` / `htmlId` on the item). That\n * is not decoration: `role=\"tab\"` promises a panel, and a tab strip that names\n * none announces \"tab, 1 of 4, selected\" over nothing. The shell cannot supply\n * those ids — only the host knows the element the settings land in — but until\n * card 8802.d it could not ACCEPT them either, so no host could fix it. A tab\n * with nothing to point at renders no attribute at all; an empty\n * `aria-controls` is worse than a missing one, because it names an element\n * that does not exist instead of admitting there is none.\n *\n * A SEGMENTED CONTROL (card 66138). The strip is a grey track and the chosen\n * bucket is a raised white chip on it. Lewis, looking at a real panel: the tab\n * he is NOT on does not invite a click — it does not even look like a button.\n * The track is the answer to that, because it makes every segment look\n * pressable, where a mark under the chosen tab only ever made the chosen tab\n * louder. It replaces the underline strip and card 8811's tint ground; see the\n * long note on `.es-tab.is-active` in `panel.css`.\n *\n * A segment takes the width its OWN label needs and shares what is left over,\n * rather than every segment taking the width of the widest label. Equal\n * segments were measured and cost too much: three of them need a 267px track\n * and four need 355px, against the 281px the shop's default panel gives.\n *\n * THE WORD DROPS WHERE IT STOPS FITTING. Natural widths fit two and three\n * buckets across the whole panel-drag band; they do not fit four, where the\n * last segment ends 39px past the panel edge. So given an `icon`, a segment can\n * spend less width instead of more — the word goes and the glyph stays.\n *\n * Where that happens depends on HOW MANY buckets a section has, because with\n * natural widths the question is about the sum of the labels:\n *\n * two buckets both words, at every width the panel drags to.\n * three buckets words down to 240px, then glyph plus the chosen word.\n * four buckets glyph plus the chosen word under 320px; glyphs under 200px.\n *\n * Which state is a CSS decision, taken against the PANEL's width — see the\n * `@container` blocks in `panel.css`, and the wrapper below that gives them\n * something to measure. React cannot know the answer at render time, and it\n * does not need to: BOTH parts are always in the markup, and CSS shows the one\n * that fits. The word is only ever hidden, never dropped, and the label rides\n * along as `title` and `aria-label` on every tab in every state, so the glyph\n * never has to speak for the tab.\n *\n * A tab with no icon is untouched at every width — it has nothing to fall back\n * to, so it keeps its word.\n */\nexport function SettingsTabs({\n items,\n activeId,\n onSelect,\n ariaLabel = 'Settings',\n className,\n}: SettingsTabsProps) {\n return (\n // The container context is the SHELL's, not a host's to remember. A panel\n // that forgot to declare one would silently pin the strip to one tier.\n <div className=\"es-tabs-fit\">\n <div\n role=\"tablist\"\n aria-label={ariaLabel}\n aria-orientation=\"horizontal\"\n className={className ? `es-tabs ${className}` : 'es-tabs'}\n >\n {items.map((item) => {\n const active = item.id === activeId;\n const cls = `es-tab${item.icon ? ' has-icon' : ''}${active ? ' is-active' : ''}`;\n return (\n <button\n key={item.id}\n type=\"button\"\n role=\"tab\"\n id={item.htmlId}\n title={item.label}\n aria-label={item.label}\n aria-selected={active}\n aria-controls={item.panelId}\n className={cls}\n onClick={() => onSelect(item.id)}\n >\n {item.icon ? (\n <span className=\"es-tab-icon\" aria-hidden=\"true\">\n {item.icon}\n </span>\n ) : null}\n <span className=\"es-tab-word\">{item.label}</span>\n {typeof item.badge === 'number' && item.badge > 0 ? (\n <span className=\"es-tab-badge\">{item.badge}</span>\n ) : null}\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n","import type { CSSProperties } from 'react';\nimport type { SettingsSegmentedProps } from './types';\n\n/**\n * A segmented control: one row of chips, one of them pressed.\n *\n * THE SAME CHIP AS `SettingsTabs`, AND A DIFFERENT PROMISE. Everything a\n * merchant sees is shared — the grey track, the raised white chip for the one\n * he is on, the glyph drawn at every width, the word that drops where it stops\n * fitting. `panel.css` dresses both from one rule, so there is no second copy\n * of the chip grammar to drift.\n *\n * What differs is what the control SAYS it does. `SettingsTabs` is a tablist\n * and `role=\"tab\"` promises that pressing a chip switches a panel. These chips\n * switch nothing: they write a value and the panel stays where it is. So this\n * is a group of pressed buttons, and its marker is `aria-pressed` — which is\n * also what the storefront's own `Segmented` has carried since the 2026-08-06\n * audit, so the two editors keep saying the same thing about the same control.\n *\n * That distinction is not decoration. Reach for `SettingsTabs` on a Weight row\n * and a control that is correct today starts announcing \"tab, 1 of 4, selected\"\n * over a panel that is not there — three times in one part panel. It is the\n * same split the package already made between `SettingsGroup` and\n * `SettingsGroupHead`: same pill, different promise, two components.\n *\n * Controlled: the host owns `value`. No hooks, no browser globals.\n *\n * ── WHERE THE WORD DROPS, AND WHY THE STRIP HAS TO SAY ───────────────────────\n *\n * The tab strip's tiers are keyed on how many tabs there are, which works\n * because the shell knows those four words. It cannot work here. Measured\n * (`tests/fixtures/segmented-fit.html`), Case — Normal / Uppercase / Lowercase\n * — needs 296px for its words and Alignment — Left / Centre / Right — needs\n * 221px. Both are three-option rows, and the panel drags between 181px and\n * 281px, so a single three-option threshold is visibly wrong for one of them.\n * Whether the words fit is a question about the SUM of the labels; the count is\n * only a stand-in, and it stops standing in the moment a second strip arrives.\n *\n * So the strip carries its own measurement. `fit` is the width of its WORDS,\n * nothing else — the chip's padding, glyph and gaps are the shell's, named on\n * the panel root, and `panel.css` adds them. Those three custom properties\n * below are that measurement and the count, handed to the stylesheet, which\n * compares them against the panel's live width in `calc()`. A container query\n * could not: its condition takes a literal length, so it cannot ask a question\n * whose answer is different for every strip.\n *\n * Without `fit` nothing drops — an unmeasured strip keeps every word at every\n * width, which is exactly what a strip with no glyphs already does.\n */\nexport function SettingsSegmented({\n options,\n value,\n onChange,\n ariaLabel,\n fit,\n className,\n}: SettingsSegmentedProps) {\n // The measurement, handed to the stylesheet. `is-measured` is what the sheet\n // scopes the threshold arithmetic to, so a strip that declared nothing falls\n // back to no threshold at all rather than to a computed one over zeroes.\n const measured: CSSProperties | undefined = fit && {\n ['--es-seg-count' as string]: options.length,\n ['--es-seg-labels' as string]: `${fit.labelsPx}px`,\n ['--es-seg-widest' as string]: `${fit.widestLabelPx}px`,\n };\n\n return (\n // The container context is the SHELL's, not a host's to remember — a panel\n // that forgot to declare one would silently pin the strip to one state.\n <div className=\"es-segs-fit\">\n <div\n role=\"group\"\n aria-label={ariaLabel}\n className={`es-segs${measured ? ' is-measured' : ''}${className ? ` ${className}` : ''}`}\n style={measured}\n >\n {options.map((option) => {\n const pressed = option.id === value;\n const cls = `es-seg${option.icon ? ' has-icon' : ''}${pressed ? ' is-active' : ''}`;\n return (\n <button\n key={option.id}\n type=\"button\"\n title={option.label}\n aria-label={option.label}\n aria-pressed={pressed}\n className={cls}\n onClick={() => onChange(option.id)}\n >\n {option.icon ? (\n <span className=\"es-seg-icon\" aria-hidden=\"true\">\n {option.icon}\n </span>\n ) : null}\n <span className=\"es-seg-word\">{option.label}</span>\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n","import type { SettingsGroupProps } from './types';\n\n/**\n * A group of settings under a soft pill row that folds with − / +.\n *\n * The sign is drawn by `panel.css` from `aria-expanded`, so the open state has\n * exactly one home (the attribute assistive tech already reads) instead of a\n * second copy in the markup.\n *\n * `count` is the reason a fold here is safe: a folded group that holds changes\n * still says so on its pill, so nothing a merchant changed can hide.\n */\nexport function SettingsGroup({\n title,\n open,\n onToggle,\n count,\n id,\n children,\n className,\n}: SettingsGroupProps) {\n const bodyId = id ? `${id}-body` : undefined;\n return (\n <div className={className ? `es-group ${className}` : 'es-group'}>\n <button\n type=\"button\"\n id={id}\n aria-expanded={open}\n aria-controls={bodyId}\n className=\"es-group-head\"\n onClick={() => onToggle(!open)}\n >\n {title}\n {typeof count === 'number' && count > 0 ? (\n <span className=\"es-group-count\">{count}</span>\n ) : null}\n <span className=\"es-group-sign\" aria-hidden=\"true\" />\n </button>\n <div id={bodyId} className=\"es-group-body\" hidden={!open}>\n {children}\n </div>\n </div>\n );\n}\n","import type { SettingsGroupHeadProps } from './types';\n\n/**\n * A group heading with NO group under it — the pill on its own.\n *\n * It exists because Puck lays the inspector out as a FLAT sibling list: a field\n * renders next to the heading above it, never inside it, so nothing here can\n * wrap the rows that follow. Heading a run of settings is therefore a job for a\n * component that heads and nothing else.\n *\n * `SettingsGroup` cannot do that job. It always renders its body div, and its\n * pill is always a <button> carrying `aria-expanded`, `aria-controls` and the\n * − / + sign. Used header-only it would draw a control that claims to expand\n * something, does nothing when clicked, and tells a screen reader the same lie.\n * A component that folds and a component that titles are two different things,\n * and this is the one that titles.\n *\n * So it is a HEADING, not a control: a real <h3> (the level is the host's, since\n * only the host knows the page outline around it) with no role bolted on, no\n * expanded state, no sign, no body. In a flat list the heading list is the only\n * structure a screen reader has left to navigate by, which is the whole reason\n * this renders a heading element rather than a styled <div>.\n *\n * It wears `.es-group-head` — the same pill as a real group's — so a section\n * that folds and a section that does not read as the same panel. `panel.css`\n * scopes the cursor and the hover tint to `button.es-group-head`, so the\n * heading does not offer a press it cannot honour.\n *\n * NO `count`. A folded group needs one because a fold can hide a change; a\n * heading folds nothing, so nothing can hide behind it.\n */\nexport function SettingsGroupHead({\n title,\n level = 3,\n id,\n className,\n}: SettingsGroupHeadProps) {\n const Tag = `h${level}` as const;\n return (\n <Tag id={id} className={className ? `es-group-head ${className}` : 'es-group-head'}>\n {title}\n </Tag>\n );\n}\n","import type { SettingsSwitchProps } from './types';\n\n/**\n * The yes-or-no control — ONE shape, both editors (card 8801).\n *\n * Point 5 of the approved settings design says one switch style everywhere.\n * Before this, the storefront editor drew a tick box and the campaign designer\n * drew a green switch, so a merchant met two shapes for the same question and\n * had to learn the control twice. Both now import this file, which is the only\n * thing that stops them drifting apart again.\n *\n * It is a real <button>, and that is the whole keyboard story: a button is\n * activated by Space and by Enter in every browser, and that activation IS a\n * click — so the one `onClick` below serves mouse and keyboard alike. A div\n * wearing `role=\"switch\"` would need a key handler of our own. Do not add one.\n * `type=\"button\"` keeps it from submitting a form it happens to sit in.\n *\n * It cannot exist without a NAME. `SettingsSwitchProps` demands `ariaLabel` or\n * `ariaLabelledBy` and forbids both at once, so an unnamed switch — which a\n * screen reader reads out as \"switch, on\" and nothing more — is a compile error\n * instead of something a docstring asks you to remember.\n *\n * ONE-WAY DEPENDENCY. `SettingsField` imports this module so it can recognise a\n * switch in its `action` slot and hand it the field's label as a name. This\n * module must NEVER import `SettingsField` back — that would close a cycle\n * between two siblings in the same folder. It imports `./types` and nothing\n * else, and `tests/settings-switch.test.tsx` (o) keeps it that way.\n *\n * CONTROLLED and hook-free like the other three: `checked` comes from the host\n * and the switch only ever ASKS to be flipped. That is what keeps this module a\n * directive-free leaf a Next-16 server component can import.\n */\nexport function SettingsSwitch({\n checked,\n onChange,\n disabled,\n id,\n ariaLabel,\n ariaLabelledBy,\n className,\n}: SettingsSwitchProps) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n id={id}\n aria-checked={checked}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n disabled={disabled}\n className={className ? `es-switch ${className}` : 'es-switch'}\n onClick={() => onChange(!checked)}\n />\n );\n}\n","import { cloneElement, isValidElement } from 'react';\nimport type { ReactElement, ReactNode } from 'react';\n\nimport { SettingsSwitch } from './SettingsSwitch';\nimport type { SettingsFieldProps } from './types';\n\n/**\n * Does this slot hold a control?\n *\n * The rule is ELEMENT-OR-NOTHING, deliberately not a list of falsy values: a\n * list rots, and the next odd value nobody thought of walks straight through.\n * `{flag && <Slider/>}` yields `false`, `{count && <Slider/>}` yields `0` when\n * the count is zero — and React RENDERS that zero, a stray 0 sitting in the\n * panel where a control belongs. A bare string or number in a control slot is a\n * caller mistake in every case any of us can name.\n *\n * Arrays are walked rather than trusted: `Array.isArray([])` is true, so an\n * empty array — or one holding only `false` and `null`, which is what a list of\n * conditional controls collapses to — would otherwise draw the empty row this\n * check exists to prevent.\n */\nfunction hasControlIn(slot: ReactNode): boolean {\n if (Array.isArray(slot)) return slot.some(hasControlIn);\n return isValidElement(slot);\n}\n\n/** What a switch is named by. Read off an unknown element, so it stays loose. */\ninterface SwitchName {\n ariaLabel?: string;\n ariaLabelledBy?: string;\n}\n\n/**\n * Give a switch in the `action` slot the field's own label as its name.\n *\n * A switch carries no text, and `htmlFor` cannot bridge a <label> to a\n * <button>, so in a boolean row nothing connects the visible label to the\n * control unless something does it explicitly. Doing it HERE means the common\n * case is right with the caller saying nothing: the accessible name and the\n * visible name are the same string and cannot drift apart.\n *\n * It only ever fills a gap. A switch that names itself keeps its own name, and\n * anything that is not a switch — a reset dot, a badge — is left alone, since\n * labelling those with the field's name would be worse than not labelling them.\n */\nfunction nameSwitch(action: ReactNode, label: string, labelId?: string): ReactNode {\n if (!isValidElement(action) || action.type !== SettingsSwitch) return action;\n\n const named = action.props as SwitchName;\n if (named.ariaLabel || named.ariaLabelledBy) return action;\n\n return cloneElement(\n action as ReactElement<SwitchName>,\n labelId ? { ariaLabelledBy: labelId } : { ariaLabel: label },\n );\n}\n\n/**\n * One setting: a label row, then its control.\n *\n * The row is stacked rather than side-by-side so the control gets the panel's\n * full width — a 308px panel cannot afford a label column AND a usable slider.\n * The label reads small and quiet; the VALUE sits at the end of the same line in\n * the text colour, which is what a merchant scans for. That contrast is the\n * point: before this, label and value looked alike and the panel read as noise.\n *\n * `hint` is where a clause-long explanation goes. A label is a name, never a\n * sentence.\n *\n * A BOOLEAN row is the one exception to \"control on its own line\" (card 8801,\n * Option A, approved). A switch is small and it is the whole control, so it\n * rides the label line in the `action` slot and the row costs one line instead\n * of two — which is what lets a 308px panel show four settings where it used to\n * show three. That is the only reason `children` is optional: a field with no\n * children draws no control row, and its label line IS the row.\n *\n * A field with no children AND no action has no control on either slot. That is\n * a mistake at the call site, so it renders NOTHING — an empty row would hide\n * the mistake behind 20px of blank panel.\n */\nexport function SettingsField({\n label,\n labelId,\n icon,\n value,\n hint,\n changed,\n htmlFor,\n action,\n children,\n className,\n}: SettingsFieldProps) {\n const hasControl = hasControlIn(children);\n const hasAction = hasControlIn(action);\n if (!hasControl && !hasAction) return null;\n\n const classes = ['es-field'];\n if (!hasControl) classes.push('is-inline');\n if (changed) classes.push('is-changed');\n if (className) classes.push(className);\n\n const labelBody = (\n <>\n {icon ? (\n <span className=\"es-field-icon\" aria-hidden=\"true\">\n {icon}\n </span>\n ) : null}\n <span className=\"es-field-label\" id={labelId}>\n {label}\n </span>\n </>\n );\n\n return (\n <div className={classes.join(' ')}>\n <div className=\"es-field-head\">\n {htmlFor ? (\n <label className=\"es-field-name\" htmlFor={htmlFor}>\n {labelBody}\n </label>\n ) : (\n <span className=\"es-field-name\">{labelBody}</span>\n )}\n {value !== undefined && value !== null ? (\n <span className=\"es-field-value\">{value}</span>\n ) : null}\n {hasAction ? nameSwitch(action, label, labelId) : null}\n </div>\n {hasControl ? <div className=\"es-field-ctl\">{children}</div> : null}\n {hint ? <p className=\"es-field-hint\">{hint}</p> : null}\n </div>\n );\n}\n","import type { SettingsDrillProps } from './types';\n\n/**\n * A DRILL-IN ROW — a name, a one-line preview, and a chevron (card 8819).\n *\n * The storefront editor's text editing becomes a panel drill-down: the section\n * screen no longer holds a text box for every part; it LISTS the parts as these\n * rows, and pressing one takes the inspector down a level to that part's own\n * editor. A part the eye hid — or one that never draws on the page at all, like\n * a `Slide name` — has no words on the canvas to click, so this list is the only\n * way back to it. That is why the list exists, and why the row is the shell's,\n * not the shop's: the campaign designer draws the same list.\n *\n * IT IS ONE BUTTON, not a control beside some text. The name and the preview are\n * the button's own content, so they ARE its accessible name — a chevron floating\n * next to unlabelled text would be a press a screen reader could not describe.\n * The chevron is `aria-hidden` (pure direction); the status slot is NOT, because\n * its meaning is the host's to name — it passes an icon carrying its own label\n * (a `title`, a visually-hidden word), and hiding the slot would bury that.\n *\n * CONTROLLED and hook-free like the other four primitives: it owns no open state.\n * Pressing it calls `onOpen`, and the host decides what drilling down means —\n * which keeps this a directive-free leaf a Next-16 server component can import.\n */\nexport function SettingsDrill({\n name,\n preview,\n offPage,\n status,\n onOpen,\n id,\n className,\n}: SettingsDrillProps) {\n const hasPreview = preview !== undefined && preview !== null;\n return (\n <button\n type=\"button\"\n id={id}\n className={className ? `es-drill ${className}` : 'es-drill'}\n onClick={onOpen}\n >\n <span className=\"es-drill-text\">\n <span className=\"es-drill-name\">{name}</span>\n {hasPreview ? (\n <span className={offPage ? 'es-drill-preview is-offpage' : 'es-drill-preview'}>\n {preview}\n </span>\n ) : null}\n </span>\n {status ? <span className=\"es-drill-status\">{status}</span> : null}\n <span className=\"es-drill-chev\" aria-hidden=\"true\" />\n </button>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/panel/SettingsTabs.tsx","../../src/panel/SettingsSegmented.tsx","../../src/panel/SettingsGroup.tsx","../../src/panel/SettingsGroupHead.tsx","../../src/panel/SettingsSwitch.tsx","../../src/panel/SettingsField.tsx","../../src/panel/SettingsDrill.tsx"],"names":["jsx","jsxs"],"mappings":"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","file":"index.js","sourcesContent":["import type { SettingsTabsProps } from './types';\n\n/**\n * The tab strip under the panel head.\n *\n * It replaces the old stack of fold headers a merchant had to open and hunt\n * through: the buckets a section actually has become tabs, in one fixed order,\n * so the panel keeps the same shape on every section. A section shows only the\n * tabs it has — pass three items and three tabs render.\n *\n * Controlled: the host decides `activeId`. No hooks, no browser globals.\n *\n * A tab can be WIRED to what it shows (`panelId` / `htmlId` on the item). That\n * is not decoration: `role=\"tab\"` promises a panel, and a tab strip that names\n * none announces \"tab, 1 of 4, selected\" over nothing. The shell cannot supply\n * those ids — only the host knows the element the settings land in — but until\n * card 8802.d it could not ACCEPT them either, so no host could fix it. A tab\n * with nothing to point at renders no attribute at all; an empty\n * `aria-controls` is worse than a missing one, because it names an element\n * that does not exist instead of admitting there is none.\n *\n * A SEGMENTED CONTROL (card 66138). The strip is a grey track and the chosen\n * bucket is a raised white chip on it. Lewis, looking at a real panel: the tab\n * he is NOT on does not invite a click — it does not even look like a button.\n * The track is the answer to that, because it makes every segment look\n * pressable, where a mark under the chosen tab only ever made the chosen tab\n * louder. It replaces the underline strip and card 8811's tint ground; see the\n * long note on `.es-tab.is-active` in `panel.css`.\n *\n * A segment takes the width its OWN label needs and shares what is left over,\n * rather than every segment taking the width of the widest label. Equal\n * segments were measured and cost too much: three of them need a 267px track\n * and four need 355px, against the 281px the shop's default panel gives.\n *\n * THE WORD DROPS WHERE IT STOPS FITTING. Natural widths fit two and three\n * buckets across the whole panel-drag band; they do not fit four, where the\n * last segment ends 39px past the panel edge. So given an `icon`, a segment can\n * spend less width instead of more — the word goes and the glyph stays.\n *\n * Where that happens depends on HOW MANY buckets a section has, because with\n * natural widths the question is about the sum of the labels:\n *\n * two buckets both words, at every width the panel drags to.\n * three buckets words down to 240px, then glyph plus the chosen word.\n * four buckets glyph plus the chosen word under 320px; glyphs under 200px.\n *\n * Which state is a CSS decision, taken against the PANEL's width — see the\n * `@container` blocks in `panel.css`, and the wrapper below that gives them\n * something to measure. React cannot know the answer at render time, and it\n * does not need to: BOTH parts are always in the markup, and CSS shows the one\n * that fits. The word is only ever hidden, never dropped, and the label rides\n * along as `title` and `aria-label` on every tab in every state, so the glyph\n * never has to speak for the tab.\n *\n * A tab with no icon is untouched at every width — it has nothing to fall back\n * to, so it keeps its word.\n */\nexport function SettingsTabs({\n items,\n activeId,\n onSelect,\n ariaLabel = 'Settings',\n className,\n}: SettingsTabsProps) {\n return (\n // The container context is the SHELL's, not a host's to remember. A panel\n // that forgot to declare one would silently pin the strip to one tier.\n <div className=\"es-tabs-fit\">\n <div\n role=\"tablist\"\n aria-label={ariaLabel}\n aria-orientation=\"horizontal\"\n className={className ? `es-tabs ${className}` : 'es-tabs'}\n >\n {items.map((item) => {\n const active = item.id === activeId;\n const cls = `es-tab${item.icon ? ' has-icon' : ''}${active ? ' is-active' : ''}`;\n return (\n <button\n key={item.id}\n type=\"button\"\n role=\"tab\"\n id={item.htmlId}\n title={item.label}\n aria-label={item.label}\n aria-selected={active}\n aria-controls={item.panelId}\n className={cls}\n onClick={() => onSelect(item.id)}\n >\n {item.icon ? (\n <span className=\"es-tab-icon\" aria-hidden=\"true\">\n {item.icon}\n </span>\n ) : null}\n <span className=\"es-tab-word\">{item.label}</span>\n {typeof item.badge === 'number' && item.badge > 0 ? (\n <span className=\"es-tab-badge\">{item.badge}</span>\n ) : null}\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n","import type { CSSProperties } from 'react';\nimport type { SettingsSegmentedProps } from './types';\n\n/**\n * A segmented control: one row of chips, one of them pressed.\n *\n * THE SAME CHIP AS `SettingsTabs`, AND A DIFFERENT PROMISE. Everything a\n * merchant sees is shared — the grey track, the raised white chip for the one\n * he is on, the glyph drawn at every width, the word that drops where it stops\n * fitting. `panel.css` dresses both from one rule, so there is no second copy\n * of the chip grammar to drift.\n *\n * What differs is what the control SAYS it does. `SettingsTabs` is a tablist\n * and `role=\"tab\"` promises that pressing a chip switches a panel. These chips\n * switch nothing: they write a value and the panel stays where it is. So this\n * is a group of pressed buttons, and its marker is `aria-pressed` — which is\n * also what the storefront's own `Segmented` has carried since the 2026-08-06\n * audit, so the two editors keep saying the same thing about the same control.\n *\n * That distinction is not decoration. Reach for `SettingsTabs` on a Weight row\n * and a control that is correct today starts announcing \"tab, 1 of 4, selected\"\n * over a panel that is not there — three times in one part panel. It is the\n * same split the package already made between `SettingsGroup` and\n * `SettingsGroupHead`: same pill, different promise, two components.\n *\n * Controlled: the host owns `value`. No hooks, no browser globals.\n *\n * ── WHERE THE WORD DROPS, AND WHY THE STRIP HAS TO SAY ───────────────────────\n *\n * The tab strip's tiers are keyed on how many tabs there are, which works\n * because the shell knows those four words. It cannot work here. Measured\n * (`tests/fixtures/segmented-fit.html`), Case — Normal / Uppercase / Lowercase\n * — needs 296px for its words and Alignment — Left / Centre / Right — needs\n * 221px. Both are three-option rows, and the panel drags between 181px and\n * 281px, so a single three-option threshold is visibly wrong for one of them.\n * Whether the words fit is a question about the SUM of the labels; the count is\n * only a stand-in, and it stops standing in the moment a second strip arrives.\n *\n * So the strip carries its own measurement. `fit` is the width of its WORDS,\n * nothing else — the chip's padding, glyph and gaps are the shell's, named on\n * the panel root, and `panel.css` adds them. Those three custom properties\n * below are that measurement and the count, handed to the stylesheet, which\n * compares them against the panel's live width in `calc()`. A container query\n * could not: its condition takes a literal length, so it cannot ask a question\n * whose answer is different for every strip.\n *\n * Without `fit` nothing drops — an unmeasured strip keeps every word at every\n * width, which is exactly what a strip with no glyphs already does.\n *\n * ── WHAT CARD 66303 WIDENED, AND WHY IT HAD TO BE HERE ───────────────────────\n *\n * `efficient-shop` draws its own `Segmented` beside this one and could not stop:\n * four things twelve live call sites need were missing here. Generic `V`\n * (weights are numbers, \"image side\" is a boolean on every published page), a\n * DRAWN label with its own `title`, and `disabled` — on one option or on the\n * strip.\n *\n * The last two could not have been bridged by a wrapper the shop writes, which\n * is the reason they are props and not a consumer's problem. This component\n * renders the `<button>`s, so nothing outside it can disable them; dimming a\n * parent instead leaves them focusable and announcing as ENABLED, which is the\n * regression the 2026-08-06 audit and card 66034 closed. And `label` is spent as\n * `title` and `aria-label` as well as the visible mark, so a node in it reaches\n * both attributes as `[object Object]` — while splitting it into `icon` +\n * `label` renders BOTH spans, and Alignment would draw the rule and the word\n * \"Left\" beside it.\n */\nexport function SettingsSegmented<V = string>({\n options,\n value,\n onChange,\n ariaLabel,\n fit,\n disabled = false,\n className,\n}: SettingsSegmentedProps<V>) {\n // The measurement, handed to the stylesheet. `is-measured` is what the sheet\n // scopes the threshold arithmetic to, so a strip that declared nothing falls\n // back to no threshold at all rather than to a computed one over zeroes.\n const measured: CSSProperties | undefined = fit && {\n ['--es-seg-count' as string]: options.length,\n ['--es-seg-labels' as string]: `${fit.labelsPx}px`,\n ['--es-seg-widest' as string]: `${fit.widestLabelPx}px`,\n };\n\n return (\n // The container context is the SHELL's, not a host's to remember — a panel\n // that forgot to declare one would silently pin the strip to one state.\n <div className=\"es-segs-fit\">\n <div\n role=\"group\"\n aria-label={ariaLabel}\n // A state ATTRIBUTE rather than a second class, so the class string the\n // strip has always rendered is untouched and the sheet's one statement\n // about a whole control out of force has somewhere of its own to hang.\n data-disabled={disabled || undefined}\n className={`es-segs${measured ? ' is-measured' : ''}${className ? ` ${className}` : ''}`}\n style={measured}\n >\n {options.map((option) => {\n // AN EQUALITY, never a truthiness test. `false` is a real answer —\n // \"image left\" is stored as `false` on every published page across\n // every tenant — and a value matching nothing presses nothing, which\n // is the mixed selection the part panel hands in as `\"\"`.\n const pressed = option.id === value;\n // THE NAME, and every option has one. A drawn label has no word to be\n // read aloud, so its `title` is required and is the name; a worded one\n // is named by its word unless it says otherwise. The narrowing is what\n // keeps a node out of the two attributes it would land in as\n // `[object Object]`.\n const name = option.title ?? (typeof option.label === 'string' ? option.label : undefined);\n const off = disabled || option.disabled === true;\n const cls = `es-seg${option.icon ? ' has-icon' : ''}${pressed ? ' is-active' : ''}`;\n return (\n <button\n // Stringified because `V` is whatever the options declare — a\n // weight is a number. For the string strips that shipped before\n // this, the key is the one they already had.\n key={String(option.id)}\n type=\"button\"\n title={name}\n aria-label={name}\n aria-pressed={pressed}\n // The button's OWN disabled, so it leaves the tab order and\n // announces the state itself. React renders nothing for `false`,\n // so a strip in force is unchanged.\n disabled={off}\n className={cls}\n onClick={() => onChange(option.id)}\n >\n {option.icon ? (\n <span className=\"es-seg-icon\" aria-hidden=\"true\">\n {option.icon}\n </span>\n ) : null}\n {/* The label, drawn as given. A worded one is the word; a drawn\n one is the mark itself, in the SAME slot rather than beside it\n — which is why a mark belongs in `label` and not in `icon`. */}\n <span className=\"es-seg-word\">{option.label}</span>\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n","import type { SettingsGroupProps } from './types';\n\n/**\n * A group of settings under a soft pill row that folds with − / +.\n *\n * The sign is drawn by `panel.css` from `aria-expanded`, so the open state has\n * exactly one home (the attribute assistive tech already reads) instead of a\n * second copy in the markup.\n *\n * `count` is the reason a fold here is safe: a folded group that holds changes\n * still says so on its pill, so nothing a merchant changed can hide.\n */\nexport function SettingsGroup({\n title,\n open,\n onToggle,\n count,\n id,\n children,\n className,\n}: SettingsGroupProps) {\n const bodyId = id ? `${id}-body` : undefined;\n return (\n <div className={className ? `es-group ${className}` : 'es-group'}>\n <button\n type=\"button\"\n id={id}\n aria-expanded={open}\n aria-controls={bodyId}\n className=\"es-group-head\"\n onClick={() => onToggle(!open)}\n >\n {title}\n {typeof count === 'number' && count > 0 ? (\n <span className=\"es-group-count\">{count}</span>\n ) : null}\n <span className=\"es-group-sign\" aria-hidden=\"true\" />\n </button>\n <div id={bodyId} className=\"es-group-body\" hidden={!open}>\n {children}\n </div>\n </div>\n );\n}\n","import type { SettingsGroupHeadProps } from './types';\n\n/**\n * A group heading with NO group under it — the pill on its own.\n *\n * It exists because Puck lays the inspector out as a FLAT sibling list: a field\n * renders next to the heading above it, never inside it, so nothing here can\n * wrap the rows that follow. Heading a run of settings is therefore a job for a\n * component that heads and nothing else.\n *\n * `SettingsGroup` cannot do that job. It always renders its body div, and its\n * pill is always a <button> carrying `aria-expanded`, `aria-controls` and the\n * − / + sign. Used header-only it would draw a control that claims to expand\n * something, does nothing when clicked, and tells a screen reader the same lie.\n * A component that folds and a component that titles are two different things,\n * and this is the one that titles.\n *\n * So it is a HEADING, not a control: a real <h3> (the level is the host's, since\n * only the host knows the page outline around it) with no role bolted on, no\n * expanded state, no sign, no body. In a flat list the heading list is the only\n * structure a screen reader has left to navigate by, which is the whole reason\n * this renders a heading element rather than a styled <div>.\n *\n * It wears `.es-group-head` — the same pill as a real group's — so a section\n * that folds and a section that does not read as the same panel. `panel.css`\n * scopes the cursor and the hover tint to `button.es-group-head`, so the\n * heading does not offer a press it cannot honour.\n *\n * NO `count`. A folded group needs one because a fold can hide a change; a\n * heading folds nothing, so nothing can hide behind it.\n */\nexport function SettingsGroupHead({\n title,\n level = 3,\n id,\n className,\n}: SettingsGroupHeadProps) {\n const Tag = `h${level}` as const;\n return (\n <Tag id={id} className={className ? `es-group-head ${className}` : 'es-group-head'}>\n {title}\n </Tag>\n );\n}\n","import type { SettingsSwitchProps } from './types';\n\n/**\n * The yes-or-no control — ONE shape, both editors (card 8801).\n *\n * Point 5 of the approved settings design says one switch style everywhere.\n * Before this, the storefront editor drew a tick box and the campaign designer\n * drew a green switch, so a merchant met two shapes for the same question and\n * had to learn the control twice. Both now import this file, which is the only\n * thing that stops them drifting apart again.\n *\n * It is a real <button>, and that is the whole keyboard story: a button is\n * activated by Space and by Enter in every browser, and that activation IS a\n * click — so the one `onClick` below serves mouse and keyboard alike. A div\n * wearing `role=\"switch\"` would need a key handler of our own. Do not add one.\n * `type=\"button\"` keeps it from submitting a form it happens to sit in.\n *\n * It cannot exist without a NAME. `SettingsSwitchProps` demands `ariaLabel` or\n * `ariaLabelledBy` and forbids both at once, so an unnamed switch — which a\n * screen reader reads out as \"switch, on\" and nothing more — is a compile error\n * instead of something a docstring asks you to remember.\n *\n * ONE-WAY DEPENDENCY. `SettingsField` imports this module so it can recognise a\n * switch in its `action` slot and hand it the field's label as a name. This\n * module must NEVER import `SettingsField` back — that would close a cycle\n * between two siblings in the same folder. It imports `./types` and nothing\n * else, and `tests/settings-switch.test.tsx` (o) keeps it that way.\n *\n * CONTROLLED and hook-free like the other three: `checked` comes from the host\n * and the switch only ever ASKS to be flipped. That is what keeps this module a\n * directive-free leaf a Next-16 server component can import.\n */\nexport function SettingsSwitch({\n checked,\n onChange,\n disabled,\n id,\n ariaLabel,\n ariaLabelledBy,\n className,\n}: SettingsSwitchProps) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n id={id}\n aria-checked={checked}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n disabled={disabled}\n className={className ? `es-switch ${className}` : 'es-switch'}\n onClick={() => onChange(!checked)}\n />\n );\n}\n","import { cloneElement, isValidElement } from 'react';\nimport type { ReactElement, ReactNode } from 'react';\n\nimport { SettingsSwitch } from './SettingsSwitch';\nimport type { SettingsFieldProps } from './types';\n\n/**\n * Does this slot hold a control?\n *\n * The rule is ELEMENT-OR-NOTHING, deliberately not a list of falsy values: a\n * list rots, and the next odd value nobody thought of walks straight through.\n * `{flag && <Slider/>}` yields `false`, `{count && <Slider/>}` yields `0` when\n * the count is zero — and React RENDERS that zero, a stray 0 sitting in the\n * panel where a control belongs. A bare string or number in a control slot is a\n * caller mistake in every case any of us can name.\n *\n * Arrays are walked rather than trusted: `Array.isArray([])` is true, so an\n * empty array — or one holding only `false` and `null`, which is what a list of\n * conditional controls collapses to — would otherwise draw the empty row this\n * check exists to prevent.\n */\nfunction hasControlIn(slot: ReactNode): boolean {\n if (Array.isArray(slot)) return slot.some(hasControlIn);\n return isValidElement(slot);\n}\n\n/** What a switch is named by. Read off an unknown element, so it stays loose. */\ninterface SwitchName {\n ariaLabel?: string;\n ariaLabelledBy?: string;\n}\n\n/**\n * Give a switch in the `action` slot the field's own label as its name.\n *\n * A switch carries no text, and `htmlFor` cannot bridge a <label> to a\n * <button>, so in a boolean row nothing connects the visible label to the\n * control unless something does it explicitly. Doing it HERE means the common\n * case is right with the caller saying nothing: the accessible name and the\n * visible name are the same string and cannot drift apart.\n *\n * It only ever fills a gap. A switch that names itself keeps its own name, and\n * anything that is not a switch — a reset dot, a badge — is left alone, since\n * labelling those with the field's name would be worse than not labelling them.\n */\nfunction nameSwitch(action: ReactNode, label: string, labelId?: string): ReactNode {\n if (!isValidElement(action) || action.type !== SettingsSwitch) return action;\n\n const named = action.props as SwitchName;\n if (named.ariaLabel || named.ariaLabelledBy) return action;\n\n return cloneElement(\n action as ReactElement<SwitchName>,\n labelId ? { ariaLabelledBy: labelId } : { ariaLabel: label },\n );\n}\n\n/**\n * One setting: a label row, then its control.\n *\n * The row is stacked rather than side-by-side so the control gets the panel's\n * full width — a 308px panel cannot afford a label column AND a usable slider.\n * The label reads small and quiet; the VALUE sits at the end of the same line in\n * the text colour, which is what a merchant scans for. That contrast is the\n * point: before this, label and value looked alike and the panel read as noise.\n *\n * `hint` is where a clause-long explanation goes. A label is a name, never a\n * sentence.\n *\n * A BOOLEAN row is the one exception to \"control on its own line\" (card 8801,\n * Option A, approved). A switch is small and it is the whole control, so it\n * rides the label line in the `action` slot and the row costs one line instead\n * of two — which is what lets a 308px panel show four settings where it used to\n * show three. That is the only reason `children` is optional: a field with no\n * children draws no control row, and its label line IS the row.\n *\n * A field with no children AND no action has no control on either slot. That is\n * a mistake at the call site, so it renders NOTHING — an empty row would hide\n * the mistake behind 20px of blank panel.\n */\nexport function SettingsField({\n label,\n labelId,\n icon,\n value,\n hint,\n changed,\n htmlFor,\n action,\n children,\n className,\n}: SettingsFieldProps) {\n const hasControl = hasControlIn(children);\n const hasAction = hasControlIn(action);\n if (!hasControl && !hasAction) return null;\n\n const classes = ['es-field'];\n if (!hasControl) classes.push('is-inline');\n if (changed) classes.push('is-changed');\n if (className) classes.push(className);\n\n const labelBody = (\n <>\n {icon ? (\n <span className=\"es-field-icon\" aria-hidden=\"true\">\n {icon}\n </span>\n ) : null}\n <span className=\"es-field-label\" id={labelId}>\n {label}\n </span>\n </>\n );\n\n return (\n <div className={classes.join(' ')}>\n <div className=\"es-field-head\">\n {htmlFor ? (\n <label className=\"es-field-name\" htmlFor={htmlFor}>\n {labelBody}\n </label>\n ) : (\n <span className=\"es-field-name\">{labelBody}</span>\n )}\n {value !== undefined && value !== null ? (\n <span className=\"es-field-value\">{value}</span>\n ) : null}\n {hasAction ? nameSwitch(action, label, labelId) : null}\n </div>\n {hasControl ? <div className=\"es-field-ctl\">{children}</div> : null}\n {hint ? <p className=\"es-field-hint\">{hint}</p> : null}\n </div>\n );\n}\n","import type { SettingsDrillProps } from './types';\n\n/**\n * A DRILL-IN ROW — a name, a one-line preview, and a chevron (card 8819).\n *\n * The storefront editor's text editing becomes a panel drill-down: the section\n * screen no longer holds a text box for every part; it LISTS the parts as these\n * rows, and pressing one takes the inspector down a level to that part's own\n * editor. A part the eye hid — or one that never draws on the page at all, like\n * a `Slide name` — has no words on the canvas to click, so this list is the only\n * way back to it. That is why the list exists, and why the row is the shell's,\n * not the shop's: the campaign designer draws the same list.\n *\n * IT IS ONE BUTTON, not a control beside some text. The name and the preview are\n * the button's own content, so they ARE its accessible name — a chevron floating\n * next to unlabelled text would be a press a screen reader could not describe.\n * The chevron is `aria-hidden` (pure direction); the status slot is NOT, because\n * its meaning is the host's to name — it passes an icon carrying its own label\n * (a `title`, a visually-hidden word), and hiding the slot would bury that.\n *\n * CONTROLLED and hook-free like the other four primitives: it owns no open state.\n * Pressing it calls `onOpen`, and the host decides what drilling down means —\n * which keeps this a directive-free leaf a Next-16 server component can import.\n */\nexport function SettingsDrill({\n name,\n preview,\n offPage,\n status,\n onOpen,\n id,\n className,\n}: SettingsDrillProps) {\n const hasPreview = preview !== undefined && preview !== null;\n return (\n <button\n type=\"button\"\n id={id}\n className={className ? `es-drill ${className}` : 'es-drill'}\n onClick={onOpen}\n >\n <span className=\"es-drill-text\">\n <span className=\"es-drill-name\">{name}</span>\n {hasPreview ? (\n <span className={offPage ? 'es-drill-preview is-offpage' : 'es-drill-preview'}>\n {preview}\n </span>\n ) : null}\n </span>\n {status ? <span className=\"es-drill-status\">{status}</span> : null}\n <span className=\"es-drill-chev\" aria-hidden=\"true\" />\n </button>\n );\n}\n"]}
|
package/dist/panel/panel.css
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
* dark mode.
|
|
8
8
|
*
|
|
9
9
|
* Two details are deliberate and were decided by measurement, not taste:
|
|
10
|
-
* · rows sit
|
|
11
|
-
* setting reads as ONE thing with air around it.
|
|
12
|
-
* saying which label owns which control
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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;
|
|
15
17
|
* · the label is small and grey, the value is larger and dark. Before that
|
|
16
18
|
* contrast existed, a merchant could not tell a name from a number.
|
|
17
19
|
*/
|
|
@@ -63,23 +65,38 @@
|
|
|
63
65
|
a heading, after the row above it 36px unchanged
|
|
64
66
|
|
|
65
67
|
The three measured rhythms do not move because 16 is still under them, which
|
|
66
|
-
is the floor doing exactly what it was built to do.
|
|
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. */
|
|
67
81
|
/* ── the rhythms: the numbers under the floor, given names (card 66228) ────── */
|
|
68
82
|
|
|
69
83
|
/* THE FLOOR ABOVE IS NOT THE RHYTHM, and handing a consumer the floor was the
|
|
70
|
-
whole problem. `--es-row-gap` answers
|
|
84
|
+
whole problem. `--es-row-gap` answers 20 for all five gaps below alike; what
|
|
71
85
|
the storefront's Layers tree needs in order to "follow the shell's spacing"
|
|
72
|
-
is the RHYTHM —
|
|
86
|
+
is the RHYTHM — 38 between two settings against 11 inside one. Under the floor
|
|
73
87
|
those were bare literals, so the only way to follow them was to COPY them
|
|
74
88
|
into `efficient-shop/components/storefront-editor.css`: a second source of
|
|
75
89
|
truth for numbers this sheet was already the source of.
|
|
76
90
|
|
|
77
|
-
THE VALUES DID NOT CHANGE. Every one
|
|
78
|
-
moved and named;
|
|
79
|
-
|
|
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.
|
|
80
96
|
|
|
81
97
|
NAMES, NOT ARITHMETIC, for the reason this sheet already gives one rule down:
|
|
82
|
-
|
|
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
|
|
83
100
|
would claim a relationship nobody decided; a name states the value and the
|
|
84
101
|
job it does.
|
|
85
102
|
|
|
@@ -87,28 +104,29 @@
|
|
|
87
104
|
and `--es-rhythm-within` are the two halves of the one rule that has to
|
|
88
105
|
survive being re-scaled: the gap INSIDE a thing stays plainly smaller than
|
|
89
106
|
the gap BETWEEN things, because that inside gap is the ONLY thing saying
|
|
90
|
-
which label owns which control.
|
|
91
|
-
|
|
92
|
-
|
|
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. */
|
|
93
111
|
.es-editor {
|
|
94
112
|
/* THE FLOOR (card 66206) — no row-to-row gap on this panel resolves below it. */
|
|
95
|
-
--es-row-gap:
|
|
113
|
+
--es-row-gap: 20px;
|
|
96
114
|
|
|
97
115
|
/* THE RHYTHMS. All five are declared in THIS rule and not a second
|
|
98
116
|
`.es-editor` block: the panel has ONE root, and a second block would put
|
|
99
117
|
half the panel's spacing somewhere a reader — and the spec, which reads the
|
|
100
118
|
root by selector — would not look. */
|
|
101
119
|
/* between one group heading and whatever is above it */
|
|
102
|
-
--es-rhythm-group:
|
|
120
|
+
--es-rhythm-group: 48px;
|
|
103
121
|
/* between a group heading and the first setting under it — closer, because
|
|
104
122
|
that setting belongs to that heading */
|
|
105
|
-
--es-rhythm-group-first:
|
|
123
|
+
--es-rhythm-group-first: 24px;
|
|
106
124
|
/* BETWEEN two settings */
|
|
107
|
-
--es-rhythm-between:
|
|
125
|
+
--es-rhythm-between: 38px;
|
|
108
126
|
/* between two one-line rows, which are one line tall and need less air */
|
|
109
|
-
--es-rhythm-between-tight:
|
|
127
|
+
--es-rhythm-between-tight: 26px;
|
|
110
128
|
/* WITHIN one setting: its label to its own control */
|
|
111
|
-
--es-rhythm-within:
|
|
129
|
+
--es-rhythm-within: 11px;
|
|
112
130
|
|
|
113
131
|
/* ── THE CHIP'S OWN METRICS (card 66248) ───────────────────────────────────
|
|
114
132
|
|
|
@@ -512,6 +530,42 @@
|
|
|
512
530
|
.es-seg.has-icon .es-seg-word { margin-left: clamp(0px, var(--es-seg-room-all), var(--es-chip-icon-gap)); }
|
|
513
531
|
.es-seg.has-icon.is-active .es-seg-word { margin-left: clamp(0px, var(--es-seg-room-chosen), var(--es-chip-icon-gap)); }
|
|
514
532
|
|
|
533
|
+
/* ── a segment out of force (card 66303; the RULE is card 66216's) ──────────
|
|
534
|
+
|
|
535
|
+
The setting still EXISTS and still shows the merchant's saved answer. Another
|
|
536
|
+
setting has simply taken the decision away — the Header's "Text colour at the
|
|
537
|
+
top" under a pinned Background, which cannot be transparent. DIMMED, NEVER
|
|
538
|
+
HIDDEN: a control that disappears reads as a feature that was removed, and
|
|
539
|
+
leaves the merchant no way to see what to change to get it back.
|
|
540
|
+
|
|
541
|
+
THE STATE IS ON THE BUTTONS, and `SettingsSegmented` renders it as their own
|
|
542
|
+
`disabled` rather than as a fade a host paints over them. A faded parent
|
|
543
|
+
leaves every segment focusable and announcing as ENABLED, which is the defect
|
|
544
|
+
the 2026-08-06 audit and card 66034 closed; it is also why the prop had to
|
|
545
|
+
come here at all, since nothing outside this component owns those buttons.
|
|
546
|
+
|
|
547
|
+
`--es-disabled` is the shell's existing token for exactly this state, already
|
|
548
|
+
worn by the drill row's quiet glyph. Nothing here is a literal, and nothing
|
|
549
|
+
here removes anything: the track keeps its border and the pressed chip keeps
|
|
550
|
+
its lift, so the strip still READS as a strip with an answer in it. */
|
|
551
|
+
.es-seg:disabled { cursor: not-allowed; color: var(--es-disabled); }
|
|
552
|
+
|
|
553
|
+
/* The chosen one stays the loudest thing on the track. It is still the
|
|
554
|
+
merchant's answer — a pressed chip faded to the same grey as the ones beside
|
|
555
|
+
it stops saying which answer that was. */
|
|
556
|
+
.es-seg:disabled.is-active { color: var(--es-text-secondary); }
|
|
557
|
+
|
|
558
|
+
/* And none of them answers the pointer any more: the hover rule above would
|
|
559
|
+
otherwise keep lighting a segment that cannot be chosen. */
|
|
560
|
+
.es-seg:disabled:hover:not(.is-active) { color: var(--es-disabled); }
|
|
561
|
+
|
|
562
|
+
/* THE WHOLE CONTROL, when every option went at once — one statement instead of
|
|
563
|
+
three identically faded chips. The track is the part that says "these buttons
|
|
564
|
+
are one control", so it is the right place to say the control is not in force.
|
|
565
|
+
A single option out of force leaves the track alone, because the strip still
|
|
566
|
+
is. */
|
|
567
|
+
.es-segs[data-disabled] { opacity: 0.5; cursor: not-allowed; }
|
|
568
|
+
|
|
515
569
|
/* ── a group: the soft pill row that folds ─────────────────────────────────── */
|
|
516
570
|
|
|
517
571
|
/* WAS 6px, AND THE 6 NEVER APPLIED (card 66203). `.es-group` is a plain div, so
|
|
@@ -551,7 +605,7 @@
|
|
|
551
605
|
border-radius: var(--es-row-radius);
|
|
552
606
|
padding: 8px 11px;
|
|
553
607
|
/* A heading needs more air above it than the settings it heads have between
|
|
554
|
-
them (
|
|
608
|
+
them (38px), or it reads as one more row. Floored on `--es-row-gap` so it
|
|
555
609
|
rises with the rest of the panel (card 66203). */
|
|
556
610
|
margin: max(var(--es-row-gap), var(--es-rhythm-group)) 0 0;
|
|
557
611
|
}
|
|
@@ -595,21 +649,31 @@ button.es-group-head:hover { background: var(--es-chip-bg); }
|
|
|
595
649
|
overriding the attribute's own display:none. */
|
|
596
650
|
.es-group-body[hidden] { display: none; }
|
|
597
651
|
/* The first setting under a head sits CLOSER than the settings below it do to
|
|
598
|
-
each other — it belongs to that head, and
|
|
652
|
+
each other — it belongs to that head, and 38px would cut it loose.
|
|
653
|
+
|
|
654
|
+
AND IT IS LIVE FOR THE FIRST TIME (card 66301). It declared 14 against a 16px
|
|
655
|
+
floor, so it has never once been 14 on screen — the floor answered every
|
|
656
|
+
time, and card 66206 measured that very move (14 → 16) and wrote it down as
|
|
657
|
+
the floor working, which it was. At 24 over a 20px floor the declared number
|
|
658
|
+
is finally the visible one, so this seam grows 16 → 24 rather than the 14 →
|
|
659
|
+
24 the file appears to say. `tests/panel-layout.test.ts` (h) is what turns a
|
|
660
|
+
floored rhythm into a failure instead of a footnote. */
|
|
599
661
|
.es-group-body > .es-field:first-child { margin-top: max(var(--es-row-gap), var(--es-rhythm-group-first)); }
|
|
600
662
|
|
|
601
663
|
/* ── one setting ───────────────────────────────────────────────────────────── */
|
|
602
664
|
|
|
603
|
-
/*
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
was too tight
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
665
|
+
/* 38 IS NOT A THIRD OPINION ABOUT THE FLOOR. A setting is TWO lines — a label
|
|
666
|
+
row and its control — held together by the 11px between them, and that 11px
|
|
667
|
+
is the only thing saying which label owns which control. The gap between
|
|
668
|
+
settings has to be plainly larger than the gap inside one or the panel stops
|
|
669
|
+
being a list of settings and becomes a list of loose parts. The pair was
|
|
670
|
+
measured at 28 and 7 and approved from mockups (card 8802.d) — 20 was tried
|
|
671
|
+
there and was too tight — and re-scaled together to 38 and 11 (card 66301),
|
|
672
|
+
which is the same rule with more air, not a new one.
|
|
673
|
+
|
|
674
|
+
Floored, not replaced, and not `calc()`: 38 was measured and then re-scaled,
|
|
675
|
+
it is not "20 plus 18", so arithmetic off the token would claim a
|
|
676
|
+
relationship nobody decided. */
|
|
613
677
|
.es-field { margin-top: max(var(--es-row-gap), var(--es-rhythm-between)); }
|
|
614
678
|
|
|
615
679
|
.es-field-head {
|
|
@@ -677,8 +741,13 @@ button.es-group-head:hover { background: var(--es-chip-bg); }
|
|
|
677
741
|
control row, the label line's 7px bottom margin would be a gap to nothing.
|
|
678
742
|
|
|
679
743
|
A one-line row is one line tall, so it needs less air above it than a
|
|
680
|
-
two-line setting does —
|
|
681
|
-
same reason everything else here is (card 66203).
|
|
744
|
+
two-line setting does — 26, not 38 — and it is floored on the token for the
|
|
745
|
+
same reason everything else here is (card 66203).
|
|
746
|
+
|
|
747
|
+
IT TIED WITH THE FLOOR UNTIL NOW (card 66301). At 16 against a 16px floor
|
|
748
|
+
nothing was wrong on screen, but the sheet could not tell you which of the
|
|
749
|
+
two numbers you were reading, and moving either one alone would have moved
|
|
750
|
+
nothing. 26 over 20 puts it plainly on its own number. */
|
|
682
751
|
.es-field.is-inline { margin-top: max(var(--es-row-gap), var(--es-rhythm-between-tight)); }
|
|
683
752
|
.es-field.is-inline .es-field-head { margin-bottom: 0; }
|
|
684
753
|
|
|
@@ -780,11 +849,25 @@ button.es-group-head:hover { background: var(--es-chip-bg); }
|
|
|
780
849
|
hairline between them and adds no margin — so the air a merchant sees is
|
|
781
850
|
each row's own vertical padding, on its own side of the line. MEASURED in
|
|
782
851
|
Chromium against these stylesheets (`tests/fixtures/panel-row-spacing.html`):
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
23.
|
|
786
|
-
|
|
787
|
-
|
|
852
|
+
|
|
853
|
+
token above the rule below it word to word recorded by
|
|
854
|
+
10px 11.40px 12.00px 23.40px card 66203
|
|
855
|
+
16px 17.40px 18.00px 35.40px card 66206
|
|
856
|
+
16px 17.39px 17.00px 34.39px card 66301
|
|
857
|
+
20px 21.39px 21.00px 42.39px card 66301
|
|
858
|
+
|
|
859
|
+
TWO ROWS FOR 16px, AND THEY DISAGREE BY A PIXEL. 66301's pair was taken in
|
|
860
|
+
one run of Chromium 124 (zenika/alpine-chrome) so its two rows compare; the
|
|
861
|
+
earlier rows are left as the cards recorded them rather than overwritten,
|
|
862
|
+
because the difference is the engine and the font it had, not this sheet.
|
|
863
|
+
Read a row against another row from the same card, never across cards.
|
|
864
|
+
|
|
865
|
+
THIS SEAM MOVES AT TWICE THE TOKEN'S RATE, and it is the only one that
|
|
866
|
+
does: both rows spend the gap, so +4 on the dial buys +8 of air. Under the
|
|
867
|
+
floor alone it was also the ONLY place raising the token moved a measured
|
|
868
|
+
distance rather than a declared one — every other seam was held by a bigger
|
|
869
|
+
rhythm — which is why it is the row Lewis was looking at when he asked for
|
|
870
|
+
card 66206. Card 66301 moves the rhythms too, so it is no longer alone.
|
|
788
871
|
|
|
789
872
|
A MARGIN WOULD BE THE WRONG FIX. A border-top with space above it detaches
|
|
790
873
|
from the row it belongs to and reads as a line floating in the panel rather
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "editor-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Shared editor-chrome primitives for the EFFICIENT editors: EditorRail (a Next-16-safe left icon-rail leaf), the shell token layer, and GoldTextInput — type in place and watch the markup formatting appear as you type.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Lewis Liu",
|