@drawnagency/primitives 0.1.111 → 0.1.112

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 (185) hide show
  1. package/dist/{chunk-3GAG6ZXB.js → chunk-3SZNUCXI.js} +8 -4
  2. package/dist/chunk-4I5OO5UB.js +785 -0
  3. package/dist/{chunk-ALMEC3XN.js → chunk-5KNXTMUC.js} +22 -18
  4. package/dist/{chunk-OUYB2GCW.js → chunk-BR4MIPTM.js} +1 -57
  5. package/dist/chunk-CXZ5YJG4.js +85 -0
  6. package/dist/{chunk-RA5IOFPY.js → chunk-DNY72JZN.js} +16 -38
  7. package/dist/{chunk-7MI3HCOF.js → chunk-EPWBPIV6.js} +10 -6
  8. package/dist/chunk-FV7JQRCN.js +94 -0
  9. package/dist/{chunk-UVIYI4J6.js → chunk-HSXMOS5Q.js} +15 -2
  10. package/dist/chunk-IIDKW7AV.js +41 -0
  11. package/dist/chunk-IINIZBQB.js +204 -0
  12. package/dist/chunk-MM7KZS5C.js +30 -0
  13. package/dist/{chunk-VCCNKTFD.js → chunk-OGGZ5AXT.js} +3 -31
  14. package/dist/{chunk-XQMDWXD6.js → chunk-P3ZNCHW7.js} +1 -1
  15. package/dist/chunk-QQ3S3P5P.js +38 -0
  16. package/dist/{chunk-ELQ4C677.js → chunk-TALRVOGO.js} +9 -44
  17. package/dist/chunk-VAYZJKVR.js +209 -0
  18. package/dist/{chunk-OTDA6KWL.js → chunk-VLPELTPI.js} +1 -1
  19. package/dist/{chunk-7WQJ57XG.js → chunk-VT3PYGVW.js} +68 -260
  20. package/dist/chunk-WUJ3XNFI.js +62 -0
  21. package/dist/{chunk-NXGIHSL5.js → chunk-Z2LO4PSQ.js} +11 -91
  22. package/dist/collab/rebase.js +1 -1
  23. package/dist/components/chat/ChatComposer.d.ts +25 -0
  24. package/dist/components/chat/ChatComposer.d.ts.map +1 -0
  25. package/dist/components/chat/ChatPanel.d.ts +5 -4
  26. package/dist/components/chat/ChatPanel.d.ts.map +1 -1
  27. package/dist/components/chat/markdown.d.ts +14 -0
  28. package/dist/components/chat/markdown.d.ts.map +1 -0
  29. package/dist/components/editor/index.js +16 -11
  30. package/dist/components/primitives/EditablePlainText.d.ts.map +1 -1
  31. package/dist/components/primitives/index.js +15 -10
  32. package/dist/components/sections/brand-guide.js +11 -9
  33. package/dist/components/sections/register-schemas.js +19 -14
  34. package/dist/components/sections/register.js +19 -14
  35. package/dist/components/shell/AudienceChips.d.ts +10 -0
  36. package/dist/components/shell/AudienceChips.d.ts.map +1 -0
  37. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  38. package/dist/components/shell/SiteSettingsChatbot.d.ts.map +1 -1
  39. package/dist/components/shell/SiteSettingsDecks.d.ts +13 -0
  40. package/dist/components/shell/SiteSettingsDecks.d.ts.map +1 -0
  41. package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
  42. package/dist/deck/DeckComponentView.d.ts +13 -0
  43. package/dist/deck/DeckComponentView.d.ts.map +1 -0
  44. package/dist/deck/DeckPlayer.d.ts +26 -0
  45. package/dist/deck/DeckPlayer.d.ts.map +1 -0
  46. package/dist/deck/DeckSlideView.d.ts +17 -0
  47. package/dist/deck/DeckSlideView.d.ts.map +1 -0
  48. package/dist/deck/DeckStage.d.ts +16 -0
  49. package/dist/deck/DeckStage.d.ts.map +1 -0
  50. package/dist/deck/components/DeckImage.d.ts +14 -0
  51. package/dist/deck/components/DeckImage.d.ts.map +1 -0
  52. package/dist/deck/components/DeckImageGrid.d.ts +27 -0
  53. package/dist/deck/components/DeckImageGrid.d.ts.map +1 -0
  54. package/dist/deck/components/DeckRichText.d.ts +22 -0
  55. package/dist/deck/components/DeckRichText.d.ts.map +1 -0
  56. package/dist/deck/components/DeckVideo.d.ts +15 -0
  57. package/dist/deck/components/DeckVideo.d.ts.map +1 -0
  58. package/dist/deck/editing.d.ts +50 -0
  59. package/dist/deck/editing.d.ts.map +1 -0
  60. package/dist/deck/flip.d.ts +11 -0
  61. package/dist/deck/flip.d.ts.map +1 -0
  62. package/dist/deck/index.d.ts +24 -0
  63. package/dist/deck/index.d.ts.map +1 -0
  64. package/dist/deck/index.js +53 -0
  65. package/dist/deck/layouts/CenteredTextLayout.d.ts +12 -0
  66. package/dist/deck/layouts/CenteredTextLayout.d.ts.map +1 -0
  67. package/dist/deck/layouts/ColumnSplitLayout.d.ts +14 -0
  68. package/dist/deck/layouts/ColumnSplitLayout.d.ts.map +1 -0
  69. package/dist/deck/layouts/FullBleedImageLayout.d.ts +14 -0
  70. package/dist/deck/layouts/FullBleedImageLayout.d.ts.map +1 -0
  71. package/dist/deck/media.d.ts +33 -0
  72. package/dist/deck/media.d.ts.map +1 -0
  73. package/dist/deck/tokens.d.ts +22 -0
  74. package/dist/deck/tokens.d.ts.map +1 -0
  75. package/dist/deck/useStageScale.d.ts +10 -0
  76. package/dist/deck/useStageScale.d.ts.map +1 -0
  77. package/dist/deck-builder/DeckBuilderApp.d.ts +30 -0
  78. package/dist/deck-builder/DeckBuilderApp.d.ts.map +1 -0
  79. package/dist/deck-builder/DeckBuilderTopBar.d.ts +24 -0
  80. package/dist/deck-builder/DeckBuilderTopBar.d.ts.map +1 -0
  81. package/dist/deck-builder/DeckChatPanel.d.ts +45 -0
  82. package/dist/deck-builder/DeckChatPanel.d.ts.map +1 -0
  83. package/dist/deck-builder/DeckEditor.d.ts +37 -0
  84. package/dist/deck-builder/DeckEditor.d.ts.map +1 -0
  85. package/dist/deck-builder/DeckLibrary.d.ts +22 -0
  86. package/dist/deck-builder/DeckLibrary.d.ts.map +1 -0
  87. package/dist/deck-builder/DeckSlideSurface.d.ts +31 -0
  88. package/dist/deck-builder/DeckSlideSurface.d.ts.map +1 -0
  89. package/dist/deck-builder/deck-api.d.ts +76 -0
  90. package/dist/deck-builder/deck-api.d.ts.map +1 -0
  91. package/dist/deck-builder/deck-chat-client.d.ts +44 -0
  92. package/dist/deck-builder/deck-chat-client.d.ts.map +1 -0
  93. package/dist/deck-builder/deck-chat-history.d.ts +10 -0
  94. package/dist/deck-builder/deck-chat-history.d.ts.map +1 -0
  95. package/dist/deck-builder/index.d.ts +13 -0
  96. package/dist/deck-builder/index.d.ts.map +1 -0
  97. package/dist/deck-builder/index.js +1505 -0
  98. package/dist/deck-builder/starter-deck.d.ts +23 -0
  99. package/dist/deck-builder/starter-deck.d.ts.map +1 -0
  100. package/dist/deck-builder/types.d.ts +29 -0
  101. package/dist/deck-builder/types.d.ts.map +1 -0
  102. package/dist/deck-builder/useSortableSlide.d.ts +29 -0
  103. package/dist/deck-builder/useSortableSlide.d.ts.map +1 -0
  104. package/dist/index.js +70 -63
  105. package/dist/lib/deck-access.d.ts +86 -0
  106. package/dist/lib/deck-access.d.ts.map +1 -0
  107. package/dist/lib/deck-access.js +58 -0
  108. package/dist/lib/deck-dexie.d.ts +88 -0
  109. package/dist/lib/deck-dexie.d.ts.map +1 -0
  110. package/dist/lib/deck-dexie.js +22 -0
  111. package/dist/lib/deck-edit.d.ts +67 -0
  112. package/dist/lib/deck-edit.d.ts.map +1 -0
  113. package/dist/lib/deck-media-assets.d.ts +27 -0
  114. package/dist/lib/deck-media-assets.d.ts.map +1 -0
  115. package/dist/lib/deck-media-assets.js +25 -0
  116. package/dist/lib/deck-media-refs.d.ts +35 -0
  117. package/dist/lib/deck-media-refs.d.ts.map +1 -0
  118. package/dist/lib/deck-media-refs.js +36 -0
  119. package/dist/lib/deck-tool.d.ts +25 -0
  120. package/dist/lib/deck-tool.d.ts.map +1 -0
  121. package/dist/lib/deck-tool.js +58 -0
  122. package/dist/lib/index-ops.js +2 -2
  123. package/dist/lib/index.js +17 -12
  124. package/dist/lib/nav.d.ts.map +1 -1
  125. package/dist/media/index.js +3 -3
  126. package/dist/schemas/deck-meta.d.ts +85 -0
  127. package/dist/schemas/deck-meta.d.ts.map +1 -0
  128. package/dist/schemas/deck-meta.js +64 -0
  129. package/dist/schemas/deck.d.ts +954 -0
  130. package/dist/schemas/deck.d.ts.map +1 -0
  131. package/dist/schemas/deck.js +65 -0
  132. package/dist/schemas/index.js +39 -37
  133. package/dist/schemas/site-config.d.ts +13 -1
  134. package/dist/schemas/site-config.d.ts.map +1 -1
  135. package/package.json +37 -1
  136. package/src/components/chat/ChatComposer.tsx +100 -0
  137. package/src/components/chat/ChatPanel.tsx +20 -69
  138. package/src/components/chat/markdown.ts +32 -0
  139. package/src/components/primitives/EditablePlainText.tsx +11 -1
  140. package/src/components/shell/AudienceChips.tsx +30 -0
  141. package/src/components/shell/EditorShell.tsx +12 -0
  142. package/src/components/shell/SiteSettingsChatbot.tsx +1 -18
  143. package/src/components/shell/SiteSettingsDecks.tsx +41 -0
  144. package/src/components/shell/SiteSettingsModal.tsx +11 -0
  145. package/src/deck/DeckComponentView.tsx +28 -0
  146. package/src/deck/DeckPlayer.tsx +166 -0
  147. package/src/deck/DeckSlideView.tsx +38 -0
  148. package/src/deck/DeckStage.tsx +39 -0
  149. package/src/deck/components/DeckImage.tsx +38 -0
  150. package/src/deck/components/DeckImageGrid.tsx +254 -0
  151. package/src/deck/components/DeckRichText.tsx +111 -0
  152. package/src/deck/components/DeckVideo.tsx +81 -0
  153. package/src/deck/editing.tsx +201 -0
  154. package/src/deck/flip.ts +70 -0
  155. package/src/deck/index.ts +30 -0
  156. package/src/deck/layouts/CenteredTextLayout.tsx +29 -0
  157. package/src/deck/layouts/ColumnSplitLayout.tsx +69 -0
  158. package/src/deck/layouts/FullBleedImageLayout.tsx +58 -0
  159. package/src/deck/media.tsx +46 -0
  160. package/src/deck/tokens.ts +57 -0
  161. package/src/deck/useStageScale.ts +44 -0
  162. package/src/deck-builder/DeckBuilderApp.tsx +572 -0
  163. package/src/deck-builder/DeckBuilderTopBar.tsx +62 -0
  164. package/src/deck-builder/DeckChatPanel.tsx +334 -0
  165. package/src/deck-builder/DeckEditor.tsx +258 -0
  166. package/src/deck-builder/DeckLibrary.tsx +195 -0
  167. package/src/deck-builder/DeckSlideSurface.tsx +55 -0
  168. package/src/deck-builder/deck-api.ts +177 -0
  169. package/src/deck-builder/deck-chat-client.ts +129 -0
  170. package/src/deck-builder/deck-chat-history.ts +62 -0
  171. package/src/deck-builder/index.ts +12 -0
  172. package/src/deck-builder/starter-deck.ts +86 -0
  173. package/src/deck-builder/types.ts +24 -0
  174. package/src/deck-builder/useSortableSlide.ts +102 -0
  175. package/src/lib/deck-access.ts +132 -0
  176. package/src/lib/deck-dexie.ts +190 -0
  177. package/src/lib/deck-edit.ts +196 -0
  178. package/src/lib/deck-media-assets.ts +57 -0
  179. package/src/lib/deck-media-refs.ts +67 -0
  180. package/src/lib/deck-tool.ts +95 -0
  181. package/src/lib/nav.ts +21 -1
  182. package/src/schemas/deck-meta.ts +88 -0
  183. package/src/schemas/deck.ts +478 -0
  184. package/src/schemas/index.ts +4 -0
  185. package/src/schemas/site-config.ts +24 -1
@@ -0,0 +1,204 @@
1
+ import {
2
+ cn
3
+ } from "./chunk-7EOSDFS3.js";
4
+
5
+ // src/components/shared/Tooltip.tsx
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ var sideClasses = {
8
+ bottom: "left-1/2 top-full mt-1.5 -translate-x-1/2",
9
+ right: "left-full top-1/2 ml-1.5 -translate-y-1/2",
10
+ "bottom-clamp": "top-full mt-1.5 left-1/2 -translate-x-1/2 max-xl:left-auto max-xl:right-0 max-xl:translate-x-0"
11
+ };
12
+ function Tooltip({ content, children, className, side = "bottom" }) {
13
+ return /* @__PURE__ */ jsxs("div", { className: cn("group/tooltip relative inline-flex", className), children: [
14
+ children,
15
+ /* @__PURE__ */ jsx(
16
+ "span",
17
+ {
18
+ className: cn(
19
+ "pointer-events-none absolute z-50 opacity-0 transition-opacity delay-300 group-hover/tooltip:opacity-100",
20
+ sideClasses[side]
21
+ ),
22
+ role: "tooltip",
23
+ children: /* @__PURE__ */ jsx("span", { className: "block whitespace-nowrap rounded bg-tooltip px-2 py-1 text-center text-xs leading-relaxed text-tooltip-muted", children: content })
24
+ }
25
+ )
26
+ ] });
27
+ }
28
+ function Kbd({ children }) {
29
+ return /* @__PURE__ */ jsx("strong", { className: "text-tooltip-contrast", children });
30
+ }
31
+
32
+ // src/components/shared/Popover.tsx
33
+ import { useEffect as useEffect2, useLayoutEffect, useRef, useState } from "react";
34
+ import { createPortal } from "react-dom";
35
+
36
+ // src/hooks/useFocusTrap.ts
37
+ import { useEffect } from "react";
38
+ var FOCUSABLE_SELECTOR = [
39
+ "a[href]",
40
+ "button:not([disabled])",
41
+ "input:not([disabled])",
42
+ "select:not([disabled])",
43
+ "textarea:not([disabled])",
44
+ '[tabindex]:not([tabindex="-1"])'
45
+ ].join(", ");
46
+ function useInitialFocus(ref, active) {
47
+ useEffect(() => {
48
+ if (!active || !ref.current) return;
49
+ const previouslyFocused = document.activeElement;
50
+ ref.current.querySelector(FOCUSABLE_SELECTOR)?.focus();
51
+ return () => {
52
+ if (previouslyFocused && document.body.contains(previouslyFocused)) {
53
+ previouslyFocused.focus();
54
+ }
55
+ };
56
+ }, [active, ref]);
57
+ }
58
+
59
+ // src/components/shared/Popover.tsx
60
+ import { jsx as jsx2 } from "react/jsx-runtime";
61
+ function Popover({
62
+ isOpen,
63
+ onClose,
64
+ anchorRef,
65
+ children,
66
+ align = "auto",
67
+ className,
68
+ portal = false,
69
+ placement = "bottom"
70
+ }) {
71
+ const panelRef = useRef(null);
72
+ const [resolvedAlign, setResolvedAlign] = useState("start");
73
+ const [fixedPos, setFixedPos] = useState(null);
74
+ useInitialFocus(panelRef, isOpen && (!portal || fixedPos !== null));
75
+ useLayoutEffect(() => {
76
+ if (!portal) return;
77
+ if (!isOpen) {
78
+ setFixedPos(null);
79
+ return;
80
+ }
81
+ const anchor = anchorRef.current;
82
+ if (!anchor) return;
83
+ const update = () => {
84
+ const r = anchor.getBoundingClientRect();
85
+ const next = placement === "top" ? { left: r.left, bottom: window.innerHeight - r.top + 4 } : { left: r.left, top: r.bottom + 4 };
86
+ setFixedPos(
87
+ (prev) => prev && prev.left === next.left && prev.top === next.top && prev.bottom === next.bottom ? prev : next
88
+ );
89
+ };
90
+ update();
91
+ window.addEventListener("resize", update);
92
+ window.addEventListener("scroll", update, true);
93
+ return () => {
94
+ window.removeEventListener("resize", update);
95
+ window.removeEventListener("scroll", update, true);
96
+ };
97
+ }, [isOpen, portal, placement, anchorRef]);
98
+ useLayoutEffect(() => {
99
+ if (portal) return;
100
+ if (!isOpen || align !== "auto") {
101
+ if (!isOpen) setResolvedAlign("start");
102
+ return;
103
+ }
104
+ const panel = panelRef.current;
105
+ if (!panel) return;
106
+ panel.style.left = "0";
107
+ panel.style.right = "auto";
108
+ const rect = panel.getBoundingClientRect();
109
+ if (rect.right > window.innerWidth) {
110
+ setResolvedAlign("end");
111
+ } else {
112
+ setResolvedAlign("start");
113
+ }
114
+ panel.style.left = "";
115
+ panel.style.right = "";
116
+ }, [isOpen, align]);
117
+ useEffect2(() => {
118
+ if (!isOpen) return;
119
+ function onKeyDown(e) {
120
+ if (e.key !== "Escape") return;
121
+ if (document.querySelector("[data-modal-layer]")) return;
122
+ onClose();
123
+ }
124
+ function onMouseDown(e) {
125
+ const target = e.target;
126
+ if (panelRef.current?.contains(target)) return;
127
+ if (anchorRef.current?.contains(target)) return;
128
+ if (target instanceof Element && target.closest("[data-modal-layer]")) return;
129
+ onClose();
130
+ }
131
+ document.addEventListener("keydown", onKeyDown);
132
+ document.addEventListener("mousedown", onMouseDown);
133
+ return () => {
134
+ document.removeEventListener("keydown", onKeyDown);
135
+ document.removeEventListener("mousedown", onMouseDown);
136
+ };
137
+ }, [isOpen, onClose, anchorRef]);
138
+ if (!isOpen) return null;
139
+ const effectiveAlign = align === "auto" ? resolvedAlign : align;
140
+ if (portal) {
141
+ if (!fixedPos) return null;
142
+ return createPortal(
143
+ /* @__PURE__ */ jsx2(
144
+ "div",
145
+ {
146
+ ref: panelRef,
147
+ role: "dialog",
148
+ "aria-modal": "false",
149
+ style: {
150
+ position: "fixed",
151
+ left: `${fixedPos.left}px`,
152
+ ...fixedPos.top !== void 0 ? { top: `${fixedPos.top}px` } : {},
153
+ ...fixedPos.bottom !== void 0 ? { bottom: `${fixedPos.bottom}px` } : {}
154
+ },
155
+ className: cn("z-50 rounded-md border border-base-200 bg-base shadow-lg", className),
156
+ children
157
+ }
158
+ ),
159
+ document.body
160
+ );
161
+ }
162
+ return /* @__PURE__ */ jsx2(
163
+ "div",
164
+ {
165
+ ref: panelRef,
166
+ role: "dialog",
167
+ "aria-modal": "false",
168
+ className: cn(
169
+ "absolute top-full z-50 mt-1 rounded-md border border-base-200 bg-base shadow-lg",
170
+ effectiveAlign === "end" ? "right-0" : "left-0",
171
+ className
172
+ ),
173
+ children
174
+ }
175
+ );
176
+ }
177
+
178
+ // src/components/shared/PopoverItem.tsx
179
+ import { forwardRef } from "react";
180
+ import { jsx as jsx3 } from "react/jsx-runtime";
181
+ var PopoverItem = forwardRef(
182
+ function PopoverItem2({ className, children, ...rest }, ref) {
183
+ return /* @__PURE__ */ jsx3(
184
+ "button",
185
+ {
186
+ ref,
187
+ type: "button",
188
+ className: cn(
189
+ "flex w-full cursor-pointer items-center gap-3 px-3 py-1.5 text-left text-xs hover:bg-base-accent",
190
+ className
191
+ ),
192
+ ...rest,
193
+ children
194
+ }
195
+ );
196
+ }
197
+ );
198
+
199
+ export {
200
+ Tooltip,
201
+ Kbd,
202
+ Popover,
203
+ PopoverItem
204
+ };
@@ -0,0 +1,30 @@
1
+ // src/lib/timestamp.ts
2
+ function formatTimestamp(iso) {
3
+ const date = new Date(iso);
4
+ const now = /* @__PURE__ */ new Date();
5
+ const diffMs = now.getTime() - date.getTime();
6
+ const diffMinutes = Math.floor(diffMs / 6e4);
7
+ const diffHours = Math.floor(diffMs / 36e5);
8
+ const sameDay = date.getUTCFullYear() === now.getUTCFullYear() && date.getUTCMonth() === now.getUTCMonth() && date.getUTCDate() === now.getUTCDate();
9
+ if (diffMinutes < 1) return "just now";
10
+ if (diffMinutes < 60) {
11
+ return `${diffMinutes} ${diffMinutes === 1 ? "minute" : "minutes"} ago`;
12
+ }
13
+ if (sameDay) {
14
+ return `${diffHours} ${diffHours === 1 ? "hour" : "hours"} ago`;
15
+ }
16
+ return date.toLocaleDateString("en-US", {
17
+ month: "short",
18
+ day: "numeric",
19
+ timeZone: "UTC"
20
+ }) + " at " + date.toLocaleTimeString("en-US", {
21
+ hour: "numeric",
22
+ minute: "2-digit",
23
+ hour12: true,
24
+ timeZone: "UTC"
25
+ });
26
+ }
27
+
28
+ export {
29
+ formatTimestamp
30
+ };
@@ -10,12 +10,12 @@ import {
10
10
  import {
11
11
  getSectionSchemaForType
12
12
  } from "./chunk-DMCGN5AS.js";
13
+ import {
14
+ IndexSchema
15
+ } from "./chunk-HSXMOS5Q.js";
13
16
  import {
14
17
  getAllSchemas
15
18
  } from "./chunk-FTPETWXE.js";
16
- import {
17
- IndexSchema
18
- } from "./chunk-UVIYI4J6.js";
19
19
  import {
20
20
  MAX_BLOCK_DEPTH,
21
21
  blockDepth
@@ -115,33 +115,6 @@ function buildGoogleFontsUrl(headingFont, bodyFont) {
115
115
  return `https://fonts.googleapis.com/css2?${params}&display=swap`;
116
116
  }
117
117
 
118
- // src/lib/timestamp.ts
119
- function formatTimestamp(iso) {
120
- const date = new Date(iso);
121
- const now = /* @__PURE__ */ new Date();
122
- const diffMs = now.getTime() - date.getTime();
123
- const diffMinutes = Math.floor(diffMs / 6e4);
124
- const diffHours = Math.floor(diffMs / 36e5);
125
- const sameDay = date.getUTCFullYear() === now.getUTCFullYear() && date.getUTCMonth() === now.getUTCMonth() && date.getUTCDate() === now.getUTCDate();
126
- if (diffMinutes < 1) return "just now";
127
- if (diffMinutes < 60) {
128
- return `${diffMinutes} ${diffMinutes === 1 ? "minute" : "minutes"} ago`;
129
- }
130
- if (sameDay) {
131
- return `${diffHours} ${diffHours === 1 ? "hour" : "hours"} ago`;
132
- }
133
- return date.toLocaleDateString("en-US", {
134
- month: "short",
135
- day: "numeric",
136
- timeZone: "UTC"
137
- }) + " at " + date.toLocaleTimeString("en-US", {
138
- hour: "numeric",
139
- minute: "2-digit",
140
- hour12: true,
141
- timeZone: "UTC"
142
- });
143
- }
144
-
145
118
  // src/lib/safeRedirect.ts
146
119
  function safeRedirect(url, fallback) {
147
120
  if (!url) return fallback;
@@ -257,7 +230,6 @@ export {
257
230
  onPrimaryVarValue,
258
231
  gridColsClass,
259
232
  buildGoogleFontsUrl,
260
- formatTimestamp,
261
233
  safeRedirect,
262
234
  mergeSiteContent,
263
235
  loadStaticSiteContent,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  slugifyPageSlug
3
- } from "./chunk-OTDA6KWL.js";
3
+ } from "./chunk-VLPELTPI.js";
4
4
 
5
5
  // src/media/utils.ts
6
6
  async function hashFileBuffer(buffer) {
@@ -0,0 +1,38 @@
1
+ // src/lib/sanitize.ts
2
+ var purifierPromise = null;
3
+ var purifier = null;
4
+ if (typeof window !== "undefined") {
5
+ purifierPromise = import("dompurify").then((mod) => {
6
+ const DOMPurify = mod.default ?? mod;
7
+ purifier = (html) => DOMPurify.sanitize(html);
8
+ return mod;
9
+ });
10
+ }
11
+ var sanitizeCache = /* @__PURE__ */ new Map();
12
+ var SANITIZE_CACHE_LIMIT = 500;
13
+ function sanitizeHtml(html) {
14
+ if (!html) return "";
15
+ if (!purifier) return html;
16
+ const cached = sanitizeCache.get(html);
17
+ if (cached !== void 0) return cached;
18
+ const clean = purifier(html);
19
+ if (sanitizeCache.size >= SANITIZE_CACHE_LIMIT) {
20
+ const oldest = sanitizeCache.keys().next().value;
21
+ if (oldest !== void 0) sanitizeCache.delete(oldest);
22
+ }
23
+ sanitizeCache.set(html, clean);
24
+ return clean;
25
+ }
26
+ function isSanitizerReady() {
27
+ return typeof window === "undefined" || purifier !== null;
28
+ }
29
+ async function ensureSanitizer() {
30
+ if (typeof window === "undefined") return;
31
+ if (!purifier && purifierPromise) await purifierPromise;
32
+ }
33
+
34
+ export {
35
+ sanitizeHtml,
36
+ isSanitizerReady,
37
+ ensureSanitizer
38
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getMediaProvider
3
- } from "./chunk-XQMDWXD6.js";
3
+ } from "./chunk-P3ZNCHW7.js";
4
4
  import {
5
5
  cn
6
6
  } from "./chunk-7EOSDFS3.js";
@@ -72,44 +72,10 @@ function SettingsIcon({ size = 14 }) {
72
72
  return /* @__PURE__ */ jsx(Settings, { size });
73
73
  }
74
74
 
75
- // src/components/shared/IconButton.tsx
76
- import { forwardRef } from "react";
77
- import { jsx as jsx2 } from "react/jsx-runtime";
78
- var sizeClasses = {
79
- sm: "h-chrome-sm w-chrome-sm",
80
- md: "h-chrome-md w-chrome-md",
81
- lg: "h-chrome-lg w-chrome-lg"
82
- };
83
- var intentClasses = {
84
- default: "text-base-contrast-light hover:text-base-contrast",
85
- destructive: "text-destructive hover:text-destructive-hover",
86
- primary: "text-primary/60 hover:text-primary"
87
- };
88
- var IconButton = forwardRef(
89
- function IconButton2({ icon, label, size = "md", intent = "default", className, ...rest }, ref) {
90
- return /* @__PURE__ */ jsx2(
91
- "button",
92
- {
93
- ref,
94
- type: "button",
95
- className: cn(
96
- "cursor-pointer flex items-center justify-center rounded transition-colors",
97
- sizeClasses[size],
98
- intentClasses[intent],
99
- className
100
- ),
101
- "aria-label": label,
102
- ...rest,
103
- children: icon
104
- }
105
- );
106
- }
107
- );
108
-
109
75
  // src/components/shared/FormLabel.tsx
110
- import { jsx as jsx3 } from "react/jsx-runtime";
76
+ import { jsx as jsx2 } from "react/jsx-runtime";
111
77
  function FormLabel({ htmlFor, children, className }) {
112
- return /* @__PURE__ */ jsx3(
78
+ return /* @__PURE__ */ jsx2(
113
79
  "label",
114
80
  {
115
81
  htmlFor,
@@ -120,14 +86,14 @@ function FormLabel({ htmlFor, children, className }) {
120
86
  }
121
87
 
122
88
  // src/components/shared/Input.tsx
123
- import { forwardRef as forwardRef2, useId } from "react";
124
- import { jsx as jsx4, jsxs } from "react/jsx-runtime";
125
- var Input = forwardRef2(function Input2({ label, value, onChange, type = "text", className, disabled, error, ...rest }, ref) {
89
+ import { forwardRef, useId } from "react";
90
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
91
+ var Input = forwardRef(function Input2({ label, value, onChange, type = "text", className, disabled, error, ...rest }, ref) {
126
92
  const id = useId();
127
93
  const errorId = `${id}-error`;
128
94
  return /* @__PURE__ */ jsxs("div", { className, children: [
129
- /* @__PURE__ */ jsx4(FormLabel, { htmlFor: id, children: label }),
130
- /* @__PURE__ */ jsx4(
95
+ /* @__PURE__ */ jsx3(FormLabel, { htmlFor: id, children: label }),
96
+ /* @__PURE__ */ jsx3(
131
97
  "input",
132
98
  {
133
99
  ref,
@@ -146,7 +112,7 @@ var Input = forwardRef2(function Input2({ label, value, onChange, type = "text",
146
112
  ...rest
147
113
  }
148
114
  ),
149
- error && /* @__PURE__ */ jsx4("p", { id: errorId, className: "mt-1 text-xs text-red-600", children: error })
115
+ error && /* @__PURE__ */ jsx3("p", { id: errorId, className: "mt-1 text-xs text-red-600", children: error })
150
116
  ] });
151
117
  });
152
118
 
@@ -157,7 +123,6 @@ export {
157
123
  AddIcon,
158
124
  DeleteIcon,
159
125
  SettingsIcon,
160
- IconButton,
161
126
  FormLabel,
162
127
  Input
163
128
  };
@@ -0,0 +1,209 @@
1
+ // src/schemas/deck.ts
2
+ import { z } from "zod";
3
+ var DECK_STAGE_WIDTH = 1600;
4
+ var DECK_STAGE_HEIGHT = 900;
5
+ var DECK_SCHEMA_VERSION = 1;
6
+ var DECK_MAX_SLIDES = 40;
7
+ var DeckIdSchema = z.string().max(64, "must be 64 characters or fewer \u2014 a deck id is also a directory name").regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase letters, digits and hyphens, starting with a letter or digit");
8
+ var DECK_LAYOUT_TYPES = ["centeredText", "fullBleedImage", "columnSplit"];
9
+ var DECK_COMPONENT_TYPES = ["richText", "image", "imageGrid", "video"];
10
+ var DECK_MEDIA_KINDS = ["portal"];
11
+ var DECK_SURFACES = ["base", "brand"];
12
+ var DECK_OVERLAY_PLACEMENTS = ["sideLeft", "sideRight", "bottomBand"];
13
+ var DECK_SPLIT_RATIOS = ["1-2", "2-1"];
14
+ function vocabularyError(kind, types, field = "type") {
15
+ return `unknown ${kind} ${field} \u2014 the deck runtime provides: ${types.map((t) => `"${t}"`).join(", ")}`;
16
+ }
17
+ var PortalMediaRefSchema = z.object({
18
+ kind: z.literal("portal"),
19
+ // Same charset as a deck id, for the same reason and one more: a media id
20
+ // becomes a key into a plain object at resolve time, and `"__proto__"` parses
21
+ // as a perfectly good string. Constrain it here rather than relying on every
22
+ // resolver to be careful.
23
+ id: z.string().max(120, "must be 120 characters or fewer").regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase letters, digits and hyphens, starting with a letter or digit").describe("Media id from the portal's media library \u2014 the manifest key, not a filename, path or URL.")
24
+ });
25
+ var DeckMediaRefSchema = z.discriminatedUnion("kind", [PortalMediaRefSchema], {
26
+ error: vocabularyError("media reference", DECK_MEDIA_KINDS, "kind")
27
+ });
28
+ var PARAGRAPH_MAX = 400;
29
+ var BULLET_MAX = 160;
30
+ var MAX_SPANS = 12;
31
+ var DeckTextSpanSchema = z.strictObject({
32
+ text: z.string().min(1).max(PARAGRAPH_MAX).describe("The span's literal text. Never HTML or markdown \u2014 the marks below are the only formatting there is."),
33
+ bold: z.boolean().optional().describe("Renders the span at full ink weight. For the one phrase in a line that carries it."),
34
+ italic: z.boolean().optional().describe("Renders the span italic. For titles of works and genuine emphasis, not decoration.")
35
+ });
36
+ function inlineText(maxChars) {
37
+ return z.union([z.string().min(1).max(maxChars), z.array(DeckTextSpanSchema).min(1).max(MAX_SPANS)], {
38
+ error: "must be plain text, or an array of { text, bold?, italic? } spans \u2014 deck rich text is structured, never HTML or markdown"
39
+ });
40
+ }
41
+ function inlineLength(value) {
42
+ return typeof value === "string" ? value.length : value.reduce((total, span) => total + span.text.length, 0);
43
+ }
44
+ var DeckRichTextSchema = z.object({
45
+ type: z.literal("richText"),
46
+ kicker: z.string().min(1).max(60).optional().describe("Short label above the heading, e.g. a campaign or section name. Rendered small, tracked and in the brand accent."),
47
+ heading: z.string().min(1).max(120).optional().describe("The slide's primary line. Keep it to a phrase \u2014 it renders at display size on the 16:9 stage."),
48
+ subheading: z.string().min(1).max(200).optional().describe("Secondary line under the heading. Optional; omit rather than repeating the heading."),
49
+ paragraphs: z.array(inlineText(PARAGRAPH_MAX)).max(3).optional().describe(
50
+ `Body copy, one entry per paragraph. Each is either a plain string or an array of { text, bold?, italic? } spans \u2014 use spans only where a phrase genuinely needs a mark. Never HTML or markdown; ${PARAGRAPH_MAX} characters per paragraph.`
51
+ ),
52
+ bullets: z.array(inlineText(BULLET_MAX)).max(6).optional().describe(
53
+ `Short bullet lines, plain strings or span arrays like paragraphs. Prefer six or fewer so they stay legible at presentation size; ${BULLET_MAX} characters per line.`
54
+ )
55
+ }).superRefine((value, ctx) => {
56
+ const hasContent = value.kicker !== void 0 || value.heading !== void 0 || value.subheading !== void 0 || (value.paragraphs?.length ?? 0) > 0 || (value.bullets?.length ?? 0) > 0;
57
+ if (!hasContent) {
58
+ ctx.addIssue({
59
+ code: "custom",
60
+ message: "a richText component needs at least one of kicker, heading, subheading, paragraphs or bullets"
61
+ });
62
+ }
63
+ const capLines = (lines, max, field) => {
64
+ lines?.forEach((line, i) => {
65
+ const length = inlineLength(line);
66
+ if (length > max) {
67
+ ctx.addIssue({
68
+ code: "custom",
69
+ path: [field, i],
70
+ message: `spans total ${length} characters \u2014 keep a ${field === "bullets" ? "bullet" : "paragraph"} to ${max} or fewer`
71
+ });
72
+ }
73
+ });
74
+ };
75
+ capLines(value.paragraphs, PARAGRAPH_MAX, "paragraphs");
76
+ capLines(value.bullets, BULLET_MAX, "bullets");
77
+ });
78
+ var DeckImageSchema = z.object({
79
+ type: z.literal("image"),
80
+ media: DeckMediaRefSchema.describe("The image to show. It fills its slot and is cropped to fit."),
81
+ caption: z.string().min(1).max(120).optional().describe("Caption printed on a solid brand block along the image's bottom edge. Omit rather than restating the slide.")
82
+ });
83
+ var DeckImageGridItemSchema = z.object({
84
+ media: DeckMediaRefSchema.describe("The tile's image."),
85
+ caption: z.string().min(1).max(80).optional().describe("Shown on the tile on hover, and while the tile is expanded. A few words, not a sentence.")
86
+ });
87
+ var DeckImageGridSchema = z.object({
88
+ type: z.literal("imageGrid"),
89
+ items: z.array(DeckImageGridItemSchema).min(2, "an image grid needs at least two images \u2014 use the image component for one").max(12, "an image grid holds at most twelve images \u2014 past that a tile is too small to read at presentation size").describe(
90
+ "The tiles, in order of importance. Two to twelve. They pack into masonry columns at their own aspect ratios \u2014 nothing is cropped and nothing is resized to match its neighbours \u2014 and the columns clip at the bottom of the slot if the stack runs past it, so put the frames that must be seen first."
91
+ )
92
+ });
93
+ var DeckVideoSchema = z.object({
94
+ type: z.literal("video"),
95
+ media: DeckMediaRefSchema.describe("The video to play. Must reference portal-hosted media \u2014 a deck cannot embed an external player."),
96
+ title: z.string().min(1).max(120).optional().describe("Label on a brand block across the bottom of the poster, before playback starts.")
97
+ });
98
+ var DeckComponentSchema = z.discriminatedUnion(
99
+ "type",
100
+ [DeckRichTextSchema, DeckImageSchema, DeckImageGridSchema, DeckVideoSchema],
101
+ { error: vocabularyError("deck component", DECK_COMPONENT_TYPES) }
102
+ );
103
+ var CenteredTextLayoutSchema = z.object({
104
+ type: z.literal("centeredText"),
105
+ background: z.enum(DECK_SURFACES).default("base").describe("`base` puts the slide on the site's surface color; `brand` fills it with the brand primary and flips the ink."),
106
+ content: DeckComponentSchema.describe("The single Deck Component this Layout centers.")
107
+ });
108
+ var DeckOverlayPanelSchema = z.object({
109
+ placement: z.enum(DECK_OVERLAY_PLACEMENTS).default("sideLeft").describe(
110
+ "`sideLeft`/`sideRight` put the panel in a full-height column over that edge of the image; `bottomBand` runs it full width across the bottom. Pick the side the image is emptiest on."
111
+ ),
112
+ content: DeckRichTextSchema.describe("The panel's text. It sits on the brand color, so keep it short \u2014 a kicker and a line or two.")
113
+ });
114
+ var FullBleedImageLayoutSchema = z.object({
115
+ type: z.literal("fullBleedImage"),
116
+ media: DeckMediaRefSchema.describe("The image filling the whole 16:9 stage. It is cropped to fit, so choose one that survives a wide crop."),
117
+ overlay: DeckOverlayPanelSchema.optional().describe("Optional text panel over the image. Omit it for a slide that is purely the picture.")
118
+ });
119
+ var ColumnSplitLayoutSchema = z.object({
120
+ type: z.literal("columnSplit"),
121
+ ratio: z.enum(DECK_SPLIT_RATIOS).default("1-2").describe("`1-2` gives the left column a third of the stage and the right two thirds; `2-1` is the reverse. Give the media the larger side."),
122
+ left: DeckComponentSchema.describe("The left column's Deck Component."),
123
+ right: DeckComponentSchema.describe("The right column's Deck Component.")
124
+ });
125
+ var DeckLayoutSchema = z.discriminatedUnion(
126
+ "type",
127
+ [CenteredTextLayoutSchema, FullBleedImageLayoutSchema, ColumnSplitLayoutSchema],
128
+ { error: vocabularyError("layout", DECK_LAYOUT_TYPES) }
129
+ );
130
+ var DeckSlideSchema = z.object({
131
+ id: DeckIdSchema.describe("Stable slide id, unique within the deck. Reused across edits so revisions stay diffable."),
132
+ title: z.string().min(1).max(80).optional().describe("Short label for the slide, used by the builder filmstrip and by slide-navigation labels. Not rendered on the stage."),
133
+ layout: DeckLayoutSchema.describe("The full-slide arrangement. A slide must use exactly one Layout.")
134
+ });
135
+ var DeckSchema = z.object({
136
+ schemaVersion: z.literal(DECK_SCHEMA_VERSION).describe(`Deck schema version. Always ${DECK_SCHEMA_VERSION}.`),
137
+ id: DeckIdSchema.describe("Stable deck id \u2014 also the directory name on the decks branch."),
138
+ title: z.string().min(1).max(120).describe("The deck's name, shown in the builder and in the deck's chrome."),
139
+ slides: z.array(DeckSlideSchema).min(1, "a deck needs at least one slide").max(
140
+ DECK_MAX_SLIDES,
141
+ `a deck is capped at ${DECK_MAX_SLIDES} slides \u2014 split a longer story into several decks`
142
+ ).describe(
143
+ `Slides in presentation order, at most ${DECK_MAX_SLIDES}. The whole deck is re-emitted on every authoring turn and re-committed on every publish, so the cap bounds both the model's output and the size of a stored deck.`
144
+ )
145
+ }).superRefine((deck, ctx) => {
146
+ const seen = /* @__PURE__ */ new Set();
147
+ deck.slides.forEach((slide, i) => {
148
+ if (seen.has(slide.id)) {
149
+ ctx.addIssue({
150
+ code: "custom",
151
+ path: ["slides", i, "id"],
152
+ message: `duplicate slide id "${slide.id}" \u2014 slide ids must be unique within a deck`
153
+ });
154
+ }
155
+ seen.add(slide.id);
156
+ });
157
+ });
158
+ function formatPath(path) {
159
+ return path.reduce((acc, key) => {
160
+ if (typeof key === "number") return `${acc}[${key}]`;
161
+ return acc === "" ? String(key) : `${acc}.${String(key)}`;
162
+ }, "");
163
+ }
164
+ function formatDeckIssues(error) {
165
+ return error.issues.map((issue) => {
166
+ const path = formatPath(issue.path);
167
+ return path === "" ? issue.message : `${path}: ${issue.message}`;
168
+ });
169
+ }
170
+ function parseDeck(input) {
171
+ const result = DeckSchema.safeParse(input);
172
+ return result.success ? { ok: true, deck: result.data } : { ok: false, errors: formatDeckIssues(result.error) };
173
+ }
174
+ function serializeDeck(deck) {
175
+ return JSON.stringify(deck, null, 2) + "\n";
176
+ }
177
+
178
+ export {
179
+ DECK_STAGE_WIDTH,
180
+ DECK_STAGE_HEIGHT,
181
+ DECK_SCHEMA_VERSION,
182
+ DECK_MAX_SLIDES,
183
+ DeckIdSchema,
184
+ DECK_LAYOUT_TYPES,
185
+ DECK_COMPONENT_TYPES,
186
+ DECK_MEDIA_KINDS,
187
+ DECK_SURFACES,
188
+ DECK_OVERLAY_PLACEMENTS,
189
+ DECK_SPLIT_RATIOS,
190
+ PortalMediaRefSchema,
191
+ DeckMediaRefSchema,
192
+ DeckTextSpanSchema,
193
+ DeckRichTextSchema,
194
+ DeckImageSchema,
195
+ DeckImageGridItemSchema,
196
+ DeckImageGridSchema,
197
+ DeckVideoSchema,
198
+ DeckComponentSchema,
199
+ CenteredTextLayoutSchema,
200
+ DeckOverlayPanelSchema,
201
+ FullBleedImageLayoutSchema,
202
+ ColumnSplitLayoutSchema,
203
+ DeckLayoutSchema,
204
+ DeckSlideSchema,
205
+ DeckSchema,
206
+ formatDeckIssues,
207
+ parseDeck,
208
+ serializeDeck
209
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RESERVED_SLUGS
3
- } from "./chunk-UVIYI4J6.js";
3
+ } from "./chunk-HSXMOS5Q.js";
4
4
 
5
5
  // src/lib/nav-tree.ts
6
6
  function reconcileNav(entries, pages) {