@elytracms/components 0.0.0-stage → 0.0.92

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 finaldream
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.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `@elytracms/components` — the components Elytra ships, as an opt-in list
3
+ * (EC-591, EC-565 2g). The engine registers no components of its own: a config
4
+ * lists what it wants —
5
+ *
6
+ * ```ts
7
+ * import { elytraComponents } from '@elytracms/components'
8
+ * export const site = { …, components: [...elytraComponents, ...siteComponents] }
9
+ * ```
10
+ *
11
+ * — a hand-picked subset (`[stackComponent, textComponent, …]`), or none. Each
12
+ * entry is a `{ manifest, implementation }` pair exactly like a project component;
13
+ * the ids are plain (`Heading`, `Stack`) and the developer's. The canvas finds what
14
+ * it needs through the roles declared on the manifests (`Stack` → `layout.stack`,
15
+ * `Grid` → `layout.grid`, `Text` → `text`, `RichText` → `richText`, `Image` →
16
+ * `image`); a role nobody fills switches its gesture off with a visible notice.
17
+ *
18
+ * Dependency direction: this package depends on `@elytracms/core`, never the
19
+ * other way round.
20
+ */
21
+ export type { ElytraComponent, ImageSource } from './primitives.js';
22
+ export { elytraComponents, containerComponent, stackComponent, gridComponent, textComponent, headingComponent, richTextComponent, imageComponent, buttonComponent, repeaterComponent, switchComponent, normalizeImageSource, focalPointObjectPosition, GRID_MOBILE_BREAKPOINT_PX, GRID_STACK_CSS, Container, Stack, Grid, Text, Heading, Image, Button, Repeater, Switch, ContainerManifest, StackManifest, GridManifest, TextManifest, HeadingManifest, ImageManifest, ButtonManifest, RepeaterManifest, SwitchManifest, } from './primitives.js';
23
+ export { RichText, RichTextManifest, RICH_TEXT_DEFAULT_VALUE } from './rich-text.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { elytraComponents, containerComponent, stackComponent, gridComponent, textComponent, headingComponent, richTextComponent, imageComponent, buttonComponent, repeaterComponent, switchComponent, normalizeImageSource, focalPointObjectPosition,
2
+ // Responsive defaults (EC-163): the stack-on-mobile breakpoint + emitted rule.
3
+ GRID_MOBILE_BREAKPOINT_PX, GRID_STACK_CSS, Container, Stack, Grid, Text, Heading, Image, Button, Repeater, Switch, ContainerManifest, StackManifest, GridManifest, TextManifest, HeadingManifest, ImageManifest, ButtonManifest, RepeaterManifest, SwitchManifest, } from './primitives.js';
4
+ export { RichText, RichTextManifest, RICH_TEXT_DEFAULT_VALUE } from './rich-text.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAqBA,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,oBAAoB,EACpB,wBAAwB;AACxB,+EAA+E;AAC/E,yBAAyB,EACzB,cAAc,EACd,SAAS,EACT,KAAK,EACL,IAAI,EACJ,IAAI,EACJ,OAAO,EACP,KAAK,EACL,MAAM,EACN,QAAQ,EACR,MAAM,EACN,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA","sourcesContent":["/**\n * `@elytracms/components` — the components Elytra ships, as an opt-in list\n * (EC-591, EC-565 2g). The engine registers no components of its own: a config\n * lists what it wants —\n *\n * ```ts\n * import { elytraComponents } from '@elytracms/components'\n * export const site = { …, components: [...elytraComponents, ...siteComponents] }\n * ```\n *\n * — a hand-picked subset (`[stackComponent, textComponent, …]`), or none. Each\n * entry is a `{ manifest, implementation }` pair exactly like a project component;\n * the ids are plain (`Heading`, `Stack`) and the developer's. The canvas finds what\n * it needs through the roles declared on the manifests (`Stack` → `layout.stack`,\n * `Grid` → `layout.grid`, `Text` → `text`, `RichText` → `richText`, `Image` →\n * `image`); a role nobody fills switches its gesture off with a visible notice.\n *\n * Dependency direction: this package depends on `@elytracms/core`, never the\n * other way round.\n */\nexport type { ElytraComponent, ImageSource } from './primitives'\nexport {\n elytraComponents,\n containerComponent,\n stackComponent,\n gridComponent,\n textComponent,\n headingComponent,\n richTextComponent,\n imageComponent,\n buttonComponent,\n repeaterComponent,\n switchComponent,\n normalizeImageSource,\n focalPointObjectPosition,\n // Responsive defaults (EC-163): the stack-on-mobile breakpoint + emitted rule.\n GRID_MOBILE_BREAKPOINT_PX,\n GRID_STACK_CSS,\n Container,\n Stack,\n Grid,\n Text,\n Heading,\n Image,\n Button,\n Repeater,\n Switch,\n ContainerManifest,\n StackManifest,\n GridManifest,\n TextManifest,\n HeadingManifest,\n ImageManifest,\n ButtonManifest,\n RepeaterManifest,\n SwitchManifest,\n} from './primitives'\nexport { RichText, RichTextManifest, RICH_TEXT_DEFAULT_VALUE } from './rich-text'\n"]}
@@ -0,0 +1,115 @@
1
+ import type { ComponentType, ReactNode } from 'react';
2
+ import type { ComponentManifest } from '@elytracms/core/component-registry';
3
+ /**
4
+ * The components Elytra ships (EC-591) — moved verbatim from
5
+ * `@elytracms/core/runtime-renderer`'s primitives, where the engine registered
6
+ * them itself under `base.*` ids. Now nothing registers them: they are
7
+ * `{ manifest, implementation }` entries a config opts into by listing them
8
+ * (`components: [...elytraComponents, ...siteComponents]`), a subset, or none.
9
+ * Plain ids, the developer's to keep or replace; the five canvas roles are
10
+ * declared on the manifests that fill them (`Stack` layout.stack, `Grid`
11
+ * layout.grid, `Text` text, `RichText` richText, `Image` image).
12
+ */
13
+ /**
14
+ * Common shape for primitive implementations: resolved props (arbitrary keys) plus React
15
+ * children for the default slot and any named-slot props.
16
+ */
17
+ type PrimitiveProps = Record<string, unknown> & {
18
+ children?: ReactNode;
19
+ };
20
+ declare function Container(props: PrimitiveProps): ReactNode;
21
+ declare const ContainerManifest: ComponentManifest;
22
+ declare function Stack(props: PrimitiveProps): ReactNode;
23
+ declare const StackManifest: ComponentManifest;
24
+ /**
25
+ * Responsive defaults (EC-163): below this viewport width a Grid's columns
26
+ * stack into one. 640px (the Tailwind `sm` boundary) sits between the
27
+ * builder's deterministic preview widths — phone (390px) stacks, tablet
28
+ * (768px) keeps columns.
29
+ */
30
+ export declare const GRID_MOBILE_BREAKPOINT_PX = 640;
31
+ /**
32
+ * The stack-on-mobile rule a stacking Grid carries with it. A **viewport media
33
+ * query** (not a container query), deliberately: the builder canvas renders
34
+ * into an iframe — a true viewport — so constraining the iframe to the phone
35
+ * width makes this exact rule fire in the preview; and the identical markup
36
+ * server-renders in `@elytracms/next` (App Router SSR) with no client JS, no
37
+ * extra stylesheet wiring, and no `container-type` context that every consumer
38
+ * would otherwise have to guarantee around every grid. The rule is emitted as
39
+ * an inline `<style>` child of the grid itself (style elements generate no
40
+ * grid item), so the behavior is self-contained wherever the implementation
41
+ * renders — canvas iframe portal, static markup, or RSC payload — and the
42
+ * `!important` lets the stylesheet rule beat the grid's own inline column
43
+ * template. Opted-out grids (`stackOnMobile: false`) carry neither the
44
+ * attribute nor the rule.
45
+ */
46
+ export declare const GRID_STACK_CSS = "@media (max-width: 640px){[data-ec-grid-stack]{grid-template-columns:minmax(0,1fr) !important}}";
47
+ declare function Grid(props: PrimitiveProps): ReactNode;
48
+ declare const GridManifest: ComponentManifest;
49
+ declare function Text(props: PrimitiveProps): ReactNode;
50
+ declare const TextManifest: ComponentManifest;
51
+ declare function Heading(props: PrimitiveProps): ReactNode;
52
+ declare const HeadingManifest: ComponentManifest;
53
+ /**
54
+ * The Image `src` may be a resolved asset (EC-195: an `asset`-typed prop the
55
+ * renderer resolved from an asset id) or a plain URL string. Normalize to
56
+ * `{ url, width, height, alt }`, letting the asset's intrinsic dimensions/alt
57
+ * fill in when the explicit props are unset — so an uploaded image reserves the
58
+ * right box (no layout shift) and carries its alt text by default.
59
+ */
60
+ export interface ImageSource {
61
+ url: string;
62
+ width: number | undefined;
63
+ height: number | undefined;
64
+ alt: string;
65
+ /** Normalized focal point `{ x, y }` 0–1 (EC-230) → CSS object-position; null when none. */
66
+ focalPoint: {
67
+ x: number;
68
+ y: number;
69
+ } | null;
70
+ /** LQIP blur-placeholder data URI (EC-420) → `next/image` blurDataURL; null when none. */
71
+ lqip: string | null;
72
+ }
73
+ export declare function normalizeImageSource(props: PrimitiveProps): ImageSource;
74
+ /** EC-230: a focal point becomes a CSS `object-position` string (percent of each axis). */
75
+ export declare function focalPointObjectPosition(focalPoint: {
76
+ x: number;
77
+ y: number;
78
+ } | null): string | undefined;
79
+ declare function Image(props: PrimitiveProps): ReactNode;
80
+ declare const ImageManifest: ComponentManifest;
81
+ declare function Button(props: PrimitiveProps): ReactNode;
82
+ declare const ButtonManifest: ComponentManifest;
83
+ declare function Repeater(props: PrimitiveProps): ReactNode;
84
+ declare const RepeaterManifest: ComponentManifest;
85
+ declare function Switch(props: PrimitiveProps): ReactNode;
86
+ declare const SwitchManifest: ComponentManifest;
87
+ /**
88
+ * One shipped component: its manifest plus the real implementation. Strictly
89
+ * typed (the implementation IS a component), and assignable to the config's
90
+ * `HostComponentEntry` and core's `ComponentEntry`, so the entries list straight
91
+ * into `elytra.config.ts`'s `components`.
92
+ */
93
+ export interface ElytraComponent {
94
+ manifest: ComponentManifest;
95
+ implementation: ComponentType<Record<string, unknown>>;
96
+ }
97
+ export declare const containerComponent: ElytraComponent;
98
+ export declare const stackComponent: ElytraComponent;
99
+ export declare const gridComponent: ElytraComponent;
100
+ export declare const textComponent: ElytraComponent;
101
+ export declare const headingComponent: ElytraComponent;
102
+ export declare const richTextComponent: ElytraComponent;
103
+ export declare const imageComponent: ElytraComponent;
104
+ export declare const buttonComponent: ElytraComponent;
105
+ export declare const repeaterComponent: ElytraComponent;
106
+ export declare const switchComponent: ElytraComponent;
107
+ /**
108
+ * Every component Elytra ships, in palette order — the list a config spreads
109
+ * into its `components` (`[...elytraComponents, ...siteComponents]`). Pick a
110
+ * subset by listing the individual entries instead; replace one by listing your
111
+ * own component under the id (and leaving the shipped entry out — the list names
112
+ * each id once, a second entry is a `duplicate-id` issue).
113
+ */
114
+ export declare const elytraComponents: readonly ElytraComponent[];
115
+ export { Container, Stack, Grid, Text, Heading, Image, Button, Repeater, Switch, ContainerManifest, StackManifest, GridManifest, TextManifest, HeadingManifest, ImageManifest, ButtonManifest, RepeaterManifest, SwitchManifest, };
@@ -0,0 +1,428 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { createElement } from 'react';
3
+ import { CONTAINER_ALIGNMENTS, GRID_MAX_COLUMNS, GRID_MIN_COLUMNS, SPACING_TOKEN_NAMES, spacingTokenCss, } from '@elytracms/core/project-graph';
4
+ import { defineComponent } from '@elytracms/core/component-registry';
5
+ import { ecProp } from '@elytracms/core/runtime-renderer';
6
+ import { RichText, RichTextManifest } from './rich-text.js';
7
+ function asString(value, fallback = '') {
8
+ return typeof value === 'string' ? value : fallback;
9
+ }
10
+ // ---------------------------------------------------------------------------
11
+ // Layout-container styling (EC-161) — gap/alignment resolve from the canonical
12
+ // design-token spacing scale; legacy numeric gaps (`n` → `n * 4px`) stay
13
+ // renderable so pre-token graphs keep their layout.
14
+ // ---------------------------------------------------------------------------
15
+ function gapCss(value) {
16
+ if (typeof value === 'string')
17
+ return spacingTokenCss(value);
18
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0)
19
+ return `${value * 4}px`;
20
+ return undefined;
21
+ }
22
+ const ALIGN_ITEMS = {
23
+ start: 'flex-start',
24
+ center: 'center',
25
+ end: 'flex-end',
26
+ stretch: 'stretch',
27
+ };
28
+ /** `alignItems` for a container `align` prop; undefined for `stretch` (the CSS default) or junk. */
29
+ function alignItemsCss(value) {
30
+ if (value === 'start' || value === 'center' || value === 'end')
31
+ return ALIGN_ITEMS[value];
32
+ return undefined;
33
+ }
34
+ const SPACING_OPTIONS = SPACING_TOKEN_NAMES.map((name) => ({ label: name, value: name }));
35
+ const ALIGN_OPTIONS = CONTAINER_ALIGNMENTS.map((name) => ({ label: name, value: name }));
36
+ // ---------------------------------------------------------------------------
37
+ // Container — a generic block wrapper with a single default slot.
38
+ // ---------------------------------------------------------------------------
39
+ function Container(props) {
40
+ // The default `children` slot and the layout `outlet` slot both render here; the outlet
41
+ // is where `renderCompositionInLayout` injects the page body when this Container is a layout root.
42
+ return (_jsxs("div", { "data-ec-primitive": "Container", children: [props.children, props.outlet] }));
43
+ }
44
+ const ContainerManifest = defineComponent({
45
+ id: 'Container',
46
+ title: 'Container',
47
+ category: 'layout',
48
+ props: {},
49
+ // The outlet is filled by page injection, so it is not authored-required.
50
+ slots: [{ name: 'children' }, { name: 'outlet' }],
51
+ });
52
+ // ---------------------------------------------------------------------------
53
+ // Stack — flex layout container (EC-161): direction, token-bound gap, alignment.
54
+ // ---------------------------------------------------------------------------
55
+ function Stack(props) {
56
+ const style = {
57
+ display: 'flex',
58
+ flexDirection: props.direction === 'horizontal' ? 'row' : 'column',
59
+ };
60
+ const gap = gapCss(props.gap);
61
+ if (gap !== undefined)
62
+ style.gap = gap;
63
+ const alignItems = alignItemsCss(props.align);
64
+ if (alignItems !== undefined)
65
+ style.alignItems = alignItems;
66
+ return (_jsx("div", { "data-ec-primitive": "Stack", style: style, children: props.children }));
67
+ }
68
+ const StackManifest = defineComponent({
69
+ id: 'Stack',
70
+ // EC-591: implicit layout's flow container and the canvas's synthetic root.
71
+ role: 'layout.stack',
72
+ title: 'Stack',
73
+ category: 'layout',
74
+ props: {
75
+ direction: {
76
+ type: 'select',
77
+ default: 'vertical',
78
+ options: [
79
+ { value: 'vertical', label: 'Vertical' },
80
+ { value: 'horizontal', label: 'Horizontal' },
81
+ ],
82
+ form: { label: 'Direction' },
83
+ },
84
+ gap: {
85
+ // A design-token spacing step name (EC-161); legacy numeric gaps still render.
86
+ type: 'select',
87
+ default: '4',
88
+ options: SPACING_OPTIONS,
89
+ form: { label: 'Gap' },
90
+ },
91
+ align: {
92
+ type: 'select',
93
+ default: 'stretch',
94
+ options: ALIGN_OPTIONS,
95
+ form: { label: 'Align' },
96
+ },
97
+ },
98
+ slots: [{ name: 'children' }],
99
+ });
100
+ // ---------------------------------------------------------------------------
101
+ // Grid — CSS grid container (EC-161): column count, token-bound gap, alignment.
102
+ // ---------------------------------------------------------------------------
103
+ function asColumns(value) {
104
+ if (typeof value !== 'number' || !Number.isFinite(value))
105
+ return GRID_MIN_COLUMNS;
106
+ return Math.min(Math.max(GRID_MIN_COLUMNS, Math.floor(value)), GRID_MAX_COLUMNS);
107
+ }
108
+ /**
109
+ * Responsive defaults (EC-163): below this viewport width a Grid's columns
110
+ * stack into one. 640px (the Tailwind `sm` boundary) sits between the
111
+ * builder's deterministic preview widths — phone (390px) stacks, tablet
112
+ * (768px) keeps columns.
113
+ */
114
+ export const GRID_MOBILE_BREAKPOINT_PX = 640;
115
+ /**
116
+ * The stack-on-mobile rule a stacking Grid carries with it. A **viewport media
117
+ * query** (not a container query), deliberately: the builder canvas renders
118
+ * into an iframe — a true viewport — so constraining the iframe to the phone
119
+ * width makes this exact rule fire in the preview; and the identical markup
120
+ * server-renders in `@elytracms/next` (App Router SSR) with no client JS, no
121
+ * extra stylesheet wiring, and no `container-type` context that every consumer
122
+ * would otherwise have to guarantee around every grid. The rule is emitted as
123
+ * an inline `<style>` child of the grid itself (style elements generate no
124
+ * grid item), so the behavior is self-contained wherever the implementation
125
+ * renders — canvas iframe portal, static markup, or RSC payload — and the
126
+ * `!important` lets the stylesheet rule beat the grid's own inline column
127
+ * template. Opted-out grids (`stackOnMobile: false`) carry neither the
128
+ * attribute nor the rule.
129
+ */
130
+ export const GRID_STACK_CSS = `@media (max-width: ${GRID_MOBILE_BREAKPOINT_PX}px){[data-ec-grid-stack]{grid-template-columns:minmax(0,1fr) !important}}`;
131
+ function Grid(props) {
132
+ const style = {
133
+ display: 'grid',
134
+ gridTemplateColumns: `repeat(${asColumns(props.columns)}, minmax(0, 1fr))`,
135
+ };
136
+ const gap = gapCss(props.gap);
137
+ if (gap !== undefined)
138
+ style.gap = gap;
139
+ const alignItems = alignItemsCss(props.align);
140
+ if (alignItems !== undefined)
141
+ style.alignItems = alignItems;
142
+ // Stack-on-mobile is the default; only the explicit opt-out keeps columns.
143
+ const stacksOnMobile = props.stackOnMobile !== false;
144
+ return (_jsxs("div", { "data-ec-primitive": "Grid", ...(stacksOnMobile ? { 'data-ec-grid-stack': '' } : {}), style: style, children: [stacksOnMobile ? _jsx("style", { children: GRID_STACK_CSS }) : null, props.children] }));
145
+ }
146
+ const GridManifest = defineComponent({
147
+ id: 'Grid',
148
+ // EC-591: the column container the "drop beside" gesture wraps into.
149
+ role: 'layout.grid',
150
+ title: 'Grid',
151
+ category: 'layout',
152
+ props: {
153
+ columns: {
154
+ type: 'number',
155
+ default: 2,
156
+ validation: { min: GRID_MIN_COLUMNS, max: GRID_MAX_COLUMNS },
157
+ form: { label: 'Columns' },
158
+ },
159
+ gap: {
160
+ type: 'select',
161
+ default: '4',
162
+ options: SPACING_OPTIONS,
163
+ form: { label: 'Gap' },
164
+ },
165
+ align: {
166
+ type: 'select',
167
+ default: 'stretch',
168
+ options: ALIGN_OPTIONS,
169
+ form: { label: 'Align' },
170
+ },
171
+ stackOnMobile: {
172
+ // Responsive default (EC-163): stacking is on unless explicitly opted out.
173
+ type: 'boolean',
174
+ default: true,
175
+ form: { label: 'Stack on phones' },
176
+ },
177
+ },
178
+ slots: [{ name: 'children' }],
179
+ });
180
+ // ---------------------------------------------------------------------------
181
+ // Text — a paragraph of text driven by the `value` prop.
182
+ // ---------------------------------------------------------------------------
183
+ function Text(props) {
184
+ // EC-408: the paragraph is the `value` prop's clickable element (studio only).
185
+ return (_jsx("p", { "data-ec-primitive": "Text", ...ecProp('value'), children: asString(props.value) }));
186
+ }
187
+ const TextManifest = defineComponent({
188
+ id: 'Text',
189
+ // EC-591: the block a seeded insert (layout presets) fills with copy (`value`).
190
+ role: 'text',
191
+ title: 'Text',
192
+ category: 'content',
193
+ props: {
194
+ value: {
195
+ type: 'text',
196
+ default: '',
197
+ // Plain-text content prop, editable in place on the canvas (EC-158).
198
+ form: { label: 'Text', control: 'textarea', inlineEditable: true },
199
+ },
200
+ },
201
+ slots: [],
202
+ });
203
+ // ---------------------------------------------------------------------------
204
+ // Heading — h1..h6 heading driven by `text` + `level`.
205
+ // ---------------------------------------------------------------------------
206
+ function Heading(props) {
207
+ const level = typeof props.level === 'number' && props.level >= 1 && props.level <= 6 ? props.level : 2;
208
+ const tag = `h${level}`;
209
+ // EC-408: the heading element is the `text` prop's clickable element.
210
+ return createElement(tag, { 'data-ec-primitive': 'Heading', ...ecProp('text') }, asString(props.text));
211
+ }
212
+ const HeadingManifest = defineComponent({
213
+ id: 'Heading',
214
+ title: 'Heading',
215
+ category: 'content',
216
+ props: {
217
+ text: {
218
+ type: 'text',
219
+ default: '',
220
+ // Plain-text content prop, editable in place on the canvas (EC-158).
221
+ form: { label: 'Text', inlineEditable: true },
222
+ },
223
+ level: {
224
+ // A numeric heading level 1–6 (the impl clamps out-of-range to 2).
225
+ type: 'number',
226
+ default: 2,
227
+ validation: { min: 1, max: 6 },
228
+ form: { label: 'Level' },
229
+ },
230
+ },
231
+ slots: [],
232
+ });
233
+ // ---------------------------------------------------------------------------
234
+ // Image — an <img> with src/alt plus intrinsic dimensions (EC-152). Width and
235
+ // height come from the asset metadata record (bindable to the delivery
236
+ // shape's `width`/`height`) so rendered images reserve layout space — no
237
+ // layout shift. A plain <img> is correct for non-Next contexts (the builder
238
+ // canvas runs on Vite); a Next host opts into `withNextImage` from
239
+ // `@elytracms/next`, which swaps this implementation for `ElytraImage`
240
+ // (next/image) by the `image` role, keeping this manifest.
241
+ // ---------------------------------------------------------------------------
242
+ function asPositiveInt(value) {
243
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined;
244
+ }
245
+ /** Read a resolved asset's `focalPoint` defensively (a `{ x, y }` pair in 0–1). */
246
+ function asFocalPoint(value) {
247
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
248
+ const point = value;
249
+ if (typeof point.x === 'number' && typeof point.y === 'number') {
250
+ return { x: point.x, y: point.y };
251
+ }
252
+ }
253
+ return null;
254
+ }
255
+ export function normalizeImageSource(props) {
256
+ const src = props.src;
257
+ const asset = src && typeof src === 'object' && !Array.isArray(src)
258
+ ? src
259
+ : null;
260
+ const url = asset ? asString(asset.url) : asString(src);
261
+ const width = asPositiveInt(props.width) ?? (asset ? asPositiveInt(asset.width) : undefined);
262
+ const height = asPositiveInt(props.height) ?? (asset ? asPositiveInt(asset.height) : undefined);
263
+ const alt = asString(props.alt) || (asset ? asString(asset.alt) : '');
264
+ const focalPoint = asset ? asFocalPoint(asset.focalPoint) : null;
265
+ // EC-420: a resolved asset may carry an LQIP data URI → next/image blurDataURL.
266
+ const lqip = asset && typeof asset.lqip === 'string' && asset.lqip.length > 0 ? asset.lqip : null;
267
+ return { url, width, height, alt, focalPoint, lqip };
268
+ }
269
+ /** EC-230: a focal point becomes a CSS `object-position` string (percent of each axis). */
270
+ export function focalPointObjectPosition(focalPoint) {
271
+ return focalPoint ? `${focalPoint.x * 100}% ${focalPoint.y * 100}%` : undefined;
272
+ }
273
+ function Image(props) {
274
+ const { url, width, height, alt, focalPoint } = normalizeImageSource(props);
275
+ const objectPosition = focalPointObjectPosition(focalPoint);
276
+ // No resolved url (no asset picked yet, or an unresolved id): a visible
277
+ // placeholder, never a broken `<img src="">` (EC-195 / EC-015).
278
+ if (url === '') {
279
+ return (_jsx("span", { "data-ec-primitive": "Image", "data-ec-image": "empty", ...ecProp('src'), role: "img", "aria-label": alt || 'No image selected', children: "No image selected" }));
280
+ }
281
+ return (_jsx("img", { "data-ec-primitive": "Image", ...ecProp('src'), src: url, alt: alt, ...(width !== undefined ? { width } : {}), ...(height !== undefined ? { height } : {}), ...(objectPosition ? { style: { objectPosition } } : {}) }));
282
+ }
283
+ const ImageManifest = defineComponent({
284
+ id: 'Image',
285
+ // EC-591: the image block (asset drop; the Next host's `next/image` swap).
286
+ role: 'image',
287
+ title: 'Image',
288
+ category: 'media',
289
+ props: {
290
+ // EC-195: `asset`-typed, so the editor picks an uploaded asset (the renderer
291
+ // resolves the id to a url + intrinsic dimensions at delivery) — a plain URL
292
+ // string still works (the resolver passes URL-like values through).
293
+ src: {
294
+ type: 'asset',
295
+ multiple: false,
296
+ default: '',
297
+ form: { label: 'Image' },
298
+ },
299
+ alt: {
300
+ type: 'text',
301
+ default: '',
302
+ form: { label: 'Alt text' },
303
+ },
304
+ width: {
305
+ type: 'number',
306
+ validation: { min: 1 },
307
+ form: { label: 'Width' },
308
+ },
309
+ height: {
310
+ type: 'number',
311
+ validation: { min: 1 },
312
+ form: { label: 'Height' },
313
+ },
314
+ // EC-195: above-the-fold images load eagerly (`next/image priority`) so the
315
+ // host can opt the hero image out of lazy-loading.
316
+ priority: {
317
+ type: 'boolean',
318
+ default: false,
319
+ form: { label: 'Load eagerly (above the fold)' },
320
+ },
321
+ },
322
+ slots: [],
323
+ });
324
+ // ---------------------------------------------------------------------------
325
+ // Button — labelled button with an optional href.
326
+ // ---------------------------------------------------------------------------
327
+ function Button(props) {
328
+ const label = asString(props.label, 'Button');
329
+ if (typeof props.href === 'string' && props.href.length > 0) {
330
+ // EC-408: `label` is the Button's clickable/visible prop (of label/href).
331
+ return (_jsx("a", { "data-ec-primitive": "Button", ...ecProp('label'), href: props.href, role: "button", children: label }));
332
+ }
333
+ return (_jsx("button", { "data-ec-primitive": "Button", ...ecProp('label'), type: "button", children: label }));
334
+ }
335
+ const ButtonManifest = defineComponent({
336
+ id: 'Button',
337
+ title: 'Button',
338
+ category: 'content',
339
+ props: {
340
+ label: {
341
+ type: 'text',
342
+ default: 'Button',
343
+ form: { label: 'Label' },
344
+ },
345
+ href: {
346
+ type: 'text',
347
+ form: { label: 'Link' },
348
+ },
349
+ },
350
+ slots: [],
351
+ });
352
+ // ---------------------------------------------------------------------------
353
+ // Repeater — renders its `item` slot template once per item (iteration is done by the
354
+ // renderer; this implementation only frames the rendered items).
355
+ // ---------------------------------------------------------------------------
356
+ function Repeater(props) {
357
+ return _jsx("div", { "data-ec-primitive": "Repeater", children: props.children });
358
+ }
359
+ const RepeaterManifest = defineComponent({
360
+ id: 'Repeater',
361
+ title: 'Repeater',
362
+ category: 'data',
363
+ // EC-190 DD7: iterate-over-a-source is a binding construct designed out of v1.
364
+ // The `items` value (an array) has no static field-def type; it rides node.props
365
+ // undeclared (passing through the renderer) until the binding/iteration removal in
366
+ // slice 5 decides Repeater's fate. No editable static prop is exposed meanwhile.
367
+ props: {},
368
+ slots: [{ name: 'item', label: 'Item template', required: true }],
369
+ iterate: { itemsProp: 'items', itemSlot: 'item' },
370
+ });
371
+ // ---------------------------------------------------------------------------
372
+ // Switch — renders exactly one named case slot, chosen by the `case` prop/binding.
373
+ // ---------------------------------------------------------------------------
374
+ function Switch(props) {
375
+ const key = asString(props.case);
376
+ const selected = props[`case:${key}`] ?? props.fallback;
377
+ return _jsx("div", { "data-ec-primitive": "Switch", children: selected });
378
+ }
379
+ const SwitchManifest = defineComponent({
380
+ id: 'Switch',
381
+ title: 'Switch',
382
+ category: 'data',
383
+ props: {
384
+ case: {
385
+ type: 'text',
386
+ default: '',
387
+ form: { label: 'Case' },
388
+ },
389
+ },
390
+ // Named case slots are dynamic; `fallback` renders when no case matches.
391
+ slots: [{ name: 'fallback', label: 'Fallback' }],
392
+ });
393
+ function entry(manifest, implementation) {
394
+ return { manifest, implementation: implementation };
395
+ }
396
+ export const containerComponent = entry(ContainerManifest, Container);
397
+ export const stackComponent = entry(StackManifest, Stack);
398
+ export const gridComponent = entry(GridManifest, Grid);
399
+ export const textComponent = entry(TextManifest, Text);
400
+ export const headingComponent = entry(HeadingManifest, Heading);
401
+ // Rich-text content component (EC-159) — content prop is the stored
402
+ // `@elytracms/core/rich-text` value, flagged inline-editable.
403
+ export const richTextComponent = entry(RichTextManifest, RichText);
404
+ export const imageComponent = entry(ImageManifest, Image);
405
+ export const buttonComponent = entry(ButtonManifest, Button);
406
+ export const repeaterComponent = entry(RepeaterManifest, Repeater);
407
+ export const switchComponent = entry(SwitchManifest, Switch);
408
+ /**
409
+ * Every component Elytra ships, in palette order — the list a config spreads
410
+ * into its `components` (`[...elytraComponents, ...siteComponents]`). Pick a
411
+ * subset by listing the individual entries instead; replace one by listing your
412
+ * own component under the id (and leaving the shipped entry out — the list names
413
+ * each id once, a second entry is a `duplicate-id` issue).
414
+ */
415
+ export const elytraComponents = [
416
+ containerComponent,
417
+ stackComponent,
418
+ gridComponent,
419
+ textComponent,
420
+ headingComponent,
421
+ richTextComponent,
422
+ imageComponent,
423
+ buttonComponent,
424
+ repeaterComponent,
425
+ switchComponent,
426
+ ];
427
+ export { Container, Stack, Grid, Text, Heading, Image, Button, Repeater, Switch, ContainerManifest, StackManifest, GridManifest, TextManifest, HeadingManifest, ImageManifest, ButtonManifest, RepeaterManifest, SwitchManifest, };
428
+ //# sourceMappingURL=primitives.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"primitives.js","sourceRoot":"","sources":["../src/primitives.tsx"],"names":[],"mappings":"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","sourcesContent":["import { createElement } from 'react'\nimport type { ComponentType, CSSProperties, ReactNode } from 'react'\nimport {\n CONTAINER_ALIGNMENTS,\n GRID_MAX_COLUMNS,\n GRID_MIN_COLUMNS,\n SPACING_TOKEN_NAMES,\n spacingTokenCss,\n} from '@elytracms/core/project-graph'\nimport type { ContainerAlignment } from '@elytracms/core/project-graph'\nimport { defineComponent } from '@elytracms/core/component-registry'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { ecProp } from '@elytracms/core/runtime-renderer'\nimport { RichText, RichTextManifest } from './rich-text'\n\n/**\n * The components Elytra ships (EC-591) — moved verbatim from\n * `@elytracms/core/runtime-renderer`'s primitives, where the engine registered\n * them itself under `base.*` ids. Now nothing registers them: they are\n * `{ manifest, implementation }` entries a config opts into by listing them\n * (`components: [...elytraComponents, ...siteComponents]`), a subset, or none.\n * Plain ids, the developer's to keep or replace; the five canvas roles are\n * declared on the manifests that fill them (`Stack` layout.stack, `Grid`\n * layout.grid, `Text` text, `RichText` richText, `Image` image).\n */\n\n/**\n * Common shape for primitive implementations: resolved props (arbitrary keys) plus React\n * children for the default slot and any named-slot props.\n */\ntype PrimitiveProps = Record<string, unknown> & { children?: ReactNode }\n\nfunction asString(value: unknown, fallback = ''): string {\n return typeof value === 'string' ? value : fallback\n}\n\n// ---------------------------------------------------------------------------\n// Layout-container styling (EC-161) — gap/alignment resolve from the canonical\n// design-token spacing scale; legacy numeric gaps (`n` → `n * 4px`) stay\n// renderable so pre-token graphs keep their layout.\n// ---------------------------------------------------------------------------\n\nfunction gapCss(value: unknown): string | undefined {\n if (typeof value === 'string') return spacingTokenCss(value)\n if (typeof value === 'number' && Number.isFinite(value) && value > 0) return `${value * 4}px`\n return undefined\n}\n\nconst ALIGN_ITEMS: Record<ContainerAlignment, CSSProperties['alignItems']> = {\n start: 'flex-start',\n center: 'center',\n end: 'flex-end',\n stretch: 'stretch',\n}\n\n/** `alignItems` for a container `align` prop; undefined for `stretch` (the CSS default) or junk. */\nfunction alignItemsCss(value: unknown): CSSProperties['alignItems'] {\n if (value === 'start' || value === 'center' || value === 'end') return ALIGN_ITEMS[value]\n return undefined\n}\n\nconst SPACING_OPTIONS = SPACING_TOKEN_NAMES.map((name) => ({ label: name, value: name }))\nconst ALIGN_OPTIONS = CONTAINER_ALIGNMENTS.map((name) => ({ label: name, value: name }))\n\n// ---------------------------------------------------------------------------\n// Container — a generic block wrapper with a single default slot.\n// ---------------------------------------------------------------------------\n\nfunction Container(props: PrimitiveProps): ReactNode {\n // The default `children` slot and the layout `outlet` slot both render here; the outlet\n // is where `renderCompositionInLayout` injects the page body when this Container is a layout root.\n return (\n <div data-ec-primitive=\"Container\">\n {props.children}\n {props.outlet as ReactNode}\n </div>\n )\n}\n\nconst ContainerManifest = defineComponent({\n id: 'Container',\n title: 'Container',\n category: 'layout',\n props: {},\n // The outlet is filled by page injection, so it is not authored-required.\n slots: [{ name: 'children' }, { name: 'outlet' }],\n})\n\n// ---------------------------------------------------------------------------\n// Stack — flex layout container (EC-161): direction, token-bound gap, alignment.\n// ---------------------------------------------------------------------------\n\nfunction Stack(props: PrimitiveProps): ReactNode {\n const style: CSSProperties = {\n display: 'flex',\n flexDirection: props.direction === 'horizontal' ? 'row' : 'column',\n }\n const gap = gapCss(props.gap)\n if (gap !== undefined) style.gap = gap\n const alignItems = alignItemsCss(props.align)\n if (alignItems !== undefined) style.alignItems = alignItems\n return (\n <div data-ec-primitive=\"Stack\" style={style}>\n {props.children}\n </div>\n )\n}\n\nconst StackManifest = defineComponent({\n id: 'Stack',\n // EC-591: implicit layout's flow container and the canvas's synthetic root.\n role: 'layout.stack',\n title: 'Stack',\n category: 'layout',\n props: {\n direction: {\n type: 'select',\n default: 'vertical',\n options: [\n { value: 'vertical', label: 'Vertical' },\n { value: 'horizontal', label: 'Horizontal' },\n ],\n form: { label: 'Direction' },\n },\n gap: {\n // A design-token spacing step name (EC-161); legacy numeric gaps still render.\n type: 'select',\n default: '4',\n options: SPACING_OPTIONS,\n form: { label: 'Gap' },\n },\n align: {\n type: 'select',\n default: 'stretch',\n options: ALIGN_OPTIONS,\n form: { label: 'Align' },\n },\n },\n slots: [{ name: 'children' }],\n})\n\n// ---------------------------------------------------------------------------\n// Grid — CSS grid container (EC-161): column count, token-bound gap, alignment.\n// ---------------------------------------------------------------------------\n\nfunction asColumns(value: unknown): number {\n if (typeof value !== 'number' || !Number.isFinite(value)) return GRID_MIN_COLUMNS\n return Math.min(Math.max(GRID_MIN_COLUMNS, Math.floor(value)), GRID_MAX_COLUMNS)\n}\n\n/**\n * Responsive defaults (EC-163): below this viewport width a Grid's columns\n * stack into one. 640px (the Tailwind `sm` boundary) sits between the\n * builder's deterministic preview widths — phone (390px) stacks, tablet\n * (768px) keeps columns.\n */\nexport const GRID_MOBILE_BREAKPOINT_PX = 640\n\n/**\n * The stack-on-mobile rule a stacking Grid carries with it. A **viewport media\n * query** (not a container query), deliberately: the builder canvas renders\n * into an iframe — a true viewport — so constraining the iframe to the phone\n * width makes this exact rule fire in the preview; and the identical markup\n * server-renders in `@elytracms/next` (App Router SSR) with no client JS, no\n * extra stylesheet wiring, and no `container-type` context that every consumer\n * would otherwise have to guarantee around every grid. The rule is emitted as\n * an inline `<style>` child of the grid itself (style elements generate no\n * grid item), so the behavior is self-contained wherever the implementation\n * renders — canvas iframe portal, static markup, or RSC payload — and the\n * `!important` lets the stylesheet rule beat the grid's own inline column\n * template. Opted-out grids (`stackOnMobile: false`) carry neither the\n * attribute nor the rule.\n */\nexport const GRID_STACK_CSS = `@media (max-width: ${GRID_MOBILE_BREAKPOINT_PX}px){[data-ec-grid-stack]{grid-template-columns:minmax(0,1fr) !important}}`\n\nfunction Grid(props: PrimitiveProps): ReactNode {\n const style: CSSProperties = {\n display: 'grid',\n gridTemplateColumns: `repeat(${asColumns(props.columns)}, minmax(0, 1fr))`,\n }\n const gap = gapCss(props.gap)\n if (gap !== undefined) style.gap = gap\n const alignItems = alignItemsCss(props.align)\n if (alignItems !== undefined) style.alignItems = alignItems\n // Stack-on-mobile is the default; only the explicit opt-out keeps columns.\n const stacksOnMobile = props.stackOnMobile !== false\n return (\n <div\n data-ec-primitive=\"Grid\"\n {...(stacksOnMobile ? { 'data-ec-grid-stack': '' } : {})}\n style={style}\n >\n {stacksOnMobile ? <style>{GRID_STACK_CSS}</style> : null}\n {props.children}\n </div>\n )\n}\n\nconst GridManifest = defineComponent({\n id: 'Grid',\n // EC-591: the column container the \"drop beside\" gesture wraps into.\n role: 'layout.grid',\n title: 'Grid',\n category: 'layout',\n props: {\n columns: {\n type: 'number',\n default: 2,\n validation: { min: GRID_MIN_COLUMNS, max: GRID_MAX_COLUMNS },\n form: { label: 'Columns' },\n },\n gap: {\n type: 'select',\n default: '4',\n options: SPACING_OPTIONS,\n form: { label: 'Gap' },\n },\n align: {\n type: 'select',\n default: 'stretch',\n options: ALIGN_OPTIONS,\n form: { label: 'Align' },\n },\n stackOnMobile: {\n // Responsive default (EC-163): stacking is on unless explicitly opted out.\n type: 'boolean',\n default: true,\n form: { label: 'Stack on phones' },\n },\n },\n slots: [{ name: 'children' }],\n})\n\n// ---------------------------------------------------------------------------\n// Text — a paragraph of text driven by the `value` prop.\n// ---------------------------------------------------------------------------\n\nfunction Text(props: PrimitiveProps): ReactNode {\n // EC-408: the paragraph is the `value` prop's clickable element (studio only).\n return (\n <p data-ec-primitive=\"Text\" {...ecProp('value')}>\n {asString(props.value)}\n </p>\n )\n}\n\nconst TextManifest = defineComponent({\n id: 'Text',\n // EC-591: the block a seeded insert (layout presets) fills with copy (`value`).\n role: 'text',\n title: 'Text',\n category: 'content',\n props: {\n value: {\n type: 'text',\n default: '',\n // Plain-text content prop, editable in place on the canvas (EC-158).\n form: { label: 'Text', control: 'textarea', inlineEditable: true },\n },\n },\n slots: [],\n})\n\n// ---------------------------------------------------------------------------\n// Heading — h1..h6 heading driven by `text` + `level`.\n// ---------------------------------------------------------------------------\n\nfunction Heading(props: PrimitiveProps): ReactNode {\n const level =\n typeof props.level === 'number' && props.level >= 1 && props.level <= 6 ? props.level : 2\n const tag = `h${level}`\n // EC-408: the heading element is the `text` prop's clickable element.\n return createElement(tag, { 'data-ec-primitive': 'Heading', ...ecProp('text') }, asString(props.text))\n}\n\nconst HeadingManifest = defineComponent({\n id: 'Heading',\n title: 'Heading',\n category: 'content',\n props: {\n text: {\n type: 'text',\n default: '',\n // Plain-text content prop, editable in place on the canvas (EC-158).\n form: { label: 'Text', inlineEditable: true },\n },\n level: {\n // A numeric heading level 1–6 (the impl clamps out-of-range to 2).\n type: 'number',\n default: 2,\n validation: { min: 1, max: 6 },\n form: { label: 'Level' },\n },\n },\n slots: [],\n})\n\n// ---------------------------------------------------------------------------\n// Image — an <img> with src/alt plus intrinsic dimensions (EC-152). Width and\n// height come from the asset metadata record (bindable to the delivery\n// shape's `width`/`height`) so rendered images reserve layout space — no\n// layout shift. A plain <img> is correct for non-Next contexts (the builder\n// canvas runs on Vite); a Next host opts into `withNextImage` from\n// `@elytracms/next`, which swaps this implementation for `ElytraImage`\n// (next/image) by the `image` role, keeping this manifest.\n// ---------------------------------------------------------------------------\n\nfunction asPositiveInt(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined\n}\n\n/**\n * The Image `src` may be a resolved asset (EC-195: an `asset`-typed prop the\n * renderer resolved from an asset id) or a plain URL string. Normalize to\n * `{ url, width, height, alt }`, letting the asset's intrinsic dimensions/alt\n * fill in when the explicit props are unset — so an uploaded image reserves the\n * right box (no layout shift) and carries its alt text by default.\n */\nexport interface ImageSource {\n url: string\n width: number | undefined\n height: number | undefined\n alt: string\n /** Normalized focal point `{ x, y }` 0–1 (EC-230) → CSS object-position; null when none. */\n focalPoint: { x: number; y: number } | null\n /** LQIP blur-placeholder data URI (EC-420) → `next/image` blurDataURL; null when none. */\n lqip: string | null\n}\n\n/** Read a resolved asset's `focalPoint` defensively (a `{ x, y }` pair in 0–1). */\nfunction asFocalPoint(value: unknown): { x: number; y: number } | null {\n if (value && typeof value === 'object' && !Array.isArray(value)) {\n const point = value as { x?: unknown; y?: unknown }\n if (typeof point.x === 'number' && typeof point.y === 'number') {\n return { x: point.x, y: point.y }\n }\n }\n return null\n}\n\nexport function normalizeImageSource(props: PrimitiveProps): ImageSource {\n const src = props.src\n const asset =\n src && typeof src === 'object' && !Array.isArray(src)\n ? (src as {\n url?: unknown\n width?: unknown\n height?: unknown\n alt?: unknown\n focalPoint?: unknown\n lqip?: unknown\n })\n : null\n const url = asset ? asString(asset.url) : asString(src)\n const width = asPositiveInt(props.width) ?? (asset ? asPositiveInt(asset.width) : undefined)\n const height = asPositiveInt(props.height) ?? (asset ? asPositiveInt(asset.height) : undefined)\n const alt = asString(props.alt) || (asset ? asString(asset.alt) : '')\n const focalPoint = asset ? asFocalPoint(asset.focalPoint) : null\n // EC-420: a resolved asset may carry an LQIP data URI → next/image blurDataURL.\n const lqip = asset && typeof asset.lqip === 'string' && asset.lqip.length > 0 ? asset.lqip : null\n return { url, width, height, alt, focalPoint, lqip }\n}\n\n/** EC-230: a focal point becomes a CSS `object-position` string (percent of each axis). */\nexport function focalPointObjectPosition(focalPoint: { x: number; y: number } | null): string | undefined {\n return focalPoint ? `${focalPoint.x * 100}% ${focalPoint.y * 100}%` : undefined\n}\n\nfunction Image(props: PrimitiveProps): ReactNode {\n const { url, width, height, alt, focalPoint } = normalizeImageSource(props)\n const objectPosition = focalPointObjectPosition(focalPoint)\n // No resolved url (no asset picked yet, or an unresolved id): a visible\n // placeholder, never a broken `<img src=\"\">` (EC-195 / EC-015).\n if (url === '') {\n return (\n <span\n data-ec-primitive=\"Image\"\n data-ec-image=\"empty\"\n // EC-408: `src` is the Image's primary/clickable prop (of src/alt/width/…).\n {...ecProp('src')}\n role=\"img\"\n aria-label={alt || 'No image selected'}\n >\n No image selected\n </span>\n )\n }\n return (\n <img\n data-ec-primitive=\"Image\"\n // EC-408: `src` is the Image's primary/clickable prop (of src/alt/width/…).\n {...ecProp('src')}\n src={url}\n alt={alt}\n {...(width !== undefined ? { width } : {})}\n {...(height !== undefined ? { height } : {})}\n {...(objectPosition ? { style: { objectPosition } } : {})}\n />\n )\n}\n\nconst ImageManifest = defineComponent({\n id: 'Image',\n // EC-591: the image block (asset drop; the Next host's `next/image` swap).\n role: 'image',\n title: 'Image',\n category: 'media',\n props: {\n // EC-195: `asset`-typed, so the editor picks an uploaded asset (the renderer\n // resolves the id to a url + intrinsic dimensions at delivery) — a plain URL\n // string still works (the resolver passes URL-like values through).\n src: {\n type: 'asset',\n multiple: false,\n default: '',\n form: { label: 'Image' },\n },\n alt: {\n type: 'text',\n default: '',\n form: { label: 'Alt text' },\n },\n width: {\n type: 'number',\n validation: { min: 1 },\n form: { label: 'Width' },\n },\n height: {\n type: 'number',\n validation: { min: 1 },\n form: { label: 'Height' },\n },\n // EC-195: above-the-fold images load eagerly (`next/image priority`) so the\n // host can opt the hero image out of lazy-loading.\n priority: {\n type: 'boolean',\n default: false,\n form: { label: 'Load eagerly (above the fold)' },\n },\n },\n slots: [],\n})\n\n// ---------------------------------------------------------------------------\n// Button — labelled button with an optional href.\n// ---------------------------------------------------------------------------\n\nfunction Button(props: PrimitiveProps): ReactNode {\n const label = asString(props.label, 'Button')\n if (typeof props.href === 'string' && props.href.length > 0) {\n // EC-408: `label` is the Button's clickable/visible prop (of label/href).\n return (\n <a data-ec-primitive=\"Button\" {...ecProp('label')} href={props.href} role=\"button\">\n {label}\n </a>\n )\n }\n return (\n <button data-ec-primitive=\"Button\" {...ecProp('label')} type=\"button\">\n {label}\n </button>\n )\n}\n\nconst ButtonManifest = defineComponent({\n id: 'Button',\n title: 'Button',\n category: 'content',\n props: {\n label: {\n type: 'text',\n default: 'Button',\n form: { label: 'Label' },\n },\n href: {\n type: 'text',\n form: { label: 'Link' },\n },\n },\n slots: [],\n})\n\n// ---------------------------------------------------------------------------\n// Repeater — renders its `item` slot template once per item (iteration is done by the\n// renderer; this implementation only frames the rendered items).\n// ---------------------------------------------------------------------------\n\nfunction Repeater(props: PrimitiveProps): ReactNode {\n return <div data-ec-primitive=\"Repeater\">{props.children}</div>\n}\n\nconst RepeaterManifest = defineComponent({\n id: 'Repeater',\n title: 'Repeater',\n category: 'data',\n // EC-190 DD7: iterate-over-a-source is a binding construct designed out of v1.\n // The `items` value (an array) has no static field-def type; it rides node.props\n // undeclared (passing through the renderer) until the binding/iteration removal in\n // slice 5 decides Repeater's fate. No editable static prop is exposed meanwhile.\n props: {},\n slots: [{ name: 'item', label: 'Item template', required: true }],\n iterate: { itemsProp: 'items', itemSlot: 'item' },\n})\n\n// ---------------------------------------------------------------------------\n// Switch — renders exactly one named case slot, chosen by the `case` prop/binding.\n// ---------------------------------------------------------------------------\n\nfunction Switch(props: PrimitiveProps): ReactNode {\n const key = asString(props.case)\n const selected = props[`case:${key}`] ?? props.fallback\n return <div data-ec-primitive=\"Switch\">{selected as ReactNode}</div>\n}\n\nconst SwitchManifest = defineComponent({\n id: 'Switch',\n title: 'Switch',\n category: 'data',\n props: {\n case: {\n type: 'text',\n default: '',\n form: { label: 'Case' },\n },\n },\n // Named case slots are dynamic; `fallback` renders when no case matches.\n slots: [{ name: 'fallback', label: 'Fallback' }],\n})\n\n// ---------------------------------------------------------------------------\n// Aggregation\n// ---------------------------------------------------------------------------\n\n/**\n * One shipped component: its manifest plus the real implementation. Strictly\n * typed (the implementation IS a component), and assignable to the config's\n * `HostComponentEntry` and core's `ComponentEntry`, so the entries list straight\n * into `elytra.config.ts`'s `components`.\n */\nexport interface ElytraComponent {\n manifest: ComponentManifest\n implementation: ComponentType<Record<string, unknown>>\n}\n\nfunction entry(manifest: ComponentManifest, implementation: ComponentType<PrimitiveProps>): ElytraComponent {\n return { manifest, implementation: implementation as ComponentType<Record<string, unknown>> }\n}\n\nexport const containerComponent = entry(ContainerManifest, Container)\nexport const stackComponent = entry(StackManifest, Stack)\nexport const gridComponent = entry(GridManifest, Grid)\nexport const textComponent = entry(TextManifest, Text)\nexport const headingComponent = entry(HeadingManifest, Heading)\n// Rich-text content component (EC-159) — content prop is the stored\n// `@elytracms/core/rich-text` value, flagged inline-editable.\nexport const richTextComponent = entry(RichTextManifest, RichText)\nexport const imageComponent = entry(ImageManifest, Image)\nexport const buttonComponent = entry(ButtonManifest, Button)\nexport const repeaterComponent = entry(RepeaterManifest, Repeater)\nexport const switchComponent = entry(SwitchManifest, Switch)\n\n/**\n * Every component Elytra ships, in palette order — the list a config spreads\n * into its `components` (`[...elytraComponents, ...siteComponents]`). Pick a\n * subset by listing the individual entries instead; replace one by listing your\n * own component under the id (and leaving the shipped entry out — the list names\n * each id once, a second entry is a `duplicate-id` issue).\n */\nexport const elytraComponents: readonly ElytraComponent[] = [\n containerComponent,\n stackComponent,\n gridComponent,\n textComponent,\n headingComponent,\n richTextComponent,\n imageComponent,\n buttonComponent,\n repeaterComponent,\n switchComponent,\n]\n\nexport {\n Container,\n Stack,\n Grid,\n Text,\n Heading,\n Image,\n Button,\n Repeater,\n Switch,\n ContainerManifest,\n StackManifest,\n GridManifest,\n TextManifest,\n HeadingManifest,\n ImageManifest,\n ButtonManifest,\n RepeaterManifest,\n SwitchManifest,\n}\n"]}
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { RichTextValue } from '@elytracms/core/rich-text';
3
+ /**
4
+ * The implementation's own content for an absent `content` prop — what a freshly
5
+ * inserted RichText renders (EC-591/2f: an absent prop is `undefined` and THIS is
6
+ * the implementation's default; the manifest `default` below is only the
7
+ * inspector's hint).
8
+ */
9
+ export declare const RICH_TEXT_DEFAULT_VALUE: RichTextValue;
10
+ export declare function RichText(props: Record<string, unknown> & {
11
+ children?: ReactNode;
12
+ }): ReactNode;
13
+ export declare const RichTextManifest: import("@elytracms/core/component-registry").ComponentManifest;
@@ -0,0 +1,120 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { defineComponent } from '@elytracms/core/component-registry';
3
+ import { parseRichTextValue, renderRichTextValue } from '@elytracms/core/rich-text';
4
+ import { ecProp, richTextPrimitiveRegistry } from '@elytracms/core/runtime-renderer';
5
+ /**
6
+ * RichText — the rich-text content component (EC-159), filling the `richText`
7
+ * role (EC-591). Its `content` prop is a stored `@elytracms/core/rich-text` value
8
+ * (the versioned, ProseMirror-shaped storage JSON); rendering goes through the
9
+ * package's server-safe renderer with the project's custom block/inline registry
10
+ * (`richTextPrimitiveRegistry`, the same one a document body's prose renders
11
+ * with), so custom blocks render identically on the canvas, in fixtures, and in an
12
+ * exported app. Unknown node types degrade to the renderer's visible fallback —
13
+ * never a crash, never silent.
14
+ *
15
+ * The prop is flagged `inlineEditable`: the builder canvas edits the rendered
16
+ * element in place (EC-158 semantics extended to rich text), committing back
17
+ * to the same storage format.
18
+ *
19
+ * EC-591: moved from `@elytracms/core/runtime-renderer` (where it was registered
20
+ * by the engine as `base.RichText`) into this opt-in package; id `RichText`.
21
+ */
22
+ /**
23
+ * Stored rich-text values are plain JSON by construction (Zod-validated, no
24
+ * runtime handles), but `RichTextNode` is an interface without an index
25
+ * signature, so the structural `JsonValue` assignment needs this one cast.
26
+ */
27
+ function asJson(value) {
28
+ return value;
29
+ }
30
+ /**
31
+ * The implementation's own content for an absent `content` prop — what a freshly
32
+ * inserted RichText renders (EC-591/2f: an absent prop is `undefined` and THIS is
33
+ * the implementation's default; the manifest `default` below is only the
34
+ * inspector's hint).
35
+ */
36
+ export const RICH_TEXT_DEFAULT_VALUE = {
37
+ version: 2,
38
+ content: [
39
+ {
40
+ type: 'paragraph',
41
+ content: [{ type: 'text', text: 'Rich text. Select to format, double-click to edit.' }],
42
+ },
43
+ ],
44
+ };
45
+ /** A fixture document exercising marks, a link, a list, and the custom blocks. */
46
+ const RICH_TEXT_FIXTURE_VALUE = {
47
+ version: 2,
48
+ content: [
49
+ { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: 'Rich text block' }] },
50
+ {
51
+ type: 'paragraph',
52
+ content: [
53
+ { type: 'text', text: 'Formatted ' },
54
+ { type: 'text', text: 'bold', marks: [{ type: 'bold' }] },
55
+ { type: 'text', text: ' and ' },
56
+ { type: 'text', text: 'italic', marks: [{ type: 'italic' }] },
57
+ { type: 'text', text: ' copy with a ' },
58
+ { type: 'text', text: 'link', marks: [{ type: 'link', attrs: { href: 'https://example.com' } }] },
59
+ { type: 'text', text: '.' },
60
+ ],
61
+ },
62
+ {
63
+ type: 'callout',
64
+ attrs: { tone: 'info' },
65
+ content: [{ type: 'text', text: 'A custom block, rendered by the project registry.' }],
66
+ },
67
+ ],
68
+ };
69
+ export function RichText(props) {
70
+ // The embed renderer (EC-189/EC-547), injected by the renderer as a prop (bound
71
+ // to `renderComposition`). A prop — not a `'use client'` React context — keeps
72
+ // this server-safe: the renderer runs in the host's RSC pass, and a function
73
+ // cannot cross the server→client boundary into a context provider. When present,
74
+ // `elytra/component` nodes render through the same pipeline as a page/block
75
+ // field; when absent (a bare render), components degrade to the visible fallback.
76
+ const renderEmbed = typeof props.renderEmbed === 'function'
77
+ ? props.renderEmbed
78
+ : undefined;
79
+ // EC-290: the ref resolver, injected by the renderer as a prop (bound to the
80
+ // host's `resolveAsset`/`resolveLink`), turns self-typed reference links/asset
81
+ // pins (`attrs.ref`, EC-277) into real URLs. Absent → reference links degrade
82
+ // visibly with an `unresolved-ref` issue, never a silent `#`.
83
+ const resolveRef = typeof props.resolveRef === 'function'
84
+ ? props.resolveRef
85
+ : undefined;
86
+ // An absent `content` renders the implementation's own default (the renderer
87
+ // passes absent props as `undefined` — EC-591/2f).
88
+ const parsed = parseRichTextValue(props.content ?? RICH_TEXT_DEFAULT_VALUE);
89
+ if (!parsed.ok) {
90
+ // The renderer withholds a schema-invalid static value (EC-591: it arrives
91
+ // absent, so this branch sees only values it could not judge) — visibly.
92
+ return (_jsx("div", { "data-ec-primitive": "RichText", className: "rich-text-invalid", role: "note", children: "Invalid rich-text content." }));
93
+ }
94
+ return (_jsx("div", { "data-ec-primitive": "RichText", ...ecProp('content'), children: renderRichTextValue(parsed.value, { registry: richTextPrimitiveRegistry, renderEmbed, resolveRef }) }));
95
+ }
96
+ export const RichTextManifest = defineComponent({
97
+ id: 'RichText',
98
+ role: 'richText',
99
+ title: 'Rich Text',
100
+ category: 'content',
101
+ description: 'Formatted long-form content stored in the rich-text format.',
102
+ props: {
103
+ content: {
104
+ // The structured @elytracms/rich-text storage value (EC-159); validated by
105
+ // the renderer's parse guard rather than a scalar string check.
106
+ type: 'richText',
107
+ // A hint only (EC-591/2f) — the inspector shows it greyed; the engine never
108
+ // writes it into a node.
109
+ default: asJson(RICH_TEXT_DEFAULT_VALUE),
110
+ // Rich-text content prop, editable in place on the canvas (EC-159).
111
+ form: { label: 'Content', inlineEditable: true },
112
+ },
113
+ },
114
+ slots: [],
115
+ fixtures: [
116
+ { name: 'Default' },
117
+ { name: 'Formatted', props: { content: asJson(RICH_TEXT_FIXTURE_VALUE) } },
118
+ ],
119
+ });
120
+ //# sourceMappingURL=rich-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rich-text.js","sourceRoot":"","sources":["../src/rich-text.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { ReactNode } from 'react'\nimport { defineComponent } from '@elytracms/core/component-registry'\nimport { parseRichTextValue, renderRichTextValue } from '@elytracms/core/rich-text'\nimport type { RichTextValue, RichTextEmbedRenderer, RichTextRefResolver } from '@elytracms/core/rich-text'\nimport type { JsonValue } from '@elytracms/core/project-graph'\nimport { ecProp, richTextPrimitiveRegistry } from '@elytracms/core/runtime-renderer'\n\n/**\n * RichText — the rich-text content component (EC-159), filling the `richText`\n * role (EC-591). Its `content` prop is a stored `@elytracms/core/rich-text` value\n * (the versioned, ProseMirror-shaped storage JSON); rendering goes through the\n * package's server-safe renderer with the project's custom block/inline registry\n * (`richTextPrimitiveRegistry`, the same one a document body's prose renders\n * with), so custom blocks render identically on the canvas, in fixtures, and in an\n * exported app. Unknown node types degrade to the renderer's visible fallback —\n * never a crash, never silent.\n *\n * The prop is flagged `inlineEditable`: the builder canvas edits the rendered\n * element in place (EC-158 semantics extended to rich text), committing back\n * to the same storage format.\n *\n * EC-591: moved from `@elytracms/core/runtime-renderer` (where it was registered\n * by the engine as `base.RichText`) into this opt-in package; id `RichText`.\n */\n\n/**\n * Stored rich-text values are plain JSON by construction (Zod-validated, no\n * runtime handles), but `RichTextNode` is an interface without an index\n * signature, so the structural `JsonValue` assignment needs this one cast.\n */\nfunction asJson(value: RichTextValue): JsonValue {\n return value as unknown as JsonValue\n}\n\n/**\n * The implementation's own content for an absent `content` prop — what a freshly\n * inserted RichText renders (EC-591/2f: an absent prop is `undefined` and THIS is\n * the implementation's default; the manifest `default` below is only the\n * inspector's hint).\n */\nexport const RICH_TEXT_DEFAULT_VALUE: RichTextValue = {\n version: 2,\n content: [\n {\n type: 'paragraph',\n content: [{ type: 'text', text: 'Rich text. Select to format, double-click to edit.' }],\n },\n ],\n}\n\n/** A fixture document exercising marks, a link, a list, and the custom blocks. */\nconst RICH_TEXT_FIXTURE_VALUE: RichTextValue = {\n version: 2,\n content: [\n { type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: 'Rich text block' }] },\n {\n type: 'paragraph',\n content: [\n { type: 'text', text: 'Formatted ' },\n { type: 'text', text: 'bold', marks: [{ type: 'bold' }] },\n { type: 'text', text: ' and ' },\n { type: 'text', text: 'italic', marks: [{ type: 'italic' }] },\n { type: 'text', text: ' copy with a ' },\n { type: 'text', text: 'link', marks: [{ type: 'link', attrs: { href: 'https://example.com' } }] },\n { type: 'text', text: '.' },\n ],\n },\n {\n type: 'callout',\n attrs: { tone: 'info' },\n content: [{ type: 'text', text: 'A custom block, rendered by the project registry.' }],\n },\n ],\n}\n\nexport function RichText(props: Record<string, unknown> & { children?: ReactNode }): ReactNode {\n // The embed renderer (EC-189/EC-547), injected by the renderer as a prop (bound\n // to `renderComposition`). A prop — not a `'use client'` React context — keeps\n // this server-safe: the renderer runs in the host's RSC pass, and a function\n // cannot cross the server→client boundary into a context provider. When present,\n // `elytra/component` nodes render through the same pipeline as a page/block\n // field; when absent (a bare render), components degrade to the visible fallback.\n const renderEmbed =\n typeof props.renderEmbed === 'function'\n ? (props.renderEmbed as RichTextEmbedRenderer)\n : undefined\n // EC-290: the ref resolver, injected by the renderer as a prop (bound to the\n // host's `resolveAsset`/`resolveLink`), turns self-typed reference links/asset\n // pins (`attrs.ref`, EC-277) into real URLs. Absent → reference links degrade\n // visibly with an `unresolved-ref` issue, never a silent `#`.\n const resolveRef =\n typeof props.resolveRef === 'function'\n ? (props.resolveRef as RichTextRefResolver)\n : undefined\n // An absent `content` renders the implementation's own default (the renderer\n // passes absent props as `undefined` — EC-591/2f).\n const parsed = parseRichTextValue(props.content ?? RICH_TEXT_DEFAULT_VALUE)\n if (!parsed.ok) {\n // The renderer withholds a schema-invalid static value (EC-591: it arrives\n // absent, so this branch sees only values it could not judge) — visibly.\n return (\n <div data-ec-primitive=\"RichText\" className=\"rich-text-invalid\" role=\"note\">\n Invalid rich-text content.\n </div>\n )\n }\n return (\n // EC-408: the RichText region is the `content` prop's element. `content` is\n // inline-editable, so a click defers to inline editing (field-focus is skipped);\n // the marker still drives the reverse focus→highlight and node-scoped resolution.\n <div data-ec-primitive=\"RichText\" {...ecProp('content')}>\n {renderRichTextValue(parsed.value, { registry: richTextPrimitiveRegistry, renderEmbed, resolveRef })}\n </div>\n )\n}\n\nexport const RichTextManifest = defineComponent({\n id: 'RichText',\n role: 'richText',\n title: 'Rich Text',\n category: 'content',\n description: 'Formatted long-form content stored in the rich-text format.',\n props: {\n content: {\n // The structured @elytracms/rich-text storage value (EC-159); validated by\n // the renderer's parse guard rather than a scalar string check.\n type: 'richText',\n // A hint only (EC-591/2f) — the inspector shows it greyed; the engine never\n // writes it into a node.\n default: asJson(RICH_TEXT_DEFAULT_VALUE),\n // Rich-text content prop, editable in place on the canvas (EC-159).\n form: { label: 'Content', inlineEditable: true },\n },\n },\n slots: [],\n fixtures: [\n { name: 'Default' },\n { name: 'Formatted', props: { content: asJson(RICH_TEXT_FIXTURE_VALUE) } },\n ],\n})\n"]}
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@elytracms/components",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "author": "finaldream <hi@finaldream.de>",
4
+ "version": "0.0.92",
5
+ "engines": {
6
+ "node": ">=22.18.0"
7
+ },
8
+ "description": "The components Elytra ships (Stack, Grid, Container, Text, Heading, RichText, Image, Button, Repeater, Switch) as an opt-in list of { manifest, implementation } entries — a config lists what it wants; the engine registers nothing.",
9
+ "license": "MIT",
10
+ "private": false,
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "dependencies": {
19
+ "@elytracms/core": "^0.0.92"
20
+ },
21
+ "peerDependencies": {
22
+ "react": "^19.0.0",
23
+ "react-dom": "^19.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^26.1.0",
27
+ "@types/react": "^19.2.17",
28
+ "@types/react-dom": "^19.2.3",
29
+ "react": "^19.2.7",
30
+ "react-dom": "^19.2.7",
31
+ "typescript": "^6.0.3"
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node ../../scripts/fix-esm-extensions.mjs dist && node ../../scripts/check-esm-extensions.mjs dist",
41
+ "typecheck": "tsc --noEmit",
42
+ "lint": "eslint src"
43
+ }
6
44
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.