@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,195 +0,0 @@
1
- import { LayoutGridIcon, MoreVerticalIcon, PlusIcon } from "lucide-react";
2
- import { useRef, useState } from "react";
3
- import { Button } from "../components/shared/Button";
4
- import { IconButton } from "../components/shared/IconButton";
5
- import { Popover } from "../components/shared/Popover";
6
- import { PopoverItem } from "../components/shared/PopoverItem";
7
- import { cn } from "../lib/cn";
8
- import { formatTimestamp } from "../lib/timestamp";
9
- import type { DeckMediaAsset } from "../schemas/deck";
10
- import { DeckSlideSurface } from "./DeckSlideSurface";
11
- import type { DeckGridEntry } from "./types";
12
-
13
- /**
14
- * Builder mode's landing view (prototype variant A): a responsive grid of the
15
- * caller's own decks, each a 16:9 thumbnail with a status chip and a kebab, and
16
- * a designed empty state in place of the grid when there are none.
17
- *
18
- * Only "Private" exists as a status today — audience grants (#36) and Public
19
- * Links (#37) are what make the chip worth its space, and inventing their
20
- * labels now would be inventing their semantics.
21
- */
22
- interface Props {
23
- entries: DeckGridEntry[];
24
- mediaAssets: Record<string, DeckMediaAsset>;
25
- canCreate: boolean;
26
- onOpen: (id: string) => void;
27
- onCreate: () => void;
28
- onDelete: (id: string) => void;
29
- }
30
-
31
- export function DeckLibrary({ entries, mediaAssets, canCreate, onOpen, onCreate, onDelete }: Props) {
32
- return (
33
- <div className="mx-auto w-full max-w-screen-xl px-4 pt-8 pb-16">
34
- {entries.length === 0 ? (
35
- <DeckLibraryEmpty canCreate={canCreate} onCreate={onCreate} />
36
- ) : (
37
- <>
38
- <div className="mb-5 flex flex-wrap items-center gap-3">
39
- <h1 className="font-body text-lg font-semibold text-base-contrast">Your decks</h1>
40
- </div>
41
- <ul className="grid grid-cols-[repeat(auto-fill,minmax(16rem,1fr))] gap-x-4 gap-y-5">
42
- {entries.map((entry) => (
43
- <DeckCard
44
- key={entry.id}
45
- entry={entry}
46
- mediaAssets={mediaAssets}
47
- onOpen={() => onOpen(entry.id)}
48
- onDelete={() => onDelete(entry.id)}
49
- />
50
- ))}
51
- </ul>
52
- </>
53
- )}
54
- </div>
55
- );
56
- }
57
-
58
- function DeckLibraryEmpty({ canCreate, onCreate }: { canCreate: boolean; onCreate: () => void }) {
59
- return (
60
- <div className="grid min-h-[55vh] place-items-center px-4 py-12 text-center">
61
- <div className="grid max-w-md justify-items-center gap-4">
62
- <div className="grid aspect-video w-44 place-items-center rounded border border-dashed border-base-300 bg-base-accent">
63
- <LayoutGridIcon size={16} className="text-base-300" aria-hidden="true" />
64
- </div>
65
- <h1 className="font-body text-lg font-semibold text-base-contrast">No decks yet</h1>
66
- <p className="text-sm leading-relaxed text-base-contrast-light">
67
- Decks are 16:9 presentations built from your brand's layouts. Start one and it stays on this device until you
68
- publish it.
69
- </p>
70
- {canCreate && (
71
- <Button size="md" onClick={onCreate}>
72
- <PlusIcon size={16} className="mr-1.5" aria-hidden="true" />
73
- New deck
74
- </Button>
75
- )}
76
- </div>
77
- </div>
78
- );
79
- }
80
-
81
- function DeckCard({
82
- entry,
83
- mediaAssets,
84
- onOpen,
85
- onDelete,
86
- }: {
87
- entry: DeckGridEntry;
88
- mediaAssets: Record<string, DeckMediaAsset>;
89
- onOpen: () => void;
90
- onDelete: () => void;
91
- }) {
92
- const [menuOpen, setMenuOpen] = useState(false);
93
- const [confirming, setConfirming] = useState(false);
94
- const kebabRef = useRef<HTMLButtonElement>(null);
95
- const firstSlide = entry.draft?.deck.slides[0] ?? null;
96
-
97
- return (
98
- <li className="group relative">
99
- {/* aria-hidden and unfocusable ON PURPOSE: a rendered slide can contain
100
- real <button>s (the image grid's tiles are buttons), so the thumbnail
101
- cannot BE a button, and duplicating the title control in the
102
- accessibility tree would put two "Open …" targets on every card. The
103
- title below is the announced control; this is its click target. */}
104
- <div
105
- aria-hidden="true"
106
- onClick={onOpen}
107
- className="w-full cursor-pointer overflow-hidden rounded border border-base-200 bg-base-accent transition-colors group-hover:border-primary"
108
- >
109
- {/* The real first slide when this browser has the deck staged; a plain
110
- plate otherwise. Fetching every published deck's body just to draw a
111
- grid of thumbnails would be a read per card — the listing route
112
- deliberately reads only the meta sidecars. */}
113
- <div className="pointer-events-none aspect-video w-full">
114
- {firstSlide ? (
115
- <DeckSlideSurface slide={firstSlide} mediaAssets={mediaAssets} className="h-full" />
116
- ) : (
117
- <div className="grid h-full place-items-center">
118
- <LayoutGridIcon size={16} className="text-base-300" aria-hidden="true" />
119
- </div>
120
- )}
121
- </div>
122
- </div>
123
-
124
- <div className="flex items-start gap-2 pt-2">
125
- <button type="button" onClick={onOpen} className="min-w-0 flex-1 cursor-pointer text-left">
126
- <span className="block truncate text-sm font-medium text-base-contrast">{entry.title}</span>
127
- <span className="mt-0.5 block text-xs text-base-contrast-light">
128
- {entry.slideCount} {entry.slideCount === 1 ? "slide" : "slides"} · {formatTimestamp(entry.updatedAt)}
129
- </span>
130
- <span className="mt-2 flex flex-wrap items-center gap-1.5">
131
- <DeckStatusChip label="Private" dotClassName="bg-[var(--color-status-draft-text)]" />
132
- {entry.unpublishedChanges && (
133
- <DeckStatusChip label="Unpublished changes" dotClassName="bg-[var(--color-status-modified-text)]" />
134
- )}
135
- </span>
136
- </button>
137
-
138
- <IconButton
139
- ref={kebabRef}
140
- icon={<MoreVerticalIcon size={16} />}
141
- label={`Actions for ${entry.title}`}
142
- onClick={() => setMenuOpen((v) => !v)}
143
- aria-haspopup="menu"
144
- aria-expanded={menuOpen}
145
- />
146
- <Popover
147
- isOpen={menuOpen}
148
- onClose={() => {
149
- setMenuOpen(false);
150
- setConfirming(false);
151
- }}
152
- anchorRef={kebabRef}
153
- align="end"
154
- className="min-w-44 py-1"
155
- >
156
- {confirming ? (
157
- <div className="px-3 py-2">
158
- <p className="text-xs text-base-contrast-light">
159
- Delete “{entry.title}”? {entry.published ? "The published deck is removed too." : "It has never been published."}
160
- </p>
161
- <div className="mt-2 flex justify-end gap-2">
162
- <Button variant="ghost" onClick={() => setConfirming(false)}>
163
- Cancel
164
- </Button>
165
- <Button
166
- variant="destructive"
167
- onClick={() => {
168
- setMenuOpen(false);
169
- setConfirming(false);
170
- onDelete();
171
- }}
172
- >
173
- Delete
174
- </Button>
175
- </div>
176
- </div>
177
- ) : (
178
- <PopoverItem className="text-red-600" onClick={() => setConfirming(true)}>
179
- Delete
180
- </PopoverItem>
181
- )}
182
- </Popover>
183
- </div>
184
- </li>
185
- );
186
- }
187
-
188
- function DeckStatusChip({ label, dotClassName }: { label: string; dotClassName: string }) {
189
- return (
190
- <span className="inline-flex items-center gap-1.5 rounded-full border border-base-200 px-2 py-0.5 align-middle text-xs font-medium text-base-contrast">
191
- <span aria-hidden="true" className={cn("h-3 w-3 rounded-full border border-base", dotClassName)} />
192
- {label}
193
- </span>
194
- );
195
- }
@@ -1,55 +0,0 @@
1
- import { useMemo } from "react";
2
- import { DeckMediaProvider, DeckSlideView, DeckStage, DeckTextEditProvider, type DeckTextEditor } from "../deck";
3
- import type { DeckMediaAsset, DeckMediaResolver, DeckSlide } from "../schemas/deck";
4
-
5
- // Re-exported rather than imported from `../deck` by its consumers: this file
6
- // is the builder's ONLY window onto the runtime (see the whitelist in
7
- // tests/deck/viewer-isolation.test.ts), and that stays true for types too.
8
- export type { DeckTextEditor };
9
-
10
- /**
11
- * The builder's one window onto the deck runtime — every other module in this
12
- * directory renders decks THROUGH this component and imports nothing from
13
- * `../deck` itself. That is deliberate: `tests/deck/viewer-isolation.test.ts`
14
- * whitelists the files that may reach the runtime, and keeping the list at one
15
- * entry is what keeps the whitelist reviewable.
16
- *
17
- * `DeckPlayer` is not used here because it owns its own slide index and slide
18
- * chrome, and in the builder the filmstrip owns both. This composes the same
19
- * three runtime pieces `DeckPlayer` does, which is why a thumbnail and the
20
- * stage render identically.
21
- */
22
- interface Props {
23
- slide: DeckSlide;
24
- /** Pre-resolved portal media, keyed by media id (see `deckMediaAssets`). */
25
- mediaAssets: Record<string, DeckMediaAsset>;
26
- /** Applied to the fit box the 16:9 stage is scaled into. */
27
- className?: string;
28
- /**
29
- * Inline text editing (#39). Omitted everywhere but the editing view's own
30
- * stage — filmstrip thumbnails, in-portal deck views and the Public Link
31
- * route all render read-only, which is the acceptance criterion "nothing in
32
- * this surface appears for read-only deck viewing" held at one place.
33
- */
34
- editor?: DeckTextEditor;
35
- }
36
-
37
- export function DeckSlideSurface({ slide, mediaAssets, className, editor }: Props) {
38
- // `Object.hasOwn`, not indexing: a media id is author-supplied and a plain
39
- // object answers "constructor" with something that is not an asset. Same
40
- // guard DeckPlayer's own map-form resolver uses, for the same reason.
41
- const resolve = useMemo<DeckMediaResolver>(
42
- () => (ref) => (ref.kind === "portal" && Object.hasOwn(mediaAssets, ref.id) ? mediaAssets[ref.id] : null),
43
- [mediaAssets],
44
- );
45
-
46
- return (
47
- <DeckStage className={className}>
48
- <DeckMediaProvider resolve={resolve}>
49
- <DeckTextEditProvider editor={editor ?? null}>
50
- <DeckSlideView slide={slide} />
51
- </DeckTextEditProvider>
52
- </DeckMediaProvider>
53
- </DeckStage>
54
- );
55
- }
@@ -1,177 +0,0 @@
1
- import { parseDeck, type Deck } from "../schemas/deck";
2
-
3
- /**
4
- * The builder's client for `/api/decks` (ADR 0002's routes).
5
- *
6
- * Every function returns a discriminated result instead of throwing, because
7
- * every failure mode here is one the UI has to SAY something specific about —
8
- * a 409 is not an error to log, it is a decision to put in front of the author.
9
- */
10
-
11
- /**
12
- * Just the sidecar fields the grid renders, narrowed from the response rather
13
- * than cast to `DeckMeta`.
14
- *
15
- * Deliberately NOT the whole sidecar and NOT `parseDeckMeta`: the meta schema
16
- * pins `schemaVersion` to a literal, so validating the full shape here would
17
- * make a portal running an older package version drop every deck from the
18
- * listing — silently, and exactly for the user whose decks they are. Reading
19
- * only what is displayed keeps a schema bump a non-event on this side.
20
- */
21
- export interface PublishedDeckMeta {
22
- title: string;
23
- slideCount: number;
24
- updatedAt: string;
25
- }
26
-
27
- export interface PublishedDeckSummary {
28
- id: string;
29
- version: string;
30
- /** null when the route reported the sidecar unreadable, or a field is missing. */
31
- meta: PublishedDeckMeta | null;
32
- }
33
-
34
- function metaFrom(source: Record<string, unknown>): PublishedDeckMeta | null {
35
- const { title, slideCount, updatedAt } = source;
36
- if (typeof title !== "string" || title.length === 0) return null;
37
- if (typeof slideCount !== "number" || !Number.isFinite(slideCount)) return null;
38
- if (typeof updatedAt !== "string" || updatedAt.length === 0) return null;
39
- return { title, slideCount, updatedAt };
40
- }
41
-
42
- export type DeckApiFailure =
43
- /** 409 — the deck moved on the branch since this draft's baseVersion. */
44
- | { kind: "conflict"; currentVersion: string | null; message: string }
45
- /** 400 — the deck did not satisfy the schema. Per-path messages. */
46
- | { kind: "invalid"; details: string[] }
47
- /** 401/403 — not signed in, or not a Deck Creator. */
48
- | { kind: "forbidden"; message: string }
49
- /** 404 — no such deck, or not one this caller may see. */
50
- | { kind: "notFound" }
51
- /** 503 and transport failures — nothing was written and a retry is right. */
52
- | { kind: "unavailable"; message: string }
53
- | { kind: "failed"; message: string };
54
-
55
- export type DeckApiResult<T> = { ok: true; value: T } | ({ ok: false } & DeckApiFailure);
56
-
57
- async function body(res: Response): Promise<Record<string, unknown>> {
58
- try {
59
- return (await res.json()) as Record<string, unknown>;
60
- } catch {
61
- return {};
62
- }
63
- }
64
-
65
- function messageFrom(data: Record<string, unknown>, fallback: string): string {
66
- return typeof data.message === "string" ? data.message : fallback;
67
- }
68
-
69
- /** Maps a non-2xx response onto the failure the UI branches on. */
70
- async function failure(res: Response): Promise<{ ok: false } & DeckApiFailure> {
71
- const data = await body(res);
72
- switch (res.status) {
73
- case 400:
74
- return {
75
- ok: false,
76
- kind: "invalid",
77
- details: Array.isArray(data.details) ? (data.details as string[]) : [messageFrom(data, "The deck is not valid.")],
78
- };
79
- case 401:
80
- case 403:
81
- return { ok: false, kind: "forbidden", message: messageFrom(data, "You are not allowed to do that.") };
82
- case 404:
83
- return { ok: false, kind: "notFound" };
84
- case 409:
85
- return {
86
- ok: false,
87
- kind: "conflict",
88
- currentVersion: typeof data.currentVersion === "string" ? data.currentVersion : null,
89
- message: messageFrom(data, "This deck changed since you loaded it."),
90
- };
91
- case 503:
92
- return { ok: false, kind: "unavailable", message: "The deck store is busy. Try again in a moment." };
93
- default:
94
- return { ok: false, kind: "failed", message: messageFrom(data, `Request failed (${res.status}).`) };
95
- }
96
- }
97
-
98
- /** A network error is indistinguishable from a 503 to the author: retry. */
99
- function transportFailure(): { ok: false } & DeckApiFailure {
100
- return { ok: false, kind: "unavailable", message: "Could not reach the portal. Check your connection and try again." };
101
- }
102
-
103
- export async function listPublishedDecks(): Promise<DeckApiResult<PublishedDeckSummary[]>> {
104
- let res: Response;
105
- try {
106
- res = await fetch("/api/decks");
107
- } catch {
108
- return transportFailure();
109
- }
110
- if (!res.ok) return failure(res);
111
- const data = await body(res);
112
- const raw = Array.isArray(data.decks) ? (data.decks as Record<string, unknown>[]) : [];
113
- // The route answers a projection of the meta sidecar plus `version`; an entry
114
- // whose sidecar it could not parse carries `invalid: true` and no fields, so
115
- // read defensively rather than assuming the shape.
116
- return {
117
- ok: true,
118
- value: raw.flatMap((entry) => {
119
- const id = typeof entry.id === "string" ? entry.id : null;
120
- const version = typeof entry.version === "string" ? entry.version : null;
121
- if (!id || !version) return [];
122
- return [{ id, version, meta: entry.invalid === true ? null : metaFrom(entry) }];
123
- }),
124
- };
125
- }
126
-
127
- export async function readPublishedDeck(
128
- id: string,
129
- ): Promise<DeckApiResult<{ deck: Deck; version: string; meta: PublishedDeckMeta | null }>> {
130
- let res: Response;
131
- try {
132
- res = await fetch(`/api/decks/${encodeURIComponent(id)}`);
133
- } catch {
134
- return transportFailure();
135
- }
136
- if (!res.ok) return failure(res);
137
- const data = await body(res);
138
- const parsed = parseDeck(data.deck);
139
- if (!parsed.ok) return { ok: false, kind: "invalid", details: parsed.errors };
140
- if (typeof data.version !== "string") return { ok: false, kind: "failed", message: "The deck store returned no version." };
141
- const meta = data.meta !== null && typeof data.meta === "object" ? metaFrom(data.meta as Record<string, unknown>) : null;
142
- return { ok: true, value: { deck: parsed.deck, version: data.version, meta } };
143
- }
144
-
145
- export async function publishDeck(
146
- deck: Deck,
147
- baseVersion: string | null,
148
- ): Promise<DeckApiResult<{ version: string; meta: PublishedDeckMeta | null }>> {
149
- let res: Response;
150
- try {
151
- res = await fetch("/api/decks", {
152
- method: "POST",
153
- headers: { "Content-Type": "application/json" },
154
- body: JSON.stringify({ deck, baseVersion }),
155
- });
156
- } catch {
157
- return transportFailure();
158
- }
159
- if (!res.ok) return failure(res);
160
- const data = await body(res);
161
- if (typeof data.version !== "string") return { ok: false, kind: "failed", message: "The deck store returned no version." };
162
- const meta = data.meta !== null && typeof data.meta === "object" ? metaFrom(data.meta as Record<string, unknown>) : null;
163
- return { ok: true, value: { version: data.version, meta } };
164
- }
165
-
166
- export async function deletePublishedDeck(id: string): Promise<DeckApiResult<true>> {
167
- let res: Response;
168
- try {
169
- res = await fetch(`/api/decks/${encodeURIComponent(id)}`, { method: "DELETE" });
170
- } catch {
171
- return transportFailure();
172
- }
173
- // A deck that is not there is already deleted — the caller wanted it gone.
174
- if (res.status === 404) return { ok: true, value: true };
175
- if (!res.ok) return failure(res);
176
- return { ok: true, value: true };
177
- }
@@ -1,129 +0,0 @@
1
- import { parseDeck, type Deck } from "../schemas/deck";
2
-
3
- /**
4
- * The builder's client for `POST /api/deck-chat` (#35).
5
- *
6
- * The route's event contract, and what each event means to the UI:
7
- *
8
- * text { text } a delta of the assistant's prose
9
- * retry { attempt, issues } the model's deck failed validation and it is
10
- * being asked to fix it — DISCARD the prose
11
- * streamed so far, or the transcript shows two
12
- * answers for one turn
13
- * deck { deck } a deck that PARSED server-side; stage it
14
- * error { error, details? } the turn produced no usable deck
15
- * done { truncated } end of turn (always last)
16
- *
17
- * A turn with no `deck` event is not a failure: the assistant is allowed to
18
- * answer a question without rewriting the deck.
19
- */
20
-
21
- export interface DeckChatMessage {
22
- role: "user" | "assistant";
23
- content: string;
24
- }
25
-
26
- export interface DeckChatHandlers {
27
- onText: (delta: string) => void;
28
- /** Prose from the failed attempt must be dropped before the next one streams. */
29
- onRetry: (attempt: number, issues: string[]) => void;
30
- }
31
-
32
- export type DeckChatResult =
33
- | { ok: true; deck: Deck | null; truncated: boolean }
34
- | { ok: false; error: string; details?: string[]; retryAfterMs?: number };
35
-
36
- export const DECK_CHAT_ERROR_COPY: Record<string, string> = {
37
- rate_limited: "You've hit the deck-building limit — try again later.",
38
- disabled: "Deck building isn't available on this site right now.",
39
- not_authorized: "You don't have access to build decks here.",
40
- invalid_deck: "The assistant couldn't produce a valid deck. Your deck is unchanged — try describing the change differently.",
41
- // The opposite problem, and it must not read like the one above: this deck is
42
- // the one already staged on this device, so rewording the request changes
43
- // nothing. Deliberately prescribes NO fix — "reopen it" is a dead end for a
44
- // draft that was never published, since there is no server copy to re-read
45
- // and the open path never clobbers a local draft anyway. A real repair
46
- // affordance (discard this draft) is #39-shaped.
47
- bad_deck: "The copy of this deck saved on this device isn't valid, so the assistant can't work on it.",
48
- stream_truncated: "The assistant's deck was too long to finish. Your deck is unchanged — try asking for fewer slides.",
49
- upstream_error: "Something went wrong — please try again.",
50
- };
51
-
52
- /** `bad_deck` is deliberately absent: it fails identically on every attempt, so
53
- * a Try again button would only loop the user through the same 400. */
54
- const RETRYABLE = new Set(["rate_limited", "upstream_error", "invalid_deck", "stream_truncated"]);
55
-
56
- export function deckChatCanRetry(error: string): boolean {
57
- return RETRYABLE.has(error);
58
- }
59
-
60
- export async function streamDeckChat(
61
- body: { deck: Deck; messages: DeckChatMessage[] },
62
- handlers: DeckChatHandlers,
63
- signal?: AbortSignal,
64
- ): Promise<DeckChatResult> {
65
- let res: Response;
66
- try {
67
- res = await fetch("/api/deck-chat", {
68
- method: "POST",
69
- headers: { "Content-Type": "application/json" },
70
- body: JSON.stringify(body),
71
- signal,
72
- });
73
- } catch {
74
- if (signal?.aborted) return { ok: true, deck: null, truncated: false };
75
- return { ok: false, error: "upstream_error" };
76
- }
77
- if (!res.ok || !res.body) {
78
- const err = (await res.json().catch(() => ({}))) as { error?: string; details?: string[]; retryAfterMs?: number };
79
- return { ok: false, error: err.error ?? "upstream_error", details: err.details, retryAfterMs: err.retryAfterMs };
80
- }
81
-
82
- const reader = res.body.getReader();
83
- const decoder = new TextDecoder();
84
- let buffer = "";
85
- let deck: Deck | null = null;
86
- let truncated = false;
87
- let failure: { error: string; details?: string[] } | null = null;
88
- try {
89
- for (;;) {
90
- const { done, value } = await reader.read();
91
- if (done) break;
92
- buffer += decoder.decode(value, { stream: true });
93
- let sep: number;
94
- while ((sep = buffer.indexOf("\n\n")) !== -1) {
95
- const frame = buffer.slice(0, sep);
96
- buffer = buffer.slice(sep + 2);
97
- const event = /^event: (.+)$/m.exec(frame)?.[1];
98
- const raw = /^data: (.+)$/m.exec(frame)?.[1];
99
- if (!event || !raw) continue;
100
- let data: { text?: string; deck?: unknown; error?: string; details?: string[]; truncated?: boolean; attempt?: number; issues?: string[] };
101
- try {
102
- data = JSON.parse(raw);
103
- } catch {
104
- continue; // malformed frame — skip
105
- }
106
- if (event === "text" && typeof data.text === "string") handlers.onText(data.text);
107
- else if (event === "retry") handlers.onRetry(data.attempt ?? 0, data.issues ?? []);
108
- else if (event === "deck") {
109
- // The route already validated this against the same schema. Parsing
110
- // again is not distrust of the route — it is what turns `unknown` off
111
- // the wire into a `Deck` the Dexie writer can be handed without a cast.
112
- const parsed = parseDeck(data.deck);
113
- if (parsed.ok) deck = parsed.deck;
114
- else failure = { error: "invalid_deck", details: parsed.errors };
115
- } else if (event === "error") failure = { error: data.error ?? "upstream_error", details: data.details };
116
- else if (event === "done") truncated = data.truncated === true;
117
- }
118
- }
119
- } catch {
120
- // A user-initiated stop is a clean finish: whatever streamed stays, and a
121
- // deck that already arrived is still good.
122
- if (!signal?.aborted) return { ok: false, error: "upstream_error" };
123
- }
124
- // A deck that arrived wins over a late error: the route emits at most one
125
- // outcome per turn, and applying a valid deck is never the wrong move.
126
- if (deck) return { ok: true, deck, truncated };
127
- if (failure) return { ok: false, ...failure };
128
- return { ok: true, deck: null, truncated };
129
- }
@@ -1,62 +0,0 @@
1
- import type { DeckChatMessage } from "./deck-chat-client";
2
-
3
- /**
4
- * Per-deck conversation persistence for the Deck Builder assistant (#35).
5
- *
6
- * localStorage, like the brand chat's archive (`components/chat/chat-history`)
7
- * — the server keeps no conversation state (ADR 0002), so the transcript is the
8
- * browser's or it is nowhere.
9
- *
10
- * SCOPED BY DECK AND BY IDENTITY, exactly as the Deck Draft is (`deckDraftScope`).
11
- * A browser profile is shared between people far more often than an account is,
12
- * and a deck's conversation is unpublished work: keyed by deck alone, the next
13
- * person to sign in on the same machine would read it. The scope value is the
14
- * same digest the draft database name uses — never a raw username.
15
- */
16
-
17
- const KEY_PREFIX = "bp-deck-chat";
18
- /** Turns kept per deck. A turn is a user line plus a reply; 60 is a long
19
- * authoring session and still a bounded localStorage footprint. */
20
- export const DECK_CHAT_MESSAGE_CAP = 60;
21
-
22
- function key(scope: string, deckId: string): string {
23
- return `${KEY_PREFIX}:${scope}:${deckId}`;
24
- }
25
-
26
- function isMessage(m: unknown): m is DeckChatMessage {
27
- return (
28
- !!m &&
29
- typeof m === "object" &&
30
- ((m as DeckChatMessage).role === "user" || (m as DeckChatMessage).role === "assistant") &&
31
- typeof (m as DeckChatMessage).content === "string"
32
- );
33
- }
34
-
35
- export function loadDeckChat(scope: string, deckId: string): DeckChatMessage[] {
36
- try {
37
- const parsed: unknown = JSON.parse(window.localStorage.getItem(key(scope, deckId)) ?? "[]");
38
- return Array.isArray(parsed) ? parsed.filter(isMessage).slice(-DECK_CHAT_MESSAGE_CAP) : [];
39
- } catch {
40
- return [];
41
- }
42
- }
43
-
44
- /** Empty turns (the mid-stream assistant stub) are never stored. */
45
- export function saveDeckChat(scope: string, deckId: string, messages: DeckChatMessage[]): void {
46
- const stored = messages.filter((m) => m.content !== "").slice(-DECK_CHAT_MESSAGE_CAP);
47
- try {
48
- if (stored.length === 0) window.localStorage.removeItem(key(scope, deckId));
49
- else window.localStorage.setItem(key(scope, deckId), JSON.stringify(stored));
50
- } catch {
51
- /* storage unavailable or full — the transcript just won't survive a reload */
52
- }
53
- }
54
-
55
- /** Called when a deck is deleted: its conversation goes with it. */
56
- export function clearDeckChat(scope: string, deckId: string): void {
57
- try {
58
- window.localStorage.removeItem(key(scope, deckId));
59
- } catch {
60
- /* nothing to do — the entry is unreachable either way */
61
- }
62
- }
@@ -1,12 +0,0 @@
1
- /**
2
- * The Deck Builder — a sealed subtree like ./deck and ./document.
3
- *
4
- * NOT re-exported from the primitives root barrel: this graph reaches the deck
5
- * runtime, and nothing on an ordinary viewer page may pull that in. Only the
6
- * builder route (`core/src/pages/decks.astro`) imports from here, as a
7
- * `client:only` island, so the whole thing is paid for by opening the builder
8
- * and by nothing else.
9
- */
10
- export { DeckBuilderApp } from "./DeckBuilderApp";
11
- export { starterDeck, newDeckId } from "./starter-deck";
12
- export type { DeckGridEntry } from "./types";