editor-shell 0.37.0 → 0.38.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/index.js CHANGED
@@ -1,5 +1,5 @@
1
+ export { puckAzureRamp, shellCssVars, shellTokens } from './chunk-VEXTYAMU.js';
1
2
  export { EditorRail, EditorRailButton, railCssVars, railTokens } from './chunk-HYOACICE.js';
2
3
  export { AddIcon, LayersIcon, MediaIcon, PageIcon as PagesIcon, SitemapIcon, StylesIcon } from './chunk-ENLK7SSS.js';
3
- export { puckAzureRamp, shellCssVars, shellTokens } from './chunk-VEXTYAMU.js';
4
4
  //# sourceMappingURL=index.js.map
5
5
  //# sourceMappingURL=index.js.map
@@ -554,6 +554,85 @@ interface SettingsPlacementProps {
554
554
  id?: string;
555
555
  className?: string;
556
556
  }
557
+ /**
558
+ * A FULL-WIDTH ACTION BUTTON that sits under a group's rows and COMMITS.
559
+ *
560
+ * "Apply to all cards" is the first caller: the merchant has styled one card and
561
+ * wants the other eleven to match. That is a WRITE, and a write is why this is
562
+ * not `SettingsDrill` with the chevron taken off — see the docblock on
563
+ * `SettingsAction` for the whole of that argument.
564
+ *
565
+ * Controlled and hook-free like every primitive here. The shell owns the look
566
+ * and the announcement; the host owns the words, the write and the timer that
567
+ * takes {@link SettingsActionBaseProps.done} back down.
568
+ */
569
+ interface SettingsActionBaseProps {
570
+ /**
571
+ * What the button says — "Apply to all cards". The package supplies NO
572
+ * default, the way `SettingsCorners` invents no corner names: the words belong
573
+ * to the section that knows what it is about to rewrite.
574
+ */
575
+ label: ReactNode;
576
+ /** Pressed. The write is the host's; this component performs none of it. */
577
+ onPress: () => void;
578
+ /**
579
+ * The press is not available. A REAL `<button disabled>`, dimmed and still
580
+ * there — never hidden. A control that vanishes leaves a merchant looking for
581
+ * something they remember being there, and hiding it also takes away the one
582
+ * place the reason can be read. Say the reason in {@link
583
+ * SettingsActionNote.note}.
584
+ */
585
+ disabled?: boolean;
586
+ /**
587
+ * The confirmation state — the press has landed. The HOST owns it and owns the
588
+ * timer that takes it back down, because only the host knows when its write
589
+ * finished.
590
+ *
591
+ * The shell owns what it LOOKS like and, more importantly, that it is
592
+ * ANNOUNCED: this button rewrites things that are not on screen, so a host
593
+ * that only swapped its own label would get the words right and the
594
+ * announcement wrong. See `SettingsAction`'s `role="status"` region.
595
+ */
596
+ done?: boolean;
597
+ /**
598
+ * What the button says while {@link done} — "Applied to 12 cards". The host's
599
+ * words again, and the string the live region announces. Falls back to
600
+ * {@link label}, so a host that flips `done` without a second string still
601
+ * gets the state and the announcement rather than nothing.
602
+ */
603
+ doneLabel?: ReactNode;
604
+ /** A glyph before the words. `aria-hidden`: the label already names the press. */
605
+ icon?: ReactNode;
606
+ className?: string;
607
+ }
608
+ /**
609
+ * The note, and the id it needs to reach anybody.
610
+ *
611
+ * `note` is where "every card already matches" or "this rewrites all 12 cards"
612
+ * goes, and it is wired to the button by `aria-describedby` — which needs a DOM
613
+ * id, and this package is hook-free so it cannot invent one (no `useId`). The
614
+ * union makes the id a COMPILE error rather than a silent hole: pass a note and
615
+ * you must pass an `id`.
616
+ *
617
+ * ⚠️ THIS IS THE REASON THE CONTROL IS HERE RATHER THAN IN THE SHOP.
618
+ * `efficient-shop`'s own `components/AddChildRow.tsx` explains its disabled
619
+ * button with a `title`, and a `title` on a DISABLED button reaches no screen
620
+ * reader and no keyboard — the tooltip needs a hover the control no longer
621
+ * answers. That hole cannot be patched from the shop's side while the shop draws
622
+ * its own button, so the button moves here and brings the wiring with it.
623
+ *
624
+ * `?: never` on the other arm is what makes the two mutually exclusive while
625
+ * still letting the component destructure both names — the same shape
626
+ * {@link SettingsSwitchLabel} uses.
627
+ */
628
+ type SettingsActionNote = {
629
+ note: ReactNode;
630
+ id: string;
631
+ } | {
632
+ note?: never;
633
+ id?: string;
634
+ };
635
+ type SettingsActionProps = SettingsActionBaseProps & SettingsActionNote;
557
636
 
558
637
  /**
559
638
  * The tab strip under the panel head.
@@ -938,4 +1017,57 @@ declare function SettingsPlacement({ label, value, points, onChange, id, classNa
938
1017
  */
939
1018
  declare function SettingsCorners({ label, value, corners, onChange, id, className, }: SettingsCornersProps): react.JSX.Element;
940
1019
 
941
- export { type SettingsCornerPoint, SettingsCorners, type SettingsCornersProps, SettingsDrill, type SettingsDrillProps, SettingsField, type SettingsFieldProps, SettingsGroup, SettingsGroupHead, type SettingsGroupHeadProps, type SettingsGroupProps, SettingsGroupSub, type SettingsGroupSubProps, SettingsPlacement, type SettingsPlacementPoint, type SettingsPlacementProps, SettingsSegmented, type SettingsSegmentedFit, type SettingsSegmentedOption, type SettingsSegmentedProps, SettingsSwitch, type SettingsSwitchBaseProps, type SettingsSwitchLabel, type SettingsSwitchProps, type SettingsTabItem, SettingsTabs, type SettingsTabsProps };
1020
+ /**
1021
+ * A FULL-WIDTH BUTTON UNDER A GROUP'S ROWS THAT COMMITS A CHANGE.
1022
+ *
1023
+ * The storefront editor's first caller is "Apply to all cards": a merchant has
1024
+ * styled one card and wants the other eleven to match. It sits at the bottom of
1025
+ * the group whose rows it copies, it is one press wide, and pressing it WRITES.
1026
+ *
1027
+ * ── WHY IT IS NOT `SettingsDrill` RENAMED ───────────────────────────────────
1028
+ *
1029
+ * A DRILL NAVIGATES. Its row is a name, a one-line preview and a chevron; it
1030
+ * commits nothing, and pressing it takes the panel down a level to the part's
1031
+ * own editor. The chevron is the promise: something opens, nothing changes, and
1032
+ * the way back is the way you came.
1033
+ *
1034
+ * THIS COMMITS AND STAYS PUT. No preview, because there is no value to preview —
1035
+ * it holds none. No chevron, because nothing opens. A disabled state that
1036
+ * explains ITSELF, which a drill has no need of: a row that only navigates is
1037
+ * rarely unavailable, and this one is unavailable often (every card already
1038
+ * matches, nothing to copy yet). And a CONFIRMATION state, which a drill has no
1039
+ * concept of at all — a doorway does not report back.
1040
+ *
1041
+ * Pointing a drill at this would ship a row that LOOKS LIKE A DOORWAY and is
1042
+ * actually a write. A merchant who has learnt that these rows open something
1043
+ * would press it to look, and eleven cards would change.
1044
+ *
1045
+ * ── THE PRESS IS ANNOUNCED, NOT JUST DRAWN ──────────────────────────────────
1046
+ *
1047
+ * The whole point of this button is that it rewrites things that are NOT ON
1048
+ * SCREEN — the other eleven cards, the sections below the fold. So the change
1049
+ * has to arrive as words: the `role="status" aria-live="polite"` region is the
1050
+ * shell's, and it is why `done` is a prop rather than a label swap the host
1051
+ * could do on its own. A host that only swapped the label would get the words
1052
+ * right and the announcement wrong, and nobody reviewing that diff would see the
1053
+ * hole — which is the kind of thing this package exists to hold once.
1054
+ *
1055
+ * The region is rendered EMPTY until `done`, and the announcement is the text
1056
+ * arriving in a region that was already there. A region that appears at the same
1057
+ * moment as its text is not reliably announced.
1058
+ *
1059
+ * ── AND THE NOTE IS WHY IT IS HERE RATHER THAN IN THE SHOP ──────────────────
1060
+ *
1061
+ * `note` is wired to the button by `aria-describedby`. `efficient-shop`'s own
1062
+ * `components/AddChildRow.tsx` explains its disabled button with a `title`, and
1063
+ * a `title` on a disabled button reaches no screen reader and no keyboard — the
1064
+ * tooltip wants a hover the control has stopped answering. The shop cannot patch
1065
+ * that from its own side while it draws its own button.
1066
+ *
1067
+ * Controlled and hook-free, like every primitive here, so a Next-16 server
1068
+ * component can import this module. The host owns the write, the `done` flag and
1069
+ * the timer that takes it back down.
1070
+ */
1071
+ declare function SettingsAction({ label, onPress, disabled, note, done, doneLabel, icon, id, className, }: SettingsActionProps): react.JSX.Element;
1072
+
1073
+ export { SettingsAction, type SettingsActionBaseProps, type SettingsActionNote, type SettingsActionProps, type SettingsCornerPoint, SettingsCorners, type SettingsCornersProps, SettingsDrill, type SettingsDrillProps, SettingsField, type SettingsFieldProps, SettingsGroup, SettingsGroupHead, type SettingsGroupHeadProps, type SettingsGroupProps, SettingsGroupSub, type SettingsGroupSubProps, SettingsPlacement, type SettingsPlacementPoint, type SettingsPlacementProps, SettingsSegmented, type SettingsSegmentedFit, type SettingsSegmentedOption, type SettingsSegmentedProps, SettingsSwitch, type SettingsSwitchBaseProps, type SettingsSwitchLabel, type SettingsSwitchProps, type SettingsTabItem, SettingsTabs, type SettingsTabsProps };
@@ -323,7 +323,42 @@ function SettingsCorners({
323
323
  }
324
324
  );
325
325
  }
326
+ function SettingsAction({
327
+ label,
328
+ onPress,
329
+ disabled,
330
+ note,
331
+ done,
332
+ doneLabel,
333
+ icon,
334
+ id,
335
+ className
336
+ }) {
337
+ const doneWords = doneLabel === void 0 ? label : doneLabel;
338
+ const words = done ? doneWords : label;
339
+ const hasNote = note !== void 0 && note !== null;
340
+ const noteId = hasNote && id ? `${id}-note` : void 0;
341
+ return /* @__PURE__ */ jsxs("div", { className: className ? `es-action-wrap ${className}` : "es-action-wrap", children: [
342
+ /* @__PURE__ */ jsxs(
343
+ "button",
344
+ {
345
+ type: "button",
346
+ id,
347
+ className: done ? "es-action is-done" : "es-action",
348
+ disabled,
349
+ "aria-describedby": noteId,
350
+ onClick: onPress,
351
+ children: [
352
+ icon ? /* @__PURE__ */ jsx("span", { className: "es-action-icon", "aria-hidden": "true", children: icon }) : null,
353
+ /* @__PURE__ */ jsx("span", { className: "es-action-label", children: words })
354
+ ]
355
+ }
356
+ ),
357
+ hasNote ? /* @__PURE__ */ jsx("p", { id: noteId, className: "es-action-note", children: note }) : null,
358
+ /* @__PURE__ */ jsx("span", { className: "es-action-live", role: "status", "aria-live": "polite", children: done ? doneWords : null })
359
+ ] });
360
+ }
326
361
 
327
- export { SettingsCorners, SettingsDrill, SettingsField, SettingsGroup, SettingsGroupHead, SettingsGroupSub, SettingsPlacement, SettingsSegmented, SettingsSwitch, SettingsTabs };
362
+ export { SettingsAction, SettingsCorners, SettingsDrill, SettingsField, SettingsGroup, SettingsGroupHead, SettingsGroupSub, SettingsPlacement, SettingsSegmented, SettingsSwitch, SettingsTabs };
328
363
  //# sourceMappingURL=index.js.map
329
364
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/panel/SettingsTabs.tsx","../../src/panel/SettingsSegmented.tsx","../../src/panel/SettingsGroup.tsx","../../src/panel/SettingsGroupHead.tsx","../../src/panel/SettingsGroupSub.tsx","../../src/panel/SettingsSwitch.tsx","../../src/panel/SettingsField.tsx","../../src/panel/SettingsDrill.tsx","../../src/panel/SettingsPlacement.tsx","../../src/panel/SettingsCorners.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 { SettingsGroupSubProps } from './types';\n\n/**\n * The QUIET second tier — a heading one rung under `SettingsGroupHead`.\n *\n * It exists because a panel sometimes has two levels of heading and this package\n * only ever drew one. `SettingsGroupHead` is a filled pill, and a pill cannot\n * serve a tier AND its subordinate: two runs of settings under one pill each\n * need a name, and giving them the same pill says they are siblings of the thing\n * above them rather than parts of it.\n *\n * THE SHOP ALREADY DREW THIS LOOK, TWICE, WHICH IS WHY IT BELONGS HERE.\n * `efficient-shop` hand-rolled `.sf-field-group` in the settings panel and\n * `.sf-group-label` in the Layers tree, and they are the same type to the\n * decimal — 10.5px / 600 / .07em / muted / uppercase. One look, hand-rolled in\n * two panels of one editor, is the definition of a thing the shell should own.\n *\n * ⚠️ NOT THE PILL WITH ITS FILL SWITCHED OFF, and that is the whole reason this\n * is a component and not a `variant` flag on the other one. The shop's own\n * `WHY NOT SettingsGroupHead` docblock refused that shape in advance: a named\n * look the shell draws, \"not a boolean that switches the pill off. A flag whose\n * job is to turn the design off moves the fork inside the shell instead of\n * removing it.\" So there is no path from here to the pill's look and none back —\n * two tiers, two looks, each one this package's own.\n *\n * WHY IT IS QUIET BY TYPE AND THE PILL IS LOUD BY GROUND. The pill separates\n * itself from the settings around it with a fill; this separates itself with\n * CASE and COLOUR and takes no ground at all. That is what lets the two sit in\n * one panel without competing, and it is also why this tier can carry no rule:\n * a hairline under a heading separates it from the rows it heads, which is the\n * opposite of what a heading is for. Removing those rules is the change the\n * owner asked for.\n *\n * A REAL HEADING, defaulting to `h4` — one under the pill's default `h3`,\n * because that is where a subordinate heading sits when the pill above it took\n * the 3. The host may say otherwise; only the host knows the outline around it.\n * Puck lays an inspector out as a FLAT sibling list, so the heading list is the\n * only structure a screen reader has left to navigate by.\n *\n * NO fold, no count, no sign, no body — the same discipline as\n * {@link SettingsGroupHead}, and for the same reason: a component that folds and\n * a component that titles are two different things, and this is one that titles.\n */\nexport function SettingsGroupSub({\n title,\n level = 4,\n id,\n className,\n}: SettingsGroupSubProps) {\n const Tag = `h${level}` as const;\n return (\n <Tag id={id} className={className ? `es-group-sub ${className}` : 'es-group-sub'}>\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","import type { SettingsPlacementProps } from './types';\n\n/**\n * A 3×3 grid of positions, one of them lit — \"which of these nine?\".\n *\n * ONE CONTROL, TWO QUESTIONS THAT ARE THE SAME QUESTION. A merchant asks it of\n * an image (\"which part of this crop stays in view?\") and of a block (\"where on\n * the thing behind it does this sit?\"). Both are one of nine positions, both\n * want the merchant to PRESS THE CORNER rather than read nine words out of a\n * dropdown, and drawing them as two controls would be two things to keep\n * looking alike for ever.\n *\n * ── WHY IT IS HERE NOW, AND WHY IT WAS NOT BEFORE ───────────────────────────\n *\n * It lived in `efficient-shop` as `FocalPicker`, and its own comment recorded\n * the rule for moving it: card 66189 said *\"the day a second section wants it,\n * that is the card that promotes it\"*. Card 66214 was that second caller and\n * deliberately declined — the control was days old and had already changed\n * signature once, so freezing its shape in a released package would have been\n * early. That was right then.\n *\n * It is not right now: the shape has not moved since, and the sections that\n * hold boxes take it past six call sites. A local copy with that many callers\n * is the shape that becomes a second source, so it comes here — the rule its\n * own author wrote, applied on the day it fires.\n *\n * ── WHAT IT DOES NOT DO ─────────────────────────────────────────────────────\n *\n * It renders the CONTROL only. The row around it — the glyph, the label, the\n * caption — is `SettingsField`'s, exactly as `SettingsSwitch` splits. `label`\n * is the radiogroup's accessible name, never a visible one.\n *\n * It also holds no opinion about what a position MEANS. The host hands nine\n * values and gets one back; whether that value becomes a CSS `object-position`\n * or a grid anchor is the host's business, and keeping it that way is what lets\n * one control answer both questions.\n *\n * Controlled and hook-free, like every primitive here, so a Next server\n * component can import this module.\n */\nexport function SettingsPlacement({\n label,\n value,\n points,\n onChange,\n id,\n className,\n}: SettingsPlacementProps) {\n return (\n <div\n id={id}\n className={`es-placement${className ? ` ${className}` : ''}`}\n role=\"radiogroup\"\n aria-label={label}\n >\n {points.map((p) => {\n const active = value === p.value;\n // A cell can be UNAVAILABLE — the caller's arithmetic, not ours: a\n // position that already holds all it can draw would silently overlay\n // the next thing put there. `disabled` rather than hidden, because nine\n // squares that became eight would read as a broken control, and the\n // reason rides on `title` so hovering says why.\n const off = p.disabled === true;\n return (\n <button\n key={p.value}\n type=\"button\"\n role=\"radio\"\n aria-checked={active}\n aria-label={p.label}\n aria-disabled={off || undefined}\n disabled={off}\n title={p.title ?? p.label}\n className={`es-placement-cell${active ? ' is-active' : ''}${off ? ' is-off' : ''}`}\n onClick={() => onChange(p.value)}\n >\n <span className=\"es-placement-dot\" />\n </button>\n );\n })}\n </div>\n );\n}\n","import type { SettingsCornersProps } from './types';\n\n/**\n * Four corners of a box, any number of them lit — \"which of these corners?\".\n *\n * ── WHY IT IS NOT {@link SettingsPlacement} WITH FOUR CELLS ──────────────────\n *\n * The nine-square asks WHICH ONE and gets one answer back; a radiogroup, and\n * pressing a second cell releases the first. This asks WHICH ONES and gets a\n * set: a merchant rounding a stack of boxes turns on the top two of the box at\n * the top and the bottom two of the box at the bottom, so the run reads as one\n * shape. Those are different questions with different keyboards — a radiogroup\n * moves with the arrows and admits exactly one, a set is toggled with Space and\n * admits any number — so they are two controls rather than one control with a\n * `multiple` flag that changes what its own ARIA means.\n *\n * They still look like siblings, because a merchant meeting the second one\n * should already know how to press it: the same track, the same chip, the same\n * \"the chosen thing is the darkest thing here\" reading the placement grid and\n * the segmented strip already share.\n *\n * ── THE CELL DRAWS THE ANSWER, NOT A DOT ────────────────────────────────────\n *\n * The placement grid's cell holds a dot, because a POSITION has no shape of its\n * own to show. A corner does: each cell here holds a small square whose matching\n * corner is rounded when the corner is on, and square when it is off. So the\n * control is a picture of what the setting does rather than four labels a\n * merchant has to hold in their head — and it stays readable for someone who\n * cannot separate the accent from the ground, because the SHAPE carries the\n * state as well as the fill.\n *\n * ── WHAT IT DOES NOT DO ─────────────────────────────────────────────────────\n *\n * It renders the CONTROL only. The row around it — the glyph, the label, the\n * caption — is `SettingsField`'s, the same split `SettingsSwitch` and\n * `SettingsPlacement` make. `label` is the group's accessible name, never a\n * visible one.\n *\n * It also holds no opinion about what a corner MEANS. The host hands the four\n * corners it wants offered and gets a set back; whether that set becomes a\n * `border-radius` shorthand, four custom properties or a stored preset is the\n * host's business, and keeping it that way is what lets one control serve a\n * section box, a card and whatever asks next.\n *\n * ⚠️ IT HAS NO OPINION ABOUT \"ALL FOUR\" EITHER. An empty set is a real answer\n * (every corner square) and so is a full one; a host that treats absence as\n * \"all four\" resolves that BEFORE handing `value` in, exactly as the placement\n * grid requires its value already resolved. A control that invented the default\n * would be answering a question only the host can answer.\n *\n * Controlled and hook-free, like every primitive here, so a Next server\n * component can import this module.\n */\nexport function SettingsCorners({\n label,\n value,\n corners,\n onChange,\n id,\n className,\n}: SettingsCornersProps) {\n return (\n <div\n id={id}\n className={`es-corners${className ? ` ${className}` : ''}`}\n role=\"group\"\n aria-label={label}\n >\n {/* ⚠️ THE FRAME IS LOAD-BEARING, NOT DECORATION. Without it the four cells\n read as four separate switches — Lewis saw exactly that on 2026-09-04\n and took them for tick boxes. The frame is what says \"these are the\n corners of ONE box\", which is the whole question the row asks. It was\n in the drawing he approved and was lost on the way into this package;\n this comment is so it is not lost again.\n\n `aria-hidden` and pointer-events off: it is the picture the cells sit\n on, never a control, and a screen reader that announced it would be\n reading out the box the four buttons already describe. */}\n <span className=\"es-corners-frame\" aria-hidden=\"true\" />\n {corners.map((c) => {\n const on = value.includes(c.value);\n // A corner can be UNAVAILABLE — the caller's arithmetic, not ours: a box\n // whose neighbour already rounds that edge, say. `disabled` rather than\n // hidden, because four cells that became three would read as a broken\n // control, and the reason rides on `title` so hovering says why.\n const off = c.disabled === true;\n return (\n <button\n key={c.value}\n type=\"button\"\n // A SET, so each cell is its own pressed/not-pressed control rather\n // than one of four radios. `aria-pressed` and not `aria-checked`:\n // the second announces \"checked\" inside a group that admits one,\n // which is the promise this control does not make.\n aria-pressed={on}\n aria-label={c.label}\n aria-disabled={off || undefined}\n disabled={off}\n title={c.title ?? c.label}\n className={`es-corners-cell es-corners-${c.value}${on ? ' is-on' : ''}${off ? ' is-off' : ''}`}\n onClick={() =>\n onChange(on ? value.filter((v) => v !== c.value) : [...value, c.value])\n }\n >\n {/* The square whose matching corner rounds. `aria-hidden` because\n the button's own label already says which corner this is; a\n second name here would read the row out twice. */}\n <span className=\"es-corners-mark\" aria-hidden=\"true\" />\n </button>\n );\n })}\n </div>\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/SettingsGroupSub.tsx","../../src/panel/SettingsSwitch.tsx","../../src/panel/SettingsField.tsx","../../src/panel/SettingsDrill.tsx","../../src/panel/SettingsPlacement.tsx","../../src/panel/SettingsCorners.tsx","../../src/panel/SettingsAction.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 { SettingsGroupSubProps } from './types';\n\n/**\n * The QUIET second tier — a heading one rung under `SettingsGroupHead`.\n *\n * It exists because a panel sometimes has two levels of heading and this package\n * only ever drew one. `SettingsGroupHead` is a filled pill, and a pill cannot\n * serve a tier AND its subordinate: two runs of settings under one pill each\n * need a name, and giving them the same pill says they are siblings of the thing\n * above them rather than parts of it.\n *\n * THE SHOP ALREADY DREW THIS LOOK, TWICE, WHICH IS WHY IT BELONGS HERE.\n * `efficient-shop` hand-rolled `.sf-field-group` in the settings panel and\n * `.sf-group-label` in the Layers tree, and they are the same type to the\n * decimal — 10.5px / 600 / .07em / muted / uppercase. One look, hand-rolled in\n * two panels of one editor, is the definition of a thing the shell should own.\n *\n * ⚠️ NOT THE PILL WITH ITS FILL SWITCHED OFF, and that is the whole reason this\n * is a component and not a `variant` flag on the other one. The shop's own\n * `WHY NOT SettingsGroupHead` docblock refused that shape in advance: a named\n * look the shell draws, \"not a boolean that switches the pill off. A flag whose\n * job is to turn the design off moves the fork inside the shell instead of\n * removing it.\" So there is no path from here to the pill's look and none back —\n * two tiers, two looks, each one this package's own.\n *\n * WHY IT IS QUIET BY TYPE AND THE PILL IS LOUD BY GROUND. The pill separates\n * itself from the settings around it with a fill; this separates itself with\n * CASE and COLOUR and takes no ground at all. That is what lets the two sit in\n * one panel without competing, and it is also why this tier can carry no rule:\n * a hairline under a heading separates it from the rows it heads, which is the\n * opposite of what a heading is for. Removing those rules is the change the\n * owner asked for.\n *\n * A REAL HEADING, defaulting to `h4` — one under the pill's default `h3`,\n * because that is where a subordinate heading sits when the pill above it took\n * the 3. The host may say otherwise; only the host knows the outline around it.\n * Puck lays an inspector out as a FLAT sibling list, so the heading list is the\n * only structure a screen reader has left to navigate by.\n *\n * NO fold, no count, no sign, no body — the same discipline as\n * {@link SettingsGroupHead}, and for the same reason: a component that folds and\n * a component that titles are two different things, and this is one that titles.\n */\nexport function SettingsGroupSub({\n title,\n level = 4,\n id,\n className,\n}: SettingsGroupSubProps) {\n const Tag = `h${level}` as const;\n return (\n <Tag id={id} className={className ? `es-group-sub ${className}` : 'es-group-sub'}>\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","import type { SettingsPlacementProps } from './types';\n\n/**\n * A 3×3 grid of positions, one of them lit — \"which of these nine?\".\n *\n * ONE CONTROL, TWO QUESTIONS THAT ARE THE SAME QUESTION. A merchant asks it of\n * an image (\"which part of this crop stays in view?\") and of a block (\"where on\n * the thing behind it does this sit?\"). Both are one of nine positions, both\n * want the merchant to PRESS THE CORNER rather than read nine words out of a\n * dropdown, and drawing them as two controls would be two things to keep\n * looking alike for ever.\n *\n * ── WHY IT IS HERE NOW, AND WHY IT WAS NOT BEFORE ───────────────────────────\n *\n * It lived in `efficient-shop` as `FocalPicker`, and its own comment recorded\n * the rule for moving it: card 66189 said *\"the day a second section wants it,\n * that is the card that promotes it\"*. Card 66214 was that second caller and\n * deliberately declined — the control was days old and had already changed\n * signature once, so freezing its shape in a released package would have been\n * early. That was right then.\n *\n * It is not right now: the shape has not moved since, and the sections that\n * hold boxes take it past six call sites. A local copy with that many callers\n * is the shape that becomes a second source, so it comes here — the rule its\n * own author wrote, applied on the day it fires.\n *\n * ── WHAT IT DOES NOT DO ─────────────────────────────────────────────────────\n *\n * It renders the CONTROL only. The row around it — the glyph, the label, the\n * caption — is `SettingsField`'s, exactly as `SettingsSwitch` splits. `label`\n * is the radiogroup's accessible name, never a visible one.\n *\n * It also holds no opinion about what a position MEANS. The host hands nine\n * values and gets one back; whether that value becomes a CSS `object-position`\n * or a grid anchor is the host's business, and keeping it that way is what lets\n * one control answer both questions.\n *\n * Controlled and hook-free, like every primitive here, so a Next server\n * component can import this module.\n */\nexport function SettingsPlacement({\n label,\n value,\n points,\n onChange,\n id,\n className,\n}: SettingsPlacementProps) {\n return (\n <div\n id={id}\n className={`es-placement${className ? ` ${className}` : ''}`}\n role=\"radiogroup\"\n aria-label={label}\n >\n {points.map((p) => {\n const active = value === p.value;\n // A cell can be UNAVAILABLE — the caller's arithmetic, not ours: a\n // position that already holds all it can draw would silently overlay\n // the next thing put there. `disabled` rather than hidden, because nine\n // squares that became eight would read as a broken control, and the\n // reason rides on `title` so hovering says why.\n const off = p.disabled === true;\n return (\n <button\n key={p.value}\n type=\"button\"\n role=\"radio\"\n aria-checked={active}\n aria-label={p.label}\n aria-disabled={off || undefined}\n disabled={off}\n title={p.title ?? p.label}\n className={`es-placement-cell${active ? ' is-active' : ''}${off ? ' is-off' : ''}`}\n onClick={() => onChange(p.value)}\n >\n <span className=\"es-placement-dot\" />\n </button>\n );\n })}\n </div>\n );\n}\n","import type { SettingsCornersProps } from './types';\n\n/**\n * Four corners of a box, any number of them lit — \"which of these corners?\".\n *\n * ── WHY IT IS NOT {@link SettingsPlacement} WITH FOUR CELLS ──────────────────\n *\n * The nine-square asks WHICH ONE and gets one answer back; a radiogroup, and\n * pressing a second cell releases the first. This asks WHICH ONES and gets a\n * set: a merchant rounding a stack of boxes turns on the top two of the box at\n * the top and the bottom two of the box at the bottom, so the run reads as one\n * shape. Those are different questions with different keyboards — a radiogroup\n * moves with the arrows and admits exactly one, a set is toggled with Space and\n * admits any number — so they are two controls rather than one control with a\n * `multiple` flag that changes what its own ARIA means.\n *\n * They still look like siblings, because a merchant meeting the second one\n * should already know how to press it: the same track, the same chip, the same\n * \"the chosen thing is the darkest thing here\" reading the placement grid and\n * the segmented strip already share.\n *\n * ── THE CELL DRAWS THE ANSWER, NOT A DOT ────────────────────────────────────\n *\n * The placement grid's cell holds a dot, because a POSITION has no shape of its\n * own to show. A corner does: each cell here holds a small square whose matching\n * corner is rounded when the corner is on, and square when it is off. So the\n * control is a picture of what the setting does rather than four labels a\n * merchant has to hold in their head — and it stays readable for someone who\n * cannot separate the accent from the ground, because the SHAPE carries the\n * state as well as the fill.\n *\n * ── WHAT IT DOES NOT DO ─────────────────────────────────────────────────────\n *\n * It renders the CONTROL only. The row around it — the glyph, the label, the\n * caption — is `SettingsField`'s, the same split `SettingsSwitch` and\n * `SettingsPlacement` make. `label` is the group's accessible name, never a\n * visible one.\n *\n * It also holds no opinion about what a corner MEANS. The host hands the four\n * corners it wants offered and gets a set back; whether that set becomes a\n * `border-radius` shorthand, four custom properties or a stored preset is the\n * host's business, and keeping it that way is what lets one control serve a\n * section box, a card and whatever asks next.\n *\n * ⚠️ IT HAS NO OPINION ABOUT \"ALL FOUR\" EITHER. An empty set is a real answer\n * (every corner square) and so is a full one; a host that treats absence as\n * \"all four\" resolves that BEFORE handing `value` in, exactly as the placement\n * grid requires its value already resolved. A control that invented the default\n * would be answering a question only the host can answer.\n *\n * Controlled and hook-free, like every primitive here, so a Next server\n * component can import this module.\n */\nexport function SettingsCorners({\n label,\n value,\n corners,\n onChange,\n id,\n className,\n}: SettingsCornersProps) {\n return (\n <div\n id={id}\n className={`es-corners${className ? ` ${className}` : ''}`}\n role=\"group\"\n aria-label={label}\n >\n {/* ⚠️ THE FRAME IS LOAD-BEARING, NOT DECORATION. Without it the four cells\n read as four separate switches — Lewis saw exactly that on 2026-09-04\n and took them for tick boxes. The frame is what says \"these are the\n corners of ONE box\", which is the whole question the row asks. It was\n in the drawing he approved and was lost on the way into this package;\n this comment is so it is not lost again.\n\n `aria-hidden` and pointer-events off: it is the picture the cells sit\n on, never a control, and a screen reader that announced it would be\n reading out the box the four buttons already describe. */}\n <span className=\"es-corners-frame\" aria-hidden=\"true\" />\n {corners.map((c) => {\n const on = value.includes(c.value);\n // A corner can be UNAVAILABLE — the caller's arithmetic, not ours: a box\n // whose neighbour already rounds that edge, say. `disabled` rather than\n // hidden, because four cells that became three would read as a broken\n // control, and the reason rides on `title` so hovering says why.\n const off = c.disabled === true;\n return (\n <button\n key={c.value}\n type=\"button\"\n // A SET, so each cell is its own pressed/not-pressed control rather\n // than one of four radios. `aria-pressed` and not `aria-checked`:\n // the second announces \"checked\" inside a group that admits one,\n // which is the promise this control does not make.\n aria-pressed={on}\n aria-label={c.label}\n aria-disabled={off || undefined}\n disabled={off}\n title={c.title ?? c.label}\n className={`es-corners-cell es-corners-${c.value}${on ? ' is-on' : ''}${off ? ' is-off' : ''}`}\n onClick={() =>\n onChange(on ? value.filter((v) => v !== c.value) : [...value, c.value])\n }\n >\n {/* The square whose matching corner rounds. `aria-hidden` because\n the button's own label already says which corner this is; a\n second name here would read the row out twice. */}\n <span className=\"es-corners-mark\" aria-hidden=\"true\" />\n </button>\n );\n })}\n </div>\n );\n}\n","import type { SettingsActionProps } from './types';\n\n/**\n * A FULL-WIDTH BUTTON UNDER A GROUP'S ROWS THAT COMMITS A CHANGE.\n *\n * The storefront editor's first caller is \"Apply to all cards\": a merchant has\n * styled one card and wants the other eleven to match. It sits at the bottom of\n * the group whose rows it copies, it is one press wide, and pressing it WRITES.\n *\n * ── WHY IT IS NOT `SettingsDrill` RENAMED ───────────────────────────────────\n *\n * A DRILL NAVIGATES. Its row is a name, a one-line preview and a chevron; it\n * commits nothing, and pressing it takes the panel down a level to the part's\n * own editor. The chevron is the promise: something opens, nothing changes, and\n * the way back is the way you came.\n *\n * THIS COMMITS AND STAYS PUT. No preview, because there is no value to preview —\n * it holds none. No chevron, because nothing opens. A disabled state that\n * explains ITSELF, which a drill has no need of: a row that only navigates is\n * rarely unavailable, and this one is unavailable often (every card already\n * matches, nothing to copy yet). And a CONFIRMATION state, which a drill has no\n * concept of at all — a doorway does not report back.\n *\n * Pointing a drill at this would ship a row that LOOKS LIKE A DOORWAY and is\n * actually a write. A merchant who has learnt that these rows open something\n * would press it to look, and eleven cards would change.\n *\n * ── THE PRESS IS ANNOUNCED, NOT JUST DRAWN ──────────────────────────────────\n *\n * The whole point of this button is that it rewrites things that are NOT ON\n * SCREEN — the other eleven cards, the sections below the fold. So the change\n * has to arrive as words: the `role=\"status\" aria-live=\"polite\"` region is the\n * shell's, and it is why `done` is a prop rather than a label swap the host\n * could do on its own. A host that only swapped the label would get the words\n * right and the announcement wrong, and nobody reviewing that diff would see the\n * hole — which is the kind of thing this package exists to hold once.\n *\n * The region is rendered EMPTY until `done`, and the announcement is the text\n * arriving in a region that was already there. A region that appears at the same\n * moment as its text is not reliably announced.\n *\n * ── AND THE NOTE IS WHY IT IS HERE RATHER THAN IN THE SHOP ──────────────────\n *\n * `note` is wired to the button by `aria-describedby`. `efficient-shop`'s own\n * `components/AddChildRow.tsx` explains its disabled button with a `title`, and\n * a `title` on a disabled button reaches no screen reader and no keyboard — the\n * tooltip wants a hover the control has stopped answering. The shop cannot patch\n * that from its own side while it draws its own button.\n *\n * Controlled and hook-free, like every primitive here, so a Next-16 server\n * component can import this module. The host owns the write, the `done` flag and\n * the timer that takes it back down.\n */\nexport function SettingsAction({\n label,\n onPress,\n disabled,\n note,\n done,\n doneLabel,\n icon,\n id,\n className,\n}: SettingsActionProps) {\n // The confirmation words, resolved ONCE: the button and the live region must\n // say the same thing, and `doneLabel` falling back to `label` here is what\n // stops a host that flipped `done` alone from announcing an empty string.\n const doneWords = doneLabel === undefined ? label : doneLabel;\n const words = done ? doneWords : label;\n // A note needs a DOM id to be wired, and this package is hook-free so it\n // cannot invent one — `SettingsActionNote` makes passing a note without an\n // `id` a compile error. A JS host that gets past the types still gets the\n // note DRAWN below: losing the words a merchant can read would be worse than\n // losing the wiring, and the type is what keeps the wiring from being lost.\n const hasNote = note !== undefined && note !== null;\n const noteId = hasNote && id ? `${id}-note` : undefined;\n\n return (\n <div className={className ? `es-action-wrap ${className}` : 'es-action-wrap'}>\n <button\n type=\"button\"\n id={id}\n className={done ? 'es-action is-done' : 'es-action'}\n // A REAL disabled button, not `aria-disabled` on a live one: there is no\n // \"explain the press\" story here that wants the button focusable — the\n // reason is on the note, which `aria-describedby` reaches whether the\n // button answers the pointer or not.\n disabled={disabled}\n aria-describedby={noteId}\n onClick={onPress}\n >\n {icon ? (\n <span className=\"es-action-icon\" aria-hidden=\"true\">\n {icon}\n </span>\n ) : null}\n <span className=\"es-action-label\">{words}</span>\n </button>\n {hasNote ? (\n <p id={noteId} className=\"es-action-note\">\n {note}\n </p>\n ) : null}\n {/* ALWAYS RENDERED, empty until the press lands — see the docblock. */}\n <span className=\"es-action-live\" role=\"status\" aria-live=\"polite\">\n {done ? doneWords : null}\n </span>\n </div>\n );\n}\n"]}
@@ -1268,3 +1268,125 @@ button.es-group-head:hover { background: var(--es-chip-bg); }
1268
1268
  @media (prefers-reduced-motion: reduce) {
1269
1269
  .es-corners-mark { transition: none; }
1270
1270
  }
1271
+
1272
+ /* ── THE ACTION BUTTON (`[ADD] A Panel Button That Does One Thing`) ─────────────────────────────────────────
1273
+ The full-width press under a group's rows that COMMITS — "Apply to all
1274
+ cards". Measured against `.es-drill`, the panel's other full-width row, and
1275
+ NOT against the shop's `.sf-add-card`: this is a settings row like the rest
1276
+ of the panel, so it takes the panel's row width and the panel's radius.
1277
+
1278
+ IT LOOKS LIKE A BUTTON AND NOT LIKE A ROW, deliberately. The drill is flat
1279
+ with a hover tint, because a list of doorways is a list. This one is a single
1280
+ press that writes, so it carries a border and a field ground: a merchant
1281
+ should be able to tell at a glance that the thing at the bottom of the group
1282
+ is not the twelfth setting.
1283
+
1284
+ NO TOP RULE. The air above it is the separator — the same discipline card
1285
+ 66350 applied to the drill list, and `panel-no-divider-lines.test.ts` is what
1286
+ keeps it. */
1287
+ .es-action-wrap {
1288
+ /* IT BELONGS TO THE ROWS ABOVE IT, so it sits closer than one setting sits to
1289
+ the next (38) and plainly apart from the last row. The floor rides
1290
+ underneath as `max()`, like every other row-to-row seam on this panel. */
1291
+ margin-top: max(var(--es-row-gap), var(--es-rhythm-between-tight));
1292
+ }
1293
+
1294
+ .es-action {
1295
+ box-sizing: border-box;
1296
+ width: 100%;
1297
+ display: flex;
1298
+ align-items: center;
1299
+ justify-content: center;
1300
+ gap: 8px;
1301
+ padding: 10px 11px;
1302
+ border: 1px solid var(--es-btn-border);
1303
+ border-radius: var(--es-row-radius);
1304
+ background: var(--es-field-bg);
1305
+ color: var(--es-btn-text);
1306
+ font: inherit;
1307
+ font-size: 12px;
1308
+ font-weight: 600;
1309
+ cursor: pointer;
1310
+ transition: background 0.12s, border-color 0.12s, color 0.12s;
1311
+ }
1312
+
1313
+ .es-action:hover:not(:disabled) {
1314
+ background: var(--es-hover);
1315
+ }
1316
+
1317
+ /* An INSET ring, for the reason the drill gives: a full-width row has the
1318
+ panel's rows hard against its edges, and an outset ring would be clipped. */
1319
+ .es-action:focus-visible {
1320
+ outline: none;
1321
+ box-shadow: inset 0 0 0 2px var(--es-accent-ring);
1322
+ }
1323
+
1324
+ /* UNAVAILABLE, AND STILL THERE. Dimmed rather than hidden — a control that
1325
+ vanishes leaves a merchant hunting for something they remember, and takes the
1326
+ reason away with it. The reason is the note below, which a screen reader
1327
+ reaches through `aria-describedby` whether the button answers a pointer or
1328
+ not; a `title` here would reach neither. */
1329
+ .es-action:disabled {
1330
+ cursor: default;
1331
+ color: var(--es-disabled);
1332
+ background: var(--es-track-bg);
1333
+ border-color: var(--es-line);
1334
+ }
1335
+
1336
+ /* THE PRESS HAS LANDED.
1337
+
1338
+ THE WORD IS THE STATE, and the colour agrees with it — the button says
1339
+ "Applied to 12 cards" and the live region announces the same string, so
1340
+ nothing here is carried by colour alone.
1341
+
1342
+ ⚠️ NOT A TINTED GROUND. `--es-accent-tint` is card 8811's retired look and
1343
+ this sheet no longer draws it anywhere; `panel-layout.test.ts` sweeps the
1344
+ whole file for it. So the confirmation is drawn the way the rest of the panel
1345
+ draws a positive: the text and the edge move, the ground does not. */
1346
+ .es-action.is-done {
1347
+ color: var(--es-success);
1348
+ border-color: var(--es-success);
1349
+ }
1350
+
1351
+ .es-action-icon {
1352
+ flex: none;
1353
+ display: inline-flex;
1354
+ align-items: center;
1355
+ color: currentColor;
1356
+ }
1357
+
1358
+ .es-action-label {
1359
+ min-width: 0;
1360
+ }
1361
+
1362
+ /* Why the press is unavailable, or what it is about to rewrite. Quiet, under
1363
+ the button, and wrapped — this is a clause, not a name, the same split
1364
+ `.es-field-hint` makes. */
1365
+ .es-action-note {
1366
+ margin: 6px 0 0;
1367
+ font-size: 11px;
1368
+ line-height: 1.4;
1369
+ color: var(--es-muted);
1370
+ }
1371
+
1372
+ /* THE ANNOUNCEMENT, and nothing on screen. The button already shows the state;
1373
+ this exists so a merchant who cannot see the button HEARS that eleven cards
1374
+ they were not looking at have changed. Taken off screen the standard way —
1375
+ `display: none` and `visibility: hidden` are both skipped by screen readers,
1376
+ which would leave this rendering nothing at all. */
1377
+ .es-action-live {
1378
+ position: absolute;
1379
+ width: 1px;
1380
+ height: 1px;
1381
+ margin: -1px;
1382
+ padding: 0;
1383
+ overflow: hidden;
1384
+ clip: rect(0 0 0 0);
1385
+ clip-path: inset(50%);
1386
+ white-space: nowrap;
1387
+ border: 0;
1388
+ }
1389
+
1390
+ @media (prefers-reduced-motion: reduce) {
1391
+ .es-action { transition: none; }
1392
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "editor-shell",
3
- "version": "0.37.0",
3
+ "version": "0.38.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 \u2014 type in place and watch the markup formatting appear as you type.",
5
5
  "license": "MIT",
6
6
  "author": "Lewis Liu",