editor-shell 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -2
- package/dist/{chunk-WJIHF6PY.js → chunk-3VMFPQGZ.js} +4 -61
- package/dist/chunk-3VMFPQGZ.js.map +1 -0
- package/dist/chunk-UB3KPBFP.js +1263 -0
- package/dist/chunk-UB3KPBFP.js.map +1 -0
- package/dist/gold/gold.css +133 -0
- package/dist/gold/index.d.ts +223 -0
- package/dist/gold/index.js +199 -0
- package/dist/gold/index.js.map +1 -0
- package/dist/icons/index.d.ts +189 -0
- package/dist/icons/index.js +3 -0
- package/dist/icons/index.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/rail/index.d.ts +3 -29
- package/dist/rail/index.js +2 -1
- package/package.json +23 -5
- package/dist/chunk-WJIHF6PY.js.map +0 -1
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
|
|
119
|
-
|
|
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.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx
|
|
1
|
+
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
|
|
3
3
|
// src/rail/EditorRailButton.tsx
|
|
4
4
|
function EditorRailButton({
|
|
@@ -46,63 +46,6 @@ function EditorRail({ items, ariaLabel = "Editor", className }) {
|
|
|
46
46
|
}
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
|
-
function Glyph({ size = 18, children, ...rest }) {
|
|
50
|
-
return /* @__PURE__ */ jsx(
|
|
51
|
-
"svg",
|
|
52
|
-
{
|
|
53
|
-
width: size,
|
|
54
|
-
height: size,
|
|
55
|
-
viewBox: "0 0 24 24",
|
|
56
|
-
fill: "none",
|
|
57
|
-
stroke: "currentColor",
|
|
58
|
-
strokeWidth: 1.7,
|
|
59
|
-
strokeLinecap: "round",
|
|
60
|
-
strokeLinejoin: "round",
|
|
61
|
-
"aria-hidden": "true",
|
|
62
|
-
...rest,
|
|
63
|
-
children
|
|
64
|
-
}
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
function PagesIcon(props) {
|
|
68
|
-
return /* @__PURE__ */ jsxs(Glyph, { ...props, children: [
|
|
69
|
-
/* @__PURE__ */ jsx("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" }),
|
|
70
|
-
/* @__PURE__ */ jsx("path", { d: "M14 3v5h5" }),
|
|
71
|
-
/* @__PURE__ */ jsx("path", { d: "M9 13h6M9 17h6" })
|
|
72
|
-
] });
|
|
73
|
-
}
|
|
74
|
-
function LayersIcon(props) {
|
|
75
|
-
return /* @__PURE__ */ jsxs(Glyph, { ...props, children: [
|
|
76
|
-
/* @__PURE__ */ jsx("path", { d: "M12 3l9 5-9 5-9-5 9-5z" }),
|
|
77
|
-
/* @__PURE__ */ jsx("path", { d: "M3 13l9 5 9-5" })
|
|
78
|
-
] });
|
|
79
|
-
}
|
|
80
|
-
function StylesIcon(props) {
|
|
81
|
-
return /* @__PURE__ */ jsxs(Glyph, { ...props, children: [
|
|
82
|
-
/* @__PURE__ */ jsx("path", { d: "M12 3c-4.5 0-8 3.4-8 7.6 0 3 2.2 4.9 4.8 4.9 1 0 1.6.7 1.6 1.5 0 .5-.3.9-.3 1.4 0 .9.8 1.6 1.9 1.6 4.4 0 8-3.6 8-8C20 6.4 16.5 3 12 3z" }),
|
|
83
|
-
/* @__PURE__ */ jsx("circle", { cx: "8.5", cy: "10", r: "0.6", fill: "currentColor" }),
|
|
84
|
-
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "7.8", r: "0.6", fill: "currentColor" }),
|
|
85
|
-
/* @__PURE__ */ jsx("circle", { cx: "15.5", cy: "10", r: "0.6", fill: "currentColor" })
|
|
86
|
-
] });
|
|
87
|
-
}
|
|
88
|
-
function MediaIcon(props) {
|
|
89
|
-
return /* @__PURE__ */ jsxs(Glyph, { ...props, children: [
|
|
90
|
-
/* @__PURE__ */ jsx("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }),
|
|
91
|
-
/* @__PURE__ */ jsx("circle", { cx: "8.5", cy: "9.5", r: "1.5" }),
|
|
92
|
-
/* @__PURE__ */ jsx("path", { d: "M21 16l-5-5-6 6" })
|
|
93
|
-
] });
|
|
94
|
-
}
|
|
95
|
-
function SitemapIcon(props) {
|
|
96
|
-
return /* @__PURE__ */ jsxs(Glyph, { ...props, children: [
|
|
97
|
-
/* @__PURE__ */ jsx("rect", { x: "9", y: "3", width: "6", height: "4.5", rx: "1" }),
|
|
98
|
-
/* @__PURE__ */ jsx("rect", { x: "3", y: "16.5", width: "6", height: "4.5", rx: "1" }),
|
|
99
|
-
/* @__PURE__ */ jsx("rect", { x: "15", y: "16.5", width: "6", height: "4.5", rx: "1" }),
|
|
100
|
-
/* @__PURE__ */ jsx("path", { d: "M12 7.5v4M6 16.5v-2.5h12v2.5M18 16.5v-2.5" })
|
|
101
|
-
] });
|
|
102
|
-
}
|
|
103
|
-
function AddIcon(props) {
|
|
104
|
-
return /* @__PURE__ */ jsx(Glyph, { ...props, children: /* @__PURE__ */ jsx("path", { d: "M12 5v14M5 12h14" }) });
|
|
105
|
-
}
|
|
106
49
|
|
|
107
50
|
// src/rail/tokens.ts
|
|
108
51
|
var railTokens = {
|
|
@@ -158,6 +101,6 @@ var railCssVars = {
|
|
|
158
101
|
accentTint: "--es-rail-accent-tint"
|
|
159
102
|
};
|
|
160
103
|
|
|
161
|
-
export {
|
|
162
|
-
//# sourceMappingURL=chunk-
|
|
163
|
-
//# sourceMappingURL=chunk-
|
|
104
|
+
export { EditorRail, EditorRailButton, railCssVars, railTokens };
|
|
105
|
+
//# sourceMappingURL=chunk-3VMFPQGZ.js.map
|
|
106
|
+
//# sourceMappingURL=chunk-3VMFPQGZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rail/EditorRailButton.tsx","../src/rail/EditorRail.tsx","../src/rail/tokens.ts"],"names":["jsx"],"mappings":"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","file":"chunk-3VMFPQGZ.js","sourcesContent":["import type { EditorRailButtonProps } from './types';\n\n/**\n * A single rail button: an icon in a 34×34 square that hovers, highlights when\n * active, and dims when disabled. Look only — the caller owns the click.\n *\n * `aria-pressed` is emitted **only** when `active` is a real boolean, so a view\n * toggle announces its pressed state while an action button (the quick-add \"+\",\n * whose item omits `active`) stays a plain button rather than a stuck toggle.\n */\nexport function EditorRailButton({\n label,\n icon,\n active,\n disabled,\n onSelect,\n className,\n}: EditorRailButtonProps) {\n const classes = ['es-rail-btn', active ? 'is-active' : '', className ?? '']\n .filter(Boolean)\n .join(' ');\n\n return (\n <button\n type=\"button\"\n className={classes}\n title={label}\n aria-label={label}\n aria-pressed={typeof active === 'boolean' ? active : undefined}\n disabled={disabled}\n onClick={disabled ? undefined : onSelect}\n >\n {icon}\n </button>\n );\n}\n","import { EditorRailButton } from './EditorRailButton';\nimport type { EditorRailProps } from './types';\n\n/**\n * The shared left icon-rail — one slim vertical strip of icon buttons that every\n * EFFICIENT editor uses to switch panels (Pages / Layers / Styles / Media /\n * Sitemap) and to quick-add (\"+\"). It is a **pure presentational switcher**: give\n * it `items[]`, it draws them; which one is `active` and what each does is the\n * host's business.\n *\n * No `'use client'` here on purpose — the rail has no hooks or state, so it is a\n * plain module a Next-16 server component can import and even statically render.\n * It's interactive only through the `onSelect` props the host passes, and the\n * host owns its client boundary (both editors' chrome are already client\n * components), so baking a directive into this leaf would only narrow where it\n * can be imported.\n *\n * A vertical `toolbar` (not a `tablist`), because the rail mixes view *toggles*\n * with plain *actions* like the quick-add — a toolbar of buttons models that\n * honestly, where a tablist would force every button to pretend to be a tab.\n */\nexport function EditorRail({ items, ariaLabel = 'Editor', className }: EditorRailProps) {\n return (\n <div\n className={['es-rail', className ?? ''].filter(Boolean).join(' ')}\n role=\"toolbar\"\n aria-orientation=\"vertical\"\n aria-label={ariaLabel}\n >\n {items.map((item) => (\n <EditorRailButton\n key={item.id}\n label={item.label}\n icon={item.icon}\n active={item.active}\n disabled={item.disabled}\n onSelect={item.onSelect}\n />\n ))}\n </div>\n );\n}\n","/**\n * Rail design tokens — the ONE source both apps' rails converge on.\n *\n * Every value below is taken verbatim from the storefront editor's rail\n * (`efficient-shop` → `storefront-editor.css`, the `.sf-rail` / `.sf-rail-btn`\n * rules), which is the visual reference. The admin portal's hand-rolled rail had\n * drifted (button 36 vs 34, icon 20 vs 18, radius 8 vs 9, a Tailwind accent\n * instead of a CSS var); these tokens end that drift.\n *\n * Exposed BOTH ways, per the same value:\n * - `railTokens` — this JS object, for anything that needs the numbers/colours\n * in code (inline styles, a theme bridge, a Storybook control);\n * - `rail.css` — the stylesheet, which declares the identical values as CSS\n * custom properties on `.es-rail` (names listed in `railCssVars`).\n *\n * Numeric fields are pixel magnitudes (unitless) so they compose in code; the\n * CSS applies the `px`.\n */\nexport const railTokens = {\n /** Rail column width. `--sf-rail-w: 46px`. */\n railWidth: 46,\n /** Vertical padding inside the rail (top === bottom). */\n railPaddingY: 8,\n /** Gap between buttons. */\n railGap: 4,\n /** Rail card corner radius. `--sf-panel-radius: 12px`. */\n railRadius: 12,\n /** Button hit-area (square). */\n buttonSize: 34,\n /** Button corner radius. */\n buttonRadius: 9,\n /** Icon glyph size. */\n iconSize: 18,\n /** Hover/active colour transition. */\n transition: 'background 0.12s, color 0.12s',\n /** Disabled-button opacity. */\n disabledOpacity: 0.35,\n color: {\n /** Rail card background. `--sf-panel`. */\n background: '#ffffff',\n /** Rail card shadow. `--sf-panel-shadow`. */\n shadow: '0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.06)',\n /** Idle icon colour. `--sf-muted` (gray-500). */\n idle: '#6b7280',\n /** Hover background. `--sf-hover` (gray-100). */\n hoverBg: '#f3f4f6',\n /** Hover icon colour. `--sf-text` (gray-900). */\n hoverFg: '#111827',\n /** Active icon colour. `--sf-accent` (blue-600). */\n accent: '#2563eb',\n /** Active background. `--sf-accent-tint` (blue-50). */\n accentTint: '#eff6ff',\n },\n} as const;\n\nexport type RailTokens = typeof railTokens;\n\n/**\n * The CSS custom-property name behind each token. `rail.css` sets these on\n * `.es-rail`; override any of them on (or above) the rail element to re-theme it\n * — e.g. `style={{ ['--es-rail-accent']: brand }}` — without shipping new CSS.\n */\nexport const railCssVars = {\n railWidth: '--es-rail-w',\n railPaddingY: '--es-rail-pad-y',\n railGap: '--es-rail-gap',\n railRadius: '--es-rail-radius',\n buttonSize: '--es-rail-btn-size',\n buttonRadius: '--es-rail-btn-radius',\n iconSize: '--es-rail-icon-size',\n background: '--es-rail-bg',\n shadow: '--es-rail-shadow',\n idle: '--es-rail-fg',\n hoverBg: '--es-rail-hover-bg',\n hoverFg: '--es-rail-hover-fg',\n accent: '--es-rail-accent',\n accentTint: '--es-rail-accent-tint',\n} as const;\n"]}
|