@drawnagency/primitives 0.1.118 → 0.1.120

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 (279) hide show
  1. package/dist/{chunk-O37AV27P.js → chunk-5JYHTHVR.js} +28 -0
  2. package/dist/{chunk-W72BPGR3.js → chunk-BZKHHUIN.js} +44 -8
  3. package/dist/{chunk-4NSOKEA7.js → chunk-HFHM4MNK.js} +12 -7
  4. package/dist/{chunk-CEFFP3CH.js → chunk-HZTPF6OK.js} +304 -83
  5. package/dist/{chunk-RIITGTV3.js → chunk-LQHBC7C6.js} +37 -2
  6. package/dist/{chunk-VY3IFGE3.js → chunk-QAMAIDXZ.js} +1 -1
  7. package/dist/{chunk-HRX3XJBU.js → chunk-RIN2QCYJ.js} +9 -11
  8. package/dist/{chunk-ZOTRHHUB.js → chunk-UNFPATDE.js} +8 -10
  9. package/dist/{chunk-ERLIL4RU.js → chunk-UZBFVVTC.js} +35 -37
  10. package/dist/{chunk-67P34WNW.js → chunk-ZKVQMIMJ.js} +3 -20
  11. package/dist/collab/rebase.js +4 -5
  12. package/dist/components/chat/ChatComposer.d.ts +0 -4
  13. package/dist/components/chat/ChatComposer.d.ts.map +1 -1
  14. package/dist/components/chat/ChatPanel.d.ts +2 -3
  15. package/dist/components/chat/ChatPanel.d.ts.map +1 -1
  16. package/dist/components/chat/markdown.d.ts +2 -3
  17. package/dist/components/chat/markdown.d.ts.map +1 -1
  18. package/dist/components/editor/AudienceIndicator.d.ts +3 -1
  19. package/dist/components/editor/AudienceIndicator.d.ts.map +1 -1
  20. package/dist/components/editor/IndicatorPill.d.ts +3 -1
  21. package/dist/components/editor/IndicatorPill.d.ts.map +1 -1
  22. package/dist/components/editor/PagesModal.d.ts +0 -2
  23. package/dist/components/editor/PagesModal.d.ts.map +1 -1
  24. package/dist/components/editor/SectionBulkEditModal.d.ts +25 -0
  25. package/dist/components/editor/SectionBulkEditModal.d.ts.map +1 -0
  26. package/dist/components/editor/StatusIndicator.d.ts +3 -1
  27. package/dist/components/editor/StatusIndicator.d.ts.map +1 -1
  28. package/dist/components/editor/index.js +28 -26
  29. package/dist/components/primitives/index.js +11 -15
  30. package/dist/components/sections/brand-guide.js +10 -13
  31. package/dist/components/sections/register-schemas.js +16 -21
  32. package/dist/components/sections/register.js +16 -21
  33. package/dist/components/shared/Popover.d.ts +1 -1
  34. package/dist/components/shared/Popover.d.ts.map +1 -1
  35. package/dist/components/shared/SectionSummaryRow.d.ts +1 -1
  36. package/dist/components/shell/AudienceChips.d.ts +1 -3
  37. package/dist/components/shell/AudienceChips.d.ts.map +1 -1
  38. package/dist/components/shell/EditorModal.d.ts.map +1 -1
  39. package/dist/components/shell/EditorShell.d.ts +0 -34
  40. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  41. package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
  42. package/dist/hooks/useSortableNavRow.d.ts +1 -1
  43. package/dist/hooks/useSortableRow.d.ts +1 -1
  44. package/dist/index.js +83 -90
  45. package/dist/lib/index-ops.js +5 -6
  46. package/dist/lib/index.js +27 -32
  47. package/dist/lib/pages.d.ts +0 -2
  48. package/dist/lib/pages.d.ts.map +1 -1
  49. package/dist/lib/sidecars.js +2 -2
  50. package/dist/lib/site-validation.js +13 -14
  51. package/dist/lib/storage-tier.js +4 -4
  52. package/dist/schemas/index.js +42 -45
  53. package/dist/schemas/site-config.d.ts +1 -277
  54. package/dist/schemas/site-config.d.ts.map +1 -1
  55. package/package.json +2 -42
  56. package/src/components/chat/ChatComposer.tsx +0 -4
  57. package/src/components/chat/ChatPanel.tsx +2 -3
  58. package/src/components/chat/markdown.ts +2 -3
  59. package/src/components/editor/AudienceIndicator.tsx +8 -4
  60. package/src/components/editor/IndicatorPill.tsx +4 -0
  61. package/src/components/editor/PagesModal.tsx +1 -15
  62. package/src/components/editor/SectionBulkEditModal.tsx +328 -0
  63. package/src/components/editor/StatusIndicator.tsx +7 -6
  64. package/src/components/shared/Popover.tsx +17 -4
  65. package/src/components/shared/SectionSummaryRow.tsx +1 -1
  66. package/src/components/shell/AudienceChips.tsx +1 -3
  67. package/src/components/shell/EditorModal.tsx +5 -2
  68. package/src/components/shell/EditorShell.tsx +15 -119
  69. package/src/components/shell/SiteSettingsModal.tsx +0 -11
  70. package/src/lib/pages.ts +0 -22
  71. package/src/schemas/index.ts +0 -4
  72. package/src/schemas/site-config.ts +4 -32
  73. package/dist/chunk-4TZJZU2T.js +0 -92
  74. package/dist/chunk-DIMDRBFK.js +0 -11
  75. package/dist/chunk-FV7JQRCN.js +0 -94
  76. package/dist/chunk-IIDKW7AV.js +0 -41
  77. package/dist/chunk-JCDZAFX2.js +0 -224
  78. package/dist/chunk-MM7KZS5C.js +0 -30
  79. package/dist/chunk-QQ3S3P5P.js +0 -38
  80. package/dist/chunk-TEOAKGZA.js +0 -784
  81. package/dist/chunk-VAYZJKVR.js +0 -209
  82. package/dist/components/editor/SectionOrderingModal.d.ts +0 -10
  83. package/dist/components/editor/SectionOrderingModal.d.ts.map +0 -1
  84. package/dist/components/shell/DocInspector.d.ts +0 -18
  85. package/dist/components/shell/DocInspector.d.ts.map +0 -1
  86. package/dist/components/shell/DocumentEditorOverlay.d.ts +0 -29
  87. package/dist/components/shell/DocumentEditorOverlay.d.ts.map +0 -1
  88. package/dist/components/shell/DocumentPresentOverlay.d.ts +0 -30
  89. package/dist/components/shell/DocumentPresentOverlay.d.ts.map +0 -1
  90. package/dist/components/shell/SiteSettingsDecks.d.ts +0 -13
  91. package/dist/components/shell/SiteSettingsDecks.d.ts.map +0 -1
  92. package/dist/deck/DeckComponentView.d.ts +0 -13
  93. package/dist/deck/DeckComponentView.d.ts.map +0 -1
  94. package/dist/deck/DeckPlayer.d.ts +0 -26
  95. package/dist/deck/DeckPlayer.d.ts.map +0 -1
  96. package/dist/deck/DeckSlideView.d.ts +0 -17
  97. package/dist/deck/DeckSlideView.d.ts.map +0 -1
  98. package/dist/deck/DeckStage.d.ts +0 -16
  99. package/dist/deck/DeckStage.d.ts.map +0 -1
  100. package/dist/deck/components/DeckImage.d.ts +0 -14
  101. package/dist/deck/components/DeckImage.d.ts.map +0 -1
  102. package/dist/deck/components/DeckImageGrid.d.ts +0 -27
  103. package/dist/deck/components/DeckImageGrid.d.ts.map +0 -1
  104. package/dist/deck/components/DeckRichText.d.ts +0 -22
  105. package/dist/deck/components/DeckRichText.d.ts.map +0 -1
  106. package/dist/deck/components/DeckVideo.d.ts +0 -15
  107. package/dist/deck/components/DeckVideo.d.ts.map +0 -1
  108. package/dist/deck/editing.d.ts +0 -50
  109. package/dist/deck/editing.d.ts.map +0 -1
  110. package/dist/deck/flip.d.ts +0 -11
  111. package/dist/deck/flip.d.ts.map +0 -1
  112. package/dist/deck/index.d.ts +0 -24
  113. package/dist/deck/index.d.ts.map +0 -1
  114. package/dist/deck/index.js +0 -51
  115. package/dist/deck/layouts/CenteredTextLayout.d.ts +0 -12
  116. package/dist/deck/layouts/CenteredTextLayout.d.ts.map +0 -1
  117. package/dist/deck/layouts/ColumnSplitLayout.d.ts +0 -14
  118. package/dist/deck/layouts/ColumnSplitLayout.d.ts.map +0 -1
  119. package/dist/deck/layouts/FullBleedImageLayout.d.ts +0 -14
  120. package/dist/deck/layouts/FullBleedImageLayout.d.ts.map +0 -1
  121. package/dist/deck/media.d.ts +0 -33
  122. package/dist/deck/media.d.ts.map +0 -1
  123. package/dist/deck/tokens.d.ts +0 -22
  124. package/dist/deck/tokens.d.ts.map +0 -1
  125. package/dist/deck/useStageScale.d.ts +0 -10
  126. package/dist/deck/useStageScale.d.ts.map +0 -1
  127. package/dist/deck-builder/DeckBuilderApp.d.ts +0 -30
  128. package/dist/deck-builder/DeckBuilderApp.d.ts.map +0 -1
  129. package/dist/deck-builder/DeckBuilderTopBar.d.ts +0 -24
  130. package/dist/deck-builder/DeckBuilderTopBar.d.ts.map +0 -1
  131. package/dist/deck-builder/DeckChatPanel.d.ts +0 -45
  132. package/dist/deck-builder/DeckChatPanel.d.ts.map +0 -1
  133. package/dist/deck-builder/DeckEditor.d.ts +0 -37
  134. package/dist/deck-builder/DeckEditor.d.ts.map +0 -1
  135. package/dist/deck-builder/DeckLibrary.d.ts +0 -22
  136. package/dist/deck-builder/DeckLibrary.d.ts.map +0 -1
  137. package/dist/deck-builder/DeckSlideSurface.d.ts +0 -31
  138. package/dist/deck-builder/DeckSlideSurface.d.ts.map +0 -1
  139. package/dist/deck-builder/deck-api.d.ts +0 -76
  140. package/dist/deck-builder/deck-api.d.ts.map +0 -1
  141. package/dist/deck-builder/deck-chat-client.d.ts +0 -44
  142. package/dist/deck-builder/deck-chat-client.d.ts.map +0 -1
  143. package/dist/deck-builder/deck-chat-history.d.ts +0 -10
  144. package/dist/deck-builder/deck-chat-history.d.ts.map +0 -1
  145. package/dist/deck-builder/index.d.ts +0 -13
  146. package/dist/deck-builder/index.d.ts.map +0 -1
  147. package/dist/deck-builder/index.js +0 -1498
  148. package/dist/deck-builder/starter-deck.d.ts +0 -23
  149. package/dist/deck-builder/starter-deck.d.ts.map +0 -1
  150. package/dist/deck-builder/types.d.ts +0 -29
  151. package/dist/deck-builder/types.d.ts.map +0 -1
  152. package/dist/deck-builder/useSortableSlide.d.ts +0 -29
  153. package/dist/deck-builder/useSortableSlide.d.ts.map +0 -1
  154. package/dist/document/assets/fonts/albra-grotesk-bold.woff +0 -0
  155. package/dist/document/assets/fonts/albra-grotesk-regular.woff +0 -0
  156. package/dist/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
  157. package/dist/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
  158. package/dist/document/components/DocCoverPage.d.ts +0 -11
  159. package/dist/document/components/DocCoverPage.d.ts.map +0 -1
  160. package/dist/document/components/DocPage.d.ts +0 -10
  161. package/dist/document/components/DocPage.d.ts.map +0 -1
  162. package/dist/document/components/DocRail.d.ts +0 -28
  163. package/dist/document/components/DocRail.d.ts.map +0 -1
  164. package/dist/document/components/DocSectionPage.d.ts +0 -12
  165. package/dist/document/components/DocSectionPage.d.ts.map +0 -1
  166. package/dist/document/components/DocViewer.d.ts +0 -21
  167. package/dist/document/components/DocViewer.d.ts.map +0 -1
  168. package/dist/document/components/Wordmark.d.ts +0 -4
  169. package/dist/document/components/Wordmark.d.ts.map +0 -1
  170. package/dist/document/components/measure.d.ts +0 -7
  171. package/dist/document/components/measure.d.ts.map +0 -1
  172. package/dist/document/components/useDocPages.d.ts +0 -12
  173. package/dist/document/components/useDocPages.d.ts.map +0 -1
  174. package/dist/document/engine/blocks.d.ts +0 -2
  175. package/dist/document/engine/blocks.d.ts.map +0 -1
  176. package/dist/document/engine/harvest.d.ts +0 -3
  177. package/dist/document/engine/harvest.d.ts.map +0 -1
  178. package/dist/document/engine/masonry.d.ts +0 -26
  179. package/dist/document/engine/masonry.d.ts.map +0 -1
  180. package/dist/document/engine/paginate.d.ts +0 -43
  181. package/dist/document/engine/paginate.d.ts.map +0 -1
  182. package/dist/document/engine/settings.d.ts +0 -91
  183. package/dist/document/engine/settings.d.ts.map +0 -1
  184. package/dist/document/engine/types.d.ts +0 -44
  185. package/dist/document/engine/types.d.ts.map +0 -1
  186. package/dist/document/index.d.ts +0 -13
  187. package/dist/document/index.d.ts.map +0 -1
  188. package/dist/document/index.js +0 -1030
  189. package/dist/document/styles/document.css +0 -217
  190. package/dist/lib/deck-access.d.ts +0 -86
  191. package/dist/lib/deck-access.d.ts.map +0 -1
  192. package/dist/lib/deck-access.js +0 -58
  193. package/dist/lib/deck-dexie.d.ts +0 -88
  194. package/dist/lib/deck-dexie.d.ts.map +0 -1
  195. package/dist/lib/deck-dexie.js +0 -22
  196. package/dist/lib/deck-edit.d.ts +0 -67
  197. package/dist/lib/deck-edit.d.ts.map +0 -1
  198. package/dist/lib/deck-media-assets.d.ts +0 -27
  199. package/dist/lib/deck-media-assets.d.ts.map +0 -1
  200. package/dist/lib/deck-media-assets.js +0 -25
  201. package/dist/lib/deck-media-refs.d.ts +0 -35
  202. package/dist/lib/deck-media-refs.d.ts.map +0 -1
  203. package/dist/lib/deck-media-refs.js +0 -36
  204. package/dist/lib/deck-tool.d.ts +0 -25
  205. package/dist/lib/deck-tool.d.ts.map +0 -1
  206. package/dist/lib/deck-tool.js +0 -58
  207. package/dist/lib/document-input.d.ts +0 -25
  208. package/dist/lib/document-input.d.ts.map +0 -1
  209. package/dist/schemas/deck-meta.d.ts +0 -85
  210. package/dist/schemas/deck-meta.d.ts.map +0 -1
  211. package/dist/schemas/deck-meta.js +0 -64
  212. package/dist/schemas/deck.d.ts +0 -954
  213. package/dist/schemas/deck.d.ts.map +0 -1
  214. package/dist/schemas/deck.js +0 -65
  215. package/src/components/editor/SectionOrderingModal.tsx +0 -80
  216. package/src/components/shell/DocInspector.tsx +0 -314
  217. package/src/components/shell/DocumentEditorOverlay.tsx +0 -179
  218. package/src/components/shell/DocumentPresentOverlay.tsx +0 -87
  219. package/src/components/shell/SiteSettingsDecks.tsx +0 -41
  220. package/src/deck/DeckComponentView.tsx +0 -28
  221. package/src/deck/DeckPlayer.tsx +0 -166
  222. package/src/deck/DeckSlideView.tsx +0 -38
  223. package/src/deck/DeckStage.tsx +0 -39
  224. package/src/deck/components/DeckImage.tsx +0 -38
  225. package/src/deck/components/DeckImageGrid.tsx +0 -254
  226. package/src/deck/components/DeckRichText.tsx +0 -111
  227. package/src/deck/components/DeckVideo.tsx +0 -81
  228. package/src/deck/editing.tsx +0 -201
  229. package/src/deck/flip.ts +0 -70
  230. package/src/deck/index.ts +0 -30
  231. package/src/deck/layouts/CenteredTextLayout.tsx +0 -29
  232. package/src/deck/layouts/ColumnSplitLayout.tsx +0 -69
  233. package/src/deck/layouts/FullBleedImageLayout.tsx +0 -58
  234. package/src/deck/media.tsx +0 -46
  235. package/src/deck/tokens.ts +0 -57
  236. package/src/deck/useStageScale.ts +0 -44
  237. package/src/deck-builder/DeckBuilderApp.tsx +0 -572
  238. package/src/deck-builder/DeckBuilderTopBar.tsx +0 -62
  239. package/src/deck-builder/DeckChatPanel.tsx +0 -334
  240. package/src/deck-builder/DeckEditor.tsx +0 -258
  241. package/src/deck-builder/DeckLibrary.tsx +0 -195
  242. package/src/deck-builder/DeckSlideSurface.tsx +0 -55
  243. package/src/deck-builder/deck-api.ts +0 -177
  244. package/src/deck-builder/deck-chat-client.ts +0 -129
  245. package/src/deck-builder/deck-chat-history.ts +0 -62
  246. package/src/deck-builder/index.ts +0 -12
  247. package/src/deck-builder/starter-deck.ts +0 -86
  248. package/src/deck-builder/types.ts +0 -24
  249. package/src/deck-builder/useSortableSlide.ts +0 -102
  250. package/src/document/assets/fonts/albra-grotesk-bold.woff +0 -0
  251. package/src/document/assets/fonts/albra-grotesk-regular.woff +0 -0
  252. package/src/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
  253. package/src/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
  254. package/src/document/components/DocCoverPage.tsx +0 -45
  255. package/src/document/components/DocPage.tsx +0 -40
  256. package/src/document/components/DocRail.tsx +0 -189
  257. package/src/document/components/DocSectionPage.tsx +0 -48
  258. package/src/document/components/DocViewer.tsx +0 -313
  259. package/src/document/components/Wordmark.tsx +0 -18
  260. package/src/document/components/measure.tsx +0 -133
  261. package/src/document/components/useDocPages.ts +0 -95
  262. package/src/document/engine/blocks.ts +0 -116
  263. package/src/document/engine/harvest.ts +0 -95
  264. package/src/document/engine/masonry.ts +0 -158
  265. package/src/document/engine/paginate.ts +0 -232
  266. package/src/document/engine/settings.ts +0 -125
  267. package/src/document/engine/types.ts +0 -64
  268. package/src/document/index.ts +0 -18
  269. package/src/document/styles/document.css +0 -217
  270. package/src/lib/deck-access.ts +0 -132
  271. package/src/lib/deck-dexie.ts +0 -190
  272. package/src/lib/deck-edit.ts +0 -196
  273. package/src/lib/deck-media-assets.ts +0 -57
  274. package/src/lib/deck-media-refs.ts +0 -67
  275. package/src/lib/deck-tool.ts +0 -95
  276. package/src/lib/document-input.ts +0 -57
  277. package/src/schemas/deck-meta.ts +0 -88
  278. package/src/schemas/deck.ts +0 -478
  279. package/dist/{chunk-DJABADOO.js → chunk-AJ2QOAZI.js} +3 -3
@@ -1,254 +0,0 @@
1
- import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
- import { cn } from "../../lib/cn";
3
- import type { DeckImageGrid as DeckImageGridContent, DeckImageGridItem, DeckMediaAsset } from "../../schemas/deck";
4
- import { captureTileRects, playTileFlip, type TileRects } from "../flip";
5
- import { DeckMissingMedia, useDeckMediaResolver } from "../media";
6
- import { DECK_ELEVATION, surfaceInk } from "../tokens";
7
-
8
- interface Props {
9
- content: DeckImageGridContent;
10
- }
11
-
12
- /**
13
- * Two columns for a small set, three once there are enough tiles to fill them.
14
- * Deliberately not a schema field: it is presentation, and one fewer knob for
15
- * an author to get wrong.
16
- */
17
- function columnsFor(count: number): number {
18
- return count <= 4 ? 2 : 3;
19
- }
20
-
21
- /**
22
- * A tile's height as a multiple of the column width. Falls back to a square
23
- * when the resolver reports no dimensions — the packing still has to be
24
- * deterministic, and a square is the least-wrong guess.
25
- */
26
- function tileRatio(asset: DeckMediaAsset | null): number {
27
- if (asset?.width === undefined || asset.height === undefined || asset.width <= 0) return 1;
28
- return asset.height / asset.width;
29
- }
30
-
31
- /**
32
- * Greedy shortest-column packing: each tile joins whichever column is currently
33
- * least tall, ties going left. With uniform aspect ratios that degenerates to
34
- * round-robin, which is what makes the simple cases look deliberate.
35
- */
36
- export function packMasonry(ratios: number[], columns: number): number[][] {
37
- const packed: number[][] = Array.from({ length: columns }, () => []);
38
- const filled = new Array<number>(columns).fill(0);
39
- ratios.forEach((ratio, index) => {
40
- let target = 0;
41
- for (let column = 1; column < columns; column++) {
42
- if (filled[column] < filled[target]) target = column;
43
- }
44
- packed[target].push(index);
45
- filled[target] += ratio;
46
- });
47
- return packed;
48
- }
49
-
50
- const TILE_BASE =
51
- "group relative block shrink-0 overflow-hidden bg-base-200 will-change-transform transition-[transform,box-shadow,opacity] duration-[380ms] ease-[cubic-bezier(0.2,0.75,0.2,1)] focus-visible:outline-2 focus-visible:outline-offset-[-6px] focus-visible:outline-[var(--color-primary)]";
52
-
53
- /**
54
- * Deck Component — image grid, and the runtime's signature interaction.
55
- *
56
- * Masonry columns: every image keeps its own aspect ratio, the column width
57
- * decides its height, and a stack that runs past the bottom of the slot is
58
- * clipped there. That clip is the point — on a fixed 16:9 stage a collage that
59
- * runs off the bottom edge reads better than one that crops every frame to a
60
- * uniform cell, and nothing may spill past the slot onto the rest of the slide.
61
- *
62
- * Hover lifts a tile; activating it expands that tile to fill the whole grid
63
- * region as a lightbox contained inside the slide (never a full-screen overlay
64
- * — the deck stays the deck). Activating again, the close affordance, or Escape
65
- * returns it. Tiles are real buttons, so keyboard activation is the browser's.
66
- */
67
- export function DeckImageGrid({ content }: Props) {
68
- const [active, setActive] = useState<number | null>(null);
69
- const gridRef = useRef<HTMLDivElement>(null);
70
- const pendingRects = useRef<TileRects | null>(null);
71
-
72
- const expanded = active !== null;
73
- const resolve = useDeckMediaResolver();
74
- const count = content.items.length;
75
-
76
- const assets = useMemo(() => content.items.map((item) => resolve(item.media)), [content.items, resolve]);
77
- const columns = columnsFor(count);
78
- const packed = useMemo(() => packMasonry(assets.map(tileRatio), columns), [assets, columns]);
79
-
80
- // Measure BEFORE the state change: React moves the DOM, and FLIP needs the
81
- // pre-move rects, which no longer exist by the time the effect runs.
82
- const transitionTo = useCallback((next: number | null) => {
83
- const grid = gridRef.current;
84
- if (grid) pendingRects.current = captureTileRects(grid);
85
- setActive(next);
86
- }, []);
87
-
88
- useLayoutEffect(() => {
89
- const grid = gridRef.current;
90
- const before = pendingRects.current;
91
- pendingRects.current = null;
92
- if (grid && before) playTileFlip(grid, before);
93
- }, [active]);
94
-
95
- // The Deck Builder replaces the whole deck every LLM turn, so a grid can lose
96
- // tiles under an open lightbox. Left alone, the expanded index would point
97
- // past the end and every tile would render hidden — a blank slot with no way
98
- // out but Escape.
99
- useEffect(() => {
100
- setActive((prev) => (prev === null || prev < count ? prev : null));
101
- }, [count]);
102
-
103
- useEffect(() => {
104
- if (!expanded) return;
105
- const onKeyDown = (event: KeyboardEvent) => {
106
- if (event.key !== "Escape") return;
107
- // NOTE: this stops the event travelling to an ancestor, which for a
108
- // window listener is nothing at all — it does NOT stop other listeners on
109
- // window from also seeing this Escape. A future shell-level Escape (exit
110
- // presentation, close the builder) has to coordinate with the grid
111
- // explicitly; it cannot rely on this call to have swallowed the key.
112
- event.stopPropagation();
113
- transitionTo(null);
114
- };
115
- window.addEventListener("keydown", onKeyDown);
116
- return () => window.removeEventListener("keydown", onKeyDown);
117
- }, [expanded, transitionTo]);
118
-
119
- /**
120
- * Close returns focus to the tile that was expanded. Without it the close
121
- * button unmounts under the caret and focus falls to <body>, which strands a
122
- * keyboard user outside the grid entirely. Focus first, then re-render, so
123
- * the unmounting button is no longer the active element when it goes.
124
- */
125
- const close = useCallback(() => {
126
- gridRef.current?.querySelector<HTMLElement>(`[data-deck-tile="${active}"]`)?.focus({ preventScroll: true });
127
- transitionTo(null);
128
- }, [active, transitionTo]);
129
-
130
- return (
131
- <div
132
- ref={gridRef}
133
- data-deck-component="imageGrid"
134
- data-deck-grid-expanded={expanded ? "true" : "false"}
135
- data-deck-grid-columns={columns}
136
- style={DECK_ELEVATION}
137
- // Clipped, not scrolled: a deck slide has no scrollbars, and a stack that
138
- // runs long is meant to run off the bottom of the slot.
139
- className="relative flex size-full gap-[14px] overflow-hidden"
140
- >
141
- {packed.map((indices, column) => (
142
- // `flex-1` rather than a width fraction: the columns have to share the
143
- // gap between them, and equal flex bases divide what is left over.
144
- <div key={column} data-deck-grid-column={column} className="flex min-w-0 flex-1 flex-col gap-[14px]">
145
- {indices.map((index) => (
146
- <Tile
147
- key={index}
148
- item={content.items[index]}
149
- asset={assets[index]}
150
- index={index}
151
- expanded={expanded}
152
- active={index === active}
153
- onActivate={() => transitionTo(index === active ? null : index)}
154
- />
155
- ))}
156
- </div>
157
- ))}
158
-
159
- {expanded && (
160
- <button
161
- type="button"
162
- data-deck-grid-close=""
163
- onClick={close}
164
- data-deck-surface="brand"
165
- style={surfaceInk("brand")}
166
- className="absolute top-[14px] right-[14px] z-[4] bg-primary px-[18px] py-[10px] text-[15px] font-semibold tracking-[0.12em] text-[var(--deck-ink)] uppercase"
167
- >
168
- Close
169
- </button>
170
- )}
171
- </div>
172
- );
173
- }
174
-
175
- interface TileProps {
176
- item: DeckImageGridItem;
177
- asset: DeckMediaAsset | null;
178
- index: number;
179
- expanded: boolean;
180
- active: boolean;
181
- onActivate: () => void;
182
- }
183
-
184
- function Tile({ item, asset, index, expanded, active, onActivate }: TileProps) {
185
- const label = item.caption ?? asset?.alt ?? `Image ${index + 1}`;
186
-
187
- return (
188
- <button
189
- type="button"
190
- data-deck-tile={index}
191
- data-deck-tile-active={active ? "true" : "false"}
192
- aria-expanded={active}
193
- aria-label={active ? `Close ${label}` : `Expand ${label}`}
194
- tabIndex={expanded && !active ? -1 : 0}
195
- onClick={onActivate}
196
- className={cn(
197
- TILE_BASE,
198
- // Every tile leaves the column flow when one is expanded, not only the
199
- // active one: leaving the siblings in place would let the columns
200
- // re-pack around the gap, which both reflows the tiles the lightbox is
201
- // covering and moves the ones the FLIP is animating.
202
- expanded ? "absolute inset-0 flex items-center justify-center" : "w-full",
203
- expanded && !active && "pointer-events-none opacity-0",
204
- // Transparent, not the tile's loading plate: an expanded frame is
205
- // letterboxed inside the slot, and the field around it should read as
206
- // the slide showing through rather than as a grey box drawn on top.
207
- expanded && active && "z-[3] bg-transparent opacity-100",
208
- !expanded && "cursor-pointer hover:z-[2] hover:scale-[1.035] hover:shadow-[var(--deck-lift)]",
209
- )}
210
- >
211
- {asset === null ? (
212
- <DeckMissingMedia className={active ? "size-full" : "aspect-square h-auto w-full"} />
213
- ) : (
214
- <img
215
- src={asset.src}
216
- alt=""
217
- // The intrinsic size, when the resolver knows it, so the column
218
- // reserves the right height before a byte arrives — otherwise the
219
- // masonry re-packs visibly as each image lands.
220
- width={asset.width}
221
- height={asset.height}
222
- // The elevation moves onto the image when expanded: the button box is
223
- // the whole slot now, and a shadow on THAT would outline the
224
- // letterbox instead of the frame the viewer is looking at.
225
- className={
226
- active ? "max-h-full max-w-full object-contain shadow-[var(--deck-lift-strong)]" : "block h-auto w-full"
227
- }
228
- />
229
- )}
230
-
231
- <span
232
- aria-hidden="true"
233
- data-deck-surface="brand"
234
- style={surfaceInk("brand")}
235
- className="absolute top-0 left-0 bg-primary px-[12px] py-[6px] text-[15px] font-bold tracking-[0.1em] text-[var(--deck-ink)] tabular-nums"
236
- >
237
- {String(index + 1).padStart(2, "0")}
238
- </span>
239
-
240
- {item.caption !== undefined && (
241
- <span
242
- data-deck-surface="brand"
243
- style={surfaceInk("brand")}
244
- className={cn(
245
- "absolute right-0 bottom-0 left-0 bg-primary text-left tracking-[0.04em] text-[var(--deck-ink)] transition-opacity duration-200",
246
- active ? "px-[30px] py-[22px] text-[24px] opacity-100" : "px-[18px] py-[14px] text-[18px] opacity-0 group-hover:opacity-100",
247
- )}
248
- >
249
- {item.caption}
250
- </span>
251
- )}
252
- </button>
253
- );
254
- }
@@ -1,111 +0,0 @@
1
- import { Fragment, type ReactNode } from "react";
2
- import type { DeckTextPath, DeckTextSlot } from "../../lib/deck-edit";
3
- import type { DeckInlineText, DeckRichText as DeckRichTextContent } from "../../schemas/deck";
4
- import { EditableText } from "../editing";
5
-
6
- interface Props {
7
- content: DeckRichTextContent;
8
- /**
9
- * Which slot of the enclosing Layout this component fills. Supplied only so a
10
- * text leaf can address itself for inline editing (#39); absent means "no
11
- * edit seam here", which is the read-only default every viewer surface gets.
12
- */
13
- slot?: DeckTextSlot;
14
- }
15
-
16
- /**
17
- * A body line. A plain string renders as itself; a span run renders its marks
18
- * as elements. Nothing here reads a string as markup — `bold` and `italic` are
19
- * booleans on the span, which is the whole point of the representation.
20
- *
21
- * The editable unit is the LEAF, never the line: a marked run offers one field
22
- * per span, so editing the words around a bold phrase cannot flatten it. That
23
- * is what lets inline editing be plain-text-only without a formatting UI and
24
- * without silently dropping marks (#39).
25
- */
26
- function InlineText({ value, path }: { value: DeckInlineText; path?: DeckTextPath }) {
27
- if (typeof value === "string") return <EditableText path={path}>{value}</EditableText>;
28
- return (
29
- <>
30
- {value.map((span, i) => {
31
- let node: ReactNode = (
32
- <EditableText path={path && { ...path, span: i }}>{span.text}</EditableText>
33
- );
34
- if (span.italic) node = <em className="italic">{node}</em>;
35
- // Bold pulls the phrase back to full ink: body copy renders muted, so
36
- // weight alone barely registers at presentation size.
37
- if (span.bold) node = <strong className="font-semibold text-[var(--deck-ink)]">{node}</strong>;
38
- return <Fragment key={i}>{node}</Fragment>;
39
- })}
40
- </>
41
- );
42
- }
43
-
44
- /**
45
- * Deck Component — rich text.
46
- *
47
- * Every size is px against the 1600x900 stage; every color, family and radius
48
- * is a token (`--deck-*` are set by the enclosing Layout, see ./tokens.ts).
49
- * Fields are typographic roles, not markup: the schema carries plain strings
50
- * and marked spans, so nothing here interpolates HTML.
51
- */
52
- export function DeckRichText({ content, slot }: Props) {
53
- const { kicker, heading, subheading, paragraphs, bullets } = content;
54
- const at = (field: DeckTextPath["field"], line?: number): DeckTextPath | undefined =>
55
- slot === undefined ? undefined : { slot, field, line };
56
-
57
- return (
58
- <div data-deck-component="richText" className="flex flex-col gap-[28px]">
59
- {kicker !== undefined && (
60
- <div className="flex flex-col gap-[18px]">
61
- <p className="text-[22px] font-semibold tracking-[0.2em] text-[var(--deck-accent)] [text-transform:var(--nav-text-transform)]">
62
- <EditableText path={at("kicker")}>{kicker}</EditableText>
63
- </p>
64
- <span
65
- aria-hidden="true"
66
- // Inline, not `self-[var(…)]`: Tailwind never generated CSS for that
67
- // arbitrary self- value, so the divider silently ignored --deck-align
68
- // and hugged the left edge on centered layouts.
69
- style={{ alignSelf: "var(--deck-align, start)" }}
70
- className="h-[4px] w-[84px] rounded-[var(--radius-inner)] bg-[var(--deck-accent)]"
71
- />
72
- </div>
73
- )}
74
-
75
- {heading !== undefined && (
76
- <h1 className="font-heading text-[88px] leading-[1.06] font-medium tracking-[-0.015em] text-[var(--deck-ink)] [text-transform:var(--heading-text-transform)]">
77
- <EditableText path={at("heading")}>{heading}</EditableText>
78
- </h1>
79
- )}
80
-
81
- {subheading !== undefined && (
82
- <h2 className="font-heading text-[38px] leading-[1.25] font-normal text-[var(--deck-ink-muted)] [text-transform:var(--subheading-text-transform)]">
83
- <EditableText path={at("subheading")}>{subheading}</EditableText>
84
- </h2>
85
- )}
86
-
87
- {paragraphs !== undefined && paragraphs.length > 0 && (
88
- <div className="flex max-w-[1000px] flex-col gap-[18px] self-[var(--deck-align,start)]">
89
- {paragraphs.map((paragraph, i) => (
90
- <p key={i} className="text-[26px] leading-[1.6] text-[var(--deck-ink-muted)]">
91
- <InlineText value={paragraph} path={at("paragraphs", i)} />
92
- </p>
93
- ))}
94
- </div>
95
- )}
96
-
97
- {bullets !== undefined && bullets.length > 0 && (
98
- <ul className="flex flex-col gap-[16px] self-[var(--deck-align,start)] text-left">
99
- {bullets.map((bullet, i) => (
100
- <li key={i} className="flex items-start gap-[18px] text-[26px] leading-[1.45] text-[var(--deck-ink)]">
101
- <span aria-hidden="true" className="mt-[14px] size-[10px] shrink-0 rounded-full bg-[var(--deck-accent)]" />
102
- <span>
103
- <InlineText value={bullet} path={at("bullets", i)} />
104
- </span>
105
- </li>
106
- ))}
107
- </ul>
108
- )}
109
- </div>
110
- );
111
- }
@@ -1,81 +0,0 @@
1
- import { Play } from "lucide-react";
2
- import { useRef, useState } from "react";
3
- import type { DeckVideo as DeckVideoContent } from "../../schemas/deck";
4
- import { DeckMissingMedia, useDeckMedia } from "../media";
5
- import { DECK_ELEVATION, surfaceInk } from "../tokens";
6
-
7
- interface Props {
8
- content: DeckVideoContent;
9
- }
10
-
11
- /**
12
- * Deck Component — video.
13
- *
14
- * Two states: a poster with a play affordance, and playback with the browser's
15
- * own controls. Nothing custom drives the media element once it is playing —
16
- * a presenter needs scrubbing and volume, and native controls are the ones
17
- * their keyboard already knows.
18
- */
19
- export function DeckVideo({ content }: Props) {
20
- const asset = useDeckMedia(content.media);
21
- const videoRef = useRef<HTMLVideoElement>(null);
22
- const [playing, setPlaying] = useState(false);
23
-
24
- const start = () => {
25
- setPlaying(true);
26
- // Rejects when the source is unplayable (wrong codec, a media id whose
27
- // bytes are gone) — the poster is already dismissed, so just don't leave
28
- // an unhandled rejection behind.
29
- videoRef.current?.play()?.catch(() => undefined);
30
- };
31
-
32
- return (
33
- <div
34
- data-deck-component="video"
35
- data-deck-video-state={playing ? "playing" : "idle"}
36
- style={DECK_ELEVATION}
37
- className="relative size-full overflow-hidden bg-base-200"
38
- >
39
- {asset === null ? (
40
- <DeckMissingMedia />
41
- ) : (
42
- <video
43
- ref={videoRef}
44
- src={asset.src}
45
- poster={asset.poster}
46
- // Nothing is fetched until the poster is dismissed: a deck is mostly
47
- // slides you never reach, and the poster is an explicit URL anyway.
48
- preload="none"
49
- playsInline
50
- controls={playing}
51
- onEnded={() => setPlaying(false)}
52
- className="size-full object-cover"
53
- />
54
- )}
55
-
56
- {!playing && asset !== null && (
57
- <button
58
- type="button"
59
- data-deck-video-play=""
60
- onClick={start}
61
- aria-label={content.title === undefined ? "Play video" : `Play ${content.title}`}
62
- className="group absolute inset-0 grid cursor-pointer place-items-center"
63
- >
64
- <span className="grid size-[108px] place-items-center rounded-full bg-primary text-[var(--color-on-primary)] shadow-[var(--deck-lift)] transition-transform duration-200 group-hover:scale-105">
65
- <Play aria-hidden="true" className="ml-[6px] size-[44px] fill-current" />
66
- </span>
67
- </button>
68
- )}
69
-
70
- {!playing && content.title !== undefined && (
71
- <span
72
- data-deck-surface="brand"
73
- style={surfaceInk("brand")}
74
- className="pointer-events-none absolute right-0 bottom-0 left-0 bg-primary px-[28px] py-[18px] text-[18px] tracking-[0.06em] text-[var(--deck-ink)] [text-transform:var(--nav-text-transform)]"
75
- >
76
- {content.title}
77
- </span>
78
- )}
79
- </div>
80
- );
81
- }
@@ -1,201 +0,0 @@
1
- import { createContext, useContext, useEffect, useRef, useState, type ReactNode } from "react";
2
- import type { DeckTextPath } from "../lib/deck-edit";
3
-
4
- /**
5
- * The runtime's inline-editing seam (#39).
6
- *
7
- * Editing happens INSIDE the real renderer rather than in an overlay that
8
- * imitates it, which is the only way "renders identically on blur" is true
9
- * rather than approximately true — the element the author types into is the
10
- * element the deck ships with.
11
- *
12
- * The seam is a context that defaults to null, so every read-only surface — a
13
- * filmstrip thumbnail, an in-portal deck view, the Public Link route — renders
14
- * exactly the markup it rendered before this file existed, with no affordance
15
- * and no listeners. Providing the context is an explicit act, performed in one
16
- * place (`DeckSlideSurface`).
17
- */
18
-
19
- export interface DeckTextEditor {
20
- /** Commit a leaf edit. The provider owns validation and staging. */
21
- onEditText: (path: DeckTextPath, text: string) => void;
22
- /**
23
- * Locked while a chat turn is in flight: a turn rewrites the WHOLE deck
24
- * (ADR 0001), so an edit committed mid-stream would be written over by the
25
- * turn's own `putDeckDraft` with nothing anywhere saying so.
26
- */
27
- disabled?: boolean;
28
- }
29
-
30
- const DeckTextEditContext = createContext<DeckTextEditor | null>(null);
31
-
32
- export function DeckTextEditProvider({ editor, children }: { editor: DeckTextEditor | null; children: ReactNode }) {
33
- return <DeckTextEditContext.Provider value={editor}>{children}</DeckTextEditContext.Provider>;
34
- }
35
-
36
- export function useDeckTextEditor(): DeckTextEditor | null {
37
- return useContext(DeckTextEditContext);
38
- }
39
-
40
- interface EditableTextProps {
41
- /** Absent when the enclosing component was rendered without a slot — i.e. by
42
- * a surface that never carries an edit seam. Renders inert. */
43
- path?: DeckTextPath;
44
- /** The leaf's current text. Always a plain string; that is the schema's shape. */
45
- children: string;
46
- }
47
-
48
- /**
49
- * One editable text leaf, or plain text when there is no editor.
50
- *
51
- * The two states are DIFFERENT ELEMENTS on purpose. React does not manage the
52
- * text of a `contentEditable` node it has no children for, so the field can be
53
- * written to freely while it is open, and swapping back to a React-rendered
54
- * span on commit is what makes the committed value the one that renders — no
55
- * reconciliation against a DOM the author mutated behind React's back.
56
- */
57
- export function EditableText({ path, children }: EditableTextProps) {
58
- const editor = useDeckTextEditor();
59
- const [editing, setEditing] = useState(false);
60
-
61
- const enabled = editor !== null && path !== undefined && editor.disabled !== true;
62
-
63
- // Losing the seam mid-edit (a chat turn starting, the surface going
64
- // read-only) closes the field rather than leaving an orphaned editable node.
65
- useEffect(() => {
66
- if (!enabled && editing) setEditing(false);
67
- }, [enabled, editing]);
68
-
69
- if (editor === null || path === undefined) return <>{children}</>;
70
-
71
- if (editing && enabled) {
72
- return (
73
- <EditableField
74
- initial={children}
75
- onCommit={(text) => {
76
- setEditing(false);
77
- // An emptied field is a cancel, not an edit: every one of these leaves
78
- // is `min(1)` in the schema, so committing "" could only ever be
79
- // refused — and refusing it with an error message would read as a
80
- // failure rather than as "nothing happened".
81
- const trimmed = text.trim();
82
- if (trimmed !== "" && trimmed !== children) editor.onEditText(path, trimmed);
83
- }}
84
- onCancel={() => setEditing(false)}
85
- />
86
- );
87
- }
88
-
89
- return (
90
- <span
91
- role="button"
92
- tabIndex={enabled ? 0 : -1}
93
- aria-disabled={enabled ? undefined : true}
94
- data-deck-editable=""
95
- onClick={enabled ? () => setEditing(true) : undefined}
96
- onKeyDown={
97
- enabled
98
- ? (event) => {
99
- if (event.key !== "Enter" && event.key !== " ") return;
100
- event.preventDefault();
101
- setEditing(true);
102
- }
103
- : undefined
104
- }
105
- className={
106
- enabled
107
- ? "cursor-text rounded-[var(--radius-inner)] decoration-dotted underline-offset-[6px] outline-none hover:underline focus-visible:underline"
108
- : "opacity-70"
109
- }
110
- >
111
- {children}
112
- </span>
113
- );
114
- }
115
-
116
- /**
117
- * The open field. Rendered with no React children so React never fights the
118
- * author for the node's text; the initial value is written once on mount.
119
- */
120
- function EditableField({
121
- initial,
122
- onCommit,
123
- onCancel,
124
- }: {
125
- initial: string;
126
- onCommit: (text: string) => void;
127
- onCancel: () => void;
128
- }) {
129
- const ref = useRef<HTMLSpanElement>(null);
130
- // Escape has to beat the blur that follows it, and blur reads this ref rather
131
- // than state because the two happen in the same tick.
132
- const cancelled = useRef(false);
133
-
134
- useEffect(() => {
135
- const element = ref.current;
136
- if (!element) return;
137
- element.textContent = initial;
138
- element.focus();
139
- const selection = window.getSelection?.();
140
- if (selection) {
141
- const range = document.createRange();
142
- range.selectNodeContents(element);
143
- selection.removeAllRanges();
144
- selection.addRange(range);
145
- }
146
- }, [initial]);
147
-
148
- return (
149
- <span
150
- ref={ref}
151
- contentEditable
152
- suppressContentEditableWarning
153
- role="textbox"
154
- aria-label="Edit text"
155
- spellCheck
156
- data-deck-editing=""
157
- onKeyDown={(event) => {
158
- // Mid-composition, Enter and Escape belong to the IME: Enter accepts a
159
- // conversion candidate and Escape abandons it. Committing or cancelling
160
- // the field there cuts a CJK author off mid-word.
161
- if (event.nativeEvent.isComposing) return;
162
- if (event.key === "Enter") {
163
- // A deck text leaf is one line in the schema, so Enter means "done",
164
- // not "newline" — the layouts own line breaking.
165
- event.preventDefault();
166
- event.currentTarget.blur();
167
- return;
168
- }
169
- if (event.key === "Escape") {
170
- event.preventDefault();
171
- cancelled.current = true;
172
- event.currentTarget.blur();
173
- }
174
- }}
175
- onPaste={(event) => {
176
- // A deck text leaf is a plain string, and the field yields `textContent`
177
- // — so pasted markup would show live formatting that vanishes on blur,
178
- // and pasted block elements would join with no separator at all
179
- // (`<div>a</div><div>b</div>` reads back as "ab"). Take the plain text
180
- // and flatten its line breaks to spaces, which is what a one-line field
181
- // can honestly hold.
182
- event.preventDefault();
183
- const text = event.clipboardData.getData("text/plain").replace(/\s+/g, " ").trim();
184
- const doc = event.currentTarget.ownerDocument;
185
- // Guarded because jsdom does not implement it; `insertText` is used
186
- // rather than a Selection-API splice because it is what keeps the
187
- // field's native undo stack intact.
188
- if (text !== "" && typeof doc.execCommand === "function") doc.execCommand("insertText", false, text);
189
- }}
190
- onBlur={(event) => {
191
- if (cancelled.current) {
192
- cancelled.current = false;
193
- onCancel();
194
- return;
195
- }
196
- onCommit(event.currentTarget.textContent ?? "");
197
- }}
198
- className="rounded-[var(--radius-inner)] outline-2 outline-offset-2 outline-[var(--deck-accent)]"
199
- />
200
- );
201
- }