@drawnagency/primitives 0.1.118 → 0.1.119

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 (258) 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-CEFFP3CH.js → chunk-G6O3TYPJ.js} +297 -83
  4. package/dist/{chunk-4NSOKEA7.js → chunk-HFHM4MNK.js} +12 -7
  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-YKKGVSJH.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/PagesModal.d.ts +0 -2
  19. package/dist/components/editor/PagesModal.d.ts.map +1 -1
  20. package/dist/components/editor/index.js +15 -19
  21. package/dist/components/primitives/index.js +11 -15
  22. package/dist/components/sections/brand-guide.js +10 -13
  23. package/dist/components/sections/register-schemas.js +16 -21
  24. package/dist/components/sections/register.js +16 -21
  25. package/dist/components/shell/AudienceChips.d.ts +1 -3
  26. package/dist/components/shell/AudienceChips.d.ts.map +1 -1
  27. package/dist/components/shell/EditorShell.d.ts +0 -34
  28. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  29. package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
  30. package/dist/hooks/useSortableNavRow.d.ts +1 -1
  31. package/dist/hooks/useSortableRow.d.ts +1 -1
  32. package/dist/index.js +83 -90
  33. package/dist/lib/index-ops.js +5 -6
  34. package/dist/lib/index.js +27 -32
  35. package/dist/lib/pages.d.ts +0 -2
  36. package/dist/lib/pages.d.ts.map +1 -1
  37. package/dist/lib/sidecars.js +2 -2
  38. package/dist/lib/site-validation.js +13 -14
  39. package/dist/lib/storage-tier.js +4 -4
  40. package/dist/schemas/index.js +42 -45
  41. package/dist/schemas/site-config.d.ts +1 -277
  42. package/dist/schemas/site-config.d.ts.map +1 -1
  43. package/package.json +2 -42
  44. package/src/components/chat/ChatComposer.tsx +0 -4
  45. package/src/components/chat/ChatPanel.tsx +2 -3
  46. package/src/components/chat/markdown.ts +2 -3
  47. package/src/components/editor/PagesModal.tsx +1 -15
  48. package/src/components/editor/StatusIndicator.tsx +4 -4
  49. package/src/components/shell/AudienceChips.tsx +1 -3
  50. package/src/components/shell/EditorShell.tsx +5 -113
  51. package/src/components/shell/SiteSettingsModal.tsx +0 -11
  52. package/src/lib/pages.ts +0 -22
  53. package/src/schemas/index.ts +0 -4
  54. package/src/schemas/site-config.ts +4 -32
  55. package/dist/chunk-4TZJZU2T.js +0 -92
  56. package/dist/chunk-DIMDRBFK.js +0 -11
  57. package/dist/chunk-FV7JQRCN.js +0 -94
  58. package/dist/chunk-IIDKW7AV.js +0 -41
  59. package/dist/chunk-JCDZAFX2.js +0 -224
  60. package/dist/chunk-MM7KZS5C.js +0 -30
  61. package/dist/chunk-QQ3S3P5P.js +0 -38
  62. package/dist/chunk-TEOAKGZA.js +0 -784
  63. package/dist/chunk-VAYZJKVR.js +0 -209
  64. package/dist/components/shell/DocInspector.d.ts +0 -18
  65. package/dist/components/shell/DocInspector.d.ts.map +0 -1
  66. package/dist/components/shell/DocumentEditorOverlay.d.ts +0 -29
  67. package/dist/components/shell/DocumentEditorOverlay.d.ts.map +0 -1
  68. package/dist/components/shell/DocumentPresentOverlay.d.ts +0 -30
  69. package/dist/components/shell/DocumentPresentOverlay.d.ts.map +0 -1
  70. package/dist/components/shell/SiteSettingsDecks.d.ts +0 -13
  71. package/dist/components/shell/SiteSettingsDecks.d.ts.map +0 -1
  72. package/dist/deck/DeckComponentView.d.ts +0 -13
  73. package/dist/deck/DeckComponentView.d.ts.map +0 -1
  74. package/dist/deck/DeckPlayer.d.ts +0 -26
  75. package/dist/deck/DeckPlayer.d.ts.map +0 -1
  76. package/dist/deck/DeckSlideView.d.ts +0 -17
  77. package/dist/deck/DeckSlideView.d.ts.map +0 -1
  78. package/dist/deck/DeckStage.d.ts +0 -16
  79. package/dist/deck/DeckStage.d.ts.map +0 -1
  80. package/dist/deck/components/DeckImage.d.ts +0 -14
  81. package/dist/deck/components/DeckImage.d.ts.map +0 -1
  82. package/dist/deck/components/DeckImageGrid.d.ts +0 -27
  83. package/dist/deck/components/DeckImageGrid.d.ts.map +0 -1
  84. package/dist/deck/components/DeckRichText.d.ts +0 -22
  85. package/dist/deck/components/DeckRichText.d.ts.map +0 -1
  86. package/dist/deck/components/DeckVideo.d.ts +0 -15
  87. package/dist/deck/components/DeckVideo.d.ts.map +0 -1
  88. package/dist/deck/editing.d.ts +0 -50
  89. package/dist/deck/editing.d.ts.map +0 -1
  90. package/dist/deck/flip.d.ts +0 -11
  91. package/dist/deck/flip.d.ts.map +0 -1
  92. package/dist/deck/index.d.ts +0 -24
  93. package/dist/deck/index.d.ts.map +0 -1
  94. package/dist/deck/index.js +0 -51
  95. package/dist/deck/layouts/CenteredTextLayout.d.ts +0 -12
  96. package/dist/deck/layouts/CenteredTextLayout.d.ts.map +0 -1
  97. package/dist/deck/layouts/ColumnSplitLayout.d.ts +0 -14
  98. package/dist/deck/layouts/ColumnSplitLayout.d.ts.map +0 -1
  99. package/dist/deck/layouts/FullBleedImageLayout.d.ts +0 -14
  100. package/dist/deck/layouts/FullBleedImageLayout.d.ts.map +0 -1
  101. package/dist/deck/media.d.ts +0 -33
  102. package/dist/deck/media.d.ts.map +0 -1
  103. package/dist/deck/tokens.d.ts +0 -22
  104. package/dist/deck/tokens.d.ts.map +0 -1
  105. package/dist/deck/useStageScale.d.ts +0 -10
  106. package/dist/deck/useStageScale.d.ts.map +0 -1
  107. package/dist/deck-builder/DeckBuilderApp.d.ts +0 -30
  108. package/dist/deck-builder/DeckBuilderApp.d.ts.map +0 -1
  109. package/dist/deck-builder/DeckBuilderTopBar.d.ts +0 -24
  110. package/dist/deck-builder/DeckBuilderTopBar.d.ts.map +0 -1
  111. package/dist/deck-builder/DeckChatPanel.d.ts +0 -45
  112. package/dist/deck-builder/DeckChatPanel.d.ts.map +0 -1
  113. package/dist/deck-builder/DeckEditor.d.ts +0 -37
  114. package/dist/deck-builder/DeckEditor.d.ts.map +0 -1
  115. package/dist/deck-builder/DeckLibrary.d.ts +0 -22
  116. package/dist/deck-builder/DeckLibrary.d.ts.map +0 -1
  117. package/dist/deck-builder/DeckSlideSurface.d.ts +0 -31
  118. package/dist/deck-builder/DeckSlideSurface.d.ts.map +0 -1
  119. package/dist/deck-builder/deck-api.d.ts +0 -76
  120. package/dist/deck-builder/deck-api.d.ts.map +0 -1
  121. package/dist/deck-builder/deck-chat-client.d.ts +0 -44
  122. package/dist/deck-builder/deck-chat-client.d.ts.map +0 -1
  123. package/dist/deck-builder/deck-chat-history.d.ts +0 -10
  124. package/dist/deck-builder/deck-chat-history.d.ts.map +0 -1
  125. package/dist/deck-builder/index.d.ts +0 -13
  126. package/dist/deck-builder/index.d.ts.map +0 -1
  127. package/dist/deck-builder/index.js +0 -1498
  128. package/dist/deck-builder/starter-deck.d.ts +0 -23
  129. package/dist/deck-builder/starter-deck.d.ts.map +0 -1
  130. package/dist/deck-builder/types.d.ts +0 -29
  131. package/dist/deck-builder/types.d.ts.map +0 -1
  132. package/dist/deck-builder/useSortableSlide.d.ts +0 -29
  133. package/dist/deck-builder/useSortableSlide.d.ts.map +0 -1
  134. package/dist/document/assets/fonts/albra-grotesk-bold.woff +0 -0
  135. package/dist/document/assets/fonts/albra-grotesk-regular.woff +0 -0
  136. package/dist/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
  137. package/dist/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
  138. package/dist/document/components/DocCoverPage.d.ts +0 -11
  139. package/dist/document/components/DocCoverPage.d.ts.map +0 -1
  140. package/dist/document/components/DocPage.d.ts +0 -10
  141. package/dist/document/components/DocPage.d.ts.map +0 -1
  142. package/dist/document/components/DocRail.d.ts +0 -28
  143. package/dist/document/components/DocRail.d.ts.map +0 -1
  144. package/dist/document/components/DocSectionPage.d.ts +0 -12
  145. package/dist/document/components/DocSectionPage.d.ts.map +0 -1
  146. package/dist/document/components/DocViewer.d.ts +0 -21
  147. package/dist/document/components/DocViewer.d.ts.map +0 -1
  148. package/dist/document/components/Wordmark.d.ts +0 -4
  149. package/dist/document/components/Wordmark.d.ts.map +0 -1
  150. package/dist/document/components/measure.d.ts +0 -7
  151. package/dist/document/components/measure.d.ts.map +0 -1
  152. package/dist/document/components/useDocPages.d.ts +0 -12
  153. package/dist/document/components/useDocPages.d.ts.map +0 -1
  154. package/dist/document/engine/blocks.d.ts +0 -2
  155. package/dist/document/engine/blocks.d.ts.map +0 -1
  156. package/dist/document/engine/harvest.d.ts +0 -3
  157. package/dist/document/engine/harvest.d.ts.map +0 -1
  158. package/dist/document/engine/masonry.d.ts +0 -26
  159. package/dist/document/engine/masonry.d.ts.map +0 -1
  160. package/dist/document/engine/paginate.d.ts +0 -43
  161. package/dist/document/engine/paginate.d.ts.map +0 -1
  162. package/dist/document/engine/settings.d.ts +0 -91
  163. package/dist/document/engine/settings.d.ts.map +0 -1
  164. package/dist/document/engine/types.d.ts +0 -44
  165. package/dist/document/engine/types.d.ts.map +0 -1
  166. package/dist/document/index.d.ts +0 -13
  167. package/dist/document/index.d.ts.map +0 -1
  168. package/dist/document/index.js +0 -1030
  169. package/dist/document/styles/document.css +0 -217
  170. package/dist/lib/deck-access.d.ts +0 -86
  171. package/dist/lib/deck-access.d.ts.map +0 -1
  172. package/dist/lib/deck-access.js +0 -58
  173. package/dist/lib/deck-dexie.d.ts +0 -88
  174. package/dist/lib/deck-dexie.d.ts.map +0 -1
  175. package/dist/lib/deck-dexie.js +0 -22
  176. package/dist/lib/deck-edit.d.ts +0 -67
  177. package/dist/lib/deck-edit.d.ts.map +0 -1
  178. package/dist/lib/deck-media-assets.d.ts +0 -27
  179. package/dist/lib/deck-media-assets.d.ts.map +0 -1
  180. package/dist/lib/deck-media-assets.js +0 -25
  181. package/dist/lib/deck-media-refs.d.ts +0 -35
  182. package/dist/lib/deck-media-refs.d.ts.map +0 -1
  183. package/dist/lib/deck-media-refs.js +0 -36
  184. package/dist/lib/deck-tool.d.ts +0 -25
  185. package/dist/lib/deck-tool.d.ts.map +0 -1
  186. package/dist/lib/deck-tool.js +0 -58
  187. package/dist/lib/document-input.d.ts +0 -25
  188. package/dist/lib/document-input.d.ts.map +0 -1
  189. package/dist/schemas/deck-meta.d.ts +0 -85
  190. package/dist/schemas/deck-meta.d.ts.map +0 -1
  191. package/dist/schemas/deck-meta.js +0 -64
  192. package/dist/schemas/deck.d.ts +0 -954
  193. package/dist/schemas/deck.d.ts.map +0 -1
  194. package/dist/schemas/deck.js +0 -65
  195. package/src/components/shell/DocInspector.tsx +0 -314
  196. package/src/components/shell/DocumentEditorOverlay.tsx +0 -179
  197. package/src/components/shell/DocumentPresentOverlay.tsx +0 -87
  198. package/src/components/shell/SiteSettingsDecks.tsx +0 -41
  199. package/src/deck/DeckComponentView.tsx +0 -28
  200. package/src/deck/DeckPlayer.tsx +0 -166
  201. package/src/deck/DeckSlideView.tsx +0 -38
  202. package/src/deck/DeckStage.tsx +0 -39
  203. package/src/deck/components/DeckImage.tsx +0 -38
  204. package/src/deck/components/DeckImageGrid.tsx +0 -254
  205. package/src/deck/components/DeckRichText.tsx +0 -111
  206. package/src/deck/components/DeckVideo.tsx +0 -81
  207. package/src/deck/editing.tsx +0 -201
  208. package/src/deck/flip.ts +0 -70
  209. package/src/deck/index.ts +0 -30
  210. package/src/deck/layouts/CenteredTextLayout.tsx +0 -29
  211. package/src/deck/layouts/ColumnSplitLayout.tsx +0 -69
  212. package/src/deck/layouts/FullBleedImageLayout.tsx +0 -58
  213. package/src/deck/media.tsx +0 -46
  214. package/src/deck/tokens.ts +0 -57
  215. package/src/deck/useStageScale.ts +0 -44
  216. package/src/deck-builder/DeckBuilderApp.tsx +0 -572
  217. package/src/deck-builder/DeckBuilderTopBar.tsx +0 -62
  218. package/src/deck-builder/DeckChatPanel.tsx +0 -334
  219. package/src/deck-builder/DeckEditor.tsx +0 -258
  220. package/src/deck-builder/DeckLibrary.tsx +0 -195
  221. package/src/deck-builder/DeckSlideSurface.tsx +0 -55
  222. package/src/deck-builder/deck-api.ts +0 -177
  223. package/src/deck-builder/deck-chat-client.ts +0 -129
  224. package/src/deck-builder/deck-chat-history.ts +0 -62
  225. package/src/deck-builder/index.ts +0 -12
  226. package/src/deck-builder/starter-deck.ts +0 -86
  227. package/src/deck-builder/types.ts +0 -24
  228. package/src/deck-builder/useSortableSlide.ts +0 -102
  229. package/src/document/assets/fonts/albra-grotesk-bold.woff +0 -0
  230. package/src/document/assets/fonts/albra-grotesk-regular.woff +0 -0
  231. package/src/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
  232. package/src/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
  233. package/src/document/components/DocCoverPage.tsx +0 -45
  234. package/src/document/components/DocPage.tsx +0 -40
  235. package/src/document/components/DocRail.tsx +0 -189
  236. package/src/document/components/DocSectionPage.tsx +0 -48
  237. package/src/document/components/DocViewer.tsx +0 -313
  238. package/src/document/components/Wordmark.tsx +0 -18
  239. package/src/document/components/measure.tsx +0 -133
  240. package/src/document/components/useDocPages.ts +0 -95
  241. package/src/document/engine/blocks.ts +0 -116
  242. package/src/document/engine/harvest.ts +0 -95
  243. package/src/document/engine/masonry.ts +0 -158
  244. package/src/document/engine/paginate.ts +0 -232
  245. package/src/document/engine/settings.ts +0 -125
  246. package/src/document/engine/types.ts +0 -64
  247. package/src/document/index.ts +0 -18
  248. package/src/document/styles/document.css +0 -217
  249. package/src/lib/deck-access.ts +0 -132
  250. package/src/lib/deck-dexie.ts +0 -190
  251. package/src/lib/deck-edit.ts +0 -196
  252. package/src/lib/deck-media-assets.ts +0 -57
  253. package/src/lib/deck-media-refs.ts +0 -67
  254. package/src/lib/deck-tool.ts +0 -95
  255. package/src/lib/document-input.ts +0 -57
  256. package/src/schemas/deck-meta.ts +0 -88
  257. package/src/schemas/deck.ts +0 -478
  258. package/dist/{chunk-DJABADOO.js → chunk-AJ2QOAZI.js} +3 -3
@@ -1,41 +0,0 @@
1
- import { AudienceChips } from "./AudienceChips";
2
- import type { SiteConfig } from "../../schemas/site-config";
3
- import type { Audience } from "../../auth/types";
4
-
5
- interface Props {
6
- siteConfig: SiteConfig;
7
- onSiteConfigChange: (config: SiteConfig) => void;
8
- audiences: Audience[];
9
- }
10
-
11
- /** Deck Builder settings. One control today — the creation opt-in — because
12
- * that is the only `deckBuilder` key that exists; public sharing (#37) is the
13
- * next one and belongs on this tab. */
14
- export function SiteSettingsDecks({ siteConfig, onSiteConfigChange, audiences }: Props) {
15
- const selected = siteConfig.deckBuilder.audiences;
16
-
17
- function toggle(name: string) {
18
- const next = selected.includes(name) ? selected.filter((n) => n !== name) : [...selected, name];
19
- onSiteConfigChange({ ...siteConfig, deckBuilder: { ...siteConfig.deckBuilder, audiences: next } });
20
- }
21
-
22
- return (
23
- <div className="space-y-3">
24
- <div>
25
- <h3 className="mb-1 text-sm font-medium text-base-contrast">Who can build decks</h3>
26
- <p className="mb-2 text-xs text-base-contrast-light">
27
- Members of the selected audiences get the Decks button and can create and publish their own presentations.
28
- None selected means no viewer can start a new one. Editors always can.
29
- </p>
30
- <AudienceChips audiences={audiences} selected={selected} onToggle={toggle} />
31
- </div>
32
- <p className="text-xs text-base-contrast-light">
33
- Removing an audience here stops its members starting new decks. Decks they already own stay theirs — they can
34
- still open, edit and delete them — so remove the deck itself if that is what you want gone.
35
- </p>
36
- <p className="text-xs text-base-contrast-light">
37
- A deck is private to the person who made it. Sharing it with an audience, or by public link, comes later.
38
- </p>
39
- </div>
40
- );
41
- }
@@ -1,28 +0,0 @@
1
- import type { DeckTextSlot } from "../lib/deck-edit";
2
- import type { DeckComponent } from "../schemas/deck";
3
- import { DeckImage } from "./components/DeckImage";
4
- import { DeckImageGrid } from "./components/DeckImageGrid";
5
- import { DeckRichText } from "./components/DeckRichText";
6
- import { DeckVideo } from "./components/DeckVideo";
7
-
8
- interface Props {
9
- component: DeckComponent;
10
- /** The Layout slot this component fills — carried only so text leaves can
11
- * address themselves for inline editing (#39). Optional: a caller that omits
12
- * it gets the read-only render. */
13
- slot?: DeckTextSlot;
14
- }
15
-
16
- /** Dispatches a Deck Component onto its runtime implementation. */
17
- export function DeckComponentView({ component, slot }: Props) {
18
- switch (component.type) {
19
- case "richText":
20
- return <DeckRichText content={component} slot={slot} />;
21
- case "image":
22
- return <DeckImage content={component} />;
23
- case "imageGrid":
24
- return <DeckImageGrid content={component} />;
25
- case "video":
26
- return <DeckVideo content={component} />;
27
- }
28
- }
@@ -1,166 +0,0 @@
1
- import { ChevronLeft, ChevronRight } from "lucide-react";
2
- import { useCallback, useEffect, useMemo, useState } from "react";
3
- import { cn } from "../lib/cn";
4
- import type { Deck, DeckMediaAsset, DeckMediaResolver } from "../schemas/deck";
5
- import { DeckMediaProvider } from "./media";
6
- import { DeckStage } from "./DeckStage";
7
- import { DeckSlideView } from "./DeckSlideView";
8
-
9
- interface Props {
10
- deck: Deck;
11
- /** Slide to open on. Out-of-range values clamp into the deck. */
12
- initialSlide?: number;
13
- /** Window-level arrow/space/Home/End navigation. Off for thumbnails. */
14
- keyboard?: boolean;
15
- /** Turns media references into bytes. The Deck Builder injects the real one. */
16
- resolveMedia?: DeckMediaResolver;
17
- /**
18
- * Pre-resolved portal media, keyed by media id. The serializable form of the
19
- * same seam, for callers that cross an Astro island boundary — a function
20
- * prop cannot survive that trip. Ignored when `resolveMedia` is given.
21
- */
22
- mediaAssets?: Record<string, DeckMediaAsset>;
23
- className?: string;
24
- }
25
-
26
- function clamp(index: number, length: number): number {
27
- return Math.min(Math.max(index, 0), length - 1);
28
- }
29
-
30
- /** Arrow keys must not steal typing from a chat panel docked beside the stage. */
31
- function isTypingTarget(target: EventTarget | null): boolean {
32
- if (!(target instanceof HTMLElement)) return false;
33
- return target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName);
34
- }
35
-
36
- /**
37
- * A native button activates on Space *keyup*; the keydown only marks it active
38
- * and suppresses scrolling. Calling preventDefault on that keydown cancels the
39
- * activation — so Space on a focused "Previous slide" used to swallow the click
40
- * and step FORWARD instead. Enter needs no such guard: it has no case below, so
41
- * it never reaches preventDefault.
42
- */
43
- function isActivatableTarget(target: EventTarget | null): boolean {
44
- if (!(target instanceof HTMLElement)) return false;
45
- if (target.tagName === "BUTTON" || target.tagName === "SUMMARY") return true;
46
- if (target.tagName === "A" && target.hasAttribute("href")) return true;
47
- return target.getAttribute("role") === "button";
48
- }
49
-
50
- const NAV_BUTTON =
51
- "flex size-[38px] items-center justify-center rounded-[var(--radius-outer)] border border-base-200 text-base-contrast transition-colors hover:bg-base-accent disabled:cursor-not-allowed disabled:opacity-35 disabled:hover:bg-transparent";
52
-
53
- /**
54
- * The deck runtime's presentation surface: one 16:9 stage plus slide
55
- * navigation. Deliberately self-contained — the Deck Builder, the in-portal
56
- * deck view and the Public Link view all mount this same component, so slide
57
- * state lives here rather than in any one of those shells.
58
- */
59
- export function DeckPlayer({ deck, initialSlide = 0, keyboard = true, resolveMedia, mediaAssets, className }: Props) {
60
- const total = deck.slides.length;
61
- const [index, setIndex] = useState(() => clamp(initialSlide, total));
62
- // `Object.hasOwn`, not indexing: a media id is author-supplied, and a plain
63
- // object answers `"constructor"` and `"toString"` with inherited members that
64
- // are not assets at all. The schema's charset rules those ids out too — this
65
- // is the second lock on the same door. The `kind` check keeps the map form
66
- // honest the day a second media kind lands: it resolves portal ids, and says
67
- // so by refusing everything else rather than looking the id up anyway.
68
- const resolve = useMemo<DeckMediaResolver>(
69
- () =>
70
- resolveMedia ??
71
- ((ref) =>
72
- ref.kind === "portal" && mediaAssets !== undefined && Object.hasOwn(mediaAssets, ref.id)
73
- ? mediaAssets[ref.id]
74
- : null),
75
- [resolveMedia, mediaAssets],
76
- );
77
-
78
- const current = clamp(index, total);
79
- const slide = deck.slides[current];
80
-
81
- const goTo = useCallback((next: number) => setIndex(clamp(next, total)), [total]);
82
- // `prev` is clamped BEFORE the delta: the builder live-replaces the deck every
83
- // LLM turn, so a shrunk deck can leave the stored index past the end. Without
84
- // the inner clamp the first Previous press would land back on the slide
85
- // already showing — a visible no-op.
86
- const step = useCallback(
87
- (delta: number) => setIndex((prev) => clamp(clamp(prev, total) + delta, total)),
88
- [total],
89
- );
90
-
91
- useEffect(() => {
92
- if (!keyboard) return;
93
- const onKeyDown = (event: KeyboardEvent) => {
94
- if (event.metaKey || event.ctrlKey || event.altKey) return;
95
- if (isTypingTarget(event.target)) return;
96
- switch (event.key) {
97
- case " ":
98
- if (isActivatableTarget(event.target)) return;
99
- event.preventDefault();
100
- step(1);
101
- break;
102
- case "ArrowRight":
103
- case "PageDown":
104
- event.preventDefault();
105
- step(1);
106
- break;
107
- case "ArrowLeft":
108
- case "PageUp":
109
- event.preventDefault();
110
- step(-1);
111
- break;
112
- case "Home":
113
- event.preventDefault();
114
- setIndex(0);
115
- break;
116
- case "End":
117
- event.preventDefault();
118
- setIndex(total - 1);
119
- break;
120
- }
121
- };
122
- window.addEventListener("keydown", onKeyDown);
123
- return () => window.removeEventListener("keydown", onKeyDown);
124
- }, [keyboard, step, total]);
125
-
126
- return (
127
- <div data-deck-player={deck.id} className={cn("flex h-full min-h-0 w-full flex-col gap-4 bg-base p-4", className)}>
128
- <DeckStage className="flex-1">
129
- <DeckMediaProvider resolve={resolve}>
130
- <DeckSlideView slide={slide} />
131
- </DeckMediaProvider>
132
- </DeckStage>
133
-
134
- <nav aria-label={`${deck.title} slides`} className="flex shrink-0 items-center justify-center gap-4">
135
- <button type="button" className={NAV_BUTTON} onClick={() => step(-1)} disabled={current === 0} aria-label="Previous slide">
136
- <ChevronLeft className="size-5" aria-hidden="true" />
137
- </button>
138
-
139
- <ol className="flex items-center gap-2">
140
- {deck.slides.map((s, i) => (
141
- <li key={s.id}>
142
- <button
143
- type="button"
144
- onClick={() => goTo(i)}
145
- aria-current={i === current ? "true" : undefined}
146
- aria-label={s.title ? `Slide ${i + 1}: ${s.title}` : `Slide ${i + 1}`}
147
- className={cn(
148
- "size-[10px] rounded-full transition-colors",
149
- i === current ? "bg-primary" : "bg-base-300 hover:bg-base-contrast-light",
150
- )}
151
- />
152
- </li>
153
- ))}
154
- </ol>
155
-
156
- <button type="button" className={NAV_BUTTON} onClick={() => step(1)} disabled={current === total - 1} aria-label="Next slide">
157
- <ChevronRight className="size-5" aria-hidden="true" />
158
- </button>
159
-
160
- <p aria-live="polite" className="ml-2 text-sm tabular-nums text-base-contrast-light">
161
- {current + 1} / {total}
162
- </p>
163
- </nav>
164
- </div>
165
- );
166
- }
@@ -1,38 +0,0 @@
1
- import type { DeckSlide } from "../schemas/deck";
2
- import { CenteredTextLayout } from "./layouts/CenteredTextLayout";
3
- import { ColumnSplitLayout } from "./layouts/ColumnSplitLayout";
4
- import { FullBleedImageLayout } from "./layouts/FullBleedImageLayout";
5
-
6
- interface Props {
7
- slide: DeckSlide;
8
- }
9
-
10
- function LayoutView({ slide }: Props) {
11
- const { layout } = slide;
12
- switch (layout.type) {
13
- case "centeredText":
14
- return <CenteredTextLayout layout={layout} />;
15
- case "fullBleedImage":
16
- return <FullBleedImageLayout layout={layout} />;
17
- case "columnSplit":
18
- return <ColumnSplitLayout layout={layout} />;
19
- }
20
- }
21
-
22
- /**
23
- * Dispatches a slide onto its Layout. One slide uses exactly one Layout.
24
- *
25
- * Keyed by slide id, and that key is load-bearing rather than a lint nicety:
26
- * two slides sharing a Layout shape reconcile onto the SAME component
27
- * instances, so an expanded image-grid tile or a playing video on slide 1 was
28
- * inherited by slide 2 the moment you pressed the arrow key. The key forces the
29
- * subtree to remount, which is the only thing that makes a slide's interaction
30
- * state belong to that slide.
31
- */
32
- export function DeckSlideView({ slide }: Props) {
33
- return (
34
- <div key={slide.id} data-deck-slide={slide.id} className="size-full">
35
- <LayoutView slide={slide} />
36
- </div>
37
- );
38
- }
@@ -1,39 +0,0 @@
1
- import { useRef, type ReactNode } from "react";
2
- import { cn } from "../lib/cn";
3
- import { DECK_STAGE_HEIGHT, DECK_STAGE_WIDTH } from "../schemas/deck";
4
- import { useStageScale } from "./useStageScale";
5
-
6
- interface Props {
7
- children: ReactNode;
8
- /** Applied to the fit box, which is what the stage is scaled to fill. */
9
- className?: string;
10
- }
11
-
12
- /**
13
- * The 16:9 stage: a fixed logical canvas centered in — and scaled to fit — its
14
- * container, so slide contents are authored in px against one coordinate system
15
- * regardless of the viewport. Scaling by transform (rather than by a font-size
16
- * cascade) keeps every measurement, including line breaks, identical at every
17
- * size, which is what makes a screenshared deck look the same as its preview.
18
- */
19
- export function DeckStage({ children, className }: Props) {
20
- const fitRef = useRef<HTMLDivElement>(null);
21
- const scale = useStageScale(fitRef);
22
-
23
- return (
24
- <div ref={fitRef} className={cn("relative min-h-0 w-full overflow-hidden", className)} data-deck-fit="">
25
- <div
26
- data-deck-stage=""
27
- data-deck-scale={scale === null ? undefined : scale.toFixed(4)}
28
- className="absolute top-1/2 left-1/2 origin-center overflow-hidden rounded-[var(--radius-outer)] font-body"
29
- style={{
30
- width: DECK_STAGE_WIDTH,
31
- height: DECK_STAGE_HEIGHT,
32
- transform: scale === null ? "translate(-50%, -50%)" : `translate(-50%, -50%) scale(${scale})`,
33
- }}
34
- >
35
- {children}
36
- </div>
37
- </div>
38
- );
39
- }
@@ -1,38 +0,0 @@
1
- import type { DeckImage as DeckImageContent } from "../../schemas/deck";
2
- import { DeckMissingMedia, useDeckMedia } from "../media";
3
- import { surfaceInk } from "../tokens";
4
-
5
- interface Props {
6
- content: DeckImageContent;
7
- }
8
-
9
- /**
10
- * Deck Component — image.
11
- *
12
- * Fills its slot and crops. The caption is a solid brand block sitting on the
13
- * image's bottom edge rather than a gradient scrim: a scrim needs a literal
14
- * color to darken toward, and legibility here comes from the block.
15
- */
16
- export function DeckImage({ content }: Props) {
17
- const asset = useDeckMedia(content.media);
18
-
19
- return (
20
- <figure data-deck-component="image" className="relative size-full overflow-hidden bg-base-200">
21
- {asset === null ? (
22
- <DeckMissingMedia />
23
- ) : (
24
- <img src={asset.src} alt={asset.alt} className="size-full object-cover" />
25
- )}
26
-
27
- {content.caption !== undefined && (
28
- <figcaption
29
- data-deck-surface="brand"
30
- style={surfaceInk("brand")}
31
- className="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)]"
32
- >
33
- {content.caption}
34
- </figcaption>
35
- )}
36
- </figure>
37
- );
38
- }
@@ -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
- }