editor-shell 0.5.0 → 0.6.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.
@@ -41,6 +41,16 @@ interface SettingsGroupProps {
41
41
  }
42
42
  interface SettingsFieldProps {
43
43
  label: string;
44
+ /**
45
+ * An `id` for the label text, so a control that cannot be reached by
46
+ * `htmlFor` can still be named by it.
47
+ *
48
+ * That is the boolean row: `htmlFor` associates a <label> with a form
49
+ * control, and a switch is a <button>. Give the label an id here and hand the
50
+ * same id to the switch's `ariaLabelledBy`, and the accessible name and the
51
+ * visible name are the same string.
52
+ */
53
+ labelId?: string;
44
54
  /** An 18px glyph from `editor-shell/icons` — one meaning, one SVG. */
45
55
  icon?: ReactNode;
46
56
  /** The current value, right-aligned on the label row ("34 px", "style"). */
@@ -51,12 +61,55 @@ interface SettingsFieldProps {
51
61
  changed?: boolean;
52
62
  /** Set when the control is a single form element, so the label points at it. */
53
63
  htmlFor?: string;
54
- /** A trailing control on the label row — a reset dot, a badge. */
64
+ /**
65
+ * A trailing control on the label row — a reset dot, a badge, or the SWITCH
66
+ * of a boolean row (card 8801, Option A: a yes-or-no setting is one line, not
67
+ * two, so the switch takes the spot a value like "34 px" normally sits in).
68
+ */
55
69
  action?: ReactNode;
56
- /** The control itself. */
57
- children: ReactNode;
70
+ /**
71
+ * The control itself, on its own line under the label.
72
+ *
73
+ * OPTIONAL since card 8801, and only so a boolean row can put its switch in
74
+ * `action` and render no control line at all. Omitting it is not a way to
75
+ * draw a label on its own: a field with NO children and NO action has no
76
+ * control on either slot, so it renders nothing rather than leaving an empty
77
+ * row behind. Every field that passes a control is unaffected.
78
+ */
79
+ children?: ReactNode;
80
+ className?: string;
81
+ }
82
+ /**
83
+ * A switch has no text of its own, so without one of these it announces
84
+ * "switch, on" and nothing else. The union makes an unnamed switch a COMPILE
85
+ * error rather than a documented risk — both editors are about to grow six of
86
+ * these per panel, and every one is a chance to forget.
87
+ *
88
+ * Prefer `ariaLabelledBy` inside a `SettingsField`: point it at the field's
89
+ * `labelId` and the accessible name IS the visible label, one string doing two
90
+ * jobs, so the two cannot drift apart. `ariaLabel` is for a switch standing on
91
+ * its own with no visible label to point at.
92
+ *
93
+ * `?: never` on the other arm is what makes the two mutually exclusive while
94
+ * still letting the component destructure both names.
95
+ */
96
+ type SettingsSwitchLabel = {
97
+ ariaLabel: string;
98
+ ariaLabelledBy?: never;
99
+ } | {
100
+ ariaLabelledBy: string;
101
+ ariaLabel?: never;
102
+ };
103
+ interface SettingsSwitchBaseProps {
104
+ /** Whether the setting is on. The HOST owns it; the switch never flips itself. */
105
+ checked: boolean;
106
+ /** Asked to flip. `next` is the value the merchant wants, not the current one. */
107
+ onChange: (next: boolean) => void;
108
+ disabled?: boolean;
109
+ id?: string;
58
110
  className?: string;
59
111
  }
112
+ type SettingsSwitchProps = SettingsSwitchBaseProps & SettingsSwitchLabel;
60
113
 
61
114
  /**
62
115
  * The tab strip under the panel head.
@@ -93,7 +146,50 @@ declare function SettingsGroup({ title, open, onToggle, count, id, children, cla
93
146
  *
94
147
  * `hint` is where a clause-long explanation goes. A label is a name, never a
95
148
  * sentence.
149
+ *
150
+ * A BOOLEAN row is the one exception to "control on its own line" (card 8801,
151
+ * Option A, approved). A switch is small and it is the whole control, so it
152
+ * rides the label line in the `action` slot and the row costs one line instead
153
+ * of two — which is what lets a 308px panel show four settings where it used to
154
+ * show three. That is the only reason `children` is optional: a field with no
155
+ * children draws no control row, and its label line IS the row.
156
+ *
157
+ * A field with no children AND no action has no control on either slot. That is
158
+ * a mistake at the call site, so it renders NOTHING — an empty row would hide
159
+ * the mistake behind 20px of blank panel.
160
+ */
161
+ declare function SettingsField({ label, labelId, icon, value, hint, changed, htmlFor, action, children, className, }: SettingsFieldProps): react.JSX.Element | null;
162
+
163
+ /**
164
+ * The yes-or-no control — ONE shape, both editors (card 8801).
165
+ *
166
+ * Point 5 of the approved settings design says one switch style everywhere.
167
+ * Before this, the storefront editor drew a tick box and the campaign designer
168
+ * drew a green switch, so a merchant met two shapes for the same question and
169
+ * had to learn the control twice. Both now import this file, which is the only
170
+ * thing that stops them drifting apart again.
171
+ *
172
+ * It is a real <button>, and that is the whole keyboard story: a button is
173
+ * activated by Space and by Enter in every browser, and that activation IS a
174
+ * click — so the one `onClick` below serves mouse and keyboard alike. A div
175
+ * wearing `role="switch"` would need a key handler of our own. Do not add one.
176
+ * `type="button"` keeps it from submitting a form it happens to sit in.
177
+ *
178
+ * It cannot exist without a NAME. `SettingsSwitchProps` demands `ariaLabel` or
179
+ * `ariaLabelledBy` and forbids both at once, so an unnamed switch — which a
180
+ * screen reader reads out as "switch, on" and nothing more — is a compile error
181
+ * instead of something a docstring asks you to remember.
182
+ *
183
+ * ONE-WAY DEPENDENCY. `SettingsField` imports this module so it can recognise a
184
+ * switch in its `action` slot and hand it the field's label as a name. This
185
+ * module must NEVER import `SettingsField` back — that would close a cycle
186
+ * between two siblings in the same folder. It imports `./types` and nothing
187
+ * else, and `tests/settings-switch.test.tsx` (o) keeps it that way.
188
+ *
189
+ * CONTROLLED and hook-free like the other three: `checked` comes from the host
190
+ * and the switch only ever ASKS to be flipped. That is what keeps this module a
191
+ * directive-free leaf a Next-16 server component can import.
96
192
  */
97
- declare function SettingsField({ label, icon, value, hint, changed, htmlFor, action, children, className, }: SettingsFieldProps): react.JSX.Element;
193
+ declare function SettingsSwitch({ checked, onChange, disabled, id, ariaLabel, ariaLabelledBy, className, }: SettingsSwitchProps): react.JSX.Element;
98
194
 
99
- export { SettingsField, type SettingsFieldProps, SettingsGroup, type SettingsGroupProps, type SettingsTabItem, SettingsTabs, type SettingsTabsProps };
195
+ export { SettingsField, type SettingsFieldProps, SettingsGroup, type SettingsGroupProps, SettingsSwitch, type SettingsSwitchBaseProps, type SettingsSwitchLabel, type SettingsSwitchProps, type SettingsTabItem, SettingsTabs, type SettingsTabsProps };
@@ -1,4 +1,5 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { isValidElement, cloneElement } from 'react';
2
3
 
3
4
  // src/panel/SettingsTabs.tsx
4
5
  function SettingsTabs({
@@ -66,8 +67,46 @@ function SettingsGroup({
66
67
  /* @__PURE__ */ jsx("div", { id: bodyId, className: "es-group-body", hidden: !open, children })
67
68
  ] });
68
69
  }
70
+ function SettingsSwitch({
71
+ checked,
72
+ onChange,
73
+ disabled,
74
+ id,
75
+ ariaLabel,
76
+ ariaLabelledBy,
77
+ className
78
+ }) {
79
+ return /* @__PURE__ */ jsx(
80
+ "button",
81
+ {
82
+ type: "button",
83
+ role: "switch",
84
+ id,
85
+ "aria-checked": checked,
86
+ "aria-label": ariaLabel,
87
+ "aria-labelledby": ariaLabelledBy,
88
+ disabled,
89
+ className: className ? `es-switch ${className}` : "es-switch",
90
+ onClick: () => onChange(!checked)
91
+ }
92
+ );
93
+ }
94
+ function hasControlIn(slot) {
95
+ if (Array.isArray(slot)) return slot.some(hasControlIn);
96
+ return isValidElement(slot);
97
+ }
98
+ function nameSwitch(action, label, labelId) {
99
+ if (!isValidElement(action) || action.type !== SettingsSwitch) return action;
100
+ const named = action.props;
101
+ if (named.ariaLabel || named.ariaLabelledBy) return action;
102
+ return cloneElement(
103
+ action,
104
+ labelId ? { ariaLabelledBy: labelId } : { ariaLabel: label }
105
+ );
106
+ }
69
107
  function SettingsField({
70
108
  label,
109
+ labelId,
71
110
  icon,
72
111
  value,
73
112
  hint,
@@ -77,24 +116,28 @@ function SettingsField({
77
116
  children,
78
117
  className
79
118
  }) {
119
+ const hasControl = hasControlIn(children);
120
+ const hasAction = hasControlIn(action);
121
+ if (!hasControl && !hasAction) return null;
80
122
  const classes = ["es-field"];
123
+ if (!hasControl) classes.push("is-inline");
81
124
  if (changed) classes.push("is-changed");
82
125
  if (className) classes.push(className);
83
126
  const labelBody = /* @__PURE__ */ jsxs(Fragment, { children: [
84
127
  icon ? /* @__PURE__ */ jsx("span", { className: "es-field-icon", "aria-hidden": "true", children: icon }) : null,
85
- /* @__PURE__ */ jsx("span", { className: "es-field-label", children: label })
128
+ /* @__PURE__ */ jsx("span", { className: "es-field-label", id: labelId, children: label })
86
129
  ] });
87
130
  return /* @__PURE__ */ jsxs("div", { className: classes.join(" "), children: [
88
131
  /* @__PURE__ */ jsxs("div", { className: "es-field-head", children: [
89
132
  htmlFor ? /* @__PURE__ */ jsx("label", { className: "es-field-name", htmlFor, children: labelBody }) : /* @__PURE__ */ jsx("span", { className: "es-field-name", children: labelBody }),
90
133
  value !== void 0 && value !== null ? /* @__PURE__ */ jsx("span", { className: "es-field-value", children: value }) : null,
91
- action
134
+ hasAction ? nameSwitch(action, label, labelId) : null
92
135
  ] }),
93
- /* @__PURE__ */ jsx("div", { className: "es-field-ctl", children }),
136
+ hasControl ? /* @__PURE__ */ jsx("div", { className: "es-field-ctl", children }) : null,
94
137
  hint ? /* @__PURE__ */ jsx("p", { className: "es-field-hint", children: hint }) : null
95
138
  ] });
96
139
  }
97
140
 
98
- export { SettingsField, SettingsGroup, SettingsTabs };
141
+ export { SettingsField, SettingsGroup, SettingsSwitch, SettingsTabs };
99
142
  //# sourceMappingURL=index.js.map
100
143
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/panel/SettingsTabs.tsx","../../src/panel/SettingsGroup.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 */\nexport function SettingsTabs({\n items,\n activeId,\n onSelect,\n ariaLabel = 'Settings',\n className,\n}: SettingsTabsProps) {\n return (\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 return (\n <button\n key={item.id}\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n className={active ? 'es-tab is-active' : 'es-tab'}\n onClick={() => onSelect(item.id)}\n >\n {item.label}\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 );\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 { SettingsFieldProps } from './types';\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 */\nexport function SettingsField({\n label,\n icon,\n value,\n hint,\n changed,\n htmlFor,\n action,\n children,\n className,\n}: SettingsFieldProps) {\n const classes = ['es-field'];\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\">{label}</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 {action}\n </div>\n <div className=\"es-field-ctl\">{children}</div>\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/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 */\nexport function SettingsTabs({\n items,\n activeId,\n onSelect,\n ariaLabel = 'Settings',\n className,\n}: SettingsTabsProps) {\n return (\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 return (\n <button\n key={item.id}\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n className={active ? 'es-tab is-active' : 'es-tab'}\n onClick={() => onSelect(item.id)}\n >\n {item.label}\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 );\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 { 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"]}
@@ -182,7 +182,83 @@
182
182
  color: var(--es-muted);
183
183
  }
184
184
 
185
+ /* A boolean row is ONE line (card 8801, Option A). The switch rides the label
186
+ line in the `action` slot, so there is no control row under it — and with no
187
+ control row, the label line's 7px bottom margin would be a gap to nothing. */
188
+ .es-field.is-inline { margin-top: 16px; }
189
+ .es-field.is-inline .es-field-head { margin-bottom: 0; }
190
+
191
+ /* ── the switch: one yes-or-no shape, both editors ─────────────────────────── */
192
+
193
+ .es-switch {
194
+ appearance: none;
195
+ -webkit-appearance: none;
196
+ box-sizing: border-box;
197
+ margin: 0;
198
+ padding: 0;
199
+ border: 0;
200
+ flex: none;
201
+ position: relative;
202
+ width: 34px;
203
+ height: 20px;
204
+ border-radius: 999px;
205
+ background: var(--es-toggle-off);
206
+ cursor: pointer;
207
+ transition: background 0.15s ease;
208
+ }
209
+
210
+ /* The knob. `box-sizing` is load-bearing, not tidiness: the locked knob below
211
+ takes a 1px border, and under content-box that border would grow it to 18px
212
+ and eat the 2px gap the whole shape is built on. */
213
+ .es-switch::after {
214
+ content: '';
215
+ box-sizing: border-box;
216
+ position: absolute;
217
+ top: 2px;
218
+ left: 2px;
219
+ width: 16px;
220
+ height: 16px;
221
+ border-radius: 50%;
222
+ background: var(--es-knob);
223
+ /* The knob has to lift off the track, and the shadow is a plain token on
224
+ purpose. A softer mix reads slightly better, but if color-mix() ever fails
225
+ to parse the WHOLE declaration drops and the knob goes flat with no
226
+ warning — not a risk worth carrying in a leaf both editors render
227
+ thousands of times for the sake of a 1px shadow. */
228
+ box-shadow: 0 1px 2px var(--es-input-border);
229
+ transition: transform 0.15s cubic-bezier(0.2, 0.8, 0.3, 1);
230
+ }
231
+
232
+ /* On is the accent — the same blue as an active tab, so the panel keeps ONE
233
+ accent. The travel is 14px: 34 track − 16 knob − 2 gap − 2 gap. */
234
+ .es-switch[aria-checked='true'] { background: var(--es-accent); }
235
+ .es-switch[aria-checked='true']::after { transform: translateX(14px); }
236
+
237
+ .es-switch:hover:not(:disabled) { filter: brightness(0.96); }
238
+
239
+ .es-switch:focus-visible {
240
+ outline: none;
241
+ box-shadow: 0 0 0 3px var(--es-accent-ring);
242
+ }
243
+
244
+ /* Held down: the knob stretches the way a physical one would.
245
+
246
+ ON, it must stretch LEFT. The knob is 16px at translateX(14px), so growing it
247
+ to 19px in place would put its right edge at 2 + 14 + 19 = 35px inside a 34px
248
+ track and poke it past the round end. Pulling the travel back to 11px lands
249
+ the right edge on 32 and keeps the 2px gap on both sides. */
250
+ .es-switch:active:not(:disabled)::after { width: 19px; }
251
+ .es-switch[aria-checked='true']:active:not(:disabled)::after { transform: translateX(11px); }
252
+
253
+ /* Locked. Still readable as on or off, but flat and grey, and the knob trades
254
+ its shadow for a hairline so it does not look pressable. */
255
+ .es-switch:disabled { background: var(--es-divider); cursor: not-allowed; }
256
+ .es-switch:disabled[aria-checked='true'] { background: var(--es-input-border); }
257
+ .es-switch:disabled::after { box-shadow: none; border: 1px solid var(--es-divider); }
258
+
185
259
  @media (prefers-reduced-motion: reduce) {
186
260
  .es-tab,
187
- .es-group-head { transition: none; }
261
+ .es-group-head,
262
+ .es-switch,
263
+ .es-switch::after { transition: none; }
188
264
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "editor-shell",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",
@@ -22,30 +22,36 @@
22
22
  "exports": {
23
23
  ".": {
24
24
  "types": "./dist/index.d.ts",
25
- "import": "./dist/index.js"
25
+ "import": "./dist/index.js",
26
+ "default": "./dist/index.js"
26
27
  },
27
28
  "./rail": {
28
29
  "types": "./dist/rail/index.d.ts",
29
- "import": "./dist/rail/index.js"
30
+ "import": "./dist/rail/index.js",
31
+ "default": "./dist/rail/index.js"
30
32
  },
31
33
  "./icons": {
32
34
  "types": "./dist/icons/index.d.ts",
33
- "import": "./dist/icons/index.js"
35
+ "import": "./dist/icons/index.js",
36
+ "default": "./dist/icons/index.js"
34
37
  },
35
38
  "./panel": {
36
39
  "types": "./dist/panel/index.d.ts",
37
- "import": "./dist/panel/index.js"
40
+ "import": "./dist/panel/index.js",
41
+ "default": "./dist/panel/index.js"
38
42
  },
39
43
  "./panel.css": "./dist/panel/panel.css",
40
44
  "./rail.css": "./dist/rail/rail.css",
41
45
  "./shell": {
42
46
  "types": "./dist/shell/index.d.ts",
43
- "import": "./dist/shell/index.js"
47
+ "import": "./dist/shell/index.js",
48
+ "default": "./dist/shell/index.js"
44
49
  },
45
50
  "./shell.css": "./dist/shell/shell.css",
46
51
  "./gold": {
47
52
  "types": "./dist/gold/index.d.ts",
48
- "import": "./dist/gold/index.js"
53
+ "import": "./dist/gold/index.js",
54
+ "default": "./dist/gold/index.js"
49
55
  },
50
56
  "./gold.css": "./dist/gold/gold.css"
51
57
  },
@@ -81,6 +87,7 @@
81
87
  "dev": "tsup --watch",
82
88
  "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
83
89
  "test": "node scripts/test.mjs",
90
+ "verify:exports": "node scripts/verify-exports-resolve.mjs",
84
91
  "clean": "rm -rf dist"
85
92
  },
86
93
  "keywords": [