@drawnagency/primitives 0.1.117 → 0.1.119
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-O37AV27P.js → chunk-5JYHTHVR.js} +28 -0
- package/dist/{chunk-W72BPGR3.js → chunk-BZKHHUIN.js} +44 -8
- package/dist/{chunk-CEFFP3CH.js → chunk-G6O3TYPJ.js} +297 -83
- package/dist/{chunk-L7WTERU5.js → chunk-HFHM4MNK.js} +33 -10
- package/dist/{chunk-RIITGTV3.js → chunk-LQHBC7C6.js} +37 -2
- package/dist/{chunk-VY3IFGE3.js → chunk-QAMAIDXZ.js} +1 -1
- package/dist/{chunk-LNQUUC2S.js → chunk-RIN2QCYJ.js} +9 -11
- package/dist/{chunk-HUOW3ZV2.js → chunk-UNFPATDE.js} +8 -10
- package/dist/{chunk-MSBHMY3C.js → chunk-YKKGVSJH.js} +35 -37
- package/dist/{chunk-67P34WNW.js → chunk-ZKVQMIMJ.js} +3 -20
- package/dist/collab/rebase.js +4 -5
- package/dist/components/chat/ChatComposer.d.ts +0 -4
- package/dist/components/chat/ChatComposer.d.ts.map +1 -1
- package/dist/components/chat/ChatPanel.d.ts +2 -3
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/chat/markdown.d.ts +2 -3
- package/dist/components/chat/markdown.d.ts.map +1 -1
- package/dist/components/editor/PagesModal.d.ts +0 -2
- package/dist/components/editor/PagesModal.d.ts.map +1 -1
- package/dist/components/editor/index.js +15 -19
- package/dist/components/primitives/EditablePlainText.d.ts.map +1 -1
- package/dist/components/primitives/index.js +11 -15
- package/dist/components/sections/brand-guide.js +10 -13
- package/dist/components/sections/register-schemas.js +16 -21
- package/dist/components/sections/register.js +16 -21
- package/dist/components/shell/AudienceChips.d.ts +1 -3
- package/dist/components/shell/AudienceChips.d.ts.map +1 -1
- package/dist/components/shell/AudienceDetailsPanel.d.ts +3 -1
- package/dist/components/shell/AudienceDetailsPanel.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts +0 -34
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
- package/dist/hooks/useSortableNavRow.d.ts +1 -1
- package/dist/hooks/useSortableRow.d.ts +1 -1
- package/dist/index.js +83 -90
- package/dist/lib/index-ops.js +5 -6
- package/dist/lib/index.js +27 -32
- package/dist/lib/pages.d.ts +0 -2
- package/dist/lib/pages.d.ts.map +1 -1
- package/dist/lib/sidecars.js +2 -2
- package/dist/lib/site-validation.js +13 -14
- package/dist/lib/storage-tier.js +4 -4
- package/dist/schemas/index.js +42 -45
- package/dist/schemas/site-config.d.ts +1 -277
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/package.json +2 -42
- package/src/components/chat/ChatComposer.tsx +0 -4
- package/src/components/chat/ChatPanel.tsx +2 -3
- package/src/components/chat/markdown.ts +2 -3
- package/src/components/editor/PagesModal.tsx +1 -15
- package/src/components/editor/StatusIndicator.tsx +4 -4
- package/src/components/primitives/EditablePlainText.tsx +27 -3
- package/src/components/shell/AudienceChips.tsx +1 -3
- package/src/components/shell/AudienceDetailsPanel.tsx +6 -4
- package/src/components/shell/EditorShell.tsx +18 -114
- package/src/components/shell/SiteSettingsModal.tsx +0 -11
- package/src/lib/pages.ts +0 -22
- package/src/schemas/index.ts +0 -4
- package/src/schemas/site-config.ts +4 -32
- package/dist/chunk-4TZJZU2T.js +0 -92
- package/dist/chunk-DIMDRBFK.js +0 -11
- package/dist/chunk-FV7JQRCN.js +0 -94
- package/dist/chunk-IIDKW7AV.js +0 -41
- package/dist/chunk-JCDZAFX2.js +0 -224
- package/dist/chunk-MM7KZS5C.js +0 -30
- package/dist/chunk-QQ3S3P5P.js +0 -38
- package/dist/chunk-TEOAKGZA.js +0 -784
- package/dist/chunk-VAYZJKVR.js +0 -209
- package/dist/components/shell/DocInspector.d.ts +0 -18
- package/dist/components/shell/DocInspector.d.ts.map +0 -1
- package/dist/components/shell/DocumentEditorOverlay.d.ts +0 -29
- package/dist/components/shell/DocumentEditorOverlay.d.ts.map +0 -1
- package/dist/components/shell/DocumentPresentOverlay.d.ts +0 -30
- package/dist/components/shell/DocumentPresentOverlay.d.ts.map +0 -1
- package/dist/components/shell/SiteSettingsDecks.d.ts +0 -13
- package/dist/components/shell/SiteSettingsDecks.d.ts.map +0 -1
- package/dist/deck/DeckComponentView.d.ts +0 -13
- package/dist/deck/DeckComponentView.d.ts.map +0 -1
- package/dist/deck/DeckPlayer.d.ts +0 -26
- package/dist/deck/DeckPlayer.d.ts.map +0 -1
- package/dist/deck/DeckSlideView.d.ts +0 -17
- package/dist/deck/DeckSlideView.d.ts.map +0 -1
- package/dist/deck/DeckStage.d.ts +0 -16
- package/dist/deck/DeckStage.d.ts.map +0 -1
- package/dist/deck/components/DeckImage.d.ts +0 -14
- package/dist/deck/components/DeckImage.d.ts.map +0 -1
- package/dist/deck/components/DeckImageGrid.d.ts +0 -27
- package/dist/deck/components/DeckImageGrid.d.ts.map +0 -1
- package/dist/deck/components/DeckRichText.d.ts +0 -22
- package/dist/deck/components/DeckRichText.d.ts.map +0 -1
- package/dist/deck/components/DeckVideo.d.ts +0 -15
- package/dist/deck/components/DeckVideo.d.ts.map +0 -1
- package/dist/deck/editing.d.ts +0 -50
- package/dist/deck/editing.d.ts.map +0 -1
- package/dist/deck/flip.d.ts +0 -11
- package/dist/deck/flip.d.ts.map +0 -1
- package/dist/deck/index.d.ts +0 -24
- package/dist/deck/index.d.ts.map +0 -1
- package/dist/deck/index.js +0 -51
- package/dist/deck/layouts/CenteredTextLayout.d.ts +0 -12
- package/dist/deck/layouts/CenteredTextLayout.d.ts.map +0 -1
- package/dist/deck/layouts/ColumnSplitLayout.d.ts +0 -14
- package/dist/deck/layouts/ColumnSplitLayout.d.ts.map +0 -1
- package/dist/deck/layouts/FullBleedImageLayout.d.ts +0 -14
- package/dist/deck/layouts/FullBleedImageLayout.d.ts.map +0 -1
- package/dist/deck/media.d.ts +0 -33
- package/dist/deck/media.d.ts.map +0 -1
- package/dist/deck/tokens.d.ts +0 -22
- package/dist/deck/tokens.d.ts.map +0 -1
- package/dist/deck/useStageScale.d.ts +0 -10
- package/dist/deck/useStageScale.d.ts.map +0 -1
- package/dist/deck-builder/DeckBuilderApp.d.ts +0 -30
- package/dist/deck-builder/DeckBuilderApp.d.ts.map +0 -1
- package/dist/deck-builder/DeckBuilderTopBar.d.ts +0 -24
- package/dist/deck-builder/DeckBuilderTopBar.d.ts.map +0 -1
- package/dist/deck-builder/DeckChatPanel.d.ts +0 -45
- package/dist/deck-builder/DeckChatPanel.d.ts.map +0 -1
- package/dist/deck-builder/DeckEditor.d.ts +0 -37
- package/dist/deck-builder/DeckEditor.d.ts.map +0 -1
- package/dist/deck-builder/DeckLibrary.d.ts +0 -22
- package/dist/deck-builder/DeckLibrary.d.ts.map +0 -1
- package/dist/deck-builder/DeckSlideSurface.d.ts +0 -31
- package/dist/deck-builder/DeckSlideSurface.d.ts.map +0 -1
- package/dist/deck-builder/deck-api.d.ts +0 -76
- package/dist/deck-builder/deck-api.d.ts.map +0 -1
- package/dist/deck-builder/deck-chat-client.d.ts +0 -44
- package/dist/deck-builder/deck-chat-client.d.ts.map +0 -1
- package/dist/deck-builder/deck-chat-history.d.ts +0 -10
- package/dist/deck-builder/deck-chat-history.d.ts.map +0 -1
- package/dist/deck-builder/index.d.ts +0 -13
- package/dist/deck-builder/index.d.ts.map +0 -1
- package/dist/deck-builder/index.js +0 -1498
- package/dist/deck-builder/starter-deck.d.ts +0 -23
- package/dist/deck-builder/starter-deck.d.ts.map +0 -1
- package/dist/deck-builder/types.d.ts +0 -29
- package/dist/deck-builder/types.d.ts.map +0 -1
- package/dist/deck-builder/useSortableSlide.d.ts +0 -29
- package/dist/deck-builder/useSortableSlide.d.ts.map +0 -1
- package/dist/document/assets/fonts/albra-grotesk-bold.woff +0 -0
- package/dist/document/assets/fonts/albra-grotesk-regular.woff +0 -0
- package/dist/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
- package/dist/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
- package/dist/document/components/DocCoverPage.d.ts +0 -11
- package/dist/document/components/DocCoverPage.d.ts.map +0 -1
- package/dist/document/components/DocPage.d.ts +0 -10
- package/dist/document/components/DocPage.d.ts.map +0 -1
- package/dist/document/components/DocRail.d.ts +0 -28
- package/dist/document/components/DocRail.d.ts.map +0 -1
- package/dist/document/components/DocSectionPage.d.ts +0 -12
- package/dist/document/components/DocSectionPage.d.ts.map +0 -1
- package/dist/document/components/DocViewer.d.ts +0 -21
- package/dist/document/components/DocViewer.d.ts.map +0 -1
- package/dist/document/components/Wordmark.d.ts +0 -4
- package/dist/document/components/Wordmark.d.ts.map +0 -1
- package/dist/document/components/measure.d.ts +0 -7
- package/dist/document/components/measure.d.ts.map +0 -1
- package/dist/document/components/useDocPages.d.ts +0 -12
- package/dist/document/components/useDocPages.d.ts.map +0 -1
- package/dist/document/engine/blocks.d.ts +0 -2
- package/dist/document/engine/blocks.d.ts.map +0 -1
- package/dist/document/engine/harvest.d.ts +0 -3
- package/dist/document/engine/harvest.d.ts.map +0 -1
- package/dist/document/engine/masonry.d.ts +0 -26
- package/dist/document/engine/masonry.d.ts.map +0 -1
- package/dist/document/engine/paginate.d.ts +0 -43
- package/dist/document/engine/paginate.d.ts.map +0 -1
- package/dist/document/engine/settings.d.ts +0 -91
- package/dist/document/engine/settings.d.ts.map +0 -1
- package/dist/document/engine/types.d.ts +0 -44
- package/dist/document/engine/types.d.ts.map +0 -1
- package/dist/document/index.d.ts +0 -13
- package/dist/document/index.d.ts.map +0 -1
- package/dist/document/index.js +0 -1030
- package/dist/document/styles/document.css +0 -217
- package/dist/lib/deck-access.d.ts +0 -86
- package/dist/lib/deck-access.d.ts.map +0 -1
- package/dist/lib/deck-access.js +0 -58
- package/dist/lib/deck-dexie.d.ts +0 -88
- package/dist/lib/deck-dexie.d.ts.map +0 -1
- package/dist/lib/deck-dexie.js +0 -22
- package/dist/lib/deck-edit.d.ts +0 -67
- package/dist/lib/deck-edit.d.ts.map +0 -1
- package/dist/lib/deck-media-assets.d.ts +0 -27
- package/dist/lib/deck-media-assets.d.ts.map +0 -1
- package/dist/lib/deck-media-assets.js +0 -25
- package/dist/lib/deck-media-refs.d.ts +0 -35
- package/dist/lib/deck-media-refs.d.ts.map +0 -1
- package/dist/lib/deck-media-refs.js +0 -36
- package/dist/lib/deck-tool.d.ts +0 -25
- package/dist/lib/deck-tool.d.ts.map +0 -1
- package/dist/lib/deck-tool.js +0 -58
- package/dist/lib/document-input.d.ts +0 -25
- package/dist/lib/document-input.d.ts.map +0 -1
- package/dist/schemas/deck-meta.d.ts +0 -85
- package/dist/schemas/deck-meta.d.ts.map +0 -1
- package/dist/schemas/deck-meta.js +0 -64
- package/dist/schemas/deck.d.ts +0 -954
- package/dist/schemas/deck.d.ts.map +0 -1
- package/dist/schemas/deck.js +0 -65
- package/src/components/shell/DocInspector.tsx +0 -314
- package/src/components/shell/DocumentEditorOverlay.tsx +0 -179
- package/src/components/shell/DocumentPresentOverlay.tsx +0 -87
- package/src/components/shell/SiteSettingsDecks.tsx +0 -41
- package/src/deck/DeckComponentView.tsx +0 -28
- package/src/deck/DeckPlayer.tsx +0 -166
- package/src/deck/DeckSlideView.tsx +0 -38
- package/src/deck/DeckStage.tsx +0 -39
- package/src/deck/components/DeckImage.tsx +0 -38
- package/src/deck/components/DeckImageGrid.tsx +0 -254
- package/src/deck/components/DeckRichText.tsx +0 -111
- package/src/deck/components/DeckVideo.tsx +0 -81
- package/src/deck/editing.tsx +0 -201
- package/src/deck/flip.ts +0 -70
- package/src/deck/index.ts +0 -30
- package/src/deck/layouts/CenteredTextLayout.tsx +0 -29
- package/src/deck/layouts/ColumnSplitLayout.tsx +0 -69
- package/src/deck/layouts/FullBleedImageLayout.tsx +0 -58
- package/src/deck/media.tsx +0 -46
- package/src/deck/tokens.ts +0 -57
- package/src/deck/useStageScale.ts +0 -44
- package/src/deck-builder/DeckBuilderApp.tsx +0 -572
- package/src/deck-builder/DeckBuilderTopBar.tsx +0 -62
- package/src/deck-builder/DeckChatPanel.tsx +0 -334
- package/src/deck-builder/DeckEditor.tsx +0 -258
- package/src/deck-builder/DeckLibrary.tsx +0 -195
- package/src/deck-builder/DeckSlideSurface.tsx +0 -55
- package/src/deck-builder/deck-api.ts +0 -177
- package/src/deck-builder/deck-chat-client.ts +0 -129
- package/src/deck-builder/deck-chat-history.ts +0 -62
- package/src/deck-builder/index.ts +0 -12
- package/src/deck-builder/starter-deck.ts +0 -86
- package/src/deck-builder/types.ts +0 -24
- package/src/deck-builder/useSortableSlide.ts +0 -102
- package/src/document/assets/fonts/albra-grotesk-bold.woff +0 -0
- package/src/document/assets/fonts/albra-grotesk-regular.woff +0 -0
- package/src/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
- package/src/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
- package/src/document/components/DocCoverPage.tsx +0 -45
- package/src/document/components/DocPage.tsx +0 -40
- package/src/document/components/DocRail.tsx +0 -189
- package/src/document/components/DocSectionPage.tsx +0 -48
- package/src/document/components/DocViewer.tsx +0 -313
- package/src/document/components/Wordmark.tsx +0 -18
- package/src/document/components/measure.tsx +0 -133
- package/src/document/components/useDocPages.ts +0 -95
- package/src/document/engine/blocks.ts +0 -116
- package/src/document/engine/harvest.ts +0 -95
- package/src/document/engine/masonry.ts +0 -158
- package/src/document/engine/paginate.ts +0 -232
- package/src/document/engine/settings.ts +0 -125
- package/src/document/engine/types.ts +0 -64
- package/src/document/index.ts +0 -18
- package/src/document/styles/document.css +0 -217
- package/src/lib/deck-access.ts +0 -132
- package/src/lib/deck-dexie.ts +0 -190
- package/src/lib/deck-edit.ts +0 -196
- package/src/lib/deck-media-assets.ts +0 -57
- package/src/lib/deck-media-refs.ts +0 -67
- package/src/lib/deck-tool.ts +0 -95
- package/src/lib/document-input.ts +0 -57
- package/src/schemas/deck-meta.ts +0 -88
- package/src/schemas/deck.ts +0 -478
- package/dist/{chunk-DJABADOO.js → chunk-AJ2QOAZI.js} +3 -3
package/src/lib/deck-dexie.ts
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import Dexie from "dexie";
|
|
2
|
-
import { serializeDeck, type Deck } from "../schemas/deck";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Deck Draft staging — the deck builder's half of the editor's unsaved-state
|
|
6
|
-
* model (`./dexie.ts`), and the reason the authoring loop performs no server
|
|
7
|
-
* writes at all (ADR 0002). A draft lives in this browser only until an
|
|
8
|
-
* explicit publish; nothing here ever talks to the network.
|
|
9
|
-
*
|
|
10
|
-
* A SEPARATE database from the editor's `brand-portal-{siteId}`, not a table on
|
|
11
|
-
* it. The builder is a VIEWER surface: opening the editor's connection from it
|
|
12
|
-
* would run the editor's upgrade chain (11 versions, several of which clear
|
|
13
|
-
* caches) in a viewer's browser, and would hold a connection open that blocks
|
|
14
|
-
* the editor's next schema bump. Nothing here needs the editor's rows.
|
|
15
|
-
*
|
|
16
|
-
* Keyed by site AND by identity scope (`deckDraftScope`). One browser profile
|
|
17
|
-
* routinely serves several people, and drafts are unpublished work — see that
|
|
18
|
-
* function for what the scope is, why it is a digest, and what happens to the
|
|
19
|
-
* databases an identity leaves behind.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
export interface DeckDraftRow {
|
|
23
|
-
/** Deck id — also the directory name on the decks branch. */
|
|
24
|
-
deckId: string;
|
|
25
|
-
deck: Deck;
|
|
26
|
-
/**
|
|
27
|
-
* The published version this draft was based on, and the `baseVersion` the
|
|
28
|
-
* next publish sends. `null` means the deck has never been published, which
|
|
29
|
-
* is exactly what POST /api/decks wants to be told to create it.
|
|
30
|
-
*/
|
|
31
|
-
baseVersion: string | null;
|
|
32
|
-
/**
|
|
33
|
-
* The published deck's serialized bytes AT `baseVersion`, or null when it has
|
|
34
|
-
* never been published. Stored rather than a dirty flag because the flag has
|
|
35
|
-
* to survive a reload, and re-deriving it would mean a server read — which is
|
|
36
|
-
* the one thing a draft must not need. Comparing bytes also makes an
|
|
37
|
-
* edit-then-undo correctly report "no unpublished changes".
|
|
38
|
-
*/
|
|
39
|
-
publishedText: string | null;
|
|
40
|
-
updatedAt: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
class DeckDraftDatabase extends Dexie {
|
|
44
|
-
drafts!: Dexie.Table<DeckDraftRow, string>;
|
|
45
|
-
|
|
46
|
-
constructor(name: string) {
|
|
47
|
-
super(name);
|
|
48
|
-
this.version(1).stores({ drafts: "deckId" });
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** `scope` is a `deckDraftScope` digest — never a raw username or user id. */
|
|
53
|
-
function databaseName(siteId: string, scope: string): string {
|
|
54
|
-
return `brand-portal-decks-${siteId}-${scope}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let db: DeckDraftDatabase | null = null;
|
|
58
|
-
|
|
59
|
-
export function initDeckDraftStore(siteId: string, scope: string): void {
|
|
60
|
-
const name = databaseName(siteId, scope);
|
|
61
|
-
// Re-initialising for the database we already hold is a no-op — replacing the
|
|
62
|
-
// instance would leave the old connection open, and an open connection on the
|
|
63
|
-
// previous version is what makes the next schema bump hang on `blocked`.
|
|
64
|
-
// (Same reasoning as initEditorStore.) Switching identity DOES replace it,
|
|
65
|
-
// which is the point: the previous identity's rows become unreachable here.
|
|
66
|
-
if (db?.name === name) return;
|
|
67
|
-
db?.close();
|
|
68
|
-
db = new DeckDraftDatabase(name);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function getDb(): DeckDraftDatabase {
|
|
72
|
-
if (!db) throw new Error("Deck draft store not initialized. Call initDeckDraftStore first.");
|
|
73
|
-
return db;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Every staged draft, newest first. Drives the builder's deck grid. */
|
|
77
|
-
export async function listDeckDrafts(): Promise<DeckDraftRow[]> {
|
|
78
|
-
const rows = await getDb().drafts.toArray();
|
|
79
|
-
return rows.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export async function getDeckDraft(deckId: string): Promise<DeckDraftRow | null> {
|
|
83
|
-
return (await getDb().drafts.get(deckId)) ?? null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Stage a deck edit. `baseVersion`/`publishedText` are carried forward from the
|
|
88
|
-
* existing row rather than taken as arguments: an edit never changes what the
|
|
89
|
-
* server holds, and letting a caller pass them is how a draft would come to
|
|
90
|
-
* claim it was published when it was not.
|
|
91
|
-
*/
|
|
92
|
-
export async function putDeckDraft(deck: Deck): Promise<DeckDraftRow> {
|
|
93
|
-
const db = getDb();
|
|
94
|
-
// Transactional, not get-then-put: this races `recordDeckPublish` on exactly
|
|
95
|
-
// the fields it carries forward. Two overlapping read-modify-writes both
|
|
96
|
-
// reading the pre-publish row would revert `baseVersion` to null, and the next
|
|
97
|
-
// publish would then send `baseVersion: null` for a deck that already exists —
|
|
98
|
-
// a 409 the author cannot explain. IDB serializes readwrite transactions on
|
|
99
|
-
// the store, so the two can only ever run one after the other.
|
|
100
|
-
return db.transaction("rw", db.drafts, async () => {
|
|
101
|
-
const existing = await db.drafts.get(deck.id);
|
|
102
|
-
const row: DeckDraftRow = {
|
|
103
|
-
deckId: deck.id,
|
|
104
|
-
deck,
|
|
105
|
-
baseVersion: existing?.baseVersion ?? null,
|
|
106
|
-
publishedText: existing?.publishedText ?? null,
|
|
107
|
-
updatedAt: new Date().toISOString(),
|
|
108
|
-
};
|
|
109
|
-
await db.drafts.put(row);
|
|
110
|
-
return row;
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Record what the server now holds — after a publish, or after loading a
|
|
116
|
-
* published deck into the draft. The deck body is restated from `deck` so the
|
|
117
|
-
* row and its published baseline can never disagree.
|
|
118
|
-
*
|
|
119
|
-
* `updatedAt` is an argument because the two callers mean different things by
|
|
120
|
-
* it. A publish genuinely changed the deck, so "now" is right. Merely OPENING a
|
|
121
|
-
* published deck changes nothing, and stamping "now" there sorted it to the top
|
|
122
|
-
* of the grid — reading a deck must not reorder the library — so that caller
|
|
123
|
-
* passes the sidecar's own timestamp.
|
|
124
|
-
*/
|
|
125
|
-
export async function markDeckDraftPublished(deck: Deck, version: string, updatedAt?: string): Promise<DeckDraftRow> {
|
|
126
|
-
const row: DeckDraftRow = {
|
|
127
|
-
deckId: deck.id,
|
|
128
|
-
deck,
|
|
129
|
-
baseVersion: version,
|
|
130
|
-
publishedText: serializeDeck(deck),
|
|
131
|
-
updatedAt: updatedAt ?? new Date().toISOString(),
|
|
132
|
-
};
|
|
133
|
-
await getDb().drafts.put(row);
|
|
134
|
-
return row;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Record a successful publish WITHOUT touching the working deck (#39).
|
|
139
|
-
*
|
|
140
|
-
* The difference from `markDeckDraftPublished` is the whole point: that
|
|
141
|
-
* function restates the body from its argument, which is a pre-request snapshot
|
|
142
|
-
* at the publish call site, so an edit landing during the round-trip was
|
|
143
|
-
* silently discarded. #35 could answer that with a lock because its only other
|
|
144
|
-
* writer was a chat turn; inline editing cannot — locking a text field the
|
|
145
|
-
* author is mid-word in is worse than the race it prevents.
|
|
146
|
-
*
|
|
147
|
-
* So this re-reads the row and writes back only the publish BASELINE: what the
|
|
148
|
-
* server now holds (`baseVersion`) and the bytes it holds (`publishedText`,
|
|
149
|
-
* serialized from the deck that was actually sent). `deck` stays whatever is
|
|
150
|
-
* current by now, which makes `hasUnpublishedChanges` correctly report the
|
|
151
|
-
* mid-flight edit as unpublished rather than losing it.
|
|
152
|
-
*
|
|
153
|
-
* Returns null when the row is gone (deleted during the round-trip) — recreating
|
|
154
|
-
* it would resurrect a deck the author just removed.
|
|
155
|
-
*/
|
|
156
|
-
export async function recordDeckPublish(deckId: string, publishedDeck: Deck, version: string): Promise<DeckDraftRow | null> {
|
|
157
|
-
const db = getDb();
|
|
158
|
-
// Transactional for the same reason `putDeckDraft` is, and it is the half
|
|
159
|
-
// that bites hardest: an inline commit whose put lands between this read and
|
|
160
|
-
// this write is REVERTED here — the row goes back to the pre-edit deck with a
|
|
161
|
-
// matching `publishedText`, so the edit is gone AND the deck reports itself
|
|
162
|
-
// clean. Re-reading inside the transaction is what makes "leave `deck` alone"
|
|
163
|
-
// mean the deck as of the write, not as of the request.
|
|
164
|
-
return db.transaction("rw", db.drafts, async () => {
|
|
165
|
-
const existing = await db.drafts.get(deckId);
|
|
166
|
-
if (!existing) return null;
|
|
167
|
-
const row: DeckDraftRow = {
|
|
168
|
-
...existing,
|
|
169
|
-
baseVersion: version,
|
|
170
|
-
publishedText: serializeDeck(publishedDeck),
|
|
171
|
-
// "Now", as the previous implementation stamped: a publish is activity the
|
|
172
|
-
// deck grid should sort on, even though it did not change the body.
|
|
173
|
-
updatedAt: new Date().toISOString(),
|
|
174
|
-
};
|
|
175
|
-
await db.drafts.put(row);
|
|
176
|
-
return row;
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export async function deleteDeckDraft(deckId: string): Promise<void> {
|
|
181
|
-
await getDb().drafts.delete(deckId);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Does this row differ from what was published? A never-published draft always
|
|
186
|
-
* has changes — there is nothing on the server to match.
|
|
187
|
-
*/
|
|
188
|
-
export function hasUnpublishedChanges(row: DeckDraftRow): boolean {
|
|
189
|
-
return row.publishedText === null || serializeDeck(row.deck) !== row.publishedText;
|
|
190
|
-
}
|
package/src/lib/deck-edit.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
parseDeck,
|
|
3
|
-
type Deck,
|
|
4
|
-
type DeckComponent,
|
|
5
|
-
type DeckRichText,
|
|
6
|
-
type DeckSlide,
|
|
7
|
-
} from "../schemas/deck";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Direct-manipulation edits to a Deck Draft (#39).
|
|
11
|
-
*
|
|
12
|
-
* Every operation here is PURE and ends in `parseDeck`, which is what makes
|
|
13
|
-
* "a manual edit is always schema-valid" a property of this module rather than
|
|
14
|
-
* of its callers. It also means the schema owns the refusals: duplicating past
|
|
15
|
-
* `DECK_MAX_SLIDES` and deleting the last slide are reported with the same
|
|
16
|
-
* messages the authoring loop's validation retry reads, not with a second copy
|
|
17
|
-
* of those rules written here.
|
|
18
|
-
*
|
|
19
|
-
* The scope line is the product decision recorded in CONTEXT.md — "chat
|
|
20
|
-
* authors, direct manipulation adjusts". There is deliberately no operation
|
|
21
|
-
* here that changes a Layout, adds or removes a Deck Component, or composes a
|
|
22
|
-
* grid. Text leaves and slide order are the whole vocabulary.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Which Deck Component within a slide's Layout. A slide-local coordinate: the
|
|
27
|
-
* runtime renders one slide and has no idea which one, so the slide index stays
|
|
28
|
-
* an argument at the call site.
|
|
29
|
-
*/
|
|
30
|
-
// Module-private VALUES, exported types only. The deck runtime imports
|
|
31
|
-
// `DeckTextPath` from here, and `import type` is erased — exporting the arrays
|
|
32
|
-
// would offer a value import that pulls this module, and the deck schema and
|
|
33
|
-
// zod behind it, into the runtime chunk served to Public Link readers. Neither
|
|
34
|
-
// fence would see it: the unit test cannot tell a type import from a value one
|
|
35
|
-
// and excludes the runtime subtree anyway.
|
|
36
|
-
const DECK_TEXT_SLOTS = ["content", "overlay", "left", "right"] as const;
|
|
37
|
-
export type DeckTextSlot = (typeof DECK_TEXT_SLOTS)[number];
|
|
38
|
-
|
|
39
|
-
const DECK_TEXT_FIELDS = ["kicker", "heading", "subheading", "paragraphs", "bullets"] as const;
|
|
40
|
-
export type DeckTextField = (typeof DECK_TEXT_FIELDS)[number];
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* A single editable text LEAF — always a plain string in the schema.
|
|
44
|
-
*
|
|
45
|
-
* That "always a leaf" framing is what keeps inline editing free of a
|
|
46
|
-
* formatting UI without ever destroying a mark: a paragraph carrying spans is
|
|
47
|
-
* edited one span at a time (`span`), so `bold`/`italic` are untouched by an
|
|
48
|
-
* edit that only ever writes `text`. A plain-string line has no `span`.
|
|
49
|
-
*/
|
|
50
|
-
export interface DeckTextPath {
|
|
51
|
-
slot: DeckTextSlot;
|
|
52
|
-
field: DeckTextField;
|
|
53
|
-
/** Index into `paragraphs`/`bullets`. Absent for the three single-line fields. */
|
|
54
|
-
line?: number;
|
|
55
|
-
/** Index into that line's spans, when the line is a span run rather than a string. */
|
|
56
|
-
span?: number;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export type DeckEditResult = { ok: true; deck: Deck } | { ok: false; errors: string[] };
|
|
60
|
-
|
|
61
|
-
/** A path that points at nothing is a bug in the caller, not author input. */
|
|
62
|
-
function unresolved(path: DeckTextPath): DeckEditResult {
|
|
63
|
-
return {
|
|
64
|
-
ok: false,
|
|
65
|
-
errors: [`no editable text at ${path.slot}.${path.field}${path.line === undefined ? "" : `[${path.line}]`}`],
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** The rich text a slot holds, or null when that slot holds something else. */
|
|
70
|
-
export function richTextAt(slide: DeckSlide, slot: DeckTextSlot): DeckRichText | null {
|
|
71
|
-
const { layout } = slide;
|
|
72
|
-
if (layout.type === "centeredText") {
|
|
73
|
-
return slot === "content" && layout.content.type === "richText" ? layout.content : null;
|
|
74
|
-
}
|
|
75
|
-
if (layout.type === "fullBleedImage") {
|
|
76
|
-
return slot === "overlay" ? (layout.overlay?.content ?? null) : null;
|
|
77
|
-
}
|
|
78
|
-
if (slot !== "left" && slot !== "right") return null;
|
|
79
|
-
const component: DeckComponent = layout[slot];
|
|
80
|
-
return component.type === "richText" ? component : null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** The same addressing, as a setter. Returns null when the slot has no rich text. */
|
|
84
|
-
function withRichTextAt(slide: DeckSlide, slot: DeckTextSlot, next: DeckRichText): DeckSlide | null {
|
|
85
|
-
const { layout } = slide;
|
|
86
|
-
if (layout.type === "centeredText") {
|
|
87
|
-
if (slot !== "content" || layout.content.type !== "richText") return null;
|
|
88
|
-
return { ...slide, layout: { ...layout, content: next } };
|
|
89
|
-
}
|
|
90
|
-
if (layout.type === "fullBleedImage") {
|
|
91
|
-
if (slot !== "overlay" || layout.overlay === undefined) return null;
|
|
92
|
-
return { ...slide, layout: { ...layout, overlay: { ...layout.overlay, content: next } } };
|
|
93
|
-
}
|
|
94
|
-
if (slot !== "left" && slot !== "right") return null;
|
|
95
|
-
if (layout[slot].type !== "richText") return null;
|
|
96
|
-
return { ...slide, layout: { ...layout, [slot]: next } };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Writes `text` into the addressed leaf, or null when the path resolves to nothing. */
|
|
100
|
-
function withLeafText(content: DeckRichText, path: DeckTextPath, text: string): DeckRichText | null {
|
|
101
|
-
if (path.field === "kicker" || path.field === "heading" || path.field === "subheading") {
|
|
102
|
-
// Only a field that is already present is editable — a blank line the
|
|
103
|
-
// author never wrote is not a place to click, and inventing one here would
|
|
104
|
-
// be the structural editing this module does not do.
|
|
105
|
-
if (content[path.field] === undefined) return null;
|
|
106
|
-
return { ...content, [path.field]: text };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const lines = content[path.field];
|
|
110
|
-
if (lines === undefined || path.line === undefined || path.line < 0 || path.line >= lines.length) return null;
|
|
111
|
-
|
|
112
|
-
const nextLines = lines.slice();
|
|
113
|
-
const line = nextLines[path.line];
|
|
114
|
-
|
|
115
|
-
if (path.span === undefined) {
|
|
116
|
-
if (typeof line !== "string") return null;
|
|
117
|
-
nextLines[path.line] = text;
|
|
118
|
-
} else {
|
|
119
|
-
if (typeof line === "string" || path.span < 0 || path.span >= line.length) return null;
|
|
120
|
-
const spans = line.slice();
|
|
121
|
-
spans[path.span] = { ...spans[path.span], text };
|
|
122
|
-
nextLines[path.line] = spans;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return { ...content, [path.field]: nextLines };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Commit an inline text edit.
|
|
130
|
-
*
|
|
131
|
-
* Rejecting (rather than clamping) an over-long or emptied field is deliberate:
|
|
132
|
-
* the caps are the schema's, and a silent truncation is an edit the author did
|
|
133
|
-
* not make. The caller shows the schema's own message and leaves the draft
|
|
134
|
-
* untouched.
|
|
135
|
-
*/
|
|
136
|
-
export function applyDeckTextEdit(deck: Deck, slideIndex: number, path: DeckTextPath, text: string): DeckEditResult {
|
|
137
|
-
const slide = deck.slides[slideIndex];
|
|
138
|
-
if (slide === undefined) return { ok: false, errors: [`no slide at index ${slideIndex}`] };
|
|
139
|
-
|
|
140
|
-
const content = richTextAt(slide, path.slot);
|
|
141
|
-
if (content === null) return unresolved(path);
|
|
142
|
-
|
|
143
|
-
const nextContent = withLeafText(content, path, text);
|
|
144
|
-
if (nextContent === null) return unresolved(path);
|
|
145
|
-
|
|
146
|
-
const nextSlide = withRichTextAt(slide, path.slot, nextContent);
|
|
147
|
-
if (nextSlide === null) return unresolved(path);
|
|
148
|
-
|
|
149
|
-
const slides = deck.slides.slice();
|
|
150
|
-
slides[slideIndex] = nextSlide;
|
|
151
|
-
return parseDeck({ ...deck, slides });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** Move a slide to a new index. `to` is the destination in the POST-removal list. */
|
|
155
|
-
export function moveDeckSlide(deck: Deck, from: number, to: number): DeckEditResult {
|
|
156
|
-
if (from < 0 || from >= deck.slides.length) return { ok: false, errors: [`no slide at index ${from}`] };
|
|
157
|
-
if (to < 0 || to >= deck.slides.length) return { ok: false, errors: [`cannot move a slide to index ${to}`] };
|
|
158
|
-
const slides = deck.slides.slice();
|
|
159
|
-
const [moved] = slides.splice(from, 1);
|
|
160
|
-
slides.splice(to, 0, moved);
|
|
161
|
-
return parseDeck({ ...deck, slides });
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* A fresh slide id for a copy. Ids are unique within a deck (the schema refuses
|
|
166
|
-
* a duplicate by name) and double as diff anchors across revisions, so a copy
|
|
167
|
-
* has to be given its own rather than inheriting one.
|
|
168
|
-
*/
|
|
169
|
-
function copySlideId(sourceId: string, taken: ReadonlySet<string>): string {
|
|
170
|
-
// Copying a copy chains "-copy-2", not "-copy-copy". The slice keeps room for
|
|
171
|
-
// the suffix inside DeckIdSchema's 64-character cap.
|
|
172
|
-
const stem = sourceId.replace(/-copy(-\d+)?$/, "").slice(0, 52).replace(/-+$/, "") || "slide";
|
|
173
|
-
let candidate = `${stem}-copy`;
|
|
174
|
-
for (let n = 2; taken.has(candidate); n++) candidate = `${stem}-copy-${n}`;
|
|
175
|
-
return candidate;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** Duplicate a slide, inserting the copy directly after the original. */
|
|
179
|
-
export function duplicateDeckSlide(deck: Deck, index: number): DeckEditResult {
|
|
180
|
-
const source = deck.slides[index];
|
|
181
|
-
if (source === undefined) return { ok: false, errors: [`no slide at index ${index}`] };
|
|
182
|
-
const copy: DeckSlide = {
|
|
183
|
-
...structuredClone(source),
|
|
184
|
-
id: copySlideId(source.id, new Set(deck.slides.map((s) => s.id))),
|
|
185
|
-
};
|
|
186
|
-
const slides = deck.slides.slice();
|
|
187
|
-
slides.splice(index + 1, 0, copy);
|
|
188
|
-
return parseDeck({ ...deck, slides });
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** Remove a slide. Removing the only one is refused by the schema, by design. */
|
|
192
|
-
export function removeDeckSlide(deck: Deck, index: number): DeckEditResult {
|
|
193
|
-
if (deck.slides[index] === undefined) return { ok: false, errors: [`no slide at index ${index}`] };
|
|
194
|
-
const slides = deck.slides.filter((_, i) => i !== index);
|
|
195
|
-
return parseDeck({ ...deck, slides });
|
|
196
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type { DeckMediaAsset } from "../schemas/deck";
|
|
2
|
-
import type { MediaManifest } from "../media/types";
|
|
3
|
-
import type { MediaProvider } from "../media/provider";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The production side of the deck runtime's media seam: the site's image
|
|
7
|
-
* manifest, resolved to the `mediaAssets` map `DeckPlayer` takes.
|
|
8
|
-
*
|
|
9
|
-
* A MAP, not a `DeckMediaResolver` function, because the builder mounts across
|
|
10
|
-
* an Astro island boundary and a function prop cannot survive that trip. It is
|
|
11
|
-
* built server-side, where the manifest and the media provider live.
|
|
12
|
-
*
|
|
13
|
-
* This replaces the dev preview route's generated plates
|
|
14
|
-
* (`core/src/lib/dev-deck-preview.ts`), which stay exactly where they are —
|
|
15
|
-
* they keep that route working offline and with no GitHub credentials.
|
|
16
|
-
*/
|
|
17
|
-
export function deckMediaAssets(
|
|
18
|
-
manifest: MediaManifest,
|
|
19
|
-
sizes: number[],
|
|
20
|
-
provider: MediaProvider,
|
|
21
|
-
/**
|
|
22
|
-
* The audience boundary, when the caller has one. Only these ids resolve.
|
|
23
|
-
*
|
|
24
|
-
* The builder's map must match the catalog the model is offered (#35, decided
|
|
25
|
-
* 2026-08-07) or the two disagree about what exists — and because this map
|
|
26
|
-
* travels as ISLAND PROPS, an unfiltered one also puts every asset's filename
|
|
27
|
-
* and alt text in the page source of a creator who cannot see those pages.
|
|
28
|
-
* Omitted means no filtering, which is what the dev preview route wants.
|
|
29
|
-
*/
|
|
30
|
-
allowIds?: ReadonlySet<string>,
|
|
31
|
-
): Record<string, DeckMediaAsset> {
|
|
32
|
-
const assets: Record<string, DeckMediaAsset> = {};
|
|
33
|
-
for (const [id, item] of Object.entries(manifest.images)) {
|
|
34
|
-
// System-generated assets (the social-preview raster) are excluded from the
|
|
35
|
-
// media library UI, so they are not part of the catalog a deck may cite.
|
|
36
|
-
if (item.hidden) continue;
|
|
37
|
-
if (allowIds && !allowIds.has(id)) continue;
|
|
38
|
-
const resolved = provider.resolve(item, sizes);
|
|
39
|
-
// An unresolvable entry is simply absent, which the runtime already renders
|
|
40
|
-
// as a quiet gap — the same thing a deleted asset produces.
|
|
41
|
-
if (!resolved) continue;
|
|
42
|
-
assets[id] = {
|
|
43
|
-
src: resolved.src,
|
|
44
|
-
alt: item.alt,
|
|
45
|
-
// The manifest always knows the intrinsic size, and supplying it is what
|
|
46
|
-
// lets the image grid reserve aspect-correct space before a byte loads.
|
|
47
|
-
width: resolved.width,
|
|
48
|
-
height: resolved.height,
|
|
49
|
-
...(resolved.tag === "video" && resolved.poster ? { poster: resolved.poster } : {}),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
// A plain object, and that is safe because every reader guards its lookups
|
|
53
|
-
// with `Object.hasOwn` (DeckPlayer's map resolver, DeckSlideSurface) and the
|
|
54
|
-
// deck schema constrains a media id's charset. A null-prototype object would
|
|
55
|
-
// also have to be re-spread to survive Astro's prop serialization.
|
|
56
|
-
return assets;
|
|
57
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Every portal media reference a deck makes, extracted from RAW deck JSON.
|
|
3
|
-
*
|
|
4
|
-
* Publish-time GC builds its keep-set from the site's manifests and deletes
|
|
5
|
-
* every other bucket object (ADR 0002's "GC must learn the decks branch"), so a
|
|
6
|
-
* deck referencing an asset the editor later removed from portal content would
|
|
7
|
-
* otherwise have its media deleted underneath it.
|
|
8
|
-
*
|
|
9
|
-
* Deliberately untyped and structural rather than a walk over `Deck`: the media
|
|
10
|
-
* reference union lands with the image/grid components (#31), and this has to
|
|
11
|
-
* work the day it does without a second change here. It matches the union's
|
|
12
|
-
* discriminator shape — `{ kind: "portal", id: string }` — wherever it appears
|
|
13
|
-
* in the tree, at any depth, in any component. Staying schema-free is also what
|
|
14
|
-
* keeps this module off the deck-schema carrier list (tests/deck/viewer-isolation).
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/** One reference, with the path an author (or a model) can act on. */
|
|
18
|
-
export interface DeckMediaRefSite {
|
|
19
|
-
/** `slides[2].layout.media` — the ref object's own path, not its `id` field. */
|
|
20
|
-
path: string;
|
|
21
|
-
id: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function step(path: string, key: string): string {
|
|
25
|
-
return path === "" ? key : `${path}.${key}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function deckMediaRefSites(deck: unknown): DeckMediaRefSite[] {
|
|
29
|
-
const sites: DeckMediaRefSite[] = [];
|
|
30
|
-
const visit = (node: unknown, path: string): void => {
|
|
31
|
-
if (Array.isArray(node)) {
|
|
32
|
-
node.forEach((item, i) => visit(item, `${path}[${i}]`));
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
if (typeof node !== "object" || node === null) return;
|
|
36
|
-
const record = node as Record<string, unknown>;
|
|
37
|
-
if (record.kind === "portal" && typeof record.id === "string" && record.id !== "") {
|
|
38
|
-
sites.push({ path, id: record.id });
|
|
39
|
-
}
|
|
40
|
-
for (const [key, value] of Object.entries(record)) visit(value, step(path, key));
|
|
41
|
-
};
|
|
42
|
-
visit(deck, "");
|
|
43
|
-
return sites;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function deckMediaIds(deck: unknown): Set<string> {
|
|
47
|
-
return new Set(deckMediaRefSites(deck).map((site) => site.id));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Media references the catalog does not contain — the server-side half of media
|
|
52
|
-
* grounding (#35).
|
|
53
|
-
*
|
|
54
|
-
* The deck schema constrains a media id's CHARSET; only the catalog can say
|
|
55
|
-
* whether an id exists, so membership is checked here rather than in the schema
|
|
56
|
-
* (which is also read by the runtime, where no catalog is in hand). One line per
|
|
57
|
-
* offending reference, in the same `path: message` shape `formatDeckIssues`
|
|
58
|
-
* produces, so the authoring loop can hand both lists back in one tool_result.
|
|
59
|
-
*/
|
|
60
|
-
export function deckUnknownMediaIssues(deck: unknown, catalogIds: ReadonlySet<string>): string[] {
|
|
61
|
-
return deckMediaRefSites(deck)
|
|
62
|
-
.filter((site) => !catalogIds.has(site.id))
|
|
63
|
-
.map(
|
|
64
|
-
(site) =>
|
|
65
|
-
`${step(site.path, "id")}: "${site.id}" is not in the portal media catalog — use only the ids listed under "Media catalog", copied exactly.`,
|
|
66
|
-
);
|
|
67
|
-
}
|
package/src/lib/deck-tool.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
DECK_COMPONENT_TYPES,
|
|
4
|
-
DECK_LAYOUT_TYPES,
|
|
5
|
-
DECK_MAX_SLIDES,
|
|
6
|
-
DECK_SCHEMA_VERSION,
|
|
7
|
-
DeckSchema,
|
|
8
|
-
} from "../schemas/deck";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* The `update_deck` tool the deck-chat loop hands the model (#35, ADR 0001).
|
|
12
|
-
*
|
|
13
|
-
* The input schema is DERIVED from `DeckSchema` rather than written out, so the
|
|
14
|
-
* contract cannot drift from what the server will actually accept: every
|
|
15
|
-
* `.describe()` in the deck schema IS the LLM's documentation, and a field
|
|
16
|
-
* added there reaches the model on the next request with no second edit here.
|
|
17
|
-
*
|
|
18
|
-
* Emitted INLINE (zod's default `reused: "inline"`), not with `$defs`/`$ref`.
|
|
19
|
-
* The ref form is less than half the bytes, but names every shared node
|
|
20
|
-
* `__schema7`, which puts the layout and component descriptions an indirection
|
|
21
|
-
* away from the slot that uses them. The tool block sits in the cached prefix
|
|
22
|
-
* (tools precede `system`, and the corpus block carries the cache breakpoint),
|
|
23
|
-
* so the size is paid once per conversation while the legibility is paid on
|
|
24
|
-
* every turn.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
export const UPDATE_DECK_TOOL_NAME = "update_deck";
|
|
28
|
-
|
|
29
|
-
export interface DeckToolDefinition {
|
|
30
|
-
name: string;
|
|
31
|
-
description: string;
|
|
32
|
-
/** JSON Schema; the tool's input IS the whole deck. */
|
|
33
|
-
input_schema: Record<string, unknown>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* The vocabulary, spelled out in prose above the schema.
|
|
38
|
-
*
|
|
39
|
-
* The derived JSON Schema already carries it, but only as `const` values buried
|
|
40
|
-
* in nested `anyOf` branches — and live authoring showed the model guessing
|
|
41
|
-
* layout and component types from there, burning validation retries on
|
|
42
|
-
* "unknown layout type" and sometimes exhausting them. A flat list costs a few
|
|
43
|
-
* dozen tokens in the cached tool block and removes the guess.
|
|
44
|
-
*
|
|
45
|
-
* The type names come from the schema's own constants, so a new Layout or
|
|
46
|
-
* Component cannot be added without appearing here; the slot shapes are spelled
|
|
47
|
-
* out because they are structure, not a list — `tests/deck/deck-tool.test.ts`
|
|
48
|
-
* pins that every name in both constants is mentioned.
|
|
49
|
-
*/
|
|
50
|
-
const VOCABULARY = [
|
|
51
|
-
`Layouts (a slide uses exactly one, and \`type\` must be one of these ${DECK_LAYOUT_TYPES.length}): ${DECK_LAYOUT_TYPES.map((t) => `\`${t}\``).join(", ")}.`,
|
|
52
|
-
"- `centeredText` — a title card. Slot: `content` (one Component). Optional `background`: `base` or `brand`.",
|
|
53
|
-
"- `fullBleedImage` — the image IS the slide. Takes `media` directly, plus an optional `overlay` panel whose `content` must be a `richText` Component. It has no other slots.",
|
|
54
|
-
"- `columnSplit` — two slots, `left` and `right`, each one Component, at a `ratio` of `1-2` or `2-1`.",
|
|
55
|
-
"",
|
|
56
|
-
`Components (what fills a Layout slot; \`type\` must be one of these ${DECK_COMPONENT_TYPES.length}): ${DECK_COMPONENT_TYPES.map((t) => `\`${t}\``).join(", ")}.`,
|
|
57
|
-
"- `richText` — kicker / heading / subheading / paragraphs / bullets. Text only; no media.",
|
|
58
|
-
"- `image` — one `media` reference and an optional caption.",
|
|
59
|
-
"- `imageGrid` — two to twelve `items`, each with its own `media`.",
|
|
60
|
-
"- `video` — one `media` reference.",
|
|
61
|
-
"",
|
|
62
|
-
"There is nothing else. A `type` outside those lists is rejected, and so is a slot a Layout does not have — do not invent `body`, `columns`, `slots` or `children`.",
|
|
63
|
-
].join("\n");
|
|
64
|
-
|
|
65
|
-
const DESCRIPTION = [
|
|
66
|
-
"Replace the ENTIRE deck with a new version. This is how every change to the deck is made — adding a slide, rewriting a line, reordering, or building the deck from nothing.",
|
|
67
|
-
"",
|
|
68
|
-
VOCABULARY,
|
|
69
|
-
"",
|
|
70
|
-
"Rules:",
|
|
71
|
-
"- Emit the COMPLETE deck every time, including slides you are not changing. There is no patch or partial form: whatever you send becomes the deck, and anything you leave out is deleted.",
|
|
72
|
-
`- Keep \`id\` exactly as it is in the current deck, and keep \`schemaVersion\` at ${DECK_SCHEMA_VERSION}.`,
|
|
73
|
-
"- Reuse a slide's existing `id` when that slide survives the edit, so revisions stay diffable. Give a genuinely new slide a new lowercase-hyphen id that no other slide uses.",
|
|
74
|
-
"- Every `media` reference must be a portal media id copied exactly from the Media catalog in your instructions. Never invent one, and never use a filename, path or URL.",
|
|
75
|
-
`- At most ${DECK_MAX_SLIDES} slides.`,
|
|
76
|
-
"",
|
|
77
|
-
"Say what you changed in your reply text; do not restate the deck JSON there.",
|
|
78
|
-
].join("\n");
|
|
79
|
-
|
|
80
|
-
export function updateDeckTool(): DeckToolDefinition {
|
|
81
|
-
const schema = z.toJSONSchema(DeckSchema, {
|
|
82
|
-
// `io: "input"` is what makes fields with defaults optional — under
|
|
83
|
-
// "output" the model would be told `background` is required. `unrepresentable
|
|
84
|
-
// : "any"` is for the superRefines (duplicate slide ids, the "a richText
|
|
85
|
-
// needs some content" rule); they are enforced server-side by parseDeck,
|
|
86
|
-
// and their messages are what a failed turn gets handed back.
|
|
87
|
-
target: "draft-2020-12",
|
|
88
|
-
io: "input",
|
|
89
|
-
unrepresentable: "any",
|
|
90
|
-
}) as Record<string, unknown>;
|
|
91
|
-
// The provider takes the schema body directly; `$schema` is inert there and
|
|
92
|
-
// is just tokens on every request.
|
|
93
|
-
delete schema.$schema;
|
|
94
|
-
return { name: UPDATE_DECK_TOOL_NAME, description: DESCRIPTION, input_schema: schema };
|
|
95
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// Content adapters that turn portal content into the document engine's input
|
|
2
|
-
// shape. These live in the primitives lib (outside the sealed document/ subtree)
|
|
3
|
-
// so the editor (EditorShell) can reach them; primitives cannot import core, so
|
|
4
|
-
// the audience-filtered viewer/presentation adapter lives in core's
|
|
5
|
-
// document-route.ts and calls into this module for image mapping.
|
|
6
|
-
import type { MediaManifest } from "../schemas/media";
|
|
7
|
-
import type { Page } from "../schemas/site-config";
|
|
8
|
-
import type { LoadedSection } from "./loader";
|
|
9
|
-
import type { DocImage, DocSectionInput, DocSourceContent } from "../document";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Map a media manifest to the engine's pre-resolved image dictionary. Port of
|
|
13
|
-
* the harness's `portalImagesFrom` against the real MediaManifest type:
|
|
14
|
-
* - only `kind === "image"` entries (videos/animated skipped);
|
|
15
|
-
* - SVGs → `original.svg` for both preview and print;
|
|
16
|
-
* - other images: previewFile = largest variant ≤ 1080 (else the smallest),
|
|
17
|
-
* printFile = largest; images with zero variants are skipped.
|
|
18
|
-
* The manifest is already schema-parsed upstream, so no tolerant re-parse here.
|
|
19
|
-
*/
|
|
20
|
-
export function docImagesFrom(manifest: MediaManifest): Record<string, DocImage> {
|
|
21
|
-
const out: Record<string, DocImage> = {};
|
|
22
|
-
for (const [imageId, item] of Object.entries(manifest.images)) {
|
|
23
|
-
if (item.kind !== "image") continue;
|
|
24
|
-
if (item.mimeType === "image/svg+xml") {
|
|
25
|
-
out[imageId] = { imageId, width: item.width, height: item.height, alt: item.alt, previewFile: "original.svg", printFile: "original.svg" };
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
const widths = item.variants.map((v) => v.width).sort((a, b) => a - b);
|
|
29
|
-
if (widths.length === 0) continue;
|
|
30
|
-
const preview = [...widths].reverse().find((w) => w <= 1080) ?? widths[0];
|
|
31
|
-
const print = widths[widths.length - 1];
|
|
32
|
-
out[imageId] = { imageId, width: item.width, height: item.height, alt: item.alt, previewFile: `${preview}.webp`, printFile: `${print}.webp` };
|
|
33
|
-
}
|
|
34
|
-
return out;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function toSectionInput(s: LoadedSection): DocSectionInput {
|
|
38
|
-
return { id: s.section.id, type: s.section.type, content: s.section.content, options: s.section.options };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Editor path: build the engine's source content from the caller's already-
|
|
43
|
-
* selected page + its draft sections, verbatim. No status/audience filtering —
|
|
44
|
-
* editors see everything (the shell passes in-memory draft content).
|
|
45
|
-
*/
|
|
46
|
-
export function buildDraftDocumentInput(opts: {
|
|
47
|
-
page: Page;
|
|
48
|
-
sections: LoadedSection[];
|
|
49
|
-
manifest: MediaManifest;
|
|
50
|
-
}): DocSourceContent {
|
|
51
|
-
const { page, sections, manifest } = opts;
|
|
52
|
-
return {
|
|
53
|
-
pageTitle: page.title,
|
|
54
|
-
sections: sections.map(toSectionInput),
|
|
55
|
-
images: docImagesFrom(manifest),
|
|
56
|
-
};
|
|
57
|
-
}
|