@storystick/react 0.0.0-stage → 0.2.0-rc.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.
Files changed (102) hide show
  1. package/LICENSE +112 -0
  2. package/NOTICE +17 -0
  3. package/PUBLIC-API.md +36 -0
  4. package/README.md +226 -2
  5. package/dist/.source-hash +1 -0
  6. package/dist/file.d.ts +18 -0
  7. package/dist/file.js +44 -0
  8. package/dist/hydrate-slice.d.ts +3 -0
  9. package/dist/hydrate-slice.js +1 -0
  10. package/dist/image-component.d.ts +26 -0
  11. package/dist/image-component.js +79 -0
  12. package/dist/image-element.d.ts +6 -0
  13. package/dist/image-element.js +68 -0
  14. package/dist/image.d.ts +3 -0
  15. package/dist/image.js +3 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +2 -0
  18. package/dist/link-adapter.d.ts +2 -0
  19. package/dist/link-adapter.js +2 -0
  20. package/dist/link-component.d.ts +53 -0
  21. package/dist/link-component.js +97 -0
  22. package/dist/link.d.ts +3 -0
  23. package/dist/link.js +3 -0
  24. package/dist/preview-generation.d.ts +21 -0
  25. package/dist/preview-generation.js +134 -0
  26. package/dist/preview-host/preview-assistant-continuation.d.ts +9 -0
  27. package/dist/preview-host/preview-assistant-continuation.js +39 -0
  28. package/dist/preview-host/preview-authoring-client.d.ts +12 -0
  29. package/dist/preview-host/preview-authoring-client.js +100 -0
  30. package/dist/preview-host/preview-editor-continuation.d.ts +13 -0
  31. package/dist/preview-host/preview-editor-continuation.js +68 -0
  32. package/dist/preview-host/preview-standalone-controls.d.ts +18 -0
  33. package/dist/preview-host/preview-standalone-controls.js +518 -0
  34. package/dist/preview-host/preview-studio-destination.d.ts +20 -0
  35. package/dist/preview-host/preview-studio-destination.js +55 -0
  36. package/dist/preview-host/use-standalone-presentation.d.ts +15 -0
  37. package/dist/preview-host/use-standalone-presentation.js +223 -0
  38. package/dist/preview-icons.d.ts +10 -0
  39. package/dist/preview-icons.js +8 -0
  40. package/dist/preview-interaction-control.d.ts +6 -0
  41. package/dist/preview-interaction-control.js +39 -0
  42. package/dist/preview-panel-state.d.ts +15 -0
  43. package/dist/preview-panel-state.js +28 -0
  44. package/dist/preview-provenance-publisher-model.d.ts +5 -0
  45. package/dist/preview-provenance-publisher-model.js +9 -0
  46. package/dist/preview-provenance-publisher.d.ts +15 -0
  47. package/dist/preview-provenance-publisher.js +68 -0
  48. package/dist/preview-provenance.d.ts +6 -0
  49. package/dist/preview-provenance.js +4 -0
  50. package/dist/preview-scope.d.ts +128 -0
  51. package/dist/preview-scope.js +161 -0
  52. package/dist/preview-slice-scope.d.ts +32 -0
  53. package/dist/preview-slice-scope.js +33 -0
  54. package/dist/preview-text-props.d.ts +18 -0
  55. package/dist/preview-text-props.js +13 -0
  56. package/dist/preview-toolbar-layout.d.ts +42 -0
  57. package/dist/preview-toolbar-layout.js +139 -0
  58. package/dist/preview-toolbar-panel.d.ts +13 -0
  59. package/dist/preview-toolbar-panel.js +6 -0
  60. package/dist/preview-toolbar-resize-handles.d.ts +9 -0
  61. package/dist/preview-toolbar-resize-handles.js +14 -0
  62. package/dist/preview-toolbar-shell.d.ts +18 -0
  63. package/dist/preview-toolbar-shell.js +138 -0
  64. package/dist/public-rich-text.d.ts +15 -0
  65. package/dist/public-rich-text.js +11 -0
  66. package/dist/public-slice-zone.d.ts +7 -0
  67. package/dist/public-slice-zone.js +6 -0
  68. package/dist/query-cache.d.ts +14 -0
  69. package/dist/query-cache.js +289 -0
  70. package/dist/query-preview-loader.d.ts +9 -0
  71. package/dist/query-preview-loader.js +28 -0
  72. package/dist/query-preview-runtime.d.ts +3 -0
  73. package/dist/query-preview-runtime.js +3 -0
  74. package/dist/rendering.d.ts +29 -0
  75. package/dist/rendering.js +17 -0
  76. package/dist/resize-edge.d.ts +8 -0
  77. package/dist/resize-edge.js +33 -0
  78. package/dist/rich-text.d.ts +150 -0
  79. package/dist/rich-text.js +618 -0
  80. package/dist/slice-render-failure.d.ts +8 -0
  81. package/dist/slice-render-failure.js +46 -0
  82. package/dist/slice-zone.d.ts +82 -0
  83. package/dist/slice-zone.js +273 -0
  84. package/dist/slice.d.ts +16 -0
  85. package/dist/slice.js +46 -0
  86. package/dist/storystick-mark.d.ts +3 -0
  87. package/dist/storystick-mark.js +5 -0
  88. package/dist/use-generated-query.d.ts +37 -0
  89. package/dist/use-generated-query.js +308 -0
  90. package/dist/use-momentary-preview-navigate-mode.d.ts +11 -0
  91. package/dist/use-momentary-preview-navigate-mode.js +158 -0
  92. package/dist/use-preview-perspective-intent.d.ts +53 -0
  93. package/dist/use-preview-perspective-intent.js +227 -0
  94. package/dist/use-preview-pointer-drag.d.ts +16 -0
  95. package/dist/use-preview-pointer-drag.js +98 -0
  96. package/dist/use-preview-toolbar-layout.d.ts +36 -0
  97. package/dist/use-preview-toolbar-layout.js +141 -0
  98. package/dist/use-render-mode-debounce.d.ts +31 -0
  99. package/dist/use-render-mode-debounce.js +143 -0
  100. package/dist/video.d.ts +21 -0
  101. package/dist/video.js +63 -0
  102. package/package.json +146 -3
@@ -0,0 +1,618 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { isPreviewSliceDirectInvocationCandidate, readPreviewSliceScopeAdapter, } from './preview-slice-scope.js';
3
+ import { readContentOrigin, prepareContentSource, } from '@storystick/client/internal';
4
+ /**
5
+ * RichText — renders ProseMirror JSON to React elements.
6
+ *
7
+ * A lightweight tree walker with no TipTap dependency.
8
+ * Uses one semantic `components` registry for blocks, marks and embeds.
9
+ *
10
+ * <RichText content={post.fields.body} components={{ embeds: sliceComponents }} />
11
+ */
12
+ import React from 'react';
13
+ import { stripPreviewProvenanceTextMarkers } from '@storystick/types/preview-provenance';
14
+ import { previewTextProps } from './preview-text-props.js';
15
+ import { getReferencedDocumentsSidecar, richTextToPlainText, isSectionAnchorSlug, resolveLink as resolveLinkValue, } from '@storystick/types';
16
+ import { hydrateSliceWithDiagnostics } from './hydrate-slice.js';
17
+ import { projectHydratedSlice, renderSliceFallback, reportSliceFailure, validateSliceVariation, } from './slice-render-failure.js';
18
+ import { stampPreviewScope } from './preview-scope.js';
19
+ import { extractSliceFields, getPreviewDocumentId, isPublishedSnapshotContext, makeSliceRenderId, serializeSliceModel, } from '@storystick/preview/provenance/slice-model';
20
+ import { buildDocumentProvenance, buildSliceProvenance, fieldSegment, previewPathKey, richTextTextNodeSegment, } from './preview-provenance.js';
21
+ // ─────────────────────────────────────────────
22
+ // Content parsing
23
+ // ─────────────────────────────────────────────
24
+ function parseContent(content) {
25
+ if (!content)
26
+ return null;
27
+ let doc = content;
28
+ if (typeof content === 'string') {
29
+ try {
30
+ doc = JSON.parse(content);
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ const node = doc;
37
+ if (!node || typeof node !== 'object' || !node.type)
38
+ return null;
39
+ if (!node.content?.length)
40
+ return null;
41
+ return node;
42
+ }
43
+ // ─────────────────────────────────────────────
44
+ // Default mark renderers
45
+ // ─────────────────────────────────────────────
46
+ function defaultMark(mark, children, key, link, title) {
47
+ switch (mark.type) {
48
+ case 'bold':
49
+ return _jsx("strong", { children: children }, key);
50
+ case 'italic':
51
+ return _jsx("em", { children: children }, key);
52
+ case 'underline':
53
+ return _jsx("u", { children: children }, key);
54
+ case 'strike':
55
+ return _jsx("s", { children: children }, key);
56
+ case 'code':
57
+ return _jsx("code", { children: children }, key);
58
+ case 'link': {
59
+ const status = link &&
60
+ link.status !== 'ok' &&
61
+ link.status !== 'external' &&
62
+ link.status !== 'asset'
63
+ ? link.status
64
+ : undefined;
65
+ return (_jsx("a", { ...link?.props, title: title, "data-ss-link-status": status, children: children }, key));
66
+ }
67
+ default:
68
+ // Custom mark — render as span with attrs
69
+ return _jsx("span", { children: children }, key);
70
+ }
71
+ }
72
+ // ─────────────────────────────────────────────
73
+ // Tree walker
74
+ // ─────────────────────────────────────────────
75
+ const STANDARD_MARK_ORDER = [
76
+ 'link',
77
+ 'bold',
78
+ 'italic',
79
+ 'underline',
80
+ 'strike',
81
+ 'code',
82
+ ];
83
+ function markOrder(left, right) {
84
+ const rank = (name) => {
85
+ const index = STANDARD_MARK_ORDER.indexOf(name);
86
+ return index < 0 ? STANDARD_MARK_ORDER.length : index;
87
+ };
88
+ return rank(left) - rank(right) || (left < right ? -1 : left > right ? 1 : 0);
89
+ }
90
+ const DEFAULT_WALK_POSITION = {
91
+ block: { type: 'paragraph' },
92
+ ancestors: [],
93
+ };
94
+ const BUILTIN_MARKS = new Set([
95
+ 'bold',
96
+ 'italic',
97
+ 'underline',
98
+ 'strike',
99
+ 'code',
100
+ 'link',
101
+ ]);
102
+ function reportRichTextDiagnostic(ctx, diagnostic) {
103
+ ctx.onDiagnostic?.(diagnostic);
104
+ if (!ctx.onDiagnostic && process.env.NODE_ENV !== 'production') {
105
+ console.warn(`[Storystick RichText] ${diagnostic.message}`);
106
+ }
107
+ }
108
+ function renderContext(position) {
109
+ return {
110
+ block: position.block,
111
+ ancestors: [...position.ancestors],
112
+ };
113
+ }
114
+ function authoredLinkTitle(mark) {
115
+ const title = mark.attrs?.title;
116
+ if (typeof title !== 'string')
117
+ return undefined;
118
+ return stripPreviewProvenanceTextMarkers(title) || undefined;
119
+ }
120
+ function extractText(node, parts) {
121
+ if (node.text)
122
+ parts.push(stripPreviewProvenanceTextMarkers(node.text));
123
+ for (const child of node.content ?? [])
124
+ extractText(child, parts);
125
+ }
126
+ function visibleNodeText(node) {
127
+ const parts = [];
128
+ extractText(node, parts);
129
+ return parts.length > 0 ? parts.join('') : undefined;
130
+ }
131
+ function headingLevel(node) {
132
+ const level = node.attrs?.level;
133
+ return typeof level === 'number' &&
134
+ Number.isInteger(level) &&
135
+ level >= 1 &&
136
+ level <= 6
137
+ ? level
138
+ : 2;
139
+ }
140
+ function headingAnchorId(node) {
141
+ const anchor = node.attrs?._anchor;
142
+ if (anchor === null || typeof anchor !== 'object' || Array.isArray(anchor)) {
143
+ return undefined;
144
+ }
145
+ const slug = anchor.slug;
146
+ return typeof slug === 'string' && isSectionAnchorSlug(slug)
147
+ ? slug
148
+ : undefined;
149
+ }
150
+ function currentList(ancestors) {
151
+ for (let index = ancestors.length - 1; index >= 0; index -= 1) {
152
+ const ancestor = ancestors[index];
153
+ if (ancestor?.type === 'list') {
154
+ return { list: ancestor.list, depth: ancestor.depth };
155
+ }
156
+ }
157
+ return { list: 'bullet', depth: 1 };
158
+ }
159
+ function renderNode(node, index, ctx, position, path) {
160
+ const key = `rt-${index}`;
161
+ // Text node — apply marks
162
+ if (node.type === 'text') {
163
+ const textNodeIndex = ctx.nextTextNodeIndex++;
164
+ const textProps = previewTextProps(node.text ?? '');
165
+ let result = textProps.children;
166
+ if (node.marks?.length) {
167
+ // Canonical nesting does not depend on the order in which marks were applied.
168
+ const marks = [...node.marks].sort((a, b) => markOrder(a.type, b.type));
169
+ for (let i = marks.length - 1; i >= 0; i--) {
170
+ const mark = marks[i];
171
+ const markKey = `${key}-m${i}`;
172
+ // Resolved once, above the override branch: a custom link renderer and
173
+ // the default one see the same href, from the same resolution path.
174
+ const link = mark.type === 'link'
175
+ ? resolveLinkValue(mark.attrs, {
176
+ source: readContentOrigin(mark.attrs) ??
177
+ ctx.linkSource ??
178
+ ctx.referencedDocs,
179
+ })
180
+ : undefined;
181
+ const title = mark.type === 'link' ? authoredLinkTitle(mark) : undefined;
182
+ const context = renderContext(position);
183
+ const MarkOverride = ctx.components?.marks?.[mark.type];
184
+ if (mark.type === 'link' && MarkOverride && link) {
185
+ result = (_jsx(MarkOverride, { link: link, title: title, context: context, children: result }, markKey));
186
+ }
187
+ else if (MarkOverride) {
188
+ result = (_jsx(MarkOverride, { context: context, ...(mark.attrs?.value !== undefined
189
+ ? { value: mark.attrs.value }
190
+ : {}), children: result }, markKey));
191
+ }
192
+ else {
193
+ if (!BUILTIN_MARKS.has(mark.type)) {
194
+ reportRichTextDiagnostic(ctx, {
195
+ reason: 'missing-mark-renderer',
196
+ type: mark.type,
197
+ path,
198
+ message: `No RichText mark renderer is registered for "${mark.type}"; its children were preserved.`,
199
+ });
200
+ }
201
+ result = defaultMark(mark, result, markKey, link, title);
202
+ }
203
+ }
204
+ }
205
+ const contextualAttrs = provenanceAttrsForTextNode(ctx, textNodeIndex);
206
+ const provenanceAttrs = Object.keys(contextualAttrs).length > 0
207
+ ? contextualAttrs
208
+ : textProps['data-ss-p'] ? { 'data-ss-p': textProps['data-ss-p'] } : {};
209
+ if (Object.keys(provenanceAttrs).length > 0) {
210
+ ctx.provenanceTextNodeCount += 1;
211
+ result = (_jsx("span", { suppressHydrationWarning: true, ...provenanceAttrs, ...(node.marks?.some(mark => mark.type === 'link')
212
+ ? { 'data-ss-richtext-link': '' }
213
+ : {}), children: result }, `${key}-p`));
214
+ }
215
+ return result;
216
+ }
217
+ // Slice embed — dispatch to component registry
218
+ if (node.type === 'sliceEmbed') {
219
+ const embedIndex = ctx.nextEmbedIndex++;
220
+ const embedType = node.attrs?._type;
221
+ if (typeof embedType !== 'string' || embedType.length === 0) {
222
+ const failure = {
223
+ reason: 'missing-type',
224
+ slice: isRecord(node.attrs) ? node.attrs : {},
225
+ index: embedIndex,
226
+ };
227
+ reportSliceFailure(failure, ctx.onRenderFailure);
228
+ return renderSliceFallback(ctx.sliceFallback, failure, `missing-embed-type-${embedIndex}`, ctx.previewing || process.env.NODE_ENV !== 'production');
229
+ }
230
+ const rawEmbedKey = node.attrs?._key;
231
+ const embedKey = typeof rawEmbedKey === 'string' && rawEmbedKey.length > 0
232
+ ? rawEmbedKey
233
+ : key;
234
+ const Component = ctx.components?.embeds?.[embedType];
235
+ if (!Component) {
236
+ const failure = {
237
+ reason: 'unregistered-type',
238
+ slice: { _type: embedType, _key: embedKey },
239
+ index: embedIndex,
240
+ registeredTypes: Object.keys(ctx.components?.embeds ?? {}).sort(),
241
+ };
242
+ reportSliceFailure(failure, ctx.onRenderFailure);
243
+ reportRichTextDiagnostic(ctx, {
244
+ reason: 'missing-embed-renderer',
245
+ type: embedType,
246
+ path,
247
+ message: `No RichText embed renderer is registered for "${embedType}".`,
248
+ });
249
+ return renderSliceFallback(ctx.sliceFallback, failure, embedKey, ctx.previewing || process.env.NODE_ENV !== 'production');
250
+ }
251
+ // Flatten attrs.data into a SliceData shape
252
+ const rawData = node.attrs?.data;
253
+ if (rawData !== undefined && !isRecord(rawData)) {
254
+ const failure = {
255
+ reason: 'invalid-slice',
256
+ value: rawData,
257
+ index: embedIndex,
258
+ };
259
+ reportSliceFailure(failure, ctx.onRenderFailure);
260
+ return renderSliceFallback(ctx.sliceFallback, failure, embedKey, ctx.previewing || process.env.NODE_ENV !== 'production');
261
+ }
262
+ const data = rawData ?? {};
263
+ const slice = {
264
+ ...data,
265
+ _type: embedType,
266
+ _key: embedKey,
267
+ ...(node.attrs &&
268
+ Object.prototype.hasOwnProperty.call(node.attrs, '_variation')
269
+ ? { _variation: node.attrs._variation }
270
+ : {}),
271
+ };
272
+ const variationFailure = validateSliceVariation(slice, ctx.contracts, embedIndex);
273
+ if (variationFailure) {
274
+ reportSliceFailure(variationFailure, ctx.onRenderFailure);
275
+ return renderSliceFallback(ctx.sliceFallback, variationFailure, embedKey, ctx.previewing || process.env.NODE_ENV !== 'production');
276
+ }
277
+ const sourceRefPath = ctx.sliceField === undefined
278
+ ? undefined
279
+ : `${ctx.parentPath ? `${ctx.parentPath}.` : ''}${ctx.sliceField}[${embedIndex}]._model`;
280
+ const contentOrigin = readContentOrigin(slice);
281
+ const hydration = contentOrigin
282
+ ? { slice, failures: contentOrigin.failures ?? [] }
283
+ : hydrateSliceWithDiagnostics(slice, ctx.source, { sourceRefPath });
284
+ if (hydration.failures.length > 0) {
285
+ const failures = hydration.failures.map((failure) => failure.reason === 'missing-model'
286
+ ? { ...failure, slice, index: embedIndex }
287
+ : { reason: 'invalid-bindings', slice, index: embedIndex });
288
+ failures.forEach((failure) => reportSliceFailure(failure, ctx.onRenderFailure));
289
+ return renderSliceFallback(ctx.sliceFallback, failures[0], embedKey, ctx.previewing || process.env.NODE_ENV !== 'production');
290
+ }
291
+ const hydrated = projectHydratedSlice(hydration.slice, ctx.contracts);
292
+ const hydratedFields = extractSliceFields(hydrated);
293
+ let propSlice = hydrated;
294
+ const componentContext = ctx.context;
295
+ if (ctx.previewing &&
296
+ ctx.previewDocId !== null &&
297
+ ctx.sliceField !== undefined &&
298
+ typeof slice._key === 'string' &&
299
+ slice._key.length > 0) {
300
+ const embedPathPrefix = ctx.parentPath
301
+ ? `${ctx.parentPath}.${ctx.sliceField}`
302
+ : ctx.sliceField;
303
+ const modelJson = serializeSliceModel({
304
+ v: 129,
305
+ renderId: ctx.embedRenderId,
306
+ doc: ctx.previewDocId,
307
+ i: contentOrigin?.index ?? embedIndex,
308
+ type: embedType,
309
+ fields: hydratedFields,
310
+ zoneField: ctx.sliceField,
311
+ zoneFieldSource: 'explicit',
312
+ parentAnchors: contentOrigin?.parentAnchors ?? ctx.parentAnchors,
313
+ ...(contentOrigin
314
+ ? {
315
+ source: {
316
+ path: `${embedPathPrefix}[${contentOrigin.index ?? embedIndex}]`,
317
+ anchors: [
318
+ ...contentOrigin.parentAnchors,
319
+ { pathPrefix: embedPathPrefix, key: slice._key },
320
+ ],
321
+ },
322
+ }
323
+ : {}),
324
+ });
325
+ if (modelJson !== null) {
326
+ propSlice = stampPreviewScope(hydrated, modelJson);
327
+ }
328
+ }
329
+ const bindings = buildSliceProvenance(contentOrigin ?? ctx.provenanceContext, slice);
330
+ const props = { slice: propSlice, context: componentContext, bindings };
331
+ const adapter = readPreviewSliceScopeAdapter();
332
+ const documentId = contentOrigin?.documentId ?? ctx.previewDocId;
333
+ if (adapter &&
334
+ documentId &&
335
+ isPreviewSliceDirectInvocationCandidate(Component)) {
336
+ const scoped = adapter({
337
+ component: Component,
338
+ props,
339
+ scope: {
340
+ routeFrame: null,
341
+ documentId,
342
+ sliceField: ctx.sliceField ?? 'richtext',
343
+ slicePath: contentOrigin?.path ??
344
+ `${ctx.sliceField ?? 'richtext'}[${embedIndex}]`,
345
+ sliceKey: embedKey,
346
+ sliceType: embedType,
347
+ sliceIndex: contentOrigin?.index ?? embedIndex,
348
+ },
349
+ });
350
+ return (_jsx(React.Fragment, { children: scoped }, embedKey));
351
+ }
352
+ return _jsx(Component, { ...props }, embedKey);
353
+ }
354
+ const renderChildren = (childPosition) => node.content?.map((child, childIndex) => renderNode(child, childIndex, ctx, childPosition, [...path, childIndex])) ?? null;
355
+ const renderTextBlockChildren = (childPosition) => {
356
+ const before = ctx.provenanceTextNodeCount;
357
+ const children = renderChildren(childPosition);
358
+ // Explicit opt-in geometry survives custom block renderers without reading
359
+ // arbitrary customer DOM. Keep each leaf's provenance for precise arrivals.
360
+ return ctx.provenanceTextNodeCount > before
361
+ ? _jsx("span", { "data-ss-richtext-block": "", suppressHydrationWarning: true, children: children })
362
+ : children;
363
+ };
364
+ switch (node.type) {
365
+ case 'paragraph': {
366
+ const block = position.block.type === 'listItem'
367
+ ? position.block
368
+ : { type: 'paragraph' };
369
+ const childPosition = { ...position, block };
370
+ const children = renderTextBlockChildren(childPosition);
371
+ const Component = ctx.components?.block?.paragraph;
372
+ return Component ? (_jsx(Component, { text: visibleNodeText(node), context: renderContext(childPosition), children: children }, key)) : (_jsx("p", { children: children }, key));
373
+ }
374
+ case 'heading': {
375
+ const level = headingLevel(node);
376
+ const id = headingAnchorId(node);
377
+ const childPosition = {
378
+ ...position,
379
+ block: { type: 'heading', level },
380
+ };
381
+ const children = renderTextBlockChildren(childPosition);
382
+ const Component = ctx.components?.block?.[`h${level}`];
383
+ if (Component) {
384
+ return (_jsx(Component, { id: id, text: visibleNodeText(node), context: renderContext(childPosition), children: children }, key));
385
+ }
386
+ const Tag = `h${level}`;
387
+ return (_jsx(Tag, { id: id, children: children }, key));
388
+ }
389
+ case 'blockquote': {
390
+ const block = { type: 'blockquote' };
391
+ const childPosition = {
392
+ block,
393
+ ancestors: [...position.ancestors, { type: 'blockquote' }],
394
+ };
395
+ const children = renderChildren(childPosition);
396
+ const Component = ctx.components?.block?.blockquote;
397
+ return Component ? (_jsx(Component, { text: visibleNodeText(node), context: { block, ancestors: [...position.ancestors] }, children: children }, key)) : (_jsx("blockquote", { children: children }, key));
398
+ }
399
+ case 'codeBlock': {
400
+ const childPosition = {
401
+ ...position,
402
+ block: { type: 'code' },
403
+ };
404
+ const children = renderTextBlockChildren(childPosition);
405
+ const language = typeof node.attrs?.language === 'string'
406
+ ? node.attrs.language
407
+ : undefined;
408
+ const Component = ctx.components?.block?.code;
409
+ return Component ? (_jsx(Component, { text: visibleNodeText(node), language: language, context: renderContext(childPosition), children: children }, key)) : (_jsx("pre", { "data-language": language, children: _jsx("code", { children: children }) }, key));
410
+ }
411
+ case 'horizontalRule': {
412
+ const Component = ctx.components?.block?.divider;
413
+ return Component ? _jsx(Component, {}, key) : _jsx("hr", {}, key);
414
+ }
415
+ case 'hardBreak': {
416
+ const Component = ctx.components?.hardBreak;
417
+ return Component ? _jsx(Component, {}, key) : _jsx("br", {}, key);
418
+ }
419
+ case 'bulletList':
420
+ case 'orderedList': {
421
+ const list = node.type === 'bulletList' ? 'bullet' : 'ordered';
422
+ const depth = currentList(position.ancestors).depth +
423
+ (position.ancestors.some((ancestor) => ancestor.type === 'list')
424
+ ? 1
425
+ : 0);
426
+ const childPosition = {
427
+ ...position,
428
+ ancestors: [...position.ancestors, { type: 'list', list, depth }],
429
+ };
430
+ const children = renderChildren(childPosition);
431
+ if (list === 'ordered') {
432
+ const start = typeof node.attrs?.start === 'number' ? node.attrs.start : undefined;
433
+ const Component = ctx.components?.list?.ordered;
434
+ return Component ? (_jsx(Component, { start: start, children: children }, key)) : (_jsx("ol", { start: start, children: children }, key));
435
+ }
436
+ const Component = ctx.components?.list?.bullet;
437
+ return Component ? (_jsx(Component, { children: children }, key)) : (_jsx("ul", { children: children }, key));
438
+ }
439
+ case 'listItem': {
440
+ const list = currentList(position.ancestors);
441
+ const block = {
442
+ type: 'listItem',
443
+ list: list.list,
444
+ depth: list.depth,
445
+ };
446
+ const childPosition = {
447
+ block,
448
+ ancestors: [
449
+ ...position.ancestors,
450
+ { type: 'listItem', list: list.list, depth: list.depth },
451
+ ],
452
+ };
453
+ const children = renderChildren(childPosition);
454
+ const Component = ctx.components?.listItem?.[list.list];
455
+ return Component ? (_jsx(Component, { children: children }, key)) : (_jsx("li", { children: children }, key));
456
+ }
457
+ default: {
458
+ reportRichTextDiagnostic(ctx, {
459
+ reason: 'unknown-block',
460
+ type: node.type,
461
+ path,
462
+ message: `Unknown RichText block "${node.type}"; its children were preserved.`,
463
+ });
464
+ const children = renderChildren(position);
465
+ return children ? _jsx("div", { children: children }, key) : null;
466
+ }
467
+ }
468
+ }
469
+ // ─────────────────────────────────────────────
470
+ // Component
471
+ // ─────────────────────────────────────────────
472
+ function renderRichText(props, inline) {
473
+ const { content: inputContent, source, components, sliceFallback, contracts, onRenderFailure, onDiagnostic, context, bindings, provenancePath, sliceField, parentAnchors, parentPath, className, fallback, } = props;
474
+ const content = prepareContentSource(inputContent, source);
475
+ const origin = readContentOrigin(content);
476
+ const doc = parseContent(content);
477
+ if (!doc) {
478
+ return (fallback ?? null);
479
+ }
480
+ const technicalSource = origin ?? source?.document;
481
+ // Preparing a raw rich-text field gives the parsed value a compact
482
+ // ContentOrigin. That origin deliberately carries reference/address data,
483
+ // but not the complete document provenance map. Keep resolution on the
484
+ // origin while reading direct-field provenance from the explicit document.
485
+ const provenanceSource = source?.document ?? origin;
486
+ const referencedDocs = getReferencedDocumentsSidecar(technicalSource);
487
+ const linkSource = technicalSource;
488
+ const previewDocId = origin?.documentId ?? getPreviewDocumentId(source?.document);
489
+ const effectiveSliceField = origin?.path.split('.').at(-1) ?? sliceField ?? source?.path;
490
+ const effectiveParentAnchors = origin?.parentAnchors ?? parentAnchors ?? [];
491
+ const effectiveParentPath = origin?.path.includes('.')
492
+ ? origin.path.slice(0, origin.path.lastIndexOf('.'))
493
+ : parentPath;
494
+ const inferredDocumentProvenance = !bindings && previewDocId !== null
495
+ ? buildDocumentProvenance(provenanceSource, {
496
+ documentId: previewDocId,
497
+ })
498
+ : undefined;
499
+ const effectiveProvenance = bindings ?? inferredDocumentProvenance;
500
+ const effectiveProvenancePath = provenancePath ?? inferTopLevelProvenancePath(origin?.path ?? source?.path);
501
+ const provenanceContext = technicalSource;
502
+ const previewing = origin
503
+ ? origin.preview === true
504
+ : previewDocId !== null && !isPublishedSnapshotContext(source?.document);
505
+ const embedRenderId = previewing && previewDocId !== null
506
+ ? makeSliceRenderId({
507
+ scopeDocId: previewDocId,
508
+ slices: collectEmbedIdentities(doc),
509
+ scopeKey: `${effectiveParentPath ?? 'root'}:${effectiveSliceField}`,
510
+ })
511
+ : '';
512
+ const ctx = {
513
+ components: components,
514
+ sliceFallback,
515
+ contracts,
516
+ onRenderFailure,
517
+ onDiagnostic,
518
+ context,
519
+ source: technicalSource,
520
+ referencedDocs,
521
+ linkSource,
522
+ provenance: effectiveProvenance,
523
+ provenancePath: effectiveProvenancePath,
524
+ provenanceContext,
525
+ sliceField: effectiveSliceField,
526
+ parentPath: effectiveParentPath,
527
+ parentAnchors: effectiveParentAnchors,
528
+ previewDocId,
529
+ previewing,
530
+ embedRenderId,
531
+ nextTextNodeIndex: 0,
532
+ provenanceTextNodeCount: 0,
533
+ nextEmbedIndex: 0,
534
+ };
535
+ let renderNodes = doc.content;
536
+ if (inline) {
537
+ const validInlineShape = renderNodes.length === 1 && renderNodes[0]?.type === 'paragraph';
538
+ if (!validInlineShape) {
539
+ reportRichTextDiagnostic(ctx, {
540
+ reason: 'inline-shape',
541
+ path: [],
542
+ message: 'RichText.Inline expected exactly one paragraph; block wrappers were omitted while preserving renderable children.',
543
+ });
544
+ }
545
+ renderNodes = renderNodes.flatMap((node) => node.type === 'text' ||
546
+ node.type === 'hardBreak' ||
547
+ node.type === 'sliceEmbed'
548
+ ? [node]
549
+ : (node.content ?? []));
550
+ }
551
+ const renderedNodes = renderNodes.map((node, index) => renderNode(node, index, ctx, DEFAULT_WALK_POSITION, [index]));
552
+ const rendered = inline && ctx.provenanceTextNodeCount > 0
553
+ ? _jsx("span", { "data-ss-richtext-block": "", suppressHydrationWarning: true, children: renderedNodes })
554
+ : renderedNodes;
555
+ if (className) {
556
+ return _jsx("div", { className: className, children: rendered });
557
+ }
558
+ return _jsx(_Fragment, { children: rendered });
559
+ }
560
+ /**
561
+ * A raw primitive cannot carry ContentOrigin metadata. For the common direct
562
+ * document-field case, the explicit ContentSource still gives RichText enough
563
+ * information to recover its document provenance without app-owned plumbing.
564
+ * Indexed paths keep requiring an explicit provenancePath because their stable
565
+ * array-item keys cannot be inferred from a numeric string alone.
566
+ */
567
+ function inferTopLevelProvenancePath(path) {
568
+ if (!path || path.includes('['))
569
+ return undefined;
570
+ const segments = path.split('.').filter(Boolean);
571
+ return segments.length > 0 ? segments.map(fieldSegment) : undefined;
572
+ }
573
+ function RichTextRoot(props) {
574
+ return renderRichText(props, false);
575
+ }
576
+ function RichTextInline(props) {
577
+ return renderRichText(props, true);
578
+ }
579
+ export const RichText = Object.assign(RichTextRoot, {
580
+ Inline: RichTextInline,
581
+ });
582
+ function isRecord(value) {
583
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
584
+ }
585
+ function collectEmbedIdentities(root) {
586
+ const out = [];
587
+ const visit = (node, index) => {
588
+ if (node.type === 'sliceEmbed') {
589
+ const embedType = node.attrs?._type;
590
+ const key = node.attrs?._key;
591
+ if (typeof embedType === 'string' && embedType.length > 0) {
592
+ out.push({
593
+ _type: embedType,
594
+ _key: typeof key === 'string' ? key : `rt-${index}`,
595
+ });
596
+ }
597
+ }
598
+ node.content?.forEach((child, childIndex) => visit(child, childIndex));
599
+ };
600
+ visit(root, 0);
601
+ return out;
602
+ }
603
+ function provenanceAttrsForTextNode(ctx, textNodeIndex) {
604
+ if (!ctx.provenance || !ctx.provenancePath)
605
+ return {};
606
+ const path = [...ctx.provenancePath, richTextTextNodeSegment(textNodeIndex)];
607
+ return ctx.provenance.paths[previewPathKey(path)] ?? {};
608
+ }
609
+ // ─────────────────────────────────────────────
610
+ // toText utility
611
+ // ─────────────────────────────────────────────
612
+ /**
613
+ * Extract plain text from richtext content.
614
+ * Handles both JSON string and parsed ProseMirror document.
615
+ */
616
+ export function toText(content, separator = ' ') {
617
+ return richTextToPlainText(content, separator);
618
+ }
@@ -0,0 +1,8 @@
1
+ import type { ReactElement, ReactNode } from 'react';
2
+ export { validateSliceVariation, projectHydratedSlice } from '@storystick/types';
3
+ export type { SliceRenderFailure, SliceContract, SliceContractRegistry } from '@storystick/types';
4
+ import type { SliceRenderFailure } from '@storystick/types';
5
+ export type SliceFallback = ReactNode | ((failure: SliceRenderFailure) => ReactNode);
6
+ export type SliceRenderFailureHandler = (failure: SliceRenderFailure) => void;
7
+ export declare function reportSliceFailure(failure: SliceRenderFailure, onRenderFailure: SliceRenderFailureHandler | undefined): void;
8
+ export declare function renderSliceFallback(fallback: SliceFallback | undefined, failure: SliceRenderFailure, key: string, visiblyFail: boolean): ReactElement | null;
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ export { validateSliceVariation, projectHydratedSlice } from '@storystick/types';
3
+ export function reportSliceFailure(failure, onRenderFailure) {
4
+ onRenderFailure?.(failure);
5
+ if (process.env.NODE_ENV === 'production')
6
+ return;
7
+ switch (failure.reason) {
8
+ case 'invalid-slice':
9
+ console.warn(`StorystickError: Slice at index ${failure.index} is not an object.`);
10
+ return;
11
+ case 'missing-type':
12
+ console.warn(`StorystickError: Slice at index ${failure.index} has no "_type" field.\n` +
13
+ 'Each slice must have: { _type: string, _key: string, ...fields }');
14
+ return;
15
+ case 'unregistered-type':
16
+ console.warn(`StorystickError: No component registered for slice type "${failure.slice._type}".\n` +
17
+ `Registered types: ${failure.registeredTypes.join(', ') || '(none)'}\n` +
18
+ `Fix: add "${failure.slice._type}" to the slice component registry.`);
19
+ return;
20
+ case 'invalid-variation':
21
+ console.warn(`StorystickError: Unknown variation ${JSON.stringify(failure.variation)} for slice "${failure.slice._type}".\n` +
22
+ `Registered variations: ${failure.registeredVariations.join(', ') || '(base only)'}\n` +
23
+ 'Fix: omit _variation for the base slice or use a declared named variation.');
24
+ return;
25
+ case 'missing-model':
26
+ console.warn(`StorystickError: Model "${failure.modelId}" for slice "${failure.slice._type}" is missing from the response sidecar.`);
27
+ return;
28
+ case 'invalid-bindings':
29
+ console.warn(`StorystickError: Field bindings for slice "${failure.slice._type}" are malformed.`);
30
+ }
31
+ }
32
+ export function renderSliceFallback(fallback, failure, key, visiblyFail) {
33
+ const rendered = fallback === undefined
34
+ ? visiblyFail
35
+ ? React.createElement('div', {
36
+ role: 'alert',
37
+ 'data-storystick-slice-failure': failure.reason,
38
+ }, `Slice could not render (${failure.reason}).`)
39
+ : null
40
+ : typeof fallback === 'function'
41
+ ? fallback(failure)
42
+ : fallback;
43
+ if (rendered === null || rendered === undefined || rendered === false)
44
+ return null;
45
+ return React.createElement(React.Fragment, { key }, rendered);
46
+ }