editor-shell 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -105,6 +105,62 @@ properties on `.es-rail`). Taken verbatim from the storefront editor's `.sf-rail
105
105
  Re-theme without new CSS by overriding a var on the element:
106
106
  `style={{ ['--es-rail-accent']: brand }}`.
107
107
 
108
+ ## The gold layer — type in place
109
+
110
+ `GoldTextInput` (subpath `editor-shell/gold`, added in 0.3.0) is the surface a
111
+ merchant types INTO on the page, with the markup formatting appearing as they
112
+ type. A textarea whose own text is transparent sits on top of a layer holding the
113
+ same characters, painted; what you read is the layer, what the caret walks is the
114
+ box. Both inherit their type from whatever the host renders them inside, so the
115
+ glyphs land on top of each other.
116
+
117
+ It is **controlled and writes nothing** — no document, no sections, no Puck. The
118
+ host owns the string and decides what a finished edit means.
119
+
120
+ ```tsx
121
+ 'use client';
122
+ import { GoldTextInput } from 'editor-shell/gold';
123
+ import { STOREFRONT_MARKUP } from 'react-os-shell/markup';
124
+ import 'editor-shell/gold.css'; // once, from a client/global entry
125
+
126
+ <GoldTextInput
127
+ value={draft}
128
+ rules={STOREFRONT_MARKUP}
129
+ placeholder="Add a heading"
130
+ ariaLabel="Section heading"
131
+ onInput={setDraft}
132
+ onCommit={(value) => save(value)} // blur, or Enter on a single-line field
133
+ onCancel={(restored) => setDraft(restored)}
134
+ />;
135
+ ```
136
+
137
+ | prop | type | default | notes |
138
+ |---|---|---|---|
139
+ | `value` | `string` | — | the authored string, delimiters and all |
140
+ | `onInput` | `(value: string) => void` | — | every keystroke; the layer repaints from what you send back |
141
+ | `onCommit` | `(value: string) => void` | — | blur, or Enter when `multiline` is false. Once per session, never if nothing was typed |
142
+ | `onCancel` | `(restored: string) => void` | — | Escape. The argument is the value held at focus time; the host puts it back |
143
+ | `multiline` | `boolean` | `false` | what Enter means, **not** how the text wraps — both modes wrap |
144
+ | `placeholder` | `string` | — | visible until the first keystroke |
145
+ | `className` | `string` | — | merged onto `.es-gold` |
146
+ | `rules` | `readonly InlineRule[]` | `STANDARD_MARKUP` | pass your product's set — `STOREFRONT_MARKUP` / `CAMPAIGN_MARKUP` |
147
+ | `ariaLabel` | `string` | — | in-place editing has no visible label |
148
+
149
+ **The grammar is not ours.** Runs are parsed by `react-os-shell/markup`, the same
150
+ module the published page and the campaign email render from, so a toolbar
151
+ button, a page and this box can never disagree about what a delimiter means. That
152
+ subpath is framework-free and imports nothing, which is why the leaf can use it;
153
+ `react-os-shell` is declared as an **optional** peer dependency, needed only if
154
+ you import `editor-shell/gold`.
155
+
156
+ **Two deliberate limits.** The asterisks stay visible (dimmed) rather than
157
+ disappearing — hiding them needs a second text engine, and is out of scope. And
158
+ the layer only paints what cannot move a glyph: colour, opacity, decoration, a
159
+ faux-bold shadow. A real `font-weight: 600` is wider, so a marked line would wrap
160
+ earlier than the transparent box on top of it and every following line would be
161
+ drawn over the wrong text. The real weight and slant appear on the page the
162
+ moment the merchant clicks away.
163
+
108
164
  ## Develop
109
165
 
110
166
  No Node on the dev Mac — everything runs in Docker:
@@ -115,5 +171,8 @@ docker run --rm -v "$PWD":/w -w /w node:26-slim \
115
171
  ```
116
172
 
117
173
  - `npm run typecheck` — `tsc` over `src` and the specs.
118
- - `npm test` — the leaf-safety gate.
119
- - `npm run build` `tsup` `dist/` (root + `rail` subpath) + the copied CSS.
174
+ - `npm test` — the leaf-safety, gold-layer and rail-shape gates. The gold-layer
175
+ spec is the one that needs a DOM (focus, keys, blur); it stands up a single
176
+ jsdom window in `tests/_dom-env.ts` and runs in its own process.
177
+ - `npm run build` — `tsup` → `dist/` (root + the `rail`, `shell` and `gold`
178
+ subpaths) + the copied CSS.
@@ -0,0 +1,189 @@
1
+ // src/shell/tokens.ts
2
+ var shellTokens = {
3
+ // ── Accent (react-os-shell blue) ──────────────────────────────────────────
4
+ /** `--es-accent` — blue-600. */
5
+ accent: "#2563eb",
6
+ /** `--es-accent-tint` — blue-50. */
7
+ accentTint: "#eff6ff",
8
+ /** `--es-accent-tint-strong` — blue-100. */
9
+ accentTintStrong: "#dbeafe",
10
+ /** `--es-accent-text` — blue-700. */
11
+ accentText: "#1d4ed8",
12
+ /** `--es-accent-ring` — focus ring wash. */
13
+ accentRing: "rgba(37, 99, 235, 0.25)",
14
+ /** `--es-accent-shadow` — accent button shadow. */
15
+ accentShadow: "rgba(37, 99, 235, 0.35)",
16
+ /** `--es-on-accent` — text/icon over the solid accent. */
17
+ onAccent: "#ffffff",
18
+ // ── Surfaces (cool gray ramp) ─────────────────────────────────────────────
19
+ /** `--es-app-bg` — gray-50, sunken canvas. */
20
+ appBg: "#f9fafb",
21
+ /** `--es-preview-bg` — gray-100, preview well. */
22
+ previewBg: "#f3f4f6",
23
+ /** `--es-panel` — raised cards / panels. */
24
+ panel: "#ffffff",
25
+ /** `--es-raised` — active seg / chip. */
26
+ raised: "#ffffff",
27
+ /** `--es-topbar-bg`. */
28
+ topbarBg: "#ffffff",
29
+ /** `--es-frame-bg` — device "paper" behind the preview iframe. */
30
+ frameBg: "#ffffff",
31
+ /** `--es-hover` — gray-100 row / icon hover. */
32
+ hover: "#f3f4f6",
33
+ /** `--es-hatch` — 45° upload-slot texture. */
34
+ hatch: "rgba(0, 0, 0, 0.018)",
35
+ /** `--es-addcard-veil` — add-section card hover wash. */
36
+ addcardVeil: "rgba(248, 249, 250, 0.66)",
37
+ // ── Fields ────────────────────────────────────────────────────────────────
38
+ /** `--es-field-bg`. */
39
+ fieldBg: "#ffffff",
40
+ /** `--es-track-bg` — gray-100 segmented / search track. */
41
+ trackBg: "#f3f4f6",
42
+ /** `--es-input-border` — gray-300. */
43
+ inputBorder: "#d1d5db",
44
+ /** `--es-field-text` — gray-800. */
45
+ fieldText: "#1f2937",
46
+ /** `--es-field-placeholder` — gray-400. */
47
+ fieldPlaceholder: "#9ca3af",
48
+ /** `--es-knob` — toggle knob. */
49
+ knob: "#ffffff",
50
+ // ── Text ────────────────────────────────────────────────────────────────
51
+ /** `--es-text` — gray-900. */
52
+ text: "#111827",
53
+ /** `--es-text-strong` — gray-950 active labels. */
54
+ textStrong: "#030712",
55
+ /** `--es-text-secondary` — gray-700. */
56
+ textSecondary: "#374151",
57
+ /** `--es-muted` — gray-500. */
58
+ muted: "#6b7280",
59
+ /** `--es-icon` — gray-500 icon buttons. */
60
+ icon: "#6b7280",
61
+ /** `--es-disabled` — gray-400 disabled / faint glyphs. */
62
+ disabled: "#9ca3af",
63
+ // ── Lines / chips / buttons ──────────────────────────────────────────────
64
+ /** `--es-divider` — gray-200. */
65
+ divider: "#e5e7eb",
66
+ /** `--es-line` — gray-200 hairlines. */
67
+ line: "#e5e7eb",
68
+ /** `--es-btn-border` — gray-300. */
69
+ btnBorder: "#d1d5db",
70
+ /** `--es-btn-text` — gray-700. */
71
+ btnText: "#374151",
72
+ /** `--es-chip-bg` — gray-100. */
73
+ chipBg: "#f3f4f6",
74
+ /** `--es-chip-fg` — gray-500. */
75
+ chipFg: "#6b7280",
76
+ // ── Named chrome literals ─────────────────────────────────────────────────
77
+ /** `--es-brand-square` — gray-900 wordmark square. */
78
+ brandSquare: "#111827",
79
+ /** `--es-device-border` — gray-200 tablet/mobile frame. */
80
+ deviceBorder: "#e5e7eb",
81
+ /** `--es-toast-bg` — gray-900 dark chip / seam pill. */
82
+ toastBg: "#111827",
83
+ /** `--es-toggle-off` — gray-300 toggle track. */
84
+ toggleOff: "#d1d5db",
85
+ /** `--es-scrollbar` — gray-300. */
86
+ scrollbar: "#d1d5db",
87
+ /** `--es-scrollbar-hover` — gray-400. */
88
+ scrollbarHover: "#9ca3af",
89
+ // ── Danger / warning (shell red / amber) ──────────────────────────────────
90
+ /** `--es-danger` — red-600. */
91
+ danger: "#dc2626",
92
+ /** `--es-danger-strong` — red-700. */
93
+ dangerStrong: "#b91c1c",
94
+ /** `--es-danger-tint` — red-50. */
95
+ dangerTint: "#fef2f2",
96
+ /** `--es-warning` — amber-700. */
97
+ warning: "#b45309",
98
+ /** `--es-preview-tint` — amber-100 "Preview" pill. */
99
+ previewTint: "#fef3c7",
100
+ // ── Radii / shape / gap ───────────────────────────────────────────────────
101
+ /** `--es-panel-radius`. */
102
+ panelRadius: 12,
103
+ /** `--es-row-radius`. */
104
+ rowRadius: 8,
105
+ /** `--es-field-radius`. */
106
+ fieldRadius: 6,
107
+ /** `--es-panel-gap` — inter-pane gutter + outer frame padding, ONE token. */
108
+ panelGap: 10,
109
+ // ── Shadows ───────────────────────────────────────────────────────────────
110
+ /** `--es-panel-shadow`. */
111
+ panelShadow: "0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.06)",
112
+ /** `--es-topbar-shadow`. */
113
+ topbarShadow: "0 1px 3px rgba(0, 0, 0, 0.06)",
114
+ // ── Type ──────────────────────────────────────────────────────────────────
115
+ /** `--es-font` — system sans stack (also fed to `--puck-font-family`). */
116
+ font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
117
+ };
118
+ var shellCssVars = {
119
+ accent: "--es-accent",
120
+ accentTint: "--es-accent-tint",
121
+ accentTintStrong: "--es-accent-tint-strong",
122
+ accentText: "--es-accent-text",
123
+ accentRing: "--es-accent-ring",
124
+ accentShadow: "--es-accent-shadow",
125
+ onAccent: "--es-on-accent",
126
+ appBg: "--es-app-bg",
127
+ previewBg: "--es-preview-bg",
128
+ panel: "--es-panel",
129
+ raised: "--es-raised",
130
+ topbarBg: "--es-topbar-bg",
131
+ frameBg: "--es-frame-bg",
132
+ hover: "--es-hover",
133
+ hatch: "--es-hatch",
134
+ addcardVeil: "--es-addcard-veil",
135
+ fieldBg: "--es-field-bg",
136
+ trackBg: "--es-track-bg",
137
+ inputBorder: "--es-input-border",
138
+ fieldText: "--es-field-text",
139
+ fieldPlaceholder: "--es-field-placeholder",
140
+ knob: "--es-knob",
141
+ text: "--es-text",
142
+ textStrong: "--es-text-strong",
143
+ textSecondary: "--es-text-secondary",
144
+ muted: "--es-muted",
145
+ icon: "--es-icon",
146
+ disabled: "--es-disabled",
147
+ divider: "--es-divider",
148
+ line: "--es-line",
149
+ btnBorder: "--es-btn-border",
150
+ btnText: "--es-btn-text",
151
+ chipBg: "--es-chip-bg",
152
+ chipFg: "--es-chip-fg",
153
+ brandSquare: "--es-brand-square",
154
+ deviceBorder: "--es-device-border",
155
+ toastBg: "--es-toast-bg",
156
+ toggleOff: "--es-toggle-off",
157
+ scrollbar: "--es-scrollbar",
158
+ scrollbarHover: "--es-scrollbar-hover",
159
+ danger: "--es-danger",
160
+ dangerStrong: "--es-danger-strong",
161
+ dangerTint: "--es-danger-tint",
162
+ warning: "--es-warning",
163
+ previewTint: "--es-preview-tint",
164
+ panelRadius: "--es-panel-radius",
165
+ rowRadius: "--es-row-radius",
166
+ fieldRadius: "--es-field-radius",
167
+ panelGap: "--es-panel-gap",
168
+ panelShadow: "--es-panel-shadow",
169
+ topbarShadow: "--es-topbar-shadow",
170
+ font: "--es-font"
171
+ };
172
+ var puckAzureRamp = {
173
+ "--puck-color-azure-01": "#172554",
174
+ "--puck-color-azure-02": "#1e3a8a",
175
+ "--puck-color-azure-03": "#1d4ed8",
176
+ "--puck-color-azure-04": "#2563eb",
177
+ "--puck-color-azure-05": "#3b82f6",
178
+ "--puck-color-azure-06": "#60a5fa",
179
+ "--puck-color-azure-07": "#93c5fd",
180
+ "--puck-color-azure-08": "#bfdbfe",
181
+ "--puck-color-azure-09": "#dbeafe",
182
+ "--puck-color-azure-10": "#eff6ff",
183
+ "--puck-color-azure-11": "#f5f9ff",
184
+ "--puck-color-azure-12": "#fafcff"
185
+ };
186
+
187
+ export { puckAzureRamp, shellCssVars, shellTokens };
188
+ //# sourceMappingURL=chunk-N7NW4W3P.js.map
189
+ //# sourceMappingURL=chunk-N7NW4W3P.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shell/tokens.ts"],"names":[],"mappings":"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","file":"chunk-N7NW4W3P.js","sourcesContent":["/**\n * Editor SHELL design tokens — the ONE source both editors' chrome converge on.\n *\n * Where `rail/tokens.ts` owns the slim icon-rail, this owns the rest of the\n * editor chrome theme: accent, surfaces, fields, text, lines, chips, named\n * chrome literals, danger/warning, radii, gap, shadows and font. Every value is\n * taken verbatim from the storefront editor's `--sf-*` block\n * (`efficient-shop/components/storefront-editor.css`), the visual reference the\n * campaign designer (`--cd-*`) and the storefront's own TS mirror (`tk`) had\n * each re-copied. After card 66024 they all resolve to these values instead.\n *\n * Exposed BOTH ways, per the same value (mirrors the rail):\n * - `shellTokens` — this JS object, for code that needs the numbers/colours\n * (e.g. the storefront's `floating-editor/tokens.ts` inline-style mirror);\n * - `shell.css` — the stylesheet, which declares the identical values as CSS\n * custom properties on `.es-editor` (names listed in `shellCssVars`).\n *\n * Numeric fields are pixel magnitudes (unitless) so they compose in code; the\n * CSS applies the `px`. Both editing surfaces are LIGHT only (no dark mode), so\n * there is a single palette here — no dark counterpart.\n */\nexport const shellTokens = {\n // ── Accent (react-os-shell blue) ──────────────────────────────────────────\n /** `--es-accent` — blue-600. */\n accent: '#2563eb',\n /** `--es-accent-tint` — blue-50. */\n accentTint: '#eff6ff',\n /** `--es-accent-tint-strong` — blue-100. */\n accentTintStrong: '#dbeafe',\n /** `--es-accent-text` — blue-700. */\n accentText: '#1d4ed8',\n /** `--es-accent-ring` — focus ring wash. */\n accentRing: 'rgba(37, 99, 235, 0.25)',\n /** `--es-accent-shadow` — accent button shadow. */\n accentShadow: 'rgba(37, 99, 235, 0.35)',\n /** `--es-on-accent` — text/icon over the solid accent. */\n onAccent: '#ffffff',\n\n // ── Surfaces (cool gray ramp) ─────────────────────────────────────────────\n /** `--es-app-bg` — gray-50, sunken canvas. */\n appBg: '#f9fafb',\n /** `--es-preview-bg` — gray-100, preview well. */\n previewBg: '#f3f4f6',\n /** `--es-panel` — raised cards / panels. */\n panel: '#ffffff',\n /** `--es-raised` — active seg / chip. */\n raised: '#ffffff',\n /** `--es-topbar-bg`. */\n topbarBg: '#ffffff',\n /** `--es-frame-bg` — device \"paper\" behind the preview iframe. */\n frameBg: '#ffffff',\n /** `--es-hover` — gray-100 row / icon hover. */\n hover: '#f3f4f6',\n /** `--es-hatch` — 45° upload-slot texture. */\n hatch: 'rgba(0, 0, 0, 0.018)',\n /** `--es-addcard-veil` — add-section card hover wash. */\n addcardVeil: 'rgba(248, 249, 250, 0.66)',\n\n // ── Fields ────────────────────────────────────────────────────────────────\n /** `--es-field-bg`. */\n fieldBg: '#ffffff',\n /** `--es-track-bg` — gray-100 segmented / search track. */\n trackBg: '#f3f4f6',\n /** `--es-input-border` — gray-300. */\n inputBorder: '#d1d5db',\n /** `--es-field-text` — gray-800. */\n fieldText: '#1f2937',\n /** `--es-field-placeholder` — gray-400. */\n fieldPlaceholder: '#9ca3af',\n /** `--es-knob` — toggle knob. */\n knob: '#ffffff',\n\n // ── Text ────────────────────────────────────────────────────────────────\n /** `--es-text` — gray-900. */\n text: '#111827',\n /** `--es-text-strong` — gray-950 active labels. */\n textStrong: '#030712',\n /** `--es-text-secondary` — gray-700. */\n textSecondary: '#374151',\n /** `--es-muted` — gray-500. */\n muted: '#6b7280',\n /** `--es-icon` — gray-500 icon buttons. */\n icon: '#6b7280',\n /** `--es-disabled` — gray-400 disabled / faint glyphs. */\n disabled: '#9ca3af',\n\n // ── Lines / chips / buttons ──────────────────────────────────────────────\n /** `--es-divider` — gray-200. */\n divider: '#e5e7eb',\n /** `--es-line` — gray-200 hairlines. */\n line: '#e5e7eb',\n /** `--es-btn-border` — gray-300. */\n btnBorder: '#d1d5db',\n /** `--es-btn-text` — gray-700. */\n btnText: '#374151',\n /** `--es-chip-bg` — gray-100. */\n chipBg: '#f3f4f6',\n /** `--es-chip-fg` — gray-500. */\n chipFg: '#6b7280',\n\n // ── Named chrome literals ─────────────────────────────────────────────────\n /** `--es-brand-square` — gray-900 wordmark square. */\n brandSquare: '#111827',\n /** `--es-device-border` — gray-200 tablet/mobile frame. */\n deviceBorder: '#e5e7eb',\n /** `--es-toast-bg` — gray-900 dark chip / seam pill. */\n toastBg: '#111827',\n /** `--es-toggle-off` — gray-300 toggle track. */\n toggleOff: '#d1d5db',\n /** `--es-scrollbar` — gray-300. */\n scrollbar: '#d1d5db',\n /** `--es-scrollbar-hover` — gray-400. */\n scrollbarHover: '#9ca3af',\n\n // ── Danger / warning (shell red / amber) ──────────────────────────────────\n /** `--es-danger` — red-600. */\n danger: '#dc2626',\n /** `--es-danger-strong` — red-700. */\n dangerStrong: '#b91c1c',\n /** `--es-danger-tint` — red-50. */\n dangerTint: '#fef2f2',\n /** `--es-warning` — amber-700. */\n warning: '#b45309',\n /** `--es-preview-tint` — amber-100 \"Preview\" pill. */\n previewTint: '#fef3c7',\n\n // ── Radii / shape / gap ───────────────────────────────────────────────────\n /** `--es-panel-radius`. */\n panelRadius: 12,\n /** `--es-row-radius`. */\n rowRadius: 8,\n /** `--es-field-radius`. */\n fieldRadius: 6,\n /** `--es-panel-gap` — inter-pane gutter + outer frame padding, ONE token. */\n panelGap: 10,\n\n // ── Shadows ───────────────────────────────────────────────────────────────\n /** `--es-panel-shadow`. */\n panelShadow: '0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.06)',\n /** `--es-topbar-shadow`. */\n topbarShadow: '0 1px 3px rgba(0, 0, 0, 0.06)',\n\n // ── Type ──────────────────────────────────────────────────────────────────\n /** `--es-font` — system sans stack (also fed to `--puck-font-family`). */\n font: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif',\n} as const;\n\nexport type ShellTokens = typeof shellTokens;\n\n/**\n * The CSS custom-property name behind each `shellTokens` key. `shell.css` sets\n * these on `.es-editor`; override any of them on (or above) the editor root to\n * re-theme without shipping new CSS. The `shell-tokens.test.ts` parity gate\n * proves this map, `shellTokens` and `shell.css` never drift apart.\n */\nexport const shellCssVars: Record<keyof ShellTokens, string> = {\n accent: '--es-accent',\n accentTint: '--es-accent-tint',\n accentTintStrong: '--es-accent-tint-strong',\n accentText: '--es-accent-text',\n accentRing: '--es-accent-ring',\n accentShadow: '--es-accent-shadow',\n onAccent: '--es-on-accent',\n appBg: '--es-app-bg',\n previewBg: '--es-preview-bg',\n panel: '--es-panel',\n raised: '--es-raised',\n topbarBg: '--es-topbar-bg',\n frameBg: '--es-frame-bg',\n hover: '--es-hover',\n hatch: '--es-hatch',\n addcardVeil: '--es-addcard-veil',\n fieldBg: '--es-field-bg',\n trackBg: '--es-track-bg',\n inputBorder: '--es-input-border',\n fieldText: '--es-field-text',\n fieldPlaceholder: '--es-field-placeholder',\n knob: '--es-knob',\n text: '--es-text',\n textStrong: '--es-text-strong',\n textSecondary: '--es-text-secondary',\n muted: '--es-muted',\n icon: '--es-icon',\n disabled: '--es-disabled',\n divider: '--es-divider',\n line: '--es-line',\n btnBorder: '--es-btn-border',\n btnText: '--es-btn-text',\n chipBg: '--es-chip-bg',\n chipFg: '--es-chip-fg',\n brandSquare: '--es-brand-square',\n deviceBorder: '--es-device-border',\n toastBg: '--es-toast-bg',\n toggleOff: '--es-toggle-off',\n scrollbar: '--es-scrollbar',\n scrollbarHover: '--es-scrollbar-hover',\n danger: '--es-danger',\n dangerStrong: '--es-danger-strong',\n dangerTint: '--es-danger-tint',\n warning: '--es-warning',\n previewTint: '--es-preview-tint',\n panelRadius: '--es-panel-radius',\n rowRadius: '--es-row-radius',\n fieldRadius: '--es-field-radius',\n panelGap: '--es-panel-gap',\n panelShadow: '--es-panel-shadow',\n topbarShadow: '--es-topbar-shadow',\n font: '--es-font',\n};\n\n/**\n * Puck's own accent ramp + font, re-tinted to the shell blue. Puck reads these\n * fixed variable names (`--puck-color-azure-NN`, `--puck-font-family`) directly,\n * so they are NOT part of the `--es-*` set — but they had been copied verbatim\n * into BOTH editors' CSS, so `shell.css` now declares them once on `.es-editor`.\n * Kept here as the JS record so the parity gate can prove `shell.css` matches.\n */\nexport const puckAzureRamp: Record<string, string> = {\n '--puck-color-azure-01': '#172554',\n '--puck-color-azure-02': '#1e3a8a',\n '--puck-color-azure-03': '#1d4ed8',\n '--puck-color-azure-04': '#2563eb',\n '--puck-color-azure-05': '#3b82f6',\n '--puck-color-azure-06': '#60a5fa',\n '--puck-color-azure-07': '#93c5fd',\n '--puck-color-azure-08': '#bfdbfe',\n '--puck-color-azure-09': '#dbeafe',\n '--puck-color-azure-10': '#eff6ff',\n '--puck-color-azure-11': '#f5f9ff',\n '--puck-color-azure-12': '#fafcff',\n};\n"]}
@@ -0,0 +1,133 @@
1
+ /*
2
+ * editor-shell — gold-layer styles (card 66088).
3
+ *
4
+ * The values mirror `tokens.ts` one-for-one and are declared ON `.es-gold`, not
5
+ * on `:root`, so importing this sheet themes only the boxes and never leaks a
6
+ * variable into the page being edited.
7
+ *
8
+ * THE ONE RULE THIS FILE EXISTS TO ENFORCE: the layer and the box must resolve
9
+ * to the SAME type metrics. Everything that can move a glyph — family, size,
10
+ * weight, style, line-height, letter-spacing, word-spacing, wrapping, padding —
11
+ * is inherited by both from whatever the host renders them inside, and is set
12
+ * identically on both below. A property added to one of them and not the other
13
+ * pulls the two surfaces apart, and the caret stops sitting under the character
14
+ * it is next to. There is no third place to change it.
15
+ *
16
+ * Import once from a client entry / global stylesheet:
17
+ * import 'editor-shell/gold.css';
18
+ */
19
+
20
+ .es-gold {
21
+ /* — tokens (see tokens.ts / goldCssVars) — */
22
+ --es-gold-accent: var(--gold, #c9a461);
23
+ --es-gold-caret: var(--es-text, #111827);
24
+ --es-gold-placeholder: var(--es-field-placeholder, #9ca3af);
25
+ --es-gold-delim-opacity: 0.35;
26
+ --es-gold-bold-shadow: 0 0 0.4px currentColor;
27
+ --es-gold-italic-decoration: underline dotted;
28
+ --es-gold-strike-decoration: line-through;
29
+
30
+ position: relative;
31
+ display: block;
32
+ }
33
+
34
+ /* The shared type contract. Read the note at the top before touching it. */
35
+ .es-gold .es-gold-layer,
36
+ .es-gold .es-gold-input,
37
+ .es-gold .es-gold-placeholder {
38
+ font: inherit;
39
+ line-height: inherit;
40
+ letter-spacing: inherit;
41
+ word-spacing: inherit;
42
+ text-align: inherit;
43
+ text-indent: inherit;
44
+ text-transform: inherit;
45
+ tab-size: inherit;
46
+ white-space: pre-wrap;
47
+ overflow-wrap: break-word;
48
+ word-break: normal;
49
+ margin: 0;
50
+ padding: 0;
51
+ border: 0;
52
+ }
53
+
54
+ /*
55
+ * The layer is the element IN FLOW: it holds the same characters as the box, so
56
+ * it wraps to the same height, so the box grows with the text and nothing has to
57
+ * be measured.
58
+ */
59
+ .es-gold .es-gold-layer {
60
+ position: relative;
61
+ color: inherit;
62
+ pointer-events: none;
63
+ overflow: hidden;
64
+ }
65
+
66
+ /*
67
+ * A zero-width space, drawn by CSS so it is NOT part of `textContent` and cannot
68
+ * break the character-for-character promise. It buys two things: an empty layer
69
+ * still has a line box (so an empty field is one line tall, not zero), and a
70
+ * value ending in a newline still shows that last, empty line — which a trailing
71
+ * "\n" in a text node does not.
72
+ */
73
+ .es-gold .es-gold-layer::after {
74
+ content: '\200b';
75
+ }
76
+
77
+ .es-gold .es-gold-input {
78
+ position: absolute;
79
+ inset: 0;
80
+ width: 100%;
81
+ height: 100%;
82
+ display: block;
83
+ background: transparent;
84
+ /* The text is invisible; the layer underneath is what you read. */
85
+ color: transparent;
86
+ caret-color: var(--es-gold-caret);
87
+ resize: none;
88
+ overflow: hidden;
89
+ outline: none;
90
+ }
91
+
92
+ /* The hint is painted by the sibling below, in the layer's own metrics. Drawing
93
+ * it here as well would double it. */
94
+ .es-gold .es-gold-input::placeholder {
95
+ color: transparent;
96
+ }
97
+
98
+ .es-gold .es-gold-placeholder {
99
+ position: absolute;
100
+ inset: 0;
101
+ color: var(--es-gold-placeholder);
102
+ pointer-events: none;
103
+ }
104
+
105
+ /* — the marks — */
106
+
107
+ /* The asterisks stay visible, just quieter. Removing them would need a second
108
+ * text engine to keep the caret honest; dimming them costs nothing. */
109
+ .es-gold .es-gold-delim {
110
+ opacity: var(--es-gold-delim-opacity);
111
+ }
112
+
113
+ .es-gold .es-gold-accent {
114
+ color: var(--es-gold-accent);
115
+ font-style: normal;
116
+ }
117
+
118
+ /* Weight and slant are NOT applied here — see tokens.ts. Both would change a
119
+ * glyph's advance, and the transparent box on top would wrap somewhere else. */
120
+ .es-gold .es-gold-bold {
121
+ /* `<strong>`'s own bold, undone — the shadow is what draws it instead. */
122
+ font-weight: inherit;
123
+ text-shadow: var(--es-gold-bold-shadow);
124
+ }
125
+
126
+ .es-gold .es-gold-italic {
127
+ font-style: normal;
128
+ text-decoration: var(--es-gold-italic-decoration);
129
+ }
130
+
131
+ .es-gold .es-gold-strike {
132
+ text-decoration: var(--es-gold-strike-decoration);
133
+ }
@@ -0,0 +1,223 @@
1
+ import * as react from 'react';
2
+ import { Ref } from 'react';
3
+ import { InlineRule, InlineKind } from 'react-os-shell/markup';
4
+
5
+ /**
6
+ * The type-in-place box: a controlled leaf, and NOTHING else.
7
+ *
8
+ * It holds no string of its own, knows no document, and performs no write. The
9
+ * host owns the value and decides what a commit means — which is what lets the
10
+ * same component serve the storefront editor and the campaign designer without
11
+ * either one leaking into it.
12
+ */
13
+ interface GoldTextInputProps {
14
+ /** The authored string, delimiters and all. The single source of truth. */
15
+ value: string;
16
+ /**
17
+ * Every keystroke. The layer repaints from what the host sends back, so a host
18
+ * that drops this on the floor gets a box that does not type — deliberately:
19
+ * one value, one owner.
20
+ */
21
+ onInput: (value: string) => void;
22
+ /**
23
+ * The edit is finished and may be written: on blur, and on Enter when
24
+ * {@link multiline} is false. Fires ONCE per editing session, and never at all
25
+ * if nothing was typed — clicking into a part and back out must not dirty the
26
+ * page (owner ruling 6), and one session is one undo step (ruling 2).
27
+ */
28
+ onCommit?: (value: string) => void;
29
+ /**
30
+ * Escape. The argument is the value the component was given when it gained
31
+ * focus; the HOST puts it back, because the host owns the string. After a
32
+ * cancel the pending edit is forgotten, so the blur that follows writes
33
+ * nothing.
34
+ */
35
+ onCancel?: (restored: string) => void;
36
+ /**
37
+ * What Enter means — NOT how the text wraps. Both modes soft-wrap exactly as
38
+ * the box does, because the layer under it has to wrap identically.
39
+ *
40
+ * - `false` (default): Enter commits and never inserts a newline;
41
+ * - `true`: Enter inserts a newline and commits nothing.
42
+ */
43
+ multiline?: boolean;
44
+ /** Shown while the value is empty — through the first focus, and away on the
45
+ * first keystroke. */
46
+ placeholder?: string;
47
+ /** Extra class(es) merged onto the wrapper. */
48
+ className?: string;
49
+ /**
50
+ * The grammar's rule set — `STOREFRONT_MARKUP` in the shop,
51
+ * `CAMPAIGN_MARKUP` in the campaign designer, both from
52
+ * `react-os-shell/markup`. Defaults to `STANDARD_MARKUP`, which is the subset
53
+ * every product shares; a product with legacy runs (the storefront's
54
+ * `*phrase*` accent) MUST pass its own set or those runs draw as plain text
55
+ * here while the page paints them gold.
56
+ */
57
+ rules?: readonly InlineRule[];
58
+ /** Accessible name for the box. In-place editing has no visible label. */
59
+ ariaLabel?: string;
60
+ }
61
+ interface GoldLayerProps {
62
+ value: string;
63
+ rules?: readonly InlineRule[];
64
+ className?: string;
65
+ /**
66
+ * A handle on the layer element.
67
+ *
68
+ * A plain prop rather than `ref` on purpose: React 18 would need
69
+ * `forwardRef` and React 19 takes `ref` as an ordinary prop, and this package
70
+ * supports both (`peerDependencies: react >=18`). One named prop behaves
71
+ * identically on either and needs no version branch.
72
+ */
73
+ layerRef?: Ref<HTMLDivElement>;
74
+ }
75
+
76
+ /**
77
+ * Type where the text sits, and watch the gold appear as you type.
78
+ *
79
+ * The arrangement, in one paragraph: a textarea whose own text is TRANSPARENT
80
+ * sits on top of a layer holding the same characters, painted. What you read is
81
+ * the layer; what the caret walks is the box. Both take their type styles from
82
+ * whatever the host renders them inside (`font: inherit` all the way down), so
83
+ * the glyphs land on top of each other instead of near each other, and the box
84
+ * inherits the page's own type rather than a size this package invented.
85
+ *
86
+ * The layer is the element IN FLOW and the box is absolutely positioned over it,
87
+ * which is deliberate: the layer holds the same characters, so it wraps to the
88
+ * same height, so the box grows as the merchant types without anyone measuring
89
+ * anything.
90
+ *
91
+ * WHAT IT DOES NOT DO: write. It has no document, no section, no Puck, no shop.
92
+ * `onCommit` says "this edit is finished"; what that means is the host's
93
+ * business. That is what makes it shareable by both editors (constitution E2)
94
+ * and testable without any of them.
95
+ *
96
+ * No `'use client'` — same reason as the rail: the directive is the HOST's to
97
+ * place, and both editors' chrome are already client components. Unlike the
98
+ * rail, though, this leaf has hooks, so it must be rendered inside that client
99
+ * boundary; a server component can import the module but cannot render it.
100
+ *
101
+ * Requires `editor-shell/gold.css`, imported once from a client entry.
102
+ */
103
+ declare function GoldTextInput({ value, onInput, onCommit, onCancel, multiline, placeholder, className, rules, ariaLabel, }: GoldTextInputProps): react.JSX.Element;
104
+
105
+ /**
106
+ * The formatted text, drawn UNDER the box.
107
+ *
108
+ * It is a MARKER layer, not a preview of the page. That distinction is the whole
109
+ * design, and it is why a bold run here is not drawn at weight 600:
110
+ *
111
+ * the caret, the selection and the line breaks all come from the transparent
112
+ * textarea on top, which has ONE font. A heavier or slanted face down here is
113
+ * wider, so a marked line would wrap a word earlier than the box does and
114
+ * every line after it would sit on top of the wrong text.
115
+ *
116
+ * So the layer may only paint what cannot move a glyph — colour, opacity,
117
+ * text-decoration, a shadow. A merchant sees WHERE the formatting starts and
118
+ * ends and in which colour it will land; the real weight and slant appear on the
119
+ * page the moment they click away. Full WYSIWYG (asterisks gone, real faces) is
120
+ * a second text engine and is deliberately out of scope — see CLAUDE.md.
121
+ *
122
+ * The elements mirror the storefront's own renderer (`goldPhrases.tsx`):
123
+ * `<strong>` for bold, `<em>` for italic and for the gold accent, `<s>` for
124
+ * struck-out. The classes carry the paint; the tokens carry the values.
125
+ */
126
+ declare function GoldLayer({ value, rules, className, layerRef }: GoldLayerProps): react.JSX.Element;
127
+
128
+ /**
129
+ * Cutting an authored string into the pieces the layer paints — WITHOUT losing a
130
+ * character.
131
+ *
132
+ * The grammar is not re-implemented here and never will be (constitution E8):
133
+ * {@link tokenizeInline} from `react-os-shell/markup` decides what is a run and
134
+ * what is not, exactly as it does for the published page. What it does not
135
+ * return is the delimiters — it hands back a run's INNER text, because every
136
+ * other consumer wants the words without the asterisks.
137
+ *
138
+ * This layer wants the asterisks. It draws underneath a transparent box whose
139
+ * caret walks the stored string, so the two surfaces must hold the same
140
+ * characters in the same order: drop the `**` and every glyph after it sits two
141
+ * columns left of the caret that is supposed to be inside it.
142
+ *
143
+ * So each parsed run is located back in the source and cut into three pieces —
144
+ * opening delimiter, inner text, closing delimiter — and EVERY piece is sliced
145
+ * out of the original string rather than rebuilt from the token. Nothing is
146
+ * retyped, so nothing can be retyped wrong.
147
+ */
148
+ /** A piece of the source: a parsed run's kind, or the delimiters around one. */
149
+ type GoldSegmentKind = InlineKind | 'delimiter';
150
+ interface GoldSegment {
151
+ kind: GoldSegmentKind;
152
+ /** Verbatim source text. Concatenating every segment reproduces the input. */
153
+ text: string;
154
+ }
155
+ /**
156
+ * Split `value` into paint-able segments under `rules`.
157
+ *
158
+ * GUARANTEE: `segments.map(s => s.text).join('') === value`, always. If the walk
159
+ * below cannot line a run up with the rule that produced it — which would mean
160
+ * the grammar and this function disagree — the whole string comes back as one
161
+ * plain `text` segment. Losing the gold is a disappointment; losing a character
162
+ * puts the caret in the wrong place, so the fallback is never in doubt.
163
+ */
164
+ declare function goldSegments(value: string, rules: readonly InlineRule[]): GoldSegment[];
165
+
166
+ /**
167
+ * Gold-layer paint tokens.
168
+ *
169
+ * SHORT LIST BY DESIGN. The layer draws inside whatever the host renders it in
170
+ * and inherits that context's type entirely — family, size, weight, line-height,
171
+ * letter-spacing — because it has to sit on top of a box that inherits the same
172
+ * (see `GoldTextInput`). So there is no type here to tokenise; what is left is
173
+ * the paint, and only the paint that cannot move a glyph.
174
+ *
175
+ * NOTHING HERE IS INVENTED (CLAUDE.md — "the tokens are not ours to invent"):
176
+ * - the gold defers to the page's own `--gold`, falling back to the
177
+ * storefront's light-theme value (`efficient-shop/app/globals.css` → `--gold:
178
+ * #c9a461`), so the mark in the box matches the mark on the page;
179
+ * - the caret and the placeholder defer to the shell layer's `--es-text` /
180
+ * `--es-field-placeholder` (see `shell/tokens.ts`) with the same values as
181
+ * their fallbacks;
182
+ * - the dim is the rail's existing disabled opacity (0.35), not a new number.
183
+ *
184
+ * Every value is a STRING, unlike the rail's and shell's pixel magnitudes:
185
+ * none of these is a length, so nothing here gains a unit on the way into CSS.
186
+ *
187
+ * Exposed BOTH ways, per the same value: `goldTokens` (this object) and
188
+ * `gold.css` (the stylesheet, which declares them on `.es-gold` — names in
189
+ * {@link goldCssVars}). The parity gate in `tests/gold-leaf-safety.test.ts`
190
+ * proves the two never drift.
191
+ */
192
+ declare const goldTokens: {
193
+ /** The mark colour — `==phrase==` and the legacy `*phrase*`. */
194
+ readonly accent: "var(--gold, #c9a461)";
195
+ /** The caret. Set explicitly because the box's own text is transparent, and
196
+ * `caret-color: auto` would make the caret transparent with it. */
197
+ readonly caret: "var(--es-text, #111827)";
198
+ /** The hint shown while the value is empty. */
199
+ readonly placeholder: "var(--es-field-placeholder, #9ca3af)";
200
+ /** How far the asterisks are dimmed. They stay VISIBLE — that is the deal. */
201
+ readonly delimiterOpacity: "0.35";
202
+ /**
203
+ * Faux-bold. A real `font-weight: 600` is wider, and a wider run down here
204
+ * wraps a line earlier than the transparent box on top of it — after which
205
+ * every following line is drawn over the wrong text. The shadow reads heavier
206
+ * and moves nothing.
207
+ */
208
+ readonly boldShadow: "0 0 0.4px currentColor";
209
+ /** Italic, marked rather than slanted — an italic FACE has its own widths, and
210
+ * the layer may not change a glyph's advance. Same reason as the bold. */
211
+ readonly italicDecoration: "underline dotted";
212
+ /** Strike-through is metric-safe, so it is drawn exactly as it will print. */
213
+ readonly strikeDecoration: "line-through";
214
+ };
215
+ type GoldTokens = typeof goldTokens;
216
+ /**
217
+ * The CSS custom-property name behind each token. `gold.css` sets these on
218
+ * `.es-gold`, so overriding one on (or above) the box re-themes it without
219
+ * shipping new CSS — e.g. `style={{ ['--es-gold-accent']: brand }}`.
220
+ */
221
+ declare const goldCssVars: Record<keyof GoldTokens, string>;
222
+
223
+ export { GoldLayer, type GoldLayerProps, type GoldSegment, type GoldSegmentKind, GoldTextInput, type GoldTextInputProps, type GoldTokens, goldCssVars, goldSegments, goldTokens };
@@ -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"]}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { AddIcon, EditorRail, EditorRailButton, EditorRailButtonProps, EditorRailItem, EditorRailProps, LayersIcon, MediaIcon, PagesIcon, RailIconProps, RailTokens, SitemapIcon, StylesIcon, railCssVars, railTokens } from './rail/index.js';
2
+ export { ShellTokens, puckAzureRamp, shellCssVars, shellTokens } from './shell/index.js';
2
3
  import 'react';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { puckAzureRamp, shellCssVars, shellTokens } from './chunk-N7NW4W3P.js';
1
2
  export { AddIcon, EditorRail, EditorRailButton, LayersIcon, MediaIcon, PagesIcon, SitemapIcon, StylesIcon, railCssVars, railTokens } from './chunk-WJIHF6PY.js';
2
3
  //# sourceMappingURL=index.js.map
3
4
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Editor SHELL design tokens — the ONE source both editors' chrome converge on.
3
+ *
4
+ * Where `rail/tokens.ts` owns the slim icon-rail, this owns the rest of the
5
+ * editor chrome theme: accent, surfaces, fields, text, lines, chips, named
6
+ * chrome literals, danger/warning, radii, gap, shadows and font. Every value is
7
+ * taken verbatim from the storefront editor's `--sf-*` block
8
+ * (`efficient-shop/components/storefront-editor.css`), the visual reference the
9
+ * campaign designer (`--cd-*`) and the storefront's own TS mirror (`tk`) had
10
+ * each re-copied. After card 66024 they all resolve to these values instead.
11
+ *
12
+ * Exposed BOTH ways, per the same value (mirrors the rail):
13
+ * - `shellTokens` — this JS object, for code that needs the numbers/colours
14
+ * (e.g. the storefront's `floating-editor/tokens.ts` inline-style mirror);
15
+ * - `shell.css` — the stylesheet, which declares the identical values as CSS
16
+ * custom properties on `.es-editor` (names listed in `shellCssVars`).
17
+ *
18
+ * Numeric fields are pixel magnitudes (unitless) so they compose in code; the
19
+ * CSS applies the `px`. Both editing surfaces are LIGHT only (no dark mode), so
20
+ * there is a single palette here — no dark counterpart.
21
+ */
22
+ declare const shellTokens: {
23
+ /** `--es-accent` — blue-600. */
24
+ readonly accent: "#2563eb";
25
+ /** `--es-accent-tint` — blue-50. */
26
+ readonly accentTint: "#eff6ff";
27
+ /** `--es-accent-tint-strong` — blue-100. */
28
+ readonly accentTintStrong: "#dbeafe";
29
+ /** `--es-accent-text` — blue-700. */
30
+ readonly accentText: "#1d4ed8";
31
+ /** `--es-accent-ring` — focus ring wash. */
32
+ readonly accentRing: "rgba(37, 99, 235, 0.25)";
33
+ /** `--es-accent-shadow` — accent button shadow. */
34
+ readonly accentShadow: "rgba(37, 99, 235, 0.35)";
35
+ /** `--es-on-accent` — text/icon over the solid accent. */
36
+ readonly onAccent: "#ffffff";
37
+ /** `--es-app-bg` — gray-50, sunken canvas. */
38
+ readonly appBg: "#f9fafb";
39
+ /** `--es-preview-bg` — gray-100, preview well. */
40
+ readonly previewBg: "#f3f4f6";
41
+ /** `--es-panel` — raised cards / panels. */
42
+ readonly panel: "#ffffff";
43
+ /** `--es-raised` — active seg / chip. */
44
+ readonly raised: "#ffffff";
45
+ /** `--es-topbar-bg`. */
46
+ readonly topbarBg: "#ffffff";
47
+ /** `--es-frame-bg` — device "paper" behind the preview iframe. */
48
+ readonly frameBg: "#ffffff";
49
+ /** `--es-hover` — gray-100 row / icon hover. */
50
+ readonly hover: "#f3f4f6";
51
+ /** `--es-hatch` — 45° upload-slot texture. */
52
+ readonly hatch: "rgba(0, 0, 0, 0.018)";
53
+ /** `--es-addcard-veil` — add-section card hover wash. */
54
+ readonly addcardVeil: "rgba(248, 249, 250, 0.66)";
55
+ /** `--es-field-bg`. */
56
+ readonly fieldBg: "#ffffff";
57
+ /** `--es-track-bg` — gray-100 segmented / search track. */
58
+ readonly trackBg: "#f3f4f6";
59
+ /** `--es-input-border` — gray-300. */
60
+ readonly inputBorder: "#d1d5db";
61
+ /** `--es-field-text` — gray-800. */
62
+ readonly fieldText: "#1f2937";
63
+ /** `--es-field-placeholder` — gray-400. */
64
+ readonly fieldPlaceholder: "#9ca3af";
65
+ /** `--es-knob` — toggle knob. */
66
+ readonly knob: "#ffffff";
67
+ /** `--es-text` — gray-900. */
68
+ readonly text: "#111827";
69
+ /** `--es-text-strong` — gray-950 active labels. */
70
+ readonly textStrong: "#030712";
71
+ /** `--es-text-secondary` — gray-700. */
72
+ readonly textSecondary: "#374151";
73
+ /** `--es-muted` — gray-500. */
74
+ readonly muted: "#6b7280";
75
+ /** `--es-icon` — gray-500 icon buttons. */
76
+ readonly icon: "#6b7280";
77
+ /** `--es-disabled` — gray-400 disabled / faint glyphs. */
78
+ readonly disabled: "#9ca3af";
79
+ /** `--es-divider` — gray-200. */
80
+ readonly divider: "#e5e7eb";
81
+ /** `--es-line` — gray-200 hairlines. */
82
+ readonly line: "#e5e7eb";
83
+ /** `--es-btn-border` — gray-300. */
84
+ readonly btnBorder: "#d1d5db";
85
+ /** `--es-btn-text` — gray-700. */
86
+ readonly btnText: "#374151";
87
+ /** `--es-chip-bg` — gray-100. */
88
+ readonly chipBg: "#f3f4f6";
89
+ /** `--es-chip-fg` — gray-500. */
90
+ readonly chipFg: "#6b7280";
91
+ /** `--es-brand-square` — gray-900 wordmark square. */
92
+ readonly brandSquare: "#111827";
93
+ /** `--es-device-border` — gray-200 tablet/mobile frame. */
94
+ readonly deviceBorder: "#e5e7eb";
95
+ /** `--es-toast-bg` — gray-900 dark chip / seam pill. */
96
+ readonly toastBg: "#111827";
97
+ /** `--es-toggle-off` — gray-300 toggle track. */
98
+ readonly toggleOff: "#d1d5db";
99
+ /** `--es-scrollbar` — gray-300. */
100
+ readonly scrollbar: "#d1d5db";
101
+ /** `--es-scrollbar-hover` — gray-400. */
102
+ readonly scrollbarHover: "#9ca3af";
103
+ /** `--es-danger` — red-600. */
104
+ readonly danger: "#dc2626";
105
+ /** `--es-danger-strong` — red-700. */
106
+ readonly dangerStrong: "#b91c1c";
107
+ /** `--es-danger-tint` — red-50. */
108
+ readonly dangerTint: "#fef2f2";
109
+ /** `--es-warning` — amber-700. */
110
+ readonly warning: "#b45309";
111
+ /** `--es-preview-tint` — amber-100 "Preview" pill. */
112
+ readonly previewTint: "#fef3c7";
113
+ /** `--es-panel-radius`. */
114
+ readonly panelRadius: 12;
115
+ /** `--es-row-radius`. */
116
+ readonly rowRadius: 8;
117
+ /** `--es-field-radius`. */
118
+ readonly fieldRadius: 6;
119
+ /** `--es-panel-gap` — inter-pane gutter + outer frame padding, ONE token. */
120
+ readonly panelGap: 10;
121
+ /** `--es-panel-shadow`. */
122
+ readonly panelShadow: "0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.06)";
123
+ /** `--es-topbar-shadow`. */
124
+ readonly topbarShadow: "0 1px 3px rgba(0, 0, 0, 0.06)";
125
+ /** `--es-font` — system sans stack (also fed to `--puck-font-family`). */
126
+ readonly font: "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif";
127
+ };
128
+ type ShellTokens = typeof shellTokens;
129
+ /**
130
+ * The CSS custom-property name behind each `shellTokens` key. `shell.css` sets
131
+ * these on `.es-editor`; override any of them on (or above) the editor root to
132
+ * re-theme without shipping new CSS. The `shell-tokens.test.ts` parity gate
133
+ * proves this map, `shellTokens` and `shell.css` never drift apart.
134
+ */
135
+ declare const shellCssVars: Record<keyof ShellTokens, string>;
136
+ /**
137
+ * Puck's own accent ramp + font, re-tinted to the shell blue. Puck reads these
138
+ * fixed variable names (`--puck-color-azure-NN`, `--puck-font-family`) directly,
139
+ * so they are NOT part of the `--es-*` set — but they had been copied verbatim
140
+ * into BOTH editors' CSS, so `shell.css` now declares them once on `.es-editor`.
141
+ * Kept here as the JS record so the parity gate can prove `shell.css` matches.
142
+ */
143
+ declare const puckAzureRamp: Record<string, string>;
144
+
145
+ export { type ShellTokens, puckAzureRamp, shellCssVars, shellTokens };
@@ -0,0 +1,3 @@
1
+ export { puckAzureRamp, shellCssVars, shellTokens } from '../chunk-N7NW4W3P.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,112 @@
1
+ /*
2
+ * editor-shell — shell chrome tokens.
3
+ *
4
+ * The values here mirror `shellTokens` in tokens.ts one-for-one (both trace back
5
+ * to the storefront editor's `--sf-*` block — the visual reference). They are
6
+ * declared as custom properties ON `.es-editor`, not on `:root`, so importing
7
+ * this stylesheet themes ONLY an editor root and never leaks variables into the
8
+ * host page (the campaign designer renders INSIDE the admin DOM, so this must
9
+ * not touch the surrounding app).
10
+ *
11
+ * Both editors add the `es-editor` class to their chrome root and alias their
12
+ * own `--sf-*` / `--cd-*` names onto these — e.g. `--sf-accent: var(--es-accent)`
13
+ * — so the light values live ONCE, here. Re-theme by overriding any `--es-*` var
14
+ * on or above the editor root.
15
+ *
16
+ * LIGHT only: both editing surfaces always render light (no dark mode), so there
17
+ * is no dark counterpart block — do not add one.
18
+ *
19
+ * Import once from a client entry / global stylesheet:
20
+ * import 'editor-shell/shell.css';
21
+ */
22
+
23
+ .es-editor {
24
+ /* — accent (react-os-shell blue) — */
25
+ --es-accent: #2563eb;
26
+ --es-accent-tint: #eff6ff;
27
+ --es-accent-tint-strong: #dbeafe;
28
+ --es-accent-text: #1d4ed8;
29
+ --es-accent-ring: rgba(37, 99, 235, 0.25);
30
+ --es-accent-shadow: rgba(37, 99, 235, 0.35);
31
+ --es-on-accent: #ffffff;
32
+
33
+ /* — surfaces (cool gray ramp) — */
34
+ --es-app-bg: #f9fafb;
35
+ --es-preview-bg: #f3f4f6;
36
+ --es-panel: #ffffff;
37
+ --es-raised: #ffffff;
38
+ --es-topbar-bg: #ffffff;
39
+ --es-frame-bg: #ffffff;
40
+ --es-hover: #f3f4f6;
41
+ --es-hatch: rgba(0, 0, 0, 0.018);
42
+ --es-addcard-veil: rgba(248, 249, 250, 0.66);
43
+
44
+ /* — fields — */
45
+ --es-field-bg: #ffffff;
46
+ --es-track-bg: #f3f4f6;
47
+ --es-input-border: #d1d5db;
48
+ --es-field-text: #1f2937;
49
+ --es-field-placeholder: #9ca3af;
50
+ --es-knob: #ffffff;
51
+
52
+ /* — text — */
53
+ --es-text: #111827;
54
+ --es-text-strong: #030712;
55
+ --es-text-secondary: #374151;
56
+ --es-muted: #6b7280;
57
+ --es-icon: #6b7280;
58
+ --es-disabled: #9ca3af;
59
+
60
+ /* — lines / chips / buttons — */
61
+ --es-divider: #e5e7eb;
62
+ --es-line: #e5e7eb;
63
+ --es-btn-border: #d1d5db;
64
+ --es-btn-text: #374151;
65
+ --es-chip-bg: #f3f4f6;
66
+ --es-chip-fg: #6b7280;
67
+
68
+ /* — named chrome literals — */
69
+ --es-brand-square: #111827;
70
+ --es-device-border: #e5e7eb;
71
+ --es-toast-bg: #111827;
72
+ --es-toggle-off: #d1d5db;
73
+ --es-scrollbar: #d1d5db;
74
+ --es-scrollbar-hover: #9ca3af;
75
+
76
+ /* — danger / warning (shell red / amber) — */
77
+ --es-danger: #dc2626;
78
+ --es-danger-strong: #b91c1c;
79
+ --es-danger-tint: #fef2f2;
80
+ --es-warning: #b45309;
81
+ --es-preview-tint: #fef3c7;
82
+
83
+ /* — radii / shape / gap — */
84
+ --es-panel-radius: 12px;
85
+ --es-row-radius: 8px;
86
+ --es-field-radius: 6px;
87
+ --es-panel-gap: 10px;
88
+
89
+ /* — shadows — */
90
+ --es-panel-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.06);
91
+ --es-topbar-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
92
+
93
+ /* — type — */
94
+ --es-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
95
+
96
+ /* — Puck's own accent ramp + font, re-tinted to the shell blue. Puck reads
97
+ these fixed names directly; declared once here so both editors stop copying
98
+ the ramp verbatim (see puckAzureRamp in tokens.ts). — */
99
+ --puck-color-azure-01: #172554;
100
+ --puck-color-azure-02: #1e3a8a;
101
+ --puck-color-azure-03: #1d4ed8;
102
+ --puck-color-azure-04: #2563eb;
103
+ --puck-color-azure-05: #3b82f6;
104
+ --puck-color-azure-06: #60a5fa;
105
+ --puck-color-azure-07: #93c5fd;
106
+ --puck-color-azure-08: #bfdbfe;
107
+ --puck-color-azure-09: #dbeafe;
108
+ --puck-color-azure-10: #eff6ff;
109
+ --puck-color-azure-11: #f5f9ff;
110
+ --puck-color-azure-12: #fafcff;
111
+ --puck-font-family: var(--es-font);
112
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "editor-shell",
3
- "version": "0.1.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.3.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,28 +28,47 @@
28
28
  "types": "./dist/rail/index.d.ts",
29
29
  "import": "./dist/rail/index.js"
30
30
  },
31
- "./rail.css": "./dist/rail/rail.css"
31
+ "./rail.css": "./dist/rail/rail.css",
32
+ "./shell": {
33
+ "types": "./dist/shell/index.d.ts",
34
+ "import": "./dist/shell/index.js"
35
+ },
36
+ "./shell.css": "./dist/shell/shell.css",
37
+ "./gold": {
38
+ "types": "./dist/gold/index.d.ts",
39
+ "import": "./dist/gold/index.js"
40
+ },
41
+ "./gold.css": "./dist/gold/gold.css"
32
42
  },
33
43
  "files": [
34
44
  "dist"
35
45
  ],
36
46
  "peerDependencies": {
37
47
  "react": ">=18",
38
- "react-dom": ">=18"
48
+ "react-dom": ">=18",
49
+ "react-os-shell": ">=4.13"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "react-os-shell": {
53
+ "optional": true
54
+ }
39
55
  },
40
56
  "dependencies": {},
41
57
  "devDependencies": {
58
+ "@types/jsdom": "^28.0.3",
42
59
  "@types/node": "^22.0.0",
43
60
  "@types/react": "^18.2.0",
44
61
  "@types/react-dom": "^18.2.0",
45
62
  "esbuild": "^0.27.0",
63
+ "jsdom": "^26.1.0",
46
64
  "react": "^18.2.0",
47
65
  "react-dom": "^18.2.0",
66
+ "react-os-shell": "^4.31.0",
48
67
  "tsup": "^8.0.0",
49
68
  "typescript": "^5.3.0"
50
69
  },
51
70
  "scripts": {
52
- "build": "tsup && cp src/rail/rail.css dist/rail/rail.css",
71
+ "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",
53
72
  "dev": "tsup --watch",
54
73
  "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
55
74
  "test": "node scripts/test.mjs",