@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,572 +0,0 @@
|
|
|
1
|
-
import { MessageSquareIcon, PlusIcon } from "lucide-react";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { Button } from "../components/shared/Button";
|
|
4
|
-
import { DeckChatPanel } from "./DeckChatPanel";
|
|
5
|
-
import { clearDeckChat } from "./deck-chat-history";
|
|
6
|
-
import {
|
|
7
|
-
deleteDeckDraft,
|
|
8
|
-
getDeckDraft,
|
|
9
|
-
hasUnpublishedChanges,
|
|
10
|
-
initDeckDraftStore,
|
|
11
|
-
listDeckDrafts,
|
|
12
|
-
markDeckDraftPublished,
|
|
13
|
-
putDeckDraft,
|
|
14
|
-
recordDeckPublish,
|
|
15
|
-
type DeckDraftRow,
|
|
16
|
-
} from "../lib/deck-dexie";
|
|
17
|
-
import {
|
|
18
|
-
applyDeckTextEdit,
|
|
19
|
-
duplicateDeckSlide,
|
|
20
|
-
moveDeckSlide,
|
|
21
|
-
removeDeckSlide,
|
|
22
|
-
type DeckEditResult,
|
|
23
|
-
type DeckTextPath,
|
|
24
|
-
} from "../lib/deck-edit";
|
|
25
|
-
import type { Deck, DeckMediaAsset } from "../schemas/deck";
|
|
26
|
-
import {
|
|
27
|
-
deletePublishedDeck,
|
|
28
|
-
listPublishedDecks,
|
|
29
|
-
publishDeck,
|
|
30
|
-
readPublishedDeck,
|
|
31
|
-
type DeckApiFailure,
|
|
32
|
-
type PublishedDeckSummary,
|
|
33
|
-
} from "./deck-api";
|
|
34
|
-
import { DeckBuilderTopBar } from "./DeckBuilderTopBar";
|
|
35
|
-
import { DeckEditor } from "./DeckEditor";
|
|
36
|
-
import { DeckLibrary } from "./DeckLibrary";
|
|
37
|
-
import { newDeckId, starterDeck } from "./starter-deck";
|
|
38
|
-
import type { DeckGridEntry } from "./types";
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* The Deck Builder shell (#34).
|
|
42
|
-
*
|
|
43
|
-
* Builder mode is its own ROUTE rather than an overlay on a viewer page, which
|
|
44
|
-
* is what makes "the sidebar drops out for a top bar" and "leaving restores
|
|
45
|
-
* portal chrome" trivially true — and what keeps every byte of the deck runtime
|
|
46
|
-
* off ordinary viewer pages, where the entry point is a plain server-rendered
|
|
47
|
-
* `<a>` FAB. `/present` is the same shape for the same reasons.
|
|
48
|
-
*
|
|
49
|
-
* The working deck is a Deck Draft in IndexedDB (ADR 0002): the builder reads
|
|
50
|
-
* the decks branch to list and to reopen, and writes to it only when the author
|
|
51
|
-
* presses Publish.
|
|
52
|
-
*/
|
|
53
|
-
interface Props {
|
|
54
|
-
/** Keys the draft database, so two portals in one browser never collide. */
|
|
55
|
-
siteId: string;
|
|
56
|
-
/** The other half of that key: a `deckDraftScope` digest of the caller's
|
|
57
|
-
* identity, so two people sharing a browser profile never share drafts. */
|
|
58
|
-
draftScope: string;
|
|
59
|
-
/** Pre-resolved portal media for the runtime's resolver seam. */
|
|
60
|
-
mediaAssets: Record<string, DeckMediaAsset>;
|
|
61
|
-
/** Where "Leave builder" goes — the page the FAB was pressed on. */
|
|
62
|
-
exitHref: string;
|
|
63
|
-
/** Server-decided: editor, or a member of a deck-enabled audience. */
|
|
64
|
-
canCreate: boolean;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
type Notice = { tone: "error" | "info"; text: string; details?: string[] } | null;
|
|
68
|
-
|
|
69
|
-
/** A pending 409: the author has to choose, so it cannot be a toast. */
|
|
70
|
-
type Conflict = { deckId: string; message: string } | null;
|
|
71
|
-
|
|
72
|
-
export function DeckBuilderApp({ siteId, draftScope, mediaAssets, exitHref, canCreate }: Props) {
|
|
73
|
-
const [drafts, setDrafts] = useState<DeckDraftRow[]>([]);
|
|
74
|
-
const [published, setPublished] = useState<PublishedDeckSummary[]>([]);
|
|
75
|
-
const [openDeckId, setOpenDeckId] = useState<string | null>(null);
|
|
76
|
-
const [slide, setSlide] = useState(0);
|
|
77
|
-
const [loading, setLoading] = useState(true);
|
|
78
|
-
const [busy, setBusy] = useState(false);
|
|
79
|
-
const [notice, setNotice] = useState<Notice>(null);
|
|
80
|
-
const [conflict, setConflict] = useState<Conflict>(null);
|
|
81
|
-
// The assistant is the primary authoring surface (#35), so it opens with the
|
|
82
|
-
// deck rather than waiting to be asked for.
|
|
83
|
-
const [chatOpen, setChatOpen] = useState(true);
|
|
84
|
-
// A chat turn is a writer to the Deck Draft. Publish locks while one is in
|
|
85
|
-
// flight — the same reason #39's inline editing will lock against it.
|
|
86
|
-
const [chatBusy, setChatBusy] = useState(false);
|
|
87
|
-
// Dexie writes are the source of truth; this only stops a resolved promise
|
|
88
|
-
// from setting state after the island is gone.
|
|
89
|
-
const alive = useRef(true);
|
|
90
|
-
/**
|
|
91
|
-
* The last deck this component wrote, held synchronously so a second edit
|
|
92
|
-
* committed inside the first one's Dexie round-trip builds on it rather than
|
|
93
|
-
* on the stale `openRow.deck`.
|
|
94
|
-
*
|
|
95
|
-
* It carries its own deck id and `currentDeck` refuses a mismatch, which is
|
|
96
|
-
* what makes it SELF-INVALIDATING rather than dependent on every writer
|
|
97
|
-
* remembering to clear it. That is not belt-and-braces: a chat turn started on
|
|
98
|
-
* deck A can resolve after the author has opened deck B — the panel unmounts
|
|
99
|
-
* without aborting the stream — and an unguarded ref then hands deck A to
|
|
100
|
-
* every subsequent edit while the stage renders B, writing those edits into
|
|
101
|
-
* A's row where they are never seen again.
|
|
102
|
-
*/
|
|
103
|
-
const editBase = useRef<{ deckId: string; deck: Deck } | null>(null);
|
|
104
|
-
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
alive.current = true;
|
|
107
|
-
return () => {
|
|
108
|
-
alive.current = false;
|
|
109
|
-
};
|
|
110
|
-
}, []);
|
|
111
|
-
|
|
112
|
-
const refreshDrafts = useCallback(async () => {
|
|
113
|
-
const rows = await listDeckDrafts();
|
|
114
|
-
if (alive.current) setDrafts(rows);
|
|
115
|
-
}, []);
|
|
116
|
-
|
|
117
|
-
useEffect(() => {
|
|
118
|
-
initDeckDraftStore(siteId, draftScope);
|
|
119
|
-
void (async () => {
|
|
120
|
-
await refreshDrafts();
|
|
121
|
-
const listed = await listPublishedDecks();
|
|
122
|
-
if (!alive.current) return;
|
|
123
|
-
// A failed listing is not a failed builder: the drafts on this device are
|
|
124
|
-
// still openable, and saying so beats an empty grid that looks like data
|
|
125
|
-
// loss. 401 is the exception — that is a session to renew, not a blip.
|
|
126
|
-
if (listed.ok) setPublished(listed.value);
|
|
127
|
-
else setNotice(noticeFor(listed, "Could not list your published decks."));
|
|
128
|
-
setLoading(false);
|
|
129
|
-
})();
|
|
130
|
-
}, [siteId, draftScope, refreshDrafts]);
|
|
131
|
-
|
|
132
|
-
// Belt to the id-tag's braces: drop a base the open deck no longer has any
|
|
133
|
-
// use for, so a closed deck's body is not held alive for the session.
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
editBase.current = null;
|
|
136
|
-
}, [openDeckId]);
|
|
137
|
-
|
|
138
|
-
const entries = useMemo(() => buildEntries(drafts, published), [drafts, published]);
|
|
139
|
-
const openRow = openDeckId === null ? null : (drafts.find((row) => row.deckId === openDeckId) ?? null);
|
|
140
|
-
|
|
141
|
-
const openDeck = useCallback(
|
|
142
|
-
async (id: string) => {
|
|
143
|
-
setNotice(null);
|
|
144
|
-
setSlide(0);
|
|
145
|
-
// Already staged: open the draft. Never re-read — the draft is the newer
|
|
146
|
-
// of the two by construction, and clobbering it with the published body
|
|
147
|
-
// is precisely the data loss Dexie staging exists to prevent.
|
|
148
|
-
if (await getDeckDraft(id)) {
|
|
149
|
-
if (alive.current) setOpenDeckId(id);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
setBusy(true);
|
|
153
|
-
const read = await readPublishedDeck(id);
|
|
154
|
-
if (!alive.current) return;
|
|
155
|
-
setBusy(false);
|
|
156
|
-
if (!read.ok) {
|
|
157
|
-
setNotice(noticeFor(read, "Could not open that deck."));
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
// The sidecar's own timestamp, not "now": opening a deck to look at it
|
|
161
|
-
// must not sort it to the top of the grid.
|
|
162
|
-
await markDeckDraftPublished(read.value.deck, read.value.version, read.value.meta?.updatedAt);
|
|
163
|
-
await refreshDrafts();
|
|
164
|
-
if (alive.current) setOpenDeckId(id);
|
|
165
|
-
},
|
|
166
|
-
[refreshDrafts],
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
const createDeck = useCallback(async () => {
|
|
170
|
-
setNotice(null);
|
|
171
|
-
const title = `Untitled deck ${new Date().toLocaleDateString("en-US", { month: "short", day: "numeric" })}`;
|
|
172
|
-
const deck = starterDeck(newDeckId(title), title);
|
|
173
|
-
await putDeckDraft(deck);
|
|
174
|
-
await refreshDrafts();
|
|
175
|
-
if (!alive.current) return;
|
|
176
|
-
setSlide(0);
|
|
177
|
-
setOpenDeckId(deck.id);
|
|
178
|
-
}, [refreshDrafts]);
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* A validated deck from the assistant. LOCAL ONLY: it lands in the Deck Draft
|
|
182
|
-
* exactly like any other edit, and publishing stays the author's explicit act
|
|
183
|
-
* (#34) — the authoring loop performs no server writes at all (ADR 0002).
|
|
184
|
-
*/
|
|
185
|
-
const applyDeck = useCallback(
|
|
186
|
-
async (next: Deck) => {
|
|
187
|
-
// A turn is a writer too, so it owns the edit base from here on — tagged
|
|
188
|
-
// with its own deck id, because a turn can resolve after the author has
|
|
189
|
-
// moved to a different deck (see the ref's own comment).
|
|
190
|
-
editBase.current = { deckId: next.id, deck: next };
|
|
191
|
-
await putDeckDraft(next);
|
|
192
|
-
await refreshDrafts();
|
|
193
|
-
if (!alive.current) return;
|
|
194
|
-
// A turn can shorten the deck out from under the filmstrip selection.
|
|
195
|
-
setSlide((current) => Math.min(current, next.slides.length - 1));
|
|
196
|
-
},
|
|
197
|
-
[refreshDrafts],
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Stage the result of a direct-manipulation edit (#39).
|
|
202
|
-
*
|
|
203
|
-
* The ops in `lib/deck-edit` end in `parseDeck`, so a refusal here is the
|
|
204
|
-
* schema's own message — an over-long heading, a deleted last slide, a
|
|
205
|
-
* duplicate past the slide cap. Nothing invalid is ever staged, and a refused
|
|
206
|
-
* edit leaves the draft exactly as it was.
|
|
207
|
-
*/
|
|
208
|
-
const stageEdit = useCallback(
|
|
209
|
-
async (result: DeckEditResult, focus?: (deck: Deck) => number) => {
|
|
210
|
-
if (!result.ok) {
|
|
211
|
-
setNotice({ tone: "error", text: "That change did not fit the deck's rules.", details: result.errors });
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
setNotice(null);
|
|
215
|
-
// Synchronously, BEFORE the await: two commits can land inside one Dexie
|
|
216
|
-
// round-trip (blurring one field is what activates the next), and until
|
|
217
|
-
// `refreshDrafts` returns, `openRow.deck` is still the pre-edit deck. The
|
|
218
|
-
// second edit would then be computed against it and overwrite the first.
|
|
219
|
-
editBase.current = { deckId: result.deck.id, deck: result.deck };
|
|
220
|
-
await putDeckDraft(result.deck);
|
|
221
|
-
await refreshDrafts();
|
|
222
|
-
if (!alive.current) return;
|
|
223
|
-
const last = result.deck.slides.length - 1;
|
|
224
|
-
setSlide((current) => Math.max(0, Math.min(focus ? focus(result.deck) : current, last)));
|
|
225
|
-
},
|
|
226
|
-
[refreshDrafts],
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* The freshest staged deck. Read through a callback rather than computed at
|
|
231
|
-
* render: `editBase` is written synchronously by `stageEdit` and re-rendering
|
|
232
|
-
* is exactly what has not happened yet at the moment the next edit commits.
|
|
233
|
-
*/
|
|
234
|
-
const currentDeck = useCallback((): Deck | null => {
|
|
235
|
-
if (openRow === null) return null;
|
|
236
|
-
const staged = editBase.current;
|
|
237
|
-
return staged !== null && staged.deckId === openRow.deckId ? staged.deck : openRow.deck;
|
|
238
|
-
}, [openRow]);
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* The stage's inline text commits. `slide` is the selection as of this render
|
|
242
|
-
* — the same one the stage is drawing, which is what the path addresses.
|
|
243
|
-
*/
|
|
244
|
-
const editText = useCallback(
|
|
245
|
-
(path: DeckTextPath, text: string) => {
|
|
246
|
-
const deck = currentDeck();
|
|
247
|
-
if (!deck || chatBusy) return;
|
|
248
|
-
const index = Math.min(slide, deck.slides.length - 1);
|
|
249
|
-
void stageEdit(applyDeckTextEdit(deck, index, path, text));
|
|
250
|
-
},
|
|
251
|
-
[currentDeck, slide, chatBusy, stageEdit],
|
|
252
|
-
);
|
|
253
|
-
|
|
254
|
-
const moveSlide = useCallback(
|
|
255
|
-
(from: number, to: number) => {
|
|
256
|
-
const deck = currentDeck();
|
|
257
|
-
if (!deck || chatBusy) return;
|
|
258
|
-
// Follow the slide that moved, not the position it left.
|
|
259
|
-
void stageEdit(moveDeckSlide(deck, from, to), () => to);
|
|
260
|
-
},
|
|
261
|
-
[currentDeck, chatBusy, stageEdit],
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
const duplicateSlide = useCallback(
|
|
265
|
-
(index: number) => {
|
|
266
|
-
const deck = currentDeck();
|
|
267
|
-
if (!deck || chatBusy) return;
|
|
268
|
-
// Land on the copy — it is the thing the author is about to change.
|
|
269
|
-
void stageEdit(duplicateDeckSlide(deck, index), () => index + 1);
|
|
270
|
-
},
|
|
271
|
-
[currentDeck, chatBusy, stageEdit],
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
const deleteSlide = useCallback(
|
|
275
|
-
(index: number) => {
|
|
276
|
-
const deck = currentDeck();
|
|
277
|
-
if (!deck || chatBusy) return;
|
|
278
|
-
void stageEdit(removeDeckSlide(deck, index), () => index - 1);
|
|
279
|
-
},
|
|
280
|
-
[currentDeck, chatBusy, stageEdit],
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
const removeDeck = useCallback(
|
|
284
|
-
async (id: string) => {
|
|
285
|
-
setNotice(null);
|
|
286
|
-
setBusy(true);
|
|
287
|
-
// Server first: a local-only delete that fails upstream would leave a
|
|
288
|
-
// published deck this browser can no longer see, let alone remove.
|
|
289
|
-
if (published.some((p) => p.id === id)) {
|
|
290
|
-
const deleted = await deletePublishedDeck(id);
|
|
291
|
-
if (!alive.current) return;
|
|
292
|
-
if (!deleted.ok) {
|
|
293
|
-
setBusy(false);
|
|
294
|
-
setNotice(noticeFor(deleted, "Could not delete that deck."));
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
setPublished((prev) => prev.filter((p) => p.id !== id));
|
|
298
|
-
}
|
|
299
|
-
await deleteDeckDraft(id);
|
|
300
|
-
// The conversation is about a deck that no longer exists — and a new deck
|
|
301
|
-
// could reuse the id, which would silently inherit it.
|
|
302
|
-
clearDeckChat(draftScope, id);
|
|
303
|
-
await refreshDrafts();
|
|
304
|
-
if (!alive.current) return;
|
|
305
|
-
setBusy(false);
|
|
306
|
-
if (openDeckId === id) setOpenDeckId(null);
|
|
307
|
-
},
|
|
308
|
-
[published, openDeckId, refreshDrafts, draftScope],
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
const publish = useCallback(async () => {
|
|
312
|
-
if (!openRow) return;
|
|
313
|
-
setNotice(null);
|
|
314
|
-
setConflict(null);
|
|
315
|
-
setBusy(true);
|
|
316
|
-
// `currentDeck()`, not `openRow.deck`: clicking Publish while a text field is
|
|
317
|
-
// open BLURS it first, so the commit and the click land inside one Dexie
|
|
318
|
-
// round-trip and the row is still the pre-edit deck at this line. Publishing
|
|
319
|
-
// that would send a deck the author had just changed on screen.
|
|
320
|
-
const publishing = currentDeck() ?? openRow.deck;
|
|
321
|
-
const result = await publishDeck(publishing, openRow.baseVersion);
|
|
322
|
-
if (!alive.current) return;
|
|
323
|
-
setBusy(false);
|
|
324
|
-
if (!result.ok) {
|
|
325
|
-
if (result.kind === "conflict") {
|
|
326
|
-
setConflict({ deckId: openRow.deckId, message: result.message });
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
setNotice(noticeFor(result, "Could not publish this deck."));
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
// The durable fix #35 deferred to #39: re-read the row and write back only
|
|
333
|
-
// the publish BASELINE, leaving `deck` as whatever is current by now. An
|
|
334
|
-
// inline edit committed during the round-trip therefore survives, and
|
|
335
|
-
// `hasUnpublishedChanges` reports it — where the old pre-request snapshot
|
|
336
|
-
// would have discarded it silently.
|
|
337
|
-
//
|
|
338
|
-
// The chat panel stays locked during a publish anyway (`disabled={busy}`),
|
|
339
|
-
// and it still carries weight: `recordDeckPublish` makes an INLINE edit
|
|
340
|
-
// during the round-trip safe, but a whole-deck turn landing inside a publish
|
|
341
|
-
// would still commit whichever half of the deck Dexie held at that instant.
|
|
342
|
-
await recordDeckPublish(openRow.deckId, publishing, result.value.version);
|
|
343
|
-
await refreshDrafts();
|
|
344
|
-
if (!alive.current) return;
|
|
345
|
-
setPublished((prev) => {
|
|
346
|
-
const next = prev.filter((p) => p.id !== openRow.deckId);
|
|
347
|
-
next.push({ id: openRow.deckId, version: result.value.version, meta: result.value.meta });
|
|
348
|
-
return next;
|
|
349
|
-
});
|
|
350
|
-
setNotice({ tone: "info", text: "Published." });
|
|
351
|
-
}, [openRow, currentDeck, refreshDrafts]);
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* The MVP's answer to a 409: RELOAD-NEWER, and only on request.
|
|
355
|
-
*
|
|
356
|
-
* Nothing is merged and nothing is overwritten silently — the author is told
|
|
357
|
-
* the deck moved and chooses between taking the newer copy (their staged
|
|
358
|
-
* edits are dropped, which is why the button says so) and keeping theirs
|
|
359
|
-
* (nothing is written; the draft stays exactly as it was). A real merge needs
|
|
360
|
-
* the revision history #35's authoring loop will produce, and guessing at one
|
|
361
|
-
* now would be a way to lose work quietly.
|
|
362
|
-
*/
|
|
363
|
-
const reloadPublished = useCallback(async () => {
|
|
364
|
-
if (!conflict) return;
|
|
365
|
-
setBusy(true);
|
|
366
|
-
const read = await readPublishedDeck(conflict.deckId);
|
|
367
|
-
if (!alive.current) return;
|
|
368
|
-
setBusy(false);
|
|
369
|
-
if (!read.ok) {
|
|
370
|
-
setNotice(noticeFor(read, "Could not reload that deck."));
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
// The deck is being REPLACED, so the staged edit base has to go with it —
|
|
374
|
-
// the open deck id does not change here, so nothing else would drop it, and
|
|
375
|
-
// keeping it would let the next inline edit rebuild the copy just discarded.
|
|
376
|
-
editBase.current = null;
|
|
377
|
-
await markDeckDraftPublished(read.value.deck, read.value.version, read.value.meta?.updatedAt);
|
|
378
|
-
await refreshDrafts();
|
|
379
|
-
if (!alive.current) return;
|
|
380
|
-
setConflict(null);
|
|
381
|
-
setSlide(0);
|
|
382
|
-
setNotice({ tone: "info", text: "Reloaded the published deck. Your staged edits were replaced." });
|
|
383
|
-
}, [conflict, refreshDrafts]);
|
|
384
|
-
|
|
385
|
-
const dirty = openRow ? hasUnpublishedChanges(openRow) : false;
|
|
386
|
-
|
|
387
|
-
return (
|
|
388
|
-
<div data-deck-builder-app="" className="flex h-screen min-h-0 flex-col bg-base font-body text-base-contrast">
|
|
389
|
-
<DeckBuilderTopBar
|
|
390
|
-
exitHref={exitHref}
|
|
391
|
-
onBackToLibrary={openDeckId === null ? undefined : () => setOpenDeckId(null)}
|
|
392
|
-
primaryAction={
|
|
393
|
-
openRow ? (
|
|
394
|
-
// Locked during a turn: the deck is being rewritten, so publishing
|
|
395
|
-
// now would commit whichever half of it Dexie held at that instant.
|
|
396
|
-
<Button variant="brand" onClick={publish} disabled={busy || chatBusy || !dirty}>
|
|
397
|
-
{dirty ? "Publish deck" : "Published"}
|
|
398
|
-
</Button>
|
|
399
|
-
) : canCreate ? (
|
|
400
|
-
<Button variant="brand" onClick={createDeck} disabled={busy}>
|
|
401
|
-
<PlusIcon size={14} className="mr-1.5" aria-hidden="true" />
|
|
402
|
-
New deck
|
|
403
|
-
</Button>
|
|
404
|
-
) : null
|
|
405
|
-
}
|
|
406
|
-
status={
|
|
407
|
-
openRow
|
|
408
|
-
? `${dirty ? "Draft · staged on this device" : "Published"} · ${openRow.deck.slides.length} slides`
|
|
409
|
-
: loading
|
|
410
|
-
? "Loading your decks…"
|
|
411
|
-
: `${entries.length} ${entries.length === 1 ? "deck" : "decks"}`
|
|
412
|
-
}
|
|
413
|
-
/>
|
|
414
|
-
|
|
415
|
-
{/* The builder body: content pane left, assistant docked right. The pane
|
|
416
|
-
scrolls; the chat column is IN FLOW rather than fixed (unlike
|
|
417
|
-
ChatPanel's docked mode), because the builder owns the whole viewport
|
|
418
|
-
and has no centered band for a fixed panel to shift out from under. */}
|
|
419
|
-
<div className="flex min-h-0 flex-1">
|
|
420
|
-
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
421
|
-
{conflict && (
|
|
422
|
-
<div role="alert" className="mx-auto flex max-w-screen-xl flex-wrap items-center gap-3 px-4 pt-4">
|
|
423
|
-
<p className="text-xs text-base-contrast">{conflict.message}</p>
|
|
424
|
-
<Button variant="secondary" onClick={reloadPublished} disabled={busy}>
|
|
425
|
-
Reload the published deck
|
|
426
|
-
</Button>
|
|
427
|
-
<Button variant="ghost" onClick={() => setConflict(null)}>
|
|
428
|
-
Keep my version
|
|
429
|
-
</Button>
|
|
430
|
-
</div>
|
|
431
|
-
)}
|
|
432
|
-
{notice && (
|
|
433
|
-
<div
|
|
434
|
-
role={notice.tone === "error" ? "alert" : "status"}
|
|
435
|
-
className="mx-auto max-w-screen-xl px-4 pt-4 text-xs text-base-contrast"
|
|
436
|
-
>
|
|
437
|
-
<p>{notice.text}</p>
|
|
438
|
-
{notice.details && (
|
|
439
|
-
<ul className="mt-1 list-disc pl-4 text-base-contrast-light">
|
|
440
|
-
{notice.details.map((detail) => (
|
|
441
|
-
<li key={detail}>{detail}</li>
|
|
442
|
-
))}
|
|
443
|
-
</ul>
|
|
444
|
-
)}
|
|
445
|
-
</div>
|
|
446
|
-
)}
|
|
447
|
-
|
|
448
|
-
{openRow ? (
|
|
449
|
-
<DeckEditor
|
|
450
|
-
deck={openRow.deck}
|
|
451
|
-
mediaAssets={mediaAssets}
|
|
452
|
-
currentSlide={slide}
|
|
453
|
-
onSelectSlide={setSlide}
|
|
454
|
-
onBack={() => setOpenDeckId(null)}
|
|
455
|
-
onEditText={editText}
|
|
456
|
-
onMoveSlide={moveSlide}
|
|
457
|
-
onDuplicateSlide={duplicateSlide}
|
|
458
|
-
onDeleteSlide={deleteSlide}
|
|
459
|
-
// Only a streaming turn locks the stage. `busy` (publish, reopen)
|
|
460
|
-
// deliberately does NOT: those are short, and the whole reason the
|
|
461
|
-
// publish baseline is now re-read is so a mid-flight edit is safe.
|
|
462
|
-
locked={chatBusy}
|
|
463
|
-
statusLine={
|
|
464
|
-
dirty
|
|
465
|
-
? openRow.baseVersion === null
|
|
466
|
-
? "Never published — staged on this device"
|
|
467
|
-
: "Unpublished changes on this device"
|
|
468
|
-
: "Published"
|
|
469
|
-
}
|
|
470
|
-
/>
|
|
471
|
-
) : loading ? (
|
|
472
|
-
<p className="mx-auto max-w-screen-xl px-4 pt-8 text-sm text-base-contrast-light">Loading your decks…</p>
|
|
473
|
-
) : (
|
|
474
|
-
<DeckLibrary
|
|
475
|
-
entries={entries}
|
|
476
|
-
mediaAssets={mediaAssets}
|
|
477
|
-
canCreate={canCreate}
|
|
478
|
-
onOpen={(id) => void openDeck(id)}
|
|
479
|
-
onCreate={() => void createDeck()}
|
|
480
|
-
onDelete={(id) => void removeDeck(id)}
|
|
481
|
-
/>
|
|
482
|
-
)}
|
|
483
|
-
</div>
|
|
484
|
-
|
|
485
|
-
{/* Docked only while a deck is open. A turn rewrites the WHOLE deck
|
|
486
|
-
(ADR 0001), so there has to be one to rewrite — creating is the
|
|
487
|
-
"New deck" button's job, and the starter deck is what the first
|
|
488
|
-
turn edits. */}
|
|
489
|
-
{openRow &&
|
|
490
|
-
(chatOpen ? (
|
|
491
|
-
<DeckChatPanel
|
|
492
|
-
deck={openRow.deck}
|
|
493
|
-
// Same staleness as publish, and the acceptance criterion "a manual
|
|
494
|
-
// edit is visible to the model on the next turn" is exactly this:
|
|
495
|
-
// Send blurs the open field, so the row has not caught up yet.
|
|
496
|
-
getDeck={currentDeck}
|
|
497
|
-
scope={draftScope}
|
|
498
|
-
onDeck={(next) => void applyDeck(next)}
|
|
499
|
-
onBusyChange={setChatBusy}
|
|
500
|
-
// The other half of the publish lock. Publish is disabled while a
|
|
501
|
-
// turn streams; this is what stops a turn STARTING inside a
|
|
502
|
-
// publish, whose tail rewrites the draft row from a pre-request
|
|
503
|
-
// snapshot (see the ⚠️ in `publish`).
|
|
504
|
-
disabled={busy}
|
|
505
|
-
onClose={() => setChatOpen(false)}
|
|
506
|
-
/>
|
|
507
|
-
) : (
|
|
508
|
-
<button
|
|
509
|
-
type="button"
|
|
510
|
-
onClick={() => setChatOpen(true)}
|
|
511
|
-
aria-label="Show deck assistant"
|
|
512
|
-
className="fixed right-4 bottom-4 z-50 flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90"
|
|
513
|
-
>
|
|
514
|
-
<MessageSquareIcon size={18} aria-hidden="true" />
|
|
515
|
-
</button>
|
|
516
|
-
))}
|
|
517
|
-
</div>
|
|
518
|
-
</div>
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/** The grid's rows: every draft on this device, joined to the branch listing. */
|
|
523
|
-
function buildEntries(drafts: DeckDraftRow[], published: PublishedDeckSummary[]): DeckGridEntry[] {
|
|
524
|
-
const byId = new Map<string, DeckGridEntry>();
|
|
525
|
-
for (const row of drafts) {
|
|
526
|
-
byId.set(row.deckId, {
|
|
527
|
-
id: row.deckId,
|
|
528
|
-
title: row.deck.title,
|
|
529
|
-
slideCount: row.deck.slides.length,
|
|
530
|
-
updatedAt: row.updatedAt,
|
|
531
|
-
draft: { deck: row.deck, baseVersion: row.baseVersion },
|
|
532
|
-
published: null,
|
|
533
|
-
unpublishedChanges: hasUnpublishedChanges(row),
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
for (const summary of published) {
|
|
537
|
-
const existing = byId.get(summary.id);
|
|
538
|
-
if (existing) {
|
|
539
|
-
existing.published = { version: summary.version };
|
|
540
|
-
continue;
|
|
541
|
-
}
|
|
542
|
-
// Published from another browser (or after this one's draft was cleared).
|
|
543
|
-
// Listed from the meta sidecar alone — opening it is what fetches the body.
|
|
544
|
-
byId.set(summary.id, {
|
|
545
|
-
id: summary.id,
|
|
546
|
-
title: summary.meta?.title ?? summary.id,
|
|
547
|
-
slideCount: summary.meta?.slideCount ?? 0,
|
|
548
|
-
updatedAt: summary.meta?.updatedAt ?? new Date(0).toISOString(),
|
|
549
|
-
draft: null,
|
|
550
|
-
published: { version: summary.version },
|
|
551
|
-
unpublishedChanges: false,
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
return [...byId.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function noticeFor(failure: { ok: false } & DeckApiFailure, fallback: string): Notice {
|
|
558
|
-
switch (failure.kind) {
|
|
559
|
-
case "invalid":
|
|
560
|
-
return { tone: "error", text: "The deck did not pass validation.", details: failure.details };
|
|
561
|
-
case "forbidden":
|
|
562
|
-
return { tone: "error", text: failure.message };
|
|
563
|
-
case "notFound":
|
|
564
|
-
return { tone: "error", text: "That deck is no longer there." };
|
|
565
|
-
case "conflict":
|
|
566
|
-
case "unavailable":
|
|
567
|
-
case "failed":
|
|
568
|
-
return { tone: "error", text: `${fallback} ${failure.message}` };
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
export default DeckBuilderApp;
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { ArrowLeftIcon, LayoutGridIcon } from "lucide-react";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
3
|
-
import { IconButton } from "../components/shared/IconButton";
|
|
4
|
-
import { Tooltip } from "../components/shared/Tooltip";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Builder mode's chrome (the 2026-08-05 prototype verdict: variant B's bar).
|
|
8
|
-
*
|
|
9
|
-
* A hand-for-hand match of EditorShell's toolbar — full-bleed bordered band,
|
|
10
|
-
* `max-w-screen-xl` inner grid of three columns at `h-16`, framed icon buttons
|
|
11
|
-
* on the right — because this bar REPLACES the portal sidebar while builder
|
|
12
|
-
* mode is open, and the two surfaces should not read as different products.
|
|
13
|
-
* Not `fixed`: the builder owns the whole viewport and lays out as a column, so
|
|
14
|
-
* the bar is a flex child that nothing can scroll under.
|
|
15
|
-
*/
|
|
16
|
-
interface Props {
|
|
17
|
-
/** Left cell: the primary action for whichever view is showing. */
|
|
18
|
-
primaryAction: ReactNode;
|
|
19
|
-
/** Center cell: one line of draft/deck status. */
|
|
20
|
-
status: ReactNode;
|
|
21
|
-
/** Where leaving builder mode goes — the page the FAB was pressed on. */
|
|
22
|
-
exitHref: string;
|
|
23
|
-
/** Shown while a deck is open; returns to the grid. */
|
|
24
|
-
onBackToLibrary?: () => void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function DeckBuilderTopBar({ primaryAction, status, exitHref, onBackToLibrary }: Props) {
|
|
28
|
-
return (
|
|
29
|
-
<div className="shrink-0 border-b border-base-200 bg-base">
|
|
30
|
-
<div className="mx-auto grid h-16 max-w-screen-xl grid-cols-3 items-center gap-4 px-4">
|
|
31
|
-
<div className="flex min-w-0 items-center gap-2">{primaryAction}</div>
|
|
32
|
-
<div className="flex items-center justify-center">
|
|
33
|
-
<p className="truncate text-xs text-base-contrast-light">{status}</p>
|
|
34
|
-
</div>
|
|
35
|
-
<div className="flex items-center justify-end gap-2">
|
|
36
|
-
{onBackToLibrary && (
|
|
37
|
-
<Tooltip side="bottom" content="Deck library">
|
|
38
|
-
<IconButton
|
|
39
|
-
icon={<LayoutGridIcon size={16} />}
|
|
40
|
-
label="Deck library"
|
|
41
|
-
size="md"
|
|
42
|
-
onClick={onBackToLibrary}
|
|
43
|
-
className="border border-base-200 bg-base-accent"
|
|
44
|
-
/>
|
|
45
|
-
</Tooltip>
|
|
46
|
-
)}
|
|
47
|
-
<div aria-hidden="true" className="mx-1 h-6 w-px self-center bg-base-200" />
|
|
48
|
-
{/* An anchor, not a Button with a click handler: leaving builder mode
|
|
49
|
-
is a navigation, and it should survive middle-click and a failed
|
|
50
|
-
hydration alike. Classes mirror Button variant="secondary". */}
|
|
51
|
-
<a
|
|
52
|
-
href={exitHref}
|
|
53
|
-
className="inline-flex cursor-pointer items-center justify-center rounded border border-base-200 px-3 py-1.5 text-xs font-medium text-base-contrast transition-colors hover:bg-base-accent"
|
|
54
|
-
>
|
|
55
|
-
<ArrowLeftIcon size={14} className="mr-1.5" aria-hidden="true" />
|
|
56
|
-
Leave builder
|
|
57
|
-
</a>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
);
|
|
62
|
-
}
|