editor-shell 0.11.0 → 0.13.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/README.md CHANGED
@@ -50,16 +50,38 @@ export function LeftRail({ view, onView, onAdd }) {
50
50
 
51
51
  ## API
52
52
 
53
- ### `<EditorRail items ariaLabel? className? />`
53
+ ### `<EditorRail items ariaLabel? className? orientation? />`
54
54
 
55
- A **pure presentational** vertical toolbar. It owns the look, never the app
56
- state — you decide which item is `active` and what each `onSelect` does.
55
+ A **pure presentational** toolbar. It owns the look, never the app state — you
56
+ decide which item is `active` and what each `onSelect` does.
57
57
 
58
58
  | prop | type | default | notes |
59
59
  |---|---|---|---|
60
- | `items` | `EditorRailItem[]` | — | buttons, top→bottom, in render order |
60
+ | `items` | `EditorRailItem[]` | — | buttons, in render order |
61
61
  | `ariaLabel` | `string` | `"Editor"` | names the toolbar for assistive tech |
62
62
  | `className` | `string` | — | merged onto `.es-rail` |
63
+ | `orientation` | `'column' \| 'row'` | `'column'` | which way the buttons run |
64
+
65
+ #### `orientation`
66
+
67
+ `'column'` is the rail every editor draws today: a fixed 46px strip wearing its
68
+ own card. It is the default, and it renders exactly what it rendered before this
69
+ prop existed.
70
+
71
+ `'row'` lays the same buttons left to right for a top bar — **no fixed width**
72
+ (as wide as its buttons) and **no card** (no background, radius or shadow),
73
+ because the bar already has its own surface. The button size, icon size, gap and
74
+ every hover/active colour are identical in both directions; only the container
75
+ changes. A row is 34px tall, so a 52px bar centres it with 9px to spare.
76
+
77
+ ```tsx
78
+ // The same rail, in the top bar's empty middle.
79
+ <EditorRail orientation="row" ariaLabel="Editor views" items={items} />
80
+ ```
81
+
82
+ `aria-orientation` follows the layout (`vertical` / `horizontal`); the `toolbar`
83
+ role does not change, because a rail that mixes view toggles with plain actions
84
+ is a toolbar whichever way it runs.
63
85
 
64
86
  ### `EditorRailItem`
65
87
 
@@ -102,6 +124,11 @@ properties on `.es-rail`). Taken verbatim from the storefront editor's `.sf-rail
102
124
  | accent (active) | `#2563eb` | `--es-rail-accent` |
103
125
  | accent tint (active bg) | `#eff6ff` | `--es-rail-accent-tint` |
104
126
 
127
+ `orientation="row"` adds **no new token**. It re-points three of the ones above
128
+ — `--es-rail-bg` to `transparent`, `--es-rail-shadow` to `none`,
129
+ `--es-rail-radius` to `0` — so the card look stays declared in one place, and it
130
+ reuses `--es-rail-pad-y` rotated onto the row's ends and `--es-rail-gap` as-is.
131
+
105
132
  Re-theme without new CSS by overriding a var on the element:
106
133
  `style={{ ['--es-rail-accent']: brand }}`.
107
134
 
@@ -24,13 +24,19 @@ function EditorRailButton({
24
24
  }
25
25
  );
26
26
  }
27
- function EditorRail({ items, ariaLabel = "Editor", className }) {
27
+ function EditorRail({
28
+ items,
29
+ ariaLabel = "Editor",
30
+ className,
31
+ orientation = "column"
32
+ }) {
33
+ const row = orientation === "row";
28
34
  return /* @__PURE__ */ jsx(
29
35
  "div",
30
36
  {
31
- className: ["es-rail", className ?? ""].filter(Boolean).join(" "),
37
+ className: ["es-rail", row ? "es-rail-row" : "", className ?? ""].filter(Boolean).join(" "),
32
38
  role: "toolbar",
33
- "aria-orientation": "vertical",
39
+ "aria-orientation": row ? "horizontal" : "vertical",
34
40
  "aria-label": ariaLabel,
35
41
  children: items.map((item) => /* @__PURE__ */ jsx(
36
42
  EditorRailButton,
@@ -102,5 +108,5 @@ var railCssVars = {
102
108
  };
103
109
 
104
110
  export { EditorRail, EditorRailButton, railCssVars, railTokens };
105
- //# sourceMappingURL=chunk-3VMFPQGZ.js.map
106
- //# sourceMappingURL=chunk-3VMFPQGZ.js.map
111
+ //# sourceMappingURL=chunk-HYOACICE.js.map
112
+ //# sourceMappingURL=chunk-HYOACICE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rail/EditorRailButton.tsx","../src/rail/EditorRail.tsx","../src/rail/tokens.ts"],"names":["jsx"],"mappings":"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","file":"chunk-HYOACICE.js","sourcesContent":["import type { EditorRailButtonProps } from './types';\n\n/**\n * A single rail button: an icon in a 34×34 square that hovers, highlights when\n * active, and dims when disabled. Look only — the caller owns the click.\n *\n * `aria-pressed` is emitted **only** when `active` is a real boolean, so a view\n * toggle announces its pressed state while an action button (the quick-add \"+\",\n * whose item omits `active`) stays a plain button rather than a stuck toggle.\n */\nexport function EditorRailButton({\n label,\n icon,\n active,\n disabled,\n onSelect,\n className,\n}: EditorRailButtonProps) {\n const classes = ['es-rail-btn', active ? 'is-active' : '', className ?? '']\n .filter(Boolean)\n .join(' ');\n\n return (\n <button\n type=\"button\"\n className={classes}\n title={label}\n aria-label={label}\n aria-pressed={typeof active === 'boolean' ? active : undefined}\n disabled={disabled}\n onClick={disabled ? undefined : onSelect}\n >\n {icon}\n </button>\n );\n}\n","import { EditorRailButton } from './EditorRailButton';\nimport type { EditorRailProps } from './types';\n\n/**\n * The shared icon-rail — one slim strip of icon buttons that every EFFICIENT\n * editor uses to switch panels (Pages / Layers / Styles / Media / Sitemap) and\n * to quick-add (\"+\"). By default it is the vertical strip down the left; pass\n * `orientation=\"row\"` and the same buttons lay into a top bar instead. It is a\n * **pure presentational switcher**: give it `items[]`, it draws them; which one\n * is `active` and what each does is the host's business.\n *\n * No `'use client'` here on purpose — the rail has no hooks or state, so it is a\n * plain module a Next-16 server component can import and even statically render.\n * It's interactive only through the `onSelect` props the host passes, and the\n * host owns its client boundary (both editors' chrome are already client\n * components), so baking a directive into this leaf would only narrow where it\n * can be imported.\n *\n * A `toolbar` (not a `tablist`), because the rail mixes view *toggles* with\n * plain *actions* like the quick-add — a toolbar of buttons models that\n * honestly, where a tablist would force every button to pretend to be a tab.\n * That is true in both directions, so `orientation` moves `aria-orientation`\n * and leaves the role alone: what the rail IS did not change, only which way it\n * runs — and a toolbar's arrow keys follow its orientation, so a row that kept\n * saying `vertical` would describe the wrong keys.\n *\n * `orientation` defaults to `'column'`, and that path is byte-identical to what\n * this component rendered before the prop existed: same class list, same\n * attributes, same order. `'row'` adds ONE modifier class beside `es-rail` —\n * never instead of it, since every button rule and token in rail.css hangs off\n * the base class — and rail.css does the rest.\n */\nexport function EditorRail({\n items,\n ariaLabel = 'Editor',\n className,\n orientation = 'column',\n}: EditorRailProps) {\n const row = orientation === 'row';\n\n return (\n <div\n className={['es-rail', row ? 'es-rail-row' : '', className ?? '']\n .filter(Boolean)\n .join(' ')}\n role=\"toolbar\"\n aria-orientation={row ? 'horizontal' : 'vertical'}\n aria-label={ariaLabel}\n >\n {items.map((item) => (\n <EditorRailButton\n key={item.id}\n label={item.label}\n icon={item.icon}\n active={item.active}\n disabled={item.disabled}\n onSelect={item.onSelect}\n />\n ))}\n </div>\n );\n}\n","/**\n * Rail design tokens — the ONE source both apps' rails converge on.\n *\n * Every value below is taken verbatim from the storefront editor's rail\n * (`efficient-shop` → `storefront-editor.css`, the `.sf-rail` / `.sf-rail-btn`\n * rules), which is the visual reference. The admin portal's hand-rolled rail had\n * drifted (button 36 vs 34, icon 20 vs 18, radius 8 vs 9, a Tailwind accent\n * instead of a CSS var); these tokens end that drift.\n *\n * Exposed BOTH ways, per the same value:\n * - `railTokens` — this JS object, for anything that needs the numbers/colours\n * in code (inline styles, a theme bridge, a Storybook control);\n * - `rail.css` — the stylesheet, which declares the identical values as CSS\n * custom properties on `.es-rail` (names listed in `railCssVars`).\n *\n * Numeric fields are pixel magnitudes (unitless) so they compose in code; the\n * CSS applies the `px`.\n */\nexport const railTokens = {\n /** Rail column width. `--sf-rail-w: 46px`. */\n railWidth: 46,\n /** Vertical padding inside the rail (top === bottom). */\n railPaddingY: 8,\n /** Gap between buttons. */\n railGap: 4,\n /** Rail card corner radius. `--sf-panel-radius: 12px`. */\n railRadius: 12,\n /** Button hit-area (square). */\n buttonSize: 34,\n /** Button corner radius. */\n buttonRadius: 9,\n /** Icon glyph size. */\n iconSize: 18,\n /** Hover/active colour transition. */\n transition: 'background 0.12s, color 0.12s',\n /** Disabled-button opacity. */\n disabledOpacity: 0.35,\n color: {\n /** Rail card background. `--sf-panel`. */\n background: '#ffffff',\n /** Rail card shadow. `--sf-panel-shadow`. */\n shadow: '0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.06)',\n /** Idle icon colour. `--sf-muted` (gray-500). */\n idle: '#6b7280',\n /** Hover background. `--sf-hover` (gray-100). */\n hoverBg: '#f3f4f6',\n /** Hover icon colour. `--sf-text` (gray-900). */\n hoverFg: '#111827',\n /** Active icon colour. `--sf-accent` (blue-600). */\n accent: '#2563eb',\n /** Active background. `--sf-accent-tint` (blue-50). */\n accentTint: '#eff6ff',\n },\n} as const;\n\nexport type RailTokens = typeof railTokens;\n\n/**\n * The CSS custom-property name behind each token. `rail.css` sets these on\n * `.es-rail`; override any of them on (or above) the rail element to re-theme it\n * — e.g. `style={{ ['--es-rail-accent']: brand }}` — without shipping new CSS.\n */\nexport const railCssVars = {\n railWidth: '--es-rail-w',\n railPaddingY: '--es-rail-pad-y',\n railGap: '--es-rail-gap',\n railRadius: '--es-rail-radius',\n buttonSize: '--es-rail-btn-size',\n buttonRadius: '--es-rail-btn-radius',\n iconSize: '--es-rail-icon-size',\n background: '--es-rail-bg',\n shadow: '--es-rail-shadow',\n idle: '--es-rail-fg',\n hoverBg: '--es-rail-hover-bg',\n hoverFg: '--es-rail-hover-fg',\n accent: '--es-rail-accent',\n accentTint: '--es-rail-accent-tint',\n} as const;\n"]}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { EditorRail, EditorRailButton, EditorRailButtonProps, EditorRailItem, EditorRailProps, RailTokens, railCssVars, railTokens } from './rail/index.js';
1
+ export { EditorRail, EditorRailButton, EditorRailButtonProps, EditorRailItem, EditorRailOrientation, EditorRailProps, RailTokens, railCssVars, railTokens } from './rail/index.js';
2
2
  export { AddIcon, LayersIcon, MediaIcon, PageIcon as PagesIcon, IconProps as RailIconProps, SitemapIcon, StylesIcon } from './icons/index.js';
3
3
  export { ShellTokens, puckAzureRamp, shellCssVars, shellTokens } from './shell/index.js';
4
4
  import 'react';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { EditorRail, EditorRailButton, railCssVars, railTokens } from './chunk-3VMFPQGZ.js';
1
+ export { EditorRail, EditorRailButton, railCssVars, railTokens } from './chunk-HYOACICE.js';
2
2
  export { AddIcon, LayersIcon, MediaIcon, PageIcon as PagesIcon, SitemapIcon, StylesIcon } from './chunk-M7B5VTWH.js';
3
3
  export { puckAzureRamp, shellCssVars, shellTokens } from './chunk-EZKBKK3E.js';
4
4
  //# sourceMappingURL=index.js.map
@@ -169,6 +169,48 @@ interface SettingsSwitchBaseProps {
169
169
  className?: string;
170
170
  }
171
171
  type SettingsSwitchProps = SettingsSwitchBaseProps & SettingsSwitchLabel;
172
+ /**
173
+ * A DRILL-IN ROW — a name, a one-line preview, and a chevron that says pressing
174
+ * it takes the panel down a level (card 8819).
175
+ *
176
+ * The storefront editor's text editing becomes a drill-down: the section screen
177
+ * lists a section's text parts as these rows, and pressing one scopes the
178
+ * inspector to that one part's editor. It lives in the shell for the reason the
179
+ * rest of this grammar does — the campaign designer draws the same list, and a
180
+ * second hand-rolled copy is exactly how the two editors drift.
181
+ *
182
+ * CONTROLLED and hook-free like the other four: it holds no open/closed state of
183
+ * its own. Pressing it calls `onOpen`, and the HOST decides what drilling down
184
+ * means, because only the host owns the panel's navigation.
185
+ */
186
+ interface SettingsDrillProps {
187
+ /** The part's name — the loud primary line ("Heading", "Slide name"). */
188
+ name: string;
189
+ /**
190
+ * One line of what the part reads as, shown quiet under the name and clipped
191
+ * with an ellipsis. Optional: a row can be a name alone, and an absent preview
192
+ * draws no empty second line rather than a blank one.
193
+ */
194
+ preview?: ReactNode;
195
+ /**
196
+ * Mark the PREVIEW as belonging to a part with no place on the page — an
197
+ * identifier the eye never draws (a `Slide name`), reachable only from this
198
+ * list. It reads quiet and italic. The mark is on the preview, not the row:
199
+ * the name still reads as a name.
200
+ */
201
+ offPage?: boolean;
202
+ /**
203
+ * A trailing glyph before the chevron — the "hidden on the page" eye is the
204
+ * standing case. A ReactNode, not a boolean, so the host owns the icon and its
205
+ * accessible name (a bare glyph says nothing to a screen reader on its own).
206
+ */
207
+ status?: ReactNode;
208
+ /** Asked to open. The host owns what drilling down does. */
209
+ onOpen: () => void;
210
+ /** A DOM id for the row button, so a host can point a label at it. */
211
+ id?: string;
212
+ className?: string;
213
+ }
172
214
 
173
215
  /**
174
216
  * The tab strip under the panel head.
@@ -327,4 +369,28 @@ declare function SettingsField({ label, labelId, icon, value, hint, changed, htm
327
369
  */
328
370
  declare function SettingsSwitch({ checked, onChange, disabled, id, ariaLabel, ariaLabelledBy, className, }: SettingsSwitchProps): react.JSX.Element;
329
371
 
330
- export { SettingsField, type SettingsFieldProps, SettingsGroup, SettingsGroupHead, type SettingsGroupHeadProps, type SettingsGroupProps, SettingsSwitch, type SettingsSwitchBaseProps, type SettingsSwitchLabel, type SettingsSwitchProps, type SettingsTabItem, SettingsTabs, type SettingsTabsProps };
372
+ /**
373
+ * A DRILL-IN ROW — a name, a one-line preview, and a chevron (card 8819).
374
+ *
375
+ * The storefront editor's text editing becomes a panel drill-down: the section
376
+ * screen no longer holds a text box for every part; it LISTS the parts as these
377
+ * rows, and pressing one takes the inspector down a level to that part's own
378
+ * editor. A part the eye hid — or one that never draws on the page at all, like
379
+ * a `Slide name` — has no words on the canvas to click, so this list is the only
380
+ * way back to it. That is why the list exists, and why the row is the shell's,
381
+ * not the shop's: the campaign designer draws the same list.
382
+ *
383
+ * IT IS ONE BUTTON, not a control beside some text. The name and the preview are
384
+ * the button's own content, so they ARE its accessible name — a chevron floating
385
+ * next to unlabelled text would be a press a screen reader could not describe.
386
+ * The chevron is `aria-hidden` (pure direction); the status slot is NOT, because
387
+ * its meaning is the host's to name — it passes an icon carrying its own label
388
+ * (a `title`, a visually-hidden word), and hiding the slot would bury that.
389
+ *
390
+ * CONTROLLED and hook-free like the other four primitives: it owns no open state.
391
+ * Pressing it calls `onOpen`, and the host decides what drilling down means —
392
+ * which keeps this a directive-free leaf a Next-16 server component can import.
393
+ */
394
+ declare function SettingsDrill({ name, preview, offPage, status, onOpen, id, className, }: SettingsDrillProps): react.JSX.Element;
395
+
396
+ export { SettingsDrill, type SettingsDrillProps, SettingsField, type SettingsFieldProps, SettingsGroup, SettingsGroupHead, type SettingsGroupHeadProps, type SettingsGroupProps, SettingsSwitch, type SettingsSwitchBaseProps, type SettingsSwitchLabel, type SettingsSwitchProps, type SettingsTabItem, SettingsTabs, type SettingsTabsProps };
@@ -156,7 +156,35 @@ function SettingsField({
156
156
  hint ? /* @__PURE__ */ jsx("p", { className: "es-field-hint", children: hint }) : null
157
157
  ] });
158
158
  }
159
+ function SettingsDrill({
160
+ name,
161
+ preview,
162
+ offPage,
163
+ status,
164
+ onOpen,
165
+ id,
166
+ className
167
+ }) {
168
+ const hasPreview = preview !== void 0 && preview !== null;
169
+ return /* @__PURE__ */ jsxs(
170
+ "button",
171
+ {
172
+ type: "button",
173
+ id,
174
+ className: className ? `es-drill ${className}` : "es-drill",
175
+ onClick: onOpen,
176
+ children: [
177
+ /* @__PURE__ */ jsxs("span", { className: "es-drill-text", children: [
178
+ /* @__PURE__ */ jsx("span", { className: "es-drill-name", children: name }),
179
+ hasPreview ? /* @__PURE__ */ jsx("span", { className: offPage ? "es-drill-preview is-offpage" : "es-drill-preview", children: preview }) : null
180
+ ] }),
181
+ status ? /* @__PURE__ */ jsx("span", { className: "es-drill-status", children: status }) : null,
182
+ /* @__PURE__ */ jsx("span", { className: "es-drill-chev", "aria-hidden": "true" })
183
+ ]
184
+ }
185
+ );
186
+ }
159
187
 
160
- export { SettingsField, SettingsGroup, SettingsGroupHead, SettingsSwitch, SettingsTabs };
188
+ export { SettingsDrill, SettingsField, SettingsGroup, SettingsGroupHead, SettingsSwitch, SettingsTabs };
161
189
  //# sourceMappingURL=index.js.map
162
190
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/panel/SettingsTabs.tsx","../../src/panel/SettingsGroup.tsx","../../src/panel/SettingsGroupHead.tsx","../../src/panel/SettingsSwitch.tsx","../../src/panel/SettingsField.tsx"],"names":["jsxs","jsx"],"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 { SettingsGroupProps } from './types';\n\n/**\n * A group of settings under a soft pill row that folds with − / +.\n *\n * The sign is drawn by `panel.css` from `aria-expanded`, so the open state has\n * exactly one home (the attribute assistive tech already reads) instead of a\n * second copy in the markup.\n *\n * `count` is the reason a fold here is safe: a folded group that holds changes\n * still says so on its pill, so nothing a merchant changed can hide.\n */\nexport function SettingsGroup({\n title,\n open,\n onToggle,\n count,\n id,\n children,\n className,\n}: SettingsGroupProps) {\n const bodyId = id ? `${id}-body` : undefined;\n return (\n <div className={className ? `es-group ${className}` : 'es-group'}>\n <button\n type=\"button\"\n id={id}\n aria-expanded={open}\n aria-controls={bodyId}\n className=\"es-group-head\"\n onClick={() => onToggle(!open)}\n >\n {title}\n {typeof count === 'number' && count > 0 ? (\n <span className=\"es-group-count\">{count}</span>\n ) : null}\n <span className=\"es-group-sign\" aria-hidden=\"true\" />\n </button>\n <div id={bodyId} className=\"es-group-body\" hidden={!open}>\n {children}\n </div>\n </div>\n );\n}\n","import type { SettingsGroupHeadProps } from './types';\n\n/**\n * A group heading with NO group under it — the pill on its own.\n *\n * It exists because Puck lays the inspector out as a FLAT sibling list: a field\n * renders next to the heading above it, never inside it, so nothing here can\n * wrap the rows that follow. Heading a run of settings is therefore a job for a\n * component that heads and nothing else.\n *\n * `SettingsGroup` cannot do that job. It always renders its body div, and its\n * pill is always a <button> carrying `aria-expanded`, `aria-controls` and the\n * − / + sign. Used header-only it would draw a control that claims to expand\n * something, does nothing when clicked, and tells a screen reader the same lie.\n * A component that folds and a component that titles are two different things,\n * and this is the one that titles.\n *\n * So it is a HEADING, not a control: a real <h3> (the level is the host's, since\n * only the host knows the page outline around it) with no role bolted on, no\n * expanded state, no sign, no body. In a flat list the heading list is the only\n * structure a screen reader has left to navigate by, which is the whole reason\n * this renders a heading element rather than a styled <div>.\n *\n * It wears `.es-group-head` — the same pill as a real group's — so a section\n * that folds and a section that does not read as the same panel. `panel.css`\n * scopes the cursor and the hover tint to `button.es-group-head`, so the\n * heading does not offer a press it cannot honour.\n *\n * NO `count`. A folded group needs one because a fold can hide a change; a\n * heading folds nothing, so nothing can hide behind it.\n */\nexport function SettingsGroupHead({\n title,\n level = 3,\n id,\n className,\n}: SettingsGroupHeadProps) {\n const Tag = `h${level}` as const;\n return (\n <Tag id={id} className={className ? `es-group-head ${className}` : 'es-group-head'}>\n {title}\n </Tag>\n );\n}\n","import type { SettingsSwitchProps } from './types';\n\n/**\n * The yes-or-no control — ONE shape, both editors (card 8801).\n *\n * Point 5 of the approved settings design says one switch style everywhere.\n * Before this, the storefront editor drew a tick box and the campaign designer\n * drew a green switch, so a merchant met two shapes for the same question and\n * had to learn the control twice. Both now import this file, which is the only\n * thing that stops them drifting apart again.\n *\n * It is a real <button>, and that is the whole keyboard story: a button is\n * activated by Space and by Enter in every browser, and that activation IS a\n * click — so the one `onClick` below serves mouse and keyboard alike. A div\n * wearing `role=\"switch\"` would need a key handler of our own. Do not add one.\n * `type=\"button\"` keeps it from submitting a form it happens to sit in.\n *\n * It cannot exist without a NAME. `SettingsSwitchProps` demands `ariaLabel` or\n * `ariaLabelledBy` and forbids both at once, so an unnamed switch — which a\n * screen reader reads out as \"switch, on\" and nothing more — is a compile error\n * instead of something a docstring asks you to remember.\n *\n * ONE-WAY DEPENDENCY. `SettingsField` imports this module so it can recognise a\n * switch in its `action` slot and hand it the field's label as a name. This\n * module must NEVER import `SettingsField` back — that would close a cycle\n * between two siblings in the same folder. It imports `./types` and nothing\n * else, and `tests/settings-switch.test.tsx` (o) keeps it that way.\n *\n * CONTROLLED and hook-free like the other three: `checked` comes from the host\n * and the switch only ever ASKS to be flipped. That is what keeps this module a\n * directive-free leaf a Next-16 server component can import.\n */\nexport function SettingsSwitch({\n checked,\n onChange,\n disabled,\n id,\n ariaLabel,\n ariaLabelledBy,\n className,\n}: SettingsSwitchProps) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n id={id}\n aria-checked={checked}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n disabled={disabled}\n className={className ? `es-switch ${className}` : 'es-switch'}\n onClick={() => onChange(!checked)}\n />\n );\n}\n","import { cloneElement, isValidElement } from 'react';\nimport type { ReactElement, ReactNode } from 'react';\n\nimport { SettingsSwitch } from './SettingsSwitch';\nimport type { SettingsFieldProps } from './types';\n\n/**\n * Does this slot hold a control?\n *\n * The rule is ELEMENT-OR-NOTHING, deliberately not a list of falsy values: a\n * list rots, and the next odd value nobody thought of walks straight through.\n * `{flag && <Slider/>}` yields `false`, `{count && <Slider/>}` yields `0` when\n * the count is zero — and React RENDERS that zero, a stray 0 sitting in the\n * panel where a control belongs. A bare string or number in a control slot is a\n * caller mistake in every case any of us can name.\n *\n * Arrays are walked rather than trusted: `Array.isArray([])` is true, so an\n * empty array — or one holding only `false` and `null`, which is what a list of\n * conditional controls collapses to — would otherwise draw the empty row this\n * check exists to prevent.\n */\nfunction hasControlIn(slot: ReactNode): boolean {\n if (Array.isArray(slot)) return slot.some(hasControlIn);\n return isValidElement(slot);\n}\n\n/** What a switch is named by. Read off an unknown element, so it stays loose. */\ninterface SwitchName {\n ariaLabel?: string;\n ariaLabelledBy?: string;\n}\n\n/**\n * Give a switch in the `action` slot the field's own label as its name.\n *\n * A switch carries no text, and `htmlFor` cannot bridge a <label> to a\n * <button>, so in a boolean row nothing connects the visible label to the\n * control unless something does it explicitly. Doing it HERE means the common\n * case is right with the caller saying nothing: the accessible name and the\n * visible name are the same string and cannot drift apart.\n *\n * It only ever fills a gap. A switch that names itself keeps its own name, and\n * anything that is not a switch — a reset dot, a badge — is left alone, since\n * labelling those with the field's name would be worse than not labelling them.\n */\nfunction nameSwitch(action: ReactNode, label: string, labelId?: string): ReactNode {\n if (!isValidElement(action) || action.type !== SettingsSwitch) return action;\n\n const named = action.props as SwitchName;\n if (named.ariaLabel || named.ariaLabelledBy) return action;\n\n return cloneElement(\n action as ReactElement<SwitchName>,\n labelId ? { ariaLabelledBy: labelId } : { ariaLabel: label },\n );\n}\n\n/**\n * One setting: a label row, then its control.\n *\n * The row is stacked rather than side-by-side so the control gets the panel's\n * full width — a 308px panel cannot afford a label column AND a usable slider.\n * The label reads small and quiet; the VALUE sits at the end of the same line in\n * the text colour, which is what a merchant scans for. That contrast is the\n * point: before this, label and value looked alike and the panel read as noise.\n *\n * `hint` is where a clause-long explanation goes. A label is a name, never a\n * sentence.\n *\n * A BOOLEAN row is the one exception to \"control on its own line\" (card 8801,\n * Option A, approved). A switch is small and it is the whole control, so it\n * rides the label line in the `action` slot and the row costs one line instead\n * of two — which is what lets a 308px panel show four settings where it used to\n * show three. That is the only reason `children` is optional: a field with no\n * children draws no control row, and its label line IS the row.\n *\n * A field with no children AND no action has no control on either slot. That is\n * a mistake at the call site, so it renders NOTHING — an empty row would hide\n * the mistake behind 20px of blank panel.\n */\nexport function SettingsField({\n label,\n labelId,\n icon,\n value,\n hint,\n changed,\n htmlFor,\n action,\n children,\n className,\n}: SettingsFieldProps) {\n const hasControl = hasControlIn(children);\n const hasAction = hasControlIn(action);\n if (!hasControl && !hasAction) return null;\n\n const classes = ['es-field'];\n if (!hasControl) classes.push('is-inline');\n if (changed) classes.push('is-changed');\n if (className) classes.push(className);\n\n const labelBody = (\n <>\n {icon ? (\n <span className=\"es-field-icon\" aria-hidden=\"true\">\n {icon}\n </span>\n ) : null}\n <span className=\"es-field-label\" id={labelId}>\n {label}\n </span>\n </>\n );\n\n return (\n <div className={classes.join(' ')}>\n <div className=\"es-field-head\">\n {htmlFor ? (\n <label className=\"es-field-name\" htmlFor={htmlFor}>\n {labelBody}\n </label>\n ) : (\n <span className=\"es-field-name\">{labelBody}</span>\n )}\n {value !== undefined && value !== null ? (\n <span className=\"es-field-value\">{value}</span>\n ) : null}\n {hasAction ? nameSwitch(action, label, labelId) : null}\n </div>\n {hasControl ? <div className=\"es-field-ctl\">{children}</div> : null}\n {hint ? <p className=\"es-field-hint\">{hint}</p> : null}\n </div>\n );\n}\n"]}
1
+ {"version":3,"sources":["../../src/panel/SettingsTabs.tsx","../../src/panel/SettingsGroup.tsx","../../src/panel/SettingsGroupHead.tsx","../../src/panel/SettingsSwitch.tsx","../../src/panel/SettingsField.tsx","../../src/panel/SettingsDrill.tsx"],"names":["jsxs","jsx"],"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 { SettingsGroupProps } from './types';\n\n/**\n * A group of settings under a soft pill row that folds with − / +.\n *\n * The sign is drawn by `panel.css` from `aria-expanded`, so the open state has\n * exactly one home (the attribute assistive tech already reads) instead of a\n * second copy in the markup.\n *\n * `count` is the reason a fold here is safe: a folded group that holds changes\n * still says so on its pill, so nothing a merchant changed can hide.\n */\nexport function SettingsGroup({\n title,\n open,\n onToggle,\n count,\n id,\n children,\n className,\n}: SettingsGroupProps) {\n const bodyId = id ? `${id}-body` : undefined;\n return (\n <div className={className ? `es-group ${className}` : 'es-group'}>\n <button\n type=\"button\"\n id={id}\n aria-expanded={open}\n aria-controls={bodyId}\n className=\"es-group-head\"\n onClick={() => onToggle(!open)}\n >\n {title}\n {typeof count === 'number' && count > 0 ? (\n <span className=\"es-group-count\">{count}</span>\n ) : null}\n <span className=\"es-group-sign\" aria-hidden=\"true\" />\n </button>\n <div id={bodyId} className=\"es-group-body\" hidden={!open}>\n {children}\n </div>\n </div>\n );\n}\n","import type { SettingsGroupHeadProps } from './types';\n\n/**\n * A group heading with NO group under it — the pill on its own.\n *\n * It exists because Puck lays the inspector out as a FLAT sibling list: a field\n * renders next to the heading above it, never inside it, so nothing here can\n * wrap the rows that follow. Heading a run of settings is therefore a job for a\n * component that heads and nothing else.\n *\n * `SettingsGroup` cannot do that job. It always renders its body div, and its\n * pill is always a <button> carrying `aria-expanded`, `aria-controls` and the\n * − / + sign. Used header-only it would draw a control that claims to expand\n * something, does nothing when clicked, and tells a screen reader the same lie.\n * A component that folds and a component that titles are two different things,\n * and this is the one that titles.\n *\n * So it is a HEADING, not a control: a real <h3> (the level is the host's, since\n * only the host knows the page outline around it) with no role bolted on, no\n * expanded state, no sign, no body. In a flat list the heading list is the only\n * structure a screen reader has left to navigate by, which is the whole reason\n * this renders a heading element rather than a styled <div>.\n *\n * It wears `.es-group-head` — the same pill as a real group's — so a section\n * that folds and a section that does not read as the same panel. `panel.css`\n * scopes the cursor and the hover tint to `button.es-group-head`, so the\n * heading does not offer a press it cannot honour.\n *\n * NO `count`. A folded group needs one because a fold can hide a change; a\n * heading folds nothing, so nothing can hide behind it.\n */\nexport function SettingsGroupHead({\n title,\n level = 3,\n id,\n className,\n}: SettingsGroupHeadProps) {\n const Tag = `h${level}` as const;\n return (\n <Tag id={id} className={className ? `es-group-head ${className}` : 'es-group-head'}>\n {title}\n </Tag>\n );\n}\n","import type { SettingsSwitchProps } from './types';\n\n/**\n * The yes-or-no control — ONE shape, both editors (card 8801).\n *\n * Point 5 of the approved settings design says one switch style everywhere.\n * Before this, the storefront editor drew a tick box and the campaign designer\n * drew a green switch, so a merchant met two shapes for the same question and\n * had to learn the control twice. Both now import this file, which is the only\n * thing that stops them drifting apart again.\n *\n * It is a real <button>, and that is the whole keyboard story: a button is\n * activated by Space and by Enter in every browser, and that activation IS a\n * click — so the one `onClick` below serves mouse and keyboard alike. A div\n * wearing `role=\"switch\"` would need a key handler of our own. Do not add one.\n * `type=\"button\"` keeps it from submitting a form it happens to sit in.\n *\n * It cannot exist without a NAME. `SettingsSwitchProps` demands `ariaLabel` or\n * `ariaLabelledBy` and forbids both at once, so an unnamed switch — which a\n * screen reader reads out as \"switch, on\" and nothing more — is a compile error\n * instead of something a docstring asks you to remember.\n *\n * ONE-WAY DEPENDENCY. `SettingsField` imports this module so it can recognise a\n * switch in its `action` slot and hand it the field's label as a name. This\n * module must NEVER import `SettingsField` back — that would close a cycle\n * between two siblings in the same folder. It imports `./types` and nothing\n * else, and `tests/settings-switch.test.tsx` (o) keeps it that way.\n *\n * CONTROLLED and hook-free like the other three: `checked` comes from the host\n * and the switch only ever ASKS to be flipped. That is what keeps this module a\n * directive-free leaf a Next-16 server component can import.\n */\nexport function SettingsSwitch({\n checked,\n onChange,\n disabled,\n id,\n ariaLabel,\n ariaLabelledBy,\n className,\n}: SettingsSwitchProps) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n id={id}\n aria-checked={checked}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n disabled={disabled}\n className={className ? `es-switch ${className}` : 'es-switch'}\n onClick={() => onChange(!checked)}\n />\n );\n}\n","import { cloneElement, isValidElement } from 'react';\nimport type { ReactElement, ReactNode } from 'react';\n\nimport { SettingsSwitch } from './SettingsSwitch';\nimport type { SettingsFieldProps } from './types';\n\n/**\n * Does this slot hold a control?\n *\n * The rule is ELEMENT-OR-NOTHING, deliberately not a list of falsy values: a\n * list rots, and the next odd value nobody thought of walks straight through.\n * `{flag && <Slider/>}` yields `false`, `{count && <Slider/>}` yields `0` when\n * the count is zero — and React RENDERS that zero, a stray 0 sitting in the\n * panel where a control belongs. A bare string or number in a control slot is a\n * caller mistake in every case any of us can name.\n *\n * Arrays are walked rather than trusted: `Array.isArray([])` is true, so an\n * empty array — or one holding only `false` and `null`, which is what a list of\n * conditional controls collapses to — would otherwise draw the empty row this\n * check exists to prevent.\n */\nfunction hasControlIn(slot: ReactNode): boolean {\n if (Array.isArray(slot)) return slot.some(hasControlIn);\n return isValidElement(slot);\n}\n\n/** What a switch is named by. Read off an unknown element, so it stays loose. */\ninterface SwitchName {\n ariaLabel?: string;\n ariaLabelledBy?: string;\n}\n\n/**\n * Give a switch in the `action` slot the field's own label as its name.\n *\n * A switch carries no text, and `htmlFor` cannot bridge a <label> to a\n * <button>, so in a boolean row nothing connects the visible label to the\n * control unless something does it explicitly. Doing it HERE means the common\n * case is right with the caller saying nothing: the accessible name and the\n * visible name are the same string and cannot drift apart.\n *\n * It only ever fills a gap. A switch that names itself keeps its own name, and\n * anything that is not a switch — a reset dot, a badge — is left alone, since\n * labelling those with the field's name would be worse than not labelling them.\n */\nfunction nameSwitch(action: ReactNode, label: string, labelId?: string): ReactNode {\n if (!isValidElement(action) || action.type !== SettingsSwitch) return action;\n\n const named = action.props as SwitchName;\n if (named.ariaLabel || named.ariaLabelledBy) return action;\n\n return cloneElement(\n action as ReactElement<SwitchName>,\n labelId ? { ariaLabelledBy: labelId } : { ariaLabel: label },\n );\n}\n\n/**\n * One setting: a label row, then its control.\n *\n * The row is stacked rather than side-by-side so the control gets the panel's\n * full width — a 308px panel cannot afford a label column AND a usable slider.\n * The label reads small and quiet; the VALUE sits at the end of the same line in\n * the text colour, which is what a merchant scans for. That contrast is the\n * point: before this, label and value looked alike and the panel read as noise.\n *\n * `hint` is where a clause-long explanation goes. A label is a name, never a\n * sentence.\n *\n * A BOOLEAN row is the one exception to \"control on its own line\" (card 8801,\n * Option A, approved). A switch is small and it is the whole control, so it\n * rides the label line in the `action` slot and the row costs one line instead\n * of two — which is what lets a 308px panel show four settings where it used to\n * show three. That is the only reason `children` is optional: a field with no\n * children draws no control row, and its label line IS the row.\n *\n * A field with no children AND no action has no control on either slot. That is\n * a mistake at the call site, so it renders NOTHING — an empty row would hide\n * the mistake behind 20px of blank panel.\n */\nexport function SettingsField({\n label,\n labelId,\n icon,\n value,\n hint,\n changed,\n htmlFor,\n action,\n children,\n className,\n}: SettingsFieldProps) {\n const hasControl = hasControlIn(children);\n const hasAction = hasControlIn(action);\n if (!hasControl && !hasAction) return null;\n\n const classes = ['es-field'];\n if (!hasControl) classes.push('is-inline');\n if (changed) classes.push('is-changed');\n if (className) classes.push(className);\n\n const labelBody = (\n <>\n {icon ? (\n <span className=\"es-field-icon\" aria-hidden=\"true\">\n {icon}\n </span>\n ) : null}\n <span className=\"es-field-label\" id={labelId}>\n {label}\n </span>\n </>\n );\n\n return (\n <div className={classes.join(' ')}>\n <div className=\"es-field-head\">\n {htmlFor ? (\n <label className=\"es-field-name\" htmlFor={htmlFor}>\n {labelBody}\n </label>\n ) : (\n <span className=\"es-field-name\">{labelBody}</span>\n )}\n {value !== undefined && value !== null ? (\n <span className=\"es-field-value\">{value}</span>\n ) : null}\n {hasAction ? nameSwitch(action, label, labelId) : null}\n </div>\n {hasControl ? <div className=\"es-field-ctl\">{children}</div> : null}\n {hint ? <p className=\"es-field-hint\">{hint}</p> : null}\n </div>\n );\n}\n","import type { SettingsDrillProps } from './types';\n\n/**\n * A DRILL-IN ROW — a name, a one-line preview, and a chevron (card 8819).\n *\n * The storefront editor's text editing becomes a panel drill-down: the section\n * screen no longer holds a text box for every part; it LISTS the parts as these\n * rows, and pressing one takes the inspector down a level to that part's own\n * editor. A part the eye hid — or one that never draws on the page at all, like\n * a `Slide name` — has no words on the canvas to click, so this list is the only\n * way back to it. That is why the list exists, and why the row is the shell's,\n * not the shop's: the campaign designer draws the same list.\n *\n * IT IS ONE BUTTON, not a control beside some text. The name and the preview are\n * the button's own content, so they ARE its accessible name — a chevron floating\n * next to unlabelled text would be a press a screen reader could not describe.\n * The chevron is `aria-hidden` (pure direction); the status slot is NOT, because\n * its meaning is the host's to name — it passes an icon carrying its own label\n * (a `title`, a visually-hidden word), and hiding the slot would bury that.\n *\n * CONTROLLED and hook-free like the other four primitives: it owns no open state.\n * Pressing it calls `onOpen`, and the host decides what drilling down means —\n * which keeps this a directive-free leaf a Next-16 server component can import.\n */\nexport function SettingsDrill({\n name,\n preview,\n offPage,\n status,\n onOpen,\n id,\n className,\n}: SettingsDrillProps) {\n const hasPreview = preview !== undefined && preview !== null;\n return (\n <button\n type=\"button\"\n id={id}\n className={className ? `es-drill ${className}` : 'es-drill'}\n onClick={onOpen}\n >\n <span className=\"es-drill-text\">\n <span className=\"es-drill-name\">{name}</span>\n {hasPreview ? (\n <span className={offPage ? 'es-drill-preview is-offpage' : 'es-drill-preview'}>\n {preview}\n </span>\n ) : null}\n </span>\n {status ? <span className=\"es-drill-status\">{status}</span> : null}\n <span className=\"es-drill-chev\" aria-hidden=\"true\" />\n </button>\n );\n}\n"]}
@@ -454,11 +454,109 @@ button.es-group-head:hover { background: var(--es-chip-bg); }
454
454
  .es-switch:disabled[aria-checked='true'] { background: var(--es-input-border); }
455
455
  .es-switch:disabled::after { box-shadow: none; border: 1px solid var(--es-divider); }
456
456
 
457
+ /* ── a drill-in row: name, preview, chevron (card 8819) ────────────────────── */
458
+
459
+ /* The row a section lists a text part as. Pressing it drills the inspector down
460
+ a level to that part's own editor.
461
+
462
+ A FULL-WIDTH LIST ROW, not the raised pill a group's head is. These stack into
463
+ a list at the bottom of the section panel, and a list reads as a list: flat
464
+ rows with a hairline between them, a quiet hover tint, no chip elevation. The
465
+ separator is drawn BETWEEN rows (`.es-drill + .es-drill`) rather than as a
466
+ bottom border on each, so the last row needs no `:last-child` exception and a
467
+ lone row outside a list carries no dangling rule.
468
+
469
+ `box-sizing` for the same reason the switch and the group head set it: this
470
+ file assumes no reset of the host's, and `width: 100%` plus the padding would
471
+ otherwise run the row past the panel. */
472
+ .es-drill {
473
+ box-sizing: border-box;
474
+ width: 100%;
475
+ display: flex;
476
+ align-items: center;
477
+ gap: 10px;
478
+ border: 0;
479
+ text-align: left;
480
+ font: inherit;
481
+ background: transparent;
482
+ padding: 10px 11px;
483
+ cursor: pointer;
484
+ transition: background 0.12s;
485
+ }
486
+
487
+ .es-drill + .es-drill { border-top: 1px solid var(--es-line); }
488
+
489
+ .es-drill:hover { background: var(--es-hover); }
490
+
491
+ /* An INSET ring, unlike the switch's and the tab's outset one: a full-width row
492
+ has neighbours hard against its edges, and an outset ring would be clipped by
493
+ the row above and below it. */
494
+ .es-drill:focus-visible {
495
+ outline: none;
496
+ box-shadow: inset 0 0 0 2px var(--es-accent-ring);
497
+ }
498
+
499
+ /* The two lines, stacked, and allowed to shrink so the preview can ellipsise
500
+ instead of shoving the chevron off the row. */
501
+ .es-drill-text {
502
+ display: flex;
503
+ flex-direction: column;
504
+ gap: 2px;
505
+ min-width: 0;
506
+ flex: 1;
507
+ }
508
+
509
+ .es-drill-name {
510
+ font-size: 12px;
511
+ font-weight: 600;
512
+ color: var(--es-text-strong);
513
+ }
514
+
515
+ /* The preview is one line, clipped. It never wraps: a part's copy can be a
516
+ paragraph, and a row that grows to fit it stops being a row. */
517
+ .es-drill-preview {
518
+ font-size: 11px;
519
+ color: var(--es-muted);
520
+ white-space: nowrap;
521
+ overflow: hidden;
522
+ text-overflow: ellipsis;
523
+ }
524
+
525
+ /* A part with no place on the page — an identifier the eye never draws. Quiet
526
+ and italic, so it reads as a note about the part rather than as its copy. */
527
+ .es-drill-preview.is-offpage {
528
+ font-style: italic;
529
+ color: var(--es-disabled);
530
+ }
531
+
532
+ /* The status glyph (the "hidden on the page" eye). Not `aria-hidden` here — the
533
+ host owns its meaning and names it; this only places it. */
534
+ .es-drill-status {
535
+ flex: none;
536
+ display: inline-flex;
537
+ align-items: center;
538
+ color: var(--es-muted);
539
+ }
540
+
541
+ /* The chevron is drawn from CSS `content`, the way the group sign is: it is pure
542
+ direction, carries no text of its own, and stays out of the accessible name. */
543
+ .es-drill-chev {
544
+ flex: none;
545
+ width: 16px;
546
+ text-align: center;
547
+ color: var(--es-muted);
548
+ font-size: 16px;
549
+ line-height: 1;
550
+ }
551
+
552
+ .es-drill-chev::before { content: '\203A'; }
553
+
457
554
  @media (prefers-reduced-motion: reduce) {
458
555
  .es-tab,
459
556
  /* `button.` and not `.es-group-head`: the transition it cancels is declared
460
557
  on `button.es-group-head`, and a plainer selector would lose to it. */
461
558
  button.es-group-head,
559
+ .es-drill,
462
560
  .es-switch,
463
561
  .es-switch::after { transition: none; }
464
562
  }
@@ -33,6 +33,23 @@ interface EditorRailItem {
33
33
  /** Click handler. Skipped while `disabled`. */
34
34
  onSelect?: () => void;
35
35
  }
36
+ /**
37
+ * Which way the rail runs.
38
+ *
39
+ * `'column'` — the original, and the default: a slim fixed-width strip down the
40
+ * side of the editor, wearing its own card (a background, a 12px radius and a
41
+ * drop shadow).
42
+ *
43
+ * `'row'` — the same buttons laid left to right, with NO fixed width and NO
44
+ * card, so the rail can sit in the middle of a top bar that already has its own
45
+ * surface. It exists because the column costs a 1280px laptop 56px of preview
46
+ * (46px of rail plus the 10px gap) that the top bar has going spare.
47
+ *
48
+ * Only the CONTAINER changes. Button size, icon size, the gap, and the hover
49
+ * and active look are the same in both directions — the row re-points three
50
+ * existing `--es-rail-*` custom properties and touches nothing else.
51
+ */
52
+ type EditorRailOrientation = 'column' | 'row';
36
53
  interface EditorRailProps {
37
54
  /** The buttons, top to bottom, in render order (place the "+" where you want it). */
38
55
  items: EditorRailItem[];
@@ -40,6 +57,12 @@ interface EditorRailProps {
40
57
  ariaLabel?: string;
41
58
  /** Extra class(es) merged onto the rail container. */
42
59
  className?: string;
60
+ /**
61
+ * Which way the buttons run. Defaults to `'column'`, which renders exactly
62
+ * what every editor draws today — adding this prop changed no existing
63
+ * output. Pass `'row'` to lay the rail into a top bar.
64
+ */
65
+ orientation?: EditorRailOrientation;
43
66
  }
44
67
  interface EditorRailButtonProps {
45
68
  label: string;
@@ -51,11 +74,12 @@ interface EditorRailButtonProps {
51
74
  }
52
75
 
53
76
  /**
54
- * The shared left icon-rail — one slim vertical strip of icon buttons that every
55
- * EFFICIENT editor uses to switch panels (Pages / Layers / Styles / Media /
56
- * Sitemap) and to quick-add ("+"). It is a **pure presentational switcher**: give
57
- * it `items[]`, it draws them; which one is `active` and what each does is the
58
- * host's business.
77
+ * The shared icon-rail — one slim strip of icon buttons that every EFFICIENT
78
+ * editor uses to switch panels (Pages / Layers / Styles / Media / Sitemap) and
79
+ * to quick-add ("+"). By default it is the vertical strip down the left; pass
80
+ * `orientation="row"` and the same buttons lay into a top bar instead. It is a
81
+ * **pure presentational switcher**: give it `items[]`, it draws them; which one
82
+ * is `active` and what each does is the host's business.
59
83
  *
60
84
  * No `'use client'` here on purpose — the rail has no hooks or state, so it is a
61
85
  * plain module a Next-16 server component can import and even statically render.
@@ -64,11 +88,21 @@ interface EditorRailButtonProps {
64
88
  * components), so baking a directive into this leaf would only narrow where it
65
89
  * can be imported.
66
90
  *
67
- * A vertical `toolbar` (not a `tablist`), because the rail mixes view *toggles*
68
- * with plain *actions* like the quick-add — a toolbar of buttons models that
91
+ * A `toolbar` (not a `tablist`), because the rail mixes view *toggles* with
92
+ * plain *actions* like the quick-add — a toolbar of buttons models that
69
93
  * honestly, where a tablist would force every button to pretend to be a tab.
94
+ * That is true in both directions, so `orientation` moves `aria-orientation`
95
+ * and leaves the role alone: what the rail IS did not change, only which way it
96
+ * runs — and a toolbar's arrow keys follow its orientation, so a row that kept
97
+ * saying `vertical` would describe the wrong keys.
98
+ *
99
+ * `orientation` defaults to `'column'`, and that path is byte-identical to what
100
+ * this component rendered before the prop existed: same class list, same
101
+ * attributes, same order. `'row'` adds ONE modifier class beside `es-rail` —
102
+ * never instead of it, since every button rule and token in rail.css hangs off
103
+ * the base class — and rail.css does the rest.
70
104
  */
71
- declare function EditorRail({ items, ariaLabel, className }: EditorRailProps): react.JSX.Element;
105
+ declare function EditorRail({ items, ariaLabel, className, orientation, }: EditorRailProps): react.JSX.Element;
72
106
 
73
107
  /**
74
108
  * A single rail button: an icon in a 34×34 square that hovers, highlights when
@@ -157,4 +191,4 @@ declare const railCssVars: {
157
191
  readonly accentTint: "--es-rail-accent-tint";
158
192
  };
159
193
 
160
- export { EditorRail, EditorRailButton, type EditorRailButtonProps, type EditorRailItem, type EditorRailProps, type RailTokens, railCssVars, railTokens };
194
+ export { EditorRail, EditorRailButton, type EditorRailButtonProps, type EditorRailItem, type EditorRailOrientation, type EditorRailProps, type RailTokens, railCssVars, railTokens };
@@ -1,4 +1,4 @@
1
- export { EditorRail, EditorRailButton, railCssVars, railTokens } from '../chunk-3VMFPQGZ.js';
1
+ export { EditorRail, EditorRailButton, railCssVars, railTokens } from '../chunk-HYOACICE.js';
2
2
  export { AddIcon, LayersIcon, MediaIcon, PageIcon as PagesIcon, SitemapIcon, StylesIcon } from '../chunk-M7B5VTWH.js';
3
3
  //# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map
@@ -40,6 +40,45 @@
40
40
  box-shadow: var(--es-rail-shadow);
41
41
  }
42
42
 
43
+ /*
44
+ * The ROW variant (card 66160) — the same rail laid into a top bar.
45
+ *
46
+ * The column costs a 1280px laptop 56px of website preview (a 46px rail plus
47
+ * the 10px panel gap) that the top bar has going spare, so the icons move up
48
+ * there. `.es-rail` above is UNTOUCHED: this rule only overrides, so a consumer
49
+ * who never passes `orientation` gets exactly the stylesheet they had.
50
+ *
51
+ * THE CARD LOOK GOES, THROUGH THE TOKENS THAT ALREADY EXIST. The bar has its
52
+ * own surface and shadow; a second white card floating inside it is the bug.
53
+ * These re-point the THREE custom properties the base rule already reads rather
54
+ * than re-stating `background` / `box-shadow` / `border-radius` here — so the
55
+ * card look stays declared in exactly one place, and a host re-theming the rail
56
+ * still only has the `--es-rail-*` names to learn.
57
+ *
58
+ * WHAT DELIBERATELY DOES NOT CHANGE: the button size, the button radius, the
59
+ * icon size, the gap and every colour. The icons must not resize when they move
60
+ * into the bar, so no `--es-rail-btn-*`, `--es-rail-icon-size` or colour token
61
+ * appears below, and there is no `.es-rail-row .es-rail-btn` rule anywhere.
62
+ * `align-items: center` is inherited from the base rule and does the right
63
+ * thing here too — in a row it centres the buttons vertically.
64
+ */
65
+ .es-rail.es-rail-row {
66
+ --es-rail-bg: transparent;
67
+ --es-rail-shadow: none;
68
+ --es-rail-radius: 0;
69
+
70
+ flex-direction: row;
71
+ /* No fixed width: the row is as wide as its buttons. `flex: none` from the
72
+ base rule keeps flex-basis on `auto`, so this sizes to content. */
73
+ width: auto;
74
+ /* The column pads `--es-rail-pad-y 0`: the one step goes on the MAIN axis —
75
+ the run of buttons — and the cross axis is handled by the fixed width.
76
+ Rotate the axes and the same step goes on the ENDS of the row. The cross
77
+ axis is 0 on purpose, so the row is exactly one button tall (34px) and the
78
+ 52px bar centres it, instead of the rail imposing a height on the bar. */
79
+ padding: 0 var(--es-rail-pad-y);
80
+ }
81
+
43
82
  .es-rail-btn {
44
83
  width: var(--es-rail-btn-size);
45
84
  height: var(--es-rail-btn-size);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "editor-shell",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Shared editor-chrome primitives for the EFFICIENT editors: EditorRail (a Next-16-safe left icon-rail leaf), the shell token layer, and GoldTextInput — type in place and watch the markup formatting appear as you type.",
5
5
  "license": "MIT",
6
6
  "author": "Lewis Liu",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/rail/EditorRailButton.tsx","../src/rail/EditorRail.tsx","../src/rail/tokens.ts"],"names":["jsx"],"mappings":"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","file":"chunk-3VMFPQGZ.js","sourcesContent":["import type { EditorRailButtonProps } from './types';\n\n/**\n * A single rail button: an icon in a 34×34 square that hovers, highlights when\n * active, and dims when disabled. Look only — the caller owns the click.\n *\n * `aria-pressed` is emitted **only** when `active` is a real boolean, so a view\n * toggle announces its pressed state while an action button (the quick-add \"+\",\n * whose item omits `active`) stays a plain button rather than a stuck toggle.\n */\nexport function EditorRailButton({\n label,\n icon,\n active,\n disabled,\n onSelect,\n className,\n}: EditorRailButtonProps) {\n const classes = ['es-rail-btn', active ? 'is-active' : '', className ?? '']\n .filter(Boolean)\n .join(' ');\n\n return (\n <button\n type=\"button\"\n className={classes}\n title={label}\n aria-label={label}\n aria-pressed={typeof active === 'boolean' ? active : undefined}\n disabled={disabled}\n onClick={disabled ? undefined : onSelect}\n >\n {icon}\n </button>\n );\n}\n","import { EditorRailButton } from './EditorRailButton';\nimport type { EditorRailProps } from './types';\n\n/**\n * The shared left icon-rail — one slim vertical strip of icon buttons that every\n * EFFICIENT editor uses to switch panels (Pages / Layers / Styles / Media /\n * Sitemap) and to quick-add (\"+\"). It is a **pure presentational switcher**: give\n * it `items[]`, it draws them; which one is `active` and what each does is the\n * host's business.\n *\n * No `'use client'` here on purpose — the rail has no hooks or state, so it is a\n * plain module a Next-16 server component can import and even statically render.\n * It's interactive only through the `onSelect` props the host passes, and the\n * host owns its client boundary (both editors' chrome are already client\n * components), so baking a directive into this leaf would only narrow where it\n * can be imported.\n *\n * A vertical `toolbar` (not a `tablist`), because the rail mixes view *toggles*\n * with plain *actions* like the quick-add — a toolbar of buttons models that\n * honestly, where a tablist would force every button to pretend to be a tab.\n */\nexport function EditorRail({ items, ariaLabel = 'Editor', className }: EditorRailProps) {\n return (\n <div\n className={['es-rail', className ?? ''].filter(Boolean).join(' ')}\n role=\"toolbar\"\n aria-orientation=\"vertical\"\n aria-label={ariaLabel}\n >\n {items.map((item) => (\n <EditorRailButton\n key={item.id}\n label={item.label}\n icon={item.icon}\n active={item.active}\n disabled={item.disabled}\n onSelect={item.onSelect}\n />\n ))}\n </div>\n );\n}\n","/**\n * Rail design tokens — the ONE source both apps' rails converge on.\n *\n * Every value below is taken verbatim from the storefront editor's rail\n * (`efficient-shop` → `storefront-editor.css`, the `.sf-rail` / `.sf-rail-btn`\n * rules), which is the visual reference. The admin portal's hand-rolled rail had\n * drifted (button 36 vs 34, icon 20 vs 18, radius 8 vs 9, a Tailwind accent\n * instead of a CSS var); these tokens end that drift.\n *\n * Exposed BOTH ways, per the same value:\n * - `railTokens` — this JS object, for anything that needs the numbers/colours\n * in code (inline styles, a theme bridge, a Storybook control);\n * - `rail.css` — the stylesheet, which declares the identical values as CSS\n * custom properties on `.es-rail` (names listed in `railCssVars`).\n *\n * Numeric fields are pixel magnitudes (unitless) so they compose in code; the\n * CSS applies the `px`.\n */\nexport const railTokens = {\n /** Rail column width. `--sf-rail-w: 46px`. */\n railWidth: 46,\n /** Vertical padding inside the rail (top === bottom). */\n railPaddingY: 8,\n /** Gap between buttons. */\n railGap: 4,\n /** Rail card corner radius. `--sf-panel-radius: 12px`. */\n railRadius: 12,\n /** Button hit-area (square). */\n buttonSize: 34,\n /** Button corner radius. */\n buttonRadius: 9,\n /** Icon glyph size. */\n iconSize: 18,\n /** Hover/active colour transition. */\n transition: 'background 0.12s, color 0.12s',\n /** Disabled-button opacity. */\n disabledOpacity: 0.35,\n color: {\n /** Rail card background. `--sf-panel`. */\n background: '#ffffff',\n /** Rail card shadow. `--sf-panel-shadow`. */\n shadow: '0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.06)',\n /** Idle icon colour. `--sf-muted` (gray-500). */\n idle: '#6b7280',\n /** Hover background. `--sf-hover` (gray-100). */\n hoverBg: '#f3f4f6',\n /** Hover icon colour. `--sf-text` (gray-900). */\n hoverFg: '#111827',\n /** Active icon colour. `--sf-accent` (blue-600). */\n accent: '#2563eb',\n /** Active background. `--sf-accent-tint` (blue-50). */\n accentTint: '#eff6ff',\n },\n} as const;\n\nexport type RailTokens = typeof railTokens;\n\n/**\n * The CSS custom-property name behind each token. `rail.css` sets these on\n * `.es-rail`; override any of them on (or above) the rail element to re-theme it\n * — e.g. `style={{ ['--es-rail-accent']: brand }}` — without shipping new CSS.\n */\nexport const railCssVars = {\n railWidth: '--es-rail-w',\n railPaddingY: '--es-rail-pad-y',\n railGap: '--es-rail-gap',\n railRadius: '--es-rail-radius',\n buttonSize: '--es-rail-btn-size',\n buttonRadius: '--es-rail-btn-radius',\n iconSize: '--es-rail-icon-size',\n background: '--es-rail-bg',\n shadow: '--es-rail-shadow',\n idle: '--es-rail-fg',\n hoverBg: '--es-rail-hover-bg',\n hoverFg: '--es-rail-hover-fg',\n accent: '--es-rail-accent',\n accentTint: '--es-rail-accent-tint',\n} as const;\n"]}