editor-shell 0.2.0 → 0.4.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.
@@ -0,0 +1,199 @@
1
+ import { useRef } from 'react';
2
+ import { tokenizeInline, STANDARD_MARKUP } from 'react-os-shell/markup';
3
+ import { jsx, jsxs } from 'react/jsx-runtime';
4
+
5
+ // src/gold/GoldTextInput.tsx
6
+ function goldSegments(value, rules) {
7
+ const out = [];
8
+ let at = 0;
9
+ for (const token of tokenizeInline(value, rules)) {
10
+ if (token.kind === "text") {
11
+ if (token.text.length > 0) {
12
+ out.push({ kind: "text", text: value.slice(at, at + token.text.length) });
13
+ at += token.text.length;
14
+ }
15
+ continue;
16
+ }
17
+ const rule = ruleAt(value, at, token.kind, token.text, rules);
18
+ if (!rule) return [{ kind: "text", text: value }];
19
+ const innerAt = at + rule.open.length;
20
+ const closeAt = innerAt + token.text.length;
21
+ const end = closeAt + rule.close.length;
22
+ out.push({ kind: "delimiter", text: value.slice(at, innerAt) });
23
+ out.push({ kind: token.kind, text: value.slice(innerAt, closeAt) });
24
+ out.push({ kind: "delimiter", text: value.slice(closeAt, end) });
25
+ at = end;
26
+ }
27
+ if (out.map((s) => s.text).join("") !== value) return [{ kind: "text", text: value }];
28
+ return out;
29
+ }
30
+ function ruleAt(value, at, kind, inner, rules) {
31
+ for (const rule of rules) {
32
+ if (rule.kind !== kind) continue;
33
+ if (!value.startsWith(rule.open, at)) continue;
34
+ const innerAt = at + rule.open.length;
35
+ if (!value.startsWith(inner, innerAt)) continue;
36
+ if (!value.startsWith(rule.close, innerAt + inner.length)) continue;
37
+ return rule;
38
+ }
39
+ return null;
40
+ }
41
+ function GoldLayer({ value, rules = STANDARD_MARKUP, className, layerRef }) {
42
+ const segments = goldSegments(value, rules);
43
+ return /* @__PURE__ */ jsx(
44
+ "div",
45
+ {
46
+ ref: layerRef,
47
+ className: ["es-gold-layer", className ?? ""].filter(Boolean).join(" "),
48
+ "aria-hidden": "true",
49
+ children: segments.map((segment, i) => paint(segment, i))
50
+ }
51
+ );
52
+ }
53
+ function paint(segment, key) {
54
+ const { kind, text } = segment;
55
+ switch (kind) {
56
+ // The delimiters STAY — dimmed, never removed. See `segments.ts`.
57
+ case "delimiter":
58
+ return /* @__PURE__ */ jsx("span", { className: "es-gold-delim", children: text }, key);
59
+ // `accent` (the legacy `*phrase*`) and `highlight` (`==phrase==`) paint the
60
+ // same, exactly as the page paints them — which is what makes converting
61
+ // stored copy from one to the other invisible here too.
62
+ case "accent":
63
+ case "highlight":
64
+ return /* @__PURE__ */ jsx("em", { className: "es-gold-mark es-gold-accent", children: text }, key);
65
+ case "bold":
66
+ return /* @__PURE__ */ jsx("strong", { className: "es-gold-mark es-gold-bold", children: text }, key);
67
+ case "italic":
68
+ return /* @__PURE__ */ jsx("em", { className: "es-gold-mark es-gold-italic", children: text }, key);
69
+ case "strike":
70
+ return /* @__PURE__ */ jsx("s", { className: "es-gold-mark es-gold-strike", children: text }, key);
71
+ // `text` — and `code`, which no product rule produces today, so a backtick
72
+ // stays ordinary copy rather than becoming a chip nobody asked for.
73
+ default:
74
+ return /* @__PURE__ */ jsx("span", { className: "es-gold-text", children: text }, key);
75
+ }
76
+ }
77
+ function GoldTextInput({
78
+ value,
79
+ onInput,
80
+ onCommit,
81
+ onCancel,
82
+ multiline = false,
83
+ placeholder,
84
+ className,
85
+ rules = STANDARD_MARKUP,
86
+ ariaLabel
87
+ }) {
88
+ const layerRef = useRef(null);
89
+ const openedWith = useRef(value);
90
+ const dirty = useRef(false);
91
+ function handleFocus() {
92
+ openedWith.current = value;
93
+ dirty.current = false;
94
+ }
95
+ function handleChange(event) {
96
+ dirty.current = true;
97
+ onInput(event.currentTarget.value);
98
+ }
99
+ function commit(current) {
100
+ if (!dirty.current) return;
101
+ dirty.current = false;
102
+ onCommit?.(current);
103
+ }
104
+ function handleBlur() {
105
+ commit(value);
106
+ }
107
+ function handleKeyDown(event) {
108
+ if (event.key === "Escape") {
109
+ event.preventDefault();
110
+ dirty.current = false;
111
+ onCancel?.(openedWith.current);
112
+ return;
113
+ }
114
+ if (event.key === "Enter" && !multiline) {
115
+ event.preventDefault();
116
+ commit(value);
117
+ return;
118
+ }
119
+ }
120
+ function handleScroll(event) {
121
+ const layer = layerRef.current;
122
+ if (!layer) return;
123
+ layer.scrollTop = event.currentTarget.scrollTop;
124
+ layer.scrollLeft = event.currentTarget.scrollLeft;
125
+ }
126
+ return /* @__PURE__ */ jsxs(
127
+ "div",
128
+ {
129
+ className: [
130
+ "es-gold",
131
+ multiline ? "es-gold-is-multiline" : "es-gold-is-single",
132
+ className ?? ""
133
+ ].filter(Boolean).join(" "),
134
+ children: [
135
+ /* @__PURE__ */ jsx(GoldLayer, { value, rules, layerRef }),
136
+ placeholder && value === "" ? (
137
+ // Drawn as the layer's SIBLING, not inside it, so the layer's text stays
138
+ // character-for-character the stored string — the invariant the whole
139
+ // component rests on. It is painted here rather than by the box's own
140
+ // `::placeholder` (which gold.css makes transparent) so the hint lands
141
+ // in exactly the place the first typed character will.
142
+ /* @__PURE__ */ jsx("div", { className: "es-gold-placeholder", "aria-hidden": "true", children: placeholder })
143
+ ) : null,
144
+ /* @__PURE__ */ jsx(
145
+ "textarea",
146
+ {
147
+ className: "es-gold-input",
148
+ value,
149
+ placeholder,
150
+ "aria-label": ariaLabel,
151
+ onChange: handleChange,
152
+ onFocus: handleFocus,
153
+ onBlur: handleBlur,
154
+ onKeyDown: handleKeyDown,
155
+ onScroll: handleScroll
156
+ }
157
+ )
158
+ ]
159
+ }
160
+ );
161
+ }
162
+
163
+ // src/gold/tokens.ts
164
+ var goldTokens = {
165
+ /** The mark colour — `==phrase==` and the legacy `*phrase*`. */
166
+ accent: "var(--gold, #c9a461)",
167
+ /** The caret. Set explicitly because the box's own text is transparent, and
168
+ * `caret-color: auto` would make the caret transparent with it. */
169
+ caret: "var(--es-text, #111827)",
170
+ /** The hint shown while the value is empty. */
171
+ placeholder: "var(--es-field-placeholder, #9ca3af)",
172
+ /** How far the asterisks are dimmed. They stay VISIBLE — that is the deal. */
173
+ delimiterOpacity: "0.35",
174
+ /**
175
+ * Faux-bold. A real `font-weight: 600` is wider, and a wider run down here
176
+ * wraps a line earlier than the transparent box on top of it — after which
177
+ * every following line is drawn over the wrong text. The shadow reads heavier
178
+ * and moves nothing.
179
+ */
180
+ boldShadow: "0 0 0.4px currentColor",
181
+ /** Italic, marked rather than slanted — an italic FACE has its own widths, and
182
+ * the layer may not change a glyph's advance. Same reason as the bold. */
183
+ italicDecoration: "underline dotted",
184
+ /** Strike-through is metric-safe, so it is drawn exactly as it will print. */
185
+ strikeDecoration: "line-through"
186
+ };
187
+ var goldCssVars = {
188
+ accent: "--es-gold-accent",
189
+ caret: "--es-gold-caret",
190
+ placeholder: "--es-gold-placeholder",
191
+ delimiterOpacity: "--es-gold-delim-opacity",
192
+ boldShadow: "--es-gold-bold-shadow",
193
+ italicDecoration: "--es-gold-italic-decoration",
194
+ strikeDecoration: "--es-gold-strike-decoration"
195
+ };
196
+
197
+ export { GoldLayer, GoldTextInput, goldCssVars, goldSegments, goldTokens };
198
+ //# sourceMappingURL=index.js.map
199
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/gold/segments.ts","../../src/gold/GoldLayer.tsx","../../src/gold/GoldTextInput.tsx","../../src/gold/tokens.ts"],"names":["STANDARD_MARKUP","jsx"],"mappings":"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","file":"index.js","sourcesContent":["import { tokenizeInline } from 'react-os-shell/markup';\nimport type { InlineKind, InlineRule } from 'react-os-shell/markup';\n\n/**\n * Cutting an authored string into the pieces the layer paints — WITHOUT losing a\n * character.\n *\n * The grammar is not re-implemented here and never will be (constitution E8):\n * {@link tokenizeInline} from `react-os-shell/markup` decides what is a run and\n * what is not, exactly as it does for the published page. What it does not\n * return is the delimiters — it hands back a run's INNER text, because every\n * other consumer wants the words without the asterisks.\n *\n * This layer wants the asterisks. It draws underneath a transparent box whose\n * caret walks the stored string, so the two surfaces must hold the same\n * characters in the same order: drop the `**` and every glyph after it sits two\n * columns left of the caret that is supposed to be inside it.\n *\n * So each parsed run is located back in the source and cut into three pieces —\n * opening delimiter, inner text, closing delimiter — and EVERY piece is sliced\n * out of the original string rather than rebuilt from the token. Nothing is\n * retyped, so nothing can be retyped wrong.\n */\n\n/** A piece of the source: a parsed run's kind, or the delimiters around one. */\nexport type GoldSegmentKind = InlineKind | 'delimiter';\n\nexport interface GoldSegment {\n kind: GoldSegmentKind;\n /** Verbatim source text. Concatenating every segment reproduces the input. */\n text: string;\n}\n\n/**\n * Split `value` into paint-able segments under `rules`.\n *\n * GUARANTEE: `segments.map(s => s.text).join('') === value`, always. If the walk\n * below cannot line a run up with the rule that produced it — which would mean\n * the grammar and this function disagree — the whole string comes back as one\n * plain `text` segment. Losing the gold is a disappointment; losing a character\n * puts the caret in the wrong place, so the fallback is never in doubt.\n */\nexport function goldSegments(value: string, rules: readonly InlineRule[]): GoldSegment[] {\n const out: GoldSegment[] = [];\n let at = 0;\n\n for (const token of tokenizeInline(value, rules)) {\n if (token.kind === 'text') {\n // Empty text tokens are part of the tokenizer's alternating contract; they\n // paint nothing, so they are dropped rather than drawn as empty spans.\n if (token.text.length > 0) {\n out.push({ kind: 'text', text: value.slice(at, at + token.text.length) });\n at += token.text.length;\n }\n continue;\n }\n\n const rule = ruleAt(value, at, token.kind, token.text, rules);\n if (!rule) return [{ kind: 'text', text: value }];\n\n const innerAt = at + rule.open.length;\n const closeAt = innerAt + token.text.length;\n const end = closeAt + rule.close.length;\n\n out.push({ kind: 'delimiter', text: value.slice(at, innerAt) });\n out.push({ kind: token.kind, text: value.slice(innerAt, closeAt) });\n out.push({ kind: 'delimiter', text: value.slice(closeAt, end) });\n at = end;\n }\n\n // The promise, checked rather than assumed — this is the one thing the whole\n // component rests on, and it costs one string compare per keystroke.\n if (out.map((s) => s.text).join('') !== value) return [{ kind: 'text', text: value }];\n return out;\n}\n\n/**\n * The rule that opened the run `tokenizeInline` reported at `at`.\n *\n * Found by shape, in the tokenizer's own rule order: the first rule of the right\n * kind whose `open`, inner text and `close` all sit where they would have to.\n * The tokenizer's extra guards (an intraword `_`, a `#` before a digit) only\n * ever make it SKIP a rule, and a skipped rule produces no run — so at a\n * position where a run exists, the first shape-match is the rule that made it.\n */\nfunction ruleAt(\n value: string,\n at: number,\n kind: InlineKind,\n inner: string,\n rules: readonly InlineRule[],\n): InlineRule | null {\n for (const rule of rules) {\n if (rule.kind !== kind) continue;\n if (!value.startsWith(rule.open, at)) continue;\n const innerAt = at + rule.open.length;\n if (!value.startsWith(inner, innerAt)) continue;\n if (!value.startsWith(rule.close, innerAt + inner.length)) continue;\n return rule;\n }\n return null;\n}\n","import { STANDARD_MARKUP } from 'react-os-shell/markup';\nimport { goldSegments } from './segments';\nimport type { GoldSegment } from './segments';\nimport type { GoldLayerProps } from './types';\n\n/**\n * The formatted text, drawn UNDER the box.\n *\n * It is a MARKER layer, not a preview of the page. That distinction is the whole\n * design, and it is why a bold run here is not drawn at weight 600:\n *\n * the caret, the selection and the line breaks all come from the transparent\n * textarea on top, which has ONE font. A heavier or slanted face down here is\n * wider, so a marked line would wrap a word earlier than the box does and\n * every line after it would sit on top of the wrong text.\n *\n * So the layer may only paint what cannot move a glyph — colour, opacity,\n * text-decoration, a shadow. A merchant sees WHERE the formatting starts and\n * ends and in which colour it will land; the real weight and slant appear on the\n * page the moment they click away. Full WYSIWYG (asterisks gone, real faces) is\n * a second text engine and is deliberately out of scope — see CLAUDE.md.\n *\n * The elements mirror the storefront's own renderer (`goldPhrases.tsx`):\n * `<strong>` for bold, `<em>` for italic and for the gold accent, `<s>` for\n * struck-out. The classes carry the paint; the tokens carry the values.\n */\nexport function GoldLayer({ value, rules = STANDARD_MARKUP, className, layerRef }: GoldLayerProps) {\n const segments = goldSegments(value, rules);\n return (\n <div\n ref={layerRef}\n className={['es-gold-layer', className ?? ''].filter(Boolean).join(' ')}\n // The box above holds the same characters and is what a screen reader\n // reads; announcing them twice would be a bug, not thoroughness.\n aria-hidden=\"true\"\n >\n {segments.map((segment, i) => paint(segment, i))}\n </div>\n );\n}\n\nfunction paint(segment: GoldSegment, key: number) {\n const { kind, text } = segment;\n switch (kind) {\n // The delimiters STAY — dimmed, never removed. See `segments.ts`.\n case 'delimiter':\n return (\n <span key={key} className=\"es-gold-delim\">\n {text}\n </span>\n );\n // `accent` (the legacy `*phrase*`) and `highlight` (`==phrase==`) paint the\n // same, exactly as the page paints them — which is what makes converting\n // stored copy from one to the other invisible here too.\n case 'accent':\n case 'highlight':\n return (\n <em key={key} className=\"es-gold-mark es-gold-accent\">\n {text}\n </em>\n );\n case 'bold':\n return (\n <strong key={key} className=\"es-gold-mark es-gold-bold\">\n {text}\n </strong>\n );\n case 'italic':\n return (\n <em key={key} className=\"es-gold-mark es-gold-italic\">\n {text}\n </em>\n );\n case 'strike':\n return (\n <s key={key} className=\"es-gold-mark es-gold-strike\">\n {text}\n </s>\n );\n // `text` — and `code`, which no product rule produces today, so a backtick\n // stays ordinary copy rather than becoming a chip nobody asked for.\n default:\n return (\n <span key={key} className=\"es-gold-text\">\n {text}\n </span>\n );\n }\n}\n","import { useRef } from 'react';\nimport type { ChangeEvent, KeyboardEvent, UIEvent } from 'react';\nimport { STANDARD_MARKUP } from 'react-os-shell/markup';\nimport { GoldLayer } from './GoldLayer';\nimport type { GoldTextInputProps } from './types';\n\n/**\n * Type where the text sits, and watch the gold appear as you type.\n *\n * The arrangement, in one paragraph: a textarea whose own text is TRANSPARENT\n * sits on top of a layer holding the same characters, painted. What you read is\n * the layer; what the caret walks is the box. Both take their type styles from\n * whatever the host renders them inside (`font: inherit` all the way down), so\n * the glyphs land on top of each other instead of near each other, and the box\n * inherits the page's own type rather than a size this package invented.\n *\n * The layer is the element IN FLOW and the box is absolutely positioned over it,\n * which is deliberate: the layer holds the same characters, so it wraps to the\n * same height, so the box grows as the merchant types without anyone measuring\n * anything.\n *\n * WHAT IT DOES NOT DO: write. It has no document, no section, no Puck, no shop.\n * `onCommit` says \"this edit is finished\"; what that means is the host's\n * business. That is what makes it shareable by both editors (constitution E2)\n * and testable without any of them.\n *\n * No `'use client'` — same reason as the rail: the directive is the HOST's to\n * place, and both editors' chrome are already client components. Unlike the\n * rail, though, this leaf has hooks, so it must be rendered inside that client\n * boundary; a server component can import the module but cannot render it.\n *\n * Requires `editor-shell/gold.css`, imported once from a client entry.\n */\nexport function GoldTextInput({\n value,\n onInput,\n onCommit,\n onCancel,\n multiline = false,\n placeholder,\n className,\n rules = STANDARD_MARKUP,\n ariaLabel,\n}: GoldTextInputProps) {\n const layerRef = useRef<HTMLDivElement | null>(null);\n\n /** The string this editing session started from — what Escape restores. */\n const openedWith = useRef(value);\n /**\n * Whether anything has been typed since focus. This one boolean carries two\n * owner rulings at once: a focus that types nothing writes nothing (6), and a\n * session that has already committed does not commit again on the blur that\n * follows (2 — one undo step per session).\n */\n const dirty = useRef(false);\n\n function handleFocus() {\n openedWith.current = value;\n dirty.current = false;\n }\n\n function handleChange(event: ChangeEvent<HTMLTextAreaElement>) {\n dirty.current = true;\n onInput(event.currentTarget.value);\n }\n\n function commit(current: string) {\n if (!dirty.current) return;\n dirty.current = false;\n onCommit?.(current);\n }\n\n function handleBlur() {\n // Click-away commits (ruling 5) — or, after an Escape or an Enter that has\n // already committed, does nothing at all.\n commit(value);\n }\n\n function handleKeyDown(event: KeyboardEvent<HTMLTextAreaElement>) {\n if (event.key === 'Escape') {\n // Escape ALWAYS cancels, even having typed nothing: it is the merchant\n // saying \"leave this alone\", and it must not fall through to a host that\n // reads Escape as \"close the editor\".\n event.preventDefault();\n dirty.current = false;\n onCancel?.(openedWith.current);\n return;\n }\n\n if (event.key === 'Enter' && !multiline) {\n // A single-line field never takes a newline — not even one that would be\n // thrown away by a commit that does not happen.\n event.preventDefault();\n commit(value);\n return;\n }\n\n // Multi-line Enter is left alone on purpose: the browser inserts the newline\n // and the ordinary change event carries it back. Nothing commits.\n }\n\n function handleScroll(event: UIEvent<HTMLTextAreaElement>) {\n // Only reachable when a host constrains the height — the box scrolls, so the\n // layer has to scroll with it or the two surfaces come apart.\n const layer = layerRef.current;\n if (!layer) return;\n layer.scrollTop = event.currentTarget.scrollTop;\n layer.scrollLeft = event.currentTarget.scrollLeft;\n }\n\n return (\n <div\n className={[\n 'es-gold',\n multiline ? 'es-gold-is-multiline' : 'es-gold-is-single',\n className ?? '',\n ]\n .filter(Boolean)\n .join(' ')}\n >\n <GoldLayer value={value} rules={rules} layerRef={layerRef} />\n {placeholder && value === '' ? (\n // Drawn as the layer's SIBLING, not inside it, so the layer's text stays\n // character-for-character the stored string — the invariant the whole\n // component rests on. It is painted here rather than by the box's own\n // `::placeholder` (which gold.css makes transparent) so the hint lands\n // in exactly the place the first typed character will.\n <div className=\"es-gold-placeholder\" aria-hidden=\"true\">\n {placeholder}\n </div>\n ) : null}\n <textarea\n className=\"es-gold-input\"\n value={value}\n placeholder={placeholder}\n aria-label={ariaLabel}\n onChange={handleChange}\n onFocus={handleFocus}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n onScroll={handleScroll}\n />\n </div>\n );\n}\n","/**\n * Gold-layer paint tokens.\n *\n * SHORT LIST BY DESIGN. The layer draws inside whatever the host renders it in\n * and inherits that context's type entirely — family, size, weight, line-height,\n * letter-spacing — because it has to sit on top of a box that inherits the same\n * (see `GoldTextInput`). So there is no type here to tokenise; what is left is\n * the paint, and only the paint that cannot move a glyph.\n *\n * NOTHING HERE IS INVENTED (CLAUDE.md — \"the tokens are not ours to invent\"):\n * - the gold defers to the page's own `--gold`, falling back to the\n * storefront's light-theme value (`efficient-shop/app/globals.css` → `--gold:\n * #c9a461`), so the mark in the box matches the mark on the page;\n * - the caret and the placeholder defer to the shell layer's `--es-text` /\n * `--es-field-placeholder` (see `shell/tokens.ts`) with the same values as\n * their fallbacks;\n * - the dim is the rail's existing disabled opacity (0.35), not a new number.\n *\n * Every value is a STRING, unlike the rail's and shell's pixel magnitudes:\n * none of these is a length, so nothing here gains a unit on the way into CSS.\n *\n * Exposed BOTH ways, per the same value: `goldTokens` (this object) and\n * `gold.css` (the stylesheet, which declares them on `.es-gold` — names in\n * {@link goldCssVars}). The parity gate in `tests/gold-leaf-safety.test.ts`\n * proves the two never drift.\n */\nexport const goldTokens = {\n /** The mark colour — `==phrase==` and the legacy `*phrase*`. */\n accent: 'var(--gold, #c9a461)',\n /** The caret. Set explicitly because the box's own text is transparent, and\n * `caret-color: auto` would make the caret transparent with it. */\n caret: 'var(--es-text, #111827)',\n /** The hint shown while the value is empty. */\n placeholder: 'var(--es-field-placeholder, #9ca3af)',\n /** How far the asterisks are dimmed. They stay VISIBLE — that is the deal. */\n delimiterOpacity: '0.35',\n /**\n * Faux-bold. A real `font-weight: 600` is wider, and a wider run down here\n * wraps a line earlier than the transparent box on top of it — after which\n * every following line is drawn over the wrong text. The shadow reads heavier\n * and moves nothing.\n */\n boldShadow: '0 0 0.4px currentColor',\n /** Italic, marked rather than slanted — an italic FACE has its own widths, and\n * the layer may not change a glyph's advance. Same reason as the bold. */\n italicDecoration: 'underline dotted',\n /** Strike-through is metric-safe, so it is drawn exactly as it will print. */\n strikeDecoration: 'line-through',\n} as const;\n\nexport type GoldTokens = typeof goldTokens;\n\n/**\n * The CSS custom-property name behind each token. `gold.css` sets these on\n * `.es-gold`, so overriding one on (or above) the box re-themes it without\n * shipping new CSS — e.g. `style={{ ['--es-gold-accent']: brand }}`.\n */\nexport const goldCssVars: Record<keyof GoldTokens, string> = {\n accent: '--es-gold-accent',\n caret: '--es-gold-caret',\n placeholder: '--es-gold-placeholder',\n delimiterOpacity: '--es-gold-delim-opacity',\n boldShadow: '--es-gold-bold-shadow',\n italicDecoration: '--es-gold-italic-decoration',\n strikeDecoration: '--es-gold-strike-decoration',\n};\n"]}
@@ -0,0 +1,189 @@
1
+ import * as react from 'react';
2
+ import { SVGProps } from 'react';
3
+
4
+ type IconProps = Omit<SVGProps<SVGSVGElement>, 'children'> & {
5
+ size?: number;
6
+ };
7
+ /** Every icon name in the set, with what it means. One meaning, one SVG. */
8
+ declare const iconMeanings: {
9
+ readonly trash: "Delete";
10
+ readonly duplicate: "Duplicate — owner pick 08-20, two-pages form";
11
+ readonly "copy-to-page": "Copy to another page";
12
+ readonly close: "Close / dismiss";
13
+ readonly add: "Add / plus";
14
+ readonly check: "Checkmark / success";
15
+ readonly undo: "Undo";
16
+ readonly redo: "Redo";
17
+ readonly rename: "Rename / edit";
18
+ readonly search: "Search";
19
+ readonly eye: "Show";
20
+ readonly "eye-off": "Hide";
21
+ readonly external: "Leaves this surface";
22
+ readonly drag: "Drag handle";
23
+ readonly lock: "Locked";
24
+ readonly revert: "Revert to inherited";
25
+ readonly chevron: "Disclosure — rotate for direction";
26
+ readonly send: "Send";
27
+ readonly page: "Page AND the Pages rail — owner ruling 08-20, one meaning";
28
+ readonly layers: "Layers";
29
+ readonly styles: "Styles";
30
+ readonly media: "Media";
31
+ readonly sitemap: "Sitemap";
32
+ readonly image: "Image / photo";
33
+ readonly grid: "Grid";
34
+ readonly cart: "Cart";
35
+ readonly desktop: "Desktop";
36
+ readonly tablet: "Tablet";
37
+ readonly phone: "Phone";
38
+ readonly warning: "Warning";
39
+ readonly type: "Text marker — the T";
40
+ readonly wheel: "Wheel";
41
+ readonly chat: "Chat";
42
+ readonly bell: "Notification bell";
43
+ readonly template: "Template / bookmark";
44
+ readonly clock: "clock";
45
+ readonly play: "play";
46
+ readonly "align-left": "align left";
47
+ readonly "align-center": "align center";
48
+ readonly opacity: "opacity";
49
+ readonly "padding-top": "padding top";
50
+ readonly "padding-bottom": "padding bottom";
51
+ readonly width: "width";
52
+ readonly anchor: "anchor";
53
+ readonly hash: "hash";
54
+ readonly columns: "columns";
55
+ readonly list: "list";
56
+ readonly "text-lines": "text lines";
57
+ readonly "text-size": "text size";
58
+ readonly "text-small": "text small";
59
+ readonly button: "button";
60
+ readonly link: "link";
61
+ readonly tag: "tag";
62
+ readonly rotate: "rotate";
63
+ readonly square: "square";
64
+ readonly drop: "drop";
65
+ readonly "plus-minus": "plus minus";
66
+ readonly dollar: "dollar";
67
+ readonly note: "note";
68
+ };
69
+ type IconName = keyof typeof iconMeanings;
70
+ /** Delete · keep · efficient-shop · components/UiIcon.tsx:99 */
71
+ declare function TrashIcon({ size, ...rest }: IconProps): react.JSX.Element;
72
+ /** Duplicate — owner pick 08-20, two-pages form · picked · efficient-admin-portal · src/components/SalesOrderDetail.tsx:628 */
73
+ declare function DuplicateIcon({ size, ...rest }: IconProps): react.JSX.Element;
74
+ /** Copy to another page · keep · efficient-shop · components/DuplicateToPageMenu.tsx:56 */
75
+ declare function CopyToPageIcon({ size, ...rest }: IconProps): react.JSX.Element;
76
+ /** Close / dismiss · pick · efficient-shop · components/AddSectionPanel.tsx:212 */
77
+ declare function CloseIcon({ size, ...rest }: IconProps): react.JSX.Element;
78
+ /** Add / plus · keep · editor-shell · src/rail/icons.tsx:92 */
79
+ declare function AddIcon({ size, ...rest }: IconProps): react.JSX.Element;
80
+ /** Checkmark / success · pick · efficient-shop · components/wheels-plp/PlpNotifyMeDialog.tsx:143 */
81
+ declare function CheckIcon({ size, ...rest }: IconProps): react.JSX.Element;
82
+ /** Undo · keep · efficient-admin-portal · src/components/email/CampaignDesigner.tsx:195 */
83
+ declare function UndoIcon({ size, ...rest }: IconProps): react.JSX.Element;
84
+ /** Redo · keep · efficient-admin-portal · src/components/email/CampaignDesigner.tsx:208 */
85
+ declare function RedoIcon({ size, ...rest }: IconProps): react.JSX.Element;
86
+ /** Rename / edit · keep · efficient-shop · components/UiIcon.tsx:90 */
87
+ declare function RenameIcon({ size, ...rest }: IconProps): react.JSX.Element;
88
+ /** Search · keep · efficient-shop · components/UiIcon.tsx:74 */
89
+ declare function SearchIcon({ size, ...rest }: IconProps): react.JSX.Element;
90
+ /** Show · keep · efficient-shop · components/UiIcon.tsx:63 */
91
+ declare function EyeIcon({ size, ...rest }: IconProps): react.JSX.Element;
92
+ /** Hide · keep · efficient-shop · components/UiIcon.tsx:69 */
93
+ declare function EyeOffIcon({ size, ...rest }: IconProps): react.JSX.Element;
94
+ /** Leaves this surface · keep · efficient-shop · components/UiIcon.tsx:82 */
95
+ declare function ExternalIcon({ size, ...rest }: IconProps): react.JSX.Element;
96
+ /** Drag handle · keep · efficient-admin-portal · src/components/email/designerIcons.tsx:78 */
97
+ declare function DragIcon({ size, ...rest }: IconProps): react.JSX.Element;
98
+ /** Locked · keep · efficient-admin-portal · src/components/email/designerIcons.tsx:8 */
99
+ declare function LockIcon({ size, ...rest }: IconProps): react.JSX.Element;
100
+ /** Revert to inherited · keep · efficient-admin-portal · src/components/email/designerIcons.tsx:31 */
101
+ declare function RevertIcon({ size, ...rest }: IconProps): react.JSX.Element;
102
+ /** Disclosure — rotate for direction · keep · efficient-shop · components/UiIcon.tsx:107 */
103
+ declare function ChevronIcon({ size, ...rest }: IconProps): react.JSX.Element;
104
+ /** Send · keep · efficient-shop · components/wheels-plp/PlpNotifyMeDialog.tsx:293 */
105
+ declare function SendIcon({ size, ...rest }: IconProps): react.JSX.Element;
106
+ /** Page AND the Pages rail — owner ruling 08-20, one meaning · picked · editor-shell · src/rail/icons.tsx:36 */
107
+ declare function PageIcon({ size, ...rest }: IconProps): react.JSX.Element;
108
+ /** Layers · keep · editor-shell · src/rail/icons.tsx:47 */
109
+ declare function LayersIcon({ size, ...rest }: IconProps): react.JSX.Element;
110
+ /** Styles · keep · editor-shell · src/rail/icons.tsx:57 */
111
+ declare function StylesIcon({ size, ...rest }: IconProps): react.JSX.Element;
112
+ /** Media · keep · editor-shell · src/rail/icons.tsx:69 */
113
+ declare function MediaIcon({ size, ...rest }: IconProps): react.JSX.Element;
114
+ /** Sitemap · keep · editor-shell · src/rail/icons.tsx:80 */
115
+ declare function SitemapIcon({ size, ...rest }: IconProps): react.JSX.Element;
116
+ /** Image / photo · pick · efficient-shop · components/AddSectionPanel.tsx:151 */
117
+ declare function ImageIcon({ size, ...rest }: IconProps): react.JSX.Element;
118
+ /** Grid · pick · efficient-shop · components/SearchDialog.tsx:504 */
119
+ declare function GridIcon({ size, ...rest }: IconProps): react.JSX.Element;
120
+ /** Cart · pick · efficient-shop · components/Header.tsx:69 */
121
+ declare function CartIcon({ size, ...rest }: IconProps): react.JSX.Element;
122
+ /** Desktop · keep · efficient-shop · components/DeviceSwitch.tsx:18 */
123
+ declare function DesktopIcon({ size, ...rest }: IconProps): react.JSX.Element;
124
+ /** Tablet · keep · efficient-shop · components/DeviceSwitch.tsx:24 */
125
+ declare function TabletIcon({ size, ...rest }: IconProps): react.JSX.Element;
126
+ /** Phone · pick · efficient-shop · components/DeviceSwitch.tsx:30 */
127
+ declare function PhoneIcon({ size, ...rest }: IconProps): react.JSX.Element;
128
+ /** Warning · pick · efficient-admin-portal · src/components/CustomerDetail.tsx:235 */
129
+ declare function WarningIcon({ size, ...rest }: IconProps): react.JSX.Element;
130
+ /** Text marker — the T · pick · efficient-shop · lib/puck/controls.tsx:269 */
131
+ declare function TypeIcon({ size, ...rest }: IconProps): react.JSX.Element;
132
+ /** Wheel · pick · efficient-shop · components/SearchDialog.tsx:485 */
133
+ declare function WheelIcon({ size, ...rest }: IconProps): react.JSX.Element;
134
+ /** Chat · pick · efficient-shop · components/ChatWidget.tsx:108 */
135
+ declare function ChatIcon({ size, ...rest }: IconProps): react.JSX.Element;
136
+ /** Notification bell · keep · efficient-shop · components/wheels-plp/PlpNotifyMeDialog.tsx:183 */
137
+ declare function BellIcon({ size, ...rest }: IconProps): react.JSX.Element;
138
+ /** Template / bookmark · keep · efficient-shop · components/TemplateGlyph.tsx:20 */
139
+ declare function TemplateIcon({ size, ...rest }: IconProps): react.JSX.Element;
140
+ /** clock · new · drawn for the settings-panel design, 2026-08-20 */
141
+ declare function ClockIcon({ size, ...rest }: IconProps): react.JSX.Element;
142
+ /** play · new · drawn for the settings-panel design, 2026-08-20 */
143
+ declare function PlayIcon({ size, ...rest }: IconProps): react.JSX.Element;
144
+ /** align left · new · drawn for the settings-panel design, 2026-08-20 */
145
+ declare function AlignLeftIcon({ size, ...rest }: IconProps): react.JSX.Element;
146
+ /** align center · new · drawn for the settings-panel design, 2026-08-20 */
147
+ declare function AlignCenterIcon({ size, ...rest }: IconProps): react.JSX.Element;
148
+ /** opacity · new · drawn for the settings-panel design, 2026-08-20 */
149
+ declare function OpacityIcon({ size, ...rest }: IconProps): react.JSX.Element;
150
+ /** padding top · new · drawn for the settings-panel design, 2026-08-20 */
151
+ declare function PaddingTopIcon({ size, ...rest }: IconProps): react.JSX.Element;
152
+ /** padding bottom · new · drawn for the settings-panel design, 2026-08-20 */
153
+ declare function PaddingBottomIcon({ size, ...rest }: IconProps): react.JSX.Element;
154
+ /** width · new · drawn for the settings-panel design, 2026-08-20 */
155
+ declare function WidthIcon({ size, ...rest }: IconProps): react.JSX.Element;
156
+ /** anchor · new · drawn for the settings-panel design, 2026-08-20 */
157
+ declare function AnchorIcon({ size, ...rest }: IconProps): react.JSX.Element;
158
+ /** hash · new · drawn for the settings-panel design, 2026-08-20 */
159
+ declare function HashIcon({ size, ...rest }: IconProps): react.JSX.Element;
160
+ /** columns · new · drawn for the settings-panel design, 2026-08-20 */
161
+ declare function ColumnsIcon({ size, ...rest }: IconProps): react.JSX.Element;
162
+ /** list · new · drawn for the settings-panel design, 2026-08-20 */
163
+ declare function ListIcon({ size, ...rest }: IconProps): react.JSX.Element;
164
+ /** text lines · new · drawn for the settings-panel design, 2026-08-20 */
165
+ declare function TextLinesIcon({ size, ...rest }: IconProps): react.JSX.Element;
166
+ /** text size · new · drawn for the settings-panel design, 2026-08-20 */
167
+ declare function TextSizeIcon({ size, ...rest }: IconProps): react.JSX.Element;
168
+ /** text small · new · drawn for the settings-panel design, 2026-08-20 */
169
+ declare function TextSmallIcon({ size, ...rest }: IconProps): react.JSX.Element;
170
+ /** button · new · drawn for the settings-panel design, 2026-08-20 */
171
+ declare function ButtonIcon({ size, ...rest }: IconProps): react.JSX.Element;
172
+ /** link · new · drawn for the settings-panel design, 2026-08-20 */
173
+ declare function LinkIcon({ size, ...rest }: IconProps): react.JSX.Element;
174
+ /** tag · new · drawn for the settings-panel design, 2026-08-20 */
175
+ declare function TagIcon({ size, ...rest }: IconProps): react.JSX.Element;
176
+ /** rotate · new · drawn for the settings-panel design, 2026-08-20 */
177
+ declare function RotateIcon({ size, ...rest }: IconProps): react.JSX.Element;
178
+ /** square · new · drawn for the settings-panel design, 2026-08-20 */
179
+ declare function SquareIcon({ size, ...rest }: IconProps): react.JSX.Element;
180
+ /** drop · new · drawn for the settings-panel design, 2026-08-20 */
181
+ declare function DropIcon({ size, ...rest }: IconProps): react.JSX.Element;
182
+ /** plus minus · new · drawn for the settings-panel design, 2026-08-20 */
183
+ declare function PlusMinusIcon({ size, ...rest }: IconProps): react.JSX.Element;
184
+ /** dollar · new · drawn for the settings-panel design, 2026-08-20 */
185
+ declare function DollarIcon({ size, ...rest }: IconProps): react.JSX.Element;
186
+ /** note · new · drawn for the settings-panel design, 2026-08-20 */
187
+ declare function NoteIcon({ size, ...rest }: IconProps): react.JSX.Element;
188
+
189
+ export { AddIcon, AlignCenterIcon, AlignLeftIcon, AnchorIcon, BellIcon, ButtonIcon, CartIcon, ChatIcon, CheckIcon, ChevronIcon, ClockIcon, CloseIcon, ColumnsIcon, CopyToPageIcon, DesktopIcon, DollarIcon, DragIcon, DropIcon, DuplicateIcon, ExternalIcon, EyeIcon, EyeOffIcon, GridIcon, HashIcon, type IconName, type IconProps, ImageIcon, LayersIcon, LinkIcon, ListIcon, LockIcon, MediaIcon, NoteIcon, OpacityIcon, PaddingBottomIcon, PaddingTopIcon, PageIcon, PhoneIcon, PlayIcon, PlusMinusIcon, RedoIcon, RenameIcon, RevertIcon, RotateIcon, SearchIcon, SendIcon, SitemapIcon, SquareIcon, StylesIcon, TabletIcon, TagIcon, TemplateIcon, TextLinesIcon, TextSizeIcon, TextSmallIcon, TrashIcon, TypeIcon, UndoIcon, WarningIcon, WheelIcon, WidthIcon, iconMeanings };
@@ -0,0 +1,3 @@
1
+ export { AddIcon, AlignCenterIcon, AlignLeftIcon, AnchorIcon, BellIcon, ButtonIcon, CartIcon, ChatIcon, CheckIcon, ChevronIcon, ClockIcon, CloseIcon, ColumnsIcon, CopyToPageIcon, DesktopIcon, DollarIcon, DragIcon, DropIcon, DuplicateIcon, ExternalIcon, EyeIcon, EyeOffIcon, GridIcon, HashIcon, ImageIcon, LayersIcon, LinkIcon, ListIcon, LockIcon, MediaIcon, NoteIcon, OpacityIcon, PaddingBottomIcon, PaddingTopIcon, PageIcon, PhoneIcon, PlayIcon, PlusMinusIcon, RedoIcon, RenameIcon, RevertIcon, RotateIcon, SearchIcon, SendIcon, SitemapIcon, SquareIcon, StylesIcon, TabletIcon, TagIcon, TemplateIcon, TextLinesIcon, TextSizeIcon, TextSmallIcon, TrashIcon, TypeIcon, UndoIcon, WarningIcon, WheelIcon, WidthIcon, iconMeanings } from '../chunk-UB3KPBFP.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { AddIcon, EditorRail, EditorRailButton, EditorRailButtonProps, EditorRailItem, EditorRailProps, LayersIcon, MediaIcon, PagesIcon, RailIconProps, RailTokens, SitemapIcon, StylesIcon, railCssVars, railTokens } from './rail/index.js';
1
+ export { EditorRail, EditorRailButton, EditorRailButtonProps, EditorRailItem, EditorRailProps, RailTokens, railCssVars, railTokens } from './rail/index.js';
2
+ export { AddIcon, LayersIcon, MediaIcon, PageIcon as PagesIcon, IconProps as RailIconProps, SitemapIcon, StylesIcon } from './icons/index.js';
2
3
  export { ShellTokens, puckAzureRamp, shellCssVars, shellTokens } from './shell/index.js';
3
4
  import 'react';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
+ export { EditorRail, EditorRailButton, railCssVars, railTokens } from './chunk-3VMFPQGZ.js';
2
+ export { AddIcon, LayersIcon, MediaIcon, PageIcon as PagesIcon, SitemapIcon, StylesIcon } from './chunk-UB3KPBFP.js';
1
3
  export { puckAzureRamp, shellCssVars, shellTokens } from './chunk-N7NW4W3P.js';
2
- export { AddIcon, EditorRail, EditorRailButton, LayersIcon, MediaIcon, PagesIcon, SitemapIcon, StylesIcon, railCssVars, railTokens } from './chunk-WJIHF6PY.js';
3
4
  //# sourceMappingURL=index.js.map
4
5
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,6 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, SVGProps } from 'react';
2
+ import { ReactNode } from 'react';
3
+ export { AddIcon, LayersIcon, MediaIcon, PageIcon as PagesIcon, IconProps as RailIconProps, SitemapIcon, StylesIcon } from '../icons/index.js';
3
4
 
4
5
  /**
5
6
  * One entry in the rail. A rail item is either a *view switch* (Pages, Layers,
@@ -79,33 +80,6 @@ declare function EditorRail({ items, ariaLabel, className }: EditorRailProps): r
79
80
  */
80
81
  declare function EditorRailButton({ label, icon, active, disabled, onSelect, className, }: EditorRailButtonProps): react.JSX.Element;
81
82
 
82
- /**
83
- * The rail's bespoke icon set — shipped WITH the rail so no editor has to reach
84
- * for its own icon library (the storefront uses simple-icons, the admin portal
85
- * heroicons; neither is this rail's source of truth). The five view glyphs are
86
- * the exact paths the storefront editor's rail already draws, relocated here so
87
- * the shared rail is visually identical to its reference; `AddIcon` is new.
88
- *
89
- * Pure SVG, no interactivity, no browser globals — these render fine inside a
90
- * React Server Component. Sized 18×18 by default (the storefront's rail size);
91
- * pass `size` to override, or let `.es-rail-btn svg` in rail.css drive it.
92
- */
93
- type RailIconProps = Omit<SVGProps<SVGSVGElement>, 'children'> & {
94
- size?: number;
95
- };
96
- /** Pages — a document with a folded corner and text lines. */
97
- declare function PagesIcon(props: RailIconProps): react.JSX.Element;
98
- /** Layers — stacked sheets (the section / component tree). */
99
- declare function LayersIcon(props: RailIconProps): react.JSX.Element;
100
- /** Styles — a paint droplet with palette dots (site styles). */
101
- declare function StylesIcon(props: RailIconProps): react.JSX.Element;
102
- /** Media — an image frame with a sun and a mountain (the media library). */
103
- declare function MediaIcon(props: RailIconProps): react.JSX.Element;
104
- /** Sitemap — a root node branching to two page nodes (the visual sitemap). */
105
- declare function SitemapIcon(props: RailIconProps): react.JSX.Element;
106
- /** Add — the quick-add "+", drawn in the same weight and cap style as the set. */
107
- declare function AddIcon(props: RailIconProps): react.JSX.Element;
108
-
109
83
  /**
110
84
  * Rail design tokens — the ONE source both apps' rails converge on.
111
85
  *
@@ -183,4 +157,4 @@ declare const railCssVars: {
183
157
  readonly accentTint: "--es-rail-accent-tint";
184
158
  };
185
159
 
186
- export { AddIcon, EditorRail, EditorRailButton, type EditorRailButtonProps, type EditorRailItem, type EditorRailProps, LayersIcon, MediaIcon, PagesIcon, type RailIconProps, type RailTokens, SitemapIcon, StylesIcon, railCssVars, railTokens };
160
+ export { EditorRail, EditorRailButton, type EditorRailButtonProps, type EditorRailItem, type EditorRailProps, type RailTokens, railCssVars, railTokens };
@@ -1,3 +1,4 @@
1
- export { AddIcon, EditorRail, EditorRailButton, LayersIcon, MediaIcon, PagesIcon, SitemapIcon, StylesIcon, railCssVars, railTokens } from '../chunk-WJIHF6PY.js';
1
+ export { EditorRail, EditorRailButton, railCssVars, railTokens } from '../chunk-3VMFPQGZ.js';
2
+ export { AddIcon, LayersIcon, MediaIcon, PageIcon as PagesIcon, SitemapIcon, StylesIcon } from '../chunk-UB3KPBFP.js';
2
3
  //# sourceMappingURL=index.js.map
3
4
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "editor-shell",
3
- "version": "0.2.0",
4
- "description": "Shared editor-chrome primitives for the EFFICIENT editors. First brick: EditorRail one shared left icon-rail, consumable as a Next-16-safe leaf.",
3
+ "version": "0.4.0",
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",
7
7
  "homepage": "https://github.com/Lewislhy/editor-shell#readme",
@@ -28,33 +28,51 @@
28
28
  "types": "./dist/rail/index.d.ts",
29
29
  "import": "./dist/rail/index.js"
30
30
  },
31
+ "./icons": {
32
+ "types": "./dist/icons/index.d.ts",
33
+ "import": "./dist/icons/index.js"
34
+ },
31
35
  "./rail.css": "./dist/rail/rail.css",
32
36
  "./shell": {
33
37
  "types": "./dist/shell/index.d.ts",
34
38
  "import": "./dist/shell/index.js"
35
39
  },
36
- "./shell.css": "./dist/shell/shell.css"
40
+ "./shell.css": "./dist/shell/shell.css",
41
+ "./gold": {
42
+ "types": "./dist/gold/index.d.ts",
43
+ "import": "./dist/gold/index.js"
44
+ },
45
+ "./gold.css": "./dist/gold/gold.css"
37
46
  },
38
47
  "files": [
39
48
  "dist"
40
49
  ],
41
50
  "peerDependencies": {
42
51
  "react": ">=18",
43
- "react-dom": ">=18"
52
+ "react-dom": ">=18",
53
+ "react-os-shell": ">=4.13"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "react-os-shell": {
57
+ "optional": true
58
+ }
44
59
  },
45
60
  "dependencies": {},
46
61
  "devDependencies": {
62
+ "@types/jsdom": "^28.0.3",
47
63
  "@types/node": "^22.0.0",
48
64
  "@types/react": "^18.2.0",
49
65
  "@types/react-dom": "^18.2.0",
50
66
  "esbuild": "^0.27.0",
67
+ "jsdom": "^26.1.0",
51
68
  "react": "^18.2.0",
52
69
  "react-dom": "^18.2.0",
70
+ "react-os-shell": "^4.31.0",
53
71
  "tsup": "^8.0.0",
54
72
  "typescript": "^5.3.0"
55
73
  },
56
74
  "scripts": {
57
- "build": "tsup && cp src/rail/rail.css dist/rail/rail.css && cp src/shell/shell.css dist/shell/shell.css",
75
+ "build": "tsup && cp src/rail/rail.css dist/rail/rail.css && cp src/shell/shell.css dist/shell/shell.css && cp src/gold/gold.css dist/gold/gold.css",
58
76
  "dev": "tsup --watch",
59
77
  "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
60
78
  "test": "node scripts/test.mjs",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/rail/EditorRailButton.tsx","../src/rail/EditorRail.tsx","../src/rail/icons.tsx","../src/rail/tokens.ts"],"names":["jsx"],"mappings":";;;AAUO,SAAS,gBAAA,CAAiB;AAAA,EAC/B,KAAA;AAAA,EACA,IAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAAA,EAA0B;AACxB,EAAA,MAAM,OAAA,GAAU,CAAC,aAAA,EAAe,MAAA,GAAS,WAAA,GAAc,EAAA,EAAI,SAAA,IAAa,EAAE,CAAA,CACvE,MAAA,CAAO,OAAO,CAAA,CACd,KAAK,GAAG,CAAA;AAEX,EAAA,uBACE,GAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACC,IAAA,EAAK,QAAA;AAAA,MACL,SAAA,EAAW,OAAA;AAAA,MACX,KAAA,EAAO,KAAA;AAAA,MACP,YAAA,EAAY,KAAA;AAAA,MACZ,cAAA,EAAc,OAAO,MAAA,KAAW,SAAA,GAAY,MAAA,GAAS,MAAA;AAAA,MACrD,QAAA;AAAA,MACA,OAAA,EAAS,WAAW,MAAA,GAAY,QAAA;AAAA,MAE/B,QAAA,EAAA;AAAA;AAAA,GACH;AAEJ;ACdO,SAAS,WAAW,EAAE,KAAA,EAAO,SAAA,GAAY,QAAA,EAAU,WAAU,EAAoB;AACtF,EAAA,uBACEA,GAAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,SAAA,EAAW,CAAC,SAAA,EAAW,SAAA,IAAa,EAAE,EAAE,MAAA,CAAO,OAAO,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAAA,MAChE,IAAA,EAAK,SAAA;AAAA,MACL,kBAAA,EAAiB,UAAA;AAAA,MACjB,YAAA,EAAY,SAAA;AAAA,MAEX,QAAA,EAAA,KAAA,CAAM,GAAA,CAAI,CAAC,IAAA,qBACVA,GAAAA;AAAA,QAAC,gBAAA;AAAA,QAAA;AAAA,UAEC,OAAO,IAAA,CAAK,KAAA;AAAA,UACZ,MAAM,IAAA,CAAK,IAAA;AAAA,UACX,QAAQ,IAAA,CAAK,MAAA;AAAA,UACb,UAAU,IAAA,CAAK,QAAA;AAAA,UACf,UAAU,IAAA,CAAK;AAAA,SAAA;AAAA,QALV,IAAA,CAAK;AAAA,OAOb;AAAA;AAAA,GACH;AAEJ;AC1BA,SAAS,MAAM,EAAE,IAAA,GAAO,IAAI,QAAA,EAAU,GAAG,MAAK,EAA4C;AACxF,EAAA,uBACEA,GAAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,KAAA,EAAO,IAAA;AAAA,MACP,MAAA,EAAQ,IAAA;AAAA,MACR,OAAA,EAAQ,WAAA;AAAA,MACR,IAAA,EAAK,MAAA;AAAA,MACL,MAAA,EAAO,cAAA;AAAA,MACP,WAAA,EAAa,GAAA;AAAA,MACb,aAAA,EAAc,OAAA;AAAA,MACd,cAAA,EAAe,OAAA;AAAA,MACf,aAAA,EAAY,MAAA;AAAA,MACX,GAAG,IAAA;AAAA,MAEH;AAAA;AAAA,GACH;AAEJ;AAGO,SAAS,UAAU,KAAA,EAAsB;AAC9C,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAO,GAAG,KAAA,EACT,QAAA,EAAA;AAAA,oBAAAA,GAAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,4DAAA,EAA6D,CAAA;AAAA,oBACrEA,GAAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,WAAA,EAAY,CAAA;AAAA,oBACpBA,GAAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,gBAAA,EAAiB;AAAA,GAAA,EAC3B,CAAA;AAEJ;AAGO,SAAS,WAAW,KAAA,EAAsB;AAC/C,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAO,GAAG,KAAA,EACT,QAAA,EAAA;AAAA,oBAAAA,GAAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,wBAAA,EAAyB,CAAA;AAAA,oBACjCA,GAAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,eAAA,EAAgB;AAAA,GAAA,EAC1B,CAAA;AAEJ;AAGO,SAAS,WAAW,KAAA,EAAsB;AAC/C,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAO,GAAG,KAAA,EACT,QAAA,EAAA;AAAA,oBAAAA,GAAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,wIAAA,EAAyI,CAAA;AAAA,oBACjJA,GAAAA,CAAC,QAAA,EAAA,EAAO,EAAA,EAAG,KAAA,EAAM,IAAG,IAAA,EAAK,CAAA,EAAE,KAAA,EAAM,IAAA,EAAK,cAAA,EAAe,CAAA;AAAA,oBACrDA,GAAAA,CAAC,QAAA,EAAA,EAAO,EAAA,EAAG,IAAA,EAAK,IAAG,KAAA,EAAM,CAAA,EAAE,KAAA,EAAM,IAAA,EAAK,cAAA,EAAe,CAAA;AAAA,oBACrDA,GAAAA,CAAC,QAAA,EAAA,EAAO,EAAA,EAAG,MAAA,EAAO,IAAG,IAAA,EAAK,CAAA,EAAE,KAAA,EAAM,IAAA,EAAK,cAAA,EAAe;AAAA,GAAA,EACxD,CAAA;AAEJ;AAGO,SAAS,UAAU,KAAA,EAAsB;AAC9C,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAO,GAAG,KAAA,EACT,QAAA,EAAA;AAAA,oBAAAA,GAAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,GAAA,EAAI,CAAA,EAAE,GAAA,EAAI,KAAA,EAAM,IAAA,EAAK,MAAA,EAAO,IAAA,EAAK,EAAA,EAAG,GAAA,EAAI,CAAA;AAAA,oBAChDA,IAAC,QAAA,EAAA,EAAO,EAAA,EAAG,OAAM,EAAA,EAAG,KAAA,EAAM,GAAE,KAAA,EAAM,CAAA;AAAA,oBAClCA,GAAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,iBAAA,EAAkB;AAAA,GAAA,EAC5B,CAAA;AAEJ;AAGO,SAAS,YAAY,KAAA,EAAsB;AAChD,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAO,GAAG,KAAA,EACT,QAAA,EAAA;AAAA,oBAAAA,GAAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,GAAA,EAAI,CAAA,EAAE,GAAA,EAAI,KAAA,EAAM,GAAA,EAAI,MAAA,EAAO,KAAA,EAAM,EAAA,EAAG,GAAA,EAAI,CAAA;AAAA,oBAChDA,GAAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,GAAA,EAAI,CAAA,EAAE,MAAA,EAAO,KAAA,EAAM,GAAA,EAAI,MAAA,EAAO,KAAA,EAAM,EAAA,EAAG,GAAA,EAAI,CAAA;AAAA,oBACnDA,GAAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,IAAA,EAAK,CAAA,EAAE,MAAA,EAAO,KAAA,EAAM,GAAA,EAAI,MAAA,EAAO,KAAA,EAAM,EAAA,EAAG,GAAA,EAAI,CAAA;AAAA,oBACpDA,GAAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,2CAAA,EAA4C;AAAA,GAAA,EACtD,CAAA;AAEJ;AAGO,SAAS,QAAQ,KAAA,EAAsB;AAC5C,EAAA,uBACEA,GAAAA,CAAC,KAAA,EAAA,EAAO,GAAG,KAAA,EACT,0BAAAA,GAAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,kBAAA,EAAmB,CAAA,EAC7B,CAAA;AAEJ;;;AC/EO,IAAM,UAAA,GAAa;AAAA;AAAA,EAExB,SAAA,EAAW,EAAA;AAAA;AAAA,EAEX,YAAA,EAAc,CAAA;AAAA;AAAA,EAEd,OAAA,EAAS,CAAA;AAAA;AAAA,EAET,UAAA,EAAY,EAAA;AAAA;AAAA,EAEZ,UAAA,EAAY,EAAA;AAAA;AAAA,EAEZ,YAAA,EAAc,CAAA;AAAA;AAAA,EAEd,QAAA,EAAU,EAAA;AAAA;AAAA,EAEV,UAAA,EAAY,+BAAA;AAAA;AAAA,EAEZ,eAAA,EAAiB,IAAA;AAAA,EACjB,KAAA,EAAO;AAAA;AAAA,IAEL,UAAA,EAAY,SAAA;AAAA;AAAA,IAEZ,MAAA,EAAQ,gEAAA;AAAA;AAAA,IAER,IAAA,EAAM,SAAA;AAAA;AAAA,IAEN,OAAA,EAAS,SAAA;AAAA;AAAA,IAET,OAAA,EAAS,SAAA;AAAA;AAAA,IAET,MAAA,EAAQ,SAAA;AAAA;AAAA,IAER,UAAA,EAAY;AAAA;AAEhB;AASO,IAAM,WAAA,GAAc;AAAA,EACzB,SAAA,EAAW,aAAA;AAAA,EACX,YAAA,EAAc,iBAAA;AAAA,EACd,OAAA,EAAS,eAAA;AAAA,EACT,UAAA,EAAY,kBAAA;AAAA,EACZ,UAAA,EAAY,oBAAA;AAAA,EACZ,YAAA,EAAc,sBAAA;AAAA,EACd,QAAA,EAAU,qBAAA;AAAA,EACV,UAAA,EAAY,cAAA;AAAA,EACZ,MAAA,EAAQ,kBAAA;AAAA,EACR,IAAA,EAAM,cAAA;AAAA,EACN,OAAA,EAAS,oBAAA;AAAA,EACT,OAAA,EAAS,oBAAA;AAAA,EACT,MAAA,EAAQ,kBAAA;AAAA,EACR,UAAA,EAAY;AACd","file":"chunk-WJIHF6PY.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","import type { ReactNode, SVGProps } from 'react';\n\n/**\n * The rail's bespoke icon set — shipped WITH the rail so no editor has to reach\n * for its own icon library (the storefront uses simple-icons, the admin portal\n * heroicons; neither is this rail's source of truth). The five view glyphs are\n * the exact paths the storefront editor's rail already draws, relocated here so\n * the shared rail is visually identical to its reference; `AddIcon` is new.\n *\n * Pure SVG, no interactivity, no browser globals — these render fine inside a\n * React Server Component. Sized 18×18 by default (the storefront's rail size);\n * pass `size` to override, or let `.es-rail-btn svg` in rail.css drive it.\n */\nexport type RailIconProps = Omit<SVGProps<SVGSVGElement>, 'children'> & { size?: number };\n\nfunction Glyph({ size = 18, children, ...rest }: RailIconProps & { children: ReactNode }) {\n return (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.7}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n {...rest}\n >\n {children}\n </svg>\n );\n}\n\n/** Pages — a document with a folded corner and text lines. */\nexport function PagesIcon(props: RailIconProps) {\n return (\n <Glyph {...props}>\n <path d=\"M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z\" />\n <path d=\"M14 3v5h5\" />\n <path d=\"M9 13h6M9 17h6\" />\n </Glyph>\n );\n}\n\n/** Layers — stacked sheets (the section / component tree). */\nexport function LayersIcon(props: RailIconProps) {\n return (\n <Glyph {...props}>\n <path d=\"M12 3l9 5-9 5-9-5 9-5z\" />\n <path d=\"M3 13l9 5 9-5\" />\n </Glyph>\n );\n}\n\n/** Styles — a paint droplet with palette dots (site styles). */\nexport function StylesIcon(props: RailIconProps) {\n return (\n <Glyph {...props}>\n <path d=\"M12 3c-4.5 0-8 3.4-8 7.6 0 3 2.2 4.9 4.8 4.9 1 0 1.6.7 1.6 1.5 0 .5-.3.9-.3 1.4 0 .9.8 1.6 1.9 1.6 4.4 0 8-3.6 8-8C20 6.4 16.5 3 12 3z\" />\n <circle cx=\"8.5\" cy=\"10\" r=\"0.6\" fill=\"currentColor\" />\n <circle cx=\"12\" cy=\"7.8\" r=\"0.6\" fill=\"currentColor\" />\n <circle cx=\"15.5\" cy=\"10\" r=\"0.6\" fill=\"currentColor\" />\n </Glyph>\n );\n}\n\n/** Media — an image frame with a sun and a mountain (the media library). */\nexport function MediaIcon(props: RailIconProps) {\n return (\n <Glyph {...props}>\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\" />\n <circle cx=\"8.5\" cy=\"9.5\" r=\"1.5\" />\n <path d=\"M21 16l-5-5-6 6\" />\n </Glyph>\n );\n}\n\n/** Sitemap — a root node branching to two page nodes (the visual sitemap). */\nexport function SitemapIcon(props: RailIconProps) {\n return (\n <Glyph {...props}>\n <rect x=\"9\" y=\"3\" width=\"6\" height=\"4.5\" rx=\"1\" />\n <rect x=\"3\" y=\"16.5\" width=\"6\" height=\"4.5\" rx=\"1\" />\n <rect x=\"15\" y=\"16.5\" width=\"6\" height=\"4.5\" rx=\"1\" />\n <path d=\"M12 7.5v4M6 16.5v-2.5h12v2.5M18 16.5v-2.5\" />\n </Glyph>\n );\n}\n\n/** Add — the quick-add \"+\", drawn in the same weight and cap style as the set. */\nexport function AddIcon(props: RailIconProps) {\n return (\n <Glyph {...props}>\n <path d=\"M12 5v14M5 12h14\" />\n </Glyph>\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"]}