@ixfx/components 0.5.16 → 0.5.17

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.
@@ -0,0 +1,1149 @@
1
+ import { n as tickledStyles, r as TickledController, t as tickledItemStyles } from "./tickled-styles-fNDdqf6l.js";
2
+ import { t as themeFallbacks } from "./fallbacks-DPgPEqo8.js";
3
+ import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
4
+ import "./interaction-D6efrBLo.js";
5
+ import "./menu-item-BYRvfyXK.js";
6
+ import { t as collectDataResult } from "./data-provider-35TyWclX.js";
7
+ import "./narrowed-text.js";
8
+ import { LitElement, css, html, nothing, unsafeCSS } from "lit";
9
+ import { property, state } from "lit/decorators.js";
10
+ //#region src/styles/scroll-fade.ts
11
+ /**
12
+ * Zero-JS scroll edge fade.
13
+ *
14
+ * A CSS alpha mask driven by a scroll-linked animation (`scroll-timeline`),
15
+ * rather than a JS scroll/resize listener - so it can never go stale after
16
+ * a resize (widening a scroller so content fully fits clears the fade
17
+ * immediately, with no JS involved), and it fades the whole element
18
+ * including its children, not just a background layer that an opaque
19
+ * item background would hide.
20
+ *
21
+ * Apply the returned `.scroll-fade-x` / `.scroll-fade-y` class to
22
+ * whichever element actually has `overflow` scrolling on that axis.
23
+ *
24
+ * CSS variables:
25
+ * - `--scroll-fade-width` (default `30px`): how far the fade extends from each edge.
26
+ *
27
+ * ```ts
28
+ * import { scrollFadeStyles } from '../styles/scroll-fade.js';
29
+ *
30
+ * static styles = [scrollFadeStyles(`x`), css`...`];
31
+ * ```
32
+ * ```html
33
+ * <div class="scroll-fade-x" style="overflow-x: scroll">...</div>
34
+ * ```
35
+ */
36
+ function scrollFadeStyles(axis) {
37
+ const className = `scroll-fade-${axis}`;
38
+ const start = `--${className}-start`;
39
+ const end = `--${className}-end`;
40
+ const timeline = `--${className}-timeline`;
41
+ const keyframesName = `${className}-fade`;
42
+ const gradientDirection = axis === `x` ? `to right` : `to bottom`;
43
+ return css`
44
+ @property ${unsafeCSS(start)} {
45
+ syntax: '<length>';
46
+ inherits: false;
47
+ initial-value: 0px;
48
+ }
49
+
50
+ @property ${unsafeCSS(end)} {
51
+ syntax: '<length>';
52
+ inherits: false;
53
+ initial-value: 0px;
54
+ }
55
+
56
+ @keyframes ${unsafeCSS(keyframesName)} {
57
+ 0% { ${unsafeCSS(start)}: 0px; }
58
+ 10%, 100% { ${unsafeCSS(start)}: var(--scroll-fade-width, 30px); }
59
+ 0%, 90% { ${unsafeCSS(end)}: var(--scroll-fade-width, 30px); }
60
+ 100% { ${unsafeCSS(end)}: 0px; }
61
+ }
62
+
63
+ .${unsafeCSS(className)} {
64
+ scroll-timeline: ${unsafeCSS(timeline)} ${unsafeCSS(axis)};
65
+ animation: ${unsafeCSS(keyframesName)};
66
+ animation-timeline: ${unsafeCSS(timeline)};
67
+ mask-image: linear-gradient(${unsafeCSS(gradientDirection)},
68
+ transparent,
69
+ black var(${unsafeCSS(start)}, 0px) calc(100% - var(${unsafeCSS(end)}, 0px)),
70
+ transparent
71
+ );
72
+ }
73
+ `;
74
+ }
75
+ //#endregion
76
+ //#region src/crumbs/crumb-navigation.ts
77
+ let CrumbNavigationElement = class CrumbNavigationElement extends LitElement {
78
+ constructor(..._args) {
79
+ super(..._args);
80
+ this._selectedNodes = /* @__PURE__ */ new Set();
81
+ this.exclusivity = `none`;
82
+ this.selectionFilter = `leaf`;
83
+ this._openIdx = -1;
84
+ this._isFocused = false;
85
+ this._menuOpen = false;
86
+ this._focusOnNarrowedText = false;
87
+ this.inputMode = ``;
88
+ this.noTruncate = false;
89
+ this.showSuggestions = false;
90
+ this.leadingCaret = false;
91
+ this.caretEmbedded = true;
92
+ this.hideRoot = false;
93
+ this.#previousSelectionKey = ``;
94
+ this.#menuOpenedByKeyboard = false;
95
+ this.#currentOpenTrigger = null;
96
+ this.tickled = new TickledController(this, {
97
+ getItems: () => this.#getTickledItems(),
98
+ onTickle: (index) => {
99
+ if (index !== void 0) this.#onTickled(index);
100
+ }
101
+ });
102
+ this.#onFocus = () => {
103
+ this._isFocused = true;
104
+ };
105
+ this.#onBlur = () => {
106
+ this._isFocused = false;
107
+ };
108
+ this.#onHostClick = () => {
109
+ this.#getHostContainer()?.focus();
110
+ };
111
+ this.#onMenuTriggerClose = () => {
112
+ document.removeEventListener(`keydown`, this.#onMenuKeyNav, { capture: true });
113
+ this.#currentOpenTrigger = null;
114
+ this.#menuOpenedByKeyboard = false;
115
+ requestAnimationFrame(() => {
116
+ this.#getHostContainer()?.focus();
117
+ });
118
+ };
119
+ this.#onMenuKeyNav = (e) => {
120
+ if (!this.#currentOpenTrigger?.querySelector(`ixfx-menu-container`) || !this.#currentOpenTrigger?.isOpen) return;
121
+ switch (e.key) {
122
+ case `ArrowLeft`:
123
+ case `ArrowRight`: {
124
+ e.preventDefault();
125
+ e.stopPropagation();
126
+ this.#currentOpenTrigger.closeMenu();
127
+ const r = this.root;
128
+ const sel = this.selectedNode;
129
+ if (!r || !sel) break;
130
+ const totalItems = (this.#findPath(r, sel) ?? [r]).length + (this.#hasChildOptions() ? 1 : 0);
131
+ const currentTickled = this.tickled.tickledId ?? -1;
132
+ if (e.key === `ArrowLeft`) {
133
+ if (currentTickled > 0) this.tickled.handleKeyboardNavigation(`previous`);
134
+ } else if (currentTickled < totalItems - 1) this.tickled.handleKeyboardNavigation(`next`);
135
+ break;
136
+ }
137
+ }
138
+ };
139
+ this.#onKeyDown = (e) => {
140
+ const r = this.root;
141
+ const sel = this.selectedNode;
142
+ if (!r || !sel) return;
143
+ const totalItems = (this.#findPath(r, sel) ?? [r]).length + (this.#hasChildOptions() ? 1 : 0);
144
+ const currentTickled = this.tickled.tickledId ?? -1;
145
+ switch (e.key) {
146
+ case `ArrowRight`:
147
+ e.preventDefault();
148
+ if (currentTickled < totalItems - 1) this.tickled.handleKeyboardNavigation(`next`);
149
+ else if (currentTickled === totalItems - 1) this.#shiftFocusToNarrowedText();
150
+ break;
151
+ case `ArrowLeft`:
152
+ e.preventDefault();
153
+ if (currentTickled > 0) this.tickled.handleKeyboardNavigation(`previous`);
154
+ break;
155
+ case `ArrowDown`:
156
+ case `ArrowUp`:
157
+ if (this.tickled.tickledId !== void 0) {
158
+ e.preventDefault();
159
+ if (this.#currentOpenTrigger?.isOpen) {
160
+ const popover = this.#currentOpenTrigger.querySelector(`ixfx-menu-container`)?.shadowRoot?.querySelector(`.popover`);
161
+ if (popover) popover.dispatchEvent(new KeyboardEvent(`keydown`, {
162
+ key: e.key,
163
+ bubbles: true
164
+ }));
165
+ } else this.#handleOpenCaretMenu();
166
+ }
167
+ break;
168
+ case `Enter`:
169
+ e.preventDefault();
170
+ this.#handleEnter();
171
+ break;
172
+ case `Escape`:
173
+ e.preventDefault();
174
+ this._menuOpen = false;
175
+ this.tickled.clearTickled();
176
+ this.blur();
177
+ break;
178
+ case `Tab`: if (e.shiftKey) {
179
+ e.preventDefault();
180
+ this.#shiftFocusToPath();
181
+ } else if (currentTickled === totalItems - 1) {
182
+ e.preventDefault();
183
+ this._menuOpen = false;
184
+ this._focusOnNarrowedText = true;
185
+ const narrowedText = this.shadowRoot?.querySelector(`ixfx-narrowed-text`);
186
+ if (narrowedText) narrowedText.focus();
187
+ }
188
+ }
189
+ };
190
+ this.#onNarrowedTextBlur = () => {
191
+ this._focusOnNarrowedText = false;
192
+ this.#shiftFocusToPath();
193
+ };
194
+ this.#onNarrowedTextShiftTab = () => {
195
+ this._focusOnNarrowedText = false;
196
+ this.#shiftFocusToPath();
197
+ };
198
+ this.#isWheelScrolling = false;
199
+ this.#onWheel = (e) => {
200
+ const wheelEvent = e;
201
+ if (wheelEvent.deltaX !== 0 || wheelEvent.deltaY !== 0) {
202
+ e.preventDefault();
203
+ if (!this.#isWheelScrolling) {
204
+ this.#isWheelScrolling = true;
205
+ this.tickled.clearTickled();
206
+ }
207
+ const hostContainer = this.#getHostContainer();
208
+ if (hostContainer) hostContainer.scrollLeft += wheelEvent.deltaX + wheelEvent.deltaY;
209
+ clearTimeout(this.#wheelScrollTimeout);
210
+ this.#wheelScrollTimeout = setTimeout(() => {
211
+ this.#isWheelScrolling = false;
212
+ }, 150);
213
+ }
214
+ };
215
+ this.#currentLeadingSiblings = void 0;
216
+ }
217
+ get selectedNodes() {
218
+ return this._selectedNodes;
219
+ }
220
+ #pendingNode;
221
+ #pendingController;
222
+ #modelUnsubscribe;
223
+ #previousSelectionKey;
224
+ #menuOpenedByKeyboard;
225
+ #currentOpenTrigger;
226
+ #getTickledItems() {
227
+ const r = this.root;
228
+ const sel = this.selectedNode;
229
+ if (!r || !sel) return [];
230
+ const pathNodes = this.#findPath(r, sel) ?? [r];
231
+ const items = [];
232
+ if (this.caretEmbedded) {
233
+ pathNodes.forEach((_node, i) => {
234
+ items.push({
235
+ id: i,
236
+ enabled: true
237
+ });
238
+ });
239
+ if (this.#hasChildOptions()) items.push({
240
+ id: pathNodes.length,
241
+ enabled: true
242
+ });
243
+ } else {
244
+ let segmentId = 0;
245
+ pathNodes.forEach((node, i) => {
246
+ const isLast = i === pathNodes.length - 1;
247
+ const hasChildren = node.item.isLeaf !== true && (node.children === void 0 || node.children.length > 0);
248
+ const showLeadingCaret = this.leadingCaret && (i > 0 || this.hideRoot) && hasChildren;
249
+ const showTrailingCaret = !this.leadingCaret && hasChildren || this.leadingCaret && isLast && hasChildren;
250
+ if (showLeadingCaret) items.push({
251
+ id: segmentId++,
252
+ enabled: true
253
+ });
254
+ items.push({
255
+ id: segmentId++,
256
+ enabled: true
257
+ });
258
+ if (showTrailingCaret) items.push({
259
+ id: segmentId++,
260
+ enabled: true
261
+ });
262
+ });
263
+ }
264
+ return items;
265
+ }
266
+ #onTickled(index) {
267
+ if (this.tickled.inputMode === `keyboard`) this.#scrollTickledIntoView(index);
268
+ }
269
+ #scrollTickledIntoView(index) {
270
+ const r = this.root;
271
+ const sel = this.selectedNode;
272
+ if (!r || !sel) return;
273
+ const pathNodes = this.#findPath(r, sel) ?? [r];
274
+ let el = null;
275
+ if (index < pathNodes.length) el = this.shadowRoot?.querySelectorAll(`.crumb-segment`)[index] ?? null;
276
+ else if (index === pathNodes.length) el = this.shadowRoot?.querySelector(`#child-options-trigger`) ?? null;
277
+ el?.scrollIntoView({
278
+ inline: `nearest`,
279
+ block: `nearest`,
280
+ behavior: `smooth`
281
+ });
282
+ }
283
+ #hasChildOptions() {
284
+ const s = this.selectedNode;
285
+ if (s === void 0) return false;
286
+ const isPending = this.#pendingNode === s;
287
+ return (s.children ?? []).length > 0 || isPending;
288
+ }
289
+ #onFocus;
290
+ #onBlur;
291
+ #getHostContainer() {
292
+ return this.shadowRoot?.querySelector(`.host-container`) ?? null;
293
+ }
294
+ #onHostClick;
295
+ #handleOpenCaretMenu() {
296
+ const currentTickled = this.tickled.tickledId;
297
+ if (currentTickled === void 0) return;
298
+ const r = this.root;
299
+ const sel = this.selectedNode;
300
+ if (!r || !sel) return;
301
+ if (currentTickled < (this.#findPath(r, sel) ?? [r]).length - 1) {
302
+ const trigger = (this.shadowRoot?.querySelectorAll(`.crumb-segment`))?.[currentTickled]?.querySelector(`.caret ixfx-menu-trigger`);
303
+ if (trigger) {
304
+ this.#menuOpenedByKeyboard = true;
305
+ this.#currentOpenTrigger = trigger;
306
+ document.addEventListener(`keydown`, this.#onMenuKeyNav, { capture: true });
307
+ trigger.openMenu();
308
+ this.#propagateInputModeToMenu();
309
+ return;
310
+ }
311
+ }
312
+ if (this.#hasChildOptions()) {
313
+ const trigger = this.shadowRoot?.querySelector(`#child-options-trigger`);
314
+ if (trigger) {
315
+ this.#menuOpenedByKeyboard = true;
316
+ this.#currentOpenTrigger = trigger;
317
+ document.addEventListener(`keydown`, this.#onMenuKeyNav, { capture: true });
318
+ trigger.openMenu();
319
+ this.#propagateInputModeToMenu();
320
+ }
321
+ }
322
+ }
323
+ #propagateInputModeToMenu() {
324
+ const inputMode = this.tickled.inputMode;
325
+ requestAnimationFrame(() => {
326
+ const container = this.#currentOpenTrigger?.querySelector(`ixfx-menu-container`);
327
+ if (container) container.setAttribute(`data-input-mode`, inputMode);
328
+ });
329
+ }
330
+ #onMenuTriggerClose;
331
+ #onMenuKeyNav;
332
+ #onKeyDown;
333
+ #handleEnter() {
334
+ const r = this.root;
335
+ const sel = this.selectedNode;
336
+ if (!r || !sel) return;
337
+ const pathNodes = this.#findPath(r, sel) ?? [r];
338
+ const hasChildOptions = this.#hasChildOptions();
339
+ const currentTickled = this.tickled.tickledId ?? -1;
340
+ if (currentTickled < pathNodes.length) {
341
+ const node = pathNodes[currentTickled];
342
+ this.#setSelected(node);
343
+ this._menuOpen = false;
344
+ this.dispatchEvent(new CustomEvent(`item-click`, {
345
+ detail: {
346
+ node,
347
+ depth: currentTickled
348
+ },
349
+ bubbles: true,
350
+ composed: true
351
+ }));
352
+ } else if (hasChildOptions && currentTickled === pathNodes.length) {
353
+ this._menuOpen = true;
354
+ this.#menuOpenedByKeyboard = true;
355
+ const trigger = this.shadowRoot?.querySelector(`#child-options-trigger`);
356
+ if (trigger) {
357
+ this.#currentOpenTrigger = trigger;
358
+ document.addEventListener(`keydown`, this.#onMenuKeyNav, { capture: true });
359
+ trigger.openMenu();
360
+ this.#propagateInputModeToMenu();
361
+ }
362
+ }
363
+ }
364
+ #shiftFocusToNarrowedText() {
365
+ const narrowedText = this.shadowRoot?.querySelector(`ixfx-narrowed-text`);
366
+ if (narrowedText) {
367
+ this._focusOnNarrowedText = true;
368
+ narrowedText.focus();
369
+ }
370
+ }
371
+ #shiftFocusToPath() {
372
+ this._focusOnNarrowedText = false;
373
+ const hostContainer = this.#getHostContainer();
374
+ if (hostContainer) hostContainer.focus();
375
+ }
376
+ #onNarrowedTextBlur;
377
+ #onNarrowedTextShiftTab;
378
+ select(node) {
379
+ this.#setSelected(node);
380
+ }
381
+ deselect(_node) {}
382
+ clearSelection() {
383
+ if (this.root) this.#setSelected(this.root);
384
+ }
385
+ /** Not implemented on crumb-navigation. */
386
+ selectAll() {}
387
+ /** Not implemented on crumb-navigation. */
388
+ deselectAll() {}
389
+ getNodes() {
390
+ if (!this.root) return [];
391
+ const result = [];
392
+ const visit = (node, depth) => {
393
+ result.push({
394
+ node,
395
+ depth
396
+ });
397
+ if (node.children) for (const child of node.children) visit(child, depth + 1);
398
+ };
399
+ for (const child of this.root.children ?? []) visit(child, 0);
400
+ return result;
401
+ }
402
+ updated(changedProps) {
403
+ if (changedProps.has(`model`)) {
404
+ this.#modelUnsubscribe?.();
405
+ this.#modelUnsubscribe = void 0;
406
+ const model = this.model;
407
+ if (model) {
408
+ this.root = model.root;
409
+ this.#modelUnsubscribe = model._subscribe((event) => {
410
+ if (event.type === `root`) {
411
+ this.root = event.root;
412
+ this.requestUpdate();
413
+ }
414
+ });
415
+ }
416
+ }
417
+ this.#runLayoutUpdate(changedProps);
418
+ }
419
+ connectedCallback() {
420
+ super.connectedCallback();
421
+ this.addEventListener(`click`, this.#onHostClick);
422
+ this.addEventListener(`wheel`, this.#onWheel, { passive: false });
423
+ }
424
+ disconnectedCallback() {
425
+ super.disconnectedCallback();
426
+ this.removeEventListener(`click`, this.#onHostClick);
427
+ this.removeEventListener(`wheel`, this.#onWheel);
428
+ this.#modelUnsubscribe?.();
429
+ this.#modelUnsubscribe = void 0;
430
+ document.removeEventListener(`keydown`, this.#onMenuKeyNav, { capture: true });
431
+ this.#currentOpenTrigger = null;
432
+ }
433
+ firstUpdated() {
434
+ const hostContainer = this.#getHostContainer();
435
+ if (hostContainer) hostContainer.scrollLeft = hostContainer.scrollWidth - hostContainer.clientWidth;
436
+ }
437
+ #isWheelScrolling;
438
+ #onWheel;
439
+ #wheelScrollTimeout;
440
+ #runLayoutUpdate(_changedProperties) {
441
+ if (!this.shadowRoot) return;
442
+ const hsiElement = this.shadowRoot.querySelector(`.horiz-select-items`);
443
+ if (!hsiElement) return;
444
+ const ulElement = hsiElement.querySelector(`ul`);
445
+ if (!ulElement) return;
446
+ const menuTrigger = this.shadowRoot.querySelector(`ixfx-menu-trigger`);
447
+ if (!menuTrigger) return;
448
+ if (ulElement.getBoundingClientRect().width > hsiElement.getBoundingClientRect().width) menuTrigger.style.display = `inline-flex`;
449
+ else menuTrigger.style.display = `none`;
450
+ for (const li of ulElement.querySelectorAll(`li`)) if ((li.getAttribute(`data-key`) ?? ``) === this.#previousSelectionKey) {
451
+ this.#scrollToValue(li);
452
+ break;
453
+ }
454
+ }
455
+ #scrollToValue(li) {
456
+ let l = li.offsetLeft;
457
+ const ul = li.parentElement;
458
+ if (!ul) return;
459
+ l -= ul.offsetLeft;
460
+ const horizSelectItems = ul.parentElement;
461
+ if (!horizSelectItems) return;
462
+ horizSelectItems.scroll({
463
+ left: l,
464
+ top: 0,
465
+ behavior: `smooth`
466
+ });
467
+ const horizSelect = horizSelectItems.parentElement;
468
+ if (!horizSelect) return;
469
+ horizSelect.style.minWidth = `${li.getBoundingClientRect().width}px`;
470
+ }
471
+ #abortPending() {
472
+ if (this.#pendingController) {
473
+ this.#pendingController.abort();
474
+ this.#pendingController = void 0;
475
+ this.#pendingNode = void 0;
476
+ }
477
+ }
478
+ #findPath(node, target, acc = []) {
479
+ const path = [...acc, node];
480
+ if (node === target) return path;
481
+ if (!node.children) return void 0;
482
+ for (const child of node.children) {
483
+ const result = this.#findPath(child, target, path);
484
+ if (result) return result;
485
+ }
486
+ }
487
+ #scrollSegmentIntoView(depth) {
488
+ if (!this.shadowRoot) return;
489
+ this.shadowRoot.querySelectorAll(`.crumb-segment`)[depth]?.scrollIntoView({
490
+ block: `nearest`,
491
+ inline: `start`
492
+ });
493
+ }
494
+ #setSelected(node) {
495
+ const previous = this.selectedNode;
496
+ if (previous) this.#previousSelectionKey = previous.item?.key ?? ``;
497
+ else this.#previousSelectionKey = ``;
498
+ const previousSet = previous ? /* @__PURE__ */ new Set([previous]) : /* @__PURE__ */ new Set();
499
+ this.selectedNode = node;
500
+ this._selectedNodes = /* @__PURE__ */ new Set([node]);
501
+ if (this.leadingCaret && this.root) {
502
+ const path = this.#findPath(this.root, node);
503
+ if (path) {
504
+ const depth = this.hideRoot && path.length > 1 ? path.length - 2 : path.length - 1;
505
+ requestAnimationFrame(() => {
506
+ this.#scrollSegmentIntoView(depth);
507
+ });
508
+ }
509
+ }
510
+ this.dispatchEvent(new CustomEvent(`select`, {
511
+ detail: {
512
+ selected: /* @__PURE__ */ new Set([node]),
513
+ previous: previousSet
514
+ },
515
+ bubbles: true,
516
+ composed: true
517
+ }));
518
+ }
519
+ #onLabelClick(node, depth) {
520
+ this._openIdx = -1;
521
+ this.#setSelected(node);
522
+ if (this.leadingCaret) this.#scrollSegmentIntoView(depth);
523
+ this.dispatchEvent(new CustomEvent(`item-click`, {
524
+ detail: {
525
+ node,
526
+ depth
527
+ },
528
+ bubbles: true,
529
+ composed: true
530
+ }));
531
+ }
532
+ #onCaretClick(node, depth, e) {
533
+ e.preventDefault();
534
+ e.stopPropagation();
535
+ if (this._openIdx === depth) {
536
+ this._openIdx = -1;
537
+ return;
538
+ }
539
+ this._openIdx = depth;
540
+ if (node.children === void 0 && this.#pendingNode !== node) {
541
+ this.#abortPending();
542
+ this.#pendingNode = node;
543
+ const controller = new AbortController();
544
+ this.#pendingController = controller;
545
+ if (this.loadChildren) this.#invokeLoadChildren(node, depth, controller).catch(() => {});
546
+ }
547
+ this.requestUpdate();
548
+ }
549
+ async #invokeLoadChildren(node, depth, controller) {
550
+ try {
551
+ const children = [...await collectDataResult(this.loadChildren({
552
+ node,
553
+ depth
554
+ }, controller.signal), controller.signal)];
555
+ if (controller.signal.aborted) return;
556
+ node.children = children;
557
+ this.#pendingNode = void 0;
558
+ this.#pendingController = void 0;
559
+ this.dispatchEvent(new CustomEvent(`expand`, {
560
+ detail: {
561
+ node,
562
+ depth
563
+ },
564
+ bubbles: true,
565
+ composed: true
566
+ }));
567
+ this.requestUpdate();
568
+ } catch {
569
+ if (controller.signal.aborted) return;
570
+ node.children = [];
571
+ this.#pendingNode = void 0;
572
+ this.#pendingController = void 0;
573
+ this.requestUpdate();
574
+ }
575
+ }
576
+ #onSegmentMenuCommand(e, node, depth) {
577
+ const key = e.detail.command;
578
+ if (this.leadingCaret) {
579
+ const child = node.children?.find((c) => c.item.key === key);
580
+ if (child) {
581
+ this.#onChildClick(child, depth + 1);
582
+ return;
583
+ }
584
+ if (this.#currentLeadingSiblings) {
585
+ const sibling = this.#currentLeadingSiblings.find((c) => c.item.key === key);
586
+ if (sibling) this.#onChildClick(sibling, depth);
587
+ this.#currentLeadingSiblings = void 0;
588
+ return;
589
+ }
590
+ }
591
+ const child = node.children?.find((c) => c.item.key === key);
592
+ if (child) this.#onChildClick(child, depth + 1);
593
+ }
594
+ #currentLeadingSiblings;
595
+ #getSiblingMenuItems(pathNodes, index) {
596
+ if (index === 0) {
597
+ if (!this.hideRoot || !this.root) return [];
598
+ const siblings = [...this.root.children ?? []].sort((a, b) => (a.item.label ?? ``).localeCompare(b.item.label ?? ``));
599
+ this.#currentLeadingSiblings = siblings;
600
+ return siblings.map((sibling) => html`
601
+ <ixfx-menu-item
602
+ label="${sibling.item.label}"
603
+ command="${sibling.item.key}"
604
+ icon-name="${sibling.item.icon ?? nothing}"
605
+ @click=${() => {
606
+ this.#onChildClick(sibling, index);
607
+ }}
608
+ ></ixfx-menu-item>
609
+ `);
610
+ }
611
+ const siblings = [...pathNodes[index - 1].children ?? []].sort((a, b) => (a.item.label ?? ``).localeCompare(b.item.label ?? ``));
612
+ this.#currentLeadingSiblings = siblings;
613
+ return siblings.map((sibling) => html`
614
+ <ixfx-menu-item
615
+ label="${sibling.item.label}"
616
+ command="${sibling.item.key}"
617
+ icon-name="${sibling.item.icon ?? nothing}"
618
+ @click=${() => {
619
+ this.#onChildClick(sibling, index);
620
+ }}
621
+ ></ixfx-menu-item>
622
+ `);
623
+ }
624
+ #onChildClick(node, depth) {
625
+ this._openIdx = -1;
626
+ this.#setSelected(node);
627
+ this.dispatchEvent(new CustomEvent(`item-click`, {
628
+ detail: {
629
+ node,
630
+ depth
631
+ },
632
+ bubbles: true,
633
+ composed: true
634
+ }));
635
+ }
636
+ /** Navigate to a node by key path. Returns false if any key is not found in already-loaded children. */
637
+ navigateTo(keys) {
638
+ if (!this.root?.children) return false;
639
+ let current = this.root;
640
+ for (const key of keys) {
641
+ const found = current?.children?.find((n) => n.item.key === key);
642
+ if (!found) return false;
643
+ current = found;
644
+ }
645
+ if (current && current !== this.root) this.#setSelected(current);
646
+ return true;
647
+ }
648
+ /** Navigate to the first node matching the predicate. Returns false if not found. */
649
+ navigateToNode(predicate) {
650
+ if (!this.root) return false;
651
+ const keyPath = this.#findNodePath(this.root.children ?? [], predicate);
652
+ if (!keyPath) return false;
653
+ return this.navigateTo(keyPath);
654
+ }
655
+ #findNodePath(nodes, predicate, prefix = []) {
656
+ for (const node of nodes) {
657
+ const path = [...prefix, node.item.key];
658
+ if (predicate(node)) return path;
659
+ if (node.children?.length) {
660
+ const result = this.#findNodePath(node.children, predicate, path);
661
+ if (result) return result;
662
+ }
663
+ }
664
+ }
665
+ #onMenuCommand(e) {
666
+ const node = this.selectedNode;
667
+ if (!node?.children) return;
668
+ const key = e.detail.command;
669
+ const child = node.children.find((c) => c.item.key === key);
670
+ if (child) {
671
+ const depth = this.root ? this.#findPath(this.root, node)?.length ?? 1 : 1;
672
+ this.#onChildClick(child, depth);
673
+ }
674
+ }
675
+ render() {
676
+ const r = this.root;
677
+ const sel = this.selectedNode;
678
+ if (!r || !sel) return html``;
679
+ this.inputMode = this.tickled.inputMode;
680
+ let pathNodes = this.#findPath(r, sel) ?? [r];
681
+ if (this.hideRoot && pathNodes.length > 1) pathNodes = pathNodes.slice(1);
682
+ this.#hasChildOptions();
683
+ const parts = [];
684
+ let segmentId = 0;
685
+ pathNodes.forEach((node, i) => {
686
+ const isLast = i === pathNodes.length - 1;
687
+ const isPending = this.#pendingNode === node;
688
+ const isOpen = this._openIdx === i;
689
+ const hasChildren = node.item.isLeaf !== true && (node.children === void 0 || node.children.length > 0);
690
+ const isTickled = this.tickled.isTickled(i);
691
+ const showLeadingCaret = this.leadingCaret && (i > 0 || this.hideRoot) && hasChildren;
692
+ const showTrailingCaret = !this.leadingCaret && hasChildren || this.leadingCaret && isLast && hasChildren;
693
+ const siblingMenuItems = showLeadingCaret ? this.#getSiblingMenuItems(pathNodes, i) : [];
694
+ const childMenuItems = (node.children ?? []).map((c) => html`
695
+ <ixfx-menu-item
696
+ label="${c.item.label}"
697
+ command="${c.item.key}"
698
+ icon-name="${c.item.icon ?? nothing}"
699
+ ></ixfx-menu-item>
700
+ `);
701
+ if (this.caretEmbedded) {
702
+ const canTruncate = !this.noTruncate && !isLast && !isTickled;
703
+ parts.push(html`
704
+ <div class="item crumb-segment ${isTickled ? `tickled` : ``} ${canTruncate ? `truncatable` : ``} ${this.leadingCaret ? `leading-caret` : ``} ${isLast ? `last-label` : ``}" data-segment-id="${i}" data-path-index="${i}"
705
+ @click=${(e) => {
706
+ if (e.target.closest(`.caret`)) return;
707
+ this.#onLabelClick(node, i);
708
+ }}
709
+ >
710
+ ${showLeadingCaret ? html`
711
+ <span class="caret ${isOpen ? `open` : ``} ${isPending ? `pending` : ``}">
712
+ ${isPending ? html`<span class="loading-spinner"></span>` : html`
713
+ <ixfx-menu-trigger mode="trigger" label="›" @menu-command=${(e) => this.#onSegmentMenuCommand(e, node, i)}>
714
+ <ixfx-menu-container slot="menu">
715
+ ${siblingMenuItems}
716
+ </ixfx-menu-container>
717
+ </ixfx-menu-trigger>
718
+ `}
719
+ </span>
720
+ ` : nothing}
721
+ <span class="label">${node.item.label}</span>
722
+ ${showTrailingCaret ? html`
723
+ <span class="caret ${isOpen ? `open` : ``} ${isPending ? `pending` : ``}">
724
+ ${isPending ? html`<span class="loading-spinner"></span>` : html`
725
+ <ixfx-menu-trigger mode="trigger" label="›" placement="right-start" @menu-command=${(e) => this.#onSegmentMenuCommand(e, node, i)}>
726
+ <ixfx-menu-container slot="menu">
727
+ ${childMenuItems}
728
+ </ixfx-menu-container>
729
+ </ixfx-menu-trigger>
730
+ `}
731
+ </span>
732
+ ` : nothing}
733
+ </div>
734
+ `);
735
+ } else {
736
+ const leadingCaretTickled = showLeadingCaret ? this.tickled.isTickled(segmentId) : false;
737
+ if (showLeadingCaret) {
738
+ parts.push(html`
739
+ <div class="item crumb-segment ${leadingCaretTickled ? `tickled` : ``}" data-segment-id="${segmentId}" data-path-index="${i}"
740
+ @click=${(e) => {
741
+ const trigger = e.target.closest(`.crumb-segment`)?.querySelector(`ixfx-menu-trigger`);
742
+ if (trigger) trigger.openMenu();
743
+ }}
744
+ >
745
+ <span class="caret ${isOpen ? `open` : ``} ${isPending ? `pending` : ``}">
746
+ ${isPending ? html`<span class="loading-spinner"></span>` : html`
747
+ <ixfx-menu-trigger mode="trigger" label="›" placement="right-start" @menu-command=${(e) => this.#onSegmentMenuCommand(e, node, i)}>
748
+ <ixfx-menu-container slot="menu">
749
+ ${siblingMenuItems}
750
+ </ixfx-menu-container>
751
+ </ixfx-menu-trigger>
752
+ `}
753
+ </span>
754
+ </div>
755
+ `);
756
+ segmentId++;
757
+ }
758
+ const labelTickled = this.tickled.isTickled(segmentId);
759
+ const canTruncate = !this.noTruncate && !isLast && !labelTickled;
760
+ parts.push(html`
761
+ <div class="item crumb-segment ${labelTickled ? `tickled` : ``} ${canTruncate ? `truncatable` : ``} ${isLast ? `last-label` : ``}" data-segment-id="${segmentId}" data-path-index="${i}"
762
+ @click=${(e) => {
763
+ if (e.target.closest(`.caret`)) return;
764
+ this.#onLabelClick(node, i);
765
+ }}
766
+ >
767
+ <span class="label">${node.item.label}</span>
768
+ </div>
769
+ `);
770
+ segmentId++;
771
+ if (showTrailingCaret) {
772
+ const trailingCaretTickled = this.tickled.isTickled(segmentId);
773
+ parts.push(html`
774
+ <div class="item crumb-segment ${trailingCaretTickled ? `tickled` : ``}" data-segment-id="${segmentId}" data-path-index="${i}"
775
+ @click=${(e) => {
776
+ const trigger = e.target.closest(`.crumb-segment`)?.querySelector(`ixfx-menu-trigger`);
777
+ if (trigger) trigger.openMenu();
778
+ }}
779
+ >
780
+ <span class="caret ${isOpen ? `open` : ``} ${isPending ? `pending` : ``}">
781
+ ${isPending ? html`<span class="loading-spinner"></span>` : html`
782
+ <ixfx-menu-trigger mode="trigger" label="›" @menu-command=${(e) => this.#onSegmentMenuCommand(e, node, i)}>
783
+ <ixfx-menu-container slot="menu">
784
+ ${childMenuItems}
785
+ </ixfx-menu-container>
786
+ </ixfx-menu-trigger>
787
+ `}
788
+ </span>
789
+ </div>
790
+ `);
791
+ segmentId++;
792
+ }
793
+ }
794
+ });
795
+ return html`
796
+ <div
797
+ class="host-container scroll-fade-x"
798
+ tabindex="0"
799
+ @focus=${this.#onFocus}
800
+ @blur=${this.#onBlur}
801
+ @keydown=${this.#onKeyDown}
802
+ @click=${this.#onHostClick}
803
+ @menu-trigger-close=${this.#onMenuTriggerClose}
804
+ @mouseout=${(e) => this.tickled.handlePointerLeave(e)}
805
+ >
806
+ <div class="parts"
807
+ @mouseover=${(e) => {
808
+ if (this.#isWheelScrolling) return;
809
+ const target = e.target;
810
+ if (!(target instanceof HTMLElement)) return;
811
+ const segment = target.closest(`.crumb-segment`);
812
+ if (!segment) return;
813
+ const segmentId = parseInt(segment.dataset.segmentId ?? ``, 10);
814
+ if (Number.isNaN(segmentId)) return;
815
+ const pathIndex = parseInt(segment.dataset.pathIndex ?? ``, 10);
816
+ const node = pathNodes[pathIndex];
817
+ if (!node) return;
818
+ this.tickled.handlePointerEnter(segmentId);
819
+ this.dispatchEvent(new CustomEvent(`tickle`, {
820
+ detail: {
821
+ node,
822
+ depth: pathIndex
823
+ },
824
+ bubbles: true,
825
+ composed: true
826
+ }));
827
+ }}
828
+ >${parts}<div class="trailing"></div></div>
829
+ ${this.showSuggestions ? this.#renderChildOptions() : nothing}
830
+ </div>
831
+ `;
832
+ }
833
+ #renderChildOptions() {
834
+ const s = this.selectedNode;
835
+ if (s === void 0) return nothing;
836
+ const isPending = this.#pendingNode === s;
837
+ const children = s.children ?? [];
838
+ if (children.length === 0 && !isPending) return nothing;
839
+ if (isPending) return html`
840
+ <div class="horiz-select">
841
+ <div class="horiz-select-items">
842
+ <span class="loading-spinner"></span>
843
+ </div>
844
+ </div>
845
+ `;
846
+ return html`
847
+ <ixfx-narrowed-text
848
+ class="horiz-select"
849
+ selection-mode="single"
850
+ orientation="horizontal"
851
+ nowrap
852
+ width=${this.#getChildOptionsWidth()}
853
+ @change=${this.#onNarrowedTextChange}
854
+ @narrowed-text-blur=${this.#onNarrowedTextBlur}
855
+ @narrowed-text-shift-tab=${this.#onNarrowedTextShiftTab}
856
+ >
857
+ ${children.map((c) => html`
858
+ <span data-value="${c.item.key}" data-label="${c.item.label}"></span>
859
+ `)}
860
+ </ixfx-narrowed-text>
861
+ `;
862
+ }
863
+ #onNarrowedTextChange(e) {
864
+ e.stopPropagation();
865
+ const detail = e.detail;
866
+ const node = this.selectedNode;
867
+ if (!node?.children) return;
868
+ const child = node.children.find((c) => c.item.key === detail.item.value);
869
+ if (child) {
870
+ const depth = this.root ? this.#findPath(this.root, node)?.length ?? 1 : 1;
871
+ this.#onChildClick(child, depth);
872
+ }
873
+ }
874
+ #getChildOptionsWidth() {
875
+ return `var(--child-options-width, 50ch)`;
876
+ }
877
+ static {
878
+ this.styles = [
879
+ themeFallbacks,
880
+ tickledStyles,
881
+ tickledItemStyles,
882
+ scrollFadeStyles(`x`),
883
+ css`
884
+ :host {
885
+ --crumb-item-bg: var(--item-bg, transparent);
886
+ --crumb-item-bg-tickled: var(--item-bg-tickled);
887
+ --crumb-item-text-tickled: var(--item-text-tickled);
888
+ --crumb-item-border: var(--item-border);
889
+ --crumb-item-border-tickled: var(--item-border-tickled);
890
+ --crumb-item-padding: var(--item-padding, var(--space-xs));
891
+ --crumb-item-border-radius: var(--item-border-radius);
892
+ --crumb-item-gap: var(--item-gap, var(--space-xs));
893
+ --crumb-separator: var(--surface-muted-text);
894
+ --crumb-truncate-min-width: 10ch;
895
+ display: flex;
896
+ width: 100%;
897
+ }
898
+
899
+ :host(.focused) {
900
+ outline: none;
901
+ }
902
+
903
+ :host(:focus-within) .host-container {
904
+ outline: 2px solid var(--accent);
905
+ outline-offset: 2px;
906
+ }
907
+
908
+ /* Edge fade (shows/hides based on scroll position) comes from the
909
+ * shared .scroll-fade-x class - see ../styles/scroll-fade.ts */
910
+ .host-container {
911
+ display: flex;
912
+ width: 100%;
913
+ outline: none;
914
+ overflow: scroll;
915
+ scroll-snap-type: x mandatory;
916
+ scrollbar-width: none;
917
+ }
918
+
919
+ .parts {
920
+ display: flex;
921
+ gap: var(--space-s);
922
+ }
923
+
924
+ .trailing {
925
+ flex: 1;
926
+ min-width: 1em;
927
+ cursor: pointer;
928
+ }
929
+
930
+ .crumb-segment {
931
+ display: flex;
932
+ align-items: center;
933
+ flex-shrink: 0;
934
+ background: var(--crumb-item-bg);
935
+ border: var(--crumb-item-border);
936
+ border-radius: var(--crumb-item-border-radius);
937
+ }
938
+
939
+ .crumb-segment.leading-caret {
940
+ flex-direction: row;
941
+ }
942
+
943
+ .crumb-segment.tickled {
944
+ background: var(--crumb-item-bg-tickled);
945
+ border-color: var(--crumb-item-border-tickled);
946
+ color: var(--crumb-item-text-tickled);
947
+ flex-shrink: 0;
948
+ min-width: auto;
949
+ }
950
+
951
+ .crumb-segment.truncatable {
952
+ flex-shrink: 1;
953
+ min-width: min(var(--crumb-truncate-min-width), auto);
954
+ overflow: hidden;
955
+ }
956
+
957
+ .crumb-segment.truncatable .label {
958
+ overflow: hidden;
959
+ text-overflow: ellipsis;
960
+ white-space: nowrap;
961
+ min-width: 0;
962
+ }
963
+
964
+ .label {
965
+ cursor: pointer;
966
+ white-space: nowrap;
967
+ color: inherit;
968
+ }
969
+
970
+ .crumb-segment.last-label .label {
971
+ color: var(--crumb-last-label-color, inherit);
972
+ font-style: var(--crumb-last-label-font-style, inherit);
973
+ font-weight: var(--crumb-last-label-font-weight, inherit);
974
+ text-decoration: var(--crumb-last-label-text-decoration, inherit);
975
+ text-underline-offset: var(--crumb-last-label-text-underline-offset, auto);
976
+ }
977
+
978
+ .crumb-segment.tickled .label {
979
+ color: var(--crumb-item-text-tickled);
980
+ overflow: visible;
981
+ text-overflow: clip;
982
+ }
983
+
984
+ .crumb-segment ixfx-menu-trigger {
985
+ --ixfx-button-padding: var(--space-xs) 8px;
986
+ }
987
+
988
+ .caret {
989
+ position: relative;
990
+ cursor: pointer;
991
+ padding: var(--crumb-item-padding);
992
+ margin-left: var(--crumb-item-gap);
993
+ margin-right: var(--crumb-item-gap);
994
+ color: var(--crumb-separator);
995
+ display: inline-flex;
996
+ align-items: center;
997
+ justify-content: center;
998
+ transition: all 0.2s ease-in-out;
999
+ }
1000
+
1001
+ .caret.open {
1002
+ background: color-mix(in srgb, var(--surface-5) 40%, transparent);
1003
+ }
1004
+
1005
+ .caret.pending {
1006
+ display: inline-flex;
1007
+ }
1008
+
1009
+ .caret > ixfx-menu-trigger {
1010
+ display: inline-flex;
1011
+ margin: 0;
1012
+ --_trigger-hover-bg: transparent;
1013
+ --_trigger-hover-opacity: 0.6;
1014
+ }
1015
+
1016
+ .caret > ixfx-menu-trigger::part(base) {
1017
+ background: none;
1018
+ border: none;
1019
+ color: inherit;
1020
+ padding: 0;
1021
+ cursor: pointer;
1022
+ font-size: inherit;
1023
+ line-height: 1;
1024
+ }
1025
+
1026
+ .caret > ixfx-menu-trigger::part(base):hover {
1027
+ background: none;
1028
+ opacity: 0.6;
1029
+ }
1030
+
1031
+ .separator {
1032
+ margin-left: var(--crumb-item-gap);
1033
+ margin-right: var(--crumb-item-gap);
1034
+ color: var(--crumb-separator);
1035
+ }
1036
+
1037
+ .separator-trigger {
1038
+ display: inline-flex;
1039
+ margin-left: var(--crumb-item-gap);
1040
+ margin-right: var(--crumb-item-gap);
1041
+ --_trigger-hover-bg: transparent;
1042
+ --_trigger-hover-opacity: 0.6;
1043
+ }
1044
+
1045
+ .separator-trigger::part(base) {
1046
+ background: none;
1047
+ border: none;
1048
+ color: var(--crumb-separator);
1049
+ padding: 0;
1050
+ cursor: pointer;
1051
+ font-size: inherit;
1052
+ line-height: 1;
1053
+ }
1054
+
1055
+ .separator-trigger::part(base):hover {
1056
+ background: none;
1057
+ opacity: 0.6;
1058
+ }
1059
+
1060
+ .separator-trigger.tickled::part(base) {
1061
+ }
1062
+
1063
+ ixfx-menu-trigger {
1064
+ display: none;
1065
+ }
1066
+
1067
+ .horiz-select > ixfx-menu-trigger {
1068
+ display: inline-flex;
1069
+ }
1070
+
1071
+ .horiz-select {
1072
+ display: flex;
1073
+ min-width: 5ch;
1074
+ }
1075
+
1076
+ .loading-spinner {
1077
+ display: inline-block;
1078
+ width: 12px;
1079
+ height: 12px;
1080
+ border: 2px solid var(--surface-5);
1081
+ border-top-color: var(--accent);
1082
+ border-radius: 50%;
1083
+ animation: crumb-spin 0.7s linear infinite;
1084
+ }
1085
+
1086
+ @keyframes crumb-spin {
1087
+ to { transform: rotate(360deg); }
1088
+ }
1089
+
1090
+ ::slotted(*), option {
1091
+ user-select: none;
1092
+ scroll-snap-align: start;
1093
+ }
1094
+ `
1095
+ ];
1096
+ }
1097
+ };
1098
+ __decorate([property({ type: Object })], CrumbNavigationElement.prototype, "root", void 0);
1099
+ __decorate([property({ attribute: false })], CrumbNavigationElement.prototype, "model", void 0);
1100
+ __decorate([property({ type: Object })], CrumbNavigationElement.prototype, "selectedNode", void 0);
1101
+ __decorate([state()], CrumbNavigationElement.prototype, "_selectedNodes", void 0);
1102
+ __decorate([property({ attribute: false })], CrumbNavigationElement.prototype, "loadChildren", void 0);
1103
+ __decorate([property({ attribute: false })], CrumbNavigationElement.prototype, "filterPredicate", void 0);
1104
+ __decorate([property({
1105
+ reflect: true,
1106
+ attribute: `exclusivity`
1107
+ })], CrumbNavigationElement.prototype, "exclusivity", void 0);
1108
+ __decorate([property({
1109
+ reflect: true,
1110
+ attribute: `selection-filter`
1111
+ })], CrumbNavigationElement.prototype, "selectionFilter", void 0);
1112
+ __decorate([state()], CrumbNavigationElement.prototype, "_openIdx", void 0);
1113
+ __decorate([state()], CrumbNavigationElement.prototype, "_isFocused", void 0);
1114
+ __decorate([state()], CrumbNavigationElement.prototype, "_menuOpen", void 0);
1115
+ __decorate([state()], CrumbNavigationElement.prototype, "_focusOnNarrowedText", void 0);
1116
+ __decorate([property({
1117
+ reflect: true,
1118
+ attribute: `input-mode`
1119
+ })], CrumbNavigationElement.prototype, "inputMode", void 0);
1120
+ __decorate([property({
1121
+ type: Boolean,
1122
+ reflect: true,
1123
+ attribute: `no-truncate`
1124
+ })], CrumbNavigationElement.prototype, "noTruncate", void 0);
1125
+ __decorate([property({
1126
+ type: Boolean,
1127
+ reflect: true,
1128
+ attribute: `show-suggestions`
1129
+ })], CrumbNavigationElement.prototype, "showSuggestions", void 0);
1130
+ __decorate([property({
1131
+ type: Boolean,
1132
+ reflect: true,
1133
+ attribute: `leading-caret`
1134
+ })], CrumbNavigationElement.prototype, "leadingCaret", void 0);
1135
+ __decorate([property({
1136
+ type: Boolean,
1137
+ reflect: true,
1138
+ attribute: `caret-embedded`
1139
+ })], CrumbNavigationElement.prototype, "caretEmbedded", void 0);
1140
+ __decorate([property({
1141
+ type: Boolean,
1142
+ reflect: true,
1143
+ attribute: `hide-root`
1144
+ })], CrumbNavigationElement.prototype, "hideRoot", void 0);
1145
+ CrumbNavigationElement = __decorate([safeCustomElement(`ixfx-crumb-navigation`)], CrumbNavigationElement);
1146
+ //#endregion
1147
+ export { scrollFadeStyles as n, CrumbNavigationElement as t };
1148
+
1149
+ //# sourceMappingURL=crumb-navigation-DFqTzElw.js.map