@drawnagency/primitives 0.1.117 → 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.
- package/dist/{chunk-O37AV27P.js → chunk-5JYHTHVR.js} +28 -0
- package/dist/{chunk-W72BPGR3.js → chunk-BZKHHUIN.js} +44 -8
- package/dist/{chunk-CEFFP3CH.js → chunk-G6O3TYPJ.js} +297 -83
- package/dist/{chunk-L7WTERU5.js → chunk-HFHM4MNK.js} +33 -10
- package/dist/{chunk-RIITGTV3.js → chunk-LQHBC7C6.js} +37 -2
- package/dist/{chunk-VY3IFGE3.js → chunk-QAMAIDXZ.js} +1 -1
- package/dist/{chunk-LNQUUC2S.js → chunk-RIN2QCYJ.js} +9 -11
- package/dist/{chunk-HUOW3ZV2.js → chunk-UNFPATDE.js} +8 -10
- package/dist/{chunk-MSBHMY3C.js → chunk-YKKGVSJH.js} +35 -37
- package/dist/{chunk-67P34WNW.js → chunk-ZKVQMIMJ.js} +3 -20
- package/dist/collab/rebase.js +4 -5
- package/dist/components/chat/ChatComposer.d.ts +0 -4
- package/dist/components/chat/ChatComposer.d.ts.map +1 -1
- package/dist/components/chat/ChatPanel.d.ts +2 -3
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/chat/markdown.d.ts +2 -3
- package/dist/components/chat/markdown.d.ts.map +1 -1
- package/dist/components/editor/PagesModal.d.ts +0 -2
- package/dist/components/editor/PagesModal.d.ts.map +1 -1
- package/dist/components/editor/index.js +15 -19
- package/dist/components/primitives/EditablePlainText.d.ts.map +1 -1
- package/dist/components/primitives/index.js +11 -15
- package/dist/components/sections/brand-guide.js +10 -13
- package/dist/components/sections/register-schemas.js +16 -21
- package/dist/components/sections/register.js +16 -21
- package/dist/components/shell/AudienceChips.d.ts +1 -3
- package/dist/components/shell/AudienceChips.d.ts.map +1 -1
- package/dist/components/shell/AudienceDetailsPanel.d.ts +3 -1
- package/dist/components/shell/AudienceDetailsPanel.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts +0 -34
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
- package/dist/hooks/useSortableNavRow.d.ts +1 -1
- package/dist/hooks/useSortableRow.d.ts +1 -1
- package/dist/index.js +83 -90
- package/dist/lib/index-ops.js +5 -6
- package/dist/lib/index.js +27 -32
- package/dist/lib/pages.d.ts +0 -2
- package/dist/lib/pages.d.ts.map +1 -1
- package/dist/lib/sidecars.js +2 -2
- package/dist/lib/site-validation.js +13 -14
- package/dist/lib/storage-tier.js +4 -4
- package/dist/schemas/index.js +42 -45
- package/dist/schemas/site-config.d.ts +1 -277
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/package.json +2 -42
- package/src/components/chat/ChatComposer.tsx +0 -4
- package/src/components/chat/ChatPanel.tsx +2 -3
- package/src/components/chat/markdown.ts +2 -3
- package/src/components/editor/PagesModal.tsx +1 -15
- package/src/components/editor/StatusIndicator.tsx +4 -4
- package/src/components/primitives/EditablePlainText.tsx +27 -3
- package/src/components/shell/AudienceChips.tsx +1 -3
- package/src/components/shell/AudienceDetailsPanel.tsx +6 -4
- package/src/components/shell/EditorShell.tsx +18 -114
- package/src/components/shell/SiteSettingsModal.tsx +0 -11
- package/src/lib/pages.ts +0 -22
- package/src/schemas/index.ts +0 -4
- package/src/schemas/site-config.ts +4 -32
- package/dist/chunk-4TZJZU2T.js +0 -92
- package/dist/chunk-DIMDRBFK.js +0 -11
- package/dist/chunk-FV7JQRCN.js +0 -94
- package/dist/chunk-IIDKW7AV.js +0 -41
- package/dist/chunk-JCDZAFX2.js +0 -224
- package/dist/chunk-MM7KZS5C.js +0 -30
- package/dist/chunk-QQ3S3P5P.js +0 -38
- package/dist/chunk-TEOAKGZA.js +0 -784
- package/dist/chunk-VAYZJKVR.js +0 -209
- package/dist/components/shell/DocInspector.d.ts +0 -18
- package/dist/components/shell/DocInspector.d.ts.map +0 -1
- package/dist/components/shell/DocumentEditorOverlay.d.ts +0 -29
- package/dist/components/shell/DocumentEditorOverlay.d.ts.map +0 -1
- package/dist/components/shell/DocumentPresentOverlay.d.ts +0 -30
- package/dist/components/shell/DocumentPresentOverlay.d.ts.map +0 -1
- package/dist/components/shell/SiteSettingsDecks.d.ts +0 -13
- package/dist/components/shell/SiteSettingsDecks.d.ts.map +0 -1
- package/dist/deck/DeckComponentView.d.ts +0 -13
- package/dist/deck/DeckComponentView.d.ts.map +0 -1
- package/dist/deck/DeckPlayer.d.ts +0 -26
- package/dist/deck/DeckPlayer.d.ts.map +0 -1
- package/dist/deck/DeckSlideView.d.ts +0 -17
- package/dist/deck/DeckSlideView.d.ts.map +0 -1
- package/dist/deck/DeckStage.d.ts +0 -16
- package/dist/deck/DeckStage.d.ts.map +0 -1
- package/dist/deck/components/DeckImage.d.ts +0 -14
- package/dist/deck/components/DeckImage.d.ts.map +0 -1
- package/dist/deck/components/DeckImageGrid.d.ts +0 -27
- package/dist/deck/components/DeckImageGrid.d.ts.map +0 -1
- package/dist/deck/components/DeckRichText.d.ts +0 -22
- package/dist/deck/components/DeckRichText.d.ts.map +0 -1
- package/dist/deck/components/DeckVideo.d.ts +0 -15
- package/dist/deck/components/DeckVideo.d.ts.map +0 -1
- package/dist/deck/editing.d.ts +0 -50
- package/dist/deck/editing.d.ts.map +0 -1
- package/dist/deck/flip.d.ts +0 -11
- package/dist/deck/flip.d.ts.map +0 -1
- package/dist/deck/index.d.ts +0 -24
- package/dist/deck/index.d.ts.map +0 -1
- package/dist/deck/index.js +0 -51
- package/dist/deck/layouts/CenteredTextLayout.d.ts +0 -12
- package/dist/deck/layouts/CenteredTextLayout.d.ts.map +0 -1
- package/dist/deck/layouts/ColumnSplitLayout.d.ts +0 -14
- package/dist/deck/layouts/ColumnSplitLayout.d.ts.map +0 -1
- package/dist/deck/layouts/FullBleedImageLayout.d.ts +0 -14
- package/dist/deck/layouts/FullBleedImageLayout.d.ts.map +0 -1
- package/dist/deck/media.d.ts +0 -33
- package/dist/deck/media.d.ts.map +0 -1
- package/dist/deck/tokens.d.ts +0 -22
- package/dist/deck/tokens.d.ts.map +0 -1
- package/dist/deck/useStageScale.d.ts +0 -10
- package/dist/deck/useStageScale.d.ts.map +0 -1
- package/dist/deck-builder/DeckBuilderApp.d.ts +0 -30
- package/dist/deck-builder/DeckBuilderApp.d.ts.map +0 -1
- package/dist/deck-builder/DeckBuilderTopBar.d.ts +0 -24
- package/dist/deck-builder/DeckBuilderTopBar.d.ts.map +0 -1
- package/dist/deck-builder/DeckChatPanel.d.ts +0 -45
- package/dist/deck-builder/DeckChatPanel.d.ts.map +0 -1
- package/dist/deck-builder/DeckEditor.d.ts +0 -37
- package/dist/deck-builder/DeckEditor.d.ts.map +0 -1
- package/dist/deck-builder/DeckLibrary.d.ts +0 -22
- package/dist/deck-builder/DeckLibrary.d.ts.map +0 -1
- package/dist/deck-builder/DeckSlideSurface.d.ts +0 -31
- package/dist/deck-builder/DeckSlideSurface.d.ts.map +0 -1
- package/dist/deck-builder/deck-api.d.ts +0 -76
- package/dist/deck-builder/deck-api.d.ts.map +0 -1
- package/dist/deck-builder/deck-chat-client.d.ts +0 -44
- package/dist/deck-builder/deck-chat-client.d.ts.map +0 -1
- package/dist/deck-builder/deck-chat-history.d.ts +0 -10
- package/dist/deck-builder/deck-chat-history.d.ts.map +0 -1
- package/dist/deck-builder/index.d.ts +0 -13
- package/dist/deck-builder/index.d.ts.map +0 -1
- package/dist/deck-builder/index.js +0 -1498
- package/dist/deck-builder/starter-deck.d.ts +0 -23
- package/dist/deck-builder/starter-deck.d.ts.map +0 -1
- package/dist/deck-builder/types.d.ts +0 -29
- package/dist/deck-builder/types.d.ts.map +0 -1
- package/dist/deck-builder/useSortableSlide.d.ts +0 -29
- package/dist/deck-builder/useSortableSlide.d.ts.map +0 -1
- package/dist/document/assets/fonts/albra-grotesk-bold.woff +0 -0
- package/dist/document/assets/fonts/albra-grotesk-regular.woff +0 -0
- package/dist/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
- package/dist/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
- package/dist/document/components/DocCoverPage.d.ts +0 -11
- package/dist/document/components/DocCoverPage.d.ts.map +0 -1
- package/dist/document/components/DocPage.d.ts +0 -10
- package/dist/document/components/DocPage.d.ts.map +0 -1
- package/dist/document/components/DocRail.d.ts +0 -28
- package/dist/document/components/DocRail.d.ts.map +0 -1
- package/dist/document/components/DocSectionPage.d.ts +0 -12
- package/dist/document/components/DocSectionPage.d.ts.map +0 -1
- package/dist/document/components/DocViewer.d.ts +0 -21
- package/dist/document/components/DocViewer.d.ts.map +0 -1
- package/dist/document/components/Wordmark.d.ts +0 -4
- package/dist/document/components/Wordmark.d.ts.map +0 -1
- package/dist/document/components/measure.d.ts +0 -7
- package/dist/document/components/measure.d.ts.map +0 -1
- package/dist/document/components/useDocPages.d.ts +0 -12
- package/dist/document/components/useDocPages.d.ts.map +0 -1
- package/dist/document/engine/blocks.d.ts +0 -2
- package/dist/document/engine/blocks.d.ts.map +0 -1
- package/dist/document/engine/harvest.d.ts +0 -3
- package/dist/document/engine/harvest.d.ts.map +0 -1
- package/dist/document/engine/masonry.d.ts +0 -26
- package/dist/document/engine/masonry.d.ts.map +0 -1
- package/dist/document/engine/paginate.d.ts +0 -43
- package/dist/document/engine/paginate.d.ts.map +0 -1
- package/dist/document/engine/settings.d.ts +0 -91
- package/dist/document/engine/settings.d.ts.map +0 -1
- package/dist/document/engine/types.d.ts +0 -44
- package/dist/document/engine/types.d.ts.map +0 -1
- package/dist/document/index.d.ts +0 -13
- package/dist/document/index.d.ts.map +0 -1
- package/dist/document/index.js +0 -1030
- package/dist/document/styles/document.css +0 -217
- package/dist/lib/deck-access.d.ts +0 -86
- package/dist/lib/deck-access.d.ts.map +0 -1
- package/dist/lib/deck-access.js +0 -58
- package/dist/lib/deck-dexie.d.ts +0 -88
- package/dist/lib/deck-dexie.d.ts.map +0 -1
- package/dist/lib/deck-dexie.js +0 -22
- package/dist/lib/deck-edit.d.ts +0 -67
- package/dist/lib/deck-edit.d.ts.map +0 -1
- package/dist/lib/deck-media-assets.d.ts +0 -27
- package/dist/lib/deck-media-assets.d.ts.map +0 -1
- package/dist/lib/deck-media-assets.js +0 -25
- package/dist/lib/deck-media-refs.d.ts +0 -35
- package/dist/lib/deck-media-refs.d.ts.map +0 -1
- package/dist/lib/deck-media-refs.js +0 -36
- package/dist/lib/deck-tool.d.ts +0 -25
- package/dist/lib/deck-tool.d.ts.map +0 -1
- package/dist/lib/deck-tool.js +0 -58
- package/dist/lib/document-input.d.ts +0 -25
- package/dist/lib/document-input.d.ts.map +0 -1
- package/dist/schemas/deck-meta.d.ts +0 -85
- package/dist/schemas/deck-meta.d.ts.map +0 -1
- package/dist/schemas/deck-meta.js +0 -64
- package/dist/schemas/deck.d.ts +0 -954
- package/dist/schemas/deck.d.ts.map +0 -1
- package/dist/schemas/deck.js +0 -65
- package/src/components/shell/DocInspector.tsx +0 -314
- package/src/components/shell/DocumentEditorOverlay.tsx +0 -179
- package/src/components/shell/DocumentPresentOverlay.tsx +0 -87
- package/src/components/shell/SiteSettingsDecks.tsx +0 -41
- package/src/deck/DeckComponentView.tsx +0 -28
- package/src/deck/DeckPlayer.tsx +0 -166
- package/src/deck/DeckSlideView.tsx +0 -38
- package/src/deck/DeckStage.tsx +0 -39
- package/src/deck/components/DeckImage.tsx +0 -38
- package/src/deck/components/DeckImageGrid.tsx +0 -254
- package/src/deck/components/DeckRichText.tsx +0 -111
- package/src/deck/components/DeckVideo.tsx +0 -81
- package/src/deck/editing.tsx +0 -201
- package/src/deck/flip.ts +0 -70
- package/src/deck/index.ts +0 -30
- package/src/deck/layouts/CenteredTextLayout.tsx +0 -29
- package/src/deck/layouts/ColumnSplitLayout.tsx +0 -69
- package/src/deck/layouts/FullBleedImageLayout.tsx +0 -58
- package/src/deck/media.tsx +0 -46
- package/src/deck/tokens.ts +0 -57
- package/src/deck/useStageScale.ts +0 -44
- package/src/deck-builder/DeckBuilderApp.tsx +0 -572
- package/src/deck-builder/DeckBuilderTopBar.tsx +0 -62
- package/src/deck-builder/DeckChatPanel.tsx +0 -334
- package/src/deck-builder/DeckEditor.tsx +0 -258
- package/src/deck-builder/DeckLibrary.tsx +0 -195
- package/src/deck-builder/DeckSlideSurface.tsx +0 -55
- package/src/deck-builder/deck-api.ts +0 -177
- package/src/deck-builder/deck-chat-client.ts +0 -129
- package/src/deck-builder/deck-chat-history.ts +0 -62
- package/src/deck-builder/index.ts +0 -12
- package/src/deck-builder/starter-deck.ts +0 -86
- package/src/deck-builder/types.ts +0 -24
- package/src/deck-builder/useSortableSlide.ts +0 -102
- package/src/document/assets/fonts/albra-grotesk-bold.woff +0 -0
- package/src/document/assets/fonts/albra-grotesk-regular.woff +0 -0
- package/src/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
- package/src/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
- package/src/document/components/DocCoverPage.tsx +0 -45
- package/src/document/components/DocPage.tsx +0 -40
- package/src/document/components/DocRail.tsx +0 -189
- package/src/document/components/DocSectionPage.tsx +0 -48
- package/src/document/components/DocViewer.tsx +0 -313
- package/src/document/components/Wordmark.tsx +0 -18
- package/src/document/components/measure.tsx +0 -133
- package/src/document/components/useDocPages.ts +0 -95
- package/src/document/engine/blocks.ts +0 -116
- package/src/document/engine/harvest.ts +0 -95
- package/src/document/engine/masonry.ts +0 -158
- package/src/document/engine/paginate.ts +0 -232
- package/src/document/engine/settings.ts +0 -125
- package/src/document/engine/types.ts +0 -64
- package/src/document/index.ts +0 -18
- package/src/document/styles/document.css +0 -217
- package/src/lib/deck-access.ts +0 -132
- package/src/lib/deck-dexie.ts +0 -190
- package/src/lib/deck-edit.ts +0 -196
- package/src/lib/deck-media-assets.ts +0 -57
- package/src/lib/deck-media-refs.ts +0 -67
- package/src/lib/deck-tool.ts +0 -95
- package/src/lib/document-input.ts +0 -57
- package/src/schemas/deck-meta.ts +0 -88
- package/src/schemas/deck.ts +0 -478
- package/dist/{chunk-DJABADOO.js → chunk-AJ2QOAZI.js} +3 -3
|
@@ -1,1498 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DeckMediaProvider,
|
|
3
|
-
DeckSlideView,
|
|
4
|
-
DeckStage,
|
|
5
|
-
DeckTextEditProvider
|
|
6
|
-
} from "../chunk-TEOAKGZA.js";
|
|
7
|
-
import {
|
|
8
|
-
Popover,
|
|
9
|
-
PopoverItem,
|
|
10
|
-
Tooltip
|
|
11
|
-
} from "../chunk-JCDZAFX2.js";
|
|
12
|
-
import {
|
|
13
|
-
computeReorderTarget
|
|
14
|
-
} from "../chunk-DIMDRBFK.js";
|
|
15
|
-
import {
|
|
16
|
-
Button
|
|
17
|
-
} from "../chunk-GGYY3JQJ.js";
|
|
18
|
-
import {
|
|
19
|
-
IconButton
|
|
20
|
-
} from "../chunk-IIDKW7AV.js";
|
|
21
|
-
import {
|
|
22
|
-
deleteDeckDraft,
|
|
23
|
-
getDeckDraft,
|
|
24
|
-
hasUnpublishedChanges,
|
|
25
|
-
initDeckDraftStore,
|
|
26
|
-
listDeckDrafts,
|
|
27
|
-
markDeckDraftPublished,
|
|
28
|
-
putDeckDraft,
|
|
29
|
-
recordDeckPublish
|
|
30
|
-
} from "../chunk-FV7JQRCN.js";
|
|
31
|
-
import {
|
|
32
|
-
formatTimestamp
|
|
33
|
-
} from "../chunk-MM7KZS5C.js";
|
|
34
|
-
import {
|
|
35
|
-
ensureSanitizer,
|
|
36
|
-
sanitizeHtml
|
|
37
|
-
} from "../chunk-QQ3S3P5P.js";
|
|
38
|
-
import {
|
|
39
|
-
cn
|
|
40
|
-
} from "../chunk-7EOSDFS3.js";
|
|
41
|
-
import {
|
|
42
|
-
DECK_SCHEMA_VERSION,
|
|
43
|
-
DeckSchema,
|
|
44
|
-
parseDeck
|
|
45
|
-
} from "../chunk-VAYZJKVR.js";
|
|
46
|
-
import "../chunk-JSBRDJBE.js";
|
|
47
|
-
|
|
48
|
-
// src/deck-builder/DeckBuilderApp.tsx
|
|
49
|
-
import { MessageSquareIcon, PlusIcon as PlusIcon2 } from "lucide-react";
|
|
50
|
-
import { useCallback as useCallback2, useEffect as useEffect5, useMemo as useMemo3, useRef as useRef5, useState as useState5 } from "react";
|
|
51
|
-
|
|
52
|
-
// src/deck-builder/DeckChatPanel.tsx
|
|
53
|
-
import { ChevronRight, SquarePen } from "lucide-react";
|
|
54
|
-
import { useCallback, useEffect as useEffect2, useRef as useRef2, useState } from "react";
|
|
55
|
-
|
|
56
|
-
// src/components/chat/ChatComposer.tsx
|
|
57
|
-
import { useEffect, useRef } from "react";
|
|
58
|
-
import { Square } from "lucide-react";
|
|
59
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
60
|
-
function ChatComposer({
|
|
61
|
-
value,
|
|
62
|
-
onChange,
|
|
63
|
-
onSend,
|
|
64
|
-
onStop,
|
|
65
|
-
busy,
|
|
66
|
-
placeholder,
|
|
67
|
-
disabled,
|
|
68
|
-
className,
|
|
69
|
-
textareaRef,
|
|
70
|
-
maxLength = 4e3
|
|
71
|
-
}) {
|
|
72
|
-
const ownRef = useRef(null);
|
|
73
|
-
const ref = textareaRef ?? ownRef;
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
const el = ref.current;
|
|
76
|
-
if (!el) return;
|
|
77
|
-
el.style.height = "auto";
|
|
78
|
-
el.style.height = `${el.scrollHeight}px`;
|
|
79
|
-
}, [value, ref]);
|
|
80
|
-
function onKeyDown(e) {
|
|
81
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
82
|
-
e.preventDefault();
|
|
83
|
-
onSend();
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return /* @__PURE__ */ jsxs(
|
|
87
|
-
"form",
|
|
88
|
-
{
|
|
89
|
-
className: cn("flex shrink-0 items-end gap-2 border-t border-base-200 p-2", className),
|
|
90
|
-
onSubmit: (e) => {
|
|
91
|
-
e.preventDefault();
|
|
92
|
-
onSend();
|
|
93
|
-
},
|
|
94
|
-
children: [
|
|
95
|
-
/* @__PURE__ */ jsx(
|
|
96
|
-
"textarea",
|
|
97
|
-
{
|
|
98
|
-
ref,
|
|
99
|
-
rows: 1,
|
|
100
|
-
value,
|
|
101
|
-
onChange: (e) => onChange(e.target.value),
|
|
102
|
-
onKeyDown,
|
|
103
|
-
placeholder,
|
|
104
|
-
maxLength,
|
|
105
|
-
disabled,
|
|
106
|
-
className: "max-h-[5.625rem] min-w-0 flex-1 resize-none overflow-y-auto rounded-inner border border-base-200 bg-base px-2 py-1 text-sm text-base-contrast focus:bg-[color-mix(in_srgb,var(--color-base),var(--color-base-contrast)_5%)] focus:outline-none disabled:opacity-60"
|
|
107
|
-
}
|
|
108
|
-
),
|
|
109
|
-
busy && onStop ? /* @__PURE__ */ jsx(
|
|
110
|
-
"button",
|
|
111
|
-
{
|
|
112
|
-
type: "button",
|
|
113
|
-
"aria-label": "Stop",
|
|
114
|
-
onClick: onStop,
|
|
115
|
-
className: "flex cursor-pointer items-center rounded-inner bg-primary px-3 py-1.5 text-sm text-primary-contrast",
|
|
116
|
-
children: /* @__PURE__ */ jsx(Square, { size: 12, fill: "currentColor", "aria-hidden": "true" })
|
|
117
|
-
}
|
|
118
|
-
) : /* @__PURE__ */ jsx(
|
|
119
|
-
"button",
|
|
120
|
-
{
|
|
121
|
-
type: "submit",
|
|
122
|
-
"aria-label": "Send",
|
|
123
|
-
disabled: busy || disabled,
|
|
124
|
-
className: "rounded-inner bg-primary px-3 py-1 text-sm text-primary-contrast disabled:opacity-50",
|
|
125
|
-
children: "\u2192"
|
|
126
|
-
}
|
|
127
|
-
)
|
|
128
|
-
]
|
|
129
|
-
}
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// src/components/chat/markdown.ts
|
|
134
|
-
import { marked } from "marked";
|
|
135
|
-
function assistantHtml(md) {
|
|
136
|
-
return sanitizeHtml(marked.parse(md, { async: false, gfm: true, breaks: true }));
|
|
137
|
-
}
|
|
138
|
-
var MD_STYLES = "[&_p:not(:first-child)]:mt-2 [&_ul]:mt-1 [&_ul]:list-disc [&_ul]:pl-4 [&_ol]:mt-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_li:not(:first-child)]:mt-1 [&_a]:underline [&_a]:underline-offset-2 [&_a]:text-primary [&_code]:rounded-inner [&_code]:bg-base-200 [&_code]:px-1 [&_code]:font-mono [&_code]:text-[0.8em] [&_pre]:mt-2 [&_pre]:overflow-x-auto [&_pre]:rounded-inner [&_pre]:bg-base-200 [&_pre]:p-2 [&_h1]:mt-2 [&_h1]:font-semibold [&_h2]:mt-2 [&_h2]:font-semibold [&_h3]:mt-2 [&_h3]:font-semibold [&_h4]:mt-2 [&_h4]:font-semibold [&_blockquote]:mt-2 [&_blockquote]:border-l-2 [&_blockquote]:border-base-200 [&_blockquote]:pl-2";
|
|
139
|
-
var USER_BUBBLE_CLASS = "ml-auto max-w-[85%] whitespace-pre-wrap rounded-outer bg-primary px-3 py-2 text-sm text-primary-contrast";
|
|
140
|
-
|
|
141
|
-
// src/deck-builder/deck-chat-client.ts
|
|
142
|
-
var DECK_CHAT_ERROR_COPY = {
|
|
143
|
-
rate_limited: "You've hit the deck-building limit \u2014 try again later.",
|
|
144
|
-
disabled: "Deck building isn't available on this site right now.",
|
|
145
|
-
not_authorized: "You don't have access to build decks here.",
|
|
146
|
-
invalid_deck: "The assistant couldn't produce a valid deck. Your deck is unchanged \u2014 try describing the change differently.",
|
|
147
|
-
// The opposite problem, and it must not read like the one above: this deck is
|
|
148
|
-
// the one already staged on this device, so rewording the request changes
|
|
149
|
-
// nothing. Deliberately prescribes NO fix — "reopen it" is a dead end for a
|
|
150
|
-
// draft that was never published, since there is no server copy to re-read
|
|
151
|
-
// and the open path never clobbers a local draft anyway. A real repair
|
|
152
|
-
// affordance (discard this draft) is #39-shaped.
|
|
153
|
-
bad_deck: "The copy of this deck saved on this device isn't valid, so the assistant can't work on it.",
|
|
154
|
-
stream_truncated: "The assistant's deck was too long to finish. Your deck is unchanged \u2014 try asking for fewer slides.",
|
|
155
|
-
upstream_error: "Something went wrong \u2014 please try again."
|
|
156
|
-
};
|
|
157
|
-
var RETRYABLE = /* @__PURE__ */ new Set(["rate_limited", "upstream_error", "invalid_deck", "stream_truncated"]);
|
|
158
|
-
function deckChatCanRetry(error) {
|
|
159
|
-
return RETRYABLE.has(error);
|
|
160
|
-
}
|
|
161
|
-
async function streamDeckChat(body2, handlers, signal) {
|
|
162
|
-
let res;
|
|
163
|
-
try {
|
|
164
|
-
res = await fetch("/api/deck-chat", {
|
|
165
|
-
method: "POST",
|
|
166
|
-
headers: { "Content-Type": "application/json" },
|
|
167
|
-
body: JSON.stringify(body2),
|
|
168
|
-
signal
|
|
169
|
-
});
|
|
170
|
-
} catch {
|
|
171
|
-
if (signal?.aborted) return { ok: true, deck: null, truncated: false };
|
|
172
|
-
return { ok: false, error: "upstream_error" };
|
|
173
|
-
}
|
|
174
|
-
if (!res.ok || !res.body) {
|
|
175
|
-
const err = await res.json().catch(() => ({}));
|
|
176
|
-
return { ok: false, error: err.error ?? "upstream_error", details: err.details, retryAfterMs: err.retryAfterMs };
|
|
177
|
-
}
|
|
178
|
-
const reader = res.body.getReader();
|
|
179
|
-
const decoder = new TextDecoder();
|
|
180
|
-
let buffer = "";
|
|
181
|
-
let deck = null;
|
|
182
|
-
let truncated = false;
|
|
183
|
-
let failure2 = null;
|
|
184
|
-
try {
|
|
185
|
-
for (; ; ) {
|
|
186
|
-
const { done, value } = await reader.read();
|
|
187
|
-
if (done) break;
|
|
188
|
-
buffer += decoder.decode(value, { stream: true });
|
|
189
|
-
let sep;
|
|
190
|
-
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
191
|
-
const frame = buffer.slice(0, sep);
|
|
192
|
-
buffer = buffer.slice(sep + 2);
|
|
193
|
-
const event = /^event: (.+)$/m.exec(frame)?.[1];
|
|
194
|
-
const raw = /^data: (.+)$/m.exec(frame)?.[1];
|
|
195
|
-
if (!event || !raw) continue;
|
|
196
|
-
let data;
|
|
197
|
-
try {
|
|
198
|
-
data = JSON.parse(raw);
|
|
199
|
-
} catch {
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
if (event === "text" && typeof data.text === "string") handlers.onText(data.text);
|
|
203
|
-
else if (event === "retry") handlers.onRetry(data.attempt ?? 0, data.issues ?? []);
|
|
204
|
-
else if (event === "deck") {
|
|
205
|
-
const parsed = parseDeck(data.deck);
|
|
206
|
-
if (parsed.ok) deck = parsed.deck;
|
|
207
|
-
else failure2 = { error: "invalid_deck", details: parsed.errors };
|
|
208
|
-
} else if (event === "error") failure2 = { error: data.error ?? "upstream_error", details: data.details };
|
|
209
|
-
else if (event === "done") truncated = data.truncated === true;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
} catch {
|
|
213
|
-
if (!signal?.aborted) return { ok: false, error: "upstream_error" };
|
|
214
|
-
}
|
|
215
|
-
if (deck) return { ok: true, deck, truncated };
|
|
216
|
-
if (failure2) return { ok: false, ...failure2 };
|
|
217
|
-
return { ok: true, deck: null, truncated };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// src/deck-builder/deck-chat-history.ts
|
|
221
|
-
var KEY_PREFIX = "bp-deck-chat";
|
|
222
|
-
var DECK_CHAT_MESSAGE_CAP = 60;
|
|
223
|
-
function key(scope, deckId) {
|
|
224
|
-
return `${KEY_PREFIX}:${scope}:${deckId}`;
|
|
225
|
-
}
|
|
226
|
-
function isMessage(m) {
|
|
227
|
-
return !!m && typeof m === "object" && (m.role === "user" || m.role === "assistant") && typeof m.content === "string";
|
|
228
|
-
}
|
|
229
|
-
function loadDeckChat(scope, deckId) {
|
|
230
|
-
try {
|
|
231
|
-
const parsed = JSON.parse(window.localStorage.getItem(key(scope, deckId)) ?? "[]");
|
|
232
|
-
return Array.isArray(parsed) ? parsed.filter(isMessage).slice(-DECK_CHAT_MESSAGE_CAP) : [];
|
|
233
|
-
} catch {
|
|
234
|
-
return [];
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
function saveDeckChat(scope, deckId, messages) {
|
|
238
|
-
const stored = messages.filter((m) => m.content !== "").slice(-DECK_CHAT_MESSAGE_CAP);
|
|
239
|
-
try {
|
|
240
|
-
if (stored.length === 0) window.localStorage.removeItem(key(scope, deckId));
|
|
241
|
-
else window.localStorage.setItem(key(scope, deckId), JSON.stringify(stored));
|
|
242
|
-
} catch {
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
function clearDeckChat(scope, deckId) {
|
|
246
|
-
try {
|
|
247
|
-
window.localStorage.removeItem(key(scope, deckId));
|
|
248
|
-
} catch {
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// src/deck-builder/DeckChatPanel.tsx
|
|
253
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
254
|
-
function DeckChatPanel({ deck, getDeck, scope, onDeck, onBusyChange, disabled = false, onClose }) {
|
|
255
|
-
const [messages, setMessages] = useState([]);
|
|
256
|
-
const [input, setInput] = useState("");
|
|
257
|
-
const [busy, setBusy] = useState(false);
|
|
258
|
-
const [updating, setUpdating] = useState(false);
|
|
259
|
-
const [notice, setNotice] = useState(null);
|
|
260
|
-
const [canRetry, setCanRetry] = useState(false);
|
|
261
|
-
const [mdReady, setMdReady] = useState(false);
|
|
262
|
-
const streamingRef = useRef2("");
|
|
263
|
-
const abortRef = useRef2(null);
|
|
264
|
-
const listRef = useRef2(null);
|
|
265
|
-
const composerRef = useRef2(null);
|
|
266
|
-
const runGenRef = useRef2(0);
|
|
267
|
-
const deckRef = useRef2(deck);
|
|
268
|
-
deckRef.current = deck;
|
|
269
|
-
const getDeckRef = useRef2(getDeck);
|
|
270
|
-
getDeckRef.current = getDeck;
|
|
271
|
-
const deckId = deck.id;
|
|
272
|
-
const freshDeck = useCallback(() => {
|
|
273
|
-
const fresh = getDeckRef.current?.();
|
|
274
|
-
return fresh !== null && fresh !== void 0 && fresh.id === deckId ? fresh : deckRef.current;
|
|
275
|
-
}, [deckId]);
|
|
276
|
-
useEffect2(() => {
|
|
277
|
-
runGenRef.current++;
|
|
278
|
-
setMessages(loadDeckChat(scope, deckId));
|
|
279
|
-
setNotice(null);
|
|
280
|
-
setCanRetry(false);
|
|
281
|
-
}, [scope, deckId]);
|
|
282
|
-
useEffect2(() => {
|
|
283
|
-
let alive = true;
|
|
284
|
-
void ensureSanitizer().then(() => {
|
|
285
|
-
if (alive) setMdReady(true);
|
|
286
|
-
}).catch(() => {
|
|
287
|
-
});
|
|
288
|
-
return () => {
|
|
289
|
-
alive = false;
|
|
290
|
-
};
|
|
291
|
-
}, []);
|
|
292
|
-
useEffect2(() => {
|
|
293
|
-
const el = listRef.current;
|
|
294
|
-
if (el) el.scrollTop = el.scrollHeight;
|
|
295
|
-
}, [messages, updating]);
|
|
296
|
-
useEffect2(() => {
|
|
297
|
-
onBusyChange?.(busy);
|
|
298
|
-
}, [busy, onBusyChange]);
|
|
299
|
-
const run = useCallback(
|
|
300
|
-
async (history) => {
|
|
301
|
-
const gen = ++runGenRef.current;
|
|
302
|
-
const superseded = () => runGenRef.current !== gen;
|
|
303
|
-
setNotice(null);
|
|
304
|
-
setCanRetry(false);
|
|
305
|
-
setBusy(true);
|
|
306
|
-
setUpdating(false);
|
|
307
|
-
setMessages([...history, { role: "assistant", content: "" }]);
|
|
308
|
-
streamingRef.current = "";
|
|
309
|
-
const controller = new AbortController();
|
|
310
|
-
abortRef.current = controller;
|
|
311
|
-
const result = await streamDeckChat(
|
|
312
|
-
// The getter wins when it answers for THIS deck; a mismatch means the
|
|
313
|
-
// shell has moved on and the render-time prop is the honest answer.
|
|
314
|
-
{ deck: freshDeck(), messages: history },
|
|
315
|
-
{
|
|
316
|
-
onText: (delta) => {
|
|
317
|
-
if (superseded()) return;
|
|
318
|
-
streamingRef.current += delta;
|
|
319
|
-
setMessages([...history, { role: "assistant", content: streamingRef.current }]);
|
|
320
|
-
},
|
|
321
|
-
onRetry: () => {
|
|
322
|
-
if (superseded()) return;
|
|
323
|
-
streamingRef.current = "";
|
|
324
|
-
setUpdating(true);
|
|
325
|
-
setMessages([...history, { role: "assistant", content: "" }]);
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
controller.signal
|
|
329
|
-
);
|
|
330
|
-
abortRef.current = null;
|
|
331
|
-
setBusy(false);
|
|
332
|
-
setUpdating(false);
|
|
333
|
-
if (superseded()) return;
|
|
334
|
-
if (!result.ok) {
|
|
335
|
-
setMessages(history);
|
|
336
|
-
saveDeckChat(scope, deckId, history);
|
|
337
|
-
setNotice(DECK_CHAT_ERROR_COPY[result.error] ?? DECK_CHAT_ERROR_COPY.upstream_error);
|
|
338
|
-
setCanRetry(deckChatCanRetry(result.error));
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
const reply = streamingRef.current || (result.deck ? "Updated the deck." : "");
|
|
342
|
-
const finished = reply ? [...history, { role: "assistant", content: reply }] : history;
|
|
343
|
-
setMessages(finished);
|
|
344
|
-
saveDeckChat(scope, deckId, finished);
|
|
345
|
-
if (result.deck) onDeck(result.deck);
|
|
346
|
-
if (result.truncated) setNotice("Some brand content didn't fit \u2014 the assistant may not know everything on this site.");
|
|
347
|
-
},
|
|
348
|
-
[deckId, freshDeck, onDeck, scope]
|
|
349
|
-
);
|
|
350
|
-
const send = useCallback(
|
|
351
|
-
(override) => {
|
|
352
|
-
const text = (override ?? input).trim();
|
|
353
|
-
if (!text || busy || disabled) return;
|
|
354
|
-
if (override === void 0) setInput("");
|
|
355
|
-
void run([...messages, { role: "user", content: text }]);
|
|
356
|
-
},
|
|
357
|
-
[busy, disabled, input, messages, run]
|
|
358
|
-
);
|
|
359
|
-
function retry() {
|
|
360
|
-
if (busy || messages.length === 0 || messages[messages.length - 1].role !== "user") return;
|
|
361
|
-
void run(messages);
|
|
362
|
-
}
|
|
363
|
-
function newConversation() {
|
|
364
|
-
if (busy) abortRef.current?.abort();
|
|
365
|
-
runGenRef.current++;
|
|
366
|
-
setMessages([]);
|
|
367
|
-
setNotice(null);
|
|
368
|
-
setCanRetry(false);
|
|
369
|
-
clearDeckChat(scope, deckId);
|
|
370
|
-
composerRef.current?.focus();
|
|
371
|
-
}
|
|
372
|
-
const lastIndex = messages.length - 1;
|
|
373
|
-
return /* @__PURE__ */ jsxs2(
|
|
374
|
-
"aside",
|
|
375
|
-
{
|
|
376
|
-
"data-deck-chat": "",
|
|
377
|
-
"aria-label": "Deck assistant",
|
|
378
|
-
className: "flex w-[var(--chat-w)] shrink-0 flex-col border-l border-base-200 bg-base",
|
|
379
|
-
children: [
|
|
380
|
-
/* @__PURE__ */ jsxs2("div", { className: "flex h-16 shrink-0 items-center justify-between gap-2 border-b border-base-200 px-3", children: [
|
|
381
|
-
/* @__PURE__ */ jsx2("span", { className: "text-sm font-medium text-base-contrast", children: "Deck assistant" }),
|
|
382
|
-
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1", children: [
|
|
383
|
-
messages.length > 0 && /* @__PURE__ */ jsx2(
|
|
384
|
-
IconButton,
|
|
385
|
-
{
|
|
386
|
-
icon: /* @__PURE__ */ jsx2(SquarePen, { size: 16 }),
|
|
387
|
-
label: "New conversation",
|
|
388
|
-
onClick: newConversation,
|
|
389
|
-
className: "rounded-inner hover:bg-base-accent"
|
|
390
|
-
}
|
|
391
|
-
),
|
|
392
|
-
/* @__PURE__ */ jsx2(
|
|
393
|
-
IconButton,
|
|
394
|
-
{
|
|
395
|
-
icon: /* @__PURE__ */ jsx2(ChevronRight, { size: 18 }),
|
|
396
|
-
label: "Hide deck assistant",
|
|
397
|
-
onClick: onClose,
|
|
398
|
-
className: "rounded-inner hover:bg-base-accent"
|
|
399
|
-
}
|
|
400
|
-
)
|
|
401
|
-
] })
|
|
402
|
-
] }),
|
|
403
|
-
/* @__PURE__ */ jsxs2("div", { ref: listRef, role: "log", "aria-label": "Conversation", className: "min-h-0 flex-1 space-y-3 overflow-y-auto p-3", children: [
|
|
404
|
-
messages.length === 0 && /* @__PURE__ */ jsxs2("div", { className: "flex h-full flex-col justify-end gap-2 pb-1", children: [
|
|
405
|
-
/* @__PURE__ */ jsx2("p", { className: "text-sm text-base-contrast-light", children: "Describe the deck you need and I'll build it from this portal's brand content." }),
|
|
406
|
-
/* @__PURE__ */ jsx2("div", { className: "mt-1 flex flex-wrap gap-1.5", children: ["Build a five-slide brand overview", "Add a slide about our color palette"].map((suggestion) => /* @__PURE__ */ jsx2(
|
|
407
|
-
"button",
|
|
408
|
-
{
|
|
409
|
-
type: "button",
|
|
410
|
-
onClick: () => send(suggestion),
|
|
411
|
-
disabled,
|
|
412
|
-
className: "cursor-pointer rounded-full border border-base-200 bg-base-accent px-2.5 py-1 text-xs text-base-contrast hover:border-primary disabled:cursor-default disabled:opacity-50",
|
|
413
|
-
children: suggestion
|
|
414
|
-
},
|
|
415
|
-
suggestion
|
|
416
|
-
)) })
|
|
417
|
-
] }),
|
|
418
|
-
messages.map(
|
|
419
|
-
(m, i) => m.role === "user" ? /* @__PURE__ */ jsx2("div", { className: USER_BUBBLE_CLASS, children: m.content }, i) : m.content === "" ? busy && i === lastIndex ? /* @__PURE__ */ jsx2("div", { className: "text-sm text-base-contrast-light motion-safe:animate-pulse", children: updating ? "Fixing the deck\u2026" : "Working on your deck\u2026" }, i) : null : mdReady ? /* @__PURE__ */ jsx2(
|
|
420
|
-
"div",
|
|
421
|
-
{
|
|
422
|
-
className: cn("text-sm leading-relaxed text-base-contrast", MD_STYLES),
|
|
423
|
-
dangerouslySetInnerHTML: { __html: assistantHtml(m.content) }
|
|
424
|
-
},
|
|
425
|
-
i
|
|
426
|
-
) : /* @__PURE__ */ jsx2("div", { className: "text-sm leading-relaxed whitespace-pre-wrap text-base-contrast", children: m.content }, i)
|
|
427
|
-
),
|
|
428
|
-
notice && /* @__PURE__ */ jsxs2("p", { className: "text-xs text-base-contrast-light", children: [
|
|
429
|
-
notice,
|
|
430
|
-
canRetry && !busy && /* @__PURE__ */ jsx2(
|
|
431
|
-
"button",
|
|
432
|
-
{
|
|
433
|
-
type: "button",
|
|
434
|
-
onClick: retry,
|
|
435
|
-
className: "ml-2 cursor-pointer text-primary underline underline-offset-2",
|
|
436
|
-
children: "Try again"
|
|
437
|
-
}
|
|
438
|
-
)
|
|
439
|
-
] })
|
|
440
|
-
] }),
|
|
441
|
-
/* @__PURE__ */ jsx2("p", { "aria-live": "polite", className: "sr-only", children: busy ? updating ? "Fixing the deck" : "Updating the deck" : "" }),
|
|
442
|
-
/* @__PURE__ */ jsx2(
|
|
443
|
-
ChatComposer,
|
|
444
|
-
{
|
|
445
|
-
value: input,
|
|
446
|
-
onChange: setInput,
|
|
447
|
-
onSend: () => send(),
|
|
448
|
-
onStop: () => abortRef.current?.abort(),
|
|
449
|
-
busy,
|
|
450
|
-
disabled,
|
|
451
|
-
placeholder: "Describe the change\u2026",
|
|
452
|
-
textareaRef: composerRef
|
|
453
|
-
}
|
|
454
|
-
),
|
|
455
|
-
/* @__PURE__ */ jsx2("p", { className: "shrink-0 border-t border-base-200 px-3 py-1 text-[10px] text-base-contrast-light", children: "Deck drafts stage on this device until you publish." })
|
|
456
|
-
]
|
|
457
|
-
}
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// src/lib/deck-edit.ts
|
|
462
|
-
function unresolved(path) {
|
|
463
|
-
return {
|
|
464
|
-
ok: false,
|
|
465
|
-
errors: [`no editable text at ${path.slot}.${path.field}${path.line === void 0 ? "" : `[${path.line}]`}`]
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
function richTextAt(slide, slot) {
|
|
469
|
-
const { layout } = slide;
|
|
470
|
-
if (layout.type === "centeredText") {
|
|
471
|
-
return slot === "content" && layout.content.type === "richText" ? layout.content : null;
|
|
472
|
-
}
|
|
473
|
-
if (layout.type === "fullBleedImage") {
|
|
474
|
-
return slot === "overlay" ? layout.overlay?.content ?? null : null;
|
|
475
|
-
}
|
|
476
|
-
if (slot !== "left" && slot !== "right") return null;
|
|
477
|
-
const component = layout[slot];
|
|
478
|
-
return component.type === "richText" ? component : null;
|
|
479
|
-
}
|
|
480
|
-
function withRichTextAt(slide, slot, next) {
|
|
481
|
-
const { layout } = slide;
|
|
482
|
-
if (layout.type === "centeredText") {
|
|
483
|
-
if (slot !== "content" || layout.content.type !== "richText") return null;
|
|
484
|
-
return { ...slide, layout: { ...layout, content: next } };
|
|
485
|
-
}
|
|
486
|
-
if (layout.type === "fullBleedImage") {
|
|
487
|
-
if (slot !== "overlay" || layout.overlay === void 0) return null;
|
|
488
|
-
return { ...slide, layout: { ...layout, overlay: { ...layout.overlay, content: next } } };
|
|
489
|
-
}
|
|
490
|
-
if (slot !== "left" && slot !== "right") return null;
|
|
491
|
-
if (layout[slot].type !== "richText") return null;
|
|
492
|
-
return { ...slide, layout: { ...layout, [slot]: next } };
|
|
493
|
-
}
|
|
494
|
-
function withLeafText(content, path, text) {
|
|
495
|
-
if (path.field === "kicker" || path.field === "heading" || path.field === "subheading") {
|
|
496
|
-
if (content[path.field] === void 0) return null;
|
|
497
|
-
return { ...content, [path.field]: text };
|
|
498
|
-
}
|
|
499
|
-
const lines = content[path.field];
|
|
500
|
-
if (lines === void 0 || path.line === void 0 || path.line < 0 || path.line >= lines.length) return null;
|
|
501
|
-
const nextLines = lines.slice();
|
|
502
|
-
const line = nextLines[path.line];
|
|
503
|
-
if (path.span === void 0) {
|
|
504
|
-
if (typeof line !== "string") return null;
|
|
505
|
-
nextLines[path.line] = text;
|
|
506
|
-
} else {
|
|
507
|
-
if (typeof line === "string" || path.span < 0 || path.span >= line.length) return null;
|
|
508
|
-
const spans = line.slice();
|
|
509
|
-
spans[path.span] = { ...spans[path.span], text };
|
|
510
|
-
nextLines[path.line] = spans;
|
|
511
|
-
}
|
|
512
|
-
return { ...content, [path.field]: nextLines };
|
|
513
|
-
}
|
|
514
|
-
function applyDeckTextEdit(deck, slideIndex, path, text) {
|
|
515
|
-
const slide = deck.slides[slideIndex];
|
|
516
|
-
if (slide === void 0) return { ok: false, errors: [`no slide at index ${slideIndex}`] };
|
|
517
|
-
const content = richTextAt(slide, path.slot);
|
|
518
|
-
if (content === null) return unresolved(path);
|
|
519
|
-
const nextContent = withLeafText(content, path, text);
|
|
520
|
-
if (nextContent === null) return unresolved(path);
|
|
521
|
-
const nextSlide = withRichTextAt(slide, path.slot, nextContent);
|
|
522
|
-
if (nextSlide === null) return unresolved(path);
|
|
523
|
-
const slides = deck.slides.slice();
|
|
524
|
-
slides[slideIndex] = nextSlide;
|
|
525
|
-
return parseDeck({ ...deck, slides });
|
|
526
|
-
}
|
|
527
|
-
function moveDeckSlide(deck, from, to) {
|
|
528
|
-
if (from < 0 || from >= deck.slides.length) return { ok: false, errors: [`no slide at index ${from}`] };
|
|
529
|
-
if (to < 0 || to >= deck.slides.length) return { ok: false, errors: [`cannot move a slide to index ${to}`] };
|
|
530
|
-
const slides = deck.slides.slice();
|
|
531
|
-
const [moved] = slides.splice(from, 1);
|
|
532
|
-
slides.splice(to, 0, moved);
|
|
533
|
-
return parseDeck({ ...deck, slides });
|
|
534
|
-
}
|
|
535
|
-
function copySlideId(sourceId, taken) {
|
|
536
|
-
const stem = sourceId.replace(/-copy(-\d+)?$/, "").slice(0, 52).replace(/-+$/, "") || "slide";
|
|
537
|
-
let candidate = `${stem}-copy`;
|
|
538
|
-
for (let n = 2; taken.has(candidate); n++) candidate = `${stem}-copy-${n}`;
|
|
539
|
-
return candidate;
|
|
540
|
-
}
|
|
541
|
-
function duplicateDeckSlide(deck, index) {
|
|
542
|
-
const source = deck.slides[index];
|
|
543
|
-
if (source === void 0) return { ok: false, errors: [`no slide at index ${index}`] };
|
|
544
|
-
const copy = {
|
|
545
|
-
...structuredClone(source),
|
|
546
|
-
id: copySlideId(source.id, new Set(deck.slides.map((s) => s.id)))
|
|
547
|
-
};
|
|
548
|
-
const slides = deck.slides.slice();
|
|
549
|
-
slides.splice(index + 1, 0, copy);
|
|
550
|
-
return parseDeck({ ...deck, slides });
|
|
551
|
-
}
|
|
552
|
-
function removeDeckSlide(deck, index) {
|
|
553
|
-
if (deck.slides[index] === void 0) return { ok: false, errors: [`no slide at index ${index}`] };
|
|
554
|
-
const slides = deck.slides.filter((_, i) => i !== index);
|
|
555
|
-
return parseDeck({ ...deck, slides });
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// src/deck-builder/deck-api.ts
|
|
559
|
-
function metaFrom(source) {
|
|
560
|
-
const { title, slideCount, updatedAt } = source;
|
|
561
|
-
if (typeof title !== "string" || title.length === 0) return null;
|
|
562
|
-
if (typeof slideCount !== "number" || !Number.isFinite(slideCount)) return null;
|
|
563
|
-
if (typeof updatedAt !== "string" || updatedAt.length === 0) return null;
|
|
564
|
-
return { title, slideCount, updatedAt };
|
|
565
|
-
}
|
|
566
|
-
async function body(res) {
|
|
567
|
-
try {
|
|
568
|
-
return await res.json();
|
|
569
|
-
} catch {
|
|
570
|
-
return {};
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
function messageFrom(data, fallback) {
|
|
574
|
-
return typeof data.message === "string" ? data.message : fallback;
|
|
575
|
-
}
|
|
576
|
-
async function failure(res) {
|
|
577
|
-
const data = await body(res);
|
|
578
|
-
switch (res.status) {
|
|
579
|
-
case 400:
|
|
580
|
-
return {
|
|
581
|
-
ok: false,
|
|
582
|
-
kind: "invalid",
|
|
583
|
-
details: Array.isArray(data.details) ? data.details : [messageFrom(data, "The deck is not valid.")]
|
|
584
|
-
};
|
|
585
|
-
case 401:
|
|
586
|
-
case 403:
|
|
587
|
-
return { ok: false, kind: "forbidden", message: messageFrom(data, "You are not allowed to do that.") };
|
|
588
|
-
case 404:
|
|
589
|
-
return { ok: false, kind: "notFound" };
|
|
590
|
-
case 409:
|
|
591
|
-
return {
|
|
592
|
-
ok: false,
|
|
593
|
-
kind: "conflict",
|
|
594
|
-
currentVersion: typeof data.currentVersion === "string" ? data.currentVersion : null,
|
|
595
|
-
message: messageFrom(data, "This deck changed since you loaded it.")
|
|
596
|
-
};
|
|
597
|
-
case 503:
|
|
598
|
-
return { ok: false, kind: "unavailable", message: "The deck store is busy. Try again in a moment." };
|
|
599
|
-
default:
|
|
600
|
-
return { ok: false, kind: "failed", message: messageFrom(data, `Request failed (${res.status}).`) };
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
function transportFailure() {
|
|
604
|
-
return { ok: false, kind: "unavailable", message: "Could not reach the portal. Check your connection and try again." };
|
|
605
|
-
}
|
|
606
|
-
async function listPublishedDecks() {
|
|
607
|
-
let res;
|
|
608
|
-
try {
|
|
609
|
-
res = await fetch("/api/decks");
|
|
610
|
-
} catch {
|
|
611
|
-
return transportFailure();
|
|
612
|
-
}
|
|
613
|
-
if (!res.ok) return failure(res);
|
|
614
|
-
const data = await body(res);
|
|
615
|
-
const raw = Array.isArray(data.decks) ? data.decks : [];
|
|
616
|
-
return {
|
|
617
|
-
ok: true,
|
|
618
|
-
value: raw.flatMap((entry) => {
|
|
619
|
-
const id = typeof entry.id === "string" ? entry.id : null;
|
|
620
|
-
const version = typeof entry.version === "string" ? entry.version : null;
|
|
621
|
-
if (!id || !version) return [];
|
|
622
|
-
return [{ id, version, meta: entry.invalid === true ? null : metaFrom(entry) }];
|
|
623
|
-
})
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
async function readPublishedDeck(id) {
|
|
627
|
-
let res;
|
|
628
|
-
try {
|
|
629
|
-
res = await fetch(`/api/decks/${encodeURIComponent(id)}`);
|
|
630
|
-
} catch {
|
|
631
|
-
return transportFailure();
|
|
632
|
-
}
|
|
633
|
-
if (!res.ok) return failure(res);
|
|
634
|
-
const data = await body(res);
|
|
635
|
-
const parsed = parseDeck(data.deck);
|
|
636
|
-
if (!parsed.ok) return { ok: false, kind: "invalid", details: parsed.errors };
|
|
637
|
-
if (typeof data.version !== "string") return { ok: false, kind: "failed", message: "The deck store returned no version." };
|
|
638
|
-
const meta = data.meta !== null && typeof data.meta === "object" ? metaFrom(data.meta) : null;
|
|
639
|
-
return { ok: true, value: { deck: parsed.deck, version: data.version, meta } };
|
|
640
|
-
}
|
|
641
|
-
async function publishDeck(deck, baseVersion) {
|
|
642
|
-
let res;
|
|
643
|
-
try {
|
|
644
|
-
res = await fetch("/api/decks", {
|
|
645
|
-
method: "POST",
|
|
646
|
-
headers: { "Content-Type": "application/json" },
|
|
647
|
-
body: JSON.stringify({ deck, baseVersion })
|
|
648
|
-
});
|
|
649
|
-
} catch {
|
|
650
|
-
return transportFailure();
|
|
651
|
-
}
|
|
652
|
-
if (!res.ok) return failure(res);
|
|
653
|
-
const data = await body(res);
|
|
654
|
-
if (typeof data.version !== "string") return { ok: false, kind: "failed", message: "The deck store returned no version." };
|
|
655
|
-
const meta = data.meta !== null && typeof data.meta === "object" ? metaFrom(data.meta) : null;
|
|
656
|
-
return { ok: true, value: { version: data.version, meta } };
|
|
657
|
-
}
|
|
658
|
-
async function deletePublishedDeck(id) {
|
|
659
|
-
let res;
|
|
660
|
-
try {
|
|
661
|
-
res = await fetch(`/api/decks/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
662
|
-
} catch {
|
|
663
|
-
return transportFailure();
|
|
664
|
-
}
|
|
665
|
-
if (res.status === 404) return { ok: true, value: true };
|
|
666
|
-
if (!res.ok) return failure(res);
|
|
667
|
-
return { ok: true, value: true };
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// src/deck-builder/DeckBuilderTopBar.tsx
|
|
671
|
-
import { ArrowLeftIcon, LayoutGridIcon } from "lucide-react";
|
|
672
|
-
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
673
|
-
function DeckBuilderTopBar({ primaryAction, status, exitHref, onBackToLibrary }) {
|
|
674
|
-
return /* @__PURE__ */ jsx3("div", { className: "shrink-0 border-b border-base-200 bg-base", children: /* @__PURE__ */ jsxs3("div", { className: "mx-auto grid h-16 max-w-screen-xl grid-cols-3 items-center gap-4 px-4", children: [
|
|
675
|
-
/* @__PURE__ */ jsx3("div", { className: "flex min-w-0 items-center gap-2", children: primaryAction }),
|
|
676
|
-
/* @__PURE__ */ jsx3("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx3("p", { className: "truncate text-xs text-base-contrast-light", children: status }) }),
|
|
677
|
-
/* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-end gap-2", children: [
|
|
678
|
-
onBackToLibrary && /* @__PURE__ */ jsx3(Tooltip, { side: "bottom", content: "Deck library", children: /* @__PURE__ */ jsx3(
|
|
679
|
-
IconButton,
|
|
680
|
-
{
|
|
681
|
-
icon: /* @__PURE__ */ jsx3(LayoutGridIcon, { size: 16 }),
|
|
682
|
-
label: "Deck library",
|
|
683
|
-
size: "md",
|
|
684
|
-
onClick: onBackToLibrary,
|
|
685
|
-
className: "border border-base-200 bg-base-accent"
|
|
686
|
-
}
|
|
687
|
-
) }),
|
|
688
|
-
/* @__PURE__ */ jsx3("div", { "aria-hidden": "true", className: "mx-1 h-6 w-px self-center bg-base-200" }),
|
|
689
|
-
/* @__PURE__ */ jsxs3(
|
|
690
|
-
"a",
|
|
691
|
-
{
|
|
692
|
-
href: exitHref,
|
|
693
|
-
className: "inline-flex cursor-pointer items-center justify-center rounded border border-base-200 px-3 py-1.5 text-xs font-medium text-base-contrast transition-colors hover:bg-base-accent",
|
|
694
|
-
children: [
|
|
695
|
-
/* @__PURE__ */ jsx3(ArrowLeftIcon, { size: 14, className: "mr-1.5", "aria-hidden": "true" }),
|
|
696
|
-
"Leave builder"
|
|
697
|
-
]
|
|
698
|
-
}
|
|
699
|
-
)
|
|
700
|
-
] })
|
|
701
|
-
] }) });
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// src/deck-builder/DeckEditor.tsx
|
|
705
|
-
import { ChevronLeftIcon, CopyIcon, MoveLeftIcon, MoveRightIcon, Trash2Icon } from "lucide-react";
|
|
706
|
-
import { useEffect as useEffect4, useMemo as useMemo2, useState as useState3 } from "react";
|
|
707
|
-
|
|
708
|
-
// src/deck-builder/DeckSlideSurface.tsx
|
|
709
|
-
import { useMemo } from "react";
|
|
710
|
-
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
711
|
-
function DeckSlideSurface({ slide, mediaAssets, className, editor }) {
|
|
712
|
-
const resolve = useMemo(
|
|
713
|
-
() => (ref) => ref.kind === "portal" && Object.hasOwn(mediaAssets, ref.id) ? mediaAssets[ref.id] : null,
|
|
714
|
-
[mediaAssets]
|
|
715
|
-
);
|
|
716
|
-
return /* @__PURE__ */ jsx4(DeckStage, { className, children: /* @__PURE__ */ jsx4(DeckMediaProvider, { resolve, children: /* @__PURE__ */ jsx4(DeckTextEditProvider, { editor: editor ?? null, children: /* @__PURE__ */ jsx4(DeckSlideView, { slide }) }) }) });
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// src/deck-builder/useSortableSlide.ts
|
|
720
|
-
import { useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
|
|
721
|
-
var DRAG_TYPE = "deck-filmstrip-slide";
|
|
722
|
-
function useSortableSlide({ index, onReorder, enabled }) {
|
|
723
|
-
const ref = useRef3(null);
|
|
724
|
-
const onReorderRef = useRef3(onReorder);
|
|
725
|
-
onReorderRef.current = onReorder;
|
|
726
|
-
const [isDragging, setIsDragging] = useState2(false);
|
|
727
|
-
const [closestEdge, setClosestEdge] = useState2(null);
|
|
728
|
-
useEffect3(() => {
|
|
729
|
-
if (!enabled) return;
|
|
730
|
-
const element = ref.current;
|
|
731
|
-
if (!element) return;
|
|
732
|
-
let cleanup;
|
|
733
|
-
let cancelled = false;
|
|
734
|
-
void Promise.all([
|
|
735
|
-
import("../adapter-HH47ZPGM.js"),
|
|
736
|
-
import("../closest-edge-EBOXL3YW.js")
|
|
737
|
-
]).then(([{ draggable, dropTargetForElements }, { attachClosestEdge, extractClosestEdge }]) => {
|
|
738
|
-
if (cancelled) return;
|
|
739
|
-
const edgeOf = (data) => {
|
|
740
|
-
const edge = extractClosestEdge(data);
|
|
741
|
-
return edge === "left" || edge === "right" ? edge : null;
|
|
742
|
-
};
|
|
743
|
-
const cleanupDraggable = draggable({
|
|
744
|
-
element,
|
|
745
|
-
getInitialData: () => ({ dragType: DRAG_TYPE, index }),
|
|
746
|
-
onGenerateDragPreview: () => {
|
|
747
|
-
element.style.opacity = "0.4";
|
|
748
|
-
requestAnimationFrame(() => {
|
|
749
|
-
element.style.opacity = "";
|
|
750
|
-
});
|
|
751
|
-
},
|
|
752
|
-
onDragStart: () => setIsDragging(true),
|
|
753
|
-
onDrop: () => setIsDragging(false)
|
|
754
|
-
});
|
|
755
|
-
const cleanupDropTarget = dropTargetForElements({
|
|
756
|
-
element,
|
|
757
|
-
canDrop: ({ source }) => source.data.dragType === DRAG_TYPE,
|
|
758
|
-
getData: ({ input, element: target }) => attachClosestEdge({ index }, { input, element: target, allowedEdges: ["left", "right"] }),
|
|
759
|
-
onDragEnter: ({ self }) => setClosestEdge(edgeOf(self.data)),
|
|
760
|
-
onDrag: ({ self }) => setClosestEdge(edgeOf(self.data)),
|
|
761
|
-
onDragLeave: () => setClosestEdge(null),
|
|
762
|
-
onDrop: ({ source, self }) => {
|
|
763
|
-
setClosestEdge(null);
|
|
764
|
-
const fromIndex = source.data.index;
|
|
765
|
-
const edge = edgeOf(self.data);
|
|
766
|
-
const toIndex = computeReorderTarget(fromIndex, index, edge === "right" ? "bottom" : edge === "left" ? "top" : null);
|
|
767
|
-
if (toIndex !== null) onReorderRef.current(fromIndex, toIndex);
|
|
768
|
-
}
|
|
769
|
-
});
|
|
770
|
-
cleanup = () => {
|
|
771
|
-
cleanupDraggable();
|
|
772
|
-
cleanupDropTarget();
|
|
773
|
-
};
|
|
774
|
-
});
|
|
775
|
-
return () => {
|
|
776
|
-
cancelled = true;
|
|
777
|
-
cleanup?.();
|
|
778
|
-
};
|
|
779
|
-
}, [index, enabled]);
|
|
780
|
-
return { ref, isDragging, closestEdge };
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// src/deck-builder/DeckEditor.tsx
|
|
784
|
-
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
785
|
-
function DeckEditor({
|
|
786
|
-
deck,
|
|
787
|
-
mediaAssets,
|
|
788
|
-
currentSlide,
|
|
789
|
-
onSelectSlide,
|
|
790
|
-
onBack,
|
|
791
|
-
statusLine,
|
|
792
|
-
onEditText,
|
|
793
|
-
onMoveSlide,
|
|
794
|
-
onDuplicateSlide,
|
|
795
|
-
onDeleteSlide,
|
|
796
|
-
locked
|
|
797
|
-
}) {
|
|
798
|
-
const index = Math.min(Math.max(currentSlide, 0), deck.slides.length - 1);
|
|
799
|
-
const slide = deck.slides[index];
|
|
800
|
-
const [confirmingDelete, setConfirmingDelete] = useState3(false);
|
|
801
|
-
useEffect4(() => {
|
|
802
|
-
setConfirmingDelete(false);
|
|
803
|
-
}, [slide.id, index, deck.slides.length, locked]);
|
|
804
|
-
const editor = useMemo2(() => ({ onEditText, disabled: locked }), [onEditText, locked]);
|
|
805
|
-
return /* @__PURE__ */ jsxs4("div", { className: "mx-auto grid w-full max-w-screen-xl gap-3.5 px-4 pt-8 pb-16", children: [
|
|
806
|
-
/* @__PURE__ */ jsxs4(
|
|
807
|
-
"button",
|
|
808
|
-
{
|
|
809
|
-
type: "button",
|
|
810
|
-
onClick: onBack,
|
|
811
|
-
className: "inline-flex cursor-pointer items-center gap-1 self-start text-xs font-medium text-base-contrast-light transition-colors hover:text-base-contrast",
|
|
812
|
-
children: [
|
|
813
|
-
/* @__PURE__ */ jsx5(ChevronLeftIcon, { size: 14, "aria-hidden": "true" }),
|
|
814
|
-
"All decks"
|
|
815
|
-
]
|
|
816
|
-
}
|
|
817
|
-
),
|
|
818
|
-
/* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-2.5", children: [
|
|
819
|
-
/* @__PURE__ */ jsx5("h1", { className: "font-body text-lg font-semibold text-base-contrast", children: deck.title }),
|
|
820
|
-
/* @__PURE__ */ jsx5("p", { className: "text-xs text-base-contrast-light", children: statusLine })
|
|
821
|
-
] }),
|
|
822
|
-
/* @__PURE__ */ jsx5("div", { className: "aspect-video w-[min(100%,calc(52vh*16/9))] overflow-hidden rounded-[var(--radius-outer)] border border-base-200 shadow-md", children: /* @__PURE__ */ jsx5(DeckSlideSurface, { slide, mediaAssets, className: "h-full", editor }) }),
|
|
823
|
-
/* @__PURE__ */ jsx5("p", { "aria-live": "polite", "data-deck-edit-hint": "", className: "min-h-4 text-xs text-base-contrast-light", children: locked ? "Editing is paused while the assistant works on this deck." : "Click any text on the slide to edit it. Drag a thumbnail to reorder." }),
|
|
824
|
-
/* @__PURE__ */ jsx5(
|
|
825
|
-
"ul",
|
|
826
|
-
{
|
|
827
|
-
"aria-label": `${deck.title} slides`,
|
|
828
|
-
className: "flex w-[min(100%,calc(52vh*16/9))] gap-2 overflow-x-auto p-0.5",
|
|
829
|
-
children: deck.slides.map((s, i) => /* @__PURE__ */ jsx5(
|
|
830
|
-
SlideThumb,
|
|
831
|
-
{
|
|
832
|
-
slide: s,
|
|
833
|
-
index: i,
|
|
834
|
-
mediaAssets,
|
|
835
|
-
selected: i === index,
|
|
836
|
-
enabled: !locked,
|
|
837
|
-
onSelect: onSelectSlide,
|
|
838
|
-
onReorder: onMoveSlide
|
|
839
|
-
},
|
|
840
|
-
s.id
|
|
841
|
-
))
|
|
842
|
-
}
|
|
843
|
-
),
|
|
844
|
-
/* @__PURE__ */ jsxs4("div", { className: "flex w-[min(100%,calc(52vh*16/9))] flex-wrap items-center gap-2", children: [
|
|
845
|
-
/* @__PURE__ */ jsxs4("span", { className: "text-xs text-base-contrast-light", children: [
|
|
846
|
-
"Slide ",
|
|
847
|
-
index + 1,
|
|
848
|
-
" of ",
|
|
849
|
-
deck.slides.length
|
|
850
|
-
] }),
|
|
851
|
-
/* @__PURE__ */ jsxs4(
|
|
852
|
-
Button,
|
|
853
|
-
{
|
|
854
|
-
variant: "ghost",
|
|
855
|
-
onClick: () => onMoveSlide(index, index - 1),
|
|
856
|
-
disabled: locked || index === 0,
|
|
857
|
-
"aria-label": "Move slide earlier",
|
|
858
|
-
children: [
|
|
859
|
-
/* @__PURE__ */ jsx5(MoveLeftIcon, { size: 14, className: "mr-1.5", "aria-hidden": "true" }),
|
|
860
|
-
"Earlier"
|
|
861
|
-
]
|
|
862
|
-
}
|
|
863
|
-
),
|
|
864
|
-
/* @__PURE__ */ jsxs4(
|
|
865
|
-
Button,
|
|
866
|
-
{
|
|
867
|
-
variant: "ghost",
|
|
868
|
-
onClick: () => onMoveSlide(index, index + 1),
|
|
869
|
-
disabled: locked || index === deck.slides.length - 1,
|
|
870
|
-
"aria-label": "Move slide later",
|
|
871
|
-
children: [
|
|
872
|
-
/* @__PURE__ */ jsx5(MoveRightIcon, { size: 14, className: "mr-1.5", "aria-hidden": "true" }),
|
|
873
|
-
"Later"
|
|
874
|
-
]
|
|
875
|
-
}
|
|
876
|
-
),
|
|
877
|
-
/* @__PURE__ */ jsxs4(Button, { variant: "ghost", onClick: () => onDuplicateSlide(index), disabled: locked, "aria-label": "Duplicate slide", children: [
|
|
878
|
-
/* @__PURE__ */ jsx5(CopyIcon, { size: 14, className: "mr-1.5", "aria-hidden": "true" }),
|
|
879
|
-
"Duplicate"
|
|
880
|
-
] }),
|
|
881
|
-
confirmingDelete ? /* @__PURE__ */ jsxs4("span", { className: "flex flex-wrap items-center gap-2", children: [
|
|
882
|
-
/* @__PURE__ */ jsxs4("span", { className: "text-xs text-base-contrast", children: [
|
|
883
|
-
"Delete slide ",
|
|
884
|
-
index + 1,
|
|
885
|
-
"?"
|
|
886
|
-
] }),
|
|
887
|
-
/* @__PURE__ */ jsx5(
|
|
888
|
-
Button,
|
|
889
|
-
{
|
|
890
|
-
variant: "destructive",
|
|
891
|
-
onClick: () => {
|
|
892
|
-
setConfirmingDelete(false);
|
|
893
|
-
onDeleteSlide(index);
|
|
894
|
-
},
|
|
895
|
-
children: "Delete"
|
|
896
|
-
}
|
|
897
|
-
),
|
|
898
|
-
/* @__PURE__ */ jsx5(Button, { variant: "ghost", onClick: () => setConfirmingDelete(false), children: "Cancel" })
|
|
899
|
-
] }) : /* @__PURE__ */ jsxs4(
|
|
900
|
-
Button,
|
|
901
|
-
{
|
|
902
|
-
variant: "ghost",
|
|
903
|
-
onClick: () => setConfirmingDelete(true),
|
|
904
|
-
disabled: locked || deck.slides.length === 1,
|
|
905
|
-
"aria-label": "Delete slide",
|
|
906
|
-
children: [
|
|
907
|
-
/* @__PURE__ */ jsx5(Trash2Icon, { size: 14, className: "mr-1.5", "aria-hidden": "true" }),
|
|
908
|
-
"Delete"
|
|
909
|
-
]
|
|
910
|
-
}
|
|
911
|
-
)
|
|
912
|
-
] })
|
|
913
|
-
] });
|
|
914
|
-
}
|
|
915
|
-
function SlideThumb({
|
|
916
|
-
slide,
|
|
917
|
-
index,
|
|
918
|
-
mediaAssets,
|
|
919
|
-
selected,
|
|
920
|
-
enabled,
|
|
921
|
-
onSelect,
|
|
922
|
-
onReorder
|
|
923
|
-
}) {
|
|
924
|
-
const { ref, isDragging, closestEdge } = useSortableSlide({ index, onReorder, enabled });
|
|
925
|
-
return /* @__PURE__ */ jsxs4("li", { ref, className: "relative shrink-0", children: [
|
|
926
|
-
closestEdge && /* @__PURE__ */ jsx5(
|
|
927
|
-
"span",
|
|
928
|
-
{
|
|
929
|
-
"aria-hidden": "true",
|
|
930
|
-
className: cn(
|
|
931
|
-
"absolute inset-y-0 z-[3] w-0.5 bg-primary",
|
|
932
|
-
closestEdge === "left" ? "-left-1" : "-right-1"
|
|
933
|
-
)
|
|
934
|
-
}
|
|
935
|
-
),
|
|
936
|
-
/* @__PURE__ */ jsxs4(
|
|
937
|
-
"div",
|
|
938
|
-
{
|
|
939
|
-
role: "button",
|
|
940
|
-
tabIndex: 0,
|
|
941
|
-
"aria-current": selected ? "true" : void 0,
|
|
942
|
-
"aria-label": slide.title ? `Slide ${index + 1}: ${slide.title}` : `Slide ${index + 1}`,
|
|
943
|
-
onClick: () => onSelect(index),
|
|
944
|
-
onKeyDown: (event) => {
|
|
945
|
-
if (event.key !== "Enter" && event.key !== " ") return;
|
|
946
|
-
event.preventDefault();
|
|
947
|
-
onSelect(index);
|
|
948
|
-
},
|
|
949
|
-
className: cn(
|
|
950
|
-
"relative aspect-video w-30 cursor-pointer overflow-hidden rounded border bg-base-accent transition-colors",
|
|
951
|
-
selected ? "border-primary shadow-[0_0_0_1px_var(--color-primary)]" : "border-base-200 hover:border-base-300",
|
|
952
|
-
isDragging && "opacity-40"
|
|
953
|
-
),
|
|
954
|
-
children: [
|
|
955
|
-
/* @__PURE__ */ jsx5("div", { className: "pointer-events-none h-full", children: /* @__PURE__ */ jsx5(DeckSlideSurface, { slide, mediaAssets, className: "h-full" }) }),
|
|
956
|
-
/* @__PURE__ */ jsx5(
|
|
957
|
-
"span",
|
|
958
|
-
{
|
|
959
|
-
"aria-hidden": "true",
|
|
960
|
-
className: cn(
|
|
961
|
-
"absolute bottom-0 left-0 z-[2] rounded-tr bg-base px-1 py-px text-[0.625rem] font-semibold",
|
|
962
|
-
selected ? "text-primary" : "text-base-contrast-light"
|
|
963
|
-
),
|
|
964
|
-
children: String(index + 1).padStart(2, "0")
|
|
965
|
-
}
|
|
966
|
-
)
|
|
967
|
-
]
|
|
968
|
-
}
|
|
969
|
-
)
|
|
970
|
-
] });
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
// src/deck-builder/DeckLibrary.tsx
|
|
974
|
-
import { LayoutGridIcon as LayoutGridIcon2, MoreVerticalIcon, PlusIcon } from "lucide-react";
|
|
975
|
-
import { useRef as useRef4, useState as useState4 } from "react";
|
|
976
|
-
import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
977
|
-
function DeckLibrary({ entries, mediaAssets, canCreate, onOpen, onCreate, onDelete }) {
|
|
978
|
-
return /* @__PURE__ */ jsx6("div", { className: "mx-auto w-full max-w-screen-xl px-4 pt-8 pb-16", children: entries.length === 0 ? /* @__PURE__ */ jsx6(DeckLibraryEmpty, { canCreate, onCreate }) : /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
979
|
-
/* @__PURE__ */ jsx6("div", { className: "mb-5 flex flex-wrap items-center gap-3", children: /* @__PURE__ */ jsx6("h1", { className: "font-body text-lg font-semibold text-base-contrast", children: "Your decks" }) }),
|
|
980
|
-
/* @__PURE__ */ jsx6("ul", { className: "grid grid-cols-[repeat(auto-fill,minmax(16rem,1fr))] gap-x-4 gap-y-5", children: entries.map((entry) => /* @__PURE__ */ jsx6(
|
|
981
|
-
DeckCard,
|
|
982
|
-
{
|
|
983
|
-
entry,
|
|
984
|
-
mediaAssets,
|
|
985
|
-
onOpen: () => onOpen(entry.id),
|
|
986
|
-
onDelete: () => onDelete(entry.id)
|
|
987
|
-
},
|
|
988
|
-
entry.id
|
|
989
|
-
)) })
|
|
990
|
-
] }) });
|
|
991
|
-
}
|
|
992
|
-
function DeckLibraryEmpty({ canCreate, onCreate }) {
|
|
993
|
-
return /* @__PURE__ */ jsx6("div", { className: "grid min-h-[55vh] place-items-center px-4 py-12 text-center", children: /* @__PURE__ */ jsxs5("div", { className: "grid max-w-md justify-items-center gap-4", children: [
|
|
994
|
-
/* @__PURE__ */ jsx6("div", { className: "grid aspect-video w-44 place-items-center rounded border border-dashed border-base-300 bg-base-accent", children: /* @__PURE__ */ jsx6(LayoutGridIcon2, { size: 16, className: "text-base-300", "aria-hidden": "true" }) }),
|
|
995
|
-
/* @__PURE__ */ jsx6("h1", { className: "font-body text-lg font-semibold text-base-contrast", children: "No decks yet" }),
|
|
996
|
-
/* @__PURE__ */ jsx6("p", { className: "text-sm leading-relaxed text-base-contrast-light", children: "Decks are 16:9 presentations built from your brand's layouts. Start one and it stays on this device until you publish it." }),
|
|
997
|
-
canCreate && /* @__PURE__ */ jsxs5(Button, { size: "md", onClick: onCreate, children: [
|
|
998
|
-
/* @__PURE__ */ jsx6(PlusIcon, { size: 16, className: "mr-1.5", "aria-hidden": "true" }),
|
|
999
|
-
"New deck"
|
|
1000
|
-
] })
|
|
1001
|
-
] }) });
|
|
1002
|
-
}
|
|
1003
|
-
function DeckCard({
|
|
1004
|
-
entry,
|
|
1005
|
-
mediaAssets,
|
|
1006
|
-
onOpen,
|
|
1007
|
-
onDelete
|
|
1008
|
-
}) {
|
|
1009
|
-
const [menuOpen, setMenuOpen] = useState4(false);
|
|
1010
|
-
const [confirming, setConfirming] = useState4(false);
|
|
1011
|
-
const kebabRef = useRef4(null);
|
|
1012
|
-
const firstSlide = entry.draft?.deck.slides[0] ?? null;
|
|
1013
|
-
return /* @__PURE__ */ jsxs5("li", { className: "group relative", children: [
|
|
1014
|
-
/* @__PURE__ */ jsx6(
|
|
1015
|
-
"div",
|
|
1016
|
-
{
|
|
1017
|
-
"aria-hidden": "true",
|
|
1018
|
-
onClick: onOpen,
|
|
1019
|
-
className: "w-full cursor-pointer overflow-hidden rounded border border-base-200 bg-base-accent transition-colors group-hover:border-primary",
|
|
1020
|
-
children: /* @__PURE__ */ jsx6("div", { className: "pointer-events-none aspect-video w-full", children: firstSlide ? /* @__PURE__ */ jsx6(DeckSlideSurface, { slide: firstSlide, mediaAssets, className: "h-full" }) : /* @__PURE__ */ jsx6("div", { className: "grid h-full place-items-center", children: /* @__PURE__ */ jsx6(LayoutGridIcon2, { size: 16, className: "text-base-300", "aria-hidden": "true" }) }) })
|
|
1021
|
-
}
|
|
1022
|
-
),
|
|
1023
|
-
/* @__PURE__ */ jsxs5("div", { className: "flex items-start gap-2 pt-2", children: [
|
|
1024
|
-
/* @__PURE__ */ jsxs5("button", { type: "button", onClick: onOpen, className: "min-w-0 flex-1 cursor-pointer text-left", children: [
|
|
1025
|
-
/* @__PURE__ */ jsx6("span", { className: "block truncate text-sm font-medium text-base-contrast", children: entry.title }),
|
|
1026
|
-
/* @__PURE__ */ jsxs5("span", { className: "mt-0.5 block text-xs text-base-contrast-light", children: [
|
|
1027
|
-
entry.slideCount,
|
|
1028
|
-
" ",
|
|
1029
|
-
entry.slideCount === 1 ? "slide" : "slides",
|
|
1030
|
-
" \xB7 ",
|
|
1031
|
-
formatTimestamp(entry.updatedAt)
|
|
1032
|
-
] }),
|
|
1033
|
-
/* @__PURE__ */ jsxs5("span", { className: "mt-2 flex flex-wrap items-center gap-1.5", children: [
|
|
1034
|
-
/* @__PURE__ */ jsx6(DeckStatusChip, { label: "Private", dotClassName: "bg-[var(--color-status-draft-text)]" }),
|
|
1035
|
-
entry.unpublishedChanges && /* @__PURE__ */ jsx6(DeckStatusChip, { label: "Unpublished changes", dotClassName: "bg-[var(--color-status-modified-text)]" })
|
|
1036
|
-
] })
|
|
1037
|
-
] }),
|
|
1038
|
-
/* @__PURE__ */ jsx6(
|
|
1039
|
-
IconButton,
|
|
1040
|
-
{
|
|
1041
|
-
ref: kebabRef,
|
|
1042
|
-
icon: /* @__PURE__ */ jsx6(MoreVerticalIcon, { size: 16 }),
|
|
1043
|
-
label: `Actions for ${entry.title}`,
|
|
1044
|
-
onClick: () => setMenuOpen((v) => !v),
|
|
1045
|
-
"aria-haspopup": "menu",
|
|
1046
|
-
"aria-expanded": menuOpen
|
|
1047
|
-
}
|
|
1048
|
-
),
|
|
1049
|
-
/* @__PURE__ */ jsx6(
|
|
1050
|
-
Popover,
|
|
1051
|
-
{
|
|
1052
|
-
isOpen: menuOpen,
|
|
1053
|
-
onClose: () => {
|
|
1054
|
-
setMenuOpen(false);
|
|
1055
|
-
setConfirming(false);
|
|
1056
|
-
},
|
|
1057
|
-
anchorRef: kebabRef,
|
|
1058
|
-
align: "end",
|
|
1059
|
-
className: "min-w-44 py-1",
|
|
1060
|
-
children: confirming ? /* @__PURE__ */ jsxs5("div", { className: "px-3 py-2", children: [
|
|
1061
|
-
/* @__PURE__ */ jsxs5("p", { className: "text-xs text-base-contrast-light", children: [
|
|
1062
|
-
"Delete \u201C",
|
|
1063
|
-
entry.title,
|
|
1064
|
-
"\u201D? ",
|
|
1065
|
-
entry.published ? "The published deck is removed too." : "It has never been published."
|
|
1066
|
-
] }),
|
|
1067
|
-
/* @__PURE__ */ jsxs5("div", { className: "mt-2 flex justify-end gap-2", children: [
|
|
1068
|
-
/* @__PURE__ */ jsx6(Button, { variant: "ghost", onClick: () => setConfirming(false), children: "Cancel" }),
|
|
1069
|
-
/* @__PURE__ */ jsx6(
|
|
1070
|
-
Button,
|
|
1071
|
-
{
|
|
1072
|
-
variant: "destructive",
|
|
1073
|
-
onClick: () => {
|
|
1074
|
-
setMenuOpen(false);
|
|
1075
|
-
setConfirming(false);
|
|
1076
|
-
onDelete();
|
|
1077
|
-
},
|
|
1078
|
-
children: "Delete"
|
|
1079
|
-
}
|
|
1080
|
-
)
|
|
1081
|
-
] })
|
|
1082
|
-
] }) : /* @__PURE__ */ jsx6(PopoverItem, { className: "text-red-600", onClick: () => setConfirming(true), children: "Delete" })
|
|
1083
|
-
}
|
|
1084
|
-
)
|
|
1085
|
-
] })
|
|
1086
|
-
] });
|
|
1087
|
-
}
|
|
1088
|
-
function DeckStatusChip({ label, dotClassName }) {
|
|
1089
|
-
return /* @__PURE__ */ jsxs5("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", children: [
|
|
1090
|
-
/* @__PURE__ */ jsx6("span", { "aria-hidden": "true", className: cn("h-3 w-3 rounded-full border border-base", dotClassName) }),
|
|
1091
|
-
label
|
|
1092
|
-
] });
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// src/deck-builder/starter-deck.ts
|
|
1096
|
-
function starterDeck(id, title) {
|
|
1097
|
-
return DeckSchema.parse({
|
|
1098
|
-
schemaVersion: DECK_SCHEMA_VERSION,
|
|
1099
|
-
id,
|
|
1100
|
-
title,
|
|
1101
|
-
slides: [
|
|
1102
|
-
{
|
|
1103
|
-
id: "cover",
|
|
1104
|
-
title: "Cover",
|
|
1105
|
-
layout: {
|
|
1106
|
-
type: "centeredText",
|
|
1107
|
-
background: "brand",
|
|
1108
|
-
content: {
|
|
1109
|
-
type: "richText",
|
|
1110
|
-
kicker: "New deck",
|
|
1111
|
-
heading: title,
|
|
1112
|
-
subheading: "Rename this slide's heading, or ask the assistant to rewrite the whole deck."
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
},
|
|
1116
|
-
{
|
|
1117
|
-
id: "context",
|
|
1118
|
-
title: "Context",
|
|
1119
|
-
layout: {
|
|
1120
|
-
type: "centeredText",
|
|
1121
|
-
content: {
|
|
1122
|
-
type: "richText",
|
|
1123
|
-
kicker: "Context",
|
|
1124
|
-
heading: "What this deck is for",
|
|
1125
|
-
paragraphs: [
|
|
1126
|
-
"Every slide uses one of the portal's prebuilt Layouts, so it inherits the brand's type, color and spacing automatically."
|
|
1127
|
-
]
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
},
|
|
1131
|
-
{
|
|
1132
|
-
id: "next",
|
|
1133
|
-
title: "Next",
|
|
1134
|
-
layout: {
|
|
1135
|
-
type: "centeredText",
|
|
1136
|
-
content: {
|
|
1137
|
-
type: "richText",
|
|
1138
|
-
kicker: "Next",
|
|
1139
|
-
heading: "Three things to try",
|
|
1140
|
-
bullets: [
|
|
1141
|
-
"Click a thumbnail below to move through the deck",
|
|
1142
|
-
"Publish when you want the deck kept for you",
|
|
1143
|
-
"Everything you change stays on this device until you do"
|
|
1144
|
-
]
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
]
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1151
|
-
function newDeckId(title) {
|
|
1152
|
-
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
1153
|
-
const suffix = Math.random().toString(36).slice(2, 8).replace(/[^a-z0-9]/g, "0").padEnd(6, "0");
|
|
1154
|
-
return slug ? `${slug}-${suffix}` : `deck-${suffix}`;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
// src/deck-builder/DeckBuilderApp.tsx
|
|
1158
|
-
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1159
|
-
function DeckBuilderApp({ siteId, draftScope, mediaAssets, exitHref, canCreate }) {
|
|
1160
|
-
const [drafts, setDrafts] = useState5([]);
|
|
1161
|
-
const [published, setPublished] = useState5([]);
|
|
1162
|
-
const [openDeckId, setOpenDeckId] = useState5(null);
|
|
1163
|
-
const [slide, setSlide] = useState5(0);
|
|
1164
|
-
const [loading, setLoading] = useState5(true);
|
|
1165
|
-
const [busy, setBusy] = useState5(false);
|
|
1166
|
-
const [notice, setNotice] = useState5(null);
|
|
1167
|
-
const [conflict, setConflict] = useState5(null);
|
|
1168
|
-
const [chatOpen, setChatOpen] = useState5(true);
|
|
1169
|
-
const [chatBusy, setChatBusy] = useState5(false);
|
|
1170
|
-
const alive = useRef5(true);
|
|
1171
|
-
const editBase = useRef5(null);
|
|
1172
|
-
useEffect5(() => {
|
|
1173
|
-
alive.current = true;
|
|
1174
|
-
return () => {
|
|
1175
|
-
alive.current = false;
|
|
1176
|
-
};
|
|
1177
|
-
}, []);
|
|
1178
|
-
const refreshDrafts = useCallback2(async () => {
|
|
1179
|
-
const rows = await listDeckDrafts();
|
|
1180
|
-
if (alive.current) setDrafts(rows);
|
|
1181
|
-
}, []);
|
|
1182
|
-
useEffect5(() => {
|
|
1183
|
-
initDeckDraftStore(siteId, draftScope);
|
|
1184
|
-
void (async () => {
|
|
1185
|
-
await refreshDrafts();
|
|
1186
|
-
const listed = await listPublishedDecks();
|
|
1187
|
-
if (!alive.current) return;
|
|
1188
|
-
if (listed.ok) setPublished(listed.value);
|
|
1189
|
-
else setNotice(noticeFor(listed, "Could not list your published decks."));
|
|
1190
|
-
setLoading(false);
|
|
1191
|
-
})();
|
|
1192
|
-
}, [siteId, draftScope, refreshDrafts]);
|
|
1193
|
-
useEffect5(() => {
|
|
1194
|
-
editBase.current = null;
|
|
1195
|
-
}, [openDeckId]);
|
|
1196
|
-
const entries = useMemo3(() => buildEntries(drafts, published), [drafts, published]);
|
|
1197
|
-
const openRow = openDeckId === null ? null : drafts.find((row) => row.deckId === openDeckId) ?? null;
|
|
1198
|
-
const openDeck = useCallback2(
|
|
1199
|
-
async (id) => {
|
|
1200
|
-
setNotice(null);
|
|
1201
|
-
setSlide(0);
|
|
1202
|
-
if (await getDeckDraft(id)) {
|
|
1203
|
-
if (alive.current) setOpenDeckId(id);
|
|
1204
|
-
return;
|
|
1205
|
-
}
|
|
1206
|
-
setBusy(true);
|
|
1207
|
-
const read = await readPublishedDeck(id);
|
|
1208
|
-
if (!alive.current) return;
|
|
1209
|
-
setBusy(false);
|
|
1210
|
-
if (!read.ok) {
|
|
1211
|
-
setNotice(noticeFor(read, "Could not open that deck."));
|
|
1212
|
-
return;
|
|
1213
|
-
}
|
|
1214
|
-
await markDeckDraftPublished(read.value.deck, read.value.version, read.value.meta?.updatedAt);
|
|
1215
|
-
await refreshDrafts();
|
|
1216
|
-
if (alive.current) setOpenDeckId(id);
|
|
1217
|
-
},
|
|
1218
|
-
[refreshDrafts]
|
|
1219
|
-
);
|
|
1220
|
-
const createDeck = useCallback2(async () => {
|
|
1221
|
-
setNotice(null);
|
|
1222
|
-
const title = `Untitled deck ${(/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { month: "short", day: "numeric" })}`;
|
|
1223
|
-
const deck = starterDeck(newDeckId(title), title);
|
|
1224
|
-
await putDeckDraft(deck);
|
|
1225
|
-
await refreshDrafts();
|
|
1226
|
-
if (!alive.current) return;
|
|
1227
|
-
setSlide(0);
|
|
1228
|
-
setOpenDeckId(deck.id);
|
|
1229
|
-
}, [refreshDrafts]);
|
|
1230
|
-
const applyDeck = useCallback2(
|
|
1231
|
-
async (next) => {
|
|
1232
|
-
editBase.current = { deckId: next.id, deck: next };
|
|
1233
|
-
await putDeckDraft(next);
|
|
1234
|
-
await refreshDrafts();
|
|
1235
|
-
if (!alive.current) return;
|
|
1236
|
-
setSlide((current) => Math.min(current, next.slides.length - 1));
|
|
1237
|
-
},
|
|
1238
|
-
[refreshDrafts]
|
|
1239
|
-
);
|
|
1240
|
-
const stageEdit = useCallback2(
|
|
1241
|
-
async (result, focus) => {
|
|
1242
|
-
if (!result.ok) {
|
|
1243
|
-
setNotice({ tone: "error", text: "That change did not fit the deck's rules.", details: result.errors });
|
|
1244
|
-
return;
|
|
1245
|
-
}
|
|
1246
|
-
setNotice(null);
|
|
1247
|
-
editBase.current = { deckId: result.deck.id, deck: result.deck };
|
|
1248
|
-
await putDeckDraft(result.deck);
|
|
1249
|
-
await refreshDrafts();
|
|
1250
|
-
if (!alive.current) return;
|
|
1251
|
-
const last = result.deck.slides.length - 1;
|
|
1252
|
-
setSlide((current) => Math.max(0, Math.min(focus ? focus(result.deck) : current, last)));
|
|
1253
|
-
},
|
|
1254
|
-
[refreshDrafts]
|
|
1255
|
-
);
|
|
1256
|
-
const currentDeck = useCallback2(() => {
|
|
1257
|
-
if (openRow === null) return null;
|
|
1258
|
-
const staged = editBase.current;
|
|
1259
|
-
return staged !== null && staged.deckId === openRow.deckId ? staged.deck : openRow.deck;
|
|
1260
|
-
}, [openRow]);
|
|
1261
|
-
const editText = useCallback2(
|
|
1262
|
-
(path, text) => {
|
|
1263
|
-
const deck = currentDeck();
|
|
1264
|
-
if (!deck || chatBusy) return;
|
|
1265
|
-
const index = Math.min(slide, deck.slides.length - 1);
|
|
1266
|
-
void stageEdit(applyDeckTextEdit(deck, index, path, text));
|
|
1267
|
-
},
|
|
1268
|
-
[currentDeck, slide, chatBusy, stageEdit]
|
|
1269
|
-
);
|
|
1270
|
-
const moveSlide = useCallback2(
|
|
1271
|
-
(from, to) => {
|
|
1272
|
-
const deck = currentDeck();
|
|
1273
|
-
if (!deck || chatBusy) return;
|
|
1274
|
-
void stageEdit(moveDeckSlide(deck, from, to), () => to);
|
|
1275
|
-
},
|
|
1276
|
-
[currentDeck, chatBusy, stageEdit]
|
|
1277
|
-
);
|
|
1278
|
-
const duplicateSlide = useCallback2(
|
|
1279
|
-
(index) => {
|
|
1280
|
-
const deck = currentDeck();
|
|
1281
|
-
if (!deck || chatBusy) return;
|
|
1282
|
-
void stageEdit(duplicateDeckSlide(deck, index), () => index + 1);
|
|
1283
|
-
},
|
|
1284
|
-
[currentDeck, chatBusy, stageEdit]
|
|
1285
|
-
);
|
|
1286
|
-
const deleteSlide = useCallback2(
|
|
1287
|
-
(index) => {
|
|
1288
|
-
const deck = currentDeck();
|
|
1289
|
-
if (!deck || chatBusy) return;
|
|
1290
|
-
void stageEdit(removeDeckSlide(deck, index), () => index - 1);
|
|
1291
|
-
},
|
|
1292
|
-
[currentDeck, chatBusy, stageEdit]
|
|
1293
|
-
);
|
|
1294
|
-
const removeDeck = useCallback2(
|
|
1295
|
-
async (id) => {
|
|
1296
|
-
setNotice(null);
|
|
1297
|
-
setBusy(true);
|
|
1298
|
-
if (published.some((p) => p.id === id)) {
|
|
1299
|
-
const deleted = await deletePublishedDeck(id);
|
|
1300
|
-
if (!alive.current) return;
|
|
1301
|
-
if (!deleted.ok) {
|
|
1302
|
-
setBusy(false);
|
|
1303
|
-
setNotice(noticeFor(deleted, "Could not delete that deck."));
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1306
|
-
setPublished((prev) => prev.filter((p) => p.id !== id));
|
|
1307
|
-
}
|
|
1308
|
-
await deleteDeckDraft(id);
|
|
1309
|
-
clearDeckChat(draftScope, id);
|
|
1310
|
-
await refreshDrafts();
|
|
1311
|
-
if (!alive.current) return;
|
|
1312
|
-
setBusy(false);
|
|
1313
|
-
if (openDeckId === id) setOpenDeckId(null);
|
|
1314
|
-
},
|
|
1315
|
-
[published, openDeckId, refreshDrafts, draftScope]
|
|
1316
|
-
);
|
|
1317
|
-
const publish = useCallback2(async () => {
|
|
1318
|
-
if (!openRow) return;
|
|
1319
|
-
setNotice(null);
|
|
1320
|
-
setConflict(null);
|
|
1321
|
-
setBusy(true);
|
|
1322
|
-
const publishing = currentDeck() ?? openRow.deck;
|
|
1323
|
-
const result = await publishDeck(publishing, openRow.baseVersion);
|
|
1324
|
-
if (!alive.current) return;
|
|
1325
|
-
setBusy(false);
|
|
1326
|
-
if (!result.ok) {
|
|
1327
|
-
if (result.kind === "conflict") {
|
|
1328
|
-
setConflict({ deckId: openRow.deckId, message: result.message });
|
|
1329
|
-
return;
|
|
1330
|
-
}
|
|
1331
|
-
setNotice(noticeFor(result, "Could not publish this deck."));
|
|
1332
|
-
return;
|
|
1333
|
-
}
|
|
1334
|
-
await recordDeckPublish(openRow.deckId, publishing, result.value.version);
|
|
1335
|
-
await refreshDrafts();
|
|
1336
|
-
if (!alive.current) return;
|
|
1337
|
-
setPublished((prev) => {
|
|
1338
|
-
const next = prev.filter((p) => p.id !== openRow.deckId);
|
|
1339
|
-
next.push({ id: openRow.deckId, version: result.value.version, meta: result.value.meta });
|
|
1340
|
-
return next;
|
|
1341
|
-
});
|
|
1342
|
-
setNotice({ tone: "info", text: "Published." });
|
|
1343
|
-
}, [openRow, currentDeck, refreshDrafts]);
|
|
1344
|
-
const reloadPublished = useCallback2(async () => {
|
|
1345
|
-
if (!conflict) return;
|
|
1346
|
-
setBusy(true);
|
|
1347
|
-
const read = await readPublishedDeck(conflict.deckId);
|
|
1348
|
-
if (!alive.current) return;
|
|
1349
|
-
setBusy(false);
|
|
1350
|
-
if (!read.ok) {
|
|
1351
|
-
setNotice(noticeFor(read, "Could not reload that deck."));
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
editBase.current = null;
|
|
1355
|
-
await markDeckDraftPublished(read.value.deck, read.value.version, read.value.meta?.updatedAt);
|
|
1356
|
-
await refreshDrafts();
|
|
1357
|
-
if (!alive.current) return;
|
|
1358
|
-
setConflict(null);
|
|
1359
|
-
setSlide(0);
|
|
1360
|
-
setNotice({ tone: "info", text: "Reloaded the published deck. Your staged edits were replaced." });
|
|
1361
|
-
}, [conflict, refreshDrafts]);
|
|
1362
|
-
const dirty = openRow ? hasUnpublishedChanges(openRow) : false;
|
|
1363
|
-
return /* @__PURE__ */ jsxs6("div", { "data-deck-builder-app": "", className: "flex h-screen min-h-0 flex-col bg-base font-body text-base-contrast", children: [
|
|
1364
|
-
/* @__PURE__ */ jsx7(
|
|
1365
|
-
DeckBuilderTopBar,
|
|
1366
|
-
{
|
|
1367
|
-
exitHref,
|
|
1368
|
-
onBackToLibrary: openDeckId === null ? void 0 : () => setOpenDeckId(null),
|
|
1369
|
-
primaryAction: openRow ? (
|
|
1370
|
-
// Locked during a turn: the deck is being rewritten, so publishing
|
|
1371
|
-
// now would commit whichever half of it Dexie held at that instant.
|
|
1372
|
-
/* @__PURE__ */ jsx7(Button, { variant: "brand", onClick: publish, disabled: busy || chatBusy || !dirty, children: dirty ? "Publish deck" : "Published" })
|
|
1373
|
-
) : canCreate ? /* @__PURE__ */ jsxs6(Button, { variant: "brand", onClick: createDeck, disabled: busy, children: [
|
|
1374
|
-
/* @__PURE__ */ jsx7(PlusIcon2, { size: 14, className: "mr-1.5", "aria-hidden": "true" }),
|
|
1375
|
-
"New deck"
|
|
1376
|
-
] }) : null,
|
|
1377
|
-
status: openRow ? `${dirty ? "Draft \xB7 staged on this device" : "Published"} \xB7 ${openRow.deck.slides.length} slides` : loading ? "Loading your decks\u2026" : `${entries.length} ${entries.length === 1 ? "deck" : "decks"}`
|
|
1378
|
-
}
|
|
1379
|
-
),
|
|
1380
|
-
/* @__PURE__ */ jsxs6("div", { className: "flex min-h-0 flex-1", children: [
|
|
1381
|
-
/* @__PURE__ */ jsxs6("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
|
|
1382
|
-
conflict && /* @__PURE__ */ jsxs6("div", { role: "alert", className: "mx-auto flex max-w-screen-xl flex-wrap items-center gap-3 px-4 pt-4", children: [
|
|
1383
|
-
/* @__PURE__ */ jsx7("p", { className: "text-xs text-base-contrast", children: conflict.message }),
|
|
1384
|
-
/* @__PURE__ */ jsx7(Button, { variant: "secondary", onClick: reloadPublished, disabled: busy, children: "Reload the published deck" }),
|
|
1385
|
-
/* @__PURE__ */ jsx7(Button, { variant: "ghost", onClick: () => setConflict(null), children: "Keep my version" })
|
|
1386
|
-
] }),
|
|
1387
|
-
notice && /* @__PURE__ */ jsxs6(
|
|
1388
|
-
"div",
|
|
1389
|
-
{
|
|
1390
|
-
role: notice.tone === "error" ? "alert" : "status",
|
|
1391
|
-
className: "mx-auto max-w-screen-xl px-4 pt-4 text-xs text-base-contrast",
|
|
1392
|
-
children: [
|
|
1393
|
-
/* @__PURE__ */ jsx7("p", { children: notice.text }),
|
|
1394
|
-
notice.details && /* @__PURE__ */ jsx7("ul", { className: "mt-1 list-disc pl-4 text-base-contrast-light", children: notice.details.map((detail) => /* @__PURE__ */ jsx7("li", { children: detail }, detail)) })
|
|
1395
|
-
]
|
|
1396
|
-
}
|
|
1397
|
-
),
|
|
1398
|
-
openRow ? /* @__PURE__ */ jsx7(
|
|
1399
|
-
DeckEditor,
|
|
1400
|
-
{
|
|
1401
|
-
deck: openRow.deck,
|
|
1402
|
-
mediaAssets,
|
|
1403
|
-
currentSlide: slide,
|
|
1404
|
-
onSelectSlide: setSlide,
|
|
1405
|
-
onBack: () => setOpenDeckId(null),
|
|
1406
|
-
onEditText: editText,
|
|
1407
|
-
onMoveSlide: moveSlide,
|
|
1408
|
-
onDuplicateSlide: duplicateSlide,
|
|
1409
|
-
onDeleteSlide: deleteSlide,
|
|
1410
|
-
locked: chatBusy,
|
|
1411
|
-
statusLine: dirty ? openRow.baseVersion === null ? "Never published \u2014 staged on this device" : "Unpublished changes on this device" : "Published"
|
|
1412
|
-
}
|
|
1413
|
-
) : loading ? /* @__PURE__ */ jsx7("p", { className: "mx-auto max-w-screen-xl px-4 pt-8 text-sm text-base-contrast-light", children: "Loading your decks\u2026" }) : /* @__PURE__ */ jsx7(
|
|
1414
|
-
DeckLibrary,
|
|
1415
|
-
{
|
|
1416
|
-
entries,
|
|
1417
|
-
mediaAssets,
|
|
1418
|
-
canCreate,
|
|
1419
|
-
onOpen: (id) => void openDeck(id),
|
|
1420
|
-
onCreate: () => void createDeck(),
|
|
1421
|
-
onDelete: (id) => void removeDeck(id)
|
|
1422
|
-
}
|
|
1423
|
-
)
|
|
1424
|
-
] }),
|
|
1425
|
-
openRow && (chatOpen ? /* @__PURE__ */ jsx7(
|
|
1426
|
-
DeckChatPanel,
|
|
1427
|
-
{
|
|
1428
|
-
deck: openRow.deck,
|
|
1429
|
-
getDeck: currentDeck,
|
|
1430
|
-
scope: draftScope,
|
|
1431
|
-
onDeck: (next) => void applyDeck(next),
|
|
1432
|
-
onBusyChange: setChatBusy,
|
|
1433
|
-
disabled: busy,
|
|
1434
|
-
onClose: () => setChatOpen(false)
|
|
1435
|
-
}
|
|
1436
|
-
) : /* @__PURE__ */ jsx7(
|
|
1437
|
-
"button",
|
|
1438
|
-
{
|
|
1439
|
-
type: "button",
|
|
1440
|
-
onClick: () => setChatOpen(true),
|
|
1441
|
-
"aria-label": "Show deck assistant",
|
|
1442
|
-
className: "fixed right-4 bottom-4 z-50 flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90",
|
|
1443
|
-
children: /* @__PURE__ */ jsx7(MessageSquareIcon, { size: 18, "aria-hidden": "true" })
|
|
1444
|
-
}
|
|
1445
|
-
))
|
|
1446
|
-
] })
|
|
1447
|
-
] });
|
|
1448
|
-
}
|
|
1449
|
-
function buildEntries(drafts, published) {
|
|
1450
|
-
const byId = /* @__PURE__ */ new Map();
|
|
1451
|
-
for (const row of drafts) {
|
|
1452
|
-
byId.set(row.deckId, {
|
|
1453
|
-
id: row.deckId,
|
|
1454
|
-
title: row.deck.title,
|
|
1455
|
-
slideCount: row.deck.slides.length,
|
|
1456
|
-
updatedAt: row.updatedAt,
|
|
1457
|
-
draft: { deck: row.deck, baseVersion: row.baseVersion },
|
|
1458
|
-
published: null,
|
|
1459
|
-
unpublishedChanges: hasUnpublishedChanges(row)
|
|
1460
|
-
});
|
|
1461
|
-
}
|
|
1462
|
-
for (const summary of published) {
|
|
1463
|
-
const existing = byId.get(summary.id);
|
|
1464
|
-
if (existing) {
|
|
1465
|
-
existing.published = { version: summary.version };
|
|
1466
|
-
continue;
|
|
1467
|
-
}
|
|
1468
|
-
byId.set(summary.id, {
|
|
1469
|
-
id: summary.id,
|
|
1470
|
-
title: summary.meta?.title ?? summary.id,
|
|
1471
|
-
slideCount: summary.meta?.slideCount ?? 0,
|
|
1472
|
-
updatedAt: summary.meta?.updatedAt ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1473
|
-
draft: null,
|
|
1474
|
-
published: { version: summary.version },
|
|
1475
|
-
unpublishedChanges: false
|
|
1476
|
-
});
|
|
1477
|
-
}
|
|
1478
|
-
return [...byId.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
1479
|
-
}
|
|
1480
|
-
function noticeFor(failure2, fallback) {
|
|
1481
|
-
switch (failure2.kind) {
|
|
1482
|
-
case "invalid":
|
|
1483
|
-
return { tone: "error", text: "The deck did not pass validation.", details: failure2.details };
|
|
1484
|
-
case "forbidden":
|
|
1485
|
-
return { tone: "error", text: failure2.message };
|
|
1486
|
-
case "notFound":
|
|
1487
|
-
return { tone: "error", text: "That deck is no longer there." };
|
|
1488
|
-
case "conflict":
|
|
1489
|
-
case "unavailable":
|
|
1490
|
-
case "failed":
|
|
1491
|
-
return { tone: "error", text: `${fallback} ${failure2.message}` };
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
export {
|
|
1495
|
-
DeckBuilderApp,
|
|
1496
|
-
newDeckId,
|
|
1497
|
-
starterDeck
|
|
1498
|
-
};
|