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