@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.
- package/README.md +4 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.js +387 -265
- package/dist/nile-breadcrumb-item/nile-breadcrumb-item.cjs.js +1 -1
- package/dist/nile-breadcrumb-item/nile-breadcrumb-item.cjs.js.map +1 -1
- package/dist/nile-breadcrumb-item/nile-breadcrumb-item.esm.js +8 -6
- package/dist/nile-combobox/group-utils.cjs.js +2 -0
- package/dist/nile-combobox/group-utils.cjs.js.map +1 -0
- package/dist/nile-combobox/group-utils.esm.js +1 -0
- package/dist/nile-combobox/index.cjs.js +1 -1
- package/dist/nile-combobox/index.esm.js +1 -1
- package/dist/nile-combobox/nile-combobox.cjs.js +1 -1
- package/dist/nile-combobox/nile-combobox.cjs.js.map +1 -1
- package/dist/nile-combobox/nile-combobox.css.cjs.js +1 -1
- package/dist/nile-combobox/nile-combobox.css.cjs.js.map +1 -1
- package/dist/nile-combobox/nile-combobox.css.esm.js +77 -4
- package/dist/nile-combobox/nile-combobox.esm.js +13 -8
- package/dist/nile-combobox/renderer.cjs.js +1 -1
- package/dist/nile-combobox/renderer.cjs.js.map +1 -1
- package/dist/nile-combobox/renderer.esm.js +84 -42
- package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.js +4 -2
- package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.js.map +1 -1
- package/dist/src/nile-combobox/group-utils.d.ts +26 -0
- package/dist/src/nile-combobox/group-utils.js +140 -0
- package/dist/src/nile-combobox/group-utils.js.map +1 -0
- package/dist/src/nile-combobox/nile-combobox.css.js +77 -4
- package/dist/src/nile-combobox/nile-combobox.css.js.map +1 -1
- package/dist/src/nile-combobox/nile-combobox.d.ts +33 -0
- package/dist/src/nile-combobox/nile-combobox.js +171 -34
- package/dist/src/nile-combobox/nile-combobox.js.map +1 -1
- package/dist/src/nile-combobox/renderer.d.ts +4 -0
- package/dist/src/nile-combobox/renderer.js +71 -2
- package/dist/src/nile-combobox/renderer.js.map +1 -1
- package/dist/src/nile-combobox/types.d.ts +30 -0
- package/dist/src/nile-combobox/types.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/nile-breadcrumb-item/nile-breadcrumb-item.ts +4 -2
- package/src/nile-combobox/group-utils.ts +157 -0
- package/src/nile-combobox/nile-combobox.css.ts +77 -4
- package/src/nile-combobox/nile-combobox.ts +223 -70
- package/src/nile-combobox/renderer.ts +119 -2
- package/src/nile-combobox/types.ts +36 -0
- package/vscode-html-custom-data.json +6 -1
package/package.json
CHANGED
|
@@ -53,13 +53,15 @@ export class NileBreadcrumbItem extends NileElement {
|
|
|
53
53
|
|
|
54
54
|
public render(): TemplateResult {
|
|
55
55
|
return html`
|
|
56
|
-
|
|
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
|
-
|
|
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-
|
|
145
|
-
padding: var(--nile-spacing-
|
|
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:
|
|
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-
|
|
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 {
|