editor-shell 0.5.1 → 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.
- package/dist/panel/index.d.ts +101 -5
- package/dist/panel/index.js +47 -4
- package/dist/panel/index.js.map +1 -1
- package/dist/panel/panel.css +77 -1
- package/package.json +1 -1
package/dist/panel/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
57
|
-
|
|
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
|
|
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 };
|
package/dist/panel/index.js
CHANGED
|
@@ -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
|
package/dist/panel/index.js.map
CHANGED
|
@@ -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"]}
|
package/dist/panel/panel.css
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|