@aquera/nile-elements 1.7.2 → 1.7.3

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 (47) hide show
  1. package/README.md +4 -0
  2. package/dist/index.cjs.js +1 -1
  3. package/dist/index.esm.js +1 -1
  4. package/dist/index.js +387 -265
  5. package/dist/nile-breadcrumb-item/nile-breadcrumb-item.cjs.js +1 -1
  6. package/dist/nile-breadcrumb-item/nile-breadcrumb-item.cjs.js.map +1 -1
  7. package/dist/nile-breadcrumb-item/nile-breadcrumb-item.esm.js +8 -6
  8. package/dist/nile-combobox/group-utils.cjs.js +2 -0
  9. package/dist/nile-combobox/group-utils.cjs.js.map +1 -0
  10. package/dist/nile-combobox/group-utils.esm.js +1 -0
  11. package/dist/nile-combobox/index.cjs.js +1 -1
  12. package/dist/nile-combobox/index.esm.js +1 -1
  13. package/dist/nile-combobox/nile-combobox.cjs.js +1 -1
  14. package/dist/nile-combobox/nile-combobox.cjs.js.map +1 -1
  15. package/dist/nile-combobox/nile-combobox.css.cjs.js +1 -1
  16. package/dist/nile-combobox/nile-combobox.css.cjs.js.map +1 -1
  17. package/dist/nile-combobox/nile-combobox.css.esm.js +77 -4
  18. package/dist/nile-combobox/nile-combobox.esm.js +13 -8
  19. package/dist/nile-combobox/renderer.cjs.js +1 -1
  20. package/dist/nile-combobox/renderer.cjs.js.map +1 -1
  21. package/dist/nile-combobox/renderer.esm.js +84 -42
  22. package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.js +4 -2
  23. package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.js.map +1 -1
  24. package/dist/src/nile-combobox/group-utils.d.ts +26 -0
  25. package/dist/src/nile-combobox/group-utils.js +140 -0
  26. package/dist/src/nile-combobox/group-utils.js.map +1 -0
  27. package/dist/src/nile-combobox/nile-combobox.css.js +77 -4
  28. package/dist/src/nile-combobox/nile-combobox.css.js.map +1 -1
  29. package/dist/src/nile-combobox/nile-combobox.d.ts +33 -0
  30. package/dist/src/nile-combobox/nile-combobox.js +171 -34
  31. package/dist/src/nile-combobox/nile-combobox.js.map +1 -1
  32. package/dist/src/nile-combobox/renderer.d.ts +4 -0
  33. package/dist/src/nile-combobox/renderer.js +71 -2
  34. package/dist/src/nile-combobox/renderer.js.map +1 -1
  35. package/dist/src/nile-combobox/types.d.ts +30 -0
  36. package/dist/src/nile-combobox/types.js.map +1 -1
  37. package/dist/src/version.js +1 -1
  38. package/dist/src/version.js.map +1 -1
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/package.json +1 -1
  41. package/src/nile-breadcrumb-item/nile-breadcrumb-item.ts +4 -2
  42. package/src/nile-combobox/group-utils.ts +157 -0
  43. package/src/nile-combobox/nile-combobox.css.ts +77 -4
  44. package/src/nile-combobox/nile-combobox.ts +223 -70
  45. package/src/nile-combobox/renderer.ts +119 -2
  46. package/src/nile-combobox/types.ts +36 -0
  47. package/vscode-html-custom-data.json +6 -1
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Webcomponent nile-elements following open-wc recommendations",
4
4
  "license": "MIT",
5
5
  "author": "nile-elements",
6
- "version": "1.7.2",
6
+ "version": "1.7.3",
7
7
  "main": "dist/src/index.js",
8
8
  "type": "module",
9
9
  "module": "dist/src/index.js",
@@ -53,13 +53,15 @@ export class NileBreadcrumbItem extends NileElement {
53
53
 
54
54
  public render(): TemplateResult {
55
55
  return html`
56
- <slot
56
+ <div part="item"
57
57
  class=${classMap({
58
58
  'nile-breadcrumb-item__slot-text': !this.isLast,
59
59
  'nile-breadcrumb-item__last-slot-text': this.isLast,
60
60
  })}
61
61
  @click=${this.handleClick}
62
- ></slot>
62
+ >
63
+ <slot></slot>
64
+ </div>
63
65
  <nile-icon
64
66
  name=${this.separator}
65
67
  aria-label=${this.separator}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Copyright Aquera Inc 2025
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import type {
9
+ ComboboxDataItem,
10
+ ComboboxGroupItem,
11
+ ComboboxOptionItem,
12
+ ComboboxRow,
13
+ ComboboxHeaderRow,
14
+ ComboboxOptionRow,
15
+ } from './types';
16
+
17
+ export function isGroup(item: any): item is ComboboxGroupItem {
18
+ return !!item && typeof item === 'object' && item.type === 'group' && Array.isArray(item.options);
19
+ }
20
+
21
+ export function hasGroups(data: any[]): boolean {
22
+ if (!Array.isArray(data)) return false;
23
+ for (const item of data) {
24
+ if (isGroup(item)) return true;
25
+ }
26
+ return false;
27
+ }
28
+
29
+ export function countOptionsDeep(group: ComboboxGroupItem): number {
30
+ let n = 0;
31
+ for (const child of group.options) {
32
+ if (isGroup(child)) n += countOptionsDeep(child);
33
+ else n += 1;
34
+ }
35
+ return n;
36
+ }
37
+
38
+ export function flattenRows(data: ComboboxDataItem[]): ComboboxRow[] {
39
+ const rows: ComboboxRow[] = [];
40
+ const walk = (items: ComboboxDataItem[], depth: number, parentIds: string[]) => {
41
+ for (const item of items) {
42
+ if (isGroup(item)) {
43
+ rows.push({
44
+ kind: 'header',
45
+ id: item.id,
46
+ label: item.label,
47
+ prefix: item.prefix,
48
+ depth,
49
+ optionCount: countOptionsDeep(item),
50
+ } as ComboboxHeaderRow);
51
+ walk(item.options, depth + 1, [...parentIds, item.id]);
52
+ } else {
53
+ rows.push({
54
+ kind: 'option',
55
+ item: item as ComboboxOptionItem,
56
+ depth,
57
+ parentIds,
58
+ } as ComboboxOptionRow);
59
+ }
60
+ }
61
+ };
62
+ walk(Array.isArray(data) ? data : [], 0, []);
63
+ return rows;
64
+ }
65
+
66
+ export function getOptionRows(rows: ComboboxRow[]): ComboboxOptionRow[] {
67
+ const out: ComboboxOptionRow[] = [];
68
+ for (const r of rows) if (r.kind === 'option') out.push(r);
69
+ return out;
70
+ }
71
+
72
+ /**
73
+ * Filter rows by a query.
74
+ *
75
+ * Rules:
76
+ * - An option row is kept if its searchText matches.
77
+ * - If a group's label matches, the entire subtree (header + all descendant
78
+ * options + nested headers) is kept.
79
+ * - Otherwise a header is kept only if at least one descendant option matches.
80
+ * - Empty groups (no surviving descendants) are dropped.
81
+ */
82
+ export function filterRows(
83
+ data: ComboboxDataItem[],
84
+ query: string,
85
+ getSearchText: (item: any) => string,
86
+ ): { rows: ComboboxRow[]; visibleOptionCount: number } {
87
+ const q = (query || '').trim().toLowerCase();
88
+ if (!q) {
89
+ const rows = flattenRows(data);
90
+ return { rows, visibleOptionCount: getOptionRows(rows).length };
91
+ }
92
+
93
+ const matchedOption = (item: ComboboxOptionItem): boolean => {
94
+ const text = (getSearchText(item) || '').toString().toLowerCase();
95
+ return text.includes(q);
96
+ };
97
+
98
+ // Walk the tree, returning a filtered subtree (or null when nothing survives).
99
+ type FilterResult =
100
+ | { kind: 'option'; item: ComboboxOptionItem }
101
+ | { kind: 'group'; group: ComboboxGroupItem; children: FilterResult[] }
102
+ | null;
103
+
104
+ const walk = (item: ComboboxDataItem, ancestorLabelMatched: boolean): FilterResult => {
105
+ if (isGroup(item)) {
106
+ const labelMatched = item.label.toLowerCase().includes(q);
107
+ const keepAll = labelMatched || ancestorLabelMatched;
108
+ const children: FilterResult[] = [];
109
+ for (const child of item.options) {
110
+ const sub = walk(child, keepAll);
111
+ if (sub) children.push(sub);
112
+ }
113
+ if (children.length === 0 && !keepAll) return null;
114
+ // If keepAll but nothing came back (empty group), still emit so user sees label.
115
+ return { kind: 'group', group: item, children };
116
+ }
117
+ if (ancestorLabelMatched || matchedOption(item as ComboboxOptionItem)) {
118
+ return { kind: 'option', item: item as ComboboxOptionItem };
119
+ }
120
+ return null;
121
+ };
122
+
123
+ const tops: FilterResult[] = [];
124
+ for (const top of data) {
125
+ const r = walk(top, false);
126
+ if (r) tops.push(r);
127
+ }
128
+
129
+ // Materialize back to flat rows.
130
+ const rows: ComboboxRow[] = [];
131
+ const emit = (results: FilterResult[], depth: number, parentIds: string[]) => {
132
+ for (const r of results) {
133
+ if (!r) continue;
134
+ if (r.kind === 'group') {
135
+ rows.push({
136
+ kind: 'header',
137
+ id: r.group.id,
138
+ label: r.group.label,
139
+ prefix: r.group.prefix,
140
+ depth,
141
+ optionCount: countOptionsDeep(r.group),
142
+ });
143
+ emit(r.children, depth + 1, [...parentIds, r.group.id]);
144
+ } else {
145
+ rows.push({
146
+ kind: 'option',
147
+ item: r.item,
148
+ depth,
149
+ parentIds,
150
+ });
151
+ }
152
+ }
153
+ };
154
+ emit(tops, 0, []);
155
+
156
+ return { rows, visibleOptionCount: getOptionRows(rows).length };
157
+ }
@@ -141,9 +141,10 @@ export const styles = css`
141
141
  /* Size: medium */
142
142
  .combobox--medium .combobox__trigger {
143
143
  border-radius: var(--nile-radius-sm, var(--ng-radius-md));
144
- font-size: var(--nile-type-scale-3, var(--ng-font-size-text-sm));
145
- padding: var(--nile-spacing-5px, var(--ng-spacing-md)) var(--nile-spacing-10px, var(--ng-spacing-lg));
144
+ font-size: var(--nile-type-scale-3, var(--ng-font-size-text-md));
145
+ padding: var(--nile-spacing-lg, var(--ng-spacing-md)) var(--nile-spacing-lg, var(--ng-spacing-lg));
146
146
  min-height: var(--nile-height-40px, var(--ng-height-40px));
147
+ box-sizing: border-box;
147
148
  }
148
149
 
149
150
  /* Size: large */
@@ -256,10 +257,10 @@ export const styles = css`
256
257
  margin: 0;
257
258
  -webkit-appearance: none;
258
259
  font-family: var(--nile-font-family-serif, var(--ng-font-family-body));
259
- font-size: var(--nile-type-scale-3, var(--ng-font-size-text-md));
260
+ font-size: inherit;
260
261
  font-weight: var(--nile-font-weight-regular, var(--ng-font-weight-regular));
261
262
  text-overflow: ellipsis;
262
- line-height: var(--nile-spacing-2xl, var(--ng-line-height-text-sm));
263
+ line-height: var(--nile-line-height-xsmall, var(--ng-line-height-text-sm));
263
264
  }
264
265
 
265
266
  .combobox__input::placeholder {
@@ -636,6 +637,78 @@ export const styles = css`
636
637
  border-radius: 0;
637
638
  }
638
639
 
640
+ /* ── Group headers (data-driven grouping) ── */
641
+
642
+ /* Plain (non-virtualized) listbox: sticky relative to the scroll container.
643
+ * Activated only when the host has [sticky-group-header]. */
644
+ :host([sticky-group-header]) .combobox__options-plain .combobox__group-header {
645
+ position: sticky;
646
+ top: calc(var(--group-depth, 0) * 28px);
647
+ z-index: 1;
648
+ }
649
+
650
+ /* Virtualized listbox: each row is absolutely positioned by the virtualizer.
651
+ * Sticky doesn't pin against the scroll container because the row wrapper's
652
+ * transform creates a containing block. Instead we render a separate
653
+ * overlay inside the listbox that mirrors the currently-active group
654
+ * header (computed on scroll). */
655
+ .combobox__group-header-slot {
656
+ pointer-events: none;
657
+ }
658
+ .combobox__group-header-slot .combobox__group-header {
659
+ height: 100%;
660
+ box-sizing: border-box;
661
+ }
662
+
663
+ .combobox__group-sticky-overlay {
664
+ position: sticky;
665
+ top: 0;
666
+ z-index: 2;
667
+ pointer-events: none;
668
+ }
669
+ .combobox__group-sticky-overlay .combobox__group-header {
670
+ box-shadow: 0 1px 0 0 var(--nile-colors-neutral-300, var(--ng-colors-border-secondary));
671
+ }
672
+
673
+ .combobox__group-header {
674
+ display: flex;
675
+ align-items: center;
676
+ gap: 6px;
677
+ box-sizing: border-box;
678
+ width: 100%;
679
+ height: 100%;
680
+ /* Match the listbox's natural inline padding so the header sits like a
681
+ * section divider, edge-to-edge with options. Nested groups indent by
682
+ * depth. Override with --combobox-group-header-indent if needed. */
683
+ padding-block: 8px;
684
+ padding-inline-end: var(--nile-spacing-lg, var(--ng-spacing-lg));
685
+ padding-inline-start: calc(var(--combobox-group-header-indent, var(--nile-spacing-lg, var(--ng-spacing-lg))) + var(--group-depth, 0) * 16px);
686
+ font-family: var(--nile-font-family-sans-serif, var(--ng-font-family-body));
687
+ font-size: var(--nile-type-scale-2, var(--ng-font-size-text-xs));
688
+ font-weight: var(--nile-font-weight-semibold, var(--ng-font-weight-semibold));
689
+ line-height: 1;
690
+ text-transform: uppercase;
691
+ letter-spacing: 0.06em;
692
+ color: var(--nile-colors-dark-500, var(--ng-colors-text-tertiary-600));
693
+ background: var(--nile-colors-neutral-100, var(--ng-colors-bg-secondary));
694
+ border-bottom: 1px solid var(--nile-colors-neutral-300, var(--ng-colors-border-secondary));
695
+ pointer-events: none;
696
+ user-select: none;
697
+ }
698
+
699
+ .combobox__group-label {
700
+ flex: 1;
701
+ min-width: 0;
702
+ overflow: hidden;
703
+ text-overflow: ellipsis;
704
+ white-space: nowrap;
705
+ }
706
+
707
+ .combobox__group-prefix {
708
+ flex-shrink: 0;
709
+ color: inherit;
710
+ }
711
+
639
712
  /* ── Help / Error ── */
640
713
 
641
714
  .form-control__help-text {