@ixfx/components 0.15.4 → 0.15.5

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 (61) hide show
  1. package/bundle/index.d.ts +307 -5
  2. package/bundle/index.js +283 -114
  3. package/dist/ac-text.d.ts +1 -1
  4. package/dist/{ac-token-Cq7lsYf-.d.ts → ac-token-qEs3FsQv.d.ts} +1 -1
  5. package/dist/ac-token.d.ts +1 -1
  6. package/dist/colour-picker.d.ts +1 -1
  7. package/dist/crumbs.d.ts +1 -1
  8. package/dist/{data-grid-Dx3l3dxw.js → data-grid-C1sZX6n9.js} +37 -9
  9. package/dist/{data-grid-CbiubdG2.d.ts → data-grid-D7qzmMMp.d.ts} +14 -7
  10. package/dist/data-grid.d.ts +1 -1
  11. package/dist/data-grid.js +1 -1
  12. package/dist/{detail-list-ClkMV0SN.d.ts → detail-list-CKIKlszv.d.ts} +3 -3
  13. package/dist/detail-list.d.ts +2 -2
  14. package/dist/{flat-list-0xTZdXzh.d.ts → flat-list-DXMyDiSH.d.ts} +2 -2
  15. package/dist/flat-list.d.ts +1 -1
  16. package/dist/{grid-list-BnWJUIBC.d.ts → grid-list-0e7snW6t.d.ts} +1 -1
  17. package/dist/grid-list.d.ts +1 -1
  18. package/dist/{grouped-item-lister-Bhyo-dkH.d.ts → grouped-item-lister-DETte0Dh.d.ts} +1 -1
  19. package/dist/grouped-item-lister.d.ts +1 -1
  20. package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +1 -1
  21. package/dist/icons.d.ts +2 -2
  22. package/dist/incr-search.d.ts +1 -1
  23. package/dist/{index-garxxqgV.d.ts → index-CYndvTWH.d.ts} +1 -1
  24. package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +1 -1
  25. package/dist/{index-BAuqR9El.d.ts → index-DEVyVIn8.d.ts} +1 -1
  26. package/dist/{index-Bz3loIAf.d.ts → index-DN2BWctP.d.ts} +1 -1
  27. package/dist/{index-cnaRPQPp.d.ts → index-DxyyVqzA.d.ts} +2 -2
  28. package/dist/{index-BPTuCRGb.d.ts → index-KN2MW_xD.d.ts} +1 -1
  29. package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +1 -1
  30. package/dist/index.d.ts +56 -20
  31. package/dist/index.js +3 -2
  32. package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +1 -1
  33. package/dist/labelled-radial-input.d.ts +2 -2
  34. package/dist/labelled-range-input.d.ts +1 -1
  35. package/dist/labelled-slider-input.d.ts +1 -1
  36. package/dist/list-group-header.d.ts +1 -1
  37. package/dist/{menu-item-B0zqw2xp.d.ts → menu-item-bF6narIz.d.ts} +1 -1
  38. package/dist/menu.d.ts +2 -2
  39. package/dist/miller.d.ts +1 -1
  40. package/dist/panel.d.ts +1 -1
  41. package/dist/{split-layout-C5SYbDrq.d.ts → split-layout-JK3xWVX2.d.ts} +1 -1
  42. package/dist/split-layout.d.ts +1 -1
  43. package/dist/sunburst-tree.d.ts +2 -0
  44. package/dist/sunburst-tree.js +2 -0
  45. package/dist/sunbursttree-list--Z0uyh8O.d.ts +266 -0
  46. package/dist/sunbursttree-list-yXwS-uEI.js +1337 -0
  47. package/dist/{tab-list-wC0xNz1b.d.ts → tab-list-Df371rqw.d.ts} +1 -1
  48. package/dist/tabs.d.ts +1 -1
  49. package/dist/{titlebar-B5zSTZnF.d.ts → titlebar-Tqu-VvMU.d.ts} +1 -1
  50. package/dist/titlebar.d.ts +1 -1
  51. package/dist/tree.d.ts +1 -1
  52. package/dist/{vertical-list-B8FoU-pU.d.ts → vertical-list-D8MksoTp.d.ts} +1 -1
  53. package/dist/vertical-list.d.ts +1 -1
  54. package/docs-user/README.md +1 -0
  55. package/docs-user/data-grid.md +11 -4
  56. package/docs-user/index.json +7 -2
  57. package/docs-user/llms.txt +1 -0
  58. package/docs-user/sunburst-tree.md +193 -0
  59. package/docs-user/tree.md +1 -1
  60. package/llms.txt +1 -0
  61. package/package.json +5 -1
@@ -0,0 +1,1337 @@
1
+ import { t as TickledController } from "./tickled-controller-D-HZymaZ.js";
2
+ import { t as themeFallbacks } from "./fallbacks-DQ_5OsmR.js";
3
+ import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
4
+ import { i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES, t as createTreeListSearchAdapter } from "./incr-search-JIhwB49y.js";
5
+ import { t as collectDataResult } from "./data-provider-35TyWclX.js";
6
+ import { LitElement, css, html, nothing, svg } from "lit";
7
+ import { property, state } from "lit/decorators.js";
8
+ import { createCommandRegistry } from "@clinth/ui-commands";
9
+ import { repeat } from "lit/directives/repeat.js";
10
+ //#region src/sunburst-tree/types.ts
11
+ /** Ids are key paths; keys only need to be unique among siblings. */
12
+ const SUNBURST_PATH_SEPARATOR = `\u001F`;
13
+ //#endregion
14
+ //#region src/sunburst-tree/layout.ts
15
+ /** Default accessor: the shared `TreeItem.weight` field. */
16
+ const defaultSunburstValueAccessor = (item) => item.weight;
17
+ /** Id of the child `key` under `parentId`. */
18
+ function sunburstChildId(parentId, key) {
19
+ return `${parentId}${SUNBURST_PATH_SEPARATOR}${key}`;
20
+ }
21
+ /** Join a key path (root key first) into an id. */
22
+ function sunburstPathToId(path) {
23
+ return path.join(SUNBURST_PATH_SEPARATOR);
24
+ }
25
+ /** True when `ancestorId` is a strict ancestor of `id`. */
26
+ function isSunburstAncestorId(ancestorId, id) {
27
+ return id.startsWith(`${ancestorId}${SUNBURST_PATH_SEPARATOR}`);
28
+ }
29
+ /** Split an id back into its key path. */
30
+ function sunburstIdToPath(id) {
31
+ return id.split(SUNBURST_PATH_SEPARATOR);
32
+ }
33
+ /** Parent id, or `undefined` for the root id. */
34
+ function sunburstParentId(id) {
35
+ const at = id.lastIndexOf(SUNBURST_PATH_SEPARATOR);
36
+ return at < 0 ? void 0 : id.slice(0, at);
37
+ }
38
+ /** Resolve an id (key path from the root, root key included) to a node. */
39
+ function findSunburstNodeById(root, id) {
40
+ if (!root) return void 0;
41
+ const [rootKey, ...rest] = sunburstIdToPath(id);
42
+ if (rootKey !== root.item.key) return void 0;
43
+ let current = root;
44
+ for (const key of rest) {
45
+ current = current.children?.find((c) => c.item.key === key);
46
+ if (!current) return void 0;
47
+ }
48
+ return current;
49
+ }
50
+ /**
51
+ * Depth-first search for the first node matching `predicate`. Returns its id
52
+ * (root key included), or `undefined`.
53
+ */
54
+ function findSunburstId(root, predicate) {
55
+ if (!root) return void 0;
56
+ const visit = (node, id) => {
57
+ if (predicate(node)) return id;
58
+ for (const child of node.children ?? []) {
59
+ const hit = visit(child, sunburstChildId(id, child.item.key));
60
+ if (hit !== void 0) return hit;
61
+ }
62
+ };
63
+ return visit(root, root.item.key);
64
+ }
65
+ /** Valid explicit value: finite and non-negative. */
66
+ function explicitValue(raw) {
67
+ return typeof raw === `number` && Number.isFinite(raw) && raw >= 0 ? raw : void 0;
68
+ }
69
+ /**
70
+ * Returns a memoised resolver for node values: the accessor's value when
71
+ * valid, else the sum of loaded children, else `1`.
72
+ */
73
+ function createValueResolver(accessor) {
74
+ const cache = /* @__PURE__ */ new Map();
75
+ const resolve = (node) => {
76
+ const cached = cache.get(node);
77
+ if (cached !== void 0) return cached;
78
+ let value = explicitValue(accessor(node.item, node));
79
+ if (value === void 0) {
80
+ const children = node.children ?? [];
81
+ value = children.length > 0 ? children.reduce((sum, c) => sum + resolve(c), 0) : 1;
82
+ }
83
+ cache.set(node, value);
84
+ return value;
85
+ };
86
+ return resolve;
87
+ }
88
+ /** True when `node` may have children (loaded or loadable). */
89
+ function sunburstHasChildren(node) {
90
+ if (node.children !== void 0) return node.children.length > 0;
91
+ return node.item.isLeaf !== true;
92
+ }
93
+ /**
94
+ * Compute the absolute layout of the whole (loaded) tree. The root spans the
95
+ * full turn at depth `0`; each child spans
96
+ * `parentSpan × value / max(parentValue, Σ siblingValues)`, packed from the
97
+ * parent's start angle in sibling order. Returned in ring-major order.
98
+ */
99
+ function computeSunburstLayout(root, options = {}) {
100
+ const out = /* @__PURE__ */ new Map();
101
+ if (!root) return out;
102
+ const { valueAccessor = defaultSunburstValueAccessor, maxDepth = Number.POSITIVE_INFINITY } = options;
103
+ const valueOf = createValueResolver(valueAccessor);
104
+ const rootEntry = {
105
+ id: root.item.key,
106
+ node: root,
107
+ parentId: void 0,
108
+ depth: 0,
109
+ x0: 0,
110
+ x1: 1,
111
+ value: valueOf(root),
112
+ index: 0,
113
+ setSize: 1,
114
+ hasChildren: sunburstHasChildren(root)
115
+ };
116
+ out.set(rootEntry.id, rootEntry);
117
+ let ring = [rootEntry];
118
+ while (ring.length > 0) {
119
+ const next = [];
120
+ for (const parent of ring) {
121
+ const children = parent.node.children ?? [];
122
+ if (children.length === 0 || parent.depth + 1 > maxDepth) continue;
123
+ const values = children.map(valueOf);
124
+ const total = values.reduce((a, b) => a + b, 0);
125
+ const denominator = Math.max(parent.value, total);
126
+ const span = parent.x1 - parent.x0;
127
+ let cursor = parent.x0;
128
+ children.forEach((node, index) => {
129
+ const width = denominator > 0 ? span * values[index] / denominator : 0;
130
+ const entry = {
131
+ id: sunburstChildId(parent.id, node.item.key),
132
+ node,
133
+ parentId: parent.id,
134
+ depth: parent.depth + 1,
135
+ x0: cursor,
136
+ x1: cursor + width,
137
+ value: values[index],
138
+ index,
139
+ setSize: children.length,
140
+ hasChildren: sunburstHasChildren(node)
141
+ };
142
+ cursor += width;
143
+ out.set(entry.id, entry);
144
+ next.push(entry);
145
+ });
146
+ }
147
+ ring = next;
148
+ }
149
+ return out;
150
+ }
151
+ //#endregion
152
+ //#region src/sunburst-tree/geometry.ts
153
+ const TAU = Math.PI * 2;
154
+ /** Spans at least this close to a full turn are drawn as full rings. */
155
+ const FULL_TURN = 1 - 1e-6;
156
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
157
+ /** View that makes `zoom` the centre and its span the full circle. */
158
+ function sunburstViewFor(zoom) {
159
+ if (!zoom || zoom.x1 - zoom.x0 <= 0) return {
160
+ x0: 0,
161
+ x1: 1,
162
+ depth: 0
163
+ };
164
+ return {
165
+ x0: zoom.x0,
166
+ x1: zoom.x1,
167
+ depth: zoom.depth
168
+ };
169
+ }
170
+ /** Screen angles + relative ring for `node` seen through `view`. */
171
+ function projectArc(node, view) {
172
+ const span = view.x1 - view.x0;
173
+ return {
174
+ a0: clamp01((node.x0 - view.x0) / span),
175
+ a1: clamp01((node.x1 - view.x0) / span),
176
+ depth: node.depth - view.depth - 1
177
+ };
178
+ }
179
+ /**
180
+ * Radius of ring boundary `d`. Boundary `0` is the centre disc's edge;
181
+ * negative boundaries shrink linearly into the centre, so the zoom node
182
+ * (ring `-1`) is the centre disc and its ancestors collapse to nothing.
183
+ */
184
+ function ringBoundary(d, rings) {
185
+ return d >= 0 ? rings.center + d * rings.ring : Math.max(0, rings.center * (1 + d));
186
+ }
187
+ /** Inner/outer radius of an arc sitting in (possibly fractional) ring `depth`. */
188
+ function arcRadii(depth, rings) {
189
+ return {
190
+ inner: ringBoundary(depth, rings),
191
+ outer: ringBoundary(depth + 1, rings)
192
+ };
193
+ }
194
+ /** Split `outerRadius` into a centre disc and `maxDepth` equal rings. */
195
+ function computeRings(outerRadius, maxDepth, centerRatio) {
196
+ const radius = Math.max(0, outerRadius);
197
+ const center = radius * Math.min(.9, Math.max(0, centerRatio));
198
+ return {
199
+ center,
200
+ ring: (radius - center) / Math.max(1, maxDepth)
201
+ };
202
+ }
203
+ function point(angle, r) {
204
+ const theta = angle * TAU;
205
+ return `${(r * Math.sin(theta)).toFixed(2)} ${(-r * Math.cos(theta)).toFixed(2)}`;
206
+ }
207
+ function circle(r) {
208
+ const d = (r * 2).toFixed(2);
209
+ return `M0 ${(-r).toFixed(2)}a${r.toFixed(2)} ${r.toFixed(2)} 0 1 1 0 ${d}a${r.toFixed(2)} ${r.toFixed(2)} 0 1 1 0 -${d}Z`;
210
+ }
211
+ /**
212
+ * SVG path for an annular sector. Full turns become a ring (two circles,
213
+ * drawn with `fill-rule: evenodd`) or a disc when `inner` is `0`. Returns
214
+ * an empty string for degenerate arcs.
215
+ */
216
+ function arcPath(a0, a1, inner, outer) {
217
+ const span = a1 - a0;
218
+ if (span <= 0 || outer <= 0 || outer <= inner) return ``;
219
+ if (span >= FULL_TURN) return inner > 0 ? `${circle(outer)}${circle(inner)}` : circle(outer);
220
+ const large = span > .5 ? 1 : 0;
221
+ const o = outer.toFixed(2);
222
+ if (inner <= 0) return `M0 0L${point(a0, outer)}A${o} ${o} 0 ${large} 1 ${point(a1, outer)}Z`;
223
+ const i = inner.toFixed(2);
224
+ return `M${point(a0, outer)}A${o} ${o} 0 ${large} 1 ${point(a1, outer)}L${point(a1, inner)}A${i} ${i} 0 ${large} 0 ${point(a0, inner)}Z`;
225
+ }
226
+ /** On-screen length (px) of the outer edge of an arc. */
227
+ function arcLength(a0, a1, radius) {
228
+ return Math.max(0, a1 - a0) * TAU * radius;
229
+ }
230
+ /**
231
+ * Guide path for curved text: an arc of radius `r` from `a0` to `a1`,
232
+ * clockwise — or, when `flip`, counter-clockwise from `a1` to `a0` so text
233
+ * in the bottom half reads left-to-right rather than upside-down.
234
+ */
235
+ function labelArcPath(a0, a1, r, flip) {
236
+ const end = Math.min(a1, a0 + .9999);
237
+ const large = end - a0 > .5 ? 1 : 0;
238
+ const rr = r.toFixed(2);
239
+ return flip ? `M${point(end, r)}A${rr} ${rr} 0 ${large} 0 ${point(a0, r)}` : `M${point(a0, r)}A${rr} ${rr} 0 ${large} 1 ${point(end, r)}`;
240
+ }
241
+ function truncate(label, fits) {
242
+ return label.length <= fits ? label : `${label.slice(0, fits - 1)}…`;
243
+ }
244
+ /**
245
+ * Where to draw `label` inside an arc, or `undefined` if it can't fit
246
+ * legibly. Prefers tangential text (following the ring) when the arc is long
247
+ * enough — curved along the ring with `curved`, else straight — otherwise
248
+ * radial text truncated to the ring width. All are oriented to read upright.
249
+ */
250
+ function labelPlacement(visual, label, rings, fontSize, options = {}) {
251
+ if (visual.depth < 0 || label.length === 0) return void 0;
252
+ const { inner, outer } = arcRadii(visual.depth, rings);
253
+ const thickness = outer - inner;
254
+ const mid = (inner + outer) / 2;
255
+ const span = visual.a1 - visual.a0;
256
+ const chord = arcLength(visual.a0, visual.a1, mid);
257
+ const charWidth = fontSize * .6;
258
+ const pad = fontSize * .5;
259
+ const midAngle = (visual.a0 + visual.a1) / 2;
260
+ const degrees = midAngle * 360;
261
+ const bottomHalf = degrees > 90 && degrees < 270;
262
+ const lineFits = thickness >= fontSize * 1.3;
263
+ if (options.curved && lineFits) {
264
+ const fits = Math.floor((chord - pad) / charWidth);
265
+ if (fits >= Math.min(label.length, 4)) return {
266
+ kind: `curved`,
267
+ path: span >= FULL_TURN ? labelArcPath(.5, 1.5, mid, false) : labelArcPath(visual.a0, visual.a1, mid, bottomHalf),
268
+ text: truncate(label, fits)
269
+ };
270
+ }
271
+ const theta = midAngle * TAU;
272
+ const x = mid * Math.sin(theta);
273
+ const y = -mid * Math.cos(theta);
274
+ if (!options.curved && lineFits && span < .5 && chord >= label.length * charWidth + pad) return {
275
+ kind: `straight`,
276
+ x,
277
+ y,
278
+ rotate: degrees + (bottomHalf ? 180 : 0),
279
+ text: label
280
+ };
281
+ if (chord < fontSize * 1.1) return void 0;
282
+ const fits = Math.floor((thickness - pad) / charWidth);
283
+ if (fits < 3) return void 0;
284
+ return {
285
+ kind: `straight`,
286
+ x,
287
+ y,
288
+ rotate: degrees - 90 + (degrees > 180 ? 180 : 0),
289
+ text: truncate(label, fits)
290
+ };
291
+ }
292
+ /**
293
+ * Target hue offset (degrees) per arc: the angular midpoint of its
294
+ * first-ring ancestor (the zoom node's child it descends from), so the ring
295
+ * around the centre spans the colour wheel and everything outside it takes a
296
+ * tone of its ancestor's hue. Arcs outside the zoomed subtree get no entry.
297
+ */
298
+ function targetHues(layout, view, zoomId) {
299
+ const hues = /* @__PURE__ */ new Map();
300
+ for (const entry of layout.values()) {
301
+ if (entry.depth <= view.depth) continue;
302
+ if (entry.parentId === zoomId) {
303
+ const { a0, a1 } = projectArc(entry, view);
304
+ hues.set(entry.id, (a0 + a1) / 2 * 360);
305
+ continue;
306
+ }
307
+ const parentHue = entry.parentId === void 0 ? void 0 : hues.get(entry.parentId);
308
+ if (parentHue !== void 0) hues.set(entry.id, parentHue);
309
+ }
310
+ return hues;
311
+ }
312
+ /** Tone offset from sibling position, so neighbouring same-hue arcs differ. */
313
+ function siblingShade(index, setSize) {
314
+ return setSize <= 1 ? 0 : (index + .5) / setSize - .5;
315
+ }
316
+ //#endregion
317
+ //#region src/sunburst-tree/keyboard.ts
318
+ /**
319
+ * Id the cursor moves to for `key`, or `undefined` when the key isn't a
320
+ * navigation key or there's nowhere to go. With no current arc, any
321
+ * navigation key lands on the first arc.
322
+ */
323
+ function sunburstKeyTarget(arcs, currentId, key) {
324
+ if (arcs.length === 0) return void 0;
325
+ const current = currentId === void 0 ? void 0 : arcs.find((a) => a.id === currentId);
326
+ if (key === `Home`) return arcs[0].id;
327
+ if (key === `End`) return arcs.at(-1).id;
328
+ if (![
329
+ `ArrowLeft`,
330
+ `ArrowRight`,
331
+ `ArrowUp`,
332
+ `ArrowDown`
333
+ ].includes(key)) return void 0;
334
+ if (!current) return arcs[0].id;
335
+ switch (key) {
336
+ case `ArrowLeft`:
337
+ case `ArrowRight`: {
338
+ const siblings = arcs.filter((a) => a.parentId === current.parentId);
339
+ return siblings[(siblings.findIndex((a) => a.id === current.id) + (key === `ArrowRight` ? 1 : -1) + siblings.length) % siblings.length].id;
340
+ }
341
+ case `ArrowUp`: return arcs.find((a) => a.id === current.parentId)?.id;
342
+ default: return arcs.find((a) => a.parentId === current.id)?.id;
343
+ }
344
+ }
345
+ //#endregion
346
+ //#region src/sunburst-tree/styles.ts
347
+ /**
348
+ * Shadow-DOM styles for `ixfx-sunbursttree-list`.
349
+ *
350
+ * Arc colour is computed here, not in JS: each arc carries its (animated)
351
+ * `--arc-hue`, `--arc-depth`, `--arc-shade` and `--arc-centre`, and the
352
+ * `--sunbursttree-*` knobs turn those into a fill — so the palette is
353
+ * themeable without touching the component.
354
+ */
355
+ const sunburstTreeStyles = css`
356
+ :host {
357
+ --sunbursttree-hue-start: 250;
358
+ --sunbursttree-chroma: 0.14;
359
+ --sunbursttree-chroma-step: 0.015;
360
+ --sunbursttree-lightness: 60%;
361
+ --sunbursttree-lightness-step: 5%;
362
+ --sunbursttree-shade-step: 10%;
363
+ --sunbursttree-center-color: var(--surface-4);
364
+ --sunbursttree-center-text: var(--surface-4-text);
365
+ --sunbursttree-gap-color: var(--surface-2);
366
+ --sunbursttree-gap: 1px;
367
+ --sunbursttree-font-size: 11px;
368
+ --sunbursttree-label-lightness: 22%;
369
+
370
+ display: block;
371
+ position: relative;
372
+ min-height: 200px;
373
+ aspect-ratio: 1;
374
+ outline: none;
375
+ }
376
+
377
+ :host(:focus),
378
+ :host(:focus-visible) {
379
+ outline: none;
380
+ }
381
+
382
+ .sunburst {
383
+ display: block;
384
+ width: 100%;
385
+ height: 100%;
386
+ outline: none;
387
+ overflow: hidden;
388
+ font-size: var(--sunbursttree-font-size);
389
+ }
390
+
391
+ .arc {
392
+ --_l: clamp(
393
+ 10%,
394
+ calc(
395
+ var(--sunbursttree-lightness)
396
+ + var(--arc-depth, 0) * var(--sunbursttree-lightness-step)
397
+ + var(--arc-shade, 0) * var(--sunbursttree-shade-step)
398
+ + var(--_boost, 0%)
399
+ ),
400
+ 95%
401
+ );
402
+ --_c: max(0, calc(var(--sunbursttree-chroma) - var(--arc-depth, 0) * var(--sunbursttree-chroma-step)));
403
+ --_arc-color: oklch(var(--_l) var(--_c) calc(var(--sunbursttree-hue-start) + var(--arc-hue, 0)));
404
+
405
+ fill: color-mix(in oklch, var(--_arc-color), var(--sunbursttree-center-color) calc(var(--arc-centre, 0) * 100%));
406
+ fill-rule: evenodd;
407
+ stroke: var(--sunbursttree-gap-color);
408
+ stroke-width: var(--sunbursttree-gap);
409
+ stroke-linejoin: round;
410
+ cursor: pointer;
411
+ }
412
+
413
+ .arc.tickled {
414
+ --_boost: 10%;
415
+ }
416
+
417
+ .arc.selected {
418
+ stroke: var(--accent);
419
+ stroke-width: calc(var(--sunbursttree-gap) + 1.5px);
420
+ paint-order: fill;
421
+ }
422
+
423
+ .arc.filtered {
424
+ fill-opacity: 0.3;
425
+ }
426
+
427
+ .arc.loading {
428
+ cursor: progress;
429
+ }
430
+
431
+ .arc.exiting {
432
+ pointer-events: none;
433
+ }
434
+
435
+ .arc.center.leaf {
436
+ cursor: default;
437
+ }
438
+
439
+ .sunburst-label {
440
+ fill: oklch(var(--sunbursttree-label-lightness) 0.03 calc(var(--sunbursttree-hue-start) + var(--arc-hue, 0)));
441
+ pointer-events: none;
442
+ user-select: none;
443
+ dominant-baseline: central;
444
+ text-anchor: middle;
445
+ }
446
+
447
+ .center-readout {
448
+ fill: var(--sunbursttree-center-text);
449
+ pointer-events: none;
450
+ user-select: none;
451
+ text-anchor: middle;
452
+ dominant-baseline: central;
453
+ }
454
+
455
+ .center-readout .center-label {
456
+ font-weight: var(--font-weight-bold, 600);
457
+ }
458
+
459
+ .center-readout .center-meta {
460
+ fill-opacity: 0.75;
461
+ }
462
+
463
+ .empty-state {
464
+ padding: var(--space-l);
465
+ color: var(--surface-muted-text);
466
+ font-size: var(--text-s);
467
+ }
468
+ `;
469
+ //#endregion
470
+ //#region src/sunburst-tree/tween.ts
471
+ function easeInOutCubic(t) {
472
+ return t < .5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
473
+ }
474
+ function easeOutCubic(t) {
475
+ return 1 - (1 - t) ** 3;
476
+ }
477
+ function prefersReducedMotion() {
478
+ return typeof matchMedia === `function` && matchMedia(`(prefers-reduced-motion: reduce)`).matches;
479
+ }
480
+ const lerp = (a, b, t) => a + (b - a) * t;
481
+ /** Interpolate hue (degrees) along the shorter way round the wheel. */
482
+ function lerpHue(a, b, t) {
483
+ return a + (((b - a) % 360 + 540) % 360 - 180) * t;
484
+ }
485
+ function lerpVisual(a, b, t) {
486
+ return {
487
+ a0: lerp(a.a0, b.a0, t),
488
+ a1: lerp(a.a1, b.a1, t),
489
+ depth: lerp(a.depth, b.depth, t),
490
+ hue: lerpHue(a.hue, b.hue, t),
491
+ shade: lerp(a.shade, b.shade, t),
492
+ opacity: lerp(a.opacity, b.opacity, t)
493
+ };
494
+ }
495
+ function planTransition(options) {
496
+ const { current, targets, enter, exit } = options;
497
+ const from = /* @__PURE__ */ new Map();
498
+ const to = /* @__PURE__ */ new Map();
499
+ const exiting = /* @__PURE__ */ new Set();
500
+ for (const [id, target] of targets) {
501
+ from.set(id, current.get(id) ?? enter(id, target));
502
+ to.set(id, target);
503
+ }
504
+ for (const [id, shown] of current) {
505
+ if (targets.has(id)) continue;
506
+ from.set(id, shown);
507
+ to.set(id, exit(id, shown));
508
+ exiting.add(id);
509
+ }
510
+ return {
511
+ from,
512
+ to,
513
+ exiting
514
+ };
515
+ }
516
+ /** Displayed state at eased progress `t` (0–1). */
517
+ function sampleTransition(transition, t) {
518
+ const out = /* @__PURE__ */ new Map();
519
+ for (const [id, target] of transition.to) {
520
+ const start = transition.from.get(id) ?? target;
521
+ out.set(id, t >= 1 ? target : lerpVisual(start, target, t));
522
+ }
523
+ return out;
524
+ }
525
+ /** Settled state: the targets, without the arcs that animated out. */
526
+ function settleTransition(transition) {
527
+ const out = /* @__PURE__ */ new Map();
528
+ for (const [id, target] of transition.to) if (!transition.exiting.has(id)) out.set(id, target);
529
+ return out;
530
+ }
531
+ const EPSILON = 1e-4;
532
+ function sameVisual(a, b) {
533
+ return Math.abs(a.a0 - b.a0) < EPSILON && Math.abs(a.a1 - b.a1) < EPSILON && Math.abs(a.depth - b.depth) < EPSILON && Math.abs(lerpHue(a.hue, b.hue, 1) - a.hue) < EPSILON * 360 && Math.abs(a.shade - b.shade) < EPSILON && Math.abs(a.opacity - b.opacity) < EPSILON;
534
+ }
535
+ /** True when two target sets would render identically. */
536
+ function sameTargets(a, b) {
537
+ if (a.size !== b.size) return false;
538
+ for (const [id, va] of a) {
539
+ const vb = b.get(id);
540
+ if (!vb || !sameVisual(va, vb)) return false;
541
+ }
542
+ return true;
543
+ }
544
+ //#endregion
545
+ //#region src/sunburst-tree/sunbursttree-list.ts
546
+ const defaultFormatter = (value) => value.toLocaleString(void 0, { maximumFractionDigits: 2 });
547
+ let instanceCount = 0;
548
+ /** Size used before the element has been measured (and in DOM-less tests). */
549
+ const FALLBACK_SIZE = 320;
550
+ let SunburstTreeListElement = class SunburstTreeListElement extends LitElement {
551
+ constructor(..._args) {
552
+ super(..._args);
553
+ this.exclusivity = `none`;
554
+ this.selectionFilter = `all`;
555
+ this.interactionMode = `standard`;
556
+ this.maxDepth = 5;
557
+ this.centerRatio = .22;
558
+ this.minArcLength = 1.5;
559
+ this.disableLabels = false;
560
+ this.curvedLabels = false;
561
+ this.animationDuration = 600;
562
+ this.valueAccessor = defaultSunburstValueAccessor;
563
+ this.valueFormatter = defaultFormatter;
564
+ this._selectedNodes = /* @__PURE__ */ new Set();
565
+ this.disableKeyboardNavigation = false;
566
+ this.registry = void 0;
567
+ this._zoomId = void 0;
568
+ this._size = {
569
+ width: 0,
570
+ height: 0
571
+ };
572
+ this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
573
+ labelSelector: `.sunburst-label`,
574
+ highlightKey: `incr-search-highlight`
575
+ }) });
576
+ this.tickled = new TickledController(this, {
577
+ getItems: () => this.#visibleArcs().map((a) => ({
578
+ id: a.id,
579
+ enabled: true
580
+ })),
581
+ onStateChange: () => {
582
+ this.setAttribute(`data-input-mode`, this.tickled.inputMode);
583
+ },
584
+ onTickle: (id) => {
585
+ const entry = id === void 0 ? void 0 : this.#layout.get(id);
586
+ if (!entry) return;
587
+ this.dispatchEvent(new CustomEvent(`tickle`, {
588
+ detail: {
589
+ node: entry.node,
590
+ depth: entry.depth - 1
591
+ },
592
+ bubbles: true,
593
+ composed: true
594
+ }));
595
+ }
596
+ });
597
+ this._layoutDirty = true;
598
+ this._ticking = false;
599
+ this.#layout = /* @__PURE__ */ new Map();
600
+ this.#view = {
601
+ x0: 0,
602
+ x1: 1,
603
+ depth: 0
604
+ };
605
+ this.#rings = {
606
+ center: 0,
607
+ ring: 0
608
+ };
609
+ this.#targets = /* @__PURE__ */ new Map();
610
+ this.#displayed = /* @__PURE__ */ new Map();
611
+ this.#animStart = 0;
612
+ this.#easing = easeInOutCubic;
613
+ this.#fontSize = 11;
614
+ this.#labelIdPrefix = `sunburst-${++instanceCount}-label-`;
615
+ this.#pendingIds = /* @__PURE__ */ new Set();
616
+ this.#aborters = /* @__PURE__ */ new Map();
617
+ this.#tick = (now) => {
618
+ this.#raf = void 0;
619
+ const transition = this.#transition;
620
+ if (!transition) return;
621
+ const t = (now - this.#animStart) / Math.max(1, this.animationDuration);
622
+ if (t >= 1) {
623
+ this.#displayed = settleTransition(transition);
624
+ this.#transition = void 0;
625
+ } else {
626
+ this.#displayed = sampleTransition(transition, this.#easing(Math.max(0, t)));
627
+ this.#raf = requestAnimationFrame(this.#tick);
628
+ }
629
+ this._ticking = true;
630
+ try {
631
+ this.requestUpdate();
632
+ } finally {
633
+ this._ticking = false;
634
+ }
635
+ };
636
+ }
637
+ static {
638
+ this.shadowRootOptions = {
639
+ ...LitElement.shadowRootOptions,
640
+ delegatesFocus: true
641
+ };
642
+ }
643
+ get selectedNodes() {
644
+ return this._selectedNodes;
645
+ }
646
+ /** Current zoom root. `undefined` when showing the full tree. Pure view-state. */
647
+ get focusedNode() {
648
+ if (this._zoomId === void 0) return void 0;
649
+ return findSunburstNodeById(this.#effectiveRoot, this._zoomId);
650
+ }
651
+ #layout;
652
+ #view;
653
+ #rings;
654
+ #targets;
655
+ #displayed;
656
+ #transition;
657
+ #animStart;
658
+ #easing;
659
+ #raf;
660
+ #fontSize;
661
+ /** Unique per instance — guide-path ids are referenced by `<textPath href>`. */
662
+ #labelIdPrefix;
663
+ #filtered;
664
+ #modelUnsubscribe;
665
+ #pendingIds;
666
+ #aborters;
667
+ #anchorIndex;
668
+ #ownedRegistry;
669
+ #unregisterSearchCommands;
670
+ #resizeObserver;
671
+ get #effectiveRoot() {
672
+ return this.model?.root ?? this.root;
673
+ }
674
+ get #zoomEntryId() {
675
+ return this._zoomId ?? this.#effectiveRoot?.item.key;
676
+ }
677
+ /** Settled, interactive arcs (the centre excluded), in ring-major order. */
678
+ #visibleArcs() {
679
+ const out = [];
680
+ for (const [id, target] of this.#targets) {
681
+ const entry = this.#layout.get(id);
682
+ if (entry && target.depth >= 0) out.push(entry);
683
+ }
684
+ return out;
685
+ }
686
+ requestUpdate(...args) {
687
+ if (!this._ticking) this._layoutDirty = true;
688
+ super.requestUpdate(...args);
689
+ }
690
+ connectedCallback() {
691
+ super.connectedCallback();
692
+ this.setAttribute(`data-input-mode`, `mouse`);
693
+ const registry = this.#ownedRegistry = createCommandRegistry();
694
+ registry.register({
695
+ id: `select-all`,
696
+ label: `Select All`,
697
+ description: `Select all visible items`,
698
+ execute: () => this.selectAll()
699
+ });
700
+ registry.register({
701
+ id: `deselect-all`,
702
+ label: `Deselect All`,
703
+ description: `Clear all selection`,
704
+ execute: () => this.deselectAll()
705
+ });
706
+ registry.register({
707
+ id: `zoom-up`,
708
+ label: `Zoom Out One Level`,
709
+ description: `Zoom to the parent of the centre`,
710
+ execute: () => this.zoomUp()
711
+ });
712
+ this.registry = registry;
713
+ this.#syncSearchCommands();
714
+ if (typeof ResizeObserver !== `undefined`) {
715
+ this.#resizeObserver = new ResizeObserver(([entry]) => {
716
+ const { width, height } = entry.contentRect;
717
+ this.#fontSize = Number.parseFloat(getComputedStyle(this).getPropertyValue(`--sunbursttree-font-size`)) || 11;
718
+ if (width !== this._size.width || height !== this._size.height) this._size = {
719
+ width,
720
+ height
721
+ };
722
+ });
723
+ this.#resizeObserver.observe(this);
724
+ }
725
+ }
726
+ disconnectedCallback() {
727
+ super.disconnectedCallback();
728
+ this.#modelUnsubscribe?.();
729
+ this.#modelUnsubscribe = void 0;
730
+ for (const aborter of this.#aborters.values()) aborter.abort();
731
+ this.#aborters.clear();
732
+ this.#pendingIds.clear();
733
+ this.#resizeObserver?.disconnect();
734
+ this.#resizeObserver = void 0;
735
+ if (this.#raf !== void 0) cancelAnimationFrame(this.#raf);
736
+ this.#raf = void 0;
737
+ if (this.#transition) this.#displayed = settleTransition(this.#transition);
738
+ this.#transition = void 0;
739
+ if (this.#ownedRegistry) {
740
+ for (const id of [
741
+ `select-all`,
742
+ `deselect-all`,
743
+ `zoom-up`
744
+ ]) this.#ownedRegistry.unregister(id);
745
+ this.#ownedRegistry = void 0;
746
+ this.registry = void 0;
747
+ }
748
+ }
749
+ #effectiveSearchModes() {
750
+ const raw = this.searchModes?.trim();
751
+ if (raw === void 0) return [...LIST_SEARCH_MODES];
752
+ const requested = new Set(raw.split(/\s+/));
753
+ return LIST_SEARCH_MODES.filter((m) => requested.has(m));
754
+ }
755
+ #syncSearchCommands() {
756
+ if (!this.#ownedRegistry) return;
757
+ this.search.setModes(this.#effectiveSearchModes());
758
+ this.#unregisterSearchCommands?.();
759
+ this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
760
+ }
761
+ willUpdate(changed) {
762
+ if (changed.has(`root`) && changed.get(`root`) !== void 0) this.#remapSelection();
763
+ if (changed.has(`interactionMode`) && changed.get(`interactionMode`) !== void 0) {
764
+ this.#anchorIndex = void 0;
765
+ this.#clearSelectionInternal();
766
+ }
767
+ if (this._layoutDirty) {
768
+ this._layoutDirty = false;
769
+ this.#retarget();
770
+ }
771
+ }
772
+ updated(changed) {
773
+ if (changed.has(`model`)) {
774
+ this.#modelUnsubscribe?.();
775
+ this.#modelUnsubscribe = this.model?._subscribe(() => this.requestUpdate());
776
+ }
777
+ if (changed.has(`searchModes`)) this.#syncSearchCommands();
778
+ this.#ensureVisibleChildren();
779
+ }
780
+ #outerRadius() {
781
+ const width = this._size.width || FALLBACK_SIZE;
782
+ const height = this._size.height || FALLBACK_SIZE;
783
+ return Math.max(0, Math.min(width, height) / 2 - 2);
784
+ }
785
+ /** Recompute the layout and start animating towards it (if anything changed). */
786
+ #retarget() {
787
+ const root = this.#effectiveRoot;
788
+ const prevLayout = this.#layout;
789
+ const prevView = this.#view;
790
+ const maxDepth = Math.max(1, Math.floor(this.maxDepth));
791
+ const zoomDepth = this._zoomId === void 0 ? 0 : sunburstIdToPath(this._zoomId).length - 1;
792
+ const layout = computeSunburstLayout(root, {
793
+ valueAccessor: this.valueAccessor,
794
+ maxDepth: Math.max(zoomDepth, prevView.depth) + maxDepth
795
+ });
796
+ if (this._zoomId !== void 0 && !layout.has(this._zoomId)) this._zoomId = void 0;
797
+ const zoomId = this.#zoomEntryId;
798
+ const view = sunburstViewFor(zoomId === void 0 ? void 0 : layout.get(zoomId));
799
+ const rings = computeRings(this.#outerRadius(), maxDepth, this.centerRatio);
800
+ const targets = this.#computeTargets(layout, view, rings, zoomId, maxDepth);
801
+ this.#layout = layout;
802
+ this.#view = view;
803
+ this.#rings = rings;
804
+ if (sameTargets(targets, this.#targets)) return;
805
+ this.#targets = targets;
806
+ const transition = planTransition({
807
+ current: this.#displayed,
808
+ targets,
809
+ enter: (id, target) => {
810
+ const before = prevLayout.get(id);
811
+ if (before) {
812
+ const p = projectArc(before, prevView);
813
+ return {
814
+ ...target,
815
+ ...p,
816
+ opacity: 0
817
+ };
818
+ }
819
+ return {
820
+ ...target,
821
+ a1: target.a0
822
+ };
823
+ },
824
+ exit: (id, shown) => {
825
+ const now = layout.get(id);
826
+ if (now) return {
827
+ ...shown,
828
+ ...projectArc(now, view),
829
+ opacity: 0
830
+ };
831
+ const mid = (shown.a0 + shown.a1) / 2;
832
+ return {
833
+ ...shown,
834
+ a0: mid,
835
+ a1: mid,
836
+ opacity: 0
837
+ };
838
+ }
839
+ });
840
+ this.#startTransition(transition);
841
+ }
842
+ #computeTargets(layout, view, rings, zoomId, maxDepth) {
843
+ const targets = /* @__PURE__ */ new Map();
844
+ if (zoomId === void 0) return targets;
845
+ const hues = targetHues(layout, view, zoomId);
846
+ for (const entry of layout.values()) {
847
+ const p = projectArc(entry, view);
848
+ if (p.depth < -1 || p.depth >= maxDepth) continue;
849
+ if (p.depth === -1 ? entry.id !== zoomId : !hues.has(entry.id)) continue;
850
+ if (p.depth >= 0 && arcLength(p.a0, p.a1, arcRadii(p.depth, rings).outer) < this.minArcLength) continue;
851
+ targets.set(entry.id, {
852
+ ...p,
853
+ hue: hues.get(entry.id) ?? this.#displayed.get(entry.id)?.hue ?? 0,
854
+ shade: siblingShade(entry.index, entry.setSize),
855
+ opacity: 1
856
+ });
857
+ }
858
+ return targets;
859
+ }
860
+ #startTransition(transition) {
861
+ if ((prefersReducedMotion() ? 0 : this.animationDuration) <= 0 || !this.isConnected) {
862
+ this.#transition = void 0;
863
+ this.#displayed = settleTransition(transition);
864
+ return;
865
+ }
866
+ this.#easing = this.#transition ? easeOutCubic : easeInOutCubic;
867
+ this.#transition = transition;
868
+ this.#displayed = sampleTransition(transition, 0);
869
+ this.#animStart = performance.now();
870
+ this.#raf ??= requestAnimationFrame(this.#tick);
871
+ }
872
+ #tick;
873
+ /** After `root` is replaced, carry the selection over by key path. */
874
+ #remapSelection() {
875
+ if (this._selectedNodes.size === 0) return;
876
+ const idOf = /* @__PURE__ */ new Map();
877
+ for (const entry of this.#layout.values()) idOf.set(entry.node, entry.id);
878
+ const root = this.#effectiveRoot;
879
+ const remap = (node) => {
880
+ const id = node === void 0 ? void 0 : idOf.get(node);
881
+ return id === void 0 ? void 0 : findSunburstNodeById(root, id);
882
+ };
883
+ this._selectedNodes = new Set([...this._selectedNodes].map(remap).filter((n) => n !== void 0));
884
+ this.selectedNode = remap(this.selectedNode) ?? [...this._selectedNodes].at(-1);
885
+ this.#anchorIndex = void 0;
886
+ }
887
+ #ensureVisibleChildren() {
888
+ if (!this.model && !this.loadChildren) return;
889
+ for (const id of this.#targets.keys()) {
890
+ const entry = this.#layout.get(id);
891
+ if (!entry || entry.node.children !== void 0 || entry.node.item.isLeaf === true || this.#pendingIds.has(id)) continue;
892
+ this.#loadChildren(entry);
893
+ }
894
+ }
895
+ async #loadChildren(entry) {
896
+ const { id, node } = entry;
897
+ const depth = entry.depth - 1;
898
+ this.#pendingIds.add(id);
899
+ const controller = new AbortController();
900
+ this.#aborters.set(id, controller);
901
+ try {
902
+ const children = this.model ? await this.model.getChildren(node, depth, controller.signal) : [...await collectDataResult(this.loadChildren({
903
+ node,
904
+ depth
905
+ }, controller.signal), controller.signal)];
906
+ if (controller.signal.aborted) return;
907
+ node.children = children;
908
+ } catch {
909
+ if (controller.signal.aborted) return;
910
+ node.children = [];
911
+ } finally {
912
+ this.#pendingIds.delete(id);
913
+ this.#aborters.delete(id);
914
+ if (!controller.signal.aborted) this.requestUpdate();
915
+ }
916
+ }
917
+ #canSelect(node) {
918
+ if (this.selectionFilter === `none`) return false;
919
+ if (this.selectionFilter === `leaf`) return node.item.isLeaf === true;
920
+ if (this.selectionFilter === `branch`) return node.item.isLeaf === false || node.children !== void 0 && node.children.length > 0;
921
+ return true;
922
+ }
923
+ #clearSelectionInternal() {
924
+ this._selectedNodes = /* @__PURE__ */ new Set();
925
+ this.selectedNode = void 0;
926
+ }
927
+ #commitSelection(next, primary = [...next].at(-1)) {
928
+ const previous = this._selectedNodes;
929
+ this._selectedNodes = next;
930
+ this.selectedNode = primary;
931
+ this.dispatchEvent(new CustomEvent(`select`, {
932
+ detail: {
933
+ selected: new Set(next),
934
+ previous
935
+ },
936
+ bubbles: true,
937
+ composed: true
938
+ }));
939
+ }
940
+ select(node) {
941
+ if (this.#canSelect(node)) this.#commitSelection(/* @__PURE__ */ new Set([node]), node);
942
+ }
943
+ deselect(node) {
944
+ if (!this._selectedNodes.has(node)) return;
945
+ const next = new Set(this._selectedNodes);
946
+ next.delete(node);
947
+ this.#commitSelection(next, this.selectedNode === node ? [...next].at(-1) : this.selectedNode);
948
+ }
949
+ clearSelection() {
950
+ if (this._selectedNodes.size > 0) this.#commitSelection(/* @__PURE__ */ new Set());
951
+ }
952
+ selectAll() {
953
+ this.#commitSelection(/* @__PURE__ */ new Set([...this._selectedNodes, ...this.#visibleArcs().map((a) => a.node).filter((n) => this.#canSelect(n))]));
954
+ }
955
+ deselectAll() {
956
+ this.#commitSelection(/* @__PURE__ */ new Set());
957
+ }
958
+ selectMany(nodes) {
959
+ this.#commitSelection(new Set([...nodes].filter((n) => this.#canSelect(n))));
960
+ }
961
+ #idOf(node) {
962
+ for (const entry of this.#layout.values()) if (entry.node === node) return entry.id;
963
+ return findSunburstId(this.#effectiveRoot, (n) => n === node);
964
+ }
965
+ #setZoom(id) {
966
+ const zoomId = id === this.#effectiveRoot?.item.key ? void 0 : id;
967
+ if (zoomId === this._zoomId) return;
968
+ const previousId = this._zoomId;
969
+ const previous = this.focusedNode;
970
+ this._zoomId = zoomId;
971
+ this.tickled.clearTickled();
972
+ const node = this.focusedNode;
973
+ const depthOf = (key) => key === void 0 ? -1 : sunburstIdToPath(key).length - 2;
974
+ this.dispatchEvent(new CustomEvent(`zoom`, {
975
+ detail: {
976
+ node,
977
+ depth: depthOf(zoomId)
978
+ },
979
+ bubbles: true,
980
+ composed: true
981
+ }));
982
+ if (zoomId !== void 0 && (previousId === void 0 || isSunburstAncestorId(previousId, zoomId)) && node) this.dispatchEvent(new CustomEvent(`expand`, {
983
+ detail: {
984
+ node,
985
+ depth: depthOf(zoomId)
986
+ },
987
+ bubbles: true,
988
+ composed: true
989
+ }));
990
+ else if (previous) this.dispatchEvent(new CustomEvent(`collapse`, {
991
+ detail: {
992
+ node: previous,
993
+ depth: depthOf(previousId)
994
+ },
995
+ bubbles: true,
996
+ composed: true
997
+ }));
998
+ }
999
+ /**
1000
+ * Zoom so `node` becomes the centre, its children the first ring. Fires
1001
+ * `zoom` and `expand` (or `collapse` when `node` is an ancestor of the
1002
+ * current centre). Pure view-state — `root` is never mutated.
1003
+ */
1004
+ zoomTo(node) {
1005
+ const id = this.#idOf(node);
1006
+ if (id !== void 0) this.#setZoom(id);
1007
+ }
1008
+ /** Zoom out one level: the centre's parent becomes the centre. */
1009
+ zoomUp() {
1010
+ if (this._zoomId !== void 0) this.#setZoom(sunburstParentId(this._zoomId));
1011
+ }
1012
+ /** Zoom all the way out to the full tree. */
1013
+ zoomOut() {
1014
+ this.#setZoom(void 0);
1015
+ }
1016
+ #canZoom(entry) {
1017
+ if (entry.x1 - entry.x0 <= 0 || entry.node.item.isLeaf === true) return false;
1018
+ if (entry.node.children !== void 0) return entry.node.children.length > 0;
1019
+ return this.model !== void 0 || this.loadChildren !== void 0;
1020
+ }
1021
+ /** Zoom so `id` is in the first ring (its parent at the centre). */
1022
+ #reveal(id) {
1023
+ const root = this.#effectiveRoot;
1024
+ const node = findSunburstNodeById(root, id);
1025
+ if (!node) return void 0;
1026
+ const parent = sunburstParentId(id);
1027
+ if (parent !== void 0) this.#setZoom(parent);
1028
+ this.tickled.setTickled(id);
1029
+ return node;
1030
+ }
1031
+ /** Zoom to reveal the node at `keys` (path from the root's children), then select it. */
1032
+ navigateTo(keys) {
1033
+ const root = this.#effectiveRoot;
1034
+ if (!root || keys.length === 0) return false;
1035
+ const node = this.#reveal(sunburstPathToId([root.item.key, ...keys]));
1036
+ if (!node) return false;
1037
+ this.select(node);
1038
+ return true;
1039
+ }
1040
+ /** Zoom + select the first node (depth-first) matching `predicate`. */
1041
+ navigateToNode(predicate) {
1042
+ const root = this.#effectiveRoot;
1043
+ const id = findSunburstId(root, (n) => n !== root && predicate(n));
1044
+ if (id === void 0) return false;
1045
+ return this.navigateTo(sunburstIdToPath(id).slice(1));
1046
+ }
1047
+ /** Zoom to reveal the first node with `key` and move the cursor to it, without selecting. */
1048
+ revealKey(key) {
1049
+ const root = this.#effectiveRoot;
1050
+ const id = findSunburstId(root, (n) => n !== root && n.item.key === key);
1051
+ return id !== void 0 && this.#reveal(id) !== void 0;
1052
+ }
1053
+ /** Arcs currently drawn (centre excluded), ring-major, depth from the root's children. */
1054
+ getNodes() {
1055
+ return this.#visibleArcs().map((a) => ({
1056
+ node: a.node,
1057
+ depth: a.depth - 1
1058
+ }));
1059
+ }
1060
+ #isFilteredOut(id) {
1061
+ const predicate = this.filterPredicate;
1062
+ if (!predicate) return false;
1063
+ if (this.#filtered?.predicate !== predicate || this.#filtered.layout !== this.#layout) {
1064
+ const matches = (node, depth) => predicate(node, depth) || (node.children?.some((c) => matches(c, depth + 1)) ?? false);
1065
+ const ids = /* @__PURE__ */ new Set();
1066
+ for (const entry of this.#layout.values()) if (entry.depth > 0 && !matches(entry.node, entry.depth - 1)) ids.add(entry.id);
1067
+ this.#filtered = {
1068
+ predicate,
1069
+ layout: this.#layout,
1070
+ ids
1071
+ };
1072
+ }
1073
+ return this.#filtered.ids.has(id);
1074
+ }
1075
+ #handleArcClick(entry, event) {
1076
+ this.dispatchEvent(new CustomEvent(`item-click`, {
1077
+ detail: {
1078
+ node: entry.node,
1079
+ depth: entry.depth - 1
1080
+ },
1081
+ bubbles: true,
1082
+ composed: true
1083
+ }));
1084
+ const { node } = entry;
1085
+ const arcs = this.#visibleArcs();
1086
+ const index = arcs.findIndex((a) => a.id === entry.id);
1087
+ if (this.interactionMode === `standard` && (event.ctrlKey || event.metaKey)) {
1088
+ if (this._selectedNodes.has(node)) this.deselect(node);
1089
+ else if (this.#canSelect(node)) this.#commitSelection(/* @__PURE__ */ new Set([...this._selectedNodes, node]), node);
1090
+ this.#anchorIndex = index;
1091
+ return;
1092
+ }
1093
+ if (this.interactionMode === `standard` && event.shiftKey && this.#anchorIndex !== void 0) {
1094
+ const from = Math.min(this.#anchorIndex, index);
1095
+ const to = Math.max(this.#anchorIndex, index);
1096
+ this.#commitSelection(new Set(arcs.slice(from, to + 1).map((a) => a.node).filter((n) => this.#canSelect(n))));
1097
+ return;
1098
+ }
1099
+ if (this.interactionMode !== `manual`) {
1100
+ this.select(node);
1101
+ this.#anchorIndex = index;
1102
+ }
1103
+ if (this.#canZoom(entry)) this.#setZoom(entry.id);
1104
+ else this.tickled.setTickled(entry.id);
1105
+ }
1106
+ #handleArcDblClick(entry) {
1107
+ const detail = {
1108
+ node: entry.node,
1109
+ depth: entry.depth - 1
1110
+ };
1111
+ this.dispatchEvent(new CustomEvent(`item-dblclick`, {
1112
+ detail,
1113
+ bubbles: true,
1114
+ composed: true
1115
+ }));
1116
+ this.dispatchEvent(new CustomEvent(`activate`, {
1117
+ detail,
1118
+ bubbles: true,
1119
+ composed: true
1120
+ }));
1121
+ }
1122
+ #handleKeyDown(event) {
1123
+ if (this.disableKeyboardNavigation) return;
1124
+ if (event.key === `Escape` || event.key === `Backspace`) {
1125
+ if (this._zoomId !== void 0) {
1126
+ event.preventDefault();
1127
+ this.zoomUp();
1128
+ }
1129
+ return;
1130
+ }
1131
+ const arcs = this.#visibleArcs();
1132
+ const current = this.tickled.tickledId;
1133
+ if (event.key === `Enter`) {
1134
+ const entry = current === void 0 ? void 0 : this.#layout.get(current);
1135
+ if (!entry) return;
1136
+ event.preventDefault();
1137
+ if (this.interactionMode !== `manual`) this.select(entry.node);
1138
+ this.dispatchEvent(new CustomEvent(`activate`, {
1139
+ detail: {
1140
+ node: entry.node,
1141
+ depth: entry.depth - 1
1142
+ },
1143
+ bubbles: true,
1144
+ composed: true
1145
+ }));
1146
+ if (this.#canZoom(entry)) this.#setZoom(entry.id);
1147
+ return;
1148
+ }
1149
+ const target = sunburstKeyTarget(arcs, current, event.key);
1150
+ if (target === void 0) return;
1151
+ event.preventDefault();
1152
+ this.tickled.setInputMode(`keyboard`);
1153
+ this.tickled.setTickled(target);
1154
+ const entry = this.#layout.get(target);
1155
+ if (entry && this.interactionMode === `implicit`) this.select(entry.node);
1156
+ }
1157
+ #renderArc(id, visual, zoomId) {
1158
+ const entry = this.#layout.get(id);
1159
+ const { inner, outer } = arcRadii(visual.depth, this.#rings);
1160
+ const d = arcPath(visual.a0, visual.a1, inner, outer);
1161
+ const exiting = !this.#targets.has(id);
1162
+ const centre = id === zoomId;
1163
+ const style = `--arc-hue:${visual.hue.toFixed(2)};--arc-depth:${Math.max(0, visual.depth).toFixed(3)};--arc-shade:${visual.shade.toFixed(3)};--arc-centre:${Math.min(1, Math.max(0, -visual.depth)).toFixed(3)};opacity:${visual.opacity.toFixed(3)}`;
1164
+ if (!entry || exiting) return svg`<path class="arc exiting" d=${d} style=${style} aria-hidden="true"></path>`;
1165
+ if (centre) {
1166
+ const canGoUp = this._zoomId !== void 0;
1167
+ return svg`<path
1168
+ class="arc center ${canGoUp ? `` : `leaf`}"
1169
+ part="center"
1170
+ d=${d}
1171
+ style=${style}
1172
+ role=${canGoUp ? `button` : `presentation`}
1173
+ aria-label=${canGoUp ? `Zoom out from ${entry.node.item.label}` : nothing}
1174
+ @click=${() => this.zoomUp()}
1175
+ @pointerenter=${() => this.tickled.clearTickled()}
1176
+ ></path>`;
1177
+ }
1178
+ const { node } = entry;
1179
+ const selected = this._selectedNodes.has(node);
1180
+ const tickled = this.tickled.isTickled(id);
1181
+ const classes = [
1182
+ `arc`,
1183
+ selected ? `selected` : ``,
1184
+ tickled ? `tickled` : ``,
1185
+ this.#isFilteredOut(id) ? `filtered` : ``,
1186
+ this.#pendingIds.has(id) ? `loading` : ``
1187
+ ].join(` `);
1188
+ return svg`<path
1189
+ class=${classes}
1190
+ part="arc"
1191
+ d=${d}
1192
+ style=${style}
1193
+ data-id=${id}
1194
+ data-key=${node.item.key}
1195
+ role="treeitem"
1196
+ aria-level=${entry.depth}
1197
+ aria-selected=${selected}
1198
+ aria-setsize=${entry.setSize}
1199
+ aria-posinset=${entry.index + 1}
1200
+ aria-label=${node.item.label}
1201
+ @click=${(e) => this.#handleArcClick(entry, e)}
1202
+ @dblclick=${() => this.#handleArcDblClick(entry)}
1203
+ @pointerenter=${() => this.tickled.handlePointerEnter(id)}
1204
+ ></path>`;
1205
+ }
1206
+ #renderLabels() {
1207
+ if (this.disableLabels) return nothing;
1208
+ const labels = [];
1209
+ const guides = [];
1210
+ for (const [id, visual] of this.#displayed) {
1211
+ const entry = this.#layout.get(id);
1212
+ if (!entry || visual.opacity < .05) continue;
1213
+ const place = labelPlacement(visual, entry.node.item.label, this.#rings, this.#fontSize, { curved: this.curvedLabels });
1214
+ if (!place) continue;
1215
+ const style = `--arc-hue:${visual.hue.toFixed(2)};opacity:${visual.opacity.toFixed(3)}`;
1216
+ if (place.kind === `curved`) {
1217
+ const guideId = `${this.#labelIdPrefix}${guides.length}`;
1218
+ guides.push(svg`<path id=${guideId} d=${place.path}></path>`);
1219
+ labels.push(svg`<text class="sunburst-label curved" part="label" style=${style}
1220
+ ><textPath href="#${guideId}" startOffset="50%">${place.text}</textPath></text>`);
1221
+ } else labels.push(svg`<text
1222
+ class="sunburst-label"
1223
+ part="label"
1224
+ transform="translate(${place.x.toFixed(2)} ${place.y.toFixed(2)}) rotate(${place.rotate.toFixed(2)})"
1225
+ style=${style}
1226
+ >${place.text}</text>`);
1227
+ }
1228
+ return svg`<defs>${guides}</defs>${labels}`;
1229
+ }
1230
+ /** Label, value and share of the centre (or of the tickled arc). */
1231
+ #renderReadout(zoomId) {
1232
+ const zoom = zoomId === void 0 ? void 0 : this.#layout.get(zoomId);
1233
+ if (!zoom) return nothing;
1234
+ const tickledId = this.tickled.tickledId;
1235
+ const tickled = tickledId === void 0 ? void 0 : this.#layout.get(tickledId);
1236
+ const shown = tickled ?? zoom;
1237
+ const maxChars = Math.max(4, Math.floor(this.#rings.center * 1.7 / (this.#fontSize * .6)));
1238
+ const label = shown.node.item.label;
1239
+ const text = label.length <= maxChars ? label : `${label.slice(0, maxChars - 1)}…`;
1240
+ const share = tickled && zoom.value > 0 ? ` · ${(tickled.value / zoom.value * 100).toFixed(tickled.value / zoom.value < .1 ? 1 : 0)}%` : ``;
1241
+ const line = this.#fontSize * 1.3;
1242
+ return svg`<g class="center-readout" part="center-readout" aria-hidden="true">
1243
+ <text class="center-label" y=${(-line / 2).toFixed(2)}>${text}</text>
1244
+ <text class="center-meta" y=${(line / 2).toFixed(2)}>${this.valueFormatter(shown.value, shown.node)}${share}</text>
1245
+ </g>`;
1246
+ }
1247
+ render() {
1248
+ if (!this.#effectiveRoot) return html`<div class="empty-state">No data</div>${this.search.renderField()}`;
1249
+ const width = this._size.width || FALLBACK_SIZE;
1250
+ const height = this._size.height || FALLBACK_SIZE;
1251
+ const zoomId = this.#zoomEntryId;
1252
+ return html`
1253
+ <svg
1254
+ class="sunburst"
1255
+ viewBox="${-width / 2} ${-height / 2} ${width} ${height}"
1256
+ role="tree"
1257
+ tabindex="0"
1258
+ aria-label=${this.#effectiveRoot.item.label}
1259
+ data-input-mode=${this.tickled.inputMode}
1260
+ @keydown=${(e) => this.#handleKeyDown(e)}
1261
+ @pointerleave=${() => this.tickled.clearTickled()}
1262
+ >
1263
+ <g class="arcs">${repeat(this.#displayed, ([id]) => id, ([id, visual]) => this.#renderArc(id, visual, zoomId))}</g>
1264
+ <g class="labels">${this.#renderLabels()}</g>
1265
+ ${this.#renderReadout(zoomId)}
1266
+ </svg>
1267
+ ${this.search.renderField()}
1268
+ `;
1269
+ }
1270
+ static {
1271
+ this.styles = [
1272
+ themeFallbacks,
1273
+ listSearchStyles,
1274
+ sunburstTreeStyles
1275
+ ];
1276
+ }
1277
+ };
1278
+ __decorate([property({ type: Object })], SunburstTreeListElement.prototype, "root", void 0);
1279
+ __decorate([property({ attribute: false })], SunburstTreeListElement.prototype, "model", void 0);
1280
+ __decorate([property({ attribute: false })], SunburstTreeListElement.prototype, "loadChildren", void 0);
1281
+ __decorate([property({
1282
+ reflect: true,
1283
+ attribute: `exclusivity`
1284
+ })], SunburstTreeListElement.prototype, "exclusivity", void 0);
1285
+ __decorate([property({
1286
+ reflect: true,
1287
+ attribute: `selection-filter`
1288
+ })], SunburstTreeListElement.prototype, "selectionFilter", void 0);
1289
+ __decorate([property({
1290
+ type: String,
1291
+ attribute: `interaction-mode`
1292
+ })], SunburstTreeListElement.prototype, "interactionMode", void 0);
1293
+ __decorate([property({
1294
+ type: Number,
1295
+ attribute: `max-depth`
1296
+ })], SunburstTreeListElement.prototype, "maxDepth", void 0);
1297
+ __decorate([property({
1298
+ type: Number,
1299
+ attribute: `center-ratio`
1300
+ })], SunburstTreeListElement.prototype, "centerRatio", void 0);
1301
+ __decorate([property({
1302
+ type: Number,
1303
+ attribute: `min-arc-length`
1304
+ })], SunburstTreeListElement.prototype, "minArcLength", void 0);
1305
+ __decorate([property({
1306
+ type: Boolean,
1307
+ attribute: `disable-labels`
1308
+ })], SunburstTreeListElement.prototype, "disableLabels", void 0);
1309
+ __decorate([property({
1310
+ type: Boolean,
1311
+ attribute: `curved-labels`,
1312
+ reflect: true
1313
+ })], SunburstTreeListElement.prototype, "curvedLabels", void 0);
1314
+ __decorate([property({
1315
+ type: Number,
1316
+ attribute: `animation-duration`
1317
+ })], SunburstTreeListElement.prototype, "animationDuration", void 0);
1318
+ __decorate([property({ attribute: false })], SunburstTreeListElement.prototype, "valueAccessor", void 0);
1319
+ __decorate([property({ attribute: false })], SunburstTreeListElement.prototype, "valueFormatter", void 0);
1320
+ __decorate([property({ attribute: false })], SunburstTreeListElement.prototype, "selectedNode", void 0);
1321
+ __decorate([state()], SunburstTreeListElement.prototype, "_selectedNodes", void 0);
1322
+ __decorate([property({ attribute: false })], SunburstTreeListElement.prototype, "filterPredicate", void 0);
1323
+ __decorate([property({
1324
+ type: Boolean,
1325
+ attribute: `disable-keyboard-navigation`
1326
+ })], SunburstTreeListElement.prototype, "disableKeyboardNavigation", void 0);
1327
+ __decorate([property({ attribute: false })], SunburstTreeListElement.prototype, "registry", void 0);
1328
+ __decorate([property({
1329
+ type: String,
1330
+ attribute: `search-modes`,
1331
+ reflect: true
1332
+ })], SunburstTreeListElement.prototype, "searchModes", void 0);
1333
+ __decorate([state()], SunburstTreeListElement.prototype, "_zoomId", void 0);
1334
+ __decorate([state()], SunburstTreeListElement.prototype, "_size", void 0);
1335
+ SunburstTreeListElement = __decorate([safeCustomElement(`ixfx-sunbursttree-list`)], SunburstTreeListElement);
1336
+ //#endregion
1337
+ export { findSunburstId as a, sunburstChildId as c, sunburstParentId as d, sunburstPathToId as f, defaultSunburstValueAccessor as i, sunburstHasChildren as l, computeSunburstLayout as n, findSunburstNodeById as o, SUNBURST_PATH_SEPARATOR as p, createValueResolver as r, isSunburstAncestorId as s, SunburstTreeListElement as t, sunburstIdToPath as u };