logisheets-core 1.1.0

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 (63) hide show
  1. package/dist/craft-interactions/index.d.ts +140 -0
  2. package/dist/craft-interactions/index.js +462 -0
  3. package/dist/field/index.d.ts +51 -0
  4. package/dist/field/index.js +70 -0
  5. package/dist/format/index.d.ts +21 -0
  6. package/dist/format/index.js +454 -0
  7. package/dist/index.d.ts +12 -0
  8. package/dist/index.js +17 -0
  9. package/dist/ops/index.d.ts +155 -0
  10. package/dist/ops/index.js +335 -0
  11. package/dist/permissions/index.d.ts +32 -0
  12. package/dist/permissions/index.js +74 -0
  13. package/dist/port.d.ts +9 -0
  14. package/dist/port.js +14 -0
  15. package/dist/selection/index.d.ts +1 -0
  16. package/dist/selection/index.js +1 -0
  17. package/dist/selection/model.d.ts +10 -0
  18. package/dist/selection/model.js +38 -0
  19. package/dist/strings/case.d.ts +2 -0
  20. package/dist/strings/case.js +6 -0
  21. package/dist/strings/char-code.d.ts +422 -0
  22. package/dist/strings/char-code.js +424 -0
  23. package/dist/strings/common_length.d.ts +4 -0
  24. package/dist/strings/common_length.js +28 -0
  25. package/dist/strings/contain.d.ts +4 -0
  26. package/dist/strings/contain.js +60 -0
  27. package/dist/strings/index.d.ts +5 -0
  28. package/dist/strings/index.js +5 -0
  29. package/dist/strings/judges.d.ts +3 -0
  30. package/dist/strings/judges.js +38 -0
  31. package/dist/strings/surrogate.d.ts +16 -0
  32. package/dist/strings/surrogate.js +30 -0
  33. package/dist/structured/index.d.ts +41 -0
  34. package/dist/structured/index.js +50 -0
  35. package/dist/transaction/index.d.ts +8 -0
  36. package/dist/transaction/index.js +11 -0
  37. package/dist/type-guard/index.d.ts +3 -0
  38. package/dist/type-guard/index.js +3 -0
  39. package/dist/type-guard/propterty.d.ts +1 -0
  40. package/dist/type-guard/propterty.js +3 -0
  41. package/dist/type-guard/string.d.ts +1 -0
  42. package/dist/type-guard/string.js +3 -0
  43. package/dist/type-guard/u8.d.ts +1 -0
  44. package/dist/type-guard/u8.js +3 -0
  45. package/dist/utils/a1notation.d.ts +18 -0
  46. package/dist/utils/a1notation.js +76 -0
  47. package/dist/utils/array.d.ts +2 -0
  48. package/dist/utils/array.js +12 -0
  49. package/dist/utils/clone.d.ts +2 -0
  50. package/dist/utils/clone.js +25 -0
  51. package/dist/utils/const.d.ts +7 -0
  52. package/dist/utils/const.js +8 -0
  53. package/dist/utils/equal.d.ts +1 -0
  54. package/dist/utils/equal.js +11 -0
  55. package/dist/utils/index.d.ts +6 -0
  56. package/dist/utils/index.js +6 -0
  57. package/dist/utils/uuid.d.ts +1 -0
  58. package/dist/utils/uuid.js +4 -0
  59. package/dist/validation/index.d.ts +34 -0
  60. package/dist/validation/index.js +60 -0
  61. package/dist/value/index.d.ts +9 -0
  62. package/dist/value/index.js +49 -0
  63. package/package.json +54 -0
@@ -0,0 +1,140 @@
1
+ export interface RadioBinding {
2
+ type: 'radio';
3
+ groupId: string;
4
+ sheetIdx: number;
5
+ blockId: number;
6
+ row: number;
7
+ col: number;
8
+ value: string;
9
+ }
10
+ export declare function subscribeRadioBindings(listener: () => void): () => void;
11
+ export declare function registerRadioBinding(binding: RadioBinding): void;
12
+ export declare function unregisterRadioBinding(binding: Pick<RadioBinding, 'groupId' | 'blockId' | 'row' | 'col'>): void;
13
+ export declare function clearRadioBindings(groupId?: string): void;
14
+ export declare function getRadioBindings(): readonly RadioBinding[];
15
+ export declare function getRadioSelection(groupId: string): string | undefined;
16
+ export declare function setRadioSelection(groupId: string, value: string): void;
17
+ export interface MultiSelectBinding {
18
+ type: 'multiSelect';
19
+ groupId: string;
20
+ sheetIdx: number;
21
+ blockId: number;
22
+ row: number;
23
+ col: number;
24
+ value: string;
25
+ }
26
+ export declare function registerMultiSelectBinding(binding: MultiSelectBinding): void;
27
+ export declare function unregisterMultiSelectBinding(binding: Pick<MultiSelectBinding, 'groupId' | 'blockId' | 'row' | 'col'>): void;
28
+ export declare function clearMultiSelectBindings(groupId?: string): void;
29
+ export declare function getMultiSelectBindings(): readonly MultiSelectBinding[];
30
+ export declare function setMultiSelectMax(groupId: string, max: number): void;
31
+ export declare function getMultiSelectMax(groupId: string): number;
32
+ export declare function getMultiSelectSelections(groupId: string): string[];
33
+ export declare function toggleMultiSelectValue(groupId: string, value: string): void;
34
+ export interface PointAllocatorBinding {
35
+ type: 'pointAllocator';
36
+ groupId: string;
37
+ sheetIdx: number;
38
+ blockId: number;
39
+ row: number;
40
+ col: number;
41
+ }
42
+ export interface PointAllocation {
43
+ blockId: number;
44
+ row: number;
45
+ col: number;
46
+ points: number;
47
+ }
48
+ export declare function registerPointAllocator(binding: PointAllocatorBinding): void;
49
+ export declare function unregisterPointAllocator(binding: Pick<PointAllocatorBinding, 'groupId' | 'blockId' | 'row' | 'col'>): void;
50
+ export declare function clearPointAllocators(groupId?: string): void;
51
+ export declare function getPointAllocatorBindings(): readonly PointAllocatorBinding[];
52
+ export declare function setPointPool(groupId: string, total: number): void;
53
+ export declare function getPointPool(groupId: string): {
54
+ total: number;
55
+ used: number;
56
+ remaining: number;
57
+ };
58
+ export declare function getPointAllocation(binding: Pick<PointAllocatorBinding, 'groupId' | 'blockId' | 'row' | 'col'>): number;
59
+ export declare function getPointAllocations(groupId: string): PointAllocation[];
60
+ export interface NumberSliderBinding {
61
+ type: 'numberSlider';
62
+ groupId: string;
63
+ sheetIdx: number;
64
+ blockId: number;
65
+ row: number;
66
+ col: number;
67
+ min: number;
68
+ max: number;
69
+ step?: number;
70
+ initialValue?: number;
71
+ }
72
+ export declare function registerNumberSlider(binding: NumberSliderBinding): void;
73
+ export declare function unregisterNumberSlider(binding: Pick<NumberSliderBinding, 'groupId' | 'blockId' | 'row' | 'col'>): void;
74
+ export declare function clearNumberSliders(groupId?: string): void;
75
+ export declare function getNumberSliderBindings(): readonly NumberSliderBinding[];
76
+ export declare function adjustPointAllocation(binding: Pick<PointAllocatorBinding, 'groupId' | 'blockId' | 'row' | 'col'>, delta: number): void;
77
+ export interface PercentAllocatorBinding {
78
+ type: 'percentAllocator';
79
+ groupId: string;
80
+ sheetIdx: number;
81
+ blockId: number;
82
+ row: number;
83
+ col: number;
84
+ }
85
+ export declare function registerPercentAllocator(binding: PercentAllocatorBinding): void;
86
+ export declare function unregisterPercentAllocator(binding: Pick<PercentAllocatorBinding, 'groupId' | 'blockId' | 'row' | 'col'>): void;
87
+ export declare function clearPercentAllocators(groupId?: string): void;
88
+ export declare function getPercentAllocatorBindings(): readonly PercentAllocatorBinding[];
89
+ /**
90
+ * Pure helper: apply `deltaPct` percentage points to the `targetIdx`
91
+ * slot of a group and rebalance the others so the group sum is always
92
+ * exactly 1 (100%) after the call.
93
+ *
94
+ * Semantics:
95
+ * - **First-touch (group sum ≈ 0)**: auto-seed an even 1/n split as
96
+ * the conceptual starting point, *then* apply the delta. So
97
+ * `[0,0] +1 on idx 0` reads as "starting from 50/50, give idx 0
98
+ * +1%" → `[0.51, 0.49]`. This is what we want when a craft has
99
+ * bound a pair but hasn't written initial values yet — the first
100
+ * click both initializes and adjusts in one step.
101
+ * - **Any positive delta** (`+k`): bump target by k% (clamped to
102
+ * [0,1]), take the same total off the other slots proportionally
103
+ * to their pre-click share.
104
+ * - **Any negative delta** (`-k`): lower target by k% (clamped),
105
+ * give the same total back to the other slots proportionally.
106
+ * - **Already-at-bound clicks** (e.g. +1 on a slot at 100%): no-op.
107
+ * - **Manually under- or over-allocated input** (sum != 1): every
108
+ * click renormalizes the group back to 1.
109
+ *
110
+ * Examples (n=2):
111
+ *
112
+ * [0, 0] +1 on idx 0 → [0.51, 0.49] (auto-seed + +1)
113
+ * [0.5, 0.5] +1 on idx 0 → [0.51, 0.49]
114
+ * [0.5, 0.5] -1 on idx 0 → [0.49, 0.51]
115
+ * [1.0, 0] +1 on idx 0 → [1.0, 0] (clamped)
116
+ * [1.0, 0] +1 on idx 1 → [0.99, 0.01]
117
+ *
118
+ * Earlier versions had a sticky-after-first-click bug: when the group
119
+ * was [0.01, 0], adding to idx 0 saw "others total = 0" and either
120
+ * reverted the bump (preserve-sum branch) or just let it climb
121
+ * (overflow-only branch) — neither was what the user wanted. Auto-
122
+ * seeding at sum≈0 makes every subsequent click see a well-formed
123
+ * group, so the renormalization branch always has weight to work with.
124
+ */
125
+ export declare function redistributePercent(current: readonly number[], targetIdx: number, deltaPct: number): number[];
126
+ interface PersistedShape {
127
+ radioBindings: RadioBinding[];
128
+ radioSelections: Record<string, string>;
129
+ multiSelectBindings: MultiSelectBinding[];
130
+ multiSelectMax: Record<string, number>;
131
+ multiSelectSelections: Record<string, string[]>;
132
+ pointBindings: PointAllocatorBinding[];
133
+ pointPoolTotals: Record<string, number>;
134
+ pointAllocations: Record<string, PointAllocation[]>;
135
+ numberSliderBindings: NumberSliderBinding[];
136
+ percentBindings: PercentAllocatorBinding[];
137
+ }
138
+ export declare function getPersistentInteractions(): PersistedShape;
139
+ export declare function loadPersistentInteractions(data: unknown): void;
140
+ export {};
@@ -0,0 +1,462 @@
1
+ const radioBindings = new Map();
2
+ const groupSelections = new Map();
3
+ const storeListeners = new Set();
4
+ function bindingKey(b) {
5
+ return `${b.groupId}-${b.blockId}-${b.row}-${b.col}`;
6
+ }
7
+ function notifyStore() {
8
+ for (const l of storeListeners)
9
+ l();
10
+ }
11
+ export function subscribeRadioBindings(listener) {
12
+ storeListeners.add(listener);
13
+ return () => {
14
+ storeListeners.delete(listener);
15
+ };
16
+ }
17
+ export function registerRadioBinding(binding) {
18
+ radioBindings.set(bindingKey(binding), binding);
19
+ notifyStore();
20
+ }
21
+ export function unregisterRadioBinding(binding) {
22
+ radioBindings.delete(bindingKey(binding));
23
+ notifyStore();
24
+ }
25
+ export function clearRadioBindings(groupId) {
26
+ if (groupId === undefined) {
27
+ radioBindings.clear();
28
+ groupSelections.clear();
29
+ }
30
+ else {
31
+ for (const [key, b] of radioBindings) {
32
+ if (b.groupId === groupId)
33
+ radioBindings.delete(key);
34
+ }
35
+ groupSelections.delete(groupId);
36
+ }
37
+ notifyStore();
38
+ }
39
+ export function getRadioBindings() {
40
+ return Array.from(radioBindings.values());
41
+ }
42
+ export function getRadioSelection(groupId) {
43
+ return groupSelections.get(groupId);
44
+ }
45
+ export function setRadioSelection(groupId, value) {
46
+ groupSelections.set(groupId, value);
47
+ notifyStore();
48
+ }
49
+ const multiSelectBindings = new Map();
50
+ const multiSelectMax = new Map();
51
+ const multiSelectSelections = new Map();
52
+ function multiSelectKey(b) {
53
+ return `${b.groupId}-${b.blockId}-${b.row}-${b.col}`;
54
+ }
55
+ export function registerMultiSelectBinding(binding) {
56
+ multiSelectBindings.set(multiSelectKey(binding), binding);
57
+ notifyStore();
58
+ }
59
+ export function unregisterMultiSelectBinding(binding) {
60
+ multiSelectBindings.delete(multiSelectKey(binding));
61
+ notifyStore();
62
+ }
63
+ export function clearMultiSelectBindings(groupId) {
64
+ if (groupId === undefined) {
65
+ multiSelectBindings.clear();
66
+ multiSelectMax.clear();
67
+ multiSelectSelections.clear();
68
+ }
69
+ else {
70
+ for (const [k, b] of multiSelectBindings) {
71
+ if (b.groupId === groupId)
72
+ multiSelectBindings.delete(k);
73
+ }
74
+ multiSelectMax.delete(groupId);
75
+ multiSelectSelections.delete(groupId);
76
+ }
77
+ notifyStore();
78
+ }
79
+ export function getMultiSelectBindings() {
80
+ return Array.from(multiSelectBindings.values());
81
+ }
82
+ export function setMultiSelectMax(groupId, max) {
83
+ multiSelectMax.set(groupId, Math.max(0, max));
84
+ notifyStore();
85
+ }
86
+ export function getMultiSelectMax(groupId) {
87
+ return multiSelectMax.get(groupId) ?? 0;
88
+ }
89
+ export function getMultiSelectSelections(groupId) {
90
+ return Array.from(multiSelectSelections.get(groupId) ?? []);
91
+ }
92
+ export function toggleMultiSelectValue(groupId, value) {
93
+ let set = multiSelectSelections.get(groupId);
94
+ if (!set) {
95
+ set = new Set();
96
+ multiSelectSelections.set(groupId, set);
97
+ }
98
+ if (set.has(value)) {
99
+ set.delete(value);
100
+ }
101
+ else {
102
+ const max = multiSelectMax.get(groupId) ?? Infinity;
103
+ if (set.size >= max)
104
+ return;
105
+ set.add(value);
106
+ }
107
+ notifyStore();
108
+ }
109
+ const pointBindings = new Map();
110
+ const pointPoolTotals = new Map();
111
+ const pointAllocations = new Map();
112
+ function pointBindingKey(b) {
113
+ return `${b.groupId}-${b.blockId}-${b.row}-${b.col}`;
114
+ }
115
+ function cellKey(blockId, row, col) {
116
+ return `${blockId}-${row}-${col}`;
117
+ }
118
+ function groupTotalUsed(groupId) {
119
+ const m = pointAllocations.get(groupId);
120
+ if (!m)
121
+ return 0;
122
+ let s = 0;
123
+ for (const v of m.values())
124
+ s += v;
125
+ return s;
126
+ }
127
+ export function registerPointAllocator(binding) {
128
+ pointBindings.set(pointBindingKey(binding), binding);
129
+ notifyStore();
130
+ }
131
+ export function unregisterPointAllocator(binding) {
132
+ pointBindings.delete(pointBindingKey(binding));
133
+ notifyStore();
134
+ }
135
+ export function clearPointAllocators(groupId) {
136
+ if (groupId === undefined) {
137
+ pointBindings.clear();
138
+ pointPoolTotals.clear();
139
+ pointAllocations.clear();
140
+ }
141
+ else {
142
+ for (const [key, b] of pointBindings) {
143
+ if (b.groupId === groupId)
144
+ pointBindings.delete(key);
145
+ }
146
+ pointPoolTotals.delete(groupId);
147
+ pointAllocations.delete(groupId);
148
+ }
149
+ notifyStore();
150
+ }
151
+ export function getPointAllocatorBindings() {
152
+ return Array.from(pointBindings.values());
153
+ }
154
+ export function setPointPool(groupId, total) {
155
+ pointPoolTotals.set(groupId, Math.max(0, total));
156
+ notifyStore();
157
+ }
158
+ export function getPointPool(groupId) {
159
+ const total = pointPoolTotals.get(groupId) ?? 0;
160
+ const used = groupTotalUsed(groupId);
161
+ return { total, used, remaining: total - used };
162
+ }
163
+ export function getPointAllocation(binding) {
164
+ return (pointAllocations
165
+ .get(binding.groupId)
166
+ ?.get(cellKey(binding.blockId, binding.row, binding.col)) ?? 0);
167
+ }
168
+ export function getPointAllocations(groupId) {
169
+ const m = pointAllocations.get(groupId);
170
+ if (!m)
171
+ return [];
172
+ const out = [];
173
+ for (const [key, points] of m) {
174
+ const [blockId, row, col] = key.split('-').map(Number);
175
+ out.push({ blockId, row, col, points });
176
+ }
177
+ return out;
178
+ }
179
+ const numberSliderBindings = new Map();
180
+ function numberSliderKey(b) {
181
+ return `${b.groupId}-${b.blockId}-${b.row}-${b.col}`;
182
+ }
183
+ export function registerNumberSlider(binding) {
184
+ numberSliderBindings.set(numberSliderKey(binding), binding);
185
+ notifyStore();
186
+ }
187
+ export function unregisterNumberSlider(binding) {
188
+ numberSliderBindings.delete(numberSliderKey(binding));
189
+ notifyStore();
190
+ }
191
+ export function clearNumberSliders(groupId) {
192
+ if (groupId === undefined) {
193
+ numberSliderBindings.clear();
194
+ }
195
+ else {
196
+ for (const [k, b] of numberSliderBindings) {
197
+ if (b.groupId === groupId)
198
+ numberSliderBindings.delete(k);
199
+ }
200
+ }
201
+ notifyStore();
202
+ }
203
+ export function getNumberSliderBindings() {
204
+ return Array.from(numberSliderBindings.values());
205
+ }
206
+ export function adjustPointAllocation(binding, delta) {
207
+ const { groupId, blockId, row, col } = binding;
208
+ let m = pointAllocations.get(groupId);
209
+ if (!m) {
210
+ m = new Map();
211
+ pointAllocations.set(groupId, m);
212
+ }
213
+ const key = cellKey(blockId, row, col);
214
+ const current = m.get(key) ?? 0;
215
+ const total = pointPoolTotals.get(groupId) ?? 0;
216
+ const used = groupTotalUsed(groupId);
217
+ const remaining = total - used;
218
+ const applied = delta > 0 ? Math.min(delta, remaining) : Math.max(delta, -current);
219
+ const next = current + applied;
220
+ if (next <= 0)
221
+ m.delete(key);
222
+ else
223
+ m.set(key, next);
224
+ notifyStore();
225
+ }
226
+ const percentBindings = new Map();
227
+ function percentBindingKey(b) {
228
+ return `${b.groupId}-${b.blockId}-${b.row}-${b.col}`;
229
+ }
230
+ export function registerPercentAllocator(binding) {
231
+ percentBindings.set(percentBindingKey(binding), binding);
232
+ notifyStore();
233
+ }
234
+ export function unregisterPercentAllocator(binding) {
235
+ percentBindings.delete(percentBindingKey(binding));
236
+ notifyStore();
237
+ }
238
+ export function clearPercentAllocators(groupId) {
239
+ if (groupId === undefined) {
240
+ percentBindings.clear();
241
+ }
242
+ else {
243
+ for (const [k, b] of percentBindings) {
244
+ if (b.groupId === groupId)
245
+ percentBindings.delete(k);
246
+ }
247
+ }
248
+ notifyStore();
249
+ }
250
+ export function getPercentAllocatorBindings() {
251
+ return Array.from(percentBindings.values());
252
+ }
253
+ /**
254
+ * Pure helper: apply `deltaPct` percentage points to the `targetIdx`
255
+ * slot of a group and rebalance the others so the group sum is always
256
+ * exactly 1 (100%) after the call.
257
+ *
258
+ * Semantics:
259
+ * - **First-touch (group sum ≈ 0)**: auto-seed an even 1/n split as
260
+ * the conceptual starting point, *then* apply the delta. So
261
+ * `[0,0] +1 on idx 0` reads as "starting from 50/50, give idx 0
262
+ * +1%" → `[0.51, 0.49]`. This is what we want when a craft has
263
+ * bound a pair but hasn't written initial values yet — the first
264
+ * click both initializes and adjusts in one step.
265
+ * - **Any positive delta** (`+k`): bump target by k% (clamped to
266
+ * [0,1]), take the same total off the other slots proportionally
267
+ * to their pre-click share.
268
+ * - **Any negative delta** (`-k`): lower target by k% (clamped),
269
+ * give the same total back to the other slots proportionally.
270
+ * - **Already-at-bound clicks** (e.g. +1 on a slot at 100%): no-op.
271
+ * - **Manually under- or over-allocated input** (sum != 1): every
272
+ * click renormalizes the group back to 1.
273
+ *
274
+ * Examples (n=2):
275
+ *
276
+ * [0, 0] +1 on idx 0 → [0.51, 0.49] (auto-seed + +1)
277
+ * [0.5, 0.5] +1 on idx 0 → [0.51, 0.49]
278
+ * [0.5, 0.5] -1 on idx 0 → [0.49, 0.51]
279
+ * [1.0, 0] +1 on idx 0 → [1.0, 0] (clamped)
280
+ * [1.0, 0] +1 on idx 1 → [0.99, 0.01]
281
+ *
282
+ * Earlier versions had a sticky-after-first-click bug: when the group
283
+ * was [0.01, 0], adding to idx 0 saw "others total = 0" and either
284
+ * reverted the bump (preserve-sum branch) or just let it climb
285
+ * (overflow-only branch) — neither was what the user wanted. Auto-
286
+ * seeding at sum≈0 makes every subsequent click see a well-formed
287
+ * group, so the renormalization branch always has weight to work with.
288
+ */
289
+ export function redistributePercent(current, targetIdx, deltaPct) {
290
+ const n = current.length;
291
+ if (n === 0)
292
+ return [];
293
+ if (n === 1)
294
+ return [Math.max(0, Math.min(1, current[0] + deltaPct / 100))];
295
+ const deltaFrac = deltaPct / 100;
296
+ const preSum = current.reduce((s, v) => s + v, 0);
297
+ // First-touch: an uninitialized group conceptually starts as an
298
+ // even split. Build that virtual base, then proceed normally.
299
+ const base = preSum > 1e-9 ? current.slice() : new Array(n).fill(1 / n);
300
+ // Apply delta to target (clamped). If nothing actually changed,
301
+ // bail out — `base` is already at sum=1 (or whatever the user
302
+ // had); no-op clicks shouldn't trigger renormalization.
303
+ const targetNew = Math.max(0, Math.min(1, base[targetIdx] + deltaFrac));
304
+ if (targetNew === base[targetIdx] && preSum > 1e-9)
305
+ return current.slice();
306
+ const next = base.slice();
307
+ next[targetIdx] = targetNew;
308
+ // Renormalize the others by the same total delta — taking from
309
+ // them on a positive click, giving back on a negative click. Share
310
+ // is proportional to each slot's pre-click value; falls back to
311
+ // an even split when the others are all 0.
312
+ const targetDelta = targetNew - base[targetIdx];
313
+ const adjust = -targetDelta; // how much the others must absorb in aggregate
314
+ if (Math.abs(adjust) > 1e-12) {
315
+ const othersWeight = base.reduce((s, v, i) => (i === targetIdx ? s : s + v), 0);
316
+ for (let i = 0; i < n; i++) {
317
+ if (i === targetIdx)
318
+ continue;
319
+ const share = othersWeight > 0 ? base[i] / othersWeight : 1 / (n - 1);
320
+ next[i] = Math.max(0, Math.min(1, base[i] + adjust * share));
321
+ }
322
+ }
323
+ // Float drift correction so sum lands on exactly 1.
324
+ const finalTotal = next.reduce((s, v) => s + v, 0);
325
+ if (finalTotal > 0 && Math.abs(finalTotal - 1) > 1e-9) {
326
+ for (let i = 0; i < n; i++)
327
+ next[i] = next[i] / finalTotal;
328
+ }
329
+ return next;
330
+ }
331
+ export function getPersistentInteractions() {
332
+ const out = {
333
+ radioBindings: Array.from(radioBindings.values()),
334
+ radioSelections: {},
335
+ multiSelectBindings: Array.from(multiSelectBindings.values()),
336
+ multiSelectMax: {},
337
+ multiSelectSelections: {},
338
+ pointBindings: Array.from(pointBindings.values()),
339
+ pointPoolTotals: {},
340
+ pointAllocations: {},
341
+ numberSliderBindings: Array.from(numberSliderBindings.values()),
342
+ percentBindings: Array.from(percentBindings.values()),
343
+ };
344
+ for (const [groupId, value] of groupSelections) {
345
+ out.radioSelections[groupId] = value;
346
+ }
347
+ for (const [groupId, max] of multiSelectMax) {
348
+ out.multiSelectMax[groupId] = max;
349
+ }
350
+ for (const [groupId, set] of multiSelectSelections) {
351
+ if (set.size > 0)
352
+ out.multiSelectSelections[groupId] = Array.from(set);
353
+ }
354
+ for (const [groupId, total] of pointPoolTotals) {
355
+ out.pointPoolTotals[groupId] = total;
356
+ }
357
+ for (const groupId of pointAllocations.keys()) {
358
+ const list = getPointAllocations(groupId);
359
+ if (list.length > 0)
360
+ out.pointAllocations[groupId] = list;
361
+ }
362
+ return out;
363
+ }
364
+ export function loadPersistentInteractions(data) {
365
+ if (!data || typeof data !== 'object')
366
+ return;
367
+ const d = data;
368
+ // Radio
369
+ radioBindings.clear();
370
+ if (Array.isArray(d.radioBindings)) {
371
+ for (const b of d.radioBindings) {
372
+ if (b && typeof b === 'object') {
373
+ radioBindings.set(bindingKey(b), b);
374
+ }
375
+ }
376
+ }
377
+ groupSelections.clear();
378
+ if (d.radioSelections) {
379
+ for (const [groupId, value] of Object.entries(d.radioSelections)) {
380
+ if (typeof value === 'string')
381
+ groupSelections.set(groupId, value);
382
+ }
383
+ }
384
+ // Multi-select
385
+ multiSelectBindings.clear();
386
+ if (Array.isArray(d.multiSelectBindings)) {
387
+ for (const b of d.multiSelectBindings) {
388
+ if (b && typeof b === 'object') {
389
+ multiSelectBindings.set(multiSelectKey(b), b);
390
+ }
391
+ }
392
+ }
393
+ multiSelectMax.clear();
394
+ if (d.multiSelectMax) {
395
+ for (const [groupId, max] of Object.entries(d.multiSelectMax)) {
396
+ if (typeof max === 'number')
397
+ multiSelectMax.set(groupId, max);
398
+ }
399
+ }
400
+ multiSelectSelections.clear();
401
+ if (d.multiSelectSelections) {
402
+ for (const [groupId, values] of Object.entries(d.multiSelectSelections)) {
403
+ if (Array.isArray(values)) {
404
+ multiSelectSelections.set(groupId, new Set(values.filter((v) => typeof v === 'string')));
405
+ }
406
+ }
407
+ }
408
+ // Point allocator
409
+ pointBindings.clear();
410
+ if (Array.isArray(d.pointBindings)) {
411
+ for (const b of d.pointBindings) {
412
+ if (b && typeof b === 'object') {
413
+ pointBindings.set(pointBindingKey(b), b);
414
+ }
415
+ }
416
+ }
417
+ pointPoolTotals.clear();
418
+ if (d.pointPoolTotals) {
419
+ for (const [groupId, total] of Object.entries(d.pointPoolTotals)) {
420
+ if (typeof total === 'number')
421
+ pointPoolTotals.set(groupId, total);
422
+ }
423
+ }
424
+ pointAllocations.clear();
425
+ if (d.pointAllocations) {
426
+ for (const [groupId, list] of Object.entries(d.pointAllocations)) {
427
+ if (!Array.isArray(list))
428
+ continue;
429
+ const m = new Map();
430
+ for (const a of list) {
431
+ if (typeof a?.blockId === 'number' &&
432
+ typeof a?.row === 'number' &&
433
+ typeof a?.col === 'number' &&
434
+ typeof a?.points === 'number' &&
435
+ a.points > 0) {
436
+ m.set(cellKey(a.blockId, a.row, a.col), a.points);
437
+ }
438
+ }
439
+ if (m.size > 0)
440
+ pointAllocations.set(groupId, m);
441
+ }
442
+ }
443
+ // Number slider (bindings only — values live in cells)
444
+ numberSliderBindings.clear();
445
+ if (Array.isArray(d.numberSliderBindings)) {
446
+ for (const b of d.numberSliderBindings) {
447
+ if (b && typeof b === 'object') {
448
+ numberSliderBindings.set(numberSliderKey(b), b);
449
+ }
450
+ }
451
+ }
452
+ // Percent allocator (bindings only — values live in cells)
453
+ percentBindings.clear();
454
+ if (Array.isArray(d.percentBindings)) {
455
+ for (const b of d.percentBindings) {
456
+ if (b && typeof b === 'object') {
457
+ percentBindings.set(percentBindingKey(b), b);
458
+ }
459
+ }
460
+ }
461
+ notifyStore();
462
+ }
@@ -0,0 +1,51 @@
1
+ import type { Value } from 'logisheets-web';
2
+ import type { Violation } from '../validation/index.js';
3
+ export type FieldTypeEnum = 'enum' | 'multiSelect' | 'datetime' | 'boolean' | 'string' | 'number' | 'image' | 'fieldRef' | 'multiSelectRef';
4
+ export interface EnumValue {
5
+ id: string;
6
+ label: string;
7
+ description: string;
8
+ color: string;
9
+ }
10
+ export interface FieldSetting {
11
+ id: string;
12
+ name: string;
13
+ type: FieldTypeEnum;
14
+ description?: string;
15
+ required: boolean;
16
+ primary: boolean;
17
+ enumId?: string;
18
+ defaultValue?: string;
19
+ format?: string;
20
+ validation?: string;
21
+ unique?: boolean;
22
+ valueFormula?: string;
23
+ refSelf?: boolean;
24
+ refSheetId?: number;
25
+ refBlockId?: number;
26
+ refFieldName?: string;
27
+ }
28
+ export interface CellRef {
29
+ sheetIdx: number;
30
+ row: number;
31
+ col: number;
32
+ }
33
+ /** A field column: its constraints plus the data cells that belong to it. */
34
+ export interface FieldColumn {
35
+ field: Pick<FieldSetting, 'name' | 'required' | 'unique'>;
36
+ cells: readonly CellRef[];
37
+ /**
38
+ * Allowed values for membership-constrained fields (enum / multiSelect /
39
+ * fieldRef / multiSelectRef). When provided, every non-empty cell value
40
+ * must appear here. The caller resolves the candidate set — for enums it
41
+ * is the enum labels; for fieldRef it is the referenced field's values.
42
+ * Leave undefined for fields with no membership constraint.
43
+ */
44
+ allowed?: readonly string[];
45
+ }
46
+ /**
47
+ * Check `required` and `unique` across every field column and return all
48
+ * violating cells. Pure: the caller supplies `getValue` (WorkbookOps wraps the
49
+ * engine), so this runs identically in the browser and on Node.
50
+ */
51
+ export declare function checkFieldConstraints(columns: readonly FieldColumn[], getValue: (sheetIdx: number, row: number, col: number) => Value): Violation[];