@bendyline/squisq-react 1.2.0 → 1.3.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 (77) hide show
  1. package/dist/InlineAudioPlayer.d.ts +15 -0
  2. package/dist/InlineAudioPlayer.d.ts.map +1 -0
  3. package/dist/InlineAudioPlayer.js +22 -0
  4. package/dist/InlineAudioPlayer.js.map +1 -0
  5. package/dist/InlineVideoPlayer.d.ts +21 -0
  6. package/dist/InlineVideoPlayer.d.ts.map +1 -0
  7. package/dist/InlineVideoPlayer.js +29 -0
  8. package/dist/InlineVideoPlayer.js.map +1 -0
  9. package/dist/LinearDocView.d.ts.map +1 -1
  10. package/dist/LinearDocView.js +124 -13
  11. package/dist/LinearDocView.js.map +1 -1
  12. package/dist/MarkdownRenderer.d.ts.map +1 -1
  13. package/dist/MarkdownRenderer.js +111 -2
  14. package/dist/MarkdownRenderer.js.map +1 -1
  15. package/dist/SocialCaptionOverlay.d.ts.map +1 -1
  16. package/dist/SocialCaptionOverlay.js +3 -2
  17. package/dist/SocialCaptionOverlay.js.map +1 -1
  18. package/dist/__tests__/JsonView.test.d.ts +2 -0
  19. package/dist/__tests__/JsonView.test.d.ts.map +1 -0
  20. package/dist/__tests__/JsonView.test.js +93 -0
  21. package/dist/__tests__/JsonView.test.js.map +1 -0
  22. package/dist/__tests__/LinearDocView.test.js +62 -0
  23. package/dist/__tests__/LinearDocView.test.js.map +1 -1
  24. package/dist/hooks/MediaContext.d.ts.map +1 -1
  25. package/dist/hooks/MediaContext.js +13 -7
  26. package/dist/hooks/MediaContext.js.map +1 -1
  27. package/dist/index.d.ts +7 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +5 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/jsonView/JsonView.d.ts +26 -0
  32. package/dist/jsonView/JsonView.d.ts.map +1 -0
  33. package/dist/jsonView/JsonView.js +12 -0
  34. package/dist/jsonView/JsonView.js.map +1 -0
  35. package/dist/jsonView/RenderNode.d.ts +22 -0
  36. package/dist/jsonView/RenderNode.d.ts.map +1 -0
  37. package/dist/jsonView/RenderNode.js +30 -0
  38. package/dist/jsonView/RenderNode.js.map +1 -0
  39. package/dist/jsonView/index.d.ts +3 -0
  40. package/dist/jsonView/index.d.ts.map +1 -0
  41. package/dist/jsonView/index.js +2 -0
  42. package/dist/jsonView/index.js.map +1 -0
  43. package/dist/jsonView/useJsonViewTokens.d.ts +14 -0
  44. package/dist/jsonView/useJsonViewTokens.d.ts.map +1 -0
  45. package/dist/jsonView/useJsonViewTokens.js +34 -0
  46. package/dist/jsonView/useJsonViewTokens.js.map +1 -0
  47. package/dist/jsonView/viewers.d.ts +30 -0
  48. package/dist/jsonView/viewers.d.ts.map +1 -0
  49. package/dist/jsonView/viewers.js +226 -0
  50. package/dist/jsonView/viewers.js.map +1 -0
  51. package/dist/squisq-player.css +1 -1
  52. package/dist/squisq-player.css.map +1 -1
  53. package/dist/squisq-player.global.js +24 -10
  54. package/dist/squisq-player.global.js.map +1 -1
  55. package/dist/standalone-source.js +1 -1
  56. package/dist/types.d.ts +5 -1
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/types.js.map +1 -1
  59. package/package.json +4 -3
  60. package/src/InlineAudioPlayer.tsx +46 -0
  61. package/src/InlineVideoPlayer.tsx +70 -0
  62. package/src/LinearDocView.tsx +136 -14
  63. package/src/MarkdownRenderer.tsx +156 -10
  64. package/src/SocialCaptionOverlay.tsx +3 -2
  65. package/src/__tests__/JsonView.test.tsx +111 -0
  66. package/src/__tests__/LinearDocView.test.tsx +66 -0
  67. package/src/hooks/MediaContext.tsx +15 -8
  68. package/src/index.ts +9 -0
  69. package/src/jsonView/JsonView.tsx +51 -0
  70. package/src/jsonView/RenderNode.tsx +51 -0
  71. package/src/jsonView/index.ts +2 -0
  72. package/src/jsonView/json-view.css +206 -0
  73. package/src/jsonView/useJsonViewTokens.ts +57 -0
  74. package/src/jsonView/viewers.tsx +343 -0
  75. package/src/styles/doc-animations.css +36 -0
  76. package/src/styles/index.css +7 -0
  77. package/src/types.ts +5 -1
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Read-only viewer renderers for `<JsonView>`. Each viewer is a small
3
+ * pure function that takes a value + schema slice and renders inline.
4
+ * Group / card-stack viewers recurse into `RenderNode`.
5
+ */
6
+
7
+ import { Fragment, useMemo } from 'react';
8
+ import {
9
+ arrayItemKind,
10
+ type ControlKind,
11
+ type SquisqAnnotatedSchema,
12
+ } from '@bendyline/squisq/jsonForm';
13
+ import { parseMarkdown } from '@bendyline/squisq/markdown';
14
+ import { MarkdownRenderer } from '../MarkdownRenderer';
15
+ import { RenderNode } from './RenderNode';
16
+
17
+ export interface ViewerProps {
18
+ value: unknown;
19
+ schema: SquisqAnnotatedSchema;
20
+ rootSchema: SquisqAnnotatedSchema;
21
+ rootData: unknown;
22
+ pointer: string;
23
+ density: 'comfortable' | 'compact';
24
+ }
25
+
26
+ // ── Primitive viewers ──────────────────────────────────────────────
27
+
28
+ export function TextViewer({ value }: ViewerProps) {
29
+ if (value === undefined || value === null || value === '') {
30
+ return <span className="squisq-jv-empty">—</span>;
31
+ }
32
+ return <span className="squisq-jv-value">{String(value)}</span>;
33
+ }
34
+
35
+ export function MultilineViewer({ value }: ViewerProps) {
36
+ if (value === undefined || value === null || value === '') {
37
+ return <span className="squisq-jv-empty">—</span>;
38
+ }
39
+ return <span className="squisq-jv-value squisq-jv-value--multiline">{String(value)}</span>;
40
+ }
41
+
42
+ export function RichTextViewer({ value }: ViewerProps) {
43
+ const nodes = useMemo(() => {
44
+ if (typeof value !== 'string' || value === '') return null;
45
+ try {
46
+ const doc = parseMarkdown(value);
47
+ return doc.children;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }, [value]);
52
+ if (!nodes) return <span className="squisq-jv-empty">—</span>;
53
+ return (
54
+ <div className="squisq-jv-richtext">
55
+ <MarkdownRenderer nodes={nodes} />
56
+ </div>
57
+ );
58
+ }
59
+
60
+ export function NumberViewer({ value }: ViewerProps) {
61
+ if (value === undefined || value === null) {
62
+ return <span className="squisq-jv-empty">—</span>;
63
+ }
64
+ return <span className="squisq-jv-value">{String(value)}</span>;
65
+ }
66
+
67
+ export function BooleanViewer({ value }: ViewerProps) {
68
+ const on = Boolean(value);
69
+ return (
70
+ <span className={`squisq-jv-toggle squisq-jv-toggle--${on ? 'on' : 'off'}`}>
71
+ {on ? 'On' : 'Off'}
72
+ </span>
73
+ );
74
+ }
75
+
76
+ export function EnumViewer({ value, schema }: ViewerProps) {
77
+ if (value === undefined || value === null || value === '') {
78
+ return <span className="squisq-jv-empty">—</span>;
79
+ }
80
+ const labels = schema.squisq?.enumLabels;
81
+ const display = labels && typeof value === 'string' ? (labels[value] ?? value) : String(value);
82
+ return <span className="squisq-jv-value">{display}</span>;
83
+ }
84
+
85
+ export function ColorViewer({ value }: ViewerProps) {
86
+ if (typeof value !== 'string' || value === '') {
87
+ return <span className="squisq-jv-empty">—</span>;
88
+ }
89
+ return (
90
+ <span className="squisq-jv-color">
91
+ <span className="squisq-jv-color__swatch" style={{ background: value }} aria-hidden />
92
+ <span className="squisq-jv-color__hex">{value}</span>
93
+ </span>
94
+ );
95
+ }
96
+
97
+ export function DateViewer({ value, schema }: ViewerProps) {
98
+ if (typeof value !== 'string' || value === '') {
99
+ return <span className="squisq-jv-empty">—</span>;
100
+ }
101
+ const fmt = schema.format;
102
+ let display = value;
103
+ try {
104
+ const date = new Date(value);
105
+ if (!Number.isNaN(date.getTime())) {
106
+ if (fmt === 'time') {
107
+ display = date.toLocaleTimeString();
108
+ } else if (fmt === 'date') {
109
+ display = date.toLocaleDateString();
110
+ } else {
111
+ display = date.toLocaleString();
112
+ }
113
+ }
114
+ } catch {
115
+ /* fall through to raw value */
116
+ }
117
+ return <span className="squisq-jv-value">{display}</span>;
118
+ }
119
+
120
+ // ── Composite viewers ─────────────────────────────────────────────
121
+
122
+ export function ChipBinViewer({ value, schema }: ViewerProps) {
123
+ if (!Array.isArray(value) || value.length === 0) {
124
+ return <span className="squisq-jv-empty">—</span>;
125
+ }
126
+ const itemSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;
127
+ const labels = itemSchema?.squisq?.enumLabels;
128
+ return (
129
+ <div className="squisq-jv-chip-bin">
130
+ {value.map((item, i) => {
131
+ const label =
132
+ labels && typeof item === 'string' ? (labels[item] ?? String(item)) : String(item);
133
+ return (
134
+ <span key={i} className="squisq-jv-chip">
135
+ {label}
136
+ </span>
137
+ );
138
+ })}
139
+ </div>
140
+ );
141
+ }
142
+
143
+ export function CardStackViewer(props: ViewerProps) {
144
+ const { value, schema, rootSchema, rootData, pointer, density } = props;
145
+ if (!Array.isArray(value) || value.length === 0) {
146
+ return <span className="squisq-jv-empty">No items</span>;
147
+ }
148
+ const itemSchema = (Array.isArray(schema.items) ? schema.items[0] : schema.items) ?? {};
149
+ const itemLabel = itemSchema.squisq?.itemLabel;
150
+ return (
151
+ <div className="squisq-jv-card-stack">
152
+ {value.map((item, i) => {
153
+ const title = resolveItemTitle(itemLabel, item, i);
154
+ return (
155
+ <div key={i} className="squisq-jv-card">
156
+ {title ? <h4 className="squisq-jv-card__title">{title}</h4> : null}
157
+ <RenderNode
158
+ value={item}
159
+ schema={itemSchema}
160
+ rootSchema={rootSchema}
161
+ rootData={rootData}
162
+ pointer={`${pointer}/${i}`}
163
+ density={density}
164
+ suppressTopGroupTitle
165
+ />
166
+ </div>
167
+ );
168
+ })}
169
+ </div>
170
+ );
171
+ }
172
+
173
+ function resolveItemTitle(
174
+ spec: string | { fromField: string } | undefined,
175
+ item: unknown,
176
+ index: number,
177
+ ): string {
178
+ if (!spec) return `Item ${index + 1}`;
179
+ if (typeof spec === 'string') return spec;
180
+ if (item && typeof item === 'object') {
181
+ const v = (item as Record<string, unknown>)[spec.fromField];
182
+ if (typeof v === 'string' && v !== '') return v;
183
+ if (typeof v === 'number') return String(v);
184
+ }
185
+ return `Item ${index + 1}`;
186
+ }
187
+
188
+ export function GroupViewer(props: ViewerProps & { suppressTitle?: boolean }) {
189
+ const { value, schema, rootSchema, rootData, pointer, density, suppressTitle } = props;
190
+ const title = !suppressTitle ? (schema.squisq?.label ?? schema.title) : undefined;
191
+ const help = schema.squisq?.help ?? schema.description;
192
+ const obj = (value && typeof value === 'object' ? (value as Record<string, unknown>) : {}) ?? {};
193
+ const propEntries = Object.entries(schema.properties ?? {});
194
+
195
+ return (
196
+ <section className="squisq-jv-group">
197
+ {title ? <h3 className="squisq-jv-group__title">{title}</h3> : null}
198
+ {help ? <p className="squisq-jv-group__help">{help}</p> : null}
199
+ {propEntries.map(([key, propSchema]) => (
200
+ <Fragment key={key}>
201
+ <RowOrSection
202
+ label={propSchema.squisq?.label ?? propSchema.title ?? key}
203
+ help={propSchema.squisq?.help ?? propSchema.description}
204
+ kindHint={propSchema}
205
+ >
206
+ <RenderNode
207
+ value={obj[key]}
208
+ schema={propSchema}
209
+ rootSchema={rootSchema}
210
+ rootData={rootData}
211
+ pointer={`${pointer}/${key}`}
212
+ density={density}
213
+ suppressTopGroupTitle
214
+ />
215
+ </RowOrSection>
216
+ </Fragment>
217
+ ))}
218
+ </section>
219
+ );
220
+ }
221
+
222
+ /**
223
+ * Decide whether to use the inline label/value Row layout, or to drop
224
+ * the row chrome entirely (for nested groups, card-stacks, richtext —
225
+ * which present their own headings).
226
+ */
227
+ function RowOrSection({
228
+ label,
229
+ help,
230
+ kindHint,
231
+ children,
232
+ }: {
233
+ label: string;
234
+ help?: string;
235
+ kindHint: SquisqAnnotatedSchema;
236
+ children: React.ReactNode;
237
+ }) {
238
+ const composite = isCompositeKind(kindHint);
239
+ if (composite) {
240
+ return <>{children}</>;
241
+ }
242
+ return (
243
+ <div className="squisq-jv-row">
244
+ <div className="squisq-jv-label" title={help}>
245
+ {label}
246
+ </div>
247
+ <div>{children}</div>
248
+ </div>
249
+ );
250
+ }
251
+
252
+ function isCompositeKind(schema: SquisqAnnotatedSchema): boolean {
253
+ const t = schema.type;
254
+ const arrayLike = t === 'array' || (Array.isArray(t) && t.includes('array'));
255
+ if (arrayLike) {
256
+ return arrayItemKind(schema) === 'object';
257
+ }
258
+ const objectLike = t === 'object' || (Array.isArray(t) && t.includes('object'));
259
+ if (objectLike) return true;
260
+ if (schema.oneOf || schema.anyOf) return true;
261
+ if (schema.format === 'markdown' || schema.squisq?.control === 'richtext') return true;
262
+ return false;
263
+ }
264
+
265
+ export function TabsViewer(props: ViewerProps) {
266
+ const { value, schema, rootSchema, rootData, pointer, density } = props;
267
+ const branches = (schema.oneOf ?? schema.anyOf ?? []).slice();
268
+ const matchedIndex = pickMatchingBranch(branches as SquisqAnnotatedSchema[], value);
269
+ const branch = branches[matchedIndex] as SquisqAnnotatedSchema | undefined;
270
+ if (!branch) {
271
+ return <TextViewer {...props} />;
272
+ }
273
+ return (
274
+ <div className="squisq-jv-tabs">
275
+ <span className="squisq-jv-tabs__discriminator">
276
+ {branch.squisq?.label ?? branch.title ?? `Option ${matchedIndex + 1}`}
277
+ </span>
278
+ <RenderNode
279
+ value={value}
280
+ schema={branch}
281
+ rootSchema={rootSchema}
282
+ rootData={rootData}
283
+ pointer={pointer}
284
+ density={density}
285
+ />
286
+ </div>
287
+ );
288
+ }
289
+
290
+ function pickMatchingBranch(branches: SquisqAnnotatedSchema[], value: unknown): number {
291
+ for (let i = 0; i < branches.length; i++) {
292
+ if (matchesShape(branches[i], value)) return i;
293
+ }
294
+ return 0;
295
+ }
296
+
297
+ function matchesShape(schema: SquisqAnnotatedSchema, value: unknown): boolean {
298
+ const t = schema.type;
299
+ const target = Array.isArray(t) ? t.find((x) => x !== 'null') : t;
300
+ if (!target) return true;
301
+ switch (target) {
302
+ case 'string':
303
+ return typeof value === 'string';
304
+ case 'number':
305
+ case 'integer':
306
+ return typeof value === 'number';
307
+ case 'boolean':
308
+ return typeof value === 'boolean';
309
+ case 'null':
310
+ return value === null;
311
+ case 'array':
312
+ return Array.isArray(value);
313
+ case 'object':
314
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
315
+ default:
316
+ return false;
317
+ }
318
+ }
319
+
320
+ // ── Registry ──────────────────────────────────────────────────────
321
+
322
+ // eslint-disable-next-line react-refresh/only-export-components
323
+ export const VIEWERS: Record<ControlKind, React.ComponentType<ViewerProps>> = {
324
+ text: TextViewer,
325
+ multiline: MultilineViewer,
326
+ richtext: RichTextViewer,
327
+ color: ColorViewer,
328
+ date: DateViewer,
329
+ time: DateViewer,
330
+ datetime: DateViewer,
331
+ slider: NumberViewer,
332
+ stepper: NumberViewer,
333
+ segmented: EnumViewer,
334
+ radio: EnumViewer,
335
+ combobox: EnumViewer,
336
+ toggle: BooleanViewer,
337
+ checkbox: BooleanViewer,
338
+ card: GroupViewer,
339
+ group: GroupViewer,
340
+ 'card-stack': CardStackViewer,
341
+ 'chip-bin': ChipBinViewer,
342
+ tabs: TabsViewer,
343
+ };
@@ -456,3 +456,39 @@
456
456
  background: rgba(255, 255, 255, 0.9);
457
457
  color: #1a1a1a;
458
458
  }
459
+
460
+ /* ── Inline media players ────────────────────────────────────────── */
461
+ /* Used by <InlineVideoPlayer> / <InlineAudioPlayer>, which the
462
+ * MarkdownRenderer swaps in for raw <video>/<audio> htmlElements so
463
+ * their src goes through MediaContext. The wrapper is a <span> with
464
+ * `display: inline-block` so the player flows inline with surrounding
465
+ * text but still respects its own intrinsic dimensions. */
466
+
467
+ .squisq-inline-video-player {
468
+ display: inline-block;
469
+ max-width: 480px;
470
+ width: 100%;
471
+ margin: 0.5em 0;
472
+ }
473
+
474
+ .squisq-inline-video-player video {
475
+ display: block;
476
+ width: 100%;
477
+ max-width: 100%;
478
+ height: auto;
479
+ border-radius: 4px;
480
+ background: #000;
481
+ }
482
+
483
+ .squisq-inline-audio-player {
484
+ display: inline-block;
485
+ max-width: 360px;
486
+ width: 100%;
487
+ margin: 0.25em 0;
488
+ vertical-align: middle;
489
+ }
490
+
491
+ .squisq-inline-audio-player audio {
492
+ display: block;
493
+ width: 100%;
494
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @bendyline/squisq-react — bundled stylesheet entry.
3
+ * Aggregates all CSS shipped with this package.
4
+ */
5
+
6
+ @import './doc-animations.css';
7
+ @import '../jsonView/json-view.css';
package/src/types.ts CHANGED
@@ -29,8 +29,12 @@ export type ControlsLayout = 'overlay' | 'sidebar' | 'bottom';
29
29
  * - `'linear'` — Long-scrolling document view. Renders the full markdown as
30
30
  * readable HTML with template-annotated sections shown as inline SVG cards.
31
31
  * No audio, no timeline, no controls — just a scrollable page.
32
+ * - `'page'` — Plain semantic HTML preview matching what the
33
+ * `markdownDocToPlainHtml` export produces. No SquisqPlayer, no SVG
34
+ * cards — just `<h1>`/`<p>`/`<ul>` etc. inside a sandboxed iframe.
35
+ * Use when you want a WYSIWYG view of the simple HTML export.
32
36
  */
33
- export type DisplayMode = 'video' | 'slideshow' | 'linear';
37
+ export type DisplayMode = 'video' | 'slideshow' | 'linear' | 'page';
34
38
 
35
39
  /**
36
40
  * Caption display style.