@panphora/clayjs 1.5.3 → 1.6.1

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 (46) hide show
  1. package/README.md +5 -3
  2. package/THIRD-PARTY-NOTICES.md +6 -0
  3. package/dist/clay.standalone.js +15041 -10657
  4. package/package.json +9 -4
  5. package/src/core/conflict-download.js +91 -0
  6. package/src/core/conflict-notice.js +720 -0
  7. package/src/core/conflict-presentation.js +301 -0
  8. package/src/core/save-conflict-notice.js +2 -192
  9. package/src/core/save-core.js +35 -2
  10. package/src/core/save-feedback.js +6 -0
  11. package/src/core/save.js +157 -3
  12. package/src/core/snapshot.js +21 -0
  13. package/src/core/stale-host-notice.js +21 -24
  14. package/src/core/unsaved-warning.js +64 -16
  15. package/src/lib/content-dom.js +4 -2
  16. package/src/lib/dirty-gate.js +37 -1
  17. package/src/lib/unsaved-state.js +55 -0
  18. package/src/loader-logic.js +26 -15
  19. package/src/loader.js +10 -3
  20. package/src/plugins/ai-edit.js +489 -179
  21. package/src/plugins/cms.js +12 -0
  22. package/src/plugins/indicator.js +35 -16
  23. package/src/plugins/quickcrop.js +103 -0
  24. package/src/plugins/richclay.js +21 -0
  25. package/src/plugins/source.js +13 -6
  26. package/src/plugins/wire.js +4 -0
  27. package/src/sync/conflict-revert.js +1138 -0
  28. package/src/sync/conflicts.js +159 -0
  29. package/src/sync/live-sync.js +130 -109
  30. package/src/sync/presence.js +42 -36
  31. package/src/sync/section-notice.js +53 -28
  32. package/src/ui/bevel-controls.js +277 -0
  33. package/src/ui/bevel-dialog.js +112 -0
  34. package/src/ui/bevel.js +247 -0
  35. package/src/ui/dialogs.js +63 -86
  36. package/src/ui/index.js +22 -11
  37. package/src/ui/modal.js +80 -387
  38. package/src/ui/skins/cms-toggle.js +15 -0
  39. package/src/ui/skins/cms.js +4 -0
  40. package/src/ui/skins/quickcrop.js +4 -0
  41. package/src/ui/skins/richclay.js +4 -0
  42. package/src/ui/toast.js +116 -401
  43. package/src/ui/vendor-skin.js +65 -0
  44. package/src/vendor/hyper-morph.vendor.js +4 -4
  45. package/src/vendor/quickcrop.vendor.js +14 -8
  46. package/src/vendor/richclay.vendor.js +22 -41
@@ -0,0 +1,159 @@
1
+ import { registerUnsavedState } from '../lib/unsaved-state.js';
2
+
3
+ // What incoming frames won over this tab's unsaved edits, kept for the life of
4
+ // the document until the person decides. Nothing here is read by a save, a
5
+ // baseline, a reconnect or a 412 release: a record leaves only through
6
+ // acknowledge(). Each apply keeps the clone the merge read as "mine", which is
7
+ // the recovery copy for Download and the source for Revert.
8
+
9
+ const REASONS = new Set(['accepted', 'reverted', 'reconciled']);
10
+ const nonce = Math.random().toString(36).slice(2, 8);
11
+ let counter = 0;
12
+ const records = new Map();
13
+ const applies = new Map();
14
+ const pending = new Set();
15
+ let registration = null;
16
+
17
+ function nextId(prefix = '') {
18
+ return `${nonce}:${prefix}${++counter}`;
19
+ }
20
+
21
+ function register() {
22
+ registration ||= registerUnsavedState({
23
+ id: 'live-sync-conflicts',
24
+ isPending: () => records.size > 0 || pending.size > 0,
25
+ });
26
+ return registration;
27
+ }
28
+
29
+ function changed(detail) {
30
+ register().changed();
31
+ document.dispatchEvent(new CustomEvent('clay:sync-conflicts-changed', {
32
+ detail: { open: [...records.keys()], added: [], removedIds: [], updatedIds: [], ...detail },
33
+ }));
34
+ }
35
+
36
+ // A report is a loss unless the engine says this tab's operation survived, or a
37
+ // hook kept the incoming change off the page. One the engine could not map to the
38
+ // live page ("missing-output") still replaced this tab's work: it shows, without
39
+ // a place to point at.
40
+ function isLoss(c) {
41
+ const r = c.recovery;
42
+ if (!r) return true;
43
+ if (r.applied === false && r.unavailable === 'hook-veto') return false;
44
+ return r.localLost !== false;
45
+ }
46
+
47
+ function install(apply, raw) {
48
+ const id = nextId();
49
+ const rec = Object.assign(raw[0], {
50
+ id, applyId: apply.id, source: apply.source, ticket: apply.ticket,
51
+ rawReports: raw, claimedBy: null,
52
+ });
53
+ records.set(id, rec);
54
+ apply.ids.add(id);
55
+ return id;
56
+ }
57
+
58
+ export function beginApply({ source, seq = null, etag = null, domain, root }) {
59
+ const id = nextId('a');
60
+ applies.set(id, { id, source, seq, etag, ticket: 0, domain, root, ids: new Set() });
61
+ pending.add(id);
62
+ register().changed();
63
+ return id;
64
+ }
65
+
66
+ export function completeApply(applyId, conflicts, { ticket }) {
67
+ const apply = applies.get(applyId);
68
+ if (!apply) return [];
69
+ apply.ticket = ticket;
70
+ const groups = new Map();
71
+ for (const c of conflicts) {
72
+ if (!isLoss(c)) continue;
73
+ const key = c.recovery?.key ?? Symbol();
74
+ if (!groups.has(key)) groups.set(key, []);
75
+ groups.get(key).push(c);
76
+ }
77
+ const ids = [...groups.values()].map((raw) => install(apply, raw));
78
+ pending.delete(applyId);
79
+ if (!ids.length) applies.delete(applyId);
80
+ if (ids.length) changed({ added: ids.map((id) => records.get(id)) });
81
+ else register().changed();
82
+ return ids;
83
+ }
84
+
85
+ export function failApply(applyId, error, { ticket = 0 } = {}) {
86
+ const apply = applies.get(applyId);
87
+ if (!apply) return [];
88
+ apply.ticket = ticket;
89
+ pending.delete(applyId);
90
+ const id = install(apply, [{ kind: 'apply-incomplete', detail: null, error: String(error) }]);
91
+ records.get(id).rawReports = [];
92
+ changed({ added: [records.get(id)] });
93
+ return [id];
94
+ }
95
+
96
+ export const conflicts = {
97
+ get size() {
98
+ return records.size;
99
+ },
100
+ list() {
101
+ return [...records.values()];
102
+ },
103
+ get(id) {
104
+ return records.get(id) || null;
105
+ },
106
+ acknowledge(ids, { reason } = {}) {
107
+ if (!Array.isArray(ids)) throw new TypeError('clay.conflicts.acknowledge needs an array of ids');
108
+ if (!REASONS.has(reason)) throw new TypeError(`clay.conflicts.acknowledge: unknown reason "${reason}"`);
109
+ const removedIds = [];
110
+ for (const id of ids) {
111
+ const rec = records.get(id);
112
+ if (!rec) continue;
113
+ records.delete(id);
114
+ removedIds.push(id);
115
+ const apply = applies.get(rec.applyId);
116
+ if (apply) {
117
+ apply.ids.delete(id);
118
+ if (!apply.ids.size) applies.delete(rec.applyId);
119
+ }
120
+ }
121
+ if (removedIds.length) changed({ removedIds, reason });
122
+ return { removedIds, remainingIds: [...records.keys()] };
123
+ },
124
+ claim(ids, { owner }) {
125
+ if (!owner) throw new TypeError('clay.conflicts.claim needs an owner');
126
+ for (const id of ids) {
127
+ const rec = records.get(id);
128
+ if (rec && rec.claimedBy && rec.claimedBy !== owner) {
129
+ throw new Error(`conflict ${id} is already claimed by ${rec.claimedBy}`);
130
+ }
131
+ }
132
+ const acquired = [];
133
+ for (const id of ids) {
134
+ const rec = records.get(id);
135
+ if (rec && !rec.claimedBy) {
136
+ rec.claimedBy = owner;
137
+ acquired.push(id);
138
+ }
139
+ }
140
+ if (acquired.length) changed({ updatedIds: acquired, reason: 'claimed' });
141
+ let released = false;
142
+ return {
143
+ release() {
144
+ if (released) return;
145
+ released = true;
146
+ const freed = acquired.filter((id) => records.get(id)?.claimedBy === owner);
147
+ for (const id of freed) records.get(id).claimedBy = null;
148
+ if (freed.length) changed({ updatedIds: freed, reason: 'released' });
149
+ },
150
+ };
151
+ },
152
+ recoveryOf(id) {
153
+ const rec = records.get(id);
154
+ return rec ? applies.get(rec.applyId) || null : null;
155
+ },
156
+ hasPendingApply() {
157
+ return pending.size > 0;
158
+ },
159
+ };