@drawnagency/primitives 0.1.111 → 0.1.113
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/auth/types.d.ts +1 -0
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/chunk-4I5OO5UB.js +785 -0
- package/dist/{chunk-7WQJ57XG.js → chunk-6BZ2KNTP.js} +68 -260
- package/dist/{chunk-UVIYI4J6.js → chunk-AHALSCQH.js} +19 -3
- package/dist/{chunk-7MI3HCOF.js → chunk-BDFTMGCL.js} +10 -6
- package/dist/chunk-CXZ5YJG4.js +85 -0
- package/dist/{chunk-RA5IOFPY.js → chunk-DNY72JZN.js} +16 -38
- package/dist/{chunk-3GAG6ZXB.js → chunk-DS4GCYV2.js} +8 -4
- package/dist/{chunk-XQMDWXD6.js → chunk-FRE2CCLV.js} +3 -2
- package/dist/chunk-FV7JQRCN.js +94 -0
- package/dist/chunk-IIDKW7AV.js +41 -0
- package/dist/chunk-IINIZBQB.js +204 -0
- package/dist/{chunk-ELQ4C677.js → chunk-JH762VUH.js} +9 -44
- package/dist/{chunk-OUYB2GCW.js → chunk-K7DYZXVU.js} +1 -57
- package/dist/{chunk-NXGIHSL5.js → chunk-KR2MHTNJ.js} +11 -91
- package/dist/chunk-MM7KZS5C.js +30 -0
- package/dist/{chunk-OTDA6KWL.js → chunk-NHN2LSIU.js} +1 -1
- package/dist/chunk-QQ3S3P5P.js +38 -0
- package/dist/chunk-VAYZJKVR.js +209 -0
- package/dist/{chunk-VCCNKTFD.js → chunk-VWWY5TL5.js} +3 -31
- package/dist/chunk-WUJ3XNFI.js +62 -0
- package/dist/{chunk-ALMEC3XN.js → chunk-Z7W3DQTR.js} +22 -18
- package/dist/collab/rebase.js +1 -1
- package/dist/components/chat/ChatComposer.d.ts +25 -0
- package/dist/components/chat/ChatComposer.d.ts.map +1 -0
- package/dist/components/chat/ChatPanel.d.ts +5 -4
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/chat/ChatWidget.d.ts.map +1 -1
- package/dist/components/chat/markdown.d.ts +14 -0
- package/dist/components/chat/markdown.d.ts.map +1 -0
- package/dist/components/editor/index.js +16 -11
- package/dist/components/primitives/EditablePlainText.d.ts.map +1 -1
- package/dist/components/primitives/index.js +15 -10
- package/dist/components/sections/brand-guide.js +11 -9
- package/dist/components/sections/register-schemas.js +19 -14
- package/dist/components/sections/register.js +19 -14
- package/dist/components/shared/Navigation.d.ts +1 -0
- package/dist/components/shared/Navigation.d.ts.map +1 -1
- package/dist/components/shell/AudienceChips.d.ts +10 -0
- package/dist/components/shell/AudienceChips.d.ts.map +1 -0
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsChatbot.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsDecks.d.ts +13 -0
- package/dist/components/shell/SiteSettingsDecks.d.ts.map +1 -0
- package/dist/components/shell/SiteSettingsDisplay.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsUsers.d.ts.map +1 -1
- package/dist/deck/DeckComponentView.d.ts +13 -0
- package/dist/deck/DeckComponentView.d.ts.map +1 -0
- package/dist/deck/DeckPlayer.d.ts +26 -0
- package/dist/deck/DeckPlayer.d.ts.map +1 -0
- package/dist/deck/DeckSlideView.d.ts +17 -0
- package/dist/deck/DeckSlideView.d.ts.map +1 -0
- package/dist/deck/DeckStage.d.ts +16 -0
- package/dist/deck/DeckStage.d.ts.map +1 -0
- package/dist/deck/components/DeckImage.d.ts +14 -0
- package/dist/deck/components/DeckImage.d.ts.map +1 -0
- package/dist/deck/components/DeckImageGrid.d.ts +27 -0
- package/dist/deck/components/DeckImageGrid.d.ts.map +1 -0
- package/dist/deck/components/DeckRichText.d.ts +22 -0
- package/dist/deck/components/DeckRichText.d.ts.map +1 -0
- package/dist/deck/components/DeckVideo.d.ts +15 -0
- package/dist/deck/components/DeckVideo.d.ts.map +1 -0
- package/dist/deck/editing.d.ts +50 -0
- package/dist/deck/editing.d.ts.map +1 -0
- package/dist/deck/flip.d.ts +11 -0
- package/dist/deck/flip.d.ts.map +1 -0
- package/dist/deck/index.d.ts +24 -0
- package/dist/deck/index.d.ts.map +1 -0
- package/dist/deck/index.js +53 -0
- package/dist/deck/layouts/CenteredTextLayout.d.ts +12 -0
- package/dist/deck/layouts/CenteredTextLayout.d.ts.map +1 -0
- package/dist/deck/layouts/ColumnSplitLayout.d.ts +14 -0
- package/dist/deck/layouts/ColumnSplitLayout.d.ts.map +1 -0
- package/dist/deck/layouts/FullBleedImageLayout.d.ts +14 -0
- package/dist/deck/layouts/FullBleedImageLayout.d.ts.map +1 -0
- package/dist/deck/media.d.ts +33 -0
- package/dist/deck/media.d.ts.map +1 -0
- package/dist/deck/tokens.d.ts +22 -0
- package/dist/deck/tokens.d.ts.map +1 -0
- package/dist/deck/useStageScale.d.ts +10 -0
- package/dist/deck/useStageScale.d.ts.map +1 -0
- package/dist/deck-builder/DeckBuilderApp.d.ts +30 -0
- package/dist/deck-builder/DeckBuilderApp.d.ts.map +1 -0
- package/dist/deck-builder/DeckBuilderTopBar.d.ts +24 -0
- package/dist/deck-builder/DeckBuilderTopBar.d.ts.map +1 -0
- package/dist/deck-builder/DeckChatPanel.d.ts +45 -0
- package/dist/deck-builder/DeckChatPanel.d.ts.map +1 -0
- package/dist/deck-builder/DeckEditor.d.ts +37 -0
- package/dist/deck-builder/DeckEditor.d.ts.map +1 -0
- package/dist/deck-builder/DeckLibrary.d.ts +22 -0
- package/dist/deck-builder/DeckLibrary.d.ts.map +1 -0
- package/dist/deck-builder/DeckSlideSurface.d.ts +31 -0
- package/dist/deck-builder/DeckSlideSurface.d.ts.map +1 -0
- package/dist/deck-builder/deck-api.d.ts +76 -0
- package/dist/deck-builder/deck-api.d.ts.map +1 -0
- package/dist/deck-builder/deck-chat-client.d.ts +44 -0
- package/dist/deck-builder/deck-chat-client.d.ts.map +1 -0
- package/dist/deck-builder/deck-chat-history.d.ts +10 -0
- package/dist/deck-builder/deck-chat-history.d.ts.map +1 -0
- package/dist/deck-builder/index.d.ts +13 -0
- package/dist/deck-builder/index.d.ts.map +1 -0
- package/dist/deck-builder/index.js +1505 -0
- package/dist/deck-builder/starter-deck.d.ts +23 -0
- package/dist/deck-builder/starter-deck.d.ts.map +1 -0
- package/dist/deck-builder/types.d.ts +29 -0
- package/dist/deck-builder/types.d.ts.map +1 -0
- package/dist/deck-builder/useSortableSlide.d.ts +29 -0
- package/dist/deck-builder/useSortableSlide.d.ts.map +1 -0
- package/dist/index.js +70 -63
- package/dist/lib/deck-access.d.ts +86 -0
- package/dist/lib/deck-access.d.ts.map +1 -0
- package/dist/lib/deck-access.js +58 -0
- package/dist/lib/deck-dexie.d.ts +88 -0
- package/dist/lib/deck-dexie.d.ts.map +1 -0
- package/dist/lib/deck-dexie.js +22 -0
- package/dist/lib/deck-edit.d.ts +67 -0
- package/dist/lib/deck-edit.d.ts.map +1 -0
- package/dist/lib/deck-media-assets.d.ts +27 -0
- package/dist/lib/deck-media-assets.d.ts.map +1 -0
- package/dist/lib/deck-media-assets.js +25 -0
- package/dist/lib/deck-media-refs.d.ts +35 -0
- package/dist/lib/deck-media-refs.d.ts.map +1 -0
- package/dist/lib/deck-media-refs.js +36 -0
- package/dist/lib/deck-tool.d.ts +25 -0
- package/dist/lib/deck-tool.d.ts.map +1 -0
- package/dist/lib/deck-tool.js +58 -0
- package/dist/lib/index-ops.js +2 -2
- package/dist/lib/index.js +17 -12
- package/dist/lib/nav.d.ts.map +1 -1
- package/dist/media/index.js +3 -3
- package/dist/media/resolve.d.ts +2 -0
- package/dist/media/resolve.d.ts.map +1 -1
- package/dist/schemas/deck-meta.d.ts +85 -0
- package/dist/schemas/deck-meta.d.ts.map +1 -0
- package/dist/schemas/deck-meta.js +64 -0
- package/dist/schemas/deck.d.ts +954 -0
- package/dist/schemas/deck.d.ts.map +1 -0
- package/dist/schemas/deck.js +65 -0
- package/dist/schemas/index.js +39 -37
- package/dist/schemas/site-config.d.ts +14 -1
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/dist/types/database.d.ts +8 -0
- package/dist/types/database.d.ts.map +1 -1
- package/package.json +37 -1
- package/src/auth/types.ts +1 -0
- package/src/components/chat/ChatComposer.tsx +100 -0
- package/src/components/chat/ChatPanel.tsx +20 -69
- package/src/components/chat/ChatWidget.tsx +5 -4
- package/src/components/chat/markdown.ts +32 -0
- package/src/components/primitives/EditablePlainText.tsx +11 -1
- package/src/components/shared/Navigation.tsx +26 -18
- package/src/components/shell/AudienceChips.tsx +30 -0
- package/src/components/shell/EditorShell.tsx +12 -0
- package/src/components/shell/SiteSettingsChatbot.tsx +1 -18
- package/src/components/shell/SiteSettingsDecks.tsx +41 -0
- package/src/components/shell/SiteSettingsDisplay.tsx +10 -1
- package/src/components/shell/SiteSettingsModal.tsx +11 -0
- package/src/components/shell/SiteSettingsUsers.tsx +49 -2
- package/src/deck/DeckComponentView.tsx +28 -0
- package/src/deck/DeckPlayer.tsx +166 -0
- package/src/deck/DeckSlideView.tsx +38 -0
- package/src/deck/DeckStage.tsx +39 -0
- package/src/deck/components/DeckImage.tsx +38 -0
- package/src/deck/components/DeckImageGrid.tsx +254 -0
- package/src/deck/components/DeckRichText.tsx +111 -0
- package/src/deck/components/DeckVideo.tsx +81 -0
- package/src/deck/editing.tsx +201 -0
- package/src/deck/flip.ts +70 -0
- package/src/deck/index.ts +30 -0
- package/src/deck/layouts/CenteredTextLayout.tsx +29 -0
- package/src/deck/layouts/ColumnSplitLayout.tsx +69 -0
- package/src/deck/layouts/FullBleedImageLayout.tsx +58 -0
- package/src/deck/media.tsx +46 -0
- package/src/deck/tokens.ts +57 -0
- package/src/deck/useStageScale.ts +44 -0
- package/src/deck-builder/DeckBuilderApp.tsx +572 -0
- package/src/deck-builder/DeckBuilderTopBar.tsx +62 -0
- package/src/deck-builder/DeckChatPanel.tsx +334 -0
- package/src/deck-builder/DeckEditor.tsx +258 -0
- package/src/deck-builder/DeckLibrary.tsx +195 -0
- package/src/deck-builder/DeckSlideSurface.tsx +55 -0
- package/src/deck-builder/deck-api.ts +177 -0
- package/src/deck-builder/deck-chat-client.ts +129 -0
- package/src/deck-builder/deck-chat-history.ts +62 -0
- package/src/deck-builder/index.ts +12 -0
- package/src/deck-builder/starter-deck.ts +86 -0
- package/src/deck-builder/types.ts +24 -0
- package/src/deck-builder/useSortableSlide.ts +102 -0
- package/src/lib/deck-access.ts +132 -0
- package/src/lib/deck-dexie.ts +190 -0
- package/src/lib/deck-edit.ts +196 -0
- package/src/lib/deck-media-assets.ts +57 -0
- package/src/lib/deck-media-refs.ts +67 -0
- package/src/lib/deck-tool.ts +95 -0
- package/src/lib/nav.ts +21 -1
- package/src/media/resolve.ts +3 -1
- package/src/schemas/deck-meta.ts +88 -0
- package/src/schemas/deck.ts +478 -0
- package/src/schemas/index.ts +4 -0
- package/src/schemas/site-config.ts +27 -1
- package/src/types/database.ts +4 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, type
|
|
2
|
-
import {
|
|
3
|
-
import { ArrowDown, ChevronDown, ChevronRight, History, Pin, PinOff, SquarePen, Square, Trash2 } from "lucide-react";
|
|
1
|
+
import { useEffect, useRef, useState, type MouseEvent } from "react";
|
|
2
|
+
import { ArrowDown, ChevronDown, ChevronRight, History, Pin, PinOff, SquarePen, Trash2 } from "lucide-react";
|
|
4
3
|
import { cn } from "../../lib/cn";
|
|
5
|
-
import { ensureSanitizer
|
|
4
|
+
import { ensureSanitizer } from "../../lib/sanitize";
|
|
5
|
+
import { assistantHtml, MD_STYLES, USER_BUBBLE_CLASS } from "./markdown";
|
|
6
|
+
import { ChatComposer } from "./ChatComposer";
|
|
6
7
|
import { resolveThinkingPhrases } from "../../lib/chatbot";
|
|
7
8
|
import { lockBodyScroll } from "../../lib/scroll-lock";
|
|
8
9
|
import { loadHistory, upsertConversation, setConversationPinned, removeConversation, type StoredConversation } from "./chat-history";
|
|
@@ -63,24 +64,10 @@ const ERROR_COPY: Record<string, string> = {
|
|
|
63
64
|
};
|
|
64
65
|
const RETRYABLE = new Set(["rate_limited", "upstream_error"]);
|
|
65
66
|
|
|
66
|
-
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
export
|
|
70
|
-
return sanitizeHtml(marked.parse(md, { async: false, gfm: true, breaks: true }) as string);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Tailwind preflight makes h1–h6 inherit size/weight, so headings only need
|
|
74
|
-
// weight + spacing to clamp to the message's text scale.
|
|
75
|
-
const MD_STYLES =
|
|
76
|
-
"[&_p:not(:first-child)]:mt-2 [&_ul]:mt-1 [&_ul]:list-disc [&_ul]:pl-4 " +
|
|
77
|
-
"[&_ol]:mt-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_li:not(:first-child)]:mt-1 " +
|
|
78
|
-
"[&_a]:underline [&_a]:underline-offset-2 [&_a]:text-primary " +
|
|
79
|
-
"[&_code]:rounded-inner [&_code]:bg-base-200 [&_code]:px-1 [&_code]:font-mono [&_code]:text-[0.8em] " +
|
|
80
|
-
"[&_pre]:mt-2 [&_pre]:overflow-x-auto [&_pre]:rounded-inner [&_pre]:bg-base-200 [&_pre]:p-2 " +
|
|
81
|
-
"[&_h1]:mt-2 [&_h1]:font-semibold [&_h2]:mt-2 [&_h2]:font-semibold " +
|
|
82
|
-
"[&_h3]:mt-2 [&_h3]:font-semibold [&_h4]:mt-2 [&_h4]:font-semibold " +
|
|
83
|
-
"[&_blockquote]:mt-2 [&_blockquote]:border-l-2 [&_blockquote]:border-base-200 [&_blockquote]:pl-2";
|
|
67
|
+
/** Re-exported from ./markdown, where the rendering rules now live so the deck
|
|
68
|
+
* assistant shares them. Kept here because tests and callers import it from
|
|
69
|
+
* this module. */
|
|
70
|
+
export { assistantHtml };
|
|
84
71
|
|
|
85
72
|
/** Parse our /api/chat SSE frames out of a fetch body. Exported for tests. */
|
|
86
73
|
export async function streamChat(
|
|
@@ -207,25 +194,6 @@ export default function ChatPanel({ open, closing, animateEnter, onClose, siteNa
|
|
|
207
194
|
const listRef = useRef<HTMLDivElement>(null);
|
|
208
195
|
const pinnedRef = useRef(true);
|
|
209
196
|
|
|
210
|
-
// Autosize the composer: height tracks content up to the className's
|
|
211
|
-
// 4-line max-h cap, then it scrolls internally. Keyed on `input` so the
|
|
212
|
-
// post-send clear snaps it back to a single row. (field-sizing: content
|
|
213
|
-
// would do this in CSS alone, but it's still Chromium-only.)
|
|
214
|
-
useEffect(() => {
|
|
215
|
-
const el = composerRef.current;
|
|
216
|
-
if (!el) return;
|
|
217
|
-
el.style.height = "auto";
|
|
218
|
-
el.style.height = `${el.scrollHeight}px`;
|
|
219
|
-
}, [input]);
|
|
220
|
-
|
|
221
|
-
function onComposerKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) {
|
|
222
|
-
// Chat convention: Enter sends, Shift+Enter inserts a newline.
|
|
223
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
224
|
-
e.preventDefault();
|
|
225
|
-
void send();
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
197
|
// The tasks prop can shrink live (the configurator's Preview-as filters it):
|
|
230
198
|
// never keep a selected task that's no longer offered, or the next send would
|
|
231
199
|
// assemble a task the previewed audience can't access.
|
|
@@ -589,7 +557,7 @@ export default function ChatPanel({ open, closing, animateEnter, onClose, siteNa
|
|
|
589
557
|
)}
|
|
590
558
|
{messages.map((m, i) =>
|
|
591
559
|
m.role === "user" ? (
|
|
592
|
-
<div key={i} className=
|
|
560
|
+
<div key={i} className={USER_BUBBLE_CLASS}>
|
|
593
561
|
{m.content}
|
|
594
562
|
</div>
|
|
595
563
|
) : m.content === "" ? (
|
|
@@ -657,33 +625,16 @@ export default function ChatPanel({ open, closing, animateEnter, onClose, siteNa
|
|
|
657
625
|
)}
|
|
658
626
|
</div>
|
|
659
627
|
<p role="status" className="sr-only">{busy ? thinking : ""}</p>
|
|
660
|
-
<
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
value={input}
|
|
671
|
-
onChange={(e) => setInput(e.target.value)}
|
|
672
|
-
onKeyDown={onComposerKeyDown}
|
|
673
|
-
placeholder={`Ask about ${siteName}…`}
|
|
674
|
-
maxLength={4000}
|
|
675
|
-
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:outline-none focus:bg-[color-mix(in_srgb,var(--color-base),var(--color-base-contrast)_5%)]"
|
|
676
|
-
/>
|
|
677
|
-
{busy ? (
|
|
678
|
-
<button type="button" aria-label="Stop" onClick={stop}
|
|
679
|
-
className="flex cursor-pointer items-center rounded-inner bg-primary px-3 py-1.5 text-sm text-primary-contrast">
|
|
680
|
-
<Square size={12} fill="currentColor" aria-hidden="true" />
|
|
681
|
-
</button>
|
|
682
|
-
) : (
|
|
683
|
-
<button type="submit" aria-label="Send"
|
|
684
|
-
className="rounded-inner bg-primary px-3 py-1 text-sm text-primary-contrast disabled:opacity-50">→</button>
|
|
685
|
-
)}
|
|
686
|
-
</form>
|
|
628
|
+
<ChatComposer
|
|
629
|
+
value={input}
|
|
630
|
+
onChange={setInput}
|
|
631
|
+
onSend={() => void send()}
|
|
632
|
+
onStop={stop}
|
|
633
|
+
busy={busy}
|
|
634
|
+
placeholder={`Ask about ${siteName}…`}
|
|
635
|
+
textareaRef={composerRef}
|
|
636
|
+
className={mode === "sheet" ? "pb-[max(0.5rem,env(safe-area-inset-bottom))]" : undefined}
|
|
637
|
+
/>
|
|
687
638
|
{isEditor && <p className="shrink-0 border-t border-base-200 px-3 py-1 text-[10px] text-base-contrast-light">Editor view — answers ground on published content.</p>}
|
|
688
639
|
</div>
|
|
689
640
|
</>
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
// see base.css); below 768px it opens as a bottom sheet. Open/close is
|
|
6
6
|
// animated (sidebar slides in as the band shifts, sheet slides up, and ALL
|
|
7
7
|
// page FABs shrink/fade away); prefers-reduced-motion keeps everything
|
|
8
|
-
// instant. FABs show only while closed. Docked-
|
|
9
|
-
//
|
|
8
|
+
// instant. FABs show only while closed. Docked-CLOSED is the default — the
|
|
9
|
+
// visitor opens it — and the choice then persists per tab (sessionStorage,
|
|
10
|
+
// so a tab that opened the chat keeps it open across pages; mirrored by Layout.astro's
|
|
10
11
|
// pre-paint script so the column is reserved before hydration) — shared
|
|
11
12
|
// across viewer pages and /edit.
|
|
12
13
|
//
|
|
@@ -116,8 +117,8 @@ export default function ChatWidget({ siteName, botName, thinkingPhrases, tasks,
|
|
|
116
117
|
const apply = () => setDocked(mq.matches);
|
|
117
118
|
apply();
|
|
118
119
|
mq.addEventListener("change", apply);
|
|
119
|
-
let storedOpen =
|
|
120
|
-
try { storedOpen = window.sessionStorage.getItem(OPEN_KEY)
|
|
120
|
+
let storedOpen = false; // default: sidebar closed until the visitor opens it
|
|
121
|
+
try { storedOpen = window.sessionStorage.getItem(OPEN_KEY) === "1"; } catch {}
|
|
121
122
|
if (mq.matches && storedOpen) { setOpen(true); setEverOpened(true); }
|
|
122
123
|
return () => mq.removeEventListener("change", apply);
|
|
123
124
|
}, []);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { marked } from "marked";
|
|
2
|
+
import { sanitizeHtml } from "../../lib/sanitize";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The assistant-message treatment, shared by every chat surface (brand chat's
|
|
6
|
+
* ChatPanel and the Deck Builder's docked assistant). Extracted so a second
|
|
7
|
+
* chat pane is a second CALLER, not a second copy of the rendering rules —
|
|
8
|
+
* particularly the sanitizer gate below, which is a security property.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Assistant markdown → sanitized HTML. Callers MUST gate on the sanitizer
|
|
12
|
+
* having loaded (`mdReady`) — before then sanitizeHtml passes raw HTML
|
|
13
|
+
* through, and model output is untrusted. */
|
|
14
|
+
export function assistantHtml(md: string): string {
|
|
15
|
+
return sanitizeHtml(marked.parse(md, { async: false, gfm: true, breaks: true }) as string);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Tailwind preflight makes h1–h6 inherit size/weight, so headings only need
|
|
19
|
+
// weight + spacing to clamp to the message's text scale.
|
|
20
|
+
export const MD_STYLES =
|
|
21
|
+
"[&_p:not(:first-child)]:mt-2 [&_ul]:mt-1 [&_ul]:list-disc [&_ul]:pl-4 " +
|
|
22
|
+
"[&_ol]:mt-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_li:not(:first-child)]:mt-1 " +
|
|
23
|
+
"[&_a]:underline [&_a]:underline-offset-2 [&_a]:text-primary " +
|
|
24
|
+
"[&_code]:rounded-inner [&_code]:bg-base-200 [&_code]:px-1 [&_code]:font-mono [&_code]:text-[0.8em] " +
|
|
25
|
+
"[&_pre]:mt-2 [&_pre]:overflow-x-auto [&_pre]:rounded-inner [&_pre]:bg-base-200 [&_pre]:p-2 " +
|
|
26
|
+
"[&_h1]:mt-2 [&_h1]:font-semibold [&_h2]:mt-2 [&_h2]:font-semibold " +
|
|
27
|
+
"[&_h3]:mt-2 [&_h3]:font-semibold [&_h4]:mt-2 [&_h4]:font-semibold " +
|
|
28
|
+
"[&_blockquote]:mt-2 [&_blockquote]:border-l-2 [&_blockquote]:border-base-200 [&_blockquote]:pl-2";
|
|
29
|
+
|
|
30
|
+
/** The user's turn: the only side of a transcript that gets a filled bubble. */
|
|
31
|
+
export const USER_BUBBLE_CLASS =
|
|
32
|
+
"ml-auto max-w-[85%] whitespace-pre-wrap rounded-outer bg-primary px-3 py-2 text-sm text-primary-contrast";
|
|
@@ -29,6 +29,16 @@ export function EditablePlainText({
|
|
|
29
29
|
...htmlAttrs
|
|
30
30
|
}: EditablePlainTextProps) {
|
|
31
31
|
const ref = useRef<HTMLElement>(null);
|
|
32
|
+
// React must NOT own the text node while the field is being typed into. Every
|
|
33
|
+
// keystroke commits (see useEditablePlainText), the parent echoes the new
|
|
34
|
+
// value straight back, and React's children diff then rewrites the element's
|
|
35
|
+
// text — which destroys the live text node and collapses the caret to offset
|
|
36
|
+
// 0, so a heading comes out backwards. In edit mode we therefore render the
|
|
37
|
+
// text React knew at the moment editing began and hand ownership to the
|
|
38
|
+
// effect below, which writes only when the DOM has genuinely drifted.
|
|
39
|
+
const uncontrolled = useRef(value);
|
|
40
|
+
if (!isEditMode) uncontrolled.current = value;
|
|
41
|
+
|
|
32
42
|
const {
|
|
33
43
|
value: localValue,
|
|
34
44
|
handleInput,
|
|
@@ -94,6 +104,6 @@ export function EditablePlainText({
|
|
|
94
104
|
"data-placeholder": isEditMode && placeholder ? placeholder : undefined,
|
|
95
105
|
...htmlAttrs,
|
|
96
106
|
},
|
|
97
|
-
value,
|
|
107
|
+
isEditMode ? uncontrolled.current : value,
|
|
98
108
|
);
|
|
99
109
|
}
|
|
@@ -16,7 +16,7 @@ interface Props {
|
|
|
16
16
|
siteName: string;
|
|
17
17
|
darkMode: "light" | "dark" | "optional";
|
|
18
18
|
lastUpdated: string | null;
|
|
19
|
-
logo?: { lightUrl: string; darkUrl: string | null; invertInDark: boolean } | null;
|
|
19
|
+
logo?: { lightUrl: string; darkUrl: string | null; invertInDark: boolean; hideTitle?: boolean } | null;
|
|
20
20
|
// Optional same-island handler (tests). In production the editor nav is a
|
|
21
21
|
// separate island from EditorShell, so a click dispatches pageSelectEvent.
|
|
22
22
|
onPageSelect?: (pageId: string) => void;
|
|
@@ -217,18 +217,24 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
217
217
|
</ul>
|
|
218
218
|
);
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
220
|
+
// `size` differs by surface: the mobile header is a 16-unit bar so the logo is
|
|
221
|
+
// height-capped there, while the sidebar sizes it to 80% of the nav's width.
|
|
222
|
+
const renderLogo = (size: string) =>
|
|
223
|
+
logo ? (
|
|
224
|
+
<>
|
|
225
|
+
<img
|
|
226
|
+
src={logo.lightUrl}
|
|
227
|
+
alt={currentSiteName}
|
|
228
|
+
className={cn("object-contain", size, logo.darkUrl ? "block dark:hidden" : logo.invertInDark && "dark:invert")}
|
|
229
|
+
/>
|
|
230
|
+
{logo.darkUrl && (
|
|
231
|
+
<img src={logo.darkUrl} alt={currentSiteName} className={cn("hidden object-contain dark:block", size)} />
|
|
232
|
+
)}
|
|
233
|
+
</>
|
|
234
|
+
) : null;
|
|
235
|
+
|
|
236
|
+
// A logo stands in for the site name unless the editor has opted to keep both.
|
|
237
|
+
const showTitle = !logo || !logo.hideTitle;
|
|
232
238
|
|
|
233
239
|
const handlePageClick = useCallback((id: string) => {
|
|
234
240
|
setEditorAudienceOpen(false);
|
|
@@ -368,8 +374,8 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
368
374
|
<header className={cn("fixed top-0 left-0 right-0 z-50 bg-base", bp.hidden)}>
|
|
369
375
|
<div className="mx-auto max-w-screen-xl flex h-16 items-center justify-between px-4">
|
|
370
376
|
<div className="flex flex-col items-start gap-1">
|
|
371
|
-
{
|
|
372
|
-
<span className="text-lg font-bold text-primary">{currentSiteName}</span>
|
|
377
|
+
{renderLogo("h-8 w-auto")}
|
|
378
|
+
{showTitle && <span className="text-lg font-bold text-primary">{currentSiteName}</span>}
|
|
373
379
|
</div>
|
|
374
380
|
<button onClick={() => setIsOpen(!isOpen)} className="cursor-pointer p-2 text-base-contrast" aria-label="Toggle navigation">
|
|
375
381
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
@@ -386,9 +392,11 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
386
392
|
<nav className={cn("fixed top-0 left-0 z-40 h-full w-64 flex flex-col overflow-y-auto border-r border-base-200 bg-base pt-16 transition-transform nav-sidebar",
|
|
387
393
|
bp.leftAuto, bp.translate,
|
|
388
394
|
isOpen ? "translate-x-0" : "-translate-x-full")}>
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
395
|
+
{/* No horizontal padding on the wrapper: the logo's 80% is of the nav
|
|
396
|
+
column itself, not of a padded box inside it. */}
|
|
397
|
+
<div className={cn("hidden py-4", bp.block)}>
|
|
398
|
+
{renderLogo("mx-auto h-auto w-4/5")}
|
|
399
|
+
{showTitle && <span className="block px-4 text-lg font-bold text-primary">{currentSiteName}</span>}
|
|
392
400
|
</div>
|
|
393
401
|
|
|
394
402
|
<ul className="space-y-1 px-4 py-2">
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cn } from "../../lib/cn";
|
|
2
|
+
import { contrastColor } from "../../lib/contrast";
|
|
3
|
+
import type { Audience } from "../../auth/types";
|
|
4
|
+
|
|
5
|
+
/** Multi-select audience chips — the site-settings idiom for "who sees this".
|
|
6
|
+
* Shared by the chatbot and deck-builder opt-ins so the two gates, which have
|
|
7
|
+
* identical strict-opt-in semantics, cannot come to look like different rules. */
|
|
8
|
+
export function AudienceChips({
|
|
9
|
+
audiences,
|
|
10
|
+
selected,
|
|
11
|
+
onToggle,
|
|
12
|
+
}: {
|
|
13
|
+
audiences: Audience[];
|
|
14
|
+
selected: string[];
|
|
15
|
+
onToggle: (name: string) => void;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex flex-wrap gap-1">
|
|
19
|
+
{audiences.map((a) => (
|
|
20
|
+
<button key={a.name} type="button" onClick={() => onToggle(a.name)}
|
|
21
|
+
className={cn("rounded-full border px-2 py-0.5 text-xs",
|
|
22
|
+
selected.includes(a.name) ? "border-primary bg-primary text-primary-contrast" : "border-base-200 bg-base-accent text-base-contrast")}
|
|
23
|
+
style={a.color && selected.includes(a.name) ? { backgroundColor: a.color, borderColor: a.color, color: contrastColor(a.color) } : undefined}>
|
|
24
|
+
{a.displayName}
|
|
25
|
+
</button>
|
|
26
|
+
))}
|
|
27
|
+
{audiences.length === 0 && <span className="text-xs text-base-contrast-light">No audiences configured</span>}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -2025,6 +2025,18 @@ export default function EditorShell({
|
|
|
2025
2025
|
</svg>
|
|
2026
2026
|
</Fab>
|
|
2027
2027
|
)}
|
|
2028
|
+
{/* Deck Builder (#34). Editors are super-admins over decks and are
|
|
2029
|
+
never gated by deckBuilder.audiences, so this is unconditional.
|
|
2030
|
+
An <a>, not a click handler: builder mode is its own route.
|
|
2031
|
+
bottom-52 clears the bottom-40 document FAB + gap. Taking over the
|
|
2032
|
+
editor top bar in place is #38 — this just goes there. */}
|
|
2033
|
+
<Fab href="/decks?next=%2Fedit" label="Build a deck" column="editor" className="bottom-52">
|
|
2034
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
|
|
2035
|
+
<rect x="3" y="4" width="18" height="12" rx="1" />
|
|
2036
|
+
<path d="M12 16v4" />
|
|
2037
|
+
<path d="M9 20h6" />
|
|
2038
|
+
</svg>
|
|
2039
|
+
</Fab>
|
|
2028
2040
|
{showDocumentEditor && resolvedActivePage?.document?.enabled && (
|
|
2029
2041
|
<Suspense fallback={<DocumentOverlayFallback />}>
|
|
2030
2042
|
{/* Branch/history view (viewSections !== null → previewing the
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { Button } from "../shared/Button";
|
|
3
3
|
import { SubTabs } from "../shared/SubTabs";
|
|
4
|
-
import {
|
|
5
|
-
import { contrastColor } from "../../lib/contrast";
|
|
4
|
+
import { AudienceChips } from "./AudienceChips";
|
|
6
5
|
import ChatPanel from "../chat/ChatPanel";
|
|
7
6
|
import type { SiteConfig, ChatbotConfig, ChatbotTask } from "../../schemas/site-config";
|
|
8
7
|
import type { Audience } from "../../auth/types";
|
|
@@ -30,22 +29,6 @@ function slugify(label: string): string {
|
|
|
30
29
|
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "task";
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
function AudienceChips({ audiences, selected, onToggle }: { audiences: Audience[]; selected: string[]; onToggle: (name: string) => void }) {
|
|
34
|
-
return (
|
|
35
|
-
<div className="flex flex-wrap gap-1">
|
|
36
|
-
{audiences.map((a) => (
|
|
37
|
-
<button key={a.name} type="button" onClick={() => onToggle(a.name)}
|
|
38
|
-
className={cn("rounded-full border px-2 py-0.5 text-xs",
|
|
39
|
-
selected.includes(a.name) ? "border-primary bg-primary text-primary-contrast" : "border-base-200 bg-base-accent text-base-contrast")}
|
|
40
|
-
style={a.color && selected.includes(a.name) ? { backgroundColor: a.color, borderColor: a.color, color: contrastColor(a.color) } : undefined}>
|
|
41
|
-
{a.displayName}
|
|
42
|
-
</button>
|
|
43
|
-
))}
|
|
44
|
-
{audiences.length === 0 && <span className="text-xs text-base-contrast-light">No audiences configured</span>}
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
32
|
export function SiteSettingsChatbot({ siteConfig, onSiteConfigChange, audiences, platformEnabled }: Props) {
|
|
50
33
|
const chatbot = siteConfig.chatbot;
|
|
51
34
|
const [newTaskLabel, setNewTaskLabel] = useState("");
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AudienceChips } from "./AudienceChips";
|
|
2
|
+
import type { SiteConfig } from "../../schemas/site-config";
|
|
3
|
+
import type { Audience } from "../../auth/types";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
siteConfig: SiteConfig;
|
|
7
|
+
onSiteConfigChange: (config: SiteConfig) => void;
|
|
8
|
+
audiences: Audience[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Deck Builder settings. One control today — the creation opt-in — because
|
|
12
|
+
* that is the only `deckBuilder` key that exists; public sharing (#37) is the
|
|
13
|
+
* next one and belongs on this tab. */
|
|
14
|
+
export function SiteSettingsDecks({ siteConfig, onSiteConfigChange, audiences }: Props) {
|
|
15
|
+
const selected = siteConfig.deckBuilder.audiences;
|
|
16
|
+
|
|
17
|
+
function toggle(name: string) {
|
|
18
|
+
const next = selected.includes(name) ? selected.filter((n) => n !== name) : [...selected, name];
|
|
19
|
+
onSiteConfigChange({ ...siteConfig, deckBuilder: { ...siteConfig.deckBuilder, audiences: next } });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="space-y-3">
|
|
24
|
+
<div>
|
|
25
|
+
<h3 className="mb-1 text-sm font-medium text-base-contrast">Who can build decks</h3>
|
|
26
|
+
<p className="mb-2 text-xs text-base-contrast-light">
|
|
27
|
+
Members of the selected audiences get the Decks button and can create and publish their own presentations.
|
|
28
|
+
None selected means no viewer can start a new one. Editors always can.
|
|
29
|
+
</p>
|
|
30
|
+
<AudienceChips audiences={audiences} selected={selected} onToggle={toggle} />
|
|
31
|
+
</div>
|
|
32
|
+
<p className="text-xs text-base-contrast-light">
|
|
33
|
+
Removing an audience here stops its members starting new decks. Decks they already own stay theirs — they can
|
|
34
|
+
still open, edit and delete them — so remove the deck itself if that is what you want gone.
|
|
35
|
+
</p>
|
|
36
|
+
<p className="text-xs text-base-contrast-light">
|
|
37
|
+
A deck is private to the person who made it. Sharing it with an audience, or by public link, comes later.
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -88,7 +88,7 @@ export function SiteSettingsDisplay({ siteConfig, onChange }: Props) {
|
|
|
88
88
|
|
|
89
89
|
function pickLogo(which: "light" | "dark") {
|
|
90
90
|
mediaLibrary?.openSelectModal((imageId) => {
|
|
91
|
-
const base = siteConfig.logo ?? { light: { imageId: "" }, dark: null, invertInDark: false, showInNav: true };
|
|
91
|
+
const base = siteConfig.logo ?? { light: { imageId: "" }, dark: null, invertInDark: false, showInNav: true, hideTitle: true };
|
|
92
92
|
update({ logo: { ...base, [which]: { imageId } } });
|
|
93
93
|
});
|
|
94
94
|
}
|
|
@@ -292,6 +292,15 @@ export function SiteSettingsDisplay({ siteConfig, onChange }: Props) {
|
|
|
292
292
|
label="Show in navigation"
|
|
293
293
|
/>
|
|
294
294
|
|
|
295
|
+
{/* Not gated on showInNav: the login page shows the logo either
|
|
296
|
+
way, so this has to stay reachable when the nav one is off. */}
|
|
297
|
+
<Checkbox
|
|
298
|
+
checked={logo.hideTitle}
|
|
299
|
+
onChange={(v) => update({ logo: { ...logo, hideTitle: v } })}
|
|
300
|
+
label="Hide site title"
|
|
301
|
+
description="The logo stands in for the site name in the navigation and on the login page. Turn this off to show both."
|
|
302
|
+
/>
|
|
303
|
+
|
|
295
304
|
{needsDarkHint && (
|
|
296
305
|
<p className="text-xs text-amber-600">
|
|
297
306
|
Add a dark variant or enable invert so the logo stays legible in dark mode.
|
|
@@ -6,6 +6,7 @@ import { SiteSettingsViewerAccess } from "./SiteSettingsViewerAccess";
|
|
|
6
6
|
import { SiteSettingsDisplay } from "./SiteSettingsDisplay";
|
|
7
7
|
import { SiteSettingsUsers } from "./SiteSettingsUsers";
|
|
8
8
|
import { SiteSettingsChatbot } from "./SiteSettingsChatbot";
|
|
9
|
+
import { SiteSettingsDecks } from "./SiteSettingsDecks";
|
|
9
10
|
import type { SiteConfig } from "../../schemas/site-config";
|
|
10
11
|
import type { Audience } from "../../auth/types";
|
|
11
12
|
|
|
@@ -81,6 +82,16 @@ export function SiteSettingsModal({ isOpen, onClose, siteConfig, onSiteConfigCha
|
|
|
81
82
|
</div>
|
|
82
83
|
),
|
|
83
84
|
},
|
|
85
|
+
{
|
|
86
|
+
id: "decks",
|
|
87
|
+
label: "Decks",
|
|
88
|
+
show: true,
|
|
89
|
+
content: (
|
|
90
|
+
<div data-testid="site-settings-tab-panel" className="flex flex-1 flex-col overflow-y-auto px-6 py-4">
|
|
91
|
+
<SiteSettingsDecks siteConfig={siteConfig} onSiteConfigChange={onSiteConfigChange} audiences={audiences} />
|
|
92
|
+
</div>
|
|
93
|
+
),
|
|
94
|
+
},
|
|
84
95
|
{
|
|
85
96
|
id: "display",
|
|
86
97
|
label: "Display",
|
|
@@ -83,6 +83,29 @@ export function SiteSettingsUsers({ currentUser }: Props) {
|
|
|
83
83
|
setConfirmingRevoke(userId);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
async function handleRoleChange(userId: string, role: Role) {
|
|
87
|
+
setError(null);
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch("/api/auth/users", {
|
|
90
|
+
method: "PATCH",
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
body: JSON.stringify({ userId, role }),
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
const data = await res.json().catch(() => ({}));
|
|
96
|
+
// Resync first: a rejected change would otherwise leave the select
|
|
97
|
+
// showing a role the user does not have. loadUsers() clears `error` on
|
|
98
|
+
// success, so the message has to be set after it, not before.
|
|
99
|
+
await loadUsers();
|
|
100
|
+
setError(data.error || "Role change failed");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
await loadUsers();
|
|
104
|
+
} catch {
|
|
105
|
+
setError("Network error while changing role");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
86
109
|
async function confirmRevoke(userId: string) {
|
|
87
110
|
setConfirmingRevoke(null);
|
|
88
111
|
try {
|
|
@@ -114,8 +137,28 @@ export function SiteSettingsUsers({ currentUser }: Props) {
|
|
|
114
137
|
{users.map((user) => (
|
|
115
138
|
<Fragment key={user.id}>
|
|
116
139
|
<div className="flex items-center border-b border-base-200 py-2 pr-2 text-base-contrast">{user.email}</div>
|
|
117
|
-
<div className="flex items-center border-b border-base-200 py-2 pr-2
|
|
118
|
-
{user.
|
|
140
|
+
<div className="flex items-center border-b border-base-200 py-2 pr-2 text-base-contrast-light">
|
|
141
|
+
{currentUser?.email === user.email ? (
|
|
142
|
+
// Owners can't change their own role — the same rule that makes
|
|
143
|
+
// this row's Remove control a dash.
|
|
144
|
+
<span className="capitalize">{user.role}</span>
|
|
145
|
+
) : (
|
|
146
|
+
<div className="relative w-full">
|
|
147
|
+
<select
|
|
148
|
+
value={user.role}
|
|
149
|
+
onChange={(e) => handleRoleChange(user.id, e.target.value as Role)}
|
|
150
|
+
aria-label={`Role for ${user.email}`}
|
|
151
|
+
className="w-full appearance-none rounded border border-base-200 bg-base py-1 pr-7 pl-2 text-sm text-base-contrast focus:border-base-contrast focus:outline-none focus:ring-1 focus:ring-base-contrast"
|
|
152
|
+
>
|
|
153
|
+
<option value="editor">Editor</option>
|
|
154
|
+
<option value="owner">Owner</option>
|
|
155
|
+
</select>
|
|
156
|
+
<ChevronDown
|
|
157
|
+
size={14}
|
|
158
|
+
className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-base-contrast-light"
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
119
162
|
</div>
|
|
120
163
|
<div className="flex items-center justify-end border-b border-base-200 py-2">
|
|
121
164
|
{currentUser?.email === user.email ? (
|
|
@@ -187,6 +230,10 @@ export function SiteSettingsUsers({ currentUser }: Props) {
|
|
|
187
230
|
</Button>
|
|
188
231
|
</form>
|
|
189
232
|
|
|
233
|
+
<p className="text-xs text-base-contrast-light">
|
|
234
|
+
Role changes take effect the next time the user signs in, or within 24 hours.
|
|
235
|
+
</p>
|
|
236
|
+
|
|
190
237
|
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
191
238
|
</div>
|
|
192
239
|
);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DeckTextSlot } from "../lib/deck-edit";
|
|
2
|
+
import type { DeckComponent } from "../schemas/deck";
|
|
3
|
+
import { DeckImage } from "./components/DeckImage";
|
|
4
|
+
import { DeckImageGrid } from "./components/DeckImageGrid";
|
|
5
|
+
import { DeckRichText } from "./components/DeckRichText";
|
|
6
|
+
import { DeckVideo } from "./components/DeckVideo";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
component: DeckComponent;
|
|
10
|
+
/** The Layout slot this component fills — carried only so text leaves can
|
|
11
|
+
* address themselves for inline editing (#39). Optional: a caller that omits
|
|
12
|
+
* it gets the read-only render. */
|
|
13
|
+
slot?: DeckTextSlot;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Dispatches a Deck Component onto its runtime implementation. */
|
|
17
|
+
export function DeckComponentView({ component, slot }: Props) {
|
|
18
|
+
switch (component.type) {
|
|
19
|
+
case "richText":
|
|
20
|
+
return <DeckRichText content={component} slot={slot} />;
|
|
21
|
+
case "image":
|
|
22
|
+
return <DeckImage content={component} />;
|
|
23
|
+
case "imageGrid":
|
|
24
|
+
return <DeckImageGrid content={component} />;
|
|
25
|
+
case "video":
|
|
26
|
+
return <DeckVideo content={component} />;
|
|
27
|
+
}
|
|
28
|
+
}
|