@drawnagency/primitives 0.1.76 → 0.1.78

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 (122) hide show
  1. package/dist/{chunk-Q3T23HVW.js → chunk-2DDWWTRX.js} +5 -5
  2. package/dist/chunk-2G2S36SY.js +13 -0
  3. package/dist/{chunk-S55OZ7F2.js → chunk-3LNKOLZP.js} +4 -0
  4. package/dist/{chunk-SWIHUIFT.js → chunk-4SIWBWKZ.js} +12 -2
  5. package/dist/{chunk-SWEVWC5K.js → chunk-BAQYZSFF.js} +22 -22
  6. package/dist/{chunk-IFHRHV7S.js → chunk-EMGLZQCM.js} +1 -1
  7. package/dist/{chunk-NKVZGPR6.js → chunk-K4JD6FD3.js} +1 -1
  8. package/dist/{chunk-SFKYUKVN.js → chunk-MK4POMLZ.js} +1 -1
  9. package/dist/{chunk-7T2WQSWF.js → chunk-NS3UTJ3E.js} +11 -11
  10. package/dist/chunk-REM2NH7K.js +44 -0
  11. package/dist/{chunk-T7MP7ANP.js → chunk-UA6OCJJK.js} +2 -2
  12. package/dist/{chunk-ZRWGV5O6.js → chunk-Z5IYWUW3.js} +3 -3
  13. package/dist/collab/provider-registry.d.ts +4 -0
  14. package/dist/collab/provider-registry.d.ts.map +1 -0
  15. package/dist/collab/provider-registry.js +9 -0
  16. package/dist/collab/rebase.d.ts +37 -0
  17. package/dist/collab/rebase.d.ts.map +1 -0
  18. package/dist/collab/rebase.js +64 -0
  19. package/dist/collab/recovery.d.ts +89 -0
  20. package/dist/collab/recovery.d.ts.map +1 -0
  21. package/dist/collab/types.d.ts +93 -0
  22. package/dist/collab/types.d.ts.map +1 -0
  23. package/dist/collab/types.js +19 -0
  24. package/dist/components/editor/AudienceIndicator.d.ts +3 -1
  25. package/dist/components/editor/AudienceIndicator.d.ts.map +1 -1
  26. package/dist/components/editor/ConflictDialog.d.ts +14 -0
  27. package/dist/components/editor/ConflictDialog.d.ts.map +1 -0
  28. package/dist/components/editor/IndicatorPill.d.ts +4 -1
  29. package/dist/components/editor/IndicatorPill.d.ts.map +1 -1
  30. package/dist/components/editor/LockBadge.d.ts +7 -0
  31. package/dist/components/editor/LockBadge.d.ts.map +1 -0
  32. package/dist/components/editor/PresenceAvatars.d.ts +9 -0
  33. package/dist/components/editor/PresenceAvatars.d.ts.map +1 -0
  34. package/dist/components/editor/SectionWrapper.d.ts +1 -1
  35. package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
  36. package/dist/components/editor/StatusIndicator.d.ts +3 -1
  37. package/dist/components/editor/StatusIndicator.d.ts.map +1 -1
  38. package/dist/components/editor/index.js +95 -47
  39. package/dist/components/primitives/index.js +5 -5
  40. package/dist/components/sections/brand-guide.js +5 -5
  41. package/dist/components/sections/register-schemas.js +11 -11
  42. package/dist/components/sections/register.js +11 -11
  43. package/dist/components/shell/EditorShell.d.ts +3 -1
  44. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  45. package/dist/components/shell/MediaBreadcrumb.d.ts +10 -0
  46. package/dist/components/shell/MediaBreadcrumb.d.ts.map +1 -0
  47. package/dist/components/shell/MediaFolderTile.d.ts +15 -0
  48. package/dist/components/shell/MediaFolderTile.d.ts.map +1 -0
  49. package/dist/components/shell/MediaFolderTree.d.ts +20 -0
  50. package/dist/components/shell/MediaFolderTree.d.ts.map +1 -0
  51. package/dist/components/shell/MediaLibraryModal.d.ts +15 -3
  52. package/dist/components/shell/MediaLibraryModal.d.ts.map +1 -1
  53. package/dist/components/shell/MoveToFolderDialog.d.ts +16 -0
  54. package/dist/components/shell/MoveToFolderDialog.d.ts.map +1 -0
  55. package/dist/hooks/useCollabSession.d.ts +23 -0
  56. package/dist/hooks/useCollabSession.d.ts.map +1 -0
  57. package/dist/hooks/useEditorPersistence.d.ts +21 -1
  58. package/dist/hooks/useEditorPersistence.d.ts.map +1 -1
  59. package/dist/hooks/useEditorPublish.d.ts +39 -2
  60. package/dist/hooks/useEditorPublish.d.ts.map +1 -1
  61. package/dist/hooks/useMediaFolderDnd.d.ts +27 -0
  62. package/dist/hooks/useMediaFolderDnd.d.ts.map +1 -0
  63. package/dist/hooks/useMediaPipeline.d.ts +14 -2
  64. package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
  65. package/dist/index.d.ts +2 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +51 -25
  68. package/dist/lib/dexie.d.ts +12 -0
  69. package/dist/lib/dexie.d.ts.map +1 -1
  70. package/dist/lib/dexie.js +27 -0
  71. package/dist/lib/index.js +12 -12
  72. package/dist/lib/media-dnd.d.ts +26 -0
  73. package/dist/lib/media-dnd.d.ts.map +1 -0
  74. package/dist/lib/media-folders.d.ts +59 -0
  75. package/dist/lib/media-folders.d.ts.map +1 -0
  76. package/dist/lib/registry.d.ts +10 -0
  77. package/dist/lib/registry.d.ts.map +1 -1
  78. package/dist/media/folder-drop.d.ts +22 -0
  79. package/dist/media/folder-drop.d.ts.map +1 -0
  80. package/dist/media/index.js +6 -2
  81. package/dist/media/types.d.ts +1 -1
  82. package/dist/media/types.d.ts.map +1 -1
  83. package/dist/media/utils.d.ts +7 -0
  84. package/dist/media/utils.d.ts.map +1 -1
  85. package/dist/schemas/index.js +5 -3
  86. package/dist/schemas/media.d.ts +11 -0
  87. package/dist/schemas/media.d.ts.map +1 -1
  88. package/dist/types/database.d.ts +70 -0
  89. package/dist/types/database.d.ts.map +1 -1
  90. package/package.json +13 -1
  91. package/src/collab/provider-registry.ts +14 -0
  92. package/src/collab/rebase.ts +140 -0
  93. package/src/collab/recovery.ts +149 -0
  94. package/src/collab/types.ts +96 -0
  95. package/src/components/editor/AudienceIndicator.tsx +4 -1
  96. package/src/components/editor/ConflictDialog.tsx +61 -0
  97. package/src/components/editor/IndicatorPill.tsx +12 -6
  98. package/src/components/editor/LockBadge.tsx +31 -0
  99. package/src/components/editor/PresenceAvatars.tsx +53 -0
  100. package/src/components/editor/SectionWrapper.tsx +60 -29
  101. package/src/components/editor/StatusIndicator.tsx +4 -0
  102. package/src/components/shell/EditorShell.tsx +581 -22
  103. package/src/components/shell/MediaBreadcrumb.tsx +77 -0
  104. package/src/components/shell/MediaFolderTile.tsx +141 -0
  105. package/src/components/shell/MediaFolderTree.tsx +127 -0
  106. package/src/components/shell/MediaLibraryModal.tsx +611 -229
  107. package/src/components/shell/MoveToFolderDialog.tsx +78 -0
  108. package/src/hooks/useCollabSession.ts +220 -0
  109. package/src/hooks/useEditorPersistence.ts +104 -1
  110. package/src/hooks/useEditorPublish.ts +216 -71
  111. package/src/hooks/useMediaFolderDnd.ts +129 -0
  112. package/src/hooks/useMediaPipeline.ts +162 -6
  113. package/src/index.ts +2 -0
  114. package/src/lib/dexie.ts +44 -0
  115. package/src/lib/media-dnd.ts +62 -0
  116. package/src/lib/media-folders.ts +232 -0
  117. package/src/lib/registry.ts +8 -0
  118. package/src/media/folder-drop.ts +62 -0
  119. package/src/media/types.ts +2 -0
  120. package/src/media/utils.ts +10 -0
  121. package/src/schemas/media.ts +13 -0
  122. package/src/types/database.ts +55 -0
@@ -1,12 +1,12 @@
1
- import {
2
- Button,
3
- usePages
4
- } from "./chunk-ZSPCE6MZ.js";
5
1
  import {
6
2
  Checkbox,
7
3
  FormLabel,
8
4
  Input
9
5
  } from "./chunk-5THS4K5D.js";
6
+ import {
7
+ Button,
8
+ usePages
9
+ } from "./chunk-ZSPCE6MZ.js";
10
10
  import {
11
11
  DeleteIcon,
12
12
  DragHandle,
@@ -14,7 +14,7 @@ import {
14
14
  SettingsIcon,
15
15
  useMediaLibrary,
16
16
  useResolvedMedia
17
- } from "./chunk-IFHRHV7S.js";
17
+ } from "./chunk-EMGLZQCM.js";
18
18
  import {
19
19
  cn
20
20
  } from "./chunk-7EOSDFS3.js";
@@ -0,0 +1,13 @@
1
+ // src/collab/provider-registry.ts
2
+ var provider = null;
3
+ function setCollabProvider(p) {
4
+ provider = p;
5
+ }
6
+ function getCollabProvider() {
7
+ return provider;
8
+ }
9
+
10
+ export {
11
+ setCollabProvider,
12
+ getCollabProvider
13
+ };
@@ -60,6 +60,9 @@ function formatFileSize(bytes) {
60
60
  if (bytes >= 1024) return `${Math.round(bytes / 1024)}KB`;
61
61
  return `${bytes}B`;
62
62
  }
63
+ function displayName(item) {
64
+ return item.originalName;
65
+ }
63
66
 
64
67
  // src/media/queue.ts
65
68
  var ProcessingQueue = class {
@@ -315,6 +318,7 @@ export {
315
318
  displayFilenameExt,
316
319
  displayFilename,
317
320
  formatFileSize,
321
+ displayName,
318
322
  ProcessingQueue,
319
323
  resolveMedia,
320
324
  resolveManifestReferences,
@@ -68,6 +68,11 @@ var VariantSchema = z2.object({
68
68
  height: z2.number().int().positive(),
69
69
  size: z2.number().int().nonnegative()
70
70
  });
71
+ var MediaFolderSchema = z2.object({
72
+ name: z2.string().min(1),
73
+ parentId: z2.string().nullable()
74
+ // null = root
75
+ });
71
76
  var MediaItemSchema = z2.object({
72
77
  id: z2.string(),
73
78
  hash: z2.string(),
@@ -88,10 +93,14 @@ var MediaItemSchema = z2.object({
88
93
  // resolution needs no env access in the browser or at SSR.
89
94
  url: z2.string().optional(),
90
95
  // Seconds; videos only. Informational (library UI).
91
- duration: z2.number().positive().optional()
96
+ duration: z2.number().positive().optional(),
97
+ // Display folder (MediaFolderSchema key). Absent = root/unfiled. NOT the
98
+ // git storage dir — that's `folder` above, which never changes.
99
+ folderId: z2.string().optional()
92
100
  });
93
101
  var ImageManifestSchema = z2.object({
94
- images: z2.record(z2.string(), MediaItemSchema)
102
+ images: z2.record(z2.string(), MediaItemSchema),
103
+ folders: z2.record(z2.string(), MediaFolderSchema).default({})
95
104
  });
96
105
  var MediaConfigSchema = z2.object({
97
106
  sizes: z2.array(z2.number()).default([640, 1080, 1920]),
@@ -268,6 +277,7 @@ export {
268
277
  HexColorSchema,
269
278
  FontNameSchema,
270
279
  VariantSchema,
280
+ MediaFolderSchema,
271
281
  MediaItemSchema,
272
282
  ImageManifestSchema,
273
283
  MediaConfigSchema,
@@ -1,25 +1,9 @@
1
- import {
2
- EditableRichText,
3
- defineHeadingSection
4
- } from "./chunk-NKVZGPR6.js";
5
- import {
6
- embedUrl
7
- } from "./chunk-Y4JQGO6G.js";
8
1
  import {
9
2
  brandGuideSectionDefs,
10
3
  pluralize,
11
4
  stripHtmlToPlainText,
12
5
  truncate
13
- } from "./chunk-7T2WQSWF.js";
14
- import {
15
- EditablePlainText,
16
- ImageDropZone,
17
- MAX_CONTAINER_COLUMNS,
18
- MediaBlock,
19
- colSpanClass,
20
- containerGridClass,
21
- spacerHiddenClass
22
- } from "./chunk-T7MP7ANP.js";
6
+ } from "./chunk-NS3UTJ3E.js";
23
7
  import {
24
8
  DeleteButton,
25
9
  DragHandle,
@@ -31,11 +15,27 @@ import {
31
15
  buildBlockDropData,
32
16
  canDropBlock,
33
17
  isBlockDragData
34
- } from "./chunk-Q3T23HVW.js";
18
+ } from "./chunk-2DDWWTRX.js";
19
+ import {
20
+ EditableRichText,
21
+ defineHeadingSection
22
+ } from "./chunk-K4JD6FD3.js";
23
+ import {
24
+ EditablePlainText,
25
+ ImageDropZone,
26
+ MAX_CONTAINER_COLUMNS,
27
+ MediaBlock,
28
+ colSpanClass,
29
+ containerGridClass,
30
+ spacerHiddenClass
31
+ } from "./chunk-UA6OCJJK.js";
35
32
  import {
36
33
  useMediaLibrary,
37
34
  useResolvedMedia
38
- } from "./chunk-IFHRHV7S.js";
35
+ } from "./chunk-EMGLZQCM.js";
36
+ import {
37
+ embedUrl
38
+ } from "./chunk-Y4JQGO6G.js";
39
39
  import {
40
40
  DEFAULT_LINK,
41
41
  LinkValueSchema
@@ -49,13 +49,13 @@ import {
49
49
  import {
50
50
  getSectionSchema
51
51
  } from "./chunk-TZQPOR5A.js";
52
+ import {
53
+ SingleMediaReferenceSchema
54
+ } from "./chunk-4SIWBWKZ.js";
52
55
  import {
53
56
  defineSection,
54
57
  getSection
55
58
  } from "./chunk-VKAGMEKE.js";
56
- import {
57
- SingleMediaReferenceSchema
58
- } from "./chunk-SWIHUIFT.js";
59
59
 
60
60
  // src/components/sections/LinkHeading/index.tsx
61
61
  import { Heading1 } from "lucide-react";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getMediaProvider
3
- } from "./chunk-S55OZ7F2.js";
3
+ } from "./chunk-3LNKOLZP.js";
4
4
  import {
5
5
  cn
6
6
  } from "./chunk-7EOSDFS3.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  EditablePlainText
3
- } from "./chunk-T7MP7ANP.js";
3
+ } from "./chunk-UA6OCJJK.js";
4
4
  import {
5
5
  Button,
6
6
  usePages
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  HexColorSchema
3
- } from "./chunk-SWIHUIFT.js";
3
+ } from "./chunk-4SIWBWKZ.js";
4
4
 
5
5
  // src/schemas/audience.ts
6
6
  import { z } from "zod";
@@ -1,21 +1,21 @@
1
+ import {
2
+ Checkbox,
3
+ FormLabel,
4
+ Input
5
+ } from "./chunk-5THS4K5D.js";
1
6
  import {
2
7
  EditableGrid,
3
8
  EditablePlainText,
4
9
  MediaBlock,
5
10
  containerGridClass,
6
11
  useEditableCollection
7
- } from "./chunk-T7MP7ANP.js";
8
- import {
9
- Checkbox,
10
- FormLabel,
11
- Input
12
- } from "./chunk-5THS4K5D.js";
12
+ } from "./chunk-UA6OCJJK.js";
13
13
  import {
14
14
  AddIcon,
15
15
  DeleteIcon,
16
16
  DragHandle,
17
17
  IconButton
18
- } from "./chunk-IFHRHV7S.js";
18
+ } from "./chunk-EMGLZQCM.js";
19
19
  import {
20
20
  DEFAULT_LINK,
21
21
  LinkValueSchema
@@ -27,13 +27,13 @@ import {
27
27
  import {
28
28
  cn
29
29
  } from "./chunk-7EOSDFS3.js";
30
- import {
31
- defineSection
32
- } from "./chunk-VKAGMEKE.js";
33
30
  import {
34
31
  HexColorSchema,
35
32
  SingleMediaReferenceSchema
36
- } from "./chunk-SWIHUIFT.js";
33
+ } from "./chunk-4SIWBWKZ.js";
34
+ import {
35
+ defineSection
36
+ } from "./chunk-VKAGMEKE.js";
37
37
 
38
38
  // src/components/sections/Colors/index.tsx
39
39
  import { z as z2 } from "zod";
@@ -0,0 +1,44 @@
1
+ // src/collab/types.ts
2
+ import { z } from "zod";
3
+ var HEARTBEAT_INTERVAL_MS = 15e3;
4
+ var LOCK_STALE_MS = 45e3;
5
+ var CollabPeerSchema = z.object({
6
+ userId: z.string(),
7
+ email: z.string(),
8
+ focusSectionId: z.string().nullable(),
9
+ heldSectionIds: z.array(z.string())
10
+ });
11
+ var SectionUpdateMessageSchema = z.object({
12
+ v: z.literal(1),
13
+ sectionId: z.string(),
14
+ content: z.unknown(),
15
+ userId: z.string()
16
+ });
17
+ var IndexUpdateMessageSchema = z.object({
18
+ v: z.literal(1),
19
+ index: z.unknown(),
20
+ userId: z.string()
21
+ });
22
+ var TakeoverMessageSchema = z.object({
23
+ v: z.literal(1),
24
+ sectionId: z.string(),
25
+ byUserId: z.string(),
26
+ byEmail: z.string()
27
+ });
28
+ var SavedMessageSchema = z.object({
29
+ v: z.literal(1),
30
+ newSha: z.string(),
31
+ changedSectionIds: z.array(z.string()),
32
+ deletedSectionIds: z.array(z.string()),
33
+ source: z.enum(["editor", "api"])
34
+ });
35
+
36
+ export {
37
+ HEARTBEAT_INTERVAL_MS,
38
+ LOCK_STALE_MS,
39
+ CollabPeerSchema,
40
+ SectionUpdateMessageSchema,
41
+ IndexUpdateMessageSchema,
42
+ TakeoverMessageSchema,
43
+ SavedMessageSchema
44
+ };
@@ -6,10 +6,10 @@ import {
6
6
  SettingsIcon,
7
7
  useMediaLibrary,
8
8
  useResolvedMedia
9
- } from "./chunk-IFHRHV7S.js";
9
+ } from "./chunk-EMGLZQCM.js";
10
10
  import {
11
11
  rebindImageRef
12
- } from "./chunk-S55OZ7F2.js";
12
+ } from "./chunk-3LNKOLZP.js";
13
13
  import {
14
14
  cn
15
15
  } from "./chunk-7EOSDFS3.js";
@@ -7,12 +7,12 @@ import {
7
7
  import {
8
8
  getSectionSchema
9
9
  } from "./chunk-TZQPOR5A.js";
10
+ import {
11
+ IndexSchema
12
+ } from "./chunk-4SIWBWKZ.js";
10
13
  import {
11
14
  getAllSchemas
12
15
  } from "./chunk-VKAGMEKE.js";
13
- import {
14
- IndexSchema
15
- } from "./chunk-SWIHUIFT.js";
16
16
  import {
17
17
  MAX_BLOCK_DEPTH,
18
18
  blockDepth
@@ -0,0 +1,4 @@
1
+ import type { CollabProvider } from "./types";
2
+ export declare function setCollabProvider(p: CollabProvider | null): void;
3
+ export declare function getCollabProvider(): CollabProvider | null;
4
+ //# sourceMappingURL=provider-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-registry.d.ts","sourceRoot":"","sources":["../../src/collab/provider-registry.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAK9C,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,cAAc,GAAG,IAAI,GAAG,IAAI,CAEhE;AAED,wBAAgB,iBAAiB,IAAI,cAAc,GAAG,IAAI,CAEzD"}
@@ -0,0 +1,9 @@
1
+ import {
2
+ getCollabProvider,
3
+ setCollabProvider
4
+ } from "../chunk-2G2S36SY.js";
5
+ import "../chunk-JSBRDJBE.js";
6
+ export {
7
+ getCollabProvider,
8
+ setCollabProvider
9
+ };
@@ -0,0 +1,37 @@
1
+ import type { LoadedSection } from "../lib/loader";
2
+ import type { SiteIndex } from "../schemas/site-config";
3
+ export interface RebaseArgs {
4
+ localSections: LoadedSection[];
5
+ localIndex: SiteIndex;
6
+ localIndexDirty: boolean;
7
+ dirtySectionIds: Set<string>;
8
+ /** Section ids created locally since the last clean baseline (i.e. dirty ids
9
+ * absent from the last-loaded saved-branch index). Callers derive this from
10
+ * the cached clean baseline (Dexie contentCache index), NOT from remoteIndex —
11
+ * remote absence is exactly what this field disambiguates: without it, a
12
+ * locally-added unsaved section is indistinguishable from a section deleted
13
+ * remotely while dirty (both dirty, present locally, absent remotely). */
14
+ locallyAddedIds: Set<string>;
15
+ locallyDeletedIds: Set<string>;
16
+ remoteSections: LoadedSection[];
17
+ remoteIndex: SiteIndex;
18
+ remoteChangedIds: Set<string>;
19
+ }
20
+ export interface RebaseResult {
21
+ sections: LoadedSection[];
22
+ index: SiteIndex;
23
+ conflictIds: string[];
24
+ }
25
+ /** Merge a fresh remote (saved-branch) snapshot under local unsaved work.
26
+ * Remote wins everywhere the local editor has no unsaved edits; dirty local
27
+ * sections are kept verbatim; overlaps are reported as conflicts for the
28
+ * caller's keep-mine/take-theirs dialog. Implements the 5 merge rules from
29
+ * the collaboration spec ("conflict-free by construction under lock
30
+ * discipline").
31
+ *
32
+ * Invariants: inputs are never mutated (the returned index is a fresh
33
+ * structure in both branches), and the result partitions: every section in
34
+ * `sections` is reachable from `index`; a dirty section deleted remotely is
35
+ * kept, re-linked into the index, and reported in `conflictIds`. */
36
+ export declare function rebaseContent(args: RebaseArgs): RebaseResult;
37
+ //# sourceMappingURL=rebase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rebase.d.ts","sourceRoot":"","sources":["../../src/collab/rebase.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAExD,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,aAAa,EAAE,CAAC;IAC/B,UAAU,EAAE,SAAS,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B;;;;;+EAK2E;IAC3E,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B,iBAAiB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC/B,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,WAAW,EAAE,SAAS,CAAC;IAKvB,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,EAAE,SAAS,CAAC;IAIjB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;;;;;;;;;qEAUqE;AACrE,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,YAAY,CA2F5D"}
@@ -0,0 +1,64 @@
1
+ import "../chunk-JSBRDJBE.js";
2
+
3
+ // src/collab/rebase.ts
4
+ function rebaseContent(args) {
5
+ const localById = new Map(args.localSections.map((s) => [s.section.id, s]));
6
+ const remoteIds = new Set(args.remoteSections.map((s) => s.section.id));
7
+ const merged = args.remoteSections.filter((s) => !args.locallyDeletedIds.has(s.section.id)).map((s) => args.dirtySectionIds.has(s.section.id) ? localById.get(s.section.id) ?? s : s);
8
+ for (const id of args.dirtySectionIds) {
9
+ if (remoteIds.has(id) || args.locallyDeletedIds.has(id)) continue;
10
+ const local = localById.get(id);
11
+ if (local) merged.push(local);
12
+ }
13
+ const conflictSet = /* @__PURE__ */ new Set();
14
+ for (const id of args.dirtySectionIds) {
15
+ if (args.locallyDeletedIds.has(id)) continue;
16
+ if (args.remoteChangedIds.has(id)) {
17
+ conflictSet.add(id);
18
+ } else if (localById.has(id) && !(id in args.remoteIndex.sections) && !args.locallyAddedIds.has(id)) {
19
+ conflictSet.add(id);
20
+ }
21
+ }
22
+ const conflictIds = [...conflictSet].sort();
23
+ let index;
24
+ if (!args.localIndexDirty) {
25
+ index = {
26
+ ...args.remoteIndex,
27
+ pages: args.remoteIndex.pages.map((p) => ({ ...p, order: [...p.order] })),
28
+ sections: { ...args.remoteIndex.sections }
29
+ };
30
+ } else {
31
+ const localOrderIds = new Set(args.localIndex.pages.flatMap((p) => p.order));
32
+ const pages = args.localIndex.pages.map((p) => ({
33
+ ...p,
34
+ order: p.order.filter((id) => id in args.remoteIndex.sections || args.dirtySectionIds.has(id))
35
+ }));
36
+ const sections = {};
37
+ for (const [id, meta] of Object.entries(args.localIndex.sections)) {
38
+ if (id in args.remoteIndex.sections || args.dirtySectionIds.has(id)) sections[id] = meta;
39
+ }
40
+ for (const remotePage of args.remoteIndex.pages) {
41
+ for (const id of remotePage.order) {
42
+ if (localOrderIds.has(id) || args.locallyDeletedIds.has(id)) continue;
43
+ const target = pages.find((p) => p.id === remotePage.id) ?? pages[0];
44
+ if (target && !target.order.includes(id)) target.order.push(id);
45
+ sections[id] = args.remoteIndex.sections[id];
46
+ }
47
+ }
48
+ index = { ...args.localIndex, pages, sections };
49
+ }
50
+ for (const s of merged) {
51
+ const id = s.section.id;
52
+ if (id in index.sections) continue;
53
+ const meta = args.localIndex.sections[id] ?? s.meta;
54
+ const localPage = args.localIndex.pages.find((p) => p.order.includes(id));
55
+ const target = (localPage && index.pages.find((p) => p.id === localPage.id)) ?? index.pages[0];
56
+ if (!target) continue;
57
+ if (!target.order.includes(id)) target.order.push(id);
58
+ index.sections[id] = meta;
59
+ }
60
+ return { sections: merged, index, conflictIds };
61
+ }
62
+ export {
63
+ rebaseContent
64
+ };
@@ -0,0 +1,89 @@
1
+ import type { LoadedSection } from "../lib/loader";
2
+ import type { Section, SectionContent } from "../schemas/sections";
3
+ import type { SiteIndex } from "../schemas/site-config";
4
+ /** The last CLEAN saved-branch snapshot (no local dirty overlays folded in).
5
+ * Lifecycle: primed by loadContent, re-primed with the committed snapshot on
6
+ * every successful save, and re-primed with the fetched remote snapshot on
7
+ * every recovery — so it is always the editor's last agreed-with-server state. */
8
+ export interface RecoveryBaseline {
9
+ sections: LoadedSection[];
10
+ index: SiteIndex;
11
+ }
12
+ /** Derive rebaseContent's remoteChangedIds + locallyAddedIds from a clean
13
+ * baseline and the freshly fetched remote snapshot.
14
+ *
15
+ * - remoteChangedIds: sections whose content differs between the clean baseline
16
+ * and the fresh remote. The baseline MUST be clean — diffing against the dirty
17
+ * in-memory view would flag every locally-edited section as remote-changed,
18
+ * turning every dirty section into a false conflict.
19
+ * - locallyAddedIds: dirty ids absent from the clean baseline INDEX — sections
20
+ * created locally and never saved. This is what lets rebase distinguish a
21
+ * local add from a section deleted remotely while dirty.
22
+ *
23
+ * Degraded mode (baseline === null — should be unreachable, since every load,
24
+ * save, and recovery re-primes the cache): fail SAFE, not silent. Every dirty
25
+ * section that still exists remotely is flagged remote-changed (forcing the
26
+ * keep-mine/take-theirs dialog instead of a silent local-wins merge), and every
27
+ * dirty section absent remotely is treated as locally added (kept — a true
28
+ * remote-deletion-while-dirty resurrects instead of conflicting, which loses
29
+ * nothing). This is deliberately NOT derived from the published mainIndex: a
30
+ * main-based baseline would silently local-win every dirty section that already
31
+ * exists on main — the exact lost-update this path exists to prevent. */
32
+ export declare function deriveRecoveryInputs({ baseline, remote, dirtyRows, deletedIds }: {
33
+ baseline: RecoveryBaseline | null;
34
+ remote: {
35
+ sections: LoadedSection[];
36
+ index: SiteIndex;
37
+ };
38
+ dirtyRows: {
39
+ sectionId: string;
40
+ }[];
41
+ deletedIds: Set<string>;
42
+ }): {
43
+ remoteChangedIds: Set<string>;
44
+ locallyAddedIds: Set<string>;
45
+ };
46
+ /** Apply the user's keep-mine/take-theirs choices to a rebase result.
47
+ * Pure — inputs are never mutated. For every conflict the LOCAL copy is
48
+ * returned in `preserved` (the caller snapshots it via preserveEdit so no
49
+ * choice is destructive). "theirs" swaps in the remote section — or, when the
50
+ * remote side DELETED it, removes the section AND strips its id from the index
51
+ * (pages' order + sections map); leaving the dangling index ref would make the
52
+ * server's dangling-ref gate reject every later save. `droppedDirtyIds` lists
53
+ * the "theirs" ids whose local dirty rows must be deleted so the retry doesn't
54
+ * re-send the overridden local content. */
55
+ export declare function applyConflictChoices({ merged, remoteSections, choices }: {
56
+ merged: {
57
+ sections: LoadedSection[];
58
+ index: SiteIndex;
59
+ };
60
+ remoteSections: LoadedSection[];
61
+ choices: Record<string, "mine" | "theirs">;
62
+ }): {
63
+ sections: LoadedSection[];
64
+ index: SiteIndex;
65
+ droppedDirtyIds: string[];
66
+ preserved: {
67
+ sectionId: string;
68
+ section: Section;
69
+ }[];
70
+ };
71
+ /** Build the COMMITTED post-save snapshot for re-priming the clean baseline.
72
+ * The committed content is exactly what performSave sent (savedRows), NOT the
73
+ * current view state — edits typed while the request was in flight must not
74
+ * leak into the "clean" baseline (they are still dirty rows and would otherwise
75
+ * read as remote changes on the next recovery). Base = the previous clean
76
+ * baseline when available (post-recovery it is the fetched remote snapshot);
77
+ * fallback = the loaded sections captured at save start. Ids absent from the
78
+ * sent index (deleted this save) are dropped; saved ids absent from the base
79
+ * (locally added) are appended with their meta from the sent index. */
80
+ export declare function buildCommittedBaseline({ baselineSections, fallbackSections, savedRows, sentIndex }: {
81
+ baselineSections: LoadedSection[] | null;
82
+ fallbackSections: LoadedSection[];
83
+ savedRows: {
84
+ sectionId: string;
85
+ content: SectionContent;
86
+ }[];
87
+ sentIndex: SiteIndex;
88
+ }): LoadedSection[];
89
+ //# sourceMappingURL=recovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../../src/collab/recovery.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAExD;;;mFAGmF;AACnF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,EAAE,SAAS,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;0EAmB0E;AAC1E,wBAAgB,oBAAoB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE;IAChF,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAClC,MAAM,EAAE;QAAE,QAAQ,EAAE,aAAa,EAAE,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAC;IACxD,SAAS,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACnC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACzB,GAAG;IAAE,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAAC,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CA0BlE;AAED;;;;;;;;4CAQ4C;AAC5C,wBAAgB,oBAAoB,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE;IACxE,MAAM,EAAE;QAAE,QAAQ,EAAE,aAAa,EAAE,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAC;IACxD,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC;CAC5C,GAAG;IACF,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,EAAE,SAAS,CAAC;IACjB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,SAAS,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;CACtD,CA4BA;AAED;;;;;;;;wEAQwE;AACxE,wBAAgB,sBAAsB,CAAC,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE;IACnG,gBAAgB,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC;IACzC,gBAAgB,EAAE,aAAa,EAAE,CAAC;IAClC,SAAS,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,cAAc,CAAA;KAAE,EAAE,CAAC;IAC5D,SAAS,EAAE,SAAS,CAAC;CACtB,GAAG,aAAa,EAAE,CAiBlB"}
@@ -0,0 +1,93 @@
1
+ import { z } from "zod";
2
+ export declare const HEARTBEAT_INTERVAL_MS = 15000;
3
+ export declare const LOCK_STALE_MS = 45000;
4
+ export declare const CollabPeerSchema: z.ZodObject<{
5
+ userId: z.ZodString;
6
+ email: z.ZodString;
7
+ focusSectionId: z.ZodNullable<z.ZodString>;
8
+ heldSectionIds: z.ZodArray<z.ZodString>;
9
+ }, z.core.$strip>;
10
+ export type CollabPeer = z.infer<typeof CollabPeerSchema>;
11
+ export declare const SectionUpdateMessageSchema: z.ZodObject<{
12
+ v: z.ZodLiteral<1>;
13
+ sectionId: z.ZodString;
14
+ content: z.ZodUnknown;
15
+ userId: z.ZodString;
16
+ }, z.core.$strip>;
17
+ export type SectionUpdateMessage = z.infer<typeof SectionUpdateMessageSchema>;
18
+ export declare const IndexUpdateMessageSchema: z.ZodObject<{
19
+ v: z.ZodLiteral<1>;
20
+ index: z.ZodUnknown;
21
+ userId: z.ZodString;
22
+ }, z.core.$strip>;
23
+ export type IndexUpdateMessage = z.infer<typeof IndexUpdateMessageSchema>;
24
+ export declare const TakeoverMessageSchema: z.ZodObject<{
25
+ v: z.ZodLiteral<1>;
26
+ sectionId: z.ZodString;
27
+ byUserId: z.ZodString;
28
+ byEmail: z.ZodString;
29
+ }, z.core.$strip>;
30
+ export type TakeoverMessage = z.infer<typeof TakeoverMessageSchema>;
31
+ export declare const SavedMessageSchema: z.ZodObject<{
32
+ v: z.ZodLiteral<1>;
33
+ newSha: z.ZodString;
34
+ changedSectionIds: z.ZodArray<z.ZodString>;
35
+ deletedSectionIds: z.ZodArray<z.ZodString>;
36
+ source: z.ZodEnum<{
37
+ editor: "editor";
38
+ api: "api";
39
+ }>;
40
+ }, z.core.$strip>;
41
+ export type SavedMessage = z.infer<typeof SavedMessageSchema>;
42
+ export type CollabStatus = "connecting" | "connected" | "offline";
43
+ /** Opaque server-minted connection blob. Provider-specific keys ride along;
44
+ * primitives only reads the three identity fields. */
45
+ export interface CollabBootstrap {
46
+ siteId: string;
47
+ userId: string;
48
+ email: string;
49
+ [key: string]: unknown;
50
+ }
51
+ export interface CollabEvents {
52
+ onPeers(peers: CollabPeer[]): void;
53
+ onSectionUpdate(msg: SectionUpdateMessage): void;
54
+ onIndexUpdate(msg: IndexUpdateMessage): void;
55
+ onTakeover(msg: TakeoverMessage): void;
56
+ onSaved(msg: SavedMessage): void;
57
+ onStatus(status: CollabStatus): void;
58
+ }
59
+ /** A live connection to one site channel. The session owns the held-lock set
60
+ * (heartbeats them every HEARTBEAT_INTERVAL_MS and includes them in its
61
+ * presence payload). acquireLock({steal:true}) also emits the takeover
62
+ * broadcast on success. */
63
+ export interface CollabSession {
64
+ setFocus(sectionId: string | null): void;
65
+ acquireLock(sectionId: string, opts?: {
66
+ steal?: boolean;
67
+ }): Promise<boolean>;
68
+ releaseLocks(sectionIds: string[]): Promise<void>;
69
+ broadcastSectionUpdate(sectionId: string, content: unknown): void;
70
+ broadcastIndexUpdate(index: unknown): void;
71
+ disconnect(): Promise<void>;
72
+ }
73
+ export interface SavedBroadcast {
74
+ newSha: string;
75
+ changedSectionIds: string[];
76
+ deletedSectionIds: string[];
77
+ source: "editor" | "api";
78
+ }
79
+ export interface CollabProvider {
80
+ /** Browser: join the site channel. Resolves null when collaboration is
81
+ * unavailable (no session, join refused) — caller runs solo-mode. */
82
+ connect(bootstrap: CollabBootstrap, events: CollabEvents): Promise<CollabSession | null>;
83
+ /** Server: mint the browser bootstrap for an authenticated editor session,
84
+ * or null when the provider can't support collab (e.g. missing env). */
85
+ getBootstrap(args: {
86
+ siteId: string;
87
+ userId: string;
88
+ email: string;
89
+ }): CollabBootstrap | null;
90
+ /** Server: post-commit fan-out of a successful save to all connected editors. */
91
+ broadcastSaved(siteId: string, event: SavedBroadcast): Promise<void>;
92
+ }
93
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/collab/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import {
2
+ CollabPeerSchema,
3
+ HEARTBEAT_INTERVAL_MS,
4
+ IndexUpdateMessageSchema,
5
+ LOCK_STALE_MS,
6
+ SavedMessageSchema,
7
+ SectionUpdateMessageSchema,
8
+ TakeoverMessageSchema
9
+ } from "../chunk-REM2NH7K.js";
10
+ import "../chunk-JSBRDJBE.js";
11
+ export {
12
+ CollabPeerSchema,
13
+ HEARTBEAT_INTERVAL_MS,
14
+ IndexUpdateMessageSchema,
15
+ LOCK_STALE_MS,
16
+ SavedMessageSchema,
17
+ SectionUpdateMessageSchema,
18
+ TakeoverMessageSchema
19
+ };