@buoy-gg/shared-ui 7.0.35 → 7.0.36

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 (59) hide show
  1. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  2. package/lib/commonjs/icons/index.js +7 -0
  3. package/lib/commonjs/index.js +48 -0
  4. package/lib/commonjs/sync/keyedArrays.js +209 -0
  5. package/lib/commonjs/sync/toolAttention.test.js +51 -0
  6. package/lib/commonjs/sync/toolUiBridge.js +58 -0
  7. package/lib/commonjs/sync/typedEdit.js +228 -0
  8. package/lib/commonjs/sync/valuePath.js +337 -0
  9. package/lib/commonjs/sync/valueShape.js +161 -0
  10. package/lib/commonjs/ui/components/ExpandablePopover.js +38 -6
  11. package/lib/module/hooks/safe-area-impl.js +1 -1
  12. package/lib/module/icons/index.js +3 -1
  13. package/lib/module/index.js +4 -0
  14. package/lib/module/sync/keyedArrays.js +204 -0
  15. package/lib/module/sync/toolAttention.test.js +50 -0
  16. package/lib/module/sync/toolUiBridge.js +55 -0
  17. package/lib/module/sync/typedEdit.js +220 -0
  18. package/lib/module/sync/valuePath.js +330 -0
  19. package/lib/module/sync/valueShape.js +153 -0
  20. package/lib/module/ui/components/ExpandablePopover.js +38 -6
  21. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  22. package/lib/typescript/commonjs/icons/index.d.ts +1 -1
  23. package/lib/typescript/commonjs/icons/index.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/index.d.ts +4 -0
  25. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/sync/keyedArrays.d.ts +35 -0
  27. package/lib/typescript/commonjs/sync/keyedArrays.d.ts.map +1 -0
  28. package/lib/typescript/commonjs/sync/toolAttention.test.d.ts +2 -0
  29. package/lib/typescript/commonjs/sync/toolAttention.test.d.ts.map +1 -0
  30. package/lib/typescript/commonjs/sync/toolUiBridge.d.ts +25 -0
  31. package/lib/typescript/commonjs/sync/toolUiBridge.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/sync/typedEdit.d.ts +70 -0
  33. package/lib/typescript/commonjs/sync/typedEdit.d.ts.map +1 -0
  34. package/lib/typescript/commonjs/sync/valuePath.d.ts +52 -0
  35. package/lib/typescript/commonjs/sync/valuePath.d.ts.map +1 -0
  36. package/lib/typescript/commonjs/sync/valueShape.d.ts +77 -0
  37. package/lib/typescript/commonjs/sync/valueShape.d.ts.map +1 -0
  38. package/lib/typescript/commonjs/ui/components/ExpandablePopover.d.ts +14 -1
  39. package/lib/typescript/commonjs/ui/components/ExpandablePopover.d.ts.map +1 -1
  40. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  41. package/lib/typescript/module/icons/index.d.ts +1 -1
  42. package/lib/typescript/module/icons/index.d.ts.map +1 -1
  43. package/lib/typescript/module/index.d.ts +4 -0
  44. package/lib/typescript/module/index.d.ts.map +1 -1
  45. package/lib/typescript/module/sync/keyedArrays.d.ts +35 -0
  46. package/lib/typescript/module/sync/keyedArrays.d.ts.map +1 -0
  47. package/lib/typescript/module/sync/toolAttention.test.d.ts +2 -0
  48. package/lib/typescript/module/sync/toolAttention.test.d.ts.map +1 -0
  49. package/lib/typescript/module/sync/toolUiBridge.d.ts +25 -0
  50. package/lib/typescript/module/sync/toolUiBridge.d.ts.map +1 -1
  51. package/lib/typescript/module/sync/typedEdit.d.ts +70 -0
  52. package/lib/typescript/module/sync/typedEdit.d.ts.map +1 -0
  53. package/lib/typescript/module/sync/valuePath.d.ts +52 -0
  54. package/lib/typescript/module/sync/valuePath.d.ts.map +1 -0
  55. package/lib/typescript/module/sync/valueShape.d.ts +77 -0
  56. package/lib/typescript/module/sync/valueShape.d.ts.map +1 -0
  57. package/lib/typescript/module/ui/components/ExpandablePopover.d.ts +14 -1
  58. package/lib/typescript/module/ui/components/ExpandablePopover.d.ts.map +1 -1
  59. package/package.json +4 -4
@@ -0,0 +1,337 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.parsePath = parsePath;
7
+ exports.patchFromPath = patchFromPath;
8
+ exports.readPath = readPath;
9
+ var _keyedArrays = require("./keyedArrays.js");
10
+ /**
11
+ * Addressing a single value inside cached data or a store — one implementation,
12
+ * shared by every adapter that reads or merges.
13
+ *
14
+ * WHY. The most common measured write failure is not a wrong value, it is a
15
+ * wrong DEPTH: ten of eleven `query` failures on the bench sent `{name: "x"}`
16
+ * for a payload shaped `{item: {name}}`. That is not a missing instruction — the
17
+ * system prompt says, verbatim, "if the value is {item:{name}} send
18
+ * {"item":{"name":"test123"}}, never {name}" — and models that had just called
19
+ * `getQueryData` still flattened it. The rule is being read past, not missed.
20
+ *
21
+ * So stop asking. `path: "item.name", value: "x"` has no depth to get wrong:
22
+ * there is exactly one way to write it and the adapter builds the nested patch.
23
+ * The same syntax on the READ side returns one slice instead of a payload that
24
+ * gets cut off at the engine's 24,000-character cap, which is the other half of
25
+ * the same problem — a caller cannot write a shape it was never able to see.
26
+ *
27
+ * Lists are addressed by IDENTITY, never by position. `lines[0]` is resolved to
28
+ * the id of whatever is at index 0 right now and rewritten as an id-addressed
29
+ * edit, because between the read and the write an item may have been removed and
30
+ * index 0 is then a different row. A list whose items carry no stable id is
31
+ * refused rather than guessed at — see keyedArrays.ts for why silently picking a
32
+ * row is the failure mode that loses data.
33
+ */
34
+
35
+ const isPlainObject = v => typeof v === "object" && v !== null && !Array.isArray(v);
36
+ /**
37
+ * Where a field of this name actually lives, as a dotted path — breadth-first,
38
+ * capped so a big payload cannot stall the walk.
39
+ *
40
+ * A wrong `path` is not the same mistake as a wrong depth, and `path` does not
41
+ * prevent it. Measured on TWO cases in one run: grok wrote
42
+ * `path: "item.name"` against the flat PokéAPI payload `{name, id, stats, …}`,
43
+ * having generalised the shape from a different screen it had seen. The value it
44
+ * wanted was at `name`, one segment, right there. A refusal that says so is a
45
+ * one-turn recovery; a refusal that only lists the available fields makes the
46
+ * caller re-derive it.
47
+ */
48
+ function findFieldPath(obj, key, maxNodes = 2000) {
49
+ const queue = [{
50
+ node: obj,
51
+ path: ""
52
+ }];
53
+ let seen = 0;
54
+ while (queue.length && seen < maxNodes) {
55
+ const {
56
+ node,
57
+ path
58
+ } = queue.shift();
59
+ seen++;
60
+ if (Array.isArray(node)) {
61
+ node.slice(0, 20).forEach((v, i) => queue.push({
62
+ node: v,
63
+ path: `${path}[${i}]`
64
+ }));
65
+ } else if (isPlainObject(node)) {
66
+ for (const [k, v] of Object.entries(node)) {
67
+ const p = path ? `${path}.${k}` : k;
68
+ if (k === key) return p;
69
+ if (isPlainObject(v) || Array.isArray(v)) queue.push({
70
+ node: v,
71
+ path: p
72
+ });
73
+ }
74
+ }
75
+ }
76
+ return undefined;
77
+ }
78
+
79
+ /**
80
+ * "No field X here" plus, when the field exists somewhere else, the path that
81
+ * would have worked. `leaf` is the segment the caller was ultimately after.
82
+ */
83
+ function missingFieldError(root, at, key, available, leaf) {
84
+ const elsewhere = findFieldPath(root, leaf ?? key);
85
+ const has = available.slice(0, 12).join(", ") || "(no fields)";
86
+ return elsewhere ? `No field "${key}" at "${at}" — did you mean path "${elsewhere}"? "${at}" has: ${has}.` : `No field "${key}" at "${at}". It has: ${has}.`;
87
+ }
88
+
89
+ /** Read a dotted field path inside one list item. */
90
+ function fieldOf(item, field) {
91
+ let cur = item;
92
+ for (const seg of field.split(".")) {
93
+ if (!isPlainObject(cur)) return undefined;
94
+ cur = cur[seg];
95
+ }
96
+ return cur;
97
+ }
98
+
99
+ /**
100
+ * `lines[0].addonIds`, `item.name`, `results[name=pikachu].url` → segments.
101
+ *
102
+ * Returns undefined for syntax it does not understand rather than doing
103
+ * something approximate with it.
104
+ */
105
+ function parsePath(path) {
106
+ if (!path.trim()) return undefined;
107
+ const out = [];
108
+ // Walk the string rather than splitting on ".", because a [field=value]
109
+ // selector may legitimately contain a dot ("results[url=a.b/c]").
110
+ let i = 0;
111
+ while (i < path.length) {
112
+ if (path[i] === ".") {
113
+ i++;
114
+ continue;
115
+ }
116
+ if (path[i] === "[") {
117
+ const end = path.indexOf("]", i);
118
+ if (end === -1) return undefined;
119
+ const inner = path.slice(i + 1, end);
120
+ if (/^\d+$/.test(inner)) out.push({
121
+ kind: "index",
122
+ index: Number(inner)
123
+ });else {
124
+ const eq = inner.indexOf("=");
125
+ if (eq <= 0) return undefined;
126
+ out.push({
127
+ kind: "match",
128
+ field: inner.slice(0, eq).trim(),
129
+ value: inner.slice(eq + 1).trim()
130
+ });
131
+ }
132
+ i = end + 1;
133
+ continue;
134
+ }
135
+ let j = i;
136
+ while (j < path.length && path[j] !== "." && path[j] !== "[") j++;
137
+ const key = path.slice(i, j);
138
+ if (!key) return undefined;
139
+ out.push({
140
+ kind: "key",
141
+ key
142
+ });
143
+ i = j;
144
+ }
145
+ return out.length ? out : undefined;
146
+ }
147
+
148
+ /** Read the value at `path`, or undefined if any step of it does not exist. */
149
+ function readPath(value, path) {
150
+ const segs = parsePath(path);
151
+ if (!segs) return {
152
+ ok: false,
153
+ error: `Could not read the path "${path}". Use dotted keys with optional [0] or [field=value], like "item.name" or "lines[lineId=seed-1].qty".`
154
+ };
155
+ let cur = value;
156
+ const walked = [];
157
+ for (const seg of segs) {
158
+ if (cur === null || cur === undefined) {
159
+ return {
160
+ ok: false,
161
+ error: `"${walked.join(".") || "(root)"}" is ${cur === null ? "null" : "not set"}, so there is nothing at "${path}".`
162
+ };
163
+ }
164
+ if (seg.kind === "key") {
165
+ if (!isPlainObject(cur)) return {
166
+ ok: false,
167
+ error: `"${walked.join(".") || "(root)"}" is not an object, so it has no field "${seg.key}".`
168
+ };
169
+ if (!(seg.key in cur)) {
170
+ const leafSeg = segs[segs.length - 1];
171
+ return {
172
+ ok: false,
173
+ error: missingFieldError(value, walked.join(".") || "(root)", seg.key, Object.keys(cur), leafSeg?.kind === "key" ? leafSeg.key : undefined)
174
+ };
175
+ }
176
+ cur = cur[seg.key];
177
+ walked.push(seg.key);
178
+ } else if (seg.kind === "index") {
179
+ if (!Array.isArray(cur)) return {
180
+ ok: false,
181
+ error: `"${walked.join(".") || "(root)"}" is not a list, so [${seg.index}] means nothing.`
182
+ };
183
+ cur = cur[seg.index];
184
+ walked.push(`[${seg.index}]`);
185
+ } else {
186
+ if (!Array.isArray(cur)) return {
187
+ ok: false,
188
+ error: `"${walked.join(".") || "(root)"}" is not a list, so [${seg.field}=${seg.value}] means nothing.`
189
+ };
190
+ const hit = cur.find(el => isPlainObject(el) && String(fieldOf(el, seg.field)) === seg.value);
191
+ if (hit === undefined) return {
192
+ ok: false,
193
+ error: `No item with ${seg.field}="${seg.value}" in "${walked.join(".") || "(root)"}".`
194
+ };
195
+ cur = hit;
196
+ walked.push(`[${seg.field}=${seg.value}]`);
197
+ }
198
+ }
199
+ return {
200
+ ok: true,
201
+ value: cur
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Build the merge patch that sets `path` to `value`, against the data as it is
207
+ * now.
208
+ *
209
+ * The result is an ordinary merge patch, so it goes through the SAME typed-edit
210
+ * guard as a hand-written one and gains no new powers — `path` is a way to say
211
+ * the same thing without being able to get the depth wrong, not a way around
212
+ * the rules. A list step becomes the id-addressed form, so the write stays
213
+ * proof against a truncated read.
214
+ */
215
+ function patchFromPath(current, path, value) {
216
+ const segs = parsePath(path);
217
+ if (!segs) return {
218
+ ok: false,
219
+ error: `Could not read the path "${path}". Use dotted keys with optional [0] or [field=value], like "item.name" or "lines[lineId=seed-1].qty".`
220
+ };
221
+ if (segs[0]?.kind !== "key") return {
222
+ ok: false,
223
+ error: `A path must start with a field name, not a list index — "${path}" does not say which list.`
224
+ };
225
+ const build = (node, at) => {
226
+ if (at >= segs.length) return {
227
+ ok: true,
228
+ patch: value
229
+ };
230
+ const seg = segs[at];
231
+ if (seg.kind === "key") {
232
+ // A field that does not exist is left for the typed-edit guard, which owns
233
+ // that decision — EXCEPT when there are more segments to walk, because
234
+ // then the caller is describing a shape that is not there at all and the
235
+ // guard's message ("no field named `item`") would not say where the value
236
+ // it wanted actually lives.
237
+ if (isPlainObject(node) && !(seg.key in node) && at + 1 < segs.length) {
238
+ const leafSeg = segs[segs.length - 1];
239
+ return {
240
+ ok: false,
241
+ error: missingFieldError(current, pathUpTo(segs, at), seg.key, Object.keys(node), leafSeg?.kind === "key" ? leafSeg.key : undefined)
242
+ };
243
+ }
244
+ const child = isPlainObject(node) ? node[seg.key] : undefined;
245
+ const inner = build(child, at + 1);
246
+ return inner.ok ? {
247
+ ok: true,
248
+ patch: {
249
+ [seg.key]: inner.patch
250
+ }
251
+ } : inner;
252
+ }
253
+ if (!Array.isArray(node)) {
254
+ return {
255
+ ok: false,
256
+ error: `"${pathUpTo(segs, at)}" is not a list, so "${path}" cannot be resolved. Read it first — \`shape\` on the read says what each field holds.`
257
+ };
258
+ }
259
+ const index = seg.kind === "index" ? seg.index : node.findIndex(el => isPlainObject(el) && String(fieldOf(el, seg.field)) === seg.value);
260
+ const item = index >= 0 ? node[index] : undefined;
261
+ const missing = seg.kind === "index" ? index < 0 || index >= node.length : index < 0;
262
+ if (missing) {
263
+ return {
264
+ ok: false,
265
+ error: seg.kind === "index" ? `"${pathUpTo(segs, at)}" has ${node.length} item(s), so there is nothing at [${seg.index}].` : `No item with ${seg.field}="${seg.value}" in "${pathUpTo(segs, at)}".`
266
+ };
267
+ }
268
+ /**
269
+ * A list of PRIMITIVES, addressed by index — `tags[0]` on
270
+ * `["alpha","beta"]`.
271
+ *
272
+ * The guard below used to be `!isPlainObject(item)`, which folded this in
273
+ * with a genuinely out-of-range index and answered `"tags" has 2 item(s),
274
+ * so there is nothing at [0]` — a sentence that contradicts itself, for the
275
+ * commonest index there is. A model reading the data, seeing two items and
276
+ * being told [0] and [1] are both empty has nowhere left to go.
277
+ *
278
+ * There is nothing to descend into on a string, so the index IS the leaf:
279
+ * rebuild the list with that one element replaced, exactly as the no-stable-id
280
+ * branch below does. Anything deeper genuinely cannot be resolved, and says so.
281
+ */
282
+ if (!isPlainObject(item)) {
283
+ if (at < segs.length - 1) {
284
+ return {
285
+ ok: false,
286
+ error: `"${pathUpTo(segs, at)}[${seg.kind === "index" ? seg.index : `${seg.field}=${seg.value}`}]" holds ${item === null ? "null" : typeof item}, not an object, so "${path}" cannot go any deeper. Set the item itself.`
287
+ };
288
+ }
289
+ const replaced = node.slice();
290
+ replaced[index] = value;
291
+ return {
292
+ ok: true,
293
+ patch: replaced
294
+ };
295
+ }
296
+ const inner = build(item, at + 1);
297
+ if (!inner.ok) return inner;
298
+ const idField = (0, _keyedArrays.identityField)(node);
299
+ // Positions move; ids do not. Prefer the addressed form whichever way the
300
+ // caller named the row.
301
+ if (idField) return {
302
+ ok: true,
303
+ patch: {
304
+ [String(item[idField])]: inner.patch
305
+ }
306
+ };
307
+
308
+ // No stable id — a Pokémon's `stats` are `{stat: {name}, base_stat}`, with
309
+ // nothing flat and unique on the item itself, and that is an ordinary shape
310
+ // rather than a broken one. Rebuild the whole list here instead of refusing.
311
+ //
312
+ // This is safe in a way the SAME array coming from a model is not: we are
313
+ // holding the current list in memory, so nothing is being reconstructed from
314
+ // a truncated read and no row can be dropped by omission. The caller still
315
+ // only named one value. What it cannot be is proof against the list changing
316
+ // between this call and the write, which addressing by id would be — so the
317
+ // id form stays preferred wherever one exists.
318
+ const next = node.slice();
319
+ next[index] = isPlainObject(inner.patch) ? {
320
+ ...item,
321
+ ...inner.patch
322
+ } : inner.patch;
323
+ return {
324
+ ok: true,
325
+ patch: next
326
+ };
327
+ };
328
+ const built = build(current, 0);
329
+ if (!built.ok) return built;
330
+ return {
331
+ ok: true,
332
+ patch: built.patch
333
+ };
334
+ }
335
+ function pathUpTo(segs, at) {
336
+ return segs.slice(0, at).map(s => s.kind === "key" ? s.key : s.kind === "index" ? `[${s.index}]` : `[${s.field}=${s.value}]`).join(".").replace(/\.\[/g, "[") || "(root)";
337
+ }
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.listShapes = listShapes;
7
+ exports.shapeOf = shapeOf;
8
+ exports.shapeSummary = shapeSummary;
9
+ exports.shapesWereTruncated = shapesWereTruncated;
10
+ exports.unshapedLists = unshapedLists;
11
+ /**
12
+ * A compact type sketch of a runtime value — one implementation, shared by every
13
+ * adapter that hands data to a model.
14
+ *
15
+ * WHY. A model cannot write a well-formed item into a list it has never seen the
16
+ * shape of, and we were not showing it. Measured on `cart/bag-still-zustand`: the
17
+ * rendered prompt is 83,000 characters and the words `lineId`, `itemId`,
18
+ * `sizeId`, `addonIds` and `unitPrice` appear in it ZERO times. Every model
19
+ * "failing" that case invented a line shape because inventing was the only move
20
+ * available. The zustand typed-edit guard would have caught the invention — but
21
+ * only on a list that already holds an item, since it builds its template from
22
+ * `list[0]`. On an EMPTY list there is no template, the guard has nothing to
23
+ * compare against, and the invented shape is written silently.
24
+ *
25
+ * So the sketch has two jobs, and the second is the one that pays:
26
+ *
27
+ * 1. Say what the items in a list look like, so an addition can match.
28
+ * 2. Say when NOTHING knows — an empty list's item shape is not merely
29
+ * unread, it is unknowable from runtime state, and the honest answer is to
30
+ * admit that rather than let a guess through unremarked.
31
+ *
32
+ * It is deliberately tiny. A sketch of a 120-line bag is the same size as a
33
+ * sketch of a 2-line one, so unlike the data it survives the engine's 24,000-
34
+ * character result cap — which is the whole point, because the cases where the
35
+ * model most needs the shape are exactly the ones where the data got cut off.
36
+ */
37
+
38
+ const isPlainObject = v => typeof v === "object" && v !== null && !Array.isArray(v);
39
+
40
+ /** Beyond this the sketch stops being cheaper to read than the data. */
41
+ const MAX_DEPTH = 3;
42
+ const MAX_KEYS = 24;
43
+ /**
44
+ * Hard caps on `listShapes`. MAX_DEPTH bounds how DEEP it goes; nothing bounded
45
+ * how WIDE it went, and it walks every entry of every object at each permitted
46
+ * level. A store holding a few hundred keyed records therefore produced a
47
+ * "compact" sketch larger than the data it was standing in for — in a value
48
+ * that rides in every prompt. Depth is not a size budget.
49
+ */
50
+ const MAX_LIST_PATHS = 40;
51
+ const MAX_ENTRIES_VISITED = 600;
52
+
53
+ /**
54
+ * `{lineId: string, qty: number, addonIds: string[]}` — a one-line type for a
55
+ * value, built from the value itself.
56
+ *
57
+ * An array is described by its FIRST item, because a list whose items disagree
58
+ * is a list nothing can safely add to and the guard will say so anyway. An empty
59
+ * array is `unknown[]`, which is the true answer and the one that matters: it
60
+ * tells a reader that adding to this list is unchecked.
61
+ */
62
+ function shapeOf(value, depth = 0) {
63
+ if (value === null) return "null";
64
+ if (Array.isArray(value)) {
65
+ if (!value.length) return "unknown[]";
66
+ if (depth >= MAX_DEPTH) return "…[]";
67
+ const inner = shapeOf(value[0], depth + 1);
68
+ return inner.includes(" ") || inner.endsWith("]") ? `(${inner})[]` : `${inner}[]`;
69
+ }
70
+ if (isPlainObject(value)) {
71
+ if (depth >= MAX_DEPTH) return "{…}";
72
+ const keys = Object.keys(value);
73
+ const shown = keys.slice(0, MAX_KEYS);
74
+ const body = shown.map(k => `${k}: ${shapeOf(value[k], depth + 1)}`).join(", ");
75
+ const more = keys.length > shown.length ? `, +${keys.length - shown.length} more` : "";
76
+ return `{${body}${more}}`;
77
+ }
78
+ const t = typeof value;
79
+ return t === "undefined" ? "undefined" : t === "function" ? "function" : t;
80
+ }
81
+ /**
82
+ * Every list inside a value, with the shape of what goes in it.
83
+ *
84
+ * This is what a caller needs before adding to a list, and it is the part that
85
+ * cannot be recovered from a truncated read of the data itself.
86
+ *
87
+ * A list with `item: undefined` is the warning case: it is empty, so its item
88
+ * shape exists nowhere in the running app — not in the store, not in the
89
+ * persisted copy, not in this sketch. Anything added to it goes in unchecked.
90
+ */
91
+ function listShapes(value, path = "", out = [], depth = 0, budget = {
92
+ visited: 0
93
+ }) {
94
+ if (depth > MAX_DEPTH) return out;
95
+ // Two independent stops: enough lists described, or enough of the value
96
+ // walked. The second is what a wide-but-shallow store hits.
97
+ if (out.length >= MAX_LIST_PATHS || budget.visited >= MAX_ENTRIES_VISITED) return out;
98
+ if (Array.isArray(value)) {
99
+ const first = value[0];
100
+ out.push({
101
+ path: path || "(root)",
102
+ length: value.length,
103
+ ...(value.length ? {
104
+ item: shapeOf(first)
105
+ } : {})
106
+ });
107
+ // One level into the items, so `lines[].addonIds` is reachable too.
108
+ if (value.length && isPlainObject(first)) {
109
+ for (const [k, v] of Object.entries(first)) {
110
+ if (++budget.visited >= MAX_ENTRIES_VISITED) break;
111
+ listShapes(v, path ? `${path}[].${k}` : `[].${k}`, out, depth + 1, budget);
112
+ }
113
+ }
114
+ return out;
115
+ }
116
+ if (isPlainObject(value)) {
117
+ for (const [k, v] of Object.entries(value)) {
118
+ if (++budget.visited >= MAX_ENTRIES_VISITED) break;
119
+ listShapes(v, path ? `${path}.${k}` : k, out, depth + 1, budget);
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+
125
+ /** True when a sketch stopped early, so a caller can say so rather than imply completeness. */
126
+ function shapesWereTruncated(value) {
127
+ const budget = {
128
+ visited: 0
129
+ };
130
+ const out = listShapes(value, "", [], 0, budget);
131
+ return out.length >= MAX_LIST_PATHS || budget.visited >= MAX_ENTRIES_VISITED;
132
+ }
133
+
134
+ /**
135
+ * The lists in a value that a caller could add to WITHOUT any way to know the
136
+ * item shape. Empty by definition on a healthy value; non-empty means a write
137
+ * into one of these paths cannot be checked by anything.
138
+ */
139
+ function unshapedLists(value) {
140
+ return listShapes(value).filter(l => l.item === undefined).map(l => l.path);
141
+ }
142
+
143
+ /**
144
+ * The sketch an adapter attaches to a read: the value's own type, plus the item
145
+ * type of every list in it.
146
+ *
147
+ * Returns undefined when there is nothing worth saying (a scalar, or an object
148
+ * holding no lists) so adapters can spread it and add no noise.
149
+ */
150
+ function shapeSummary(value) {
151
+ if (value === undefined) return undefined;
152
+ const lists = listShapes(value);
153
+ const shape = shapeOf(value);
154
+ if (!lists.length && !isPlainObject(value) && !Array.isArray(value)) return undefined;
155
+ return lists.length ? {
156
+ shape,
157
+ lists
158
+ } : {
159
+ shape
160
+ };
161
+ }
@@ -32,7 +32,8 @@ function CollapsedPeek({
32
32
  height,
33
33
  customContent,
34
34
  label,
35
- testID
35
+ testID,
36
+ attention
36
37
  }) {
37
38
  // When progress=0 (collapsed): peek is visible (opacity=1, scale=1)
38
39
  // When progress=1 (expanded): peek is hidden (opacity=0, scale=0.9)
@@ -52,17 +53,17 @@ function CollapsedPeek({
52
53
  scale
53
54
  }]
54
55
  }],
55
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
56
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
56
57
  onPress: onPress,
57
58
  activeOpacity: 0.8,
58
59
  style: [styles.peekButton, {
59
60
  width,
60
61
  height
61
62
  }],
62
- accessibilityLabel: label || `Expand options`,
63
+ accessibilityLabel: attention?.label ? `${label || "Expand options"} — ${attention.label}` : label || `Expand options`,
63
64
  accessibilityRole: "button",
64
65
  testID: testID,
65
- children: customContent || /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
66
+ children: [customContent || /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
66
67
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_index.ChevronUp, {
67
68
  size: 12,
68
69
  color: _gameUIColors.buoyColors.textMuted,
@@ -71,7 +72,17 @@ function CollapsedPeek({
71
72
  style: styles.peekCount,
72
73
  children: count
73
74
  })]
74
- })
75
+ }), attention ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
76
+ pointerEvents: "none",
77
+ testID: testID ? `${testID}-attention` : undefined,
78
+ style: [styles.peekAttention, {
79
+ backgroundColor: attention.color
80
+ }],
81
+ children: attention.glyph ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
82
+ style: styles.peekAttentionGlyph,
83
+ children: attention.glyph
84
+ }) : null
85
+ }) : null]
75
86
  })
76
87
  });
77
88
  }
@@ -141,6 +152,7 @@ function ExpandablePopover({
141
152
  defaultExpanded = false,
142
153
  collapsedContent,
143
154
  collapsedLabel,
155
+ attention = null,
144
156
  expandedStyle,
145
157
  persistState = false,
146
158
  storageKey,
@@ -270,7 +282,8 @@ function ExpandablePopover({
270
282
  height: peekHeight,
271
283
  customContent: collapsedContent,
272
284
  label: collapsedLabel,
273
- testID: testID
285
+ testID: testID,
286
+ attention: attention
274
287
  })
275
288
  })]
276
289
  });
@@ -281,6 +294,25 @@ function ExpandablePopover({
281
294
  // ============================================================================
282
295
 
283
296
  const styles = _reactNative.StyleSheet.create({
297
+ peekAttention: {
298
+ position: "absolute",
299
+ top: 2,
300
+ right: 2,
301
+ minWidth: 11,
302
+ height: 11,
303
+ borderRadius: 6,
304
+ paddingHorizontal: 2,
305
+ alignItems: "center",
306
+ justifyContent: "center",
307
+ borderWidth: 1.5,
308
+ borderColor: "#0C0C0C"
309
+ },
310
+ peekAttentionGlyph: {
311
+ color: "#000",
312
+ fontSize: 8,
313
+ fontWeight: "800",
314
+ lineHeight: 9
315
+ },
284
316
  container: {
285
317
  overflow: "visible",
286
318
  zIndex: 1000
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * Auto-generated safe area implementation
5
5
  * Detected: none
6
- * Generated at: 2026-09-04T23:36:20.710Z
6
+ * Generated at: 2026-09-10T21:40:50.832Z
7
7
  *
8
8
  * DO NOT EDIT - This file is generated by scripts/detect-safe-area.js
9
9
  *
@@ -48,7 +48,9 @@ export * from "./lucide-icons.js";
48
48
  // stands it upright, which the glyph data has no way to express (the ports draw
49
49
  // the resting pose only). `WifiIcon` stays lucide for the same kind of reason —
50
50
  // see the `WifiBifIcon` note below.
51
- export { ActivityGlyph as Activity, AlertCircleGlyph as AlertCircle, AlertTriangleGlyph as AlertTriangle, BookmarkGlyph as Bookmark, BoxGlyph as Box, BugGlyph as Bug, CheckCircleGlyph as CheckCircle, CheckGlyph as Check, ChevronDownGlyph as ChevronDown, ChevronLeftGlyph as ChevronLeft, ChevronRightGlyph as ChevronRight, ChevronUpGlyph as ChevronUp, ClockGlyph as Clock, CopyGlyph as Copy, DatabaseGlyph as Database, DockBottomGlyph as DockBottom, DownloadGlyph as Download, Edit3Glyph as Edit3, EyeOffGlyph as EyeOff, EyeGlyph as Eye, FileJsonGlyph as FileJson, FileTextGlyph as FileText, FilmGlyph as Film, FilterGlyph as Filter, FlaskConicalGlyph as FlaskConical, FloatWindowGlyph as FloatWindow, GitBranchGlyph as GitBranch, GlobeGlyph as Globe, HashGlyph as Hash, HomeGlyph as Home, ImageGlyph as Image, InfoGlyph as Info, KeyGlyph as Key, LayersGlyph as Layers, LinkGlyph as Link, LockGlyph as Lock, MinusGlyph as Minus, MusicGlyph as Music, NavigationGlyph as Navigation, PaletteGlyph as Palette, PauseGlyph as Pause, PlayGlyph as Play, PlusGlyph as Plus, PowerGlyph as Power, RefreshCwGlyph as RefreshCw, SearchGlyph as Search, ServerGlyph as Server, SettingsGlyph as Settings, ShieldGlyph as Shield, Trash2Glyph as Trash2,
51
+ export { ActivityGlyph as Activity, AlertCircleGlyph as AlertCircle, AlertTriangleGlyph as AlertTriangle, BookmarkGlyph as Bookmark, BoxGlyph as Box, BugGlyph as Bug, CheckCircleGlyph as CheckCircle, CheckGlyph as Check, ChevronDownGlyph as ChevronDown, ChevronLeftGlyph as ChevronLeft, ChevronRightGlyph as ChevronRight, ChevronUpGlyph as ChevronUp, ClockGlyph as Clock, CopyGlyph as Copy, DatabaseGlyph as Database, DockBottomGlyph as DockBottom, DownloadGlyph as Download, Edit3Glyph as Edit3, EyeOffGlyph as EyeOff, EyeGlyph as Eye, FileJsonGlyph as FileJson, FileTextGlyph as FileText, FilmGlyph as Film, FilterGlyph as Filter, FlaskConicalGlyph as FlaskConical, FloatWindowGlyph as FloatWindow, GitBranchGlyph as GitBranch, GlobeGlyph as Globe, HashGlyph as Hash, HomeGlyph as Home, ImageGlyph as Image, InfoGlyph as Info, KeyGlyph as Key, LayersGlyph as Layers, LinkGlyph as Link, LockGlyph as Lock, MinusGlyph as Minus, MusicGlyph as Music, NavigationGlyph as Navigation, PaletteGlyph as Palette, PauseGlyph as Pause, PlayGlyph as Play, PlusGlyph as Plus, PowerGlyph as Power, RefreshCwGlyph as RefreshCw, SearchGlyph as Search, ServerGlyph as Server, SettingsGlyph as Settings,
52
+ // Solid gear (brand tier, so a caller's strokeWidth can't blow the hole open).
53
+ SettingsSolidIcon as SettingsSolid, ShieldGlyph as Shield, Trash2Glyph as Trash2,
52
54
  // RN aliases `Trash` onto `Trash2Icon`; the glyph has to shadow both.
53
55
  Trash2Glyph as Trash, UnlockGlyph as Unlock, UploadGlyph as Upload, UserGlyph as User, WifiGlyph as Wifi, XCircleGlyph as XCircle, XGlyph as X, ZapGlyph as Zap } from "@buoy-gg/floating-tools-core";
54
56
 
@@ -5,6 +5,10 @@ export * from "./ui/index.js";
5
5
 
6
6
  // Store base classes
7
7
  export * from "./stores/index.js";
8
+ export * from "./sync/keyedArrays.js";
9
+ export * from "./sync/valueShape.js";
10
+ export * from "./sync/valuePath.js";
11
+ export * from "./sync/typedEdit.js";
8
12
  export * from "./sync/wireBudget.js";
9
13
  export * from "./sync/wireForm.js";
10
14
  export * from "./sync/toolSyncAdapter.js";