editor-shell 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lewis Liu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # editor-shell
2
+
3
+ Shared **editor-chrome** primitives for the EFFICIENT editors — the owner's own
4
+ editor shell, so every editor draws the same chrome from one source instead of
5
+ hand-rolling and drifting.
6
+
7
+ > **Status: foundation.** Ships one brick — the shared left icon-rail. Not
8
+ > published to npm; no product repo consumes it yet.
9
+
10
+ ## Why
11
+
12
+ The storefront editor (`efficient-shop`, Next 16 + Puck) and the campaign
13
+ designer (`efficient-admin-portal`, Vite/React) each hand-rolled their own narrow
14
+ left icon-rail. They had already drifted — button 34px vs 36px, icon 18px vs
15
+ 20px, radius 9 vs 8, a CSS-var accent vs a Tailwind one. `EditorRail` is the one
16
+ rail they converge on.
17
+
18
+ ## The leaf
19
+
20
+ The storefront's editor chrome renders in **Next-16 server components**, so the
21
+ rail is a **self-contained leaf** at `editor-shell/rail`: it imports nothing heavy
22
+ and touches no `document` / `window` / `localStorage` at import or render, so a
23
+ server component can pull it in safely. `tests/rail-leaf-safety.test.tsx` proves
24
+ it.
25
+
26
+ ```tsx
27
+ // A client component in any editor:
28
+ 'use client';
29
+ import {
30
+ EditorRail,
31
+ PagesIcon, LayersIcon, StylesIcon, MediaIcon, SitemapIcon, AddIcon,
32
+ } from 'editor-shell/rail';
33
+ import 'editor-shell/rail.css'; // once, from a client/global entry
34
+
35
+ export function LeftRail({ view, onView, onAdd }) {
36
+ return (
37
+ <EditorRail
38
+ ariaLabel="Left panel views"
39
+ items={[
40
+ { id: 'pages', label: 'Pages', icon: <PagesIcon />, active: view === 'pages', onSelect: () => onView('pages') },
41
+ { id: 'layers', label: 'Layers', icon: <LayersIcon />, active: view === 'layers', onSelect: () => onView('layers') },
42
+ { id: 'styles', label: 'Styles', icon: <StylesIcon />, active: view === 'styles', onSelect: () => onView('styles') },
43
+ { id: 'media', label: 'Media', icon: <MediaIcon />, onSelect: onOpenMedia },
44
+ { id: 'add', label: 'Add page', icon: <AddIcon />, onSelect: onAdd },
45
+ ]}
46
+ />
47
+ );
48
+ }
49
+ ```
50
+
51
+ ## API
52
+
53
+ ### `<EditorRail items ariaLabel? className? />`
54
+
55
+ A **pure presentational** vertical toolbar. It owns the look, never the app
56
+ state — you decide which item is `active` and what each `onSelect` does.
57
+
58
+ | prop | type | default | notes |
59
+ |---|---|---|---|
60
+ | `items` | `EditorRailItem[]` | — | buttons, top→bottom, in render order |
61
+ | `ariaLabel` | `string` | `"Editor"` | names the toolbar for assistive tech |
62
+ | `className` | `string` | — | merged onto `.es-rail` |
63
+
64
+ ### `EditorRailItem`
65
+
66
+ ```ts
67
+ interface EditorRailItem {
68
+ id: string; // React key + selection correlation
69
+ label: string; // aria-label + hover title
70
+ icon: React.ReactNode; // an 18px currentColor glyph — use the shipped icons
71
+ active?: boolean; // present ⇒ view toggle (renders aria-pressed); omit for actions
72
+ disabled?: boolean;
73
+ onSelect?: () => void;
74
+ }
75
+ ```
76
+
77
+ A **view item** passes `active: true | false` and renders as a toggle
78
+ (`aria-pressed`, `.is-active` highlight). An **action item** (the quick-add "+")
79
+ omits `active` and renders as a plain button.
80
+
81
+ `EditorRailButton` — the single-button building block `EditorRail` maps to — is
82
+ exported too, for bespoke layouts.
83
+
84
+ ## Tokens
85
+
86
+ The same values, two ways — `railTokens` (JS) and `rail.css` (CSS custom
87
+ properties on `.es-rail`). Taken verbatim from the storefront editor's `.sf-rail`
88
+ (the visual reference).
89
+
90
+ | token | value | CSS var |
91
+ |---|---|---|
92
+ | rail width | `46px` | `--es-rail-w` |
93
+ | rail padding (y) | `8px` | `--es-rail-pad-y` |
94
+ | button gap | `4px` | `--es-rail-gap` |
95
+ | rail radius | `12px` | `--es-rail-radius` |
96
+ | button size | `34px` | `--es-rail-btn-size` |
97
+ | button radius | `9px` | `--es-rail-btn-radius` |
98
+ | icon size | `18px` | `--es-rail-icon-size` |
99
+ | background | `#ffffff` | `--es-rail-bg` |
100
+ | idle icon | `#6b7280` | `--es-rail-fg` |
101
+ | hover bg / fg | `#f3f4f6` / `#111827` | `--es-rail-hover-bg` / `--es-rail-hover-fg` |
102
+ | accent (active) | `#2563eb` | `--es-rail-accent` |
103
+ | accent tint (active bg) | `#eff6ff` | `--es-rail-accent-tint` |
104
+
105
+ Re-theme without new CSS by overriding a var on the element:
106
+ `style={{ ['--es-rail-accent']: brand }}`.
107
+
108
+ ## Develop
109
+
110
+ No Node on the dev Mac — everything runs in Docker:
111
+
112
+ ```bash
113
+ docker run --rm -v "$PWD":/w -w /w node:26-slim \
114
+ sh -lc "npm install && npm run typecheck && npm test && npm run build"
115
+ ```
116
+
117
+ - `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.
@@ -0,0 +1,163 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+
3
+ // src/rail/EditorRailButton.tsx
4
+ function EditorRailButton({
5
+ label,
6
+ icon,
7
+ active,
8
+ disabled,
9
+ onSelect,
10
+ className
11
+ }) {
12
+ const classes = ["es-rail-btn", active ? "is-active" : "", className ?? ""].filter(Boolean).join(" ");
13
+ return /* @__PURE__ */ jsx(
14
+ "button",
15
+ {
16
+ type: "button",
17
+ className: classes,
18
+ title: label,
19
+ "aria-label": label,
20
+ "aria-pressed": typeof active === "boolean" ? active : void 0,
21
+ disabled,
22
+ onClick: disabled ? void 0 : onSelect,
23
+ children: icon
24
+ }
25
+ );
26
+ }
27
+ function EditorRail({ items, ariaLabel = "Editor", className }) {
28
+ return /* @__PURE__ */ jsx(
29
+ "div",
30
+ {
31
+ className: ["es-rail", className ?? ""].filter(Boolean).join(" "),
32
+ role: "toolbar",
33
+ "aria-orientation": "vertical",
34
+ "aria-label": ariaLabel,
35
+ children: items.map((item) => /* @__PURE__ */ jsx(
36
+ EditorRailButton,
37
+ {
38
+ label: item.label,
39
+ icon: item.icon,
40
+ active: item.active,
41
+ disabled: item.disabled,
42
+ onSelect: item.onSelect
43
+ },
44
+ item.id
45
+ ))
46
+ }
47
+ );
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
+
107
+ // src/rail/tokens.ts
108
+ var railTokens = {
109
+ /** Rail column width. `--sf-rail-w: 46px`. */
110
+ railWidth: 46,
111
+ /** Vertical padding inside the rail (top === bottom). */
112
+ railPaddingY: 8,
113
+ /** Gap between buttons. */
114
+ railGap: 4,
115
+ /** Rail card corner radius. `--sf-panel-radius: 12px`. */
116
+ railRadius: 12,
117
+ /** Button hit-area (square). */
118
+ buttonSize: 34,
119
+ /** Button corner radius. */
120
+ buttonRadius: 9,
121
+ /** Icon glyph size. */
122
+ iconSize: 18,
123
+ /** Hover/active colour transition. */
124
+ transition: "background 0.12s, color 0.12s",
125
+ /** Disabled-button opacity. */
126
+ disabledOpacity: 0.35,
127
+ color: {
128
+ /** Rail card background. `--sf-panel`. */
129
+ background: "#ffffff",
130
+ /** Rail card shadow. `--sf-panel-shadow`. */
131
+ shadow: "0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.06)",
132
+ /** Idle icon colour. `--sf-muted` (gray-500). */
133
+ idle: "#6b7280",
134
+ /** Hover background. `--sf-hover` (gray-100). */
135
+ hoverBg: "#f3f4f6",
136
+ /** Hover icon colour. `--sf-text` (gray-900). */
137
+ hoverFg: "#111827",
138
+ /** Active icon colour. `--sf-accent` (blue-600). */
139
+ accent: "#2563eb",
140
+ /** Active background. `--sf-accent-tint` (blue-50). */
141
+ accentTint: "#eff6ff"
142
+ }
143
+ };
144
+ var railCssVars = {
145
+ railWidth: "--es-rail-w",
146
+ railPaddingY: "--es-rail-pad-y",
147
+ railGap: "--es-rail-gap",
148
+ railRadius: "--es-rail-radius",
149
+ buttonSize: "--es-rail-btn-size",
150
+ buttonRadius: "--es-rail-btn-radius",
151
+ iconSize: "--es-rail-icon-size",
152
+ background: "--es-rail-bg",
153
+ shadow: "--es-rail-shadow",
154
+ idle: "--es-rail-fg",
155
+ hoverBg: "--es-rail-hover-bg",
156
+ hoverFg: "--es-rail-hover-fg",
157
+ accent: "--es-rail-accent",
158
+ accentTint: "--es-rail-accent-tint"
159
+ };
160
+
161
+ export { AddIcon, EditorRail, EditorRailButton, LayersIcon, MediaIcon, PagesIcon, SitemapIcon, StylesIcon, railCssVars, railTokens };
162
+ //# sourceMappingURL=chunk-WJIHF6PY.js.map
163
+ //# sourceMappingURL=chunk-WJIHF6PY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rail/EditorRailButton.tsx","../src/rail/EditorRail.tsx","../src/rail/icons.tsx","../src/rail/tokens.ts"],"names":["jsx"],"mappings":"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","file":"chunk-WJIHF6PY.js","sourcesContent":["import type { EditorRailButtonProps } from './types';\n\n/**\n * A single rail button: an icon in a 34×34 square that hovers, highlights when\n * active, and dims when disabled. Look only — the caller owns the click.\n *\n * `aria-pressed` is emitted **only** when `active` is a real boolean, so a view\n * toggle announces its pressed state while an action button (the quick-add \"+\",\n * whose item omits `active`) stays a plain button rather than a stuck toggle.\n */\nexport function EditorRailButton({\n label,\n icon,\n active,\n disabled,\n onSelect,\n className,\n}: EditorRailButtonProps) {\n const classes = ['es-rail-btn', active ? 'is-active' : '', className ?? '']\n .filter(Boolean)\n .join(' ');\n\n return (\n <button\n type=\"button\"\n className={classes}\n title={label}\n aria-label={label}\n aria-pressed={typeof active === 'boolean' ? active : undefined}\n disabled={disabled}\n onClick={disabled ? undefined : onSelect}\n >\n {icon}\n </button>\n );\n}\n","import { EditorRailButton } from './EditorRailButton';\nimport type { EditorRailProps } from './types';\n\n/**\n * The shared left icon-rail — one slim vertical strip of icon buttons that every\n * EFFICIENT editor uses to switch panels (Pages / Layers / Styles / Media /\n * Sitemap) and to quick-add (\"+\"). It is a **pure presentational switcher**: give\n * it `items[]`, it draws them; which one is `active` and what each does is the\n * host's business.\n *\n * No `'use client'` here on purpose — the rail has no hooks or state, so it is a\n * plain module a Next-16 server component can import and even statically render.\n * It's interactive only through the `onSelect` props the host passes, and the\n * host owns its client boundary (both editors' chrome are already client\n * components), so baking a directive into this leaf would only narrow where it\n * can be imported.\n *\n * A vertical `toolbar` (not a `tablist`), because the rail mixes view *toggles*\n * with plain *actions* like the quick-add — a toolbar of buttons models that\n * honestly, where a tablist would force every button to pretend to be a tab.\n */\nexport function EditorRail({ items, ariaLabel = 'Editor', className }: EditorRailProps) {\n return (\n <div\n className={['es-rail', className ?? ''].filter(Boolean).join(' ')}\n role=\"toolbar\"\n aria-orientation=\"vertical\"\n aria-label={ariaLabel}\n >\n {items.map((item) => (\n <EditorRailButton\n key={item.id}\n label={item.label}\n icon={item.icon}\n active={item.active}\n disabled={item.disabled}\n onSelect={item.onSelect}\n />\n ))}\n </div>\n );\n}\n","import type { ReactNode, SVGProps } from 'react';\n\n/**\n * The rail's bespoke icon set — shipped WITH the rail so no editor has to reach\n * for its own icon library (the storefront uses simple-icons, the admin portal\n * heroicons; neither is this rail's source of truth). The five view glyphs are\n * the exact paths the storefront editor's rail already draws, relocated here so\n * the shared rail is visually identical to its reference; `AddIcon` is new.\n *\n * Pure SVG, no interactivity, no browser globals — these render fine inside a\n * React Server Component. Sized 18×18 by default (the storefront's rail size);\n * pass `size` to override, or let `.es-rail-btn svg` in rail.css drive it.\n */\nexport type RailIconProps = Omit<SVGProps<SVGSVGElement>, 'children'> & { size?: number };\n\nfunction Glyph({ size = 18, children, ...rest }: RailIconProps & { children: ReactNode }) {\n return (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.7}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n {...rest}\n >\n {children}\n </svg>\n );\n}\n\n/** Pages — a document with a folded corner and text lines. */\nexport function PagesIcon(props: RailIconProps) {\n return (\n <Glyph {...props}>\n <path d=\"M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z\" />\n <path d=\"M14 3v5h5\" />\n <path d=\"M9 13h6M9 17h6\" />\n </Glyph>\n );\n}\n\n/** Layers — stacked sheets (the section / component tree). */\nexport function LayersIcon(props: RailIconProps) {\n return (\n <Glyph {...props}>\n <path d=\"M12 3l9 5-9 5-9-5 9-5z\" />\n <path d=\"M3 13l9 5 9-5\" />\n </Glyph>\n );\n}\n\n/** Styles — a paint droplet with palette dots (site styles). */\nexport function StylesIcon(props: RailIconProps) {\n return (\n <Glyph {...props}>\n <path d=\"M12 3c-4.5 0-8 3.4-8 7.6 0 3 2.2 4.9 4.8 4.9 1 0 1.6.7 1.6 1.5 0 .5-.3.9-.3 1.4 0 .9.8 1.6 1.9 1.6 4.4 0 8-3.6 8-8C20 6.4 16.5 3 12 3z\" />\n <circle cx=\"8.5\" cy=\"10\" r=\"0.6\" fill=\"currentColor\" />\n <circle cx=\"12\" cy=\"7.8\" r=\"0.6\" fill=\"currentColor\" />\n <circle cx=\"15.5\" cy=\"10\" r=\"0.6\" fill=\"currentColor\" />\n </Glyph>\n );\n}\n\n/** Media — an image frame with a sun and a mountain (the media library). */\nexport function MediaIcon(props: RailIconProps) {\n return (\n <Glyph {...props}>\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\" />\n <circle cx=\"8.5\" cy=\"9.5\" r=\"1.5\" />\n <path d=\"M21 16l-5-5-6 6\" />\n </Glyph>\n );\n}\n\n/** Sitemap — a root node branching to two page nodes (the visual sitemap). */\nexport function SitemapIcon(props: RailIconProps) {\n return (\n <Glyph {...props}>\n <rect x=\"9\" y=\"3\" width=\"6\" height=\"4.5\" rx=\"1\" />\n <rect x=\"3\" y=\"16.5\" width=\"6\" height=\"4.5\" rx=\"1\" />\n <rect x=\"15\" y=\"16.5\" width=\"6\" height=\"4.5\" rx=\"1\" />\n <path d=\"M12 7.5v4M6 16.5v-2.5h12v2.5M18 16.5v-2.5\" />\n </Glyph>\n );\n}\n\n/** Add — the quick-add \"+\", drawn in the same weight and cap style as the set. */\nexport function AddIcon(props: RailIconProps) {\n return (\n <Glyph {...props}>\n <path d=\"M12 5v14M5 12h14\" />\n </Glyph>\n );\n}\n","/**\n * Rail design tokens — the ONE source both apps' rails converge on.\n *\n * Every value below is taken verbatim from the storefront editor's rail\n * (`efficient-shop` → `storefront-editor.css`, the `.sf-rail` / `.sf-rail-btn`\n * rules), which is the visual reference. The admin portal's hand-rolled rail had\n * drifted (button 36 vs 34, icon 20 vs 18, radius 8 vs 9, a Tailwind accent\n * instead of a CSS var); these tokens end that drift.\n *\n * Exposed BOTH ways, per the same value:\n * - `railTokens` — this JS object, for anything that needs the numbers/colours\n * in code (inline styles, a theme bridge, a Storybook control);\n * - `rail.css` — the stylesheet, which declares the identical values as CSS\n * custom properties on `.es-rail` (names listed in `railCssVars`).\n *\n * Numeric fields are pixel magnitudes (unitless) so they compose in code; the\n * CSS applies the `px`.\n */\nexport const railTokens = {\n /** Rail column width. `--sf-rail-w: 46px`. */\n railWidth: 46,\n /** Vertical padding inside the rail (top === bottom). */\n railPaddingY: 8,\n /** Gap between buttons. */\n railGap: 4,\n /** Rail card corner radius. `--sf-panel-radius: 12px`. */\n railRadius: 12,\n /** Button hit-area (square). */\n buttonSize: 34,\n /** Button corner radius. */\n buttonRadius: 9,\n /** Icon glyph size. */\n iconSize: 18,\n /** Hover/active colour transition. */\n transition: 'background 0.12s, color 0.12s',\n /** Disabled-button opacity. */\n disabledOpacity: 0.35,\n color: {\n /** Rail card background. `--sf-panel`. */\n background: '#ffffff',\n /** Rail card shadow. `--sf-panel-shadow`. */\n shadow: '0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.06)',\n /** Idle icon colour. `--sf-muted` (gray-500). */\n idle: '#6b7280',\n /** Hover background. `--sf-hover` (gray-100). */\n hoverBg: '#f3f4f6',\n /** Hover icon colour. `--sf-text` (gray-900). */\n hoverFg: '#111827',\n /** Active icon colour. `--sf-accent` (blue-600). */\n accent: '#2563eb',\n /** Active background. `--sf-accent-tint` (blue-50). */\n accentTint: '#eff6ff',\n },\n} as const;\n\nexport type RailTokens = typeof railTokens;\n\n/**\n * The CSS custom-property name behind each token. `rail.css` sets these on\n * `.es-rail`; override any of them on (or above) the rail element to re-theme it\n * — e.g. `style={{ ['--es-rail-accent']: brand }}` — without shipping new CSS.\n */\nexport const railCssVars = {\n railWidth: '--es-rail-w',\n railPaddingY: '--es-rail-pad-y',\n railGap: '--es-rail-gap',\n railRadius: '--es-rail-radius',\n buttonSize: '--es-rail-btn-size',\n buttonRadius: '--es-rail-btn-radius',\n iconSize: '--es-rail-icon-size',\n background: '--es-rail-bg',\n shadow: '--es-rail-shadow',\n idle: '--es-rail-fg',\n hoverBg: '--es-rail-hover-bg',\n hoverFg: '--es-rail-hover-fg',\n accent: '--es-rail-accent',\n accentTint: '--es-rail-accent-tint',\n} as const;\n"]}
@@ -0,0 +1,2 @@
1
+ export { AddIcon, EditorRail, EditorRailButton, EditorRailButtonProps, EditorRailItem, EditorRailProps, LayersIcon, MediaIcon, PagesIcon, RailIconProps, RailTokens, SitemapIcon, StylesIcon, railCssVars, railTokens } from './rail/index.js';
2
+ import 'react';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { AddIcon, EditorRail, EditorRailButton, LayersIcon, MediaIcon, PagesIcon, SitemapIcon, StylesIcon, railCssVars, railTokens } from './chunk-WJIHF6PY.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,186 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, SVGProps } from 'react';
3
+
4
+ /**
5
+ * One entry in the rail. A rail item is either a *view switch* (Pages, Layers,
6
+ * Styles, …) or an *action* (the quick-add "+"). The shape is the same for both;
7
+ * what tells them apart is `active`:
8
+ *
9
+ * - a view item passes `active: true | false` → it renders as a toggle
10
+ * (`aria-pressed`) and lights up when its surface is open;
11
+ * - an action item omits `active` → it renders as a plain button with no
12
+ * pressed state, because "Add" is a thing you *do*, not a thing you're *in*.
13
+ *
14
+ * The rail is purely presentational: it owns the LOOK, never the app state. The
15
+ * host decides which item is `active` and what each `onSelect` does.
16
+ */
17
+ interface EditorRailItem {
18
+ /** Stable identity — used as the React key and to correlate selection. */
19
+ id: string;
20
+ /** Accessible name; becomes both the `aria-label` and the hover `title`. */
21
+ label: string;
22
+ /**
23
+ * The glyph. Pass one of the icons this package ships
24
+ * (`<PagesIcon />`, `<AddIcon />`, …) so every editor's rail draws the same
25
+ * set. Any 18×18, `currentColor`-stroked node works.
26
+ */
27
+ icon: ReactNode;
28
+ /** Present ⇒ this is a view toggle and this is whether its surface is open. */
29
+ active?: boolean;
30
+ /** Dim + non-interactive. */
31
+ disabled?: boolean;
32
+ /** Click handler. Skipped while `disabled`. */
33
+ onSelect?: () => void;
34
+ }
35
+ interface EditorRailProps {
36
+ /** The buttons, top to bottom, in render order (place the "+" where you want it). */
37
+ items: EditorRailItem[];
38
+ /** Names the toolbar for assistive tech. Defaults to `"Editor"`. */
39
+ ariaLabel?: string;
40
+ /** Extra class(es) merged onto the rail container. */
41
+ className?: string;
42
+ }
43
+ interface EditorRailButtonProps {
44
+ label: string;
45
+ icon: ReactNode;
46
+ active?: boolean;
47
+ disabled?: boolean;
48
+ onSelect?: () => void;
49
+ className?: string;
50
+ }
51
+
52
+ /**
53
+ * The shared left icon-rail — one slim vertical strip of icon buttons that every
54
+ * EFFICIENT editor uses to switch panels (Pages / Layers / Styles / Media /
55
+ * Sitemap) and to quick-add ("+"). It is a **pure presentational switcher**: give
56
+ * it `items[]`, it draws them; which one is `active` and what each does is the
57
+ * host's business.
58
+ *
59
+ * No `'use client'` here on purpose — the rail has no hooks or state, so it is a
60
+ * plain module a Next-16 server component can import and even statically render.
61
+ * It's interactive only through the `onSelect` props the host passes, and the
62
+ * host owns its client boundary (both editors' chrome are already client
63
+ * components), so baking a directive into this leaf would only narrow where it
64
+ * can be imported.
65
+ *
66
+ * A vertical `toolbar` (not a `tablist`), because the rail mixes view *toggles*
67
+ * with plain *actions* like the quick-add — a toolbar of buttons models that
68
+ * honestly, where a tablist would force every button to pretend to be a tab.
69
+ */
70
+ declare function EditorRail({ items, ariaLabel, className }: EditorRailProps): react.JSX.Element;
71
+
72
+ /**
73
+ * A single rail button: an icon in a 34×34 square that hovers, highlights when
74
+ * active, and dims when disabled. Look only — the caller owns the click.
75
+ *
76
+ * `aria-pressed` is emitted **only** when `active` is a real boolean, so a view
77
+ * toggle announces its pressed state while an action button (the quick-add "+",
78
+ * whose item omits `active`) stays a plain button rather than a stuck toggle.
79
+ */
80
+ declare function EditorRailButton({ label, icon, active, disabled, onSelect, className, }: EditorRailButtonProps): react.JSX.Element;
81
+
82
+ /**
83
+ * The rail's bespoke icon set — shipped WITH the rail so no editor has to reach
84
+ * for its own icon library (the storefront uses simple-icons, the admin portal
85
+ * heroicons; neither is this rail's source of truth). The five view glyphs are
86
+ * the exact paths the storefront editor's rail already draws, relocated here so
87
+ * the shared rail is visually identical to its reference; `AddIcon` is new.
88
+ *
89
+ * Pure SVG, no interactivity, no browser globals — these render fine inside a
90
+ * React Server Component. Sized 18×18 by default (the storefront's rail size);
91
+ * pass `size` to override, or let `.es-rail-btn svg` in rail.css drive it.
92
+ */
93
+ type RailIconProps = Omit<SVGProps<SVGSVGElement>, 'children'> & {
94
+ size?: number;
95
+ };
96
+ /** Pages — a document with a folded corner and text lines. */
97
+ declare function PagesIcon(props: RailIconProps): react.JSX.Element;
98
+ /** Layers — stacked sheets (the section / component tree). */
99
+ declare function LayersIcon(props: RailIconProps): react.JSX.Element;
100
+ /** Styles — a paint droplet with palette dots (site styles). */
101
+ declare function StylesIcon(props: RailIconProps): react.JSX.Element;
102
+ /** Media — an image frame with a sun and a mountain (the media library). */
103
+ declare function MediaIcon(props: RailIconProps): react.JSX.Element;
104
+ /** Sitemap — a root node branching to two page nodes (the visual sitemap). */
105
+ declare function SitemapIcon(props: RailIconProps): react.JSX.Element;
106
+ /** Add — the quick-add "+", drawn in the same weight and cap style as the set. */
107
+ declare function AddIcon(props: RailIconProps): react.JSX.Element;
108
+
109
+ /**
110
+ * Rail design tokens — the ONE source both apps' rails converge on.
111
+ *
112
+ * Every value below is taken verbatim from the storefront editor's rail
113
+ * (`efficient-shop` → `storefront-editor.css`, the `.sf-rail` / `.sf-rail-btn`
114
+ * rules), which is the visual reference. The admin portal's hand-rolled rail had
115
+ * drifted (button 36 vs 34, icon 20 vs 18, radius 8 vs 9, a Tailwind accent
116
+ * instead of a CSS var); these tokens end that drift.
117
+ *
118
+ * Exposed BOTH ways, per the same value:
119
+ * - `railTokens` — this JS object, for anything that needs the numbers/colours
120
+ * in code (inline styles, a theme bridge, a Storybook control);
121
+ * - `rail.css` — the stylesheet, which declares the identical values as CSS
122
+ * custom properties on `.es-rail` (names listed in `railCssVars`).
123
+ *
124
+ * Numeric fields are pixel magnitudes (unitless) so they compose in code; the
125
+ * CSS applies the `px`.
126
+ */
127
+ declare const railTokens: {
128
+ /** Rail column width. `--sf-rail-w: 46px`. */
129
+ readonly railWidth: 46;
130
+ /** Vertical padding inside the rail (top === bottom). */
131
+ readonly railPaddingY: 8;
132
+ /** Gap between buttons. */
133
+ readonly railGap: 4;
134
+ /** Rail card corner radius. `--sf-panel-radius: 12px`. */
135
+ readonly railRadius: 12;
136
+ /** Button hit-area (square). */
137
+ readonly buttonSize: 34;
138
+ /** Button corner radius. */
139
+ readonly buttonRadius: 9;
140
+ /** Icon glyph size. */
141
+ readonly iconSize: 18;
142
+ /** Hover/active colour transition. */
143
+ readonly transition: "background 0.12s, color 0.12s";
144
+ /** Disabled-button opacity. */
145
+ readonly disabledOpacity: 0.35;
146
+ readonly color: {
147
+ /** Rail card background. `--sf-panel`. */
148
+ readonly background: "#ffffff";
149
+ /** Rail card shadow. `--sf-panel-shadow`. */
150
+ readonly shadow: "0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.06)";
151
+ /** Idle icon colour. `--sf-muted` (gray-500). */
152
+ readonly idle: "#6b7280";
153
+ /** Hover background. `--sf-hover` (gray-100). */
154
+ readonly hoverBg: "#f3f4f6";
155
+ /** Hover icon colour. `--sf-text` (gray-900). */
156
+ readonly hoverFg: "#111827";
157
+ /** Active icon colour. `--sf-accent` (blue-600). */
158
+ readonly accent: "#2563eb";
159
+ /** Active background. `--sf-accent-tint` (blue-50). */
160
+ readonly accentTint: "#eff6ff";
161
+ };
162
+ };
163
+ type RailTokens = typeof railTokens;
164
+ /**
165
+ * The CSS custom-property name behind each token. `rail.css` sets these on
166
+ * `.es-rail`; override any of them on (or above) the rail element to re-theme it
167
+ * — e.g. `style={{ ['--es-rail-accent']: brand }}` — without shipping new CSS.
168
+ */
169
+ declare const railCssVars: {
170
+ readonly railWidth: "--es-rail-w";
171
+ readonly railPaddingY: "--es-rail-pad-y";
172
+ readonly railGap: "--es-rail-gap";
173
+ readonly railRadius: "--es-rail-radius";
174
+ readonly buttonSize: "--es-rail-btn-size";
175
+ readonly buttonRadius: "--es-rail-btn-radius";
176
+ readonly iconSize: "--es-rail-icon-size";
177
+ readonly background: "--es-rail-bg";
178
+ readonly shadow: "--es-rail-shadow";
179
+ readonly idle: "--es-rail-fg";
180
+ readonly hoverBg: "--es-rail-hover-bg";
181
+ readonly hoverFg: "--es-rail-hover-fg";
182
+ readonly accent: "--es-rail-accent";
183
+ readonly accentTint: "--es-rail-accent-tint";
184
+ };
185
+
186
+ export { AddIcon, EditorRail, EditorRailButton, type EditorRailButtonProps, type EditorRailItem, type EditorRailProps, LayersIcon, MediaIcon, PagesIcon, type RailIconProps, type RailTokens, SitemapIcon, StylesIcon, railCssVars, railTokens };
@@ -0,0 +1,3 @@
1
+ export { AddIcon, EditorRail, EditorRailButton, LayersIcon, MediaIcon, PagesIcon, SitemapIcon, StylesIcon, railCssVars, railTokens } from '../chunk-WJIHF6PY.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,76 @@
1
+ /*
2
+ * editor-shell — rail styles.
3
+ *
4
+ * The values here mirror `railTokens` in tokens.ts one-for-one (both trace back
5
+ * to the storefront editor's `.sf-rail` rules — the visual reference). They are
6
+ * declared as custom properties ON `.es-rail`, not on `:root`, so importing this
7
+ * stylesheet themes ONLY the rail and never leaks variables into the host page.
8
+ * Re-theme by overriding any `--es-rail-*` var on or above the rail element.
9
+ *
10
+ * Import once from a client entry / global stylesheet:
11
+ * import 'editor-shell/rail.css';
12
+ */
13
+
14
+ .es-rail {
15
+ /* — tokens (see tokens.ts / railCssVars) — */
16
+ --es-rail-w: 46px;
17
+ --es-rail-pad-y: 8px;
18
+ --es-rail-gap: 4px;
19
+ --es-rail-radius: 12px;
20
+ --es-rail-btn-size: 34px;
21
+ --es-rail-btn-radius: 9px;
22
+ --es-rail-icon-size: 18px;
23
+ --es-rail-bg: #ffffff;
24
+ --es-rail-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.06);
25
+ --es-rail-fg: #6b7280;
26
+ --es-rail-hover-bg: #f3f4f6;
27
+ --es-rail-hover-fg: #111827;
28
+ --es-rail-accent: #2563eb;
29
+ --es-rail-accent-tint: #eff6ff;
30
+
31
+ flex: none;
32
+ width: var(--es-rail-w);
33
+ display: flex;
34
+ flex-direction: column;
35
+ align-items: center;
36
+ gap: var(--es-rail-gap);
37
+ padding: var(--es-rail-pad-y) 0;
38
+ background: var(--es-rail-bg);
39
+ border-radius: var(--es-rail-radius);
40
+ box-shadow: var(--es-rail-shadow);
41
+ }
42
+
43
+ .es-rail-btn {
44
+ width: var(--es-rail-btn-size);
45
+ height: var(--es-rail-btn-size);
46
+ border: none;
47
+ background: transparent;
48
+ border-radius: var(--es-rail-btn-radius);
49
+ color: var(--es-rail-fg);
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ cursor: pointer;
54
+ transition: background 0.12s, color 0.12s;
55
+ }
56
+
57
+ .es-rail-btn svg {
58
+ width: var(--es-rail-icon-size);
59
+ height: var(--es-rail-icon-size);
60
+ display: block;
61
+ }
62
+
63
+ .es-rail-btn:hover:not(:disabled) {
64
+ background: var(--es-rail-hover-bg);
65
+ color: var(--es-rail-hover-fg);
66
+ }
67
+
68
+ .es-rail-btn.is-active {
69
+ background: var(--es-rail-accent-tint);
70
+ color: var(--es-rail-accent);
71
+ }
72
+
73
+ .es-rail-btn:disabled {
74
+ opacity: 0.35;
75
+ cursor: default;
76
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
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.",
5
+ "license": "MIT",
6
+ "author": "Lewis Liu",
7
+ "homepage": "https://github.com/Lewislhy/editor-shell#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Lewislhy/editor-shell.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/Lewislhy/editor-shell/issues"
14
+ },
15
+ "type": "module",
16
+ "sideEffects": [
17
+ "**/*.css"
18
+ ],
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ },
27
+ "./rail": {
28
+ "types": "./dist/rail/index.d.ts",
29
+ "import": "./dist/rail/index.js"
30
+ },
31
+ "./rail.css": "./dist/rail/rail.css"
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "peerDependencies": {
37
+ "react": ">=18",
38
+ "react-dom": ">=18"
39
+ },
40
+ "dependencies": {},
41
+ "devDependencies": {
42
+ "@types/node": "^22.0.0",
43
+ "@types/react": "^18.2.0",
44
+ "@types/react-dom": "^18.2.0",
45
+ "esbuild": "^0.27.0",
46
+ "react": "^18.2.0",
47
+ "react-dom": "^18.2.0",
48
+ "tsup": "^8.0.0",
49
+ "typescript": "^5.3.0"
50
+ },
51
+ "scripts": {
52
+ "build": "tsup && cp src/rail/rail.css dist/rail/rail.css",
53
+ "dev": "tsup --watch",
54
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
55
+ "test": "node scripts/test.mjs",
56
+ "clean": "rm -rf dist"
57
+ },
58
+ "keywords": [
59
+ "react",
60
+ "editor",
61
+ "ui-shell",
62
+ "rail",
63
+ "toolbar",
64
+ "next",
65
+ "rsc",
66
+ "leaf"
67
+ ],
68
+ "engines": {
69
+ "node": ">=20"
70
+ }
71
+ }