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.
- package/dist/craft-interactions/index.d.ts +140 -0
- package/dist/craft-interactions/index.js +462 -0
- package/dist/field/index.d.ts +51 -0
- package/dist/field/index.js +70 -0
- package/dist/format/index.d.ts +21 -0
- package/dist/format/index.js +454 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +17 -0
- package/dist/ops/index.d.ts +155 -0
- package/dist/ops/index.js +335 -0
- package/dist/permissions/index.d.ts +32 -0
- package/dist/permissions/index.js +74 -0
- package/dist/port.d.ts +9 -0
- package/dist/port.js +14 -0
- package/dist/selection/index.d.ts +1 -0
- package/dist/selection/index.js +1 -0
- package/dist/selection/model.d.ts +10 -0
- package/dist/selection/model.js +38 -0
- package/dist/strings/case.d.ts +2 -0
- package/dist/strings/case.js +6 -0
- package/dist/strings/char-code.d.ts +422 -0
- package/dist/strings/char-code.js +424 -0
- package/dist/strings/common_length.d.ts +4 -0
- package/dist/strings/common_length.js +28 -0
- package/dist/strings/contain.d.ts +4 -0
- package/dist/strings/contain.js +60 -0
- package/dist/strings/index.d.ts +5 -0
- package/dist/strings/index.js +5 -0
- package/dist/strings/judges.d.ts +3 -0
- package/dist/strings/judges.js +38 -0
- package/dist/strings/surrogate.d.ts +16 -0
- package/dist/strings/surrogate.js +30 -0
- package/dist/structured/index.d.ts +41 -0
- package/dist/structured/index.js +50 -0
- package/dist/transaction/index.d.ts +8 -0
- package/dist/transaction/index.js +11 -0
- package/dist/type-guard/index.d.ts +3 -0
- package/dist/type-guard/index.js +3 -0
- package/dist/type-guard/propterty.d.ts +1 -0
- package/dist/type-guard/propterty.js +3 -0
- package/dist/type-guard/string.d.ts +1 -0
- package/dist/type-guard/string.js +3 -0
- package/dist/type-guard/u8.d.ts +1 -0
- package/dist/type-guard/u8.js +3 -0
- package/dist/utils/a1notation.d.ts +18 -0
- package/dist/utils/a1notation.js +76 -0
- package/dist/utils/array.d.ts +2 -0
- package/dist/utils/array.js +12 -0
- package/dist/utils/clone.d.ts +2 -0
- package/dist/utils/clone.js +25 -0
- package/dist/utils/const.d.ts +7 -0
- package/dist/utils/const.js +8 -0
- package/dist/utils/equal.d.ts +1 -0
- package/dist/utils/equal.js +11 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/uuid.d.ts +1 -0
- package/dist/utils/uuid.js +4 -0
- package/dist/validation/index.d.ts +34 -0
- package/dist/validation/index.js +60 -0
- package/dist/value/index.d.ts +9 -0
- package/dist/value/index.js +49 -0
- 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[];
|