@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
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import type { Deck, DeckMediaAsset, DeckMediaResolver } from "../schemas/deck";
|
|
5
|
+
import { DeckMediaProvider } from "./media";
|
|
6
|
+
import { DeckStage } from "./DeckStage";
|
|
7
|
+
import { DeckSlideView } from "./DeckSlideView";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
deck: Deck;
|
|
11
|
+
/** Slide to open on. Out-of-range values clamp into the deck. */
|
|
12
|
+
initialSlide?: number;
|
|
13
|
+
/** Window-level arrow/space/Home/End navigation. Off for thumbnails. */
|
|
14
|
+
keyboard?: boolean;
|
|
15
|
+
/** Turns media references into bytes. The Deck Builder injects the real one. */
|
|
16
|
+
resolveMedia?: DeckMediaResolver;
|
|
17
|
+
/**
|
|
18
|
+
* Pre-resolved portal media, keyed by media id. The serializable form of the
|
|
19
|
+
* same seam, for callers that cross an Astro island boundary — a function
|
|
20
|
+
* prop cannot survive that trip. Ignored when `resolveMedia` is given.
|
|
21
|
+
*/
|
|
22
|
+
mediaAssets?: Record<string, DeckMediaAsset>;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function clamp(index: number, length: number): number {
|
|
27
|
+
return Math.min(Math.max(index, 0), length - 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Arrow keys must not steal typing from a chat panel docked beside the stage. */
|
|
31
|
+
function isTypingTarget(target: EventTarget | null): boolean {
|
|
32
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
33
|
+
return target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A native button activates on Space *keyup*; the keydown only marks it active
|
|
38
|
+
* and suppresses scrolling. Calling preventDefault on that keydown cancels the
|
|
39
|
+
* activation — so Space on a focused "Previous slide" used to swallow the click
|
|
40
|
+
* and step FORWARD instead. Enter needs no such guard: it has no case below, so
|
|
41
|
+
* it never reaches preventDefault.
|
|
42
|
+
*/
|
|
43
|
+
function isActivatableTarget(target: EventTarget | null): boolean {
|
|
44
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
45
|
+
if (target.tagName === "BUTTON" || target.tagName === "SUMMARY") return true;
|
|
46
|
+
if (target.tagName === "A" && target.hasAttribute("href")) return true;
|
|
47
|
+
return target.getAttribute("role") === "button";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const NAV_BUTTON =
|
|
51
|
+
"flex size-[38px] items-center justify-center rounded-[var(--radius-outer)] border border-base-200 text-base-contrast transition-colors hover:bg-base-accent disabled:cursor-not-allowed disabled:opacity-35 disabled:hover:bg-transparent";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The deck runtime's presentation surface: one 16:9 stage plus slide
|
|
55
|
+
* navigation. Deliberately self-contained — the Deck Builder, the in-portal
|
|
56
|
+
* deck view and the Public Link view all mount this same component, so slide
|
|
57
|
+
* state lives here rather than in any one of those shells.
|
|
58
|
+
*/
|
|
59
|
+
export function DeckPlayer({ deck, initialSlide = 0, keyboard = true, resolveMedia, mediaAssets, className }: Props) {
|
|
60
|
+
const total = deck.slides.length;
|
|
61
|
+
const [index, setIndex] = useState(() => clamp(initialSlide, total));
|
|
62
|
+
// `Object.hasOwn`, not indexing: a media id is author-supplied, and a plain
|
|
63
|
+
// object answers `"constructor"` and `"toString"` with inherited members that
|
|
64
|
+
// are not assets at all. The schema's charset rules those ids out too — this
|
|
65
|
+
// is the second lock on the same door. The `kind` check keeps the map form
|
|
66
|
+
// honest the day a second media kind lands: it resolves portal ids, and says
|
|
67
|
+
// so by refusing everything else rather than looking the id up anyway.
|
|
68
|
+
const resolve = useMemo<DeckMediaResolver>(
|
|
69
|
+
() =>
|
|
70
|
+
resolveMedia ??
|
|
71
|
+
((ref) =>
|
|
72
|
+
ref.kind === "portal" && mediaAssets !== undefined && Object.hasOwn(mediaAssets, ref.id)
|
|
73
|
+
? mediaAssets[ref.id]
|
|
74
|
+
: null),
|
|
75
|
+
[resolveMedia, mediaAssets],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const current = clamp(index, total);
|
|
79
|
+
const slide = deck.slides[current];
|
|
80
|
+
|
|
81
|
+
const goTo = useCallback((next: number) => setIndex(clamp(next, total)), [total]);
|
|
82
|
+
// `prev` is clamped BEFORE the delta: the builder live-replaces the deck every
|
|
83
|
+
// LLM turn, so a shrunk deck can leave the stored index past the end. Without
|
|
84
|
+
// the inner clamp the first Previous press would land back on the slide
|
|
85
|
+
// already showing — a visible no-op.
|
|
86
|
+
const step = useCallback(
|
|
87
|
+
(delta: number) => setIndex((prev) => clamp(clamp(prev, total) + delta, total)),
|
|
88
|
+
[total],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!keyboard) return;
|
|
93
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
94
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
95
|
+
if (isTypingTarget(event.target)) return;
|
|
96
|
+
switch (event.key) {
|
|
97
|
+
case " ":
|
|
98
|
+
if (isActivatableTarget(event.target)) return;
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
step(1);
|
|
101
|
+
break;
|
|
102
|
+
case "ArrowRight":
|
|
103
|
+
case "PageDown":
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
step(1);
|
|
106
|
+
break;
|
|
107
|
+
case "ArrowLeft":
|
|
108
|
+
case "PageUp":
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
step(-1);
|
|
111
|
+
break;
|
|
112
|
+
case "Home":
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
setIndex(0);
|
|
115
|
+
break;
|
|
116
|
+
case "End":
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
setIndex(total - 1);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
window.addEventListener("keydown", onKeyDown);
|
|
123
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
124
|
+
}, [keyboard, step, total]);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div data-deck-player={deck.id} className={cn("flex h-full min-h-0 w-full flex-col gap-4 bg-base p-4", className)}>
|
|
128
|
+
<DeckStage className="flex-1">
|
|
129
|
+
<DeckMediaProvider resolve={resolve}>
|
|
130
|
+
<DeckSlideView slide={slide} />
|
|
131
|
+
</DeckMediaProvider>
|
|
132
|
+
</DeckStage>
|
|
133
|
+
|
|
134
|
+
<nav aria-label={`${deck.title} slides`} className="flex shrink-0 items-center justify-center gap-4">
|
|
135
|
+
<button type="button" className={NAV_BUTTON} onClick={() => step(-1)} disabled={current === 0} aria-label="Previous slide">
|
|
136
|
+
<ChevronLeft className="size-5" aria-hidden="true" />
|
|
137
|
+
</button>
|
|
138
|
+
|
|
139
|
+
<ol className="flex items-center gap-2">
|
|
140
|
+
{deck.slides.map((s, i) => (
|
|
141
|
+
<li key={s.id}>
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={() => goTo(i)}
|
|
145
|
+
aria-current={i === current ? "true" : undefined}
|
|
146
|
+
aria-label={s.title ? `Slide ${i + 1}: ${s.title}` : `Slide ${i + 1}`}
|
|
147
|
+
className={cn(
|
|
148
|
+
"size-[10px] rounded-full transition-colors",
|
|
149
|
+
i === current ? "bg-primary" : "bg-base-300 hover:bg-base-contrast-light",
|
|
150
|
+
)}
|
|
151
|
+
/>
|
|
152
|
+
</li>
|
|
153
|
+
))}
|
|
154
|
+
</ol>
|
|
155
|
+
|
|
156
|
+
<button type="button" className={NAV_BUTTON} onClick={() => step(1)} disabled={current === total - 1} aria-label="Next slide">
|
|
157
|
+
<ChevronRight className="size-5" aria-hidden="true" />
|
|
158
|
+
</button>
|
|
159
|
+
|
|
160
|
+
<p aria-live="polite" className="ml-2 text-sm tabular-nums text-base-contrast-light">
|
|
161
|
+
{current + 1} / {total}
|
|
162
|
+
</p>
|
|
163
|
+
</nav>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DeckSlide } from "../schemas/deck";
|
|
2
|
+
import { CenteredTextLayout } from "./layouts/CenteredTextLayout";
|
|
3
|
+
import { ColumnSplitLayout } from "./layouts/ColumnSplitLayout";
|
|
4
|
+
import { FullBleedImageLayout } from "./layouts/FullBleedImageLayout";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
slide: DeckSlide;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function LayoutView({ slide }: Props) {
|
|
11
|
+
const { layout } = slide;
|
|
12
|
+
switch (layout.type) {
|
|
13
|
+
case "centeredText":
|
|
14
|
+
return <CenteredTextLayout layout={layout} />;
|
|
15
|
+
case "fullBleedImage":
|
|
16
|
+
return <FullBleedImageLayout layout={layout} />;
|
|
17
|
+
case "columnSplit":
|
|
18
|
+
return <ColumnSplitLayout layout={layout} />;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Dispatches a slide onto its Layout. One slide uses exactly one Layout.
|
|
24
|
+
*
|
|
25
|
+
* Keyed by slide id, and that key is load-bearing rather than a lint nicety:
|
|
26
|
+
* two slides sharing a Layout shape reconcile onto the SAME component
|
|
27
|
+
* instances, so an expanded image-grid tile or a playing video on slide 1 was
|
|
28
|
+
* inherited by slide 2 the moment you pressed the arrow key. The key forces the
|
|
29
|
+
* subtree to remount, which is the only thing that makes a slide's interaction
|
|
30
|
+
* state belong to that slide.
|
|
31
|
+
*/
|
|
32
|
+
export function DeckSlideView({ slide }: Props) {
|
|
33
|
+
return (
|
|
34
|
+
<div key={slide.id} data-deck-slide={slide.id} className="size-full">
|
|
35
|
+
<LayoutView slide={slide} />
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useRef, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
import { DECK_STAGE_HEIGHT, DECK_STAGE_WIDTH } from "../schemas/deck";
|
|
4
|
+
import { useStageScale } from "./useStageScale";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
/** Applied to the fit box, which is what the stage is scaled to fill. */
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The 16:9 stage: a fixed logical canvas centered in — and scaled to fit — its
|
|
14
|
+
* container, so slide contents are authored in px against one coordinate system
|
|
15
|
+
* regardless of the viewport. Scaling by transform (rather than by a font-size
|
|
16
|
+
* cascade) keeps every measurement, including line breaks, identical at every
|
|
17
|
+
* size, which is what makes a screenshared deck look the same as its preview.
|
|
18
|
+
*/
|
|
19
|
+
export function DeckStage({ children, className }: Props) {
|
|
20
|
+
const fitRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
const scale = useStageScale(fitRef);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div ref={fitRef} className={cn("relative min-h-0 w-full overflow-hidden", className)} data-deck-fit="">
|
|
25
|
+
<div
|
|
26
|
+
data-deck-stage=""
|
|
27
|
+
data-deck-scale={scale === null ? undefined : scale.toFixed(4)}
|
|
28
|
+
className="absolute top-1/2 left-1/2 origin-center overflow-hidden rounded-[var(--radius-outer)] font-body"
|
|
29
|
+
style={{
|
|
30
|
+
width: DECK_STAGE_WIDTH,
|
|
31
|
+
height: DECK_STAGE_HEIGHT,
|
|
32
|
+
transform: scale === null ? "translate(-50%, -50%)" : `translate(-50%, -50%) scale(${scale})`,
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DeckImage as DeckImageContent } from "../../schemas/deck";
|
|
2
|
+
import { DeckMissingMedia, useDeckMedia } from "../media";
|
|
3
|
+
import { surfaceInk } from "../tokens";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
content: DeckImageContent;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Deck Component — image.
|
|
11
|
+
*
|
|
12
|
+
* Fills its slot and crops. The caption is a solid brand block sitting on the
|
|
13
|
+
* image's bottom edge rather than a gradient scrim: a scrim needs a literal
|
|
14
|
+
* color to darken toward, and legibility here comes from the block.
|
|
15
|
+
*/
|
|
16
|
+
export function DeckImage({ content }: Props) {
|
|
17
|
+
const asset = useDeckMedia(content.media);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<figure data-deck-component="image" className="relative size-full overflow-hidden bg-base-200">
|
|
21
|
+
{asset === null ? (
|
|
22
|
+
<DeckMissingMedia />
|
|
23
|
+
) : (
|
|
24
|
+
<img src={asset.src} alt={asset.alt} className="size-full object-cover" />
|
|
25
|
+
)}
|
|
26
|
+
|
|
27
|
+
{content.caption !== undefined && (
|
|
28
|
+
<figcaption
|
|
29
|
+
data-deck-surface="brand"
|
|
30
|
+
style={surfaceInk("brand")}
|
|
31
|
+
className="absolute right-0 bottom-0 left-0 bg-primary px-[28px] py-[18px] text-[18px] tracking-[0.06em] text-[var(--deck-ink)] [text-transform:var(--nav-text-transform)]"
|
|
32
|
+
>
|
|
33
|
+
{content.caption}
|
|
34
|
+
</figcaption>
|
|
35
|
+
)}
|
|
36
|
+
</figure>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
import type { DeckImageGrid as DeckImageGridContent, DeckImageGridItem, DeckMediaAsset } from "../../schemas/deck";
|
|
4
|
+
import { captureTileRects, playTileFlip, type TileRects } from "../flip";
|
|
5
|
+
import { DeckMissingMedia, useDeckMediaResolver } from "../media";
|
|
6
|
+
import { DECK_ELEVATION, surfaceInk } from "../tokens";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
content: DeckImageGridContent;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Two columns for a small set, three once there are enough tiles to fill them.
|
|
14
|
+
* Deliberately not a schema field: it is presentation, and one fewer knob for
|
|
15
|
+
* an author to get wrong.
|
|
16
|
+
*/
|
|
17
|
+
function columnsFor(count: number): number {
|
|
18
|
+
return count <= 4 ? 2 : 3;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A tile's height as a multiple of the column width. Falls back to a square
|
|
23
|
+
* when the resolver reports no dimensions — the packing still has to be
|
|
24
|
+
* deterministic, and a square is the least-wrong guess.
|
|
25
|
+
*/
|
|
26
|
+
function tileRatio(asset: DeckMediaAsset | null): number {
|
|
27
|
+
if (asset?.width === undefined || asset.height === undefined || asset.width <= 0) return 1;
|
|
28
|
+
return asset.height / asset.width;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Greedy shortest-column packing: each tile joins whichever column is currently
|
|
33
|
+
* least tall, ties going left. With uniform aspect ratios that degenerates to
|
|
34
|
+
* round-robin, which is what makes the simple cases look deliberate.
|
|
35
|
+
*/
|
|
36
|
+
export function packMasonry(ratios: number[], columns: number): number[][] {
|
|
37
|
+
const packed: number[][] = Array.from({ length: columns }, () => []);
|
|
38
|
+
const filled = new Array<number>(columns).fill(0);
|
|
39
|
+
ratios.forEach((ratio, index) => {
|
|
40
|
+
let target = 0;
|
|
41
|
+
for (let column = 1; column < columns; column++) {
|
|
42
|
+
if (filled[column] < filled[target]) target = column;
|
|
43
|
+
}
|
|
44
|
+
packed[target].push(index);
|
|
45
|
+
filled[target] += ratio;
|
|
46
|
+
});
|
|
47
|
+
return packed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const TILE_BASE =
|
|
51
|
+
"group relative block shrink-0 overflow-hidden bg-base-200 will-change-transform transition-[transform,box-shadow,opacity] duration-[380ms] ease-[cubic-bezier(0.2,0.75,0.2,1)] focus-visible:outline-2 focus-visible:outline-offset-[-6px] focus-visible:outline-[var(--color-primary)]";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Deck Component — image grid, and the runtime's signature interaction.
|
|
55
|
+
*
|
|
56
|
+
* Masonry columns: every image keeps its own aspect ratio, the column width
|
|
57
|
+
* decides its height, and a stack that runs past the bottom of the slot is
|
|
58
|
+
* clipped there. That clip is the point — on a fixed 16:9 stage a collage that
|
|
59
|
+
* runs off the bottom edge reads better than one that crops every frame to a
|
|
60
|
+
* uniform cell, and nothing may spill past the slot onto the rest of the slide.
|
|
61
|
+
*
|
|
62
|
+
* Hover lifts a tile; activating it expands that tile to fill the whole grid
|
|
63
|
+
* region as a lightbox contained inside the slide (never a full-screen overlay
|
|
64
|
+
* — the deck stays the deck). Activating again, the close affordance, or Escape
|
|
65
|
+
* returns it. Tiles are real buttons, so keyboard activation is the browser's.
|
|
66
|
+
*/
|
|
67
|
+
export function DeckImageGrid({ content }: Props) {
|
|
68
|
+
const [active, setActive] = useState<number | null>(null);
|
|
69
|
+
const gridRef = useRef<HTMLDivElement>(null);
|
|
70
|
+
const pendingRects = useRef<TileRects | null>(null);
|
|
71
|
+
|
|
72
|
+
const expanded = active !== null;
|
|
73
|
+
const resolve = useDeckMediaResolver();
|
|
74
|
+
const count = content.items.length;
|
|
75
|
+
|
|
76
|
+
const assets = useMemo(() => content.items.map((item) => resolve(item.media)), [content.items, resolve]);
|
|
77
|
+
const columns = columnsFor(count);
|
|
78
|
+
const packed = useMemo(() => packMasonry(assets.map(tileRatio), columns), [assets, columns]);
|
|
79
|
+
|
|
80
|
+
// Measure BEFORE the state change: React moves the DOM, and FLIP needs the
|
|
81
|
+
// pre-move rects, which no longer exist by the time the effect runs.
|
|
82
|
+
const transitionTo = useCallback((next: number | null) => {
|
|
83
|
+
const grid = gridRef.current;
|
|
84
|
+
if (grid) pendingRects.current = captureTileRects(grid);
|
|
85
|
+
setActive(next);
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
useLayoutEffect(() => {
|
|
89
|
+
const grid = gridRef.current;
|
|
90
|
+
const before = pendingRects.current;
|
|
91
|
+
pendingRects.current = null;
|
|
92
|
+
if (grid && before) playTileFlip(grid, before);
|
|
93
|
+
}, [active]);
|
|
94
|
+
|
|
95
|
+
// The Deck Builder replaces the whole deck every LLM turn, so a grid can lose
|
|
96
|
+
// tiles under an open lightbox. Left alone, the expanded index would point
|
|
97
|
+
// past the end and every tile would render hidden — a blank slot with no way
|
|
98
|
+
// out but Escape.
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
setActive((prev) => (prev === null || prev < count ? prev : null));
|
|
101
|
+
}, [count]);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!expanded) return;
|
|
105
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
106
|
+
if (event.key !== "Escape") return;
|
|
107
|
+
// NOTE: this stops the event travelling to an ancestor, which for a
|
|
108
|
+
// window listener is nothing at all — it does NOT stop other listeners on
|
|
109
|
+
// window from also seeing this Escape. A future shell-level Escape (exit
|
|
110
|
+
// presentation, close the builder) has to coordinate with the grid
|
|
111
|
+
// explicitly; it cannot rely on this call to have swallowed the key.
|
|
112
|
+
event.stopPropagation();
|
|
113
|
+
transitionTo(null);
|
|
114
|
+
};
|
|
115
|
+
window.addEventListener("keydown", onKeyDown);
|
|
116
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
117
|
+
}, [expanded, transitionTo]);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Close returns focus to the tile that was expanded. Without it the close
|
|
121
|
+
* button unmounts under the caret and focus falls to <body>, which strands a
|
|
122
|
+
* keyboard user outside the grid entirely. Focus first, then re-render, so
|
|
123
|
+
* the unmounting button is no longer the active element when it goes.
|
|
124
|
+
*/
|
|
125
|
+
const close = useCallback(() => {
|
|
126
|
+
gridRef.current?.querySelector<HTMLElement>(`[data-deck-tile="${active}"]`)?.focus({ preventScroll: true });
|
|
127
|
+
transitionTo(null);
|
|
128
|
+
}, [active, transitionTo]);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div
|
|
132
|
+
ref={gridRef}
|
|
133
|
+
data-deck-component="imageGrid"
|
|
134
|
+
data-deck-grid-expanded={expanded ? "true" : "false"}
|
|
135
|
+
data-deck-grid-columns={columns}
|
|
136
|
+
style={DECK_ELEVATION}
|
|
137
|
+
// Clipped, not scrolled: a deck slide has no scrollbars, and a stack that
|
|
138
|
+
// runs long is meant to run off the bottom of the slot.
|
|
139
|
+
className="relative flex size-full gap-[14px] overflow-hidden"
|
|
140
|
+
>
|
|
141
|
+
{packed.map((indices, column) => (
|
|
142
|
+
// `flex-1` rather than a width fraction: the columns have to share the
|
|
143
|
+
// gap between them, and equal flex bases divide what is left over.
|
|
144
|
+
<div key={column} data-deck-grid-column={column} className="flex min-w-0 flex-1 flex-col gap-[14px]">
|
|
145
|
+
{indices.map((index) => (
|
|
146
|
+
<Tile
|
|
147
|
+
key={index}
|
|
148
|
+
item={content.items[index]}
|
|
149
|
+
asset={assets[index]}
|
|
150
|
+
index={index}
|
|
151
|
+
expanded={expanded}
|
|
152
|
+
active={index === active}
|
|
153
|
+
onActivate={() => transitionTo(index === active ? null : index)}
|
|
154
|
+
/>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
))}
|
|
158
|
+
|
|
159
|
+
{expanded && (
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
data-deck-grid-close=""
|
|
163
|
+
onClick={close}
|
|
164
|
+
data-deck-surface="brand"
|
|
165
|
+
style={surfaceInk("brand")}
|
|
166
|
+
className="absolute top-[14px] right-[14px] z-[4] bg-primary px-[18px] py-[10px] text-[15px] font-semibold tracking-[0.12em] text-[var(--deck-ink)] uppercase"
|
|
167
|
+
>
|
|
168
|
+
Close
|
|
169
|
+
</button>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface TileProps {
|
|
176
|
+
item: DeckImageGridItem;
|
|
177
|
+
asset: DeckMediaAsset | null;
|
|
178
|
+
index: number;
|
|
179
|
+
expanded: boolean;
|
|
180
|
+
active: boolean;
|
|
181
|
+
onActivate: () => void;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function Tile({ item, asset, index, expanded, active, onActivate }: TileProps) {
|
|
185
|
+
const label = item.caption ?? asset?.alt ?? `Image ${index + 1}`;
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
data-deck-tile={index}
|
|
191
|
+
data-deck-tile-active={active ? "true" : "false"}
|
|
192
|
+
aria-expanded={active}
|
|
193
|
+
aria-label={active ? `Close ${label}` : `Expand ${label}`}
|
|
194
|
+
tabIndex={expanded && !active ? -1 : 0}
|
|
195
|
+
onClick={onActivate}
|
|
196
|
+
className={cn(
|
|
197
|
+
TILE_BASE,
|
|
198
|
+
// Every tile leaves the column flow when one is expanded, not only the
|
|
199
|
+
// active one: leaving the siblings in place would let the columns
|
|
200
|
+
// re-pack around the gap, which both reflows the tiles the lightbox is
|
|
201
|
+
// covering and moves the ones the FLIP is animating.
|
|
202
|
+
expanded ? "absolute inset-0 flex items-center justify-center" : "w-full",
|
|
203
|
+
expanded && !active && "pointer-events-none opacity-0",
|
|
204
|
+
// Transparent, not the tile's loading plate: an expanded frame is
|
|
205
|
+
// letterboxed inside the slot, and the field around it should read as
|
|
206
|
+
// the slide showing through rather than as a grey box drawn on top.
|
|
207
|
+
expanded && active && "z-[3] bg-transparent opacity-100",
|
|
208
|
+
!expanded && "cursor-pointer hover:z-[2] hover:scale-[1.035] hover:shadow-[var(--deck-lift)]",
|
|
209
|
+
)}
|
|
210
|
+
>
|
|
211
|
+
{asset === null ? (
|
|
212
|
+
<DeckMissingMedia className={active ? "size-full" : "aspect-square h-auto w-full"} />
|
|
213
|
+
) : (
|
|
214
|
+
<img
|
|
215
|
+
src={asset.src}
|
|
216
|
+
alt=""
|
|
217
|
+
// The intrinsic size, when the resolver knows it, so the column
|
|
218
|
+
// reserves the right height before a byte arrives — otherwise the
|
|
219
|
+
// masonry re-packs visibly as each image lands.
|
|
220
|
+
width={asset.width}
|
|
221
|
+
height={asset.height}
|
|
222
|
+
// The elevation moves onto the image when expanded: the button box is
|
|
223
|
+
// the whole slot now, and a shadow on THAT would outline the
|
|
224
|
+
// letterbox instead of the frame the viewer is looking at.
|
|
225
|
+
className={
|
|
226
|
+
active ? "max-h-full max-w-full object-contain shadow-[var(--deck-lift-strong)]" : "block h-auto w-full"
|
|
227
|
+
}
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
<span
|
|
232
|
+
aria-hidden="true"
|
|
233
|
+
data-deck-surface="brand"
|
|
234
|
+
style={surfaceInk("brand")}
|
|
235
|
+
className="absolute top-0 left-0 bg-primary px-[12px] py-[6px] text-[15px] font-bold tracking-[0.1em] text-[var(--deck-ink)] tabular-nums"
|
|
236
|
+
>
|
|
237
|
+
{String(index + 1).padStart(2, "0")}
|
|
238
|
+
</span>
|
|
239
|
+
|
|
240
|
+
{item.caption !== undefined && (
|
|
241
|
+
<span
|
|
242
|
+
data-deck-surface="brand"
|
|
243
|
+
style={surfaceInk("brand")}
|
|
244
|
+
className={cn(
|
|
245
|
+
"absolute right-0 bottom-0 left-0 bg-primary text-left tracking-[0.04em] text-[var(--deck-ink)] transition-opacity duration-200",
|
|
246
|
+
active ? "px-[30px] py-[22px] text-[24px] opacity-100" : "px-[18px] py-[14px] text-[18px] opacity-0 group-hover:opacity-100",
|
|
247
|
+
)}
|
|
248
|
+
>
|
|
249
|
+
{item.caption}
|
|
250
|
+
</span>
|
|
251
|
+
)}
|
|
252
|
+
</button>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Fragment, type ReactNode } from "react";
|
|
2
|
+
import type { DeckTextPath, DeckTextSlot } from "../../lib/deck-edit";
|
|
3
|
+
import type { DeckInlineText, DeckRichText as DeckRichTextContent } from "../../schemas/deck";
|
|
4
|
+
import { EditableText } from "../editing";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
content: DeckRichTextContent;
|
|
8
|
+
/**
|
|
9
|
+
* Which slot of the enclosing Layout this component fills. Supplied only so a
|
|
10
|
+
* text leaf can address itself for inline editing (#39); absent means "no
|
|
11
|
+
* edit seam here", which is the read-only default every viewer surface gets.
|
|
12
|
+
*/
|
|
13
|
+
slot?: DeckTextSlot;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A body line. A plain string renders as itself; a span run renders its marks
|
|
18
|
+
* as elements. Nothing here reads a string as markup — `bold` and `italic` are
|
|
19
|
+
* booleans on the span, which is the whole point of the representation.
|
|
20
|
+
*
|
|
21
|
+
* The editable unit is the LEAF, never the line: a marked run offers one field
|
|
22
|
+
* per span, so editing the words around a bold phrase cannot flatten it. That
|
|
23
|
+
* is what lets inline editing be plain-text-only without a formatting UI and
|
|
24
|
+
* without silently dropping marks (#39).
|
|
25
|
+
*/
|
|
26
|
+
function InlineText({ value, path }: { value: DeckInlineText; path?: DeckTextPath }) {
|
|
27
|
+
if (typeof value === "string") return <EditableText path={path}>{value}</EditableText>;
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
{value.map((span, i) => {
|
|
31
|
+
let node: ReactNode = (
|
|
32
|
+
<EditableText path={path && { ...path, span: i }}>{span.text}</EditableText>
|
|
33
|
+
);
|
|
34
|
+
if (span.italic) node = <em className="italic">{node}</em>;
|
|
35
|
+
// Bold pulls the phrase back to full ink: body copy renders muted, so
|
|
36
|
+
// weight alone barely registers at presentation size.
|
|
37
|
+
if (span.bold) node = <strong className="font-semibold text-[var(--deck-ink)]">{node}</strong>;
|
|
38
|
+
return <Fragment key={i}>{node}</Fragment>;
|
|
39
|
+
})}
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Deck Component — rich text.
|
|
46
|
+
*
|
|
47
|
+
* Every size is px against the 1600x900 stage; every color, family and radius
|
|
48
|
+
* is a token (`--deck-*` are set by the enclosing Layout, see ./tokens.ts).
|
|
49
|
+
* Fields are typographic roles, not markup: the schema carries plain strings
|
|
50
|
+
* and marked spans, so nothing here interpolates HTML.
|
|
51
|
+
*/
|
|
52
|
+
export function DeckRichText({ content, slot }: Props) {
|
|
53
|
+
const { kicker, heading, subheading, paragraphs, bullets } = content;
|
|
54
|
+
const at = (field: DeckTextPath["field"], line?: number): DeckTextPath | undefined =>
|
|
55
|
+
slot === undefined ? undefined : { slot, field, line };
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div data-deck-component="richText" className="flex flex-col gap-[28px]">
|
|
59
|
+
{kicker !== undefined && (
|
|
60
|
+
<div className="flex flex-col gap-[18px]">
|
|
61
|
+
<p className="text-[22px] font-semibold tracking-[0.2em] text-[var(--deck-accent)] [text-transform:var(--nav-text-transform)]">
|
|
62
|
+
<EditableText path={at("kicker")}>{kicker}</EditableText>
|
|
63
|
+
</p>
|
|
64
|
+
<span
|
|
65
|
+
aria-hidden="true"
|
|
66
|
+
// Inline, not `self-[var(…)]`: Tailwind never generated CSS for that
|
|
67
|
+
// arbitrary self- value, so the divider silently ignored --deck-align
|
|
68
|
+
// and hugged the left edge on centered layouts.
|
|
69
|
+
style={{ alignSelf: "var(--deck-align, start)" }}
|
|
70
|
+
className="h-[4px] w-[84px] rounded-[var(--radius-inner)] bg-[var(--deck-accent)]"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{heading !== undefined && (
|
|
76
|
+
<h1 className="font-heading text-[88px] leading-[1.06] font-medium tracking-[-0.015em] text-[var(--deck-ink)] [text-transform:var(--heading-text-transform)]">
|
|
77
|
+
<EditableText path={at("heading")}>{heading}</EditableText>
|
|
78
|
+
</h1>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{subheading !== undefined && (
|
|
82
|
+
<h2 className="font-heading text-[38px] leading-[1.25] font-normal text-[var(--deck-ink-muted)] [text-transform:var(--subheading-text-transform)]">
|
|
83
|
+
<EditableText path={at("subheading")}>{subheading}</EditableText>
|
|
84
|
+
</h2>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
{paragraphs !== undefined && paragraphs.length > 0 && (
|
|
88
|
+
<div className="flex max-w-[1000px] flex-col gap-[18px] self-[var(--deck-align,start)]">
|
|
89
|
+
{paragraphs.map((paragraph, i) => (
|
|
90
|
+
<p key={i} className="text-[26px] leading-[1.6] text-[var(--deck-ink-muted)]">
|
|
91
|
+
<InlineText value={paragraph} path={at("paragraphs", i)} />
|
|
92
|
+
</p>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{bullets !== undefined && bullets.length > 0 && (
|
|
98
|
+
<ul className="flex flex-col gap-[16px] self-[var(--deck-align,start)] text-left">
|
|
99
|
+
{bullets.map((bullet, i) => (
|
|
100
|
+
<li key={i} className="flex items-start gap-[18px] text-[26px] leading-[1.45] text-[var(--deck-ink)]">
|
|
101
|
+
<span aria-hidden="true" className="mt-[14px] size-[10px] shrink-0 rounded-full bg-[var(--deck-accent)]" />
|
|
102
|
+
<span>
|
|
103
|
+
<InlineText value={bullet} path={at("bullets", i)} />
|
|
104
|
+
</span>
|
|
105
|
+
</li>
|
|
106
|
+
))}
|
|
107
|
+
</ul>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|