@sanity-labs/slides 0.0.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 (224) hide show
  1. package/README.md +241 -0
  2. package/SKILL.md +119 -0
  3. package/dist/cli.d.ts +38 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +386 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/core/components.d.ts +179 -0
  8. package/dist/core/components.d.ts.map +1 -0
  9. package/dist/core/components.js +40 -0
  10. package/dist/core/components.js.map +1 -0
  11. package/dist/core/fake-runtime.d.ts +138 -0
  12. package/dist/core/fake-runtime.d.ts.map +1 -0
  13. package/dist/core/fake-runtime.js +210 -0
  14. package/dist/core/fake-runtime.js.map +1 -0
  15. package/dist/core/font-resolver.d.ts +28 -0
  16. package/dist/core/font-resolver.d.ts.map +1 -0
  17. package/dist/core/font-resolver.js +30 -0
  18. package/dist/core/font-resolver.js.map +1 -0
  19. package/dist/core/geometry.d.ts +71 -0
  20. package/dist/core/geometry.d.ts.map +1 -0
  21. package/dist/core/geometry.js +44 -0
  22. package/dist/core/geometry.js.map +1 -0
  23. package/dist/core/index.d.ts +19 -0
  24. package/dist/core/index.d.ts.map +1 -0
  25. package/dist/core/index.js +20 -0
  26. package/dist/core/index.js.map +1 -0
  27. package/dist/core/manifest.d.ts +123 -0
  28. package/dist/core/manifest.d.ts.map +1 -0
  29. package/dist/core/manifest.js +43 -0
  30. package/dist/core/manifest.js.map +1 -0
  31. package/dist/core/op-translator-pptx.d.ts +150 -0
  32. package/dist/core/op-translator-pptx.d.ts.map +1 -0
  33. package/dist/core/op-translator-pptx.js +245 -0
  34. package/dist/core/op-translator-pptx.js.map +1 -0
  35. package/dist/core/pptx-runtime.d.ts +103 -0
  36. package/dist/core/pptx-runtime.d.ts.map +1 -0
  37. package/dist/core/pptx-runtime.js +405 -0
  38. package/dist/core/pptx-runtime.js.map +1 -0
  39. package/dist/core/reconciler.d.ts +113 -0
  40. package/dist/core/reconciler.d.ts.map +1 -0
  41. package/dist/core/reconciler.js +453 -0
  42. package/dist/core/reconciler.js.map +1 -0
  43. package/dist/core/runtime.d.ts +161 -0
  44. package/dist/core/runtime.d.ts.map +1 -0
  45. package/dist/core/runtime.js +11 -0
  46. package/dist/core/runtime.js.map +1 -0
  47. package/dist/core/template.d.ts +32 -0
  48. package/dist/core/template.d.ts.map +1 -0
  49. package/dist/core/template.js +3 -0
  50. package/dist/core/template.js.map +1 -0
  51. package/dist/dev/auto-examples.d.ts +6 -0
  52. package/dist/dev/auto-examples.d.ts.map +1 -0
  53. package/dist/dev/auto-examples.js +79 -0
  54. package/dist/dev/auto-examples.js.map +1 -0
  55. package/dist/dev/bin/slides-dev.d.ts +3 -0
  56. package/dist/dev/bin/slides-dev.d.ts.map +1 -0
  57. package/dist/dev/bin/slides-dev.js +87 -0
  58. package/dist/dev/bin/slides-dev.js.map +1 -0
  59. package/dist/dev/bin/slides-dev.mjs +24 -0
  60. package/dist/dev/compose-deck.d.ts +18 -0
  61. package/dist/dev/compose-deck.d.ts.map +1 -0
  62. package/dist/dev/compose-deck.js +19 -0
  63. package/dist/dev/compose-deck.js.map +1 -0
  64. package/dist/dev/deck-viewer.d.ts +19 -0
  65. package/dist/dev/deck-viewer.d.ts.map +1 -0
  66. package/dist/dev/deck-viewer.js +237 -0
  67. package/dist/dev/deck-viewer.js.map +1 -0
  68. package/dist/dev/dev-server/client/entry.d.ts +2 -0
  69. package/dist/dev/dev-server/client/entry.d.ts.map +1 -0
  70. package/dist/dev/dev-server/client/entry.js +12 -0
  71. package/dist/dev/dev-server/client/entry.js.map +1 -0
  72. package/dist/dev/dev-server/output.d.ts +8 -0
  73. package/dist/dev/dev-server/output.d.ts.map +1 -0
  74. package/dist/dev/dev-server/output.js +32 -0
  75. package/dist/dev/dev-server/output.js.map +1 -0
  76. package/dist/dev/dev-server/server-only-stub.d.ts +7 -0
  77. package/dist/dev/dev-server/server-only-stub.d.ts.map +1 -0
  78. package/dist/dev/dev-server/server-only-stub.js +12 -0
  79. package/dist/dev/dev-server/server-only-stub.js.map +1 -0
  80. package/dist/dev/dev-server/start.d.ts +14 -0
  81. package/dist/dev/dev-server/start.d.ts.map +1 -0
  82. package/dist/dev/dev-server/start.js +135 -0
  83. package/dist/dev/dev-server/start.js.map +1 -0
  84. package/dist/dev/index.d.ts +5 -0
  85. package/dist/dev/index.d.ts.map +1 -0
  86. package/dist/dev/index.js +5 -0
  87. package/dist/dev/index.js.map +1 -0
  88. package/dist/dev/lib/cn.d.ts +3 -0
  89. package/dist/dev/lib/cn.d.ts.map +1 -0
  90. package/dist/dev/lib/cn.js +3 -0
  91. package/dist/dev/lib/cn.js.map +1 -0
  92. package/dist/dev/slide-canvas.d.ts +12 -0
  93. package/dist/dev/slide-canvas.d.ts.map +1 -0
  94. package/dist/dev/slide-canvas.js +123 -0
  95. package/dist/dev/slide-canvas.js.map +1 -0
  96. package/dist/dev/styles.css +37 -0
  97. package/dist/dev/ui/icon-button.d.ts +12 -0
  98. package/dist/dev/ui/icon-button.d.ts.map +1 -0
  99. package/dist/dev/ui/icon-button.js +6 -0
  100. package/dist/dev/ui/icon-button.js.map +1 -0
  101. package/dist/dev/ui/kbd.d.ts +6 -0
  102. package/dist/dev/ui/kbd.d.ts.map +1 -0
  103. package/dist/dev/ui/kbd.js +4 -0
  104. package/dist/dev/ui/kbd.js.map +1 -0
  105. package/dist/dev/ui/text-button.d.ts +10 -0
  106. package/dist/dev/ui/text-button.d.ts.map +1 -0
  107. package/dist/dev/ui/text-button.js +6 -0
  108. package/dist/dev/ui/text-button.js.map +1 -0
  109. package/dist/dev/url-state.d.ts +7 -0
  110. package/dist/dev/url-state.d.ts.map +1 -0
  111. package/dist/dev/url-state.js +13 -0
  112. package/dist/dev/url-state.js.map +1 -0
  113. package/dist/dev/use-keyboard-nav.d.ts +17 -0
  114. package/dist/dev/use-keyboard-nav.d.ts.map +1 -0
  115. package/dist/dev/use-keyboard-nav.js +53 -0
  116. package/dist/dev/use-keyboard-nav.js.map +1 -0
  117. package/dist/index.d.ts +17 -0
  118. package/dist/index.d.ts.map +1 -0
  119. package/dist/index.js +17 -0
  120. package/dist/index.js.map +1 -0
  121. package/dist/mcp/errors.d.ts +57 -0
  122. package/dist/mcp/errors.d.ts.map +1 -0
  123. package/dist/mcp/errors.js +44 -0
  124. package/dist/mcp/errors.js.map +1 -0
  125. package/dist/mcp/index.d.ts +29 -0
  126. package/dist/mcp/index.d.ts.map +1 -0
  127. package/dist/mcp/index.js +29 -0
  128. package/dist/mcp/index.js.map +1 -0
  129. package/dist/mcp/naming.d.ts +37 -0
  130. package/dist/mcp/naming.d.ts.map +1 -0
  131. package/dist/mcp/naming.js +43 -0
  132. package/dist/mcp/naming.js.map +1 -0
  133. package/dist/mcp/render.d.ts +45 -0
  134. package/dist/mcp/render.d.ts.map +1 -0
  135. package/dist/mcp/render.js +77 -0
  136. package/dist/mcp/render.js.map +1 -0
  137. package/dist/mcp/schema.d.ts +54 -0
  138. package/dist/mcp/schema.d.ts.map +1 -0
  139. package/dist/mcp/schema.js +55 -0
  140. package/dist/mcp/schema.js.map +1 -0
  141. package/dist/mcp/server.d.ts +63 -0
  142. package/dist/mcp/server.d.ts.map +1 -0
  143. package/dist/mcp/server.js +196 -0
  144. package/dist/mcp/server.js.map +1 -0
  145. package/dist/scaffold/index.d.ts +39 -0
  146. package/dist/scaffold/index.d.ts.map +1 -0
  147. package/dist/scaffold/index.js +84 -0
  148. package/dist/scaffold/index.js.map +1 -0
  149. package/dist/scaffold/template-base/README.md +134 -0
  150. package/dist/scaffold/template-base/_gitignore +4 -0
  151. package/dist/scaffold/template-base/package.json +35 -0
  152. package/dist/scaffold/template-base/src/components/Cover.tsx +30 -0
  153. package/dist/scaffold/template-base/src/index.ts +27 -0
  154. package/dist/scaffold/template-base/src/preview.tsx +9 -0
  155. package/dist/scaffold/template-base/tsconfig.build.json +10 -0
  156. package/dist/scaffold/template-base/tsconfig.json +18 -0
  157. package/package.json +164 -0
  158. package/src/__tests__/fixtures/test-template/index.tsx +77 -0
  159. package/src/__tests__/pptx-mcp.test.ts +85 -0
  160. package/src/__tests__/pptx-smoke.test.ts +45 -0
  161. package/src/__tests__/preview.test.ts +28 -0
  162. package/src/cli.ts +426 -0
  163. package/src/core/__snapshots__/reconciler.test.ts.snap +320 -0
  164. package/src/core/components.test.ts +57 -0
  165. package/src/core/components.ts +196 -0
  166. package/src/core/fake-runtime.test.ts +174 -0
  167. package/src/core/fake-runtime.ts +302 -0
  168. package/src/core/font-resolver.ts +46 -0
  169. package/src/core/geometry.test.ts +58 -0
  170. package/src/core/geometry.ts +91 -0
  171. package/src/core/index.ts +69 -0
  172. package/src/core/manifest.test.ts +33 -0
  173. package/src/core/manifest.ts +150 -0
  174. package/src/core/op-translator-pptx.test.ts +204 -0
  175. package/src/core/op-translator-pptx.ts +365 -0
  176. package/src/core/pptx-runtime.test.ts +137 -0
  177. package/src/core/pptx-runtime.ts +504 -0
  178. package/src/core/reconciler.test.ts +644 -0
  179. package/src/core/reconciler.ts +603 -0
  180. package/src/core/runtime.ts +150 -0
  181. package/src/core/template.test.ts +136 -0
  182. package/src/core/template.ts +37 -0
  183. package/src/dev/auto-examples.ts +89 -0
  184. package/src/dev/bin/slides-dev.mjs +24 -0
  185. package/src/dev/bin/slides-dev.ts +101 -0
  186. package/src/dev/compose-deck.test.ts +68 -0
  187. package/src/dev/compose-deck.ts +40 -0
  188. package/src/dev/deck-viewer.tsx +677 -0
  189. package/src/dev/dev-server/client/entry.tsx +15 -0
  190. package/src/dev/dev-server/client/index.html +24 -0
  191. package/src/dev/dev-server/output.ts +37 -0
  192. package/src/dev/dev-server/server-only-stub.ts +12 -0
  193. package/src/dev/dev-server/start.ts +155 -0
  194. package/src/dev/index.ts +4 -0
  195. package/src/dev/lib/cn.ts +3 -0
  196. package/src/dev/slide-canvas.test.tsx +66 -0
  197. package/src/dev/slide-canvas.tsx +170 -0
  198. package/src/dev/styles.css +37 -0
  199. package/src/dev/ui/icon-button.tsx +31 -0
  200. package/src/dev/ui/kbd.tsx +20 -0
  201. package/src/dev/ui/text-button.tsx +31 -0
  202. package/src/dev/url-state.test.ts +22 -0
  203. package/src/dev/url-state.ts +17 -0
  204. package/src/dev/use-keyboard-nav.ts +64 -0
  205. package/src/index.ts +17 -0
  206. package/src/mcp/errors.test.ts +51 -0
  207. package/src/mcp/errors.ts +76 -0
  208. package/src/mcp/index.ts +45 -0
  209. package/src/mcp/naming.test.ts +39 -0
  210. package/src/mcp/naming.ts +49 -0
  211. package/src/mcp/render.ts +110 -0
  212. package/src/mcp/schema.test.ts +86 -0
  213. package/src/mcp/schema.ts +93 -0
  214. package/src/mcp/server.test.ts +309 -0
  215. package/src/mcp/server.ts +276 -0
  216. package/src/scaffold/index.ts +102 -0
  217. package/src/scaffold/template-base/README.md +134 -0
  218. package/src/scaffold/template-base/_gitignore +4 -0
  219. package/src/scaffold/template-base/package.json +35 -0
  220. package/src/scaffold/template-base/src/components/Cover.tsx +30 -0
  221. package/src/scaffold/template-base/src/index.ts +27 -0
  222. package/src/scaffold/template-base/src/preview.tsx +9 -0
  223. package/src/scaffold/template-base/tsconfig.build.json +10 -0
  224. package/src/scaffold/template-base/tsconfig.json +18 -0
@@ -0,0 +1,320 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`renderToOps — Box fill > a Box with both fill and text emits fill *before* insertText > ops 1`] = `
4
+ [
5
+ {
6
+ "insertAt": 0,
7
+ "slideId": "slide_1",
8
+ "type": "createSlide",
9
+ },
10
+ {
11
+ "rect": {
12
+ "h": 1270000,
13
+ "w": 7620000,
14
+ "x": 685800,
15
+ "y": 685800,
16
+ },
17
+ "shape": "TEXT_BOX",
18
+ "shapeId": "shape_2",
19
+ "slideId": "slide_1",
20
+ "type": "createShape",
21
+ },
22
+ {
23
+ "objectId": "shape_2",
24
+ "properties": {
25
+ "fillColor": "#0b0b0b",
26
+ },
27
+ "type": "updateShapeProperties",
28
+ },
29
+ {
30
+ "objectId": "shape_2",
31
+ "text": "Hello",
32
+ "type": "insertText",
33
+ },
34
+ ]
35
+ `;
36
+
37
+ exports[`renderToOps — Box fill > an empty Box with a solid fill emits createShape → updateShapeProperties (no text ops) > ops 1`] = `
38
+ [
39
+ {
40
+ "insertAt": 0,
41
+ "slideId": "slide_1",
42
+ "type": "createSlide",
43
+ },
44
+ {
45
+ "rect": {
46
+ "h": 6858000,
47
+ "w": 12192000,
48
+ "x": 0,
49
+ "y": 0,
50
+ },
51
+ "shape": "TEXT_BOX",
52
+ "shapeId": "shape_2",
53
+ "slideId": "slide_1",
54
+ "type": "createShape",
55
+ },
56
+ {
57
+ "objectId": "shape_2",
58
+ "properties": {
59
+ "fillColor": "#ff5500",
60
+ },
61
+ "type": "updateShapeProperties",
62
+ },
63
+ ]
64
+ `;
65
+
66
+ exports[`renderToOps — Box-level styles > Box textStyle and paragraphStyle become full-range update ops > ops 1`] = `
67
+ [
68
+ {
69
+ "insertAt": 0,
70
+ "slideId": "slide_1",
71
+ "type": "createSlide",
72
+ },
73
+ {
74
+ "rect": {
75
+ "h": 1270000,
76
+ "w": 7620000,
77
+ "x": 685800,
78
+ "y": 685800,
79
+ },
80
+ "shape": "TEXT_BOX",
81
+ "shapeId": "shape_2",
82
+ "slideId": "slide_1",
83
+ "type": "createShape",
84
+ },
85
+ {
86
+ "objectId": "shape_2",
87
+ "text": "Title",
88
+ "type": "insertText",
89
+ },
90
+ {
91
+ "objectId": "shape_2",
92
+ "range": {
93
+ "end": 5,
94
+ "start": 0,
95
+ },
96
+ "style": {
97
+ "fontFamily": "Geist",
98
+ "fontSize": 56,
99
+ "foregroundColor": "#0b0b0b",
100
+ },
101
+ "type": "updateTextStyle",
102
+ },
103
+ {
104
+ "objectId": "shape_2",
105
+ "range": {
106
+ "end": 5,
107
+ "start": 0,
108
+ },
109
+ "style": {
110
+ "alignment": "START",
111
+ "lineSpacing": 1.1,
112
+ },
113
+ "type": "updateParagraphStyle",
114
+ },
115
+ ]
116
+ `;
117
+
118
+ exports[`renderToOps — Image primitive > emits createSlide → createImage with the resolved URL and rect > ops 1`] = `
119
+ [
120
+ {
121
+ "insertAt": 0,
122
+ "slideId": "slide_1",
123
+ "type": "createSlide",
124
+ },
125
+ {
126
+ "imageId": "image_2",
127
+ "rect": {
128
+ "h": 6858000,
129
+ "w": 12192000,
130
+ "x": 0,
131
+ "y": 0,
132
+ },
133
+ "slideId": "slide_1",
134
+ "type": "createImage",
135
+ "url": "https://cdn.example.com/hero.png",
136
+ },
137
+ ]
138
+ `;
139
+
140
+ exports[`renderToOps — function components compose > a function component returning <Slide> resolves correctly > ops 1`] = `
141
+ [
142
+ {
143
+ "insertAt": 0,
144
+ "slideId": "slide_1",
145
+ "type": "createSlide",
146
+ },
147
+ {
148
+ "rect": {
149
+ "h": 1270000,
150
+ "w": 7620000,
151
+ "x": 685800,
152
+ "y": 685800,
153
+ },
154
+ "shape": "TEXT_BOX",
155
+ "shapeId": "shape_2",
156
+ "slideId": "slide_1",
157
+ "type": "createShape",
158
+ },
159
+ {
160
+ "objectId": "shape_2",
161
+ "text": "Hello",
162
+ "type": "insertText",
163
+ },
164
+ {
165
+ "objectId": "shape_2",
166
+ "range": {
167
+ "end": 5,
168
+ "start": 0,
169
+ },
170
+ "style": {
171
+ "bold": true,
172
+ },
173
+ "type": "updateTextStyle",
174
+ },
175
+ ]
176
+ `;
177
+
178
+ exports[`renderToOps — multi-slide deck > emits ops for each slide in document order with stable IDs > ops 1`] = `
179
+ [
180
+ {
181
+ "insertAt": 0,
182
+ "slideId": "slide_1",
183
+ "type": "createSlide",
184
+ },
185
+ {
186
+ "rect": {
187
+ "h": 635000,
188
+ "w": 1270000,
189
+ "x": 0,
190
+ "y": 0,
191
+ },
192
+ "shape": "TEXT_BOX",
193
+ "shapeId": "shape_2",
194
+ "slideId": "slide_1",
195
+ "type": "createShape",
196
+ },
197
+ {
198
+ "objectId": "shape_2",
199
+ "text": "A",
200
+ "type": "insertText",
201
+ },
202
+ {
203
+ "insertAt": 1,
204
+ "slideId": "slide_3",
205
+ "type": "createSlide",
206
+ },
207
+ {
208
+ "rect": {
209
+ "h": 635000,
210
+ "w": 1270000,
211
+ "x": 0,
212
+ "y": 0,
213
+ },
214
+ "shape": "TEXT_BOX",
215
+ "shapeId": "shape_4",
216
+ "slideId": "slide_3",
217
+ "type": "createShape",
218
+ },
219
+ {
220
+ "objectId": "shape_4",
221
+ "text": "B",
222
+ "type": "insertText",
223
+ },
224
+ ]
225
+ `;
226
+
227
+ exports[`renderToOps — multiple text runs in one Box > concatenates text and emits per-run style spans for non-empty styles > ops 1`] = `
228
+ [
229
+ {
230
+ "insertAt": 0,
231
+ "slideId": "slide_1",
232
+ "type": "createSlide",
233
+ },
234
+ {
235
+ "rect": {
236
+ "h": 1270000,
237
+ "w": 7620000,
238
+ "x": 685800,
239
+ "y": 685800,
240
+ },
241
+ "shape": "TEXT_BOX",
242
+ "shapeId": "shape_2",
243
+ "slideId": "slide_1",
244
+ "type": "createShape",
245
+ },
246
+ {
247
+ "objectId": "shape_2",
248
+ "text": "Bold middle orange",
249
+ "type": "insertText",
250
+ },
251
+ {
252
+ "objectId": "shape_2",
253
+ "range": {
254
+ "end": 5,
255
+ "start": 0,
256
+ },
257
+ "style": {
258
+ "bold": true,
259
+ },
260
+ "type": "updateTextStyle",
261
+ },
262
+ {
263
+ "objectId": "shape_2",
264
+ "range": {
265
+ "end": 18,
266
+ "start": 12,
267
+ },
268
+ "style": {
269
+ "foregroundColor": "#ff5500",
270
+ },
271
+ "type": "updateTextStyle",
272
+ },
273
+ ]
274
+ `;
275
+
276
+ exports[`renderToOps — single slide with one box > emits createSlide → createShape → insertText in order > manifest 1`] = `
277
+ {
278
+ "artifacts": [],
279
+ "deckId": null,
280
+ "generatedAt": "2026-05-04T15:00:00.000Z",
281
+ "generatedBy": "react-pptx",
282
+ "manifestVersion": "1",
283
+ "slots": {},
284
+ "templateName": "test",
285
+ }
286
+ `;
287
+
288
+ exports[`renderToOps — single slide with one box > emits createSlide → createShape → insertText in order > ops 1`] = `
289
+ [
290
+ {
291
+ "insertAt": 0,
292
+ "slideId": "slide_1",
293
+ "type": "createSlide",
294
+ },
295
+ {
296
+ "rect": {
297
+ "h": 1270000,
298
+ "w": 7620000,
299
+ "x": 685800,
300
+ "y": 685800,
301
+ },
302
+ "shape": "TEXT_BOX",
303
+ "shapeId": "shape_2",
304
+ "slideId": "slide_1",
305
+ "type": "createShape",
306
+ },
307
+ {
308
+ "objectId": "shape_2",
309
+ "text": "Hello, world.",
310
+ "type": "insertText",
311
+ },
312
+ ]
313
+ `;
314
+
315
+ exports[`renderToOps — slotId attaches the shape to the manifest > records SlotId → shapeId for each slot-bearing Box > slot map 1`] = `
316
+ {
317
+ "cover:subtitle": "shape_3",
318
+ "cover:title": "shape_2",
319
+ }
320
+ `;
@@ -0,0 +1,57 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { Box, Color, Text, Image, isPrimitive, PRIMITIVES, Slide } from './components.js';
3
+
4
+ const TEST_ARTIFACT = {
5
+ type: 'image',
6
+ identifier: 'test-image',
7
+ resolvedUrl: 'https://example.com/x.png',
8
+ resolvedAt: '2026-05-04T15:00:00.000Z',
9
+ } as const;
10
+
11
+ describe('primitives', () => {
12
+ test('each primitive has a stable __rgsKind tag', () => {
13
+ expect(Slide.__rgsKind).toBe('Slide');
14
+ expect(Box.__rgsKind).toBe('Box');
15
+ expect(Text.__rgsKind).toBe('Text');
16
+ expect(Color.__rgsKind).toBe('Color');
17
+ expect(Image.__rgsKind).toBe('Image');
18
+ });
19
+
20
+ test('primitives render to null (inert under react-dom)', () => {
21
+ // Calling them as plain functions; the reconciler never does this directly,
22
+ // but accidental react-dom rendering must not blow up.
23
+ expect(Slide({})).toBeNull();
24
+ expect(Box({ rect: { x: 0, y: 0, w: 1, h: 1 } })).toBeNull();
25
+ expect(Text({})).toBeNull();
26
+ expect(Color({ color: '#ffffff' })).toBeNull();
27
+ expect(
28
+ Image({
29
+ rect: { x: 0, y: 0, w: 1, h: 1 },
30
+ image: { url: TEST_ARTIFACT.resolvedUrl, artifact: TEST_ARTIFACT },
31
+ }),
32
+ ).toBeNull();
33
+ });
34
+
35
+ test('PRIMITIVES contains exactly the five host elements', () => {
36
+ expect(Object.keys(PRIMITIVES).sort()).toEqual(['Box', 'Color', 'Image', 'Slide', 'Text']);
37
+ });
38
+ });
39
+
40
+ describe('isPrimitive', () => {
41
+ test('returns true for each primitive component', () => {
42
+ expect(isPrimitive(Slide)).toBe(true);
43
+ expect(isPrimitive(Box)).toBe(true);
44
+ expect(isPrimitive(Text)).toBe(true);
45
+ expect(isPrimitive(Color)).toBe(true);
46
+ expect(isPrimitive(Image)).toBe(true);
47
+ });
48
+
49
+ test('returns false for arbitrary functions and primitives', () => {
50
+ const NotMine = () => null;
51
+ expect(isPrimitive(NotMine)).toBe(false);
52
+ expect(isPrimitive('div')).toBe(false);
53
+ expect(isPrimitive(undefined)).toBe(false);
54
+ expect(isPrimitive(null)).toBe(false);
55
+ expect(isPrimitive(42)).toBe(false);
56
+ });
57
+ });
@@ -0,0 +1,196 @@
1
+ /**
2
+ * The minimal JSX primitive surface the reconciler walks.
3
+ *
4
+ * **This is intentionally a small, lower-case set.** High-level brand
5
+ * components (`<Cover/>`, `<TwoColumn/>`, etc.) live in brand packages
6
+ * (e.g., `@sanity-labs/slides`) and *compose* these primitives. The substrate
7
+ * exports just enough to render an arbitrary slide tree.
8
+ *
9
+ * Why hand-rolled host elements rather than `<div>`-style intrinsic elements:
10
+ * we want the reconciler to recognize these by *type identity*, not by string
11
+ * tag names. Type identity removes a class of bugs ("did you mean 'box' or
12
+ * 'Box'?") and gives the type-checker something concrete to enforce.
13
+ *
14
+ * These don't render to the DOM — they're inert markers the reconciler walks.
15
+ * They return `null` so they're harmless if accidentally rendered through
16
+ * react-dom (e.g., during Storybook prototyping).
17
+ */
18
+
19
+ import type { ReactNode } from 'react';
20
+ import type { Rect } from './geometry.js';
21
+ import type { ArtifactRef, SlotId } from './manifest.js';
22
+ import type { HexColor, ParagraphStyle, TextStyle } from './runtime.js';
23
+
24
+ /**
25
+ * A single slide in the deck.
26
+ *
27
+ * Every `<Slide>` becomes a `createSlide` op. Children are rendered into the
28
+ * slide's coordinate space.
29
+ */
30
+ export interface SlideProps {
31
+ /** Children: `<Box>` and `<Image>` shapes laid out inside the slide. */
32
+ children?: ReactNode;
33
+ }
34
+
35
+ /**
36
+ * A discriminated union describing a `<Box>`'s background fill.
37
+ *
38
+ * Shaped as a tagged union — *not* a flat `fill?: HexColor` — so future fill
39
+ * kinds (texture, image-as-background, gradient) can be added without
40
+ * sprouting parallel optional props. The discriminator is load-bearing:
41
+ * off-brand fills are inexpressible at the type level when a brand component
42
+ * narrows what kinds it accepts.
43
+ *
44
+ * Today only `'solid'` is supported. Image fills (full-bleed photos) are
45
+ * authored as `<Image>` siblings of `<Box>` rather than nested inside a
46
+ * Box's fill.
47
+ */
48
+ export type BoxFill = { kind: 'solid'; color: HexColor };
49
+
50
+ /**
51
+ * A rectangular text box on the slide canvas.
52
+ *
53
+ * Becomes a `createShape` op with shape `TEXT_BOX` (the only shape this
54
+ * primitive set produces; other shape kinds are reserved for the future).
55
+ * Children are concatenated into a single text run with style spans.
56
+ */
57
+ export interface BoxProps {
58
+ /** The shape's rect in *points*. The reconciler converts to EMU at the boundary. */
59
+ rect: Rect;
60
+
61
+ /**
62
+ * Optional slot ID, encoded into the shape's alt-text. Required for any text
63
+ * box that re-fill should target. Format: `<componentName>:<slotName>`,
64
+ * e.g., `"cover:title"`. See `manifest.ts` for encoding details.
65
+ */
66
+ slotId?: SlotId;
67
+
68
+ /**
69
+ * Optional background fill. When set, the reconciler emits an
70
+ * `updateShapeProperties` op immediately after the shape is created,
71
+ * before any text or text-style ops. An empty Box (no children) with a
72
+ * `fill` is a valid full-bleed colored background.
73
+ */
74
+ fill?: BoxFill;
75
+
76
+ /** Default text style applied to the shape's contents. */
77
+ textStyle?: TextStyle;
78
+
79
+ /** Default paragraph style. */
80
+ paragraphStyle?: ParagraphStyle;
81
+
82
+ /** Children: `<Text>` runs and/or raw strings. */
83
+ children?: ReactNode;
84
+ }
85
+
86
+ /**
87
+ * A styled text run inside a `<Box>`.
88
+ *
89
+ * Multiple `<Text>` children of the same `<Box>` are concatenated; each
90
+ * run's style is applied to its character range via `updateTextStyle` ops.
91
+ * Raw string children of `<Box>` are equivalent to `<Text>{text}</Text>`
92
+ * with no style override.
93
+ *
94
+ * The intent of carrying `textStyle` here (rather than mandating a typography
95
+ * token) is: high-level brand components compose this primitive *and* enforce
96
+ * brand-token discipline at *their* layer. The substrate stays brand-agnostic.
97
+ */
98
+ export interface TextProps {
99
+ /** Style applied to this run only. Merges over `<Box>`'s `textStyle`. */
100
+ textStyle?: TextStyle;
101
+ /** Children: text content. Nested elements are flattened to their text. */
102
+ children?: ReactNode;
103
+ }
104
+
105
+ /**
106
+ * A wrapper that sets the foreground color of its children.
107
+ *
108
+ * A thin convenience over `<Text textStyle={{foregroundColor}}/>` so
109
+ * brand components can read more declaratively. The reconciler treats this
110
+ * identically to a `<Text>` with the same style.
111
+ */
112
+ export interface ColorProps {
113
+ /** The color to apply, as a 24-bit hex. (`#RRGGBB`.) */
114
+ color: HexColor;
115
+ /** Children: text or `<Text>` runs. */
116
+ children?: ReactNode;
117
+ }
118
+
119
+ /**
120
+ * A reference to a brand-resolved image, including its provenance.
121
+ *
122
+ * The `artifact` field is **required**: every image rendered through the
123
+ * substrate is recorded in the generation manifest's `artifacts` list, so
124
+ * downstream tooling can detect 404s and verify content integrity. An
125
+ * `<Image>` without a `ArtifactRef` is impossible to construct —
126
+ * type-level enforcement of the manifest contract.
127
+ */
128
+ export interface ImageRef {
129
+ /** Resolved URL the runtime will fetch the image bytes from. */
130
+ readonly url: string;
131
+ /** Template-artifact provenance — recorded in manifest.artifacts. Required. */
132
+ readonly artifact: ArtifactRef;
133
+ }
134
+
135
+ /**
136
+ * A bitmap image placed on a slide, rendered as a `createImage` op.
137
+ *
138
+ * Sibling of `<Box>` inside a `<Slide>`. Used for full-bleed background
139
+ * photos, dot-grid texture overlays, brand logos, and any other rasterized
140
+ * brand artifact. Vector content (lines, rectangles, etc.) goes through
141
+ * `<Box>` and the (future) shape-kind extension to its fill prop.
142
+ */
143
+ export interface ImageProps {
144
+ /** The image's rect in *points*. The reconciler converts to EMU at the boundary. */
145
+ rect: Rect;
146
+
147
+ /**
148
+ * The image to render, including its brand-artifact provenance. Required —
149
+ * an `<Image>` without an `ImageRef` is unconstructable, by design.
150
+ */
151
+ image: ImageRef;
152
+
153
+ /**
154
+ * Optional slot ID. When set, the reconciler records `SlotId → imageId` in
155
+ * the manifest's slot registry.
156
+ */
157
+ slotId?: SlotId;
158
+
159
+ /**
160
+ * Accessibility alt-text shown to assistive tech.
161
+ *
162
+ * Carried through to the `createImage` op's `altText` field.
163
+ */
164
+ altText?: string;
165
+ }
166
+
167
+ /** A unique brand for each primitive's React component. */
168
+ type Marker<TProps> = ((props: TProps) => null) & { readonly __rgsKind: string };
169
+
170
+ const makeMarker = <TProps>(kind: string): Marker<TProps> => {
171
+ const component = (_props: TProps): null => null;
172
+ // Display name aids debugging in React DevTools and assertion messages.
173
+ Object.defineProperty(component, 'name', { value: kind });
174
+ return Object.assign(component, { __rgsKind: kind } as const);
175
+ };
176
+
177
+ export const Slide = makeMarker<SlideProps>('Slide');
178
+ export const Box = makeMarker<BoxProps>('Box');
179
+ export const Text = makeMarker<TextProps>('Text');
180
+ export const Color = makeMarker<ColorProps>('Color');
181
+ export const Image = makeMarker<ImageProps>('Image');
182
+
183
+ /**
184
+ * The set of primitive React components the reconciler recognizes.
185
+ *
186
+ * Used internally for type identity checks (see `reconciler.ts`). Exposed for
187
+ * downstream packages that want to detect "is this a substrate primitive?"
188
+ * without importing each one individually.
189
+ */
190
+ export const PRIMITIVES = { Slide, Box, Text, Color, Image } as const;
191
+
192
+ const PRIMITIVE_SET = new Set<unknown>(Object.values(PRIMITIVES));
193
+
194
+ /** Type guard: is this React element type one of our primitives? */
195
+ export const isPrimitive = (type: unknown): type is (typeof PRIMITIVES)[keyof typeof PRIMITIVES] =>
196
+ PRIMITIVE_SET.has(type);