@drawnagency/primitives 0.1.107 → 0.1.109

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.
Files changed (35) hide show
  1. package/dist/{chunk-PZJL2S3Z.js → chunk-3ZRQ4G5F.js} +1 -1
  2. package/dist/{chunk-LUAI5DWR.js → chunk-5PVRACZY.js} +2 -2
  3. package/dist/{chunk-IPKZ5FU6.js → chunk-67ZOQCUD.js} +7 -7
  4. package/dist/chunk-F2LYQPXV.js +77 -0
  5. package/dist/{chunk-JIQBLG6Y.js → chunk-LQHYVPBS.js} +6 -6
  6. package/dist/{chunk-ZHHJVKU7.js → chunk-WBDPDGFA.js} +59 -175
  7. package/dist/{chunk-5IZ2KTMU.js → chunk-WXBHP4JY.js} +1 -1
  8. package/dist/chunk-XIDE4TDF.js +194 -0
  9. package/dist/{chunk-D6TLUVOP.js → chunk-ZKVKNUJC.js} +47 -1
  10. package/dist/components/editor/index.js +4 -4
  11. package/dist/components/primitives/index.d.ts +2 -0
  12. package/dist/components/primitives/index.d.ts.map +1 -1
  13. package/dist/components/primitives/index.js +12 -5
  14. package/dist/components/sections/brand-guide.js +5 -5
  15. package/dist/components/sections/register-schemas.js +9 -9
  16. package/dist/components/sections/register.js +9 -9
  17. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  18. package/dist/hooks/useEditorPublish.d.ts +2 -0
  19. package/dist/hooks/useEditorPublish.d.ts.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/lib/dexie.d.ts +25 -8
  22. package/dist/lib/dexie.d.ts.map +1 -1
  23. package/dist/lib/dexie.js +19 -17
  24. package/dist/schemas/index.js +1 -1
  25. package/dist/schemas/link.d.ts.map +1 -1
  26. package/dist/schemas/link.js +1 -1
  27. package/package.json +1 -1
  28. package/src/components/primitives/index.ts +5 -0
  29. package/src/components/shared/LinkField.tsx +1 -1
  30. package/src/components/shell/EditorShell.tsx +64 -29
  31. package/src/hooks/useEditorPublish.ts +29 -4
  32. package/src/lib/dexie.ts +62 -28
  33. package/src/schemas/link.ts +4 -1
  34. package/dist/chunk-5THS4K5D.js +0 -123
  35. package/dist/chunk-ZSPCE6MZ.js +0 -70
@@ -35,7 +35,7 @@ export function LinkField({ label, value, onChange }: Props) {
35
35
  <div className="flex flex-col gap-3">
36
36
  <FormLabel>{label}</FormLabel>
37
37
  <div className="flex gap-2" role="tablist">
38
- {(["external", "internal"] as const).map((kind) => (
38
+ {(["internal", "external"] as const).map((kind) => (
39
39
  <button
40
40
  key={kind}
41
41
  type="button"
@@ -67,8 +67,6 @@ import {
67
67
  discardLocalChanges,
68
68
  getCachedContent,
69
69
  cacheContent,
70
- persistMediaManifest,
71
- getMediaManifest,
72
70
  getPendingMediaItems,
73
71
  getPendingMediaBlobs,
74
72
  getPendingMediaDeletions,
@@ -311,6 +309,13 @@ export default function EditorShell({
311
309
  // immediate 409 retry assembles the saved manifest against the rebased copy.
312
310
  const mediaManifestRef = useRef<MediaManifest>(mediaManifest);
313
311
  useEffect(() => { mediaManifestRef.current = mediaManifest; }, [mediaManifest]);
312
+ // Same reason, for the document sidecar: the collab handlers cache it against a
313
+ // remote sha, and reading the state directly there would either capture a stale
314
+ // closure copy (writing an outdated sidecar for that sha — "File missing" on the
315
+ // next cache hit) or force documentManifest into their dep arrays, churning the
316
+ // callback identity the collab wiring subscribes with.
317
+ const documentManifestRef = useRef<DocumentManifest | null>(documentManifest);
318
+ useEffect(() => { documentManifestRef.current = documentManifest; }, [documentManifest]);
314
319
  // Guards the debounced preview-image composition effect (below): skips
315
320
  // recomposing on the very first run when a preview already exists, and
316
321
  // dedupes concurrent runs against the same composition signature.
@@ -603,13 +608,22 @@ export default function EditorShell({
603
608
  // snapshot, never the rebased result. Caching the rebase (local dirty
604
609
  // overlays folded in) would poison the next recovery's diff: dirty
605
610
  // sections would read as remote-unchanged (silent local-wins) and a
606
- // locally-added section would read as "deleted remotely".
611
+ // locally-added section would read as "deleted remotely". The manifest
612
+ // follows the same rule: the remote one, not the rebased view.
607
613
  await cacheContent(
608
614
  remote.sha ?? remote.savedBranchSha ?? headSha,
609
615
  remote.sections,
610
616
  remote.index,
611
617
  remote.siteConfig,
612
- { savedBranchSha: remote.savedBranchSha, changedSectionIds: remote.changedSectionIds, mainIndex: remote.mainIndex },
618
+ {
619
+ media: remote.mediaManifest ?? mediaManifestRef.current,
620
+ documents: remote.documentManifest ?? documentManifestRef.current ?? {},
621
+ },
622
+ {
623
+ savedBranchSha: remote.savedBranchSha,
624
+ changedSectionIds: remote.changedSectionIds,
625
+ mainIndex: remote.mainIndex,
626
+ },
613
627
  );
614
628
  return true;
615
629
  } catch (err) {
@@ -698,13 +712,21 @@ export default function EditorShell({
698
712
  // ...but the CLEAN fetched remote snapshot is what gets cached as the
699
713
  // baseline (amendment 1: caching the rebased result would poison the next
700
714
  // 409 recovery — dirty sections would read as remote-unchanged and a ghost
701
- // race would never surface).
715
+ // race would never surface). Same for the manifest: remote, not rebased.
702
716
  await cacheContent(
703
717
  remote.sha ?? remote.savedBranchSha ?? headSha,
704
718
  remote.sections,
705
719
  remote.index,
706
720
  remote.siteConfig,
707
- { savedBranchSha: remote.savedBranchSha, changedSectionIds: remote.changedSectionIds, mainIndex: remote.mainIndex },
721
+ {
722
+ media: remote.mediaManifest ?? mediaManifestRef.current,
723
+ documents: remote.documentManifest ?? documentManifestRef.current ?? {},
724
+ },
725
+ {
726
+ savedBranchSha: remote.savedBranchSha,
727
+ changedSectionIds: remote.changedSectionIds,
728
+ mainIndex: remote.mainIndex,
729
+ },
708
730
  );
709
731
  } catch (err) {
710
732
  console.error("Remote saved-event sync failed:", err);
@@ -947,17 +969,24 @@ export default function EditorShell({
947
969
  // just-saved sections now differ from main, so fold them into
948
970
  // changedSectionIds (state + ref + cache stay consistent, and a reload that
949
971
  // hits this cache shows the same badges the server would report).
950
- onBaselineCommitted: async ({ sha, sections: baselineSections, index, savedIds }) => {
972
+ onBaselineCommitted: async ({ sha, sections: baselineSections, index, savedIds, mediaManifest: committedManifest, documentManifest: committedDocumentManifest }) => {
951
973
  const changed = new Set(
952
974
  [...changedSectionIdsRef.current, ...savedIds].filter((id) => id in index.sections),
953
975
  );
954
976
  changedSectionIdsRef.current = changed;
955
977
  setChangedSectionIds(changed);
956
- await cacheContent(sha, baselineSections, index, (siteConfig ?? {}) as Record<string, unknown>, {
957
- savedBranchSha: sha,
958
- changedSectionIds: [...changed],
959
- mainIndex: mainIndexRef.current,
960
- });
978
+ await cacheContent(
979
+ sha, baselineSections, index, (siteConfig ?? {}) as Record<string, unknown>,
980
+ // What the save actually committed — NOT the refs/state, which the
981
+ // onMediaPublished / onDocumentsPublished callbacks only refresh through a
982
+ // state effect and can still lag by a render at this point.
983
+ { media: committedManifest, documents: committedDocumentManifest },
984
+ {
985
+ savedBranchSha: sha,
986
+ changedSectionIds: [...changed],
987
+ mainIndex: mainIndexRef.current,
988
+ },
989
+ );
961
990
  },
962
991
  savingRef: inFlightSaveRef,
963
992
  getEditGeneration: persistence.currentGeneration,
@@ -1148,36 +1177,40 @@ export default function EditorShell({
1148
1177
  // deliberately don't block every load on a freshness fetch to validate this.
1149
1178
  const cacheKeySha = draftHeadSha ?? headSha;
1150
1179
 
1180
+ // A row missing either sidecar predates them moving onto it (v11); it is
1181
+ // not a usable snapshot — serving it would render every image as "No Image"
1182
+ // and every committed document as "File missing" — so treat it as a miss
1183
+ // and re-fetch. An EMPTY manifest is a legitimate value, not an absence.
1151
1184
  const cached = await getCachedContent();
1152
- if (cached && cached.sha === cacheKeySha) {
1185
+ if (cached && cached.sha === cacheKeySha && cached.mediaManifest && cached.documentManifest) {
1153
1186
  loadedSections = cached.sections;
1154
1187
  loadedIndex = cached.index;
1155
1188
  loadedConfig = SiteConfigSchema.parse(cached.siteConfig);
1156
- const savedManifest = await getMediaManifest();
1157
- if (savedManifest) loadedManifest = savedManifest;
1189
+ loadedManifest = cached.mediaManifest;
1158
1190
  setSavedSha(cached.savedBranchSha ?? null);
1159
1191
  setMainSha(headSha);
1160
1192
  setChangedSectionIds(new Set(cached.changedSectionIds ?? []));
1161
1193
  setMainIndex(cached.mainIndex ?? loadedIndex);
1162
1194
  // Without this, a cache-hit load left documentManifest null forever —
1163
1195
  // the siteDocuments memo's null-guard then never lifted and every
1164
- // document row (committed or staged) read "File missing".
1165
- setDocumentManifest(cached.documentManifest ?? {});
1196
+ // document row (committed or staged) read "File missing". The row's copy
1197
+ // is authoritative: the hit gate above rejected a row without one.
1198
+ setDocumentManifest(cached.documentManifest);
1166
1199
  } else {
1167
1200
  const data = await fetchSavedContent();
1168
1201
  loadedSections = data.sections;
1169
1202
  loadedIndex = data.index;
1170
1203
  loadedConfig = SiteConfigSchema.parse(data.siteConfig);
1171
- await cacheContent(data.sha, data.sections, data.index, data.siteConfig, {
1172
- savedBranchSha: data.savedBranchSha,
1173
- changedSectionIds: data.changedSectionIds,
1174
- mainIndex: data.mainIndex,
1175
- documentManifest: data.documentManifest ?? {},
1176
- });
1177
- if (data.mediaManifest) {
1178
- loadedManifest = data.mediaManifest;
1179
- await persistMediaManifest(loadedManifest);
1180
- }
1204
+ if (data.mediaManifest) loadedManifest = data.mediaManifest;
1205
+ await cacheContent(
1206
+ data.sha, data.sections, data.index, data.siteConfig,
1207
+ { media: loadedManifest, documents: data.documentManifest ?? {} },
1208
+ {
1209
+ savedBranchSha: data.savedBranchSha,
1210
+ changedSectionIds: data.changedSectionIds,
1211
+ mainIndex: data.mainIndex,
1212
+ },
1213
+ )
1181
1214
  setDocumentManifest(data.documentManifest ?? {});
1182
1215
  setSavedSha(data.savedBranchSha);
1183
1216
  setMainSha(headSha);
@@ -1343,9 +1376,11 @@ export default function EditorShell({
1343
1376
  setSiteConfig(parsed.data);
1344
1377
  applySiteConfigPreview(parsed.data);
1345
1378
  }
1379
+ // Discard restores the cached snapshot, so the sidecars must come from that
1380
+ // same row — anything else could disagree with the sections we just set.
1381
+ if (cached.mediaManifest) setMediaManifest(cached.mediaManifest);
1382
+ if (cached.documentManifest) setDocumentManifest(cached.documentManifest);
1346
1383
  }
1347
- const savedManifest = await getMediaManifest();
1348
- setMediaManifest(savedManifest ?? { images: {}, folders: {} });
1349
1384
  persistence.clearConfigDirty();
1350
1385
  // Discard wiped the Dexie siteIndex row — no unsaved structure remains.
1351
1386
  // Tokenless: discard is unconditional, unlike the save-success clear.
@@ -98,7 +98,7 @@ interface PublishDeps {
98
98
  * baseline that recoverConflict diffs against. Without this, the baseline
99
99
  * dies at the first save (discardSavedChanges clears it) and later 409
100
100
  * recoveries would silently merge local-wins. */
101
- onBaselineCommitted?: (args: { sha: string; sections: LoadedSection[]; index: SiteIndex; savedIds: string[] }) => Promise<void>;
101
+ onBaselineCommitted?: (args: { sha: string; sections: LoadedSection[]; index: SiteIndex; savedIds: string[]; mediaManifest: MediaManifest; documentManifest: DocumentManifest }) => Promise<void>;
102
102
  /** Injected by EditorShell. Receives non-fatal server warnings from a
103
103
  * successful save (e.g. the nav.json replacement was declined by the
104
104
  * degraded-nav guard) so the shell can surface them instead of the save
@@ -291,6 +291,15 @@ export function useEditorPublish({
291
291
  uploadedItems: MediaItem[];
292
292
  // The index actually sent (deletions applied) — the committed index state.
293
293
  sentIndex: SiteIndex;
294
+ // The media manifest as committed by this save: the assembled manifest when
295
+ // media changed, otherwise the (unchanged) manifest we saved against. The
296
+ // content-cache re-prime writes it alongside the new sha, so the two can
297
+ // never drift.
298
+ committedManifest: MediaManifest;
299
+ // The document sidecar as committed by this save — the view we saved against,
300
+ // plus the entries this save committed, minus the ids it deleted. Same
301
+ // contract as committedManifest: it rides the cache write for this sha.
302
+ committedDocumentManifest: DocumentManifest;
294
303
  committedDocumentIds: string[];
295
304
  // The deleted ids the final request actually carried — empty when nothing
296
305
  // was staged, and never populated on a failure (see the catch below).
@@ -499,6 +508,10 @@ export function useEditorPublish({
499
508
  savedSections: dirty.map(({ sectionId, updatedAt, content }) => ({ sectionId, updatedAt, content })),
500
509
  uploadedItems: [...uploadedItems, ...finalUploads.map((u) => u.item)],
501
510
  sentIndex: siteIndex,
511
+ committedManifest: gathered.updatedManifest ?? args.mediaManifest,
512
+ // Read AFTER the final request: committedDocuments is complete and the
513
+ // deletions (which ride only that request) have landed.
514
+ committedDocumentManifest: documentManifestToSend(),
502
515
  committedDocumentIds,
503
516
  committedDocumentDeletions: args.pendingDocumentDeletions,
504
517
  };
@@ -659,7 +672,7 @@ export function useEditorPublish({
659
672
  return;
660
673
  }
661
674
 
662
- const { sha, savedSections, uploadedItems, sentIndex, committedDocumentIds, committedDocumentDeletions } = await saveWithConflictRecovery();
675
+ const { sha, savedSections, uploadedItems, sentIndex, committedManifest, committedDocumentManifest, committedDocumentIds, committedDocumentDeletions } = await saveWithConflictRecovery();
663
676
  onShasUpdated(sha, null);
664
677
 
665
678
  try {
@@ -681,6 +694,8 @@ export function useEditorPublish({
681
694
  sections: baselineSections,
682
695
  index: sentIndex,
683
696
  savedIds: savedSections.map((s) => s.sectionId),
697
+ mediaManifest: committedManifest,
698
+ documentManifest: committedDocumentManifest,
684
699
  });
685
700
  }
686
701
  finishSave(uploadedItems, savedSections.map((s) => s.sectionId));
@@ -750,10 +765,12 @@ export function useEditorPublish({
750
765
  let savedSections: { sectionId: string; updatedAt: string; content: SectionContent }[] = [];
751
766
  let uploadedItems: MediaItem[] = [];
752
767
  let sentIndex: SiteIndex | null = null;
768
+ let committedManifest: MediaManifest | null = null;
769
+ let committedDocumentManifest: DocumentManifest | null = null;
753
770
  let committedDocumentIds: string[] = [];
754
771
  let committedDocumentDeletions: string[] = [];
755
772
  if (hasLocalEdits) {
756
- ({ savedSections, uploadedItems, sentIndex, committedDocumentIds, committedDocumentDeletions } = await saveWithConflictRecovery());
773
+ ({ savedSections, uploadedItems, sentIndex, committedManifest, committedDocumentManifest, committedDocumentIds, committedDocumentDeletions } = await saveWithConflictRecovery());
757
774
  }
758
775
 
759
776
  setPublishAction("publishing");
@@ -789,7 +806,15 @@ export function useEditorPublish({
789
806
  savedRows: savedSections,
790
807
  sentIndex: sentIndex!,
791
808
  });
792
- await cacheContent(sha, baselineSections, sentIndex!, siteConfig!);
809
+ // Both sidecars ride the same write as the sha they belong to: publish
810
+ // deleted the draft, so main's head IS this sha and the next load hits
811
+ // this row. Omitting them here is what blanked freshly uploaded images
812
+ // ("No Image", while the published site was correct) and freshly
813
+ // committed documents ("File missing").
814
+ await cacheContent(sha, baselineSections, sentIndex!, siteConfig!, {
815
+ media: committedManifest!,
816
+ documents: committedDocumentManifest!,
817
+ });
793
818
  finishSave(uploadedItems, savedSections.map((s) => s.sectionId));
794
819
  if (committedDocumentIds.length > 0) onDocumentsPublished?.(committedDocumentIds);
795
820
  if (committedDocumentDeletions.length > 0) onDocumentsDeleted?.(committedDocumentDeletions);
package/src/lib/dexie.ts CHANGED
@@ -50,19 +50,21 @@ interface ContentCacheRow {
50
50
  savedBranchSha?: string | null;
51
51
  changedSectionIds?: string[];
52
52
  mainIndex?: SiteIndex | null;
53
- // The document sidecar at this sha. Optional for rows written by older
54
- // packages; readers fall back to {} (same posture as the fetch path's
55
- // `data.documentManifest ?? {}`). Without this, a cache-hit editor load left
56
- // documentManifest null and the whole documents feature dead for the session.
53
+ // Both sidecars AT THIS SHA. They live HERE and nowhere else, and cacheContent
54
+ // takes them as a required argument, because every way of storing them apart
55
+ // from the sha drifted: the media manifest was a separate singleton row and the
56
+ // document manifest was an optional extra, and both were written only by the
57
+ // cache-miss load path — so every save/publish that re-primed this row advanced
58
+ // the sha and left them behind. A later cache hit then restored pre-upload
59
+ // media (freshly uploaded images rendered "No Image" in the editor while the
60
+ // published site was correct) and an empty document manifest (committed
61
+ // documents read "File missing"). Optional in the TYPE only because Dexie rows
62
+ // are not schema-enforced; readers treat absence as a MISS (getCachedContent),
63
+ // never as "nothing here".
64
+ mediaManifest?: MediaManifest | null;
57
65
  documentManifest?: DocumentManifest | null;
58
66
  }
59
67
 
60
- interface MediaManifestRow {
61
- key: string;
62
- manifest: MediaManifest;
63
- updatedAt: string;
64
- }
65
-
66
68
  interface PendingMediaRow {
67
69
  id: string;
68
70
  item: MediaItem;
@@ -101,7 +103,6 @@ class EditorDatabase extends Dexie {
101
103
  meta!: Dexie.Table<MetaRow, string>;
102
104
  siteConfig!: Dexie.Table<SiteConfigRow, string>;
103
105
  contentCache!: Dexie.Table<ContentCacheRow, string>;
104
- mediaManifest!: Dexie.Table<MediaManifestRow, string>;
105
106
  pendingMedia!: Dexie.Table<PendingMediaRow, string>;
106
107
  pendingMediaDeletions!: Dexie.Table<PendingMediaDeletionRow, string>;
107
108
  preservedEdits!: Dexie.Table<PreservedEditRow, string>;
@@ -223,6 +224,28 @@ class EditorDatabase extends Dexie {
223
224
  preservedEdits: "id, sectionId",
224
225
  pendingDocuments: "id",
225
226
  });
227
+ // v11: the media manifest moved onto the sha-keyed contentCache row and the
228
+ // standalone `mediaManifest` store is deleted. It was written by the
229
+ // cache-miss load path ONLY, so every save/publish that re-primed
230
+ // contentCache advanced the cached sha while leaving the manifest at its
231
+ // older value — and the next cache hit served that stale manifest, blanking
232
+ // every image uploaded since. Dropping the store makes the drift
233
+ // unrepresentable. contentCache is cleared because no pre-v11 row carries a
234
+ // manifest; the next load re-fetches and writes both together.
235
+ this.version(11).stores({
236
+ sections: "sectionId",
237
+ siteIndex: "key",
238
+ meta: "key",
239
+ siteConfig: "key",
240
+ contentCache: "key",
241
+ mediaManifest: null,
242
+ pendingMedia: "id",
243
+ pendingMediaDeletions: "id",
244
+ preservedEdits: "id, sectionId",
245
+ pendingDocuments: "id",
246
+ }).upgrade(async (tx) => {
247
+ await tx.table("contentCache").clear();
248
+ });
226
249
  }
227
250
  }
228
251
 
@@ -478,12 +501,28 @@ export async function getDeletedSections(): Promise<string[]> {
478
501
  return row?.deletedSections ?? [];
479
502
  }
480
503
 
504
+ /**
505
+ * Write the content snapshot for one commit.
506
+ *
507
+ * `manifests` is REQUIRED, and that is the whole point: both sidecars belong to
508
+ * this sha, so every writer that advances the row must restate them. They used to
509
+ * be optional (the media one lived in a separate store entirely), and every
510
+ * save/publish that re-primed this row left them behind at an older sha — a later
511
+ * cache hit then served pre-upload media ("No Image" on images the published site
512
+ * rendered fine) and an empty document manifest ("File missing" on committed
513
+ * documents). Passing them positionally makes forgetting a compile error.
514
+ */
481
515
  export async function cacheContent(
482
516
  sha: string,
483
517
  sections: LoadedSection[],
484
518
  index: SiteIndex,
485
519
  siteConfig: Record<string, unknown>,
486
- diff?: { savedBranchSha: string | null; changedSectionIds: string[]; mainIndex: SiteIndex | null; documentManifest?: DocumentManifest },
520
+ manifests: { media: MediaManifest; documents: DocumentManifest },
521
+ diff?: {
522
+ savedBranchSha?: string | null;
523
+ changedSectionIds?: string[];
524
+ mainIndex?: SiteIndex | null;
525
+ },
487
526
  ): Promise<void> {
488
527
  const now = new Date().toISOString();
489
528
  await getDb().contentCache.put({
@@ -496,7 +535,8 @@ export async function cacheContent(
496
535
  savedBranchSha: diff?.savedBranchSha ?? null,
497
536
  changedSectionIds: diff?.changedSectionIds ?? [],
498
537
  mainIndex: diff?.mainIndex ?? null,
499
- documentManifest: diff?.documentManifest ?? null,
538
+ mediaManifest: manifests.media,
539
+ documentManifest: manifests.documents,
500
540
  });
501
541
  }
502
542
 
@@ -508,7 +548,13 @@ export async function getCachedContent(): Promise<{
508
548
  savedBranchSha: string | null;
509
549
  changedSectionIds: string[];
510
550
  mainIndex: SiteIndex | null;
511
- documentManifest: DocumentManifest;
551
+ /** Both are null ONLY for a row written before the sidecars moved onto it.
552
+ * Callers must treat null as an unusable cache and re-fetch — substituting an
553
+ * empty manifest would blank every image ("No Image") or every committed
554
+ * document ("File missing") on the page. An EMPTY manifest is a legitimate
555
+ * value and is not null. */
556
+ mediaManifest: MediaManifest | null;
557
+ documentManifest: DocumentManifest | null;
512
558
  } | null> {
513
559
  const row = await getDb().contentCache.get("current");
514
560
  if (!row) return null;
@@ -522,10 +568,8 @@ export async function getCachedContent(): Promise<{
522
568
  // No stored mainIndex (e.g. legacy or post-publish cache) means "no draft",
523
569
  // so the published index equals the current index.
524
570
  mainIndex: row.mainIndex ?? row.index,
525
- // Legacy row (pre-documents package): {} matches the fetch path's posture.
526
- // The narrow stale edge — an old row at a sha that DOES hold a manifest —
527
- // heals on the next fresh load or save.
528
- documentManifest: row.documentManifest ?? {},
571
+ mediaManifest: row.mediaManifest ?? null,
572
+ documentManifest: row.documentManifest ?? null,
529
573
  };
530
574
  }
531
575
 
@@ -536,16 +580,6 @@ export async function clearContentCache(): Promise<void> {
536
580
  await getDb().contentCache.clear();
537
581
  }
538
582
 
539
- export async function persistMediaManifest(manifest: MediaManifest): Promise<void> {
540
- const now = new Date().toISOString();
541
- await getDb().mediaManifest.put({ key: "current", manifest, updatedAt: now });
542
- }
543
-
544
- export async function getMediaManifest(): Promise<MediaManifest | null> {
545
- const row = await getDb().mediaManifest.get("current");
546
- return row?.manifest ?? null;
547
- }
548
-
549
583
  export async function addPendingMediaItem(
550
584
  item: MediaItem,
551
585
  localUrls: Record<string, string> = {},
@@ -43,4 +43,7 @@ export const LinkValueSchema = z.discriminatedUnion("kind", [
43
43
 
44
44
  export type LinkValue = z.infer<typeof LinkValueSchema>;
45
45
 
46
- export const DEFAULT_LINK: LinkValue = { kind: "external", href: "", target: "_self" };
46
+ // Defaults to an (empty) internal link so the picker opens on the Internal-page
47
+ // tab everywhere it is used; an empty pageId resolves to no href, same as an
48
+ // empty external link, so this stays a safe "unset" sentinel.
49
+ export const DEFAULT_LINK: LinkValue = { kind: "internal", pageId: "", anchorSectionId: null, target: "_self" };
@@ -1,123 +0,0 @@
1
- import {
2
- cn
3
- } from "./chunk-7EOSDFS3.js";
4
-
5
- // src/components/shared/FormLabel.tsx
6
- import { jsx } from "react/jsx-runtime";
7
- function FormLabel({ htmlFor, children, className }) {
8
- return /* @__PURE__ */ jsx(
9
- "label",
10
- {
11
- htmlFor,
12
- className: cn("mb-1.5 block text-sm font-medium text-base-contrast", className),
13
- children
14
- }
15
- );
16
- }
17
-
18
- // src/components/shared/Input.tsx
19
- import { forwardRef, useId } from "react";
20
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
21
- var Input = forwardRef(function Input2({ label, value, onChange, type = "text", className, disabled, error, ...rest }, ref) {
22
- const id = useId();
23
- const errorId = `${id}-error`;
24
- return /* @__PURE__ */ jsxs("div", { className, children: [
25
- /* @__PURE__ */ jsx2(FormLabel, { htmlFor: id, children: label }),
26
- /* @__PURE__ */ jsx2(
27
- "input",
28
- {
29
- ref,
30
- id,
31
- type,
32
- value,
33
- onChange: (e) => onChange(e.target.value),
34
- disabled,
35
- "aria-invalid": error ? true : void 0,
36
- "aria-describedby": error ? errorId : void 0,
37
- className: cn(
38
- "w-full rounded border bg-base px-3 py-2 text-sm text-base-contrast focus:outline-none focus:ring-1",
39
- error ? "border-red-600 focus:border-red-600 focus:ring-red-600" : "border-base-200 focus:border-base-contrast focus:ring-base-contrast",
40
- disabled && "cursor-not-allowed opacity-50"
41
- ),
42
- ...rest
43
- }
44
- ),
45
- error && /* @__PURE__ */ jsx2("p", { id: errorId, className: "mt-1 text-xs text-red-600", children: error })
46
- ] });
47
- });
48
-
49
- // src/components/shared/Checkbox.tsx
50
- import { useId as useId2 } from "react";
51
- import { Check } from "lucide-react";
52
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
53
- function Checkbox({
54
- checked,
55
- onChange,
56
- label,
57
- description,
58
- disabled,
59
- className,
60
- startAdornment,
61
- align = "start"
62
- }) {
63
- const id = useId2();
64
- return /* @__PURE__ */ jsxs2(
65
- "label",
66
- {
67
- htmlFor: id,
68
- className: cn(
69
- "flex cursor-pointer gap-3 select-none hover:opacity-80",
70
- align === "start" ? "items-start" : "items-center",
71
- disabled && "cursor-not-allowed opacity-50",
72
- className
73
- ),
74
- children: [
75
- /* @__PURE__ */ jsxs2(
76
- "span",
77
- {
78
- className: cn(
79
- "relative flex h-5 w-5 shrink-0 items-center justify-center",
80
- align === "start" && "mt-0.5"
81
- ),
82
- children: [
83
- /* @__PURE__ */ jsx3(
84
- "input",
85
- {
86
- id,
87
- type: "checkbox",
88
- checked,
89
- disabled,
90
- onChange: (e) => onChange(e.target.checked),
91
- className: "sr-only"
92
- }
93
- ),
94
- /* @__PURE__ */ jsx3(
95
- "span",
96
- {
97
- "aria-hidden": "true",
98
- className: cn(
99
- "flex h-5 w-5 items-center justify-center rounded border transition-colors",
100
- checked ? "border-primary bg-primary text-primary-contrast" : "border-base-300 bg-base hover:border-primary",
101
- disabled && "pointer-events-none"
102
- ),
103
- children: checked && /* @__PURE__ */ jsx3(Check, { size: 14, strokeWidth: 3 })
104
- }
105
- )
106
- ]
107
- }
108
- ),
109
- startAdornment,
110
- /* @__PURE__ */ jsxs2("div", { className: "text-sm", children: [
111
- /* @__PURE__ */ jsx3("span", { className: "font-medium text-base-contrast", children: label }),
112
- description && /* @__PURE__ */ jsx3("span", { className: "block text-base-contrast-light", children: description })
113
- ] })
114
- ]
115
- }
116
- );
117
- }
118
-
119
- export {
120
- FormLabel,
121
- Input,
122
- Checkbox
123
- };
@@ -1,70 +0,0 @@
1
- import {
2
- cn
3
- } from "./chunk-7EOSDFS3.js";
4
-
5
- // src/components/shared/Button.tsx
6
- import { forwardRef } from "react";
7
- import { jsx } from "react/jsx-runtime";
8
- var boxSize = {
9
- sm: "rounded px-3 py-1.5 text-xs font-medium",
10
- md: "rounded px-4 py-2 text-sm font-medium"
11
- };
12
- var ghostSize = {
13
- sm: "text-xs font-medium",
14
- md: "text-sm font-medium"
15
- };
16
- var variantClasses = {
17
- primary: "bg-base-contrast text-base-accent hover:bg-base-contrast/90",
18
- brand: "bg-primary text-primary-contrast hover:opacity-90",
19
- secondary: "border border-base-200 text-base-contrast hover:bg-base-accent",
20
- destructive: "border border-red-600 text-red-600 hover:bg-red-600 hover:text-white"
21
- };
22
- var ghostToneClasses = {
23
- neutral: "text-base-contrast-light hover:text-base-contrast",
24
- destructive: "text-red-600 hover:text-red-800"
25
- };
26
- var Button = forwardRef(function Button2({
27
- variant = "primary",
28
- size = "sm",
29
- tone = "neutral",
30
- fullWidth = false,
31
- isLoading = false,
32
- loadingLabel,
33
- disabled,
34
- className,
35
- children,
36
- ...rest
37
- }, ref) {
38
- const isGhost = variant === "ghost";
39
- const resolvedDisabled = disabled || isLoading;
40
- const label = isLoading && loadingLabel ? loadingLabel : children;
41
- return /* @__PURE__ */ jsx(
42
- "button",
43
- {
44
- ref,
45
- disabled: resolvedDisabled,
46
- className: cn(
47
- "inline-flex items-center justify-center transition-colors",
48
- isGhost ? ghostSize[size] : boxSize[size],
49
- isGhost ? ghostToneClasses[tone] : variantClasses[variant],
50
- fullWidth && "w-full",
51
- resolvedDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
52
- className
53
- ),
54
- ...rest,
55
- children: label
56
- }
57
- );
58
- });
59
-
60
- // src/components/shared/PagesContext.tsx
61
- import { createContext, useContext } from "react";
62
- var PagesContext = createContext({ pages: [], getPageHeadings: () => [] });
63
- function usePages() {
64
- return useContext(PagesContext);
65
- }
66
-
67
- export {
68
- Button,
69
- usePages
70
- };