editor-shell 0.7.0 → 0.8.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
@@ -153,9 +153,12 @@ subpath is framework-free and imports nothing, which is why the leaf can use it;
153
153
  `react-os-shell` is declared as an **optional** peer dependency, needed only if
154
154
  you import `editor-shell/gold`.
155
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
156
+ **Two deliberate limits.** The asterisks are FADED, never removed. They go to
157
+ nothing while the box is unfocused, so a merchant who is not typing does not read
158
+ `**` in their own page text, and they come back dimmed while the caret is in the
159
+ box — but the characters stay in the layer at their full width in both states.
160
+ Actually removing them needs a second text engine, and is out of scope. And the
161
+ layer only paints what cannot move a glyph: colour, opacity, decoration, a
159
162
  faux-bold shadow. A real `font-weight: 600` is wider, so a marked line would wrap
160
163
  earlier than the transparent box on top of it and every following line would be
161
164
  drawn over the wrong text. The real weight and slant appear on the page the
@@ -86,7 +86,7 @@ var shellTokens = {
86
86
  scrollbar: "#d1d5db",
87
87
  /** `--es-scrollbar-hover` — gray-400. */
88
88
  scrollbarHover: "#9ca3af",
89
- // ── Danger / warning (shell red / amber) ──────────────────────────────────
89
+ // ── Danger / warning / success (shell red / amber / green) ────────────────
90
90
  /** `--es-danger` — red-600. */
91
91
  danger: "#dc2626",
92
92
  /** `--es-danger-strong` — red-700. */
@@ -97,6 +97,17 @@ var shellTokens = {
97
97
  warning: "#b45309",
98
98
  /** `--es-preview-tint` — amber-100 "Preview" pill. */
99
99
  previewTint: "#fef3c7",
100
+ /**
101
+ * `--es-success` — green-700. THE ONE VALUE HERE WITH NO `--sf-*` BEHIND IT:
102
+ * the storefront editor's block has a red and an amber and no green, and card
103
+ * 8802.g needs one for a green tick. So it is picked to sit with the block
104
+ * rather than invented next to it — the same Tailwind ramp every other colour
105
+ * in this file comes from, at the 700 step `--es-warning` already sits on.
106
+ * 700 and not 600 for the reason amber went there: it is the darkest step
107
+ * still plainly the hue, and it clears WCAG AA (5.0:1) on the shell's white
108
+ * panels, so a tick may carry a label without needing a second token.
109
+ */
110
+ success: "#15803d",
100
111
  // ── Radii / shape / gap ───────────────────────────────────────────────────
101
112
  /** `--es-panel-radius`. */
102
113
  panelRadius: 12,
@@ -161,6 +172,7 @@ var shellCssVars = {
161
172
  dangerTint: "--es-danger-tint",
162
173
  warning: "--es-warning",
163
174
  previewTint: "--es-preview-tint",
175
+ success: "--es-success",
164
176
  panelRadius: "--es-panel-radius",
165
177
  rowRadius: "--es-row-radius",
166
178
  fieldRadius: "--es-field-radius",
@@ -185,5 +197,5 @@ var puckAzureRamp = {
185
197
  };
186
198
 
187
199
  export { puckAzureRamp, shellCssVars, shellTokens };
188
- //# sourceMappingURL=chunk-N7NW4W3P.js.map
189
- //# sourceMappingURL=chunk-N7NW4W3P.js.map
200
+ //# sourceMappingURL=chunk-ML5SEFU4.js.map
201
+ //# sourceMappingURL=chunk-ML5SEFU4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shell/tokens.ts"],"names":[],"mappings":"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","file":"chunk-ML5SEFU4.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/success, radii, gap, shadows and font. Every\n * value bar one is 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 * The one exception is `success`, which that block has no counterpart for; see\n * its own note below.\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 / success (shell red / amber / green) ────────────────\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 * `--es-success` — green-700. THE ONE VALUE HERE WITH NO `--sf-*` BEHIND IT:\n * the storefront editor's block has a red and an amber and no green, and card\n * 8802.g needs one for a green tick. So it is picked to sit with the block\n * rather than invented next to it — the same Tailwind ramp every other colour\n * in this file comes from, at the 700 step `--es-warning` already sits on.\n * 700 and not 600 for the reason amber went there: it is the darkest step\n * still plainly the hue, and it clears WCAG AA (5.0:1) on the shell's white\n * panels, so a tick may carry a label without needing a second token.\n */\n success: '#15803d',\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 success: '--es-success',\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"]}
@@ -104,9 +104,24 @@
104
104
 
105
105
  /* — the marks — */
106
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. */
107
+ /*
108
+ * Quiet unless you are IN the box (card 8802.h). A merchant who is not typing
109
+ * should not read `**` and `==` sitting inside their own page text — it looks
110
+ * like a bug. While the caret is in here they are what is being aimed at, so
111
+ * `:focus-within` brings them back at the token dim.
112
+ *
113
+ * FADED, never removed, and done here rather than in React for the same reason.
114
+ * The characters stay in the layer at their full width: dropping a `**` would
115
+ * shift every following glyph two columns off its twin in the transparent box on
116
+ * top, and hiding them for real needs a second text engine. Opacity moves
117
+ * nothing, which is the only reason this is allowed at all — and it needs no
118
+ * state, so focusing the box does not re-render it for a change that is paint.
119
+ */
109
120
  .es-gold .es-gold-delim {
121
+ opacity: 0;
122
+ }
123
+
124
+ .es-gold:focus-within .es-gold-delim {
110
125
  opacity: var(--es-gold-delim-opacity);
111
126
  }
112
127
 
@@ -197,7 +197,10 @@ declare const goldTokens: {
197
197
  readonly caret: "var(--es-text, #111827)";
198
198
  /** The hint shown while the value is empty. */
199
199
  readonly placeholder: "var(--es-field-placeholder, #9ca3af)";
200
- /** How far the asterisks are dimmed. They stay VISIBLE that is the deal. */
200
+ /** How far the asterisks are dimmed WHILE THE BOX HAS FOCUS. Out of focus they
201
+ * are faded out altogether (`gold.css`, `:focus-within`, card 8802.h) — but
202
+ * FADED, never removed: the characters stay in the layer at their full width,
203
+ * which is the deal. Anything that took them out would move a glyph. */
201
204
  readonly delimiterOpacity: "0.35";
202
205
  /**
203
206
  * Faux-bold. A real `font-weight: 600` is wider, and a wider run down here
@@ -169,7 +169,10 @@ var goldTokens = {
169
169
  caret: "var(--es-text, #111827)",
170
170
  /** The hint shown while the value is empty. */
171
171
  placeholder: "var(--es-field-placeholder, #9ca3af)",
172
- /** How far the asterisks are dimmed. They stay VISIBLE that is the deal. */
172
+ /** How far the asterisks are dimmed WHILE THE BOX HAS FOCUS. Out of focus they
173
+ * are faded out altogether (`gold.css`, `:focus-within`, card 8802.h) — but
174
+ * FADED, never removed: the characters stay in the layer at their full width,
175
+ * which is the deal. Anything that took them out would move a glyph. */
173
176
  delimiterOpacity: "0.35",
174
177
  /**
175
178
  * Faux-bold. A real `font-weight: 600` is wider, and a wider run down here
@@ -1 +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"]}
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 WHILE THE BOX HAS FOCUS. Out of focus they\n * are faded out altogether (`gold.css`, `:focus-within`, card 8802.h) — but\n * FADED, never removed: the characters stay in the layer at their full width,\n * which is the deal. Anything that took them out would move a glyph. */\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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { EditorRail, EditorRailButton, railCssVars, railTokens } from './chunk-3VMFPQGZ.js';
2
2
  export { AddIcon, LayersIcon, MediaIcon, PageIcon as PagesIcon, SitemapIcon, StylesIcon } from './chunk-LG7TJWKE.js';
3
- export { puckAzureRamp, shellCssVars, shellTokens } from './chunk-N7NW4W3P.js';
3
+ export { puckAzureRamp, shellCssVars, shellTokens } from './chunk-ML5SEFU4.js';
4
4
  //# sourceMappingURL=index.js.map
5
5
  //# sourceMappingURL=index.js.map
@@ -3,11 +3,13 @@
3
3
  *
4
4
  * Where `rail/tokens.ts` owns the slim icon-rail, this owns the rest of the
5
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
6
+ * chrome literals, danger/warning/success, radii, gap, shadows and font. Every
7
+ * value bar one is taken verbatim from the storefront editor's `--sf-*` block
8
8
  * (`efficient-shop/components/storefront-editor.css`), the visual reference the
9
9
  * campaign designer (`--cd-*`) and the storefront's own TS mirror (`tk`) had
10
10
  * each re-copied. After card 66024 they all resolve to these values instead.
11
+ * The one exception is `success`, which that block has no counterpart for; see
12
+ * its own note below.
11
13
  *
12
14
  * Exposed BOTH ways, per the same value (mirrors the rail):
13
15
  * - `shellTokens` — this JS object, for code that needs the numbers/colours
@@ -110,6 +112,17 @@ declare const shellTokens: {
110
112
  readonly warning: "#b45309";
111
113
  /** `--es-preview-tint` — amber-100 "Preview" pill. */
112
114
  readonly previewTint: "#fef3c7";
115
+ /**
116
+ * `--es-success` — green-700. THE ONE VALUE HERE WITH NO `--sf-*` BEHIND IT:
117
+ * the storefront editor's block has a red and an amber and no green, and card
118
+ * 8802.g needs one for a green tick. So it is picked to sit with the block
119
+ * rather than invented next to it — the same Tailwind ramp every other colour
120
+ * in this file comes from, at the 700 step `--es-warning` already sits on.
121
+ * 700 and not 600 for the reason amber went there: it is the darkest step
122
+ * still plainly the hue, and it clears WCAG AA (5.0:1) on the shell's white
123
+ * panels, so a tick may carry a label without needing a second token.
124
+ */
125
+ readonly success: "#15803d";
113
126
  /** `--es-panel-radius`. */
114
127
  readonly panelRadius: 12;
115
128
  /** `--es-row-radius`. */
@@ -1,3 +1,3 @@
1
- export { puckAzureRamp, shellCssVars, shellTokens } from '../chunk-N7NW4W3P.js';
1
+ export { puckAzureRamp, shellCssVars, shellTokens } from '../chunk-ML5SEFU4.js';
2
2
  //# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
@@ -73,12 +73,13 @@
73
73
  --es-scrollbar: #d1d5db;
74
74
  --es-scrollbar-hover: #9ca3af;
75
75
 
76
- /* — danger / warning (shell red / amber) — */
76
+ /* — danger / warning / success (shell red / amber / green) — */
77
77
  --es-danger: #dc2626;
78
78
  --es-danger-strong: #b91c1c;
79
79
  --es-danger-tint: #fef2f2;
80
80
  --es-warning: #b45309;
81
81
  --es-preview-tint: #fef3c7;
82
+ --es-success: #15803d;
82
83
 
83
84
  /* — radii / shape / gap — */
84
85
  --es-panel-radius: 12px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "editor-shell",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Shared editor-chrome primitives for the EFFICIENT editors: EditorRail (a Next-16-safe left icon-rail leaf), the shell token layer, and GoldTextInput — type in place and watch the markup formatting appear as you type.",
5
5
  "license": "MIT",
6
6
  "author": "Lewis Liu",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/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"]}