@ixfx/components 0.13.2 → 0.13.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/bundle/index.d.ts +97 -14
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +2183 -636
- package/bundle/index.js.map +1 -1
- package/dist/ac-token.js +1 -1
- package/dist/{data-grid-Ck1Qx2H1.d.ts → data-grid-DHWJN66w.d.ts} +7 -5
- package/dist/{data-grid-Ck1Qx2H1.d.ts.map → data-grid-DHWJN66w.d.ts.map} +1 -1
- package/dist/data-grid.d.ts +1 -1
- package/dist/data-grid.js +210 -120
- package/dist/data-grid.js.map +1 -1
- package/dist/detail-list-C96B5Txz.js +2001 -0
- package/dist/detail-list-C96B5Txz.js.map +1 -0
- package/dist/{detail-list-C1xzr1z7.d.ts → detail-list-dX7jLO6b.d.ts} +12 -3
- package/dist/{detail-list-C1xzr1z7.d.ts.map → detail-list-dX7jLO6b.d.ts.map} +1 -1
- package/dist/detail-list.d.ts +2 -2
- package/dist/detail-list.js +1 -1680
- package/dist/{flat-list-Ohxqsy0g.d.ts → flat-list-Y98IqH00.d.ts} +2 -2
- package/dist/{flat-list-Ohxqsy0g.d.ts.map → flat-list-Y98IqH00.d.ts.map} +1 -1
- package/dist/flat-list.d.ts +1 -1
- package/dist/flat-list.js +5 -1
- package/dist/flat-list.js.map +1 -1
- package/dist/{grid-list-pefLORIr.d.ts → grid-list-CoT9e1jB.d.ts} +9 -2
- package/dist/{grid-list-pefLORIr.d.ts.map → grid-list-CoT9e1jB.d.ts.map} +1 -1
- package/dist/{grid-list-DjIl910A.js → grid-list-DeqAd9-h.js} +189 -40
- package/dist/grid-list-DeqAd9-h.js.map +1 -0
- package/dist/grid-list.d.ts +1 -1
- package/dist/grid-list.js +1 -1
- package/dist/{incr-search-D91M4Ozq.js → incr-search-JIhwB49y.js} +27 -5
- package/dist/incr-search-JIhwB49y.js.map +1 -0
- package/dist/incr-search.d.ts +1 -1
- package/dist/incr-search.js +1 -1
- package/dist/index-CnQPCywE.d.ts +325 -0
- package/dist/index-CnQPCywE.d.ts.map +1 -0
- package/dist/{index-CxZ61DO1.d.ts → index-Dlah7Zfi.d.ts} +8 -1
- package/dist/{index-CxZ61DO1.d.ts.map → index-Dlah7Zfi.d.ts.map} +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -25
- package/dist/index.js.map +1 -1
- package/dist/miller.d.ts +1 -1
- package/dist/miller.d.ts.map +1 -1
- package/dist/miller.js +146 -100
- package/dist/miller.js.map +1 -1
- package/dist/narrowed-text.js +1 -1
- package/dist/split-layout-C5SYbDrq.d.ts.map +1 -1
- package/dist/{split-layout-CXotkdbx.js → split-layout-Deoxy47d.js} +6 -1
- package/dist/{split-layout-CXotkdbx.js.map → split-layout-Deoxy47d.js.map} +1 -1
- package/dist/split-layout.js +1 -1
- package/dist/{tab-list-v8mKX7Fu.d.ts → tab-list-D_dmImJM.d.ts} +2 -3
- package/dist/tab-list-D_dmImJM.d.ts.map +1 -0
- package/dist/tabs.d.ts +1 -1
- package/dist/tabs.js +22 -0
- package/dist/tabs.js.map +1 -1
- package/dist/{tree-DGveys4K.js → tree-DD8OaqsK.js} +1010 -230
- package/dist/tree-DD8OaqsK.js.map +1 -0
- package/dist/tree.d.ts +4 -267
- package/dist/tree.js +1 -1
- package/dist/{vertical-list-CqBc-t8w.d.ts → vertical-list-lj_19DCx.d.ts} +3 -2
- package/dist/{vertical-list-CqBc-t8w.d.ts.map → vertical-list-lj_19DCx.d.ts.map} +1 -1
- package/dist/vertical-list.d.ts +1 -1
- package/dist/vertical-list.js +113 -24
- package/dist/vertical-list.js.map +1 -1
- package/docs-user/data-grid.md +74 -4
- package/docs-user/detail-list.md +65 -0
- package/docs-user/grid-list.md +81 -4
- package/docs-user/index.json +1 -1
- package/docs-user/miller.md +59 -3
- package/docs-user/tree.md +63 -2
- package/docs-user/vertical-list.md +9 -1
- package/package.json +6 -1
- package/dist/detail-list.js.map +0 -1
- package/dist/grid-list-DjIl910A.js.map +0 -1
- package/dist/incr-search-D91M4Ozq.js.map +0 -1
- package/dist/tab-list-v8mKX7Fu.d.ts.map +0 -1
- package/dist/tree-DGveys4K.js.map +0 -1
- package/dist/tree.d.ts.map +0 -1
package/docs-user/data-grid.md
CHANGED
|
@@ -112,6 +112,13 @@ grid.getRowKey = (row, i) => row.key ?? row.id ?? String(i);
|
|
|
112
112
|
|
|
113
113
|
Reassign `rows` to update. Sorting is derived from `sortState`.
|
|
114
114
|
|
|
115
|
+
The sorted / filtered / grouped row model is only re-derived when one of its
|
|
116
|
+
inputs is **reassigned** — `rows`, `sortState`, `columns`, `filterPredicate`,
|
|
117
|
+
`groupBy`, `groupLabel`, `getRowKey` (or a group is collapsed). Scroll, hover,
|
|
118
|
+
selection and edit updates skip it, so they cost O(visible rows), not O(rows).
|
|
119
|
+
Mutating a row object in place and calling `requestUpdate()` re-renders the
|
|
120
|
+
cell but does not re-filter, re-group or re-sort — reassign `rows` for that.
|
|
121
|
+
|
|
115
122
|
When the row set changes without a wholesale replace — rows spliced in, rows
|
|
116
123
|
filtered out — the new rows fade in and, for ~0.3s afterwards, every row slides
|
|
117
124
|
to its new position instead of snapping (so an inserted row's neighbours make
|
|
@@ -136,6 +143,63 @@ over the appended slice only. When a sort is active the new rows show unsorted
|
|
|
136
143
|
for one frame, then a single coalesced re-sort runs. Scroll position, selection
|
|
137
144
|
and cursor are preserved across the append.
|
|
138
145
|
|
|
146
|
+
### Performance
|
|
147
|
+
|
|
148
|
+
**Any change to `data-grid.ts` that touches `willUpdate`, `render`, the row
|
|
149
|
+
model (`_rebuildFlat`, sorting, grouping, filtering), scrolling or
|
|
150
|
+
`appendRows` should be checked with both tools below.** The per-frame paths are
|
|
151
|
+
easy to make O(rows) by accident: before these guards existed, one stray
|
|
152
|
+
rebuild-per-update made scrolling 50k grouped rows cost ~146 ms a frame.
|
|
153
|
+
|
|
154
|
+
There are two layers:
|
|
155
|
+
|
|
156
|
+
| | `data-grid.perf.test.ts` | `pnpm bench:data-grid` |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| Runs | with `pnpm test` (happy-dom) | against the dev server, in headless Chrome |
|
|
159
|
+
| Measures | grid JS work: call counts and 2k → 20k scaling | real main-thread ms per frame: script + style/layout/paint |
|
|
160
|
+
| Catches | O(n) / O(n²) creeping into scroll, hover, add/remove | real-browser slowdowns, including rendering cost |
|
|
161
|
+
| Use it | always (it's part of CI) | before / after any perf-sensitive change |
|
|
162
|
+
|
|
163
|
+
**Unit-level guards** — `data-grid.perf.test.ts` asserts that a scroll frame /
|
|
164
|
+
hover does work proportional to the visible window (flat from 2k to 20k rows,
|
|
165
|
+
including sorted / grouped / filtered), that a sub-row scroll doesn't
|
|
166
|
+
re-render, and that adding / removing rows scales at most linearly. They use
|
|
167
|
+
call counts and size ratios rather than absolute times, so they don't flake on
|
|
168
|
+
slow machines. `pnpm test:perf` prints the per-scenario timings.
|
|
169
|
+
|
|
170
|
+
**Real-browser benchmark** — `scripts/bench/data-grid.ts` drives
|
|
171
|
+
`demo/data-grid.html` in an isolated headless Chrome (its own temp profile; set
|
|
172
|
+
`CHROME_PATH` if Chrome isn't in a standard location). It needs the dev server
|
|
173
|
+
running (`pnpm start`). For 1k / 50k rows, plain and grouped by `status`, it
|
|
174
|
+
reports:
|
|
175
|
+
|
|
176
|
+
- `scroll script` / `scroll render` — main-thread ms per frame at 40 px/frame
|
|
177
|
+
(wheel / trackpad), from a timeline trace.
|
|
178
|
+
- `jump script` / `jump render` — the same at 1200 px/frame (scrollbar drag /
|
|
179
|
+
fling: every row in the window is replaced each frame).
|
|
180
|
+
- `insert3Ms`, `removeOneMs`, `appendRows500Ms`, `selectMs` — median time from
|
|
181
|
+
the call until Lit has rendered and style + layout are flushed.
|
|
182
|
+
|
|
183
|
+
Workflow — record a baseline on the unchanged code, make the change, compare:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
pnpm bench:data-grid --out /tmp/grid-base.json # before
|
|
187
|
+
pnpm bench:data-grid --compare /tmp/grid-base.json # after
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`--compare` prints each metric's % change and lists anything over
|
|
191
|
+
`--threshold` (default 50%, and never changes under 1 ms — run-to-run noise is
|
|
192
|
+
~±25%). Add `--fail-on-regression` to exit non-zero. Other options: `--rows
|
|
193
|
+
1000,50000`, `--group-by status` (empty to skip grouped runs), `--url`.
|
|
194
|
+
Baselines are machine-specific, so keep them local rather than committing
|
|
195
|
+
them. The dev server runs Lit in dev mode, so absolute numbers are higher than
|
|
196
|
+
a production build — compare runs against each other, not against a budget.
|
|
197
|
+
|
|
198
|
+
Reference numbers (M-series Mac, dev mode, 50k rows): a smooth-scroll frame is
|
|
199
|
+
~1.5 ms script + ~2 ms render; a jump frame ~6 ms + ~4.5 ms; add / remove one
|
|
200
|
+
row ~7 ms; select ~1 ms. All flat across row count and grouping, except
|
|
201
|
+
add / remove, which is linear in rows.
|
|
202
|
+
|
|
139
203
|
---
|
|
140
204
|
|
|
141
205
|
## Selection & interaction
|
|
@@ -395,9 +459,9 @@ Demo: `Score` is random float `0–100` with 0–3 decimals, `align:'right'`, `f
|
|
|
395
459
|
|
|
396
460
|
Gear menu (`ixfx-menu-trigger` placement `bottom-end`, `inset-area: block-end span-inline-end`) lists `Columns` (check-items) and `Group by` (radio). Toggle visibility via `col.visible=false` or menu. Event: `datagrid-column-visibility` `{key, visible}`.
|
|
397
461
|
|
|
398
|
-
`column-menu` attribute: `
|
|
399
|
-
|
|
400
|
-
|
|
462
|
+
`column-menu` attribute: `none` (default) hides the gear menu; `auto` opts in to
|
|
463
|
+
rendering it. `col.visible` + `datagrid-column-visibility` give full external
|
|
464
|
+
control, so a host can own the column-visibility UI.
|
|
401
465
|
|
|
402
466
|
Other events: `datagrid-column-reorder` `{from,to,columns}`, `datagrid-column-resize` `{key,width}`, `datagrid-cell-click` `{rowKey,colKey,rowIndex,colIndex}`, `datagrid-cell-edit` `{rowKey,colKey,oldValue,newValue}`.
|
|
403
467
|
|
|
@@ -410,12 +474,18 @@ Grid is focusable (`tabindex=0`). When not editing:
|
|
|
410
474
|
| Key | Action |
|
|
411
475
|
|---|---|
|
|
412
476
|
| `ArrowDown/Up` | Move tickled cursor, scroll into view; with `Shift` + `vscode`/`standard` extends range |
|
|
413
|
-
| `Home/End` | First/last |
|
|
477
|
+
| `Home/End` | First/last data row |
|
|
414
478
|
| `Enter` | If `editability=user` and row has editable cell, open editor (prefers `lastClickedCell`); else select row |
|
|
415
479
|
| `Space` | Toggle selection |
|
|
416
480
|
| `Escape` | Clear selection (or cancel edit) |
|
|
417
481
|
| `Ctrl/Cmd+A` | Select all (clamped to anchor group if `!crossGroupSelection`) |
|
|
418
482
|
|
|
483
|
+
The cursor skips group header rows. Moving it scrolls synchronously, so the target row is rendered in the same update, and moving up onto the first row of a group reveals its header.
|
|
484
|
+
|
|
485
|
+
**Accessibility:** the grid is `role="grid"` with `aria-rowcount` / `aria-colcount`; header cells are `columnheader` (with `aria-sort`), rows carry `aria-rowindex` (absolute, so it stays correct while windowed) and `aria-selected`, and group rows have a `rowheader` with `aria-expanded`.
|
|
486
|
+
|
|
487
|
+
**Editing while scrolling:** the row being edited is pinned in the DOM even if scrolled out of the window, so its input keeps focus and the edit isn't lost. It is not listed in `renderedKeys()`.
|
|
488
|
+
|
|
419
489
|
---
|
|
420
490
|
|
|
421
491
|
## Incremental search
|
package/docs-user/detail-list.md
CHANGED
|
@@ -40,6 +40,7 @@ Selection semantics match `ixfx-vertical-list` — same modes, same `list-*` eve
|
|
|
40
40
|
- [Streaming provider](#streaming-provider)
|
|
41
41
|
- [CSS variables](#css-variables)
|
|
42
42
|
- [CSS parts](#css-parts)
|
|
43
|
+
- [Performance](#performance)
|
|
43
44
|
|
|
44
45
|
---
|
|
45
46
|
|
|
@@ -168,6 +169,13 @@ list.formatter = (item, index) => {
|
|
|
168
169
|
};
|
|
169
170
|
```
|
|
170
171
|
|
|
172
|
+
Reassigning `items` keeps the row of every object that is still in the new
|
|
173
|
+
array (matched by identity), so `list.items = [...list.items, next]` creates
|
|
174
|
+
one `<li>` rather than rebuilding the list — selection, cursor and scroll stay
|
|
175
|
+
put, and the new row animates in like `addItem()`. Pass new objects (or change
|
|
176
|
+
`displayProperty`, `formatter`, …) to rebuild rows. With a `formatter`, a row
|
|
177
|
+
is only kept at the same index, since the formatter receives the index.
|
|
178
|
+
|
|
171
179
|
Retrieve the original data for an element:
|
|
172
180
|
|
|
173
181
|
```typescript
|
|
@@ -625,3 +633,60 @@ ixfx-detail-list::part(search-overlay) {
|
|
|
625
633
|
background: rgba(0, 0, 0, 0.8);
|
|
626
634
|
}
|
|
627
635
|
```
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## Performance
|
|
640
|
+
|
|
641
|
+
Every item is a real `<li>` in the light DOM — there is no virtual window —
|
|
642
|
+
so the per-frame and per-hover paths must never scale with the item count.
|
|
643
|
+
They don't: scrolling binary-searches the column flow (`visibleKeys()` reads
|
|
644
|
+
O(log n) rects), the column grid used for keyboard navigation is measured
|
|
645
|
+
lazily and re-validated with a few probe reads, hover and selection update
|
|
646
|
+
only the rows that changed, and each row has `contain: paint` so restyling
|
|
647
|
+
one row doesn't repaint the list.
|
|
648
|
+
|
|
649
|
+
`contain: paint` means row content that overflows the row box is clipped to
|
|
650
|
+
it (and each row is its own stacking context). Rows are fixed-size, so this
|
|
651
|
+
only matters if a badge or custom node is wider than the column. It costs
|
|
652
|
+
~0.6 ms more compositor work per scroll frame at 10k rows and saves ~30 ms of
|
|
653
|
+
paint per hover / selection change.
|
|
654
|
+
|
|
655
|
+
**Any change to `detail-list.ts` that touches item caching, geometry,
|
|
656
|
+
`_syncItemStates`, scrolling or the add / remove paths should be checked with
|
|
657
|
+
both tools below.** At 10k items these paths used to cost ~30 ms of script per
|
|
658
|
+
scroll frame and 180 ms per arrow-key press.
|
|
659
|
+
|
|
660
|
+
| | `detail-list.perf.test.ts` (+ `detail-list-geometry.test.ts`) | `pnpm bench:detail-list` |
|
|
661
|
+
|---|---|---|
|
|
662
|
+
| Runs | with `pnpm test` (happy-dom, stubbed layout) | against the dev server, in headless Chrome |
|
|
663
|
+
| Measures | work counters: layout reads per scroll / keypress, rows created per append, attribute writes per hover; 1k → 10k scaling | real main-thread ms: script + style / layout / paint |
|
|
664
|
+
| Catches | O(n) creeping into scroll, hover, navigation, add / remove; stale caches | real-browser slowdowns, including paint |
|
|
665
|
+
| Use it | always (part of CI) | before / after any perf-sensitive change |
|
|
666
|
+
|
|
667
|
+
**Real-browser benchmark** — `scripts/bench/detail-list.ts` drives
|
|
668
|
+
`#list-objects` in `demo/detail-list.html` with 1k / 10k generated items,
|
|
669
|
+
plain and grouped by `kind`, and reports:
|
|
670
|
+
|
|
671
|
+
- `scroll` / `jump` `script` + `render` — main-thread ms per frame scrolling
|
|
672
|
+
the columns at 40 px / 1200 px per frame, from a timeline trace.
|
|
673
|
+
- `hover`, `keyNav`, `select`, `addItem`, `removeItem`, `itemsAppend`
|
|
674
|
+
(`items = [...items, x]`) — main-thread ms per operation (script + render),
|
|
675
|
+
run with `prefers-reduced-motion: reduce` so row animations don't count.
|
|
676
|
+
|
|
677
|
+
Record a baseline on the unchanged code, make the change, compare:
|
|
678
|
+
|
|
679
|
+
```sh
|
|
680
|
+
pnpm start # dev server, separate terminal
|
|
681
|
+
pnpm bench:detail-list --out /tmp/list-base.json # before
|
|
682
|
+
pnpm bench:detail-list --compare /tmp/list-base.json # after
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
`--compare` lists metrics over `--threshold` (default 50%, ignoring changes
|
|
686
|
+
under 1 ms); add `--fail-on-regression` to exit non-zero. Other options:
|
|
687
|
+
`--items 1000,10000`, `--group-by kind` (empty to skip grouped runs), `--url`.
|
|
688
|
+
Baselines are machine-specific and the dev server runs Lit in dev mode, so
|
|
689
|
+
compare runs against each other rather than against fixed budgets. The data
|
|
690
|
+
grid has the same tooling (`pnpm bench:data-grid`); shared harness code is in
|
|
691
|
+
`scripts/bench/`.
|
|
692
|
+
|
package/docs-user/grid-list.md
CHANGED
|
@@ -21,8 +21,9 @@ single vertical column, or a horizontally-scrolling strip.
|
|
|
21
21
|
10. [Keyboard](#keyboard)
|
|
22
22
|
11. [Grouping](#grouping)
|
|
23
23
|
12. [Virtualisation](#virtualisation)
|
|
24
|
-
13. [
|
|
25
|
-
14. [
|
|
24
|
+
13. [Performance](#performance)
|
|
25
|
+
14. [CSS parts](#css-parts)
|
|
26
|
+
15. [Choosing a list component](#choosing-a-list-component)
|
|
26
27
|
|
|
27
28
|
---
|
|
28
29
|
|
|
@@ -46,8 +47,10 @@ single vertical column, or a horizontally-scrolling strip.
|
|
|
46
47
|
```
|
|
47
48
|
|
|
48
49
|
Items are **light-DOM children**. Each is projected into its own virtualised
|
|
49
|
-
cell via a named `<slot
|
|
50
|
-
|
|
50
|
+
cell via a named `<slot>`: the list sets each rendered item's `slot` attribute
|
|
51
|
+
(and removes it from items outside the window), so don't set `slot` on items
|
|
52
|
+
yourself. Add or remove children at any time — a `MutationObserver` picks the
|
|
53
|
+
change up and re-renders.
|
|
51
54
|
|
|
52
55
|
```typescript
|
|
53
56
|
import type { GridListElement } from '@ixfx/components/grid-list';
|
|
@@ -463,6 +466,13 @@ grid.visibleKeys(); // keys strictly intersecting the viewport (no overscan)
|
|
|
463
466
|
keyless light-DOM items (auto-assigned internally) are omitted from
|
|
464
467
|
`renderedKeys()` / `visibleKeys()` / `selectedKeys`.
|
|
465
468
|
|
|
469
|
+
### Accessibility
|
|
470
|
+
|
|
471
|
+
Cells are `role="option"` with `aria-setsize` (the filtered, non-collapsed count)
|
|
472
|
+
and `aria-posinset`, so assistive technology sees the true list size even though
|
|
473
|
+
only a window of cells is in the DOM. Group headers are `role="presentation"`.
|
|
474
|
+
A container resize that changes the column count keeps the centre item in view.
|
|
475
|
+
|
|
466
476
|
### Hidden containers
|
|
467
477
|
|
|
468
478
|
`refreshLayout()` re-reads the viewport after an ancestor's visibility or size
|
|
@@ -491,6 +501,73 @@ it's suppressed under `prefers-reduced-motion: reduce`. Duration:
|
|
|
491
501
|
|
|
492
502
|
---
|
|
493
503
|
|
|
504
|
+
## Performance
|
|
505
|
+
|
|
506
|
+
**Any change to `grid-list.ts`, `grid-list-source.ts` or `grid-list-layout.ts`
|
|
507
|
+
that touches `render`, the per-cell template, the item source, scrolling,
|
|
508
|
+
cursor movement or item insertion should be checked with `pnpm bench:grid-list`.**
|
|
509
|
+
A per-frame path that quietly becomes O(items) is easy to introduce — see the
|
|
510
|
+
list at the end of this section for the ones that have bitten before.
|
|
511
|
+
|
|
512
|
+
`scripts/bench/grid-list.ts` drives `demo/grid-list.html` in an isolated
|
|
513
|
+
headless Chrome (its own temp profile; set `CHROME_PATH` if Chrome isn't in a
|
|
514
|
+
standard location). It needs the dev server running (`pnpm start`). It mounts
|
|
515
|
+
its own full-window list in **data mode** (adapter + `items`, default 5k and 50k
|
|
516
|
+
items) and **element mode** (light-DOM children, default 2k and 10k), and
|
|
517
|
+
reports main-thread ms from a timeline trace:
|
|
518
|
+
|
|
519
|
+
- `scroll` — script + render per frame at 40 px/frame (wheel / trackpad).
|
|
520
|
+
- `jump` — the same at 1200 px/frame (scrollbar drag / fling: the whole window
|
|
521
|
+
is replaced every frame).
|
|
522
|
+
- `key arrow` / `key page` / `key jump` — script + render per key press for
|
|
523
|
+
Arrow keys, PageUp/PageDown, and End/Home (cursor crosses the whole list).
|
|
524
|
+
- `append1`, `append100`, `insertMid`, `removeMid` — script + render per dynamic
|
|
525
|
+
change (data mode: `appendItems` / `items` / `removeItems`; element mode:
|
|
526
|
+
light-DOM `append` / `insertBefore` / `remove`). Run with
|
|
527
|
+
`prefers-reduced-motion` so the 200 ms slide animation isn't measured.
|
|
528
|
+
|
|
529
|
+
Workflow — record a baseline on the unchanged code, make the change, compare:
|
|
530
|
+
|
|
531
|
+
```sh
|
|
532
|
+
pnpm bench:grid-list --out /tmp/grid-list-base.json # before
|
|
533
|
+
pnpm bench:grid-list --compare /tmp/grid-list-base.json # after
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
`--compare` prints each metric's % change and lists anything over
|
|
537
|
+
`--threshold` (default 50%, and never changes under 1 ms — run-to-run noise is
|
|
538
|
+
a few %). Add `--fail-on-regression` to exit non-zero. `--data-items 5000,100000`
|
|
539
|
+
and `--element-items 2000,20000` change the sizes. Baselines are
|
|
540
|
+
machine-specific, so keep them local rather than committing them. The dev
|
|
541
|
+
server runs Lit in dev mode, so absolute numbers are higher than a production
|
|
542
|
+
build — compare runs against each other, not against a budget.
|
|
543
|
+
|
|
544
|
+
Reference numbers (M-series Mac, dev mode; all flat in item count except
|
|
545
|
+
inserts): a smooth-scroll frame ~2–3 ms script + ~2.5 ms render; a jump-scroll
|
|
546
|
+
frame ~5 ms script (data) / ~9–16 ms (element) + ~7 ms render; an Arrow / Page
|
|
547
|
+
key ~2–3 ms + ~3 ms; End / Home ~6 ms (data) / ~9–13 ms (element) + ~7 ms.
|
|
548
|
+
Inserting is O(items): one `appendItems` costs ~5 ms at 5k items and ~21 ms at
|
|
549
|
+
50k (the source and search index are rebuilt, and `items` is copied); the
|
|
550
|
+
element-mode equivalents are ~8 ms at 2k and ~16 ms at 10k.
|
|
551
|
+
|
|
552
|
+
Things that keep the per-frame paths flat, and that the benchmark guards:
|
|
553
|
+
|
|
554
|
+
- **Cell recycling.** Cells are keyed by a stable cell id, not the item key, so
|
|
555
|
+
a cell scrolled out of the window is handed to an item scrolling in rather
|
|
556
|
+
than destroyed and recreated. (Item-keyed `repeat` gets steadily slower in
|
|
557
|
+
Chrome when every key in the window is new — a scrollbar drag or fling.) A
|
|
558
|
+
recycled cell carries the `recycled` class for one render, which disables its
|
|
559
|
+
transitions so it never animates from the previous item's state or position.
|
|
560
|
+
- **Per-cell slots (element mode).** Each cell has a fixed
|
|
561
|
+
`<slot name="gl-cell-<id>">`; the item it shows gets a matching `slot`
|
|
562
|
+
attribute, and items outside the window have none. Renaming slots instead
|
|
563
|
+
makes the browser re-match every light-DOM child.
|
|
564
|
+
- **The light DOM is only re-read when it changes** (child list, or an item's
|
|
565
|
+
`hidden` / `data-key` / `data-value`), not on every scroll / hover / cursor
|
|
566
|
+
update; `data-tickled` / `data-selected` are updated only on the items whose
|
|
567
|
+
state changed.
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
494
571
|
## CSS parts
|
|
495
572
|
|
|
496
573
|
| Part | Element |
|
package/docs-user/index.json
CHANGED
package/docs-user/miller.md
CHANGED
|
@@ -104,7 +104,9 @@ Explicit multi-select similar to a file manager:
|
|
|
104
104
|
| Shift+Click | Contiguous range from anchor to item (within same depth) |
|
|
105
105
|
| Cmd/Ctrl+Click | Toggle item without expanding (no anchor change) |
|
|
106
106
|
| Arrow Up/Down | Move focus; re-seats anchor |
|
|
107
|
-
|
|
|
107
|
+
| Home / End | Move focus to the first / last item in the column |
|
|
108
|
+
| Page Up / Page Down | Move focus up / down by one visible page (clamped to the column) |
|
|
109
|
+
| Shift+Arrow / Home / End / Page keys | Extend range from anchor (bootstraps anchor from current position if unset) |
|
|
108
110
|
| Enter | Select focused item (sets anchor) |
|
|
109
111
|
| Shift+Enter | Extend range to focused item |
|
|
110
112
|
|
|
@@ -118,7 +120,8 @@ Extends `standard` with persistent multi-selection across cursor movement:
|
|
|
118
120
|
| Shift+Click | Contiguous range from anchor — same as `standard` |
|
|
119
121
|
| Cmd/Ctrl+Click | Toggle item without expanding — same as `standard` |
|
|
120
122
|
| Arrow Up/Down | Move cursor **only** — selection is preserved, anchor is not re-seated |
|
|
121
|
-
|
|
|
123
|
+
| Home / End / Page Up / Page Down | Move cursor only, as Arrow Up/Down |
|
|
124
|
+
| Shift+Arrow / Home / End / Page keys | Extend range from anchor — same as `standard` |
|
|
122
125
|
| Arrow Right | Expand/navigate into branch — selection is preserved (does not clear) |
|
|
123
126
|
| Enter | Add focused item to selection (sets anchor) |
|
|
124
127
|
| Shift+Enter | Extend range to focused item |
|
|
@@ -426,13 +429,66 @@ Miller columns support full keyboard navigation when the component has focus:
|
|
|
426
429
|
| `Arrow Down` | Move focus to next item in column |
|
|
427
430
|
| `Arrow Right` | Select focused item and open its children (if any) |
|
|
428
431
|
| `Arrow Left` | Go back to previous column |
|
|
432
|
+
| `Home` / `End` | Move focus to the first / last item in the column |
|
|
433
|
+
| `Page Up` / `Page Down` | Move focus up / down by one visible page |
|
|
429
434
|
| `Enter` | Fire `select` event on focused item; in `standard` mode also selects and sets anchor |
|
|
430
|
-
| `Shift+Arrow` | In `standard` mode: extend selection range from anchor |
|
|
435
|
+
| `Shift+Arrow` / `Home` / `End` / `Page` keys | In `standard` mode: extend selection range from anchor |
|
|
431
436
|
| `Shift+Enter` | In `standard` mode: extend selection range to focused item |
|
|
432
437
|
| `Escape` | Cancel pending lazy load / go back one column |
|
|
433
438
|
|
|
434
439
|
---
|
|
435
440
|
|
|
441
|
+
## Performance
|
|
442
|
+
|
|
443
|
+
**Any change to `miller-base.ts` or `miller-list.ts` that touches `render`,
|
|
444
|
+
the per-item template, selection handling, cursor movement, column opening or
|
|
445
|
+
lazy loading should be checked with `pnpm bench:miller`.** Every update
|
|
446
|
+
re-renders every item in every open column, so an accidental O(items) (or
|
|
447
|
+
O(descendants), in `checked` mode) per-keystroke cost is easy to introduce and
|
|
448
|
+
shows up directly as key-repeat lag.
|
|
449
|
+
|
|
450
|
+
`scripts/bench/miller.ts` drives `demo/miller.html` in an isolated headless
|
|
451
|
+
Chrome (its own temp profile; set `CHROME_PATH` if Chrome isn't in a standard
|
|
452
|
+
location). It needs the dev server running (`pnpm start`). It mounts its own
|
|
453
|
+
list and runs two scenarios per item count (default 500 and 2000 rows per
|
|
454
|
+
column): `standard`, and `checked` with a loaded 20 × 10 subtree under most
|
|
455
|
+
rows (which the checkbox state scan must walk). It reports:
|
|
456
|
+
|
|
457
|
+
- `arrow` / `page` / `hover` — main-thread `script` and `render` ms per
|
|
458
|
+
Arrow Down, Page Up/Down, or hover (from a timeline trace).
|
|
459
|
+
- `resize` — the same, per frame of a column-resize drag.
|
|
460
|
+
- `openMs` — median ms to click a branch whose children are already loaded and
|
|
461
|
+
render the new column.
|
|
462
|
+
- `lazyOpenMs` — the same for an unloaded branch resolved by an instant
|
|
463
|
+
`loadChildren`. This exposes any artificial delay in the lazy-load path
|
|
464
|
+
(clicks must start loading immediately; only keyboard navigation is
|
|
465
|
+
debounced).
|
|
466
|
+
- `checkToggleMs` — median ms to toggle a branch checkbox (`checked` only).
|
|
467
|
+
|
|
468
|
+
Workflow — record a baseline on the unchanged code, make the change, compare:
|
|
469
|
+
|
|
470
|
+
```sh
|
|
471
|
+
pnpm bench:miller --out /tmp/miller-base.json # before
|
|
472
|
+
pnpm bench:miller --compare /tmp/miller-base.json # after
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
`--compare` prints each metric's % change and lists anything over
|
|
476
|
+
`--threshold` (default 50%, and never changes under 1 ms — run-to-run noise is
|
|
477
|
+
a few %). Add `--fail-on-regression` to exit non-zero. `--items 500,5000`
|
|
478
|
+
changes the column sizes. Baselines are machine-specific, so keep them local
|
|
479
|
+
rather than committing them. The dev server runs Lit in dev mode, so absolute
|
|
480
|
+
numbers are higher than a production build — compare runs against each other,
|
|
481
|
+
not against a budget.
|
|
482
|
+
|
|
483
|
+
Reference numbers (M-series Mac, dev mode, 2000 rows per column, `standard`):
|
|
484
|
+
an Arrow / Page / hover update is ~4 ms script + ~1 ms render; a resize frame
|
|
485
|
+
~9.5 ms render; opening a loaded column ~20 ms, a lazy one ~7 ms. In `checked`
|
|
486
|
+
mode with the 20 × 10 subtrees, key and hover updates are ~11-12 ms and a
|
|
487
|
+
checkbox toggle ~27 ms, because each render scans every visible row's loaded
|
|
488
|
+
subtree.
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
436
492
|
## Subclassing MillerBaseElement
|
|
437
493
|
|
|
438
494
|
Create custom column renderers by subclassing `MillerBaseElement`. Override
|
package/docs-user/tree.md
CHANGED
|
@@ -11,6 +11,7 @@ Three components share a common `TreeComponent` interface: **`ixfx-tree-list`**,
|
|
|
11
11
|
1. [Data model](#data-model)
|
|
12
12
|
2. [Properties](#properties)
|
|
13
13
|
- [Custom checkbox (`checkboxRenderer`)](#custom-checkbox-ixfx-tree-list-only--checkboxrenderer)
|
|
14
|
+
- [Virtualisation](#virtualisation-ixfx-tree-list)
|
|
14
15
|
3. [Selection](#selection)
|
|
15
16
|
4. [Events](#events)
|
|
16
17
|
5. [Keyboard navigation](#keyboard-navigation)
|
|
@@ -45,6 +46,8 @@ type TreeNode = {
|
|
|
45
46
|
|
|
46
47
|
`TreeNode` is **immutable** — structural changes always produce a new root. Never mutate a node in place; use `TreeDataModel` convenience methods or replace `root` directly.
|
|
47
48
|
|
|
49
|
+
**Escape hatch for in-place edits.** The component itself fills in `node.children` when lazy-loading, and `TreeController` does the same. If you must edit `node.children` or `node.item` (e.g. an icon) in place, call `el.invalidateStructure()` afterwards. It drops the cached whole-tree indexes (parent map, icon depths, filter matches, checkbox states) and re-renders. Visible rows are re-derived on every update, so ordinary expand / collapse never needs it.
|
|
50
|
+
|
|
48
51
|
---
|
|
49
52
|
|
|
50
53
|
### Static trees
|
|
@@ -140,9 +143,11 @@ These properties are present on all three components (`ixfx-tree-list`, `ixfx-mi
|
|
|
140
143
|
| `selectedNodes` | `ReadonlySet<TreeNode>` | `{}` | Full selection set (read-only) |
|
|
141
144
|
| `interactionMode` | `TreeInteractionMode` | `'standard'` | How gestures map to selection changes (`ixfx-tree-list` / `ixfx-miller-list` only) |
|
|
142
145
|
| `selectionFilter` | `'none' \| 'leaf' \| 'branch'` | `'leaf'` | Which node kinds can be selected (reflected attribute) |
|
|
143
|
-
| `filterPredicate` | `TreeFilterPredicate \| undefined` | `undefined` | Filter; only nodes
|
|
146
|
+
| `filterPredicate` | `TreeFilterPredicate \| undefined` | `undefined` | Filter; only matching nodes and their ancestors are rendered (non-matching rows are not in the DOM, keyboard navigation, `selectAll` and Shift-range skip them) |
|
|
144
147
|
| `exclusivity` | `'none' \| 'depth' \| 'global'` | `'none'` | Expansion exclusivity (reflected attribute) |
|
|
145
148
|
| `stickyHeaders` (`sticky-headers`) | `boolean` | `true` | VS Code-style sticky headers; set `sticky-headers="false"` to opt out (`ixfx-tree-list` only) |
|
|
149
|
+
| `virtualize` | `'auto' \| 'on' \| 'off'` | `'auto'` | Render only the rows near the viewport; `auto` switches on above `virtualizeThreshold` visible rows (`ixfx-tree-list` only). See [Virtualisation](#virtualisation-ixfx-tree-list) |
|
|
150
|
+
| `virtualizeThreshold` (`virtualize-threshold`) | `number` | `200` | Visible-row count above which `virtualize="auto"` windows the list |
|
|
146
151
|
|
|
147
152
|
### `interactionMode` (`ixfx-tree-list` / `ixfx-miller-list`)
|
|
148
153
|
|
|
@@ -220,6 +225,51 @@ On by default: while scrolling vertically, the ancestors of the top-visible row
|
|
|
220
225
|
- An ancestor that is itself still visible is never duplicated in the header.
|
|
221
226
|
- Style via `--tree-sticky-bg`, `--tree-sticky-border`, `--tree-sticky-shadow`, and the `sticky-headers` / `sticky-header` parts.
|
|
222
227
|
|
|
228
|
+
### Virtualisation (`ixfx-tree-list`)
|
|
229
|
+
|
|
230
|
+
A node with 10,000 children needs only about 25 `<li>` elements in the DOM. When
|
|
231
|
+
the number of visible rows (expanded rows, after any filter) exceeds
|
|
232
|
+
`virtualize-threshold` (default 200), `ixfx-tree-list` renders only the rows
|
|
233
|
+
near the viewport, plus 5 rows of overscan either side. Two spacer `<li>`s
|
|
234
|
+
hold the scroll height for the rest. Force it with `virtualize="on"` or disable
|
|
235
|
+
it with `virtualize="off"`; `data-virtual` is set on the host while the list is
|
|
236
|
+
windowed.
|
|
237
|
+
|
|
238
|
+
```html
|
|
239
|
+
<ixfx-tree-list virtualize="on" style="--tree-item-height: 28px"></ixfx-tree-list>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Everything is keyed by node, not by DOM element, so these behave exactly as
|
|
243
|
+
they do without windowing and cover rows that are not rendered:
|
|
244
|
+
|
|
245
|
+
- **Cursor, selection, `selectAll`, Shift-range** and **checkbox states**.
|
|
246
|
+
- **Keyboard**: arrows move across the whole list, and Home / End / PageUp / PageDown
|
|
247
|
+
jump by index. The cursor row is scrolled into view in the same frame, so it
|
|
248
|
+
is never missing.
|
|
249
|
+
- **Incremental search**: `navigate`, `select` and `filter` modes search the whole tree.
|
|
250
|
+
Match highlights are repainted as the window moves.
|
|
251
|
+
- **`navigateTo` / `revealKey`**: expand ancestors and scroll to the row.
|
|
252
|
+
- **Drag and drop**: drop targets are computed from the layout, so auto-scroll while dragging works.
|
|
253
|
+
- **Sticky headers**: found by arithmetic, with no per-row measuring.
|
|
254
|
+
|
|
255
|
+
Constraints while windowed:
|
|
256
|
+
|
|
257
|
+
- Every row must render at the same height. The list measures a rendered row's
|
|
258
|
+
box (which includes its padding and border, so it is taller than
|
|
259
|
+
`--tree-item-height` alone) and its top margin, and places rows by that stride.
|
|
260
|
+
Keep labels single-line (they truncate with an ellipsis) and custom row content within the row.
|
|
261
|
+
- **Expand / collapse and insert / remove rows do not animate.** The
|
|
262
|
+
fading "ghost" rows need variable heights. Below the threshold, or with
|
|
263
|
+
`virtualize="off"`, animation works as before.
|
|
264
|
+
- The sticky-header overlay is taken out of the layout flow so it cannot shift row positions.
|
|
265
|
+
- Rows outside the window do not exist in the DOM. Use the component's API
|
|
266
|
+
(`getNodes()`, `selectedNodes`, `revealKey()`) rather than `querySelector` to find a node.
|
|
267
|
+
|
|
268
|
+
Every row gets `aria-level`, `aria-setsize` and `aria-posinset`, counted over
|
|
269
|
+
**all** siblings (those matching the filter), so assistive technology
|
|
270
|
+
reports "5,000 of 10,000" even though most siblings are not in the DOM. The list
|
|
271
|
+
also exposes `aria-rowcount`.
|
|
272
|
+
|
|
223
273
|
---
|
|
224
274
|
|
|
225
275
|
## Selection
|
|
@@ -250,6 +300,8 @@ el.deselectAll();
|
|
|
250
300
|
| `checked` | Checks the checkbox of each visible node (recursively toggles descendants) | Unchecks each visible node |
|
|
251
301
|
| All other modes | Adds every visible selectable node to the selection | Clears the selection |
|
|
252
302
|
|
|
303
|
+
While a `filterPredicate` is active, `selectAll()` / `deselectAll()` and Shift-range selection cover only the rows that are shown (as in `ixfx-vertical-list`).
|
|
304
|
+
|
|
253
305
|
Both methods dispatch a single `select` event with the full old and new selection sets. `selectAll` respects `selectionFilter` — only nodes that pass the filter are selected.
|
|
254
306
|
|
|
255
307
|
### Reading the selection
|
|
@@ -423,9 +475,12 @@ The element must be focused (click it or tab to it) before keyboard navigation w
|
|
|
423
475
|
| `Arrow Right` (on leaf) | No-op |
|
|
424
476
|
| `Arrow Left` (on expanded branch) | Collapse branch |
|
|
425
477
|
| `Arrow Left` (on leaf or collapsed branch) | Move to parent |
|
|
478
|
+
| `Home` / `End` | Move cursor to the first / last visible item |
|
|
479
|
+
| `Page Up` / `Page Down` | Move cursor up / down by about one viewport |
|
|
426
480
|
| `Enter` | Select and activate focused item |
|
|
427
481
|
| `Escape` | Cancel in-flight lazy load |
|
|
428
482
|
|
|
483
|
+
Holding `Shift` with any movement key extends the range selection (in `standard` / `vscode` modes).
|
|
429
484
|
Moving the cursor fires a `tickle` event. Pressing Enter fires `select` then `activate`.
|
|
430
485
|
|
|
431
486
|
### `ixfx-miller-list`
|
|
@@ -540,6 +595,8 @@ Each tree-like component must implement the following. These are enforced by the
|
|
|
540
595
|
- [ ] `role="tree"` on the outermost interactive container
|
|
541
596
|
- [ ] `aria-label` or `aria-labelledby` to name the tree
|
|
542
597
|
|
|
598
|
+
`ixfx-tree-list` sets all of the item attributes below, including `aria-setsize` / `aria-posinset` (see [Virtualisation](#virtualisation-ixfx-tree-list)); the checklist is still outstanding for the other tree components.
|
|
599
|
+
|
|
543
600
|
**Each item element**
|
|
544
601
|
|
|
545
602
|
- [ ] `role="treeitem"`
|
|
@@ -573,6 +630,9 @@ at once the animation is skipped, as it is entirely under
|
|
|
573
630
|
`prefers-reduced-motion: reduce`. Duration: `--ixfx-list-item-move-duration`
|
|
574
631
|
(default `200ms`).
|
|
575
632
|
|
|
633
|
+
Typing in a filter search does not animate, and neither does a windowed
|
|
634
|
+
(virtualised) list: see [Virtualisation](#virtualisation-ixfx-tree-list).
|
|
635
|
+
|
|
576
636
|
---
|
|
577
637
|
|
|
578
638
|
## CSS variables
|
|
@@ -603,7 +663,8 @@ These are set by the global theme and inherited by all tree components.
|
|
|
603
663
|
| Variable | Default | Description |
|
|
604
664
|
|----------|---------|-------------|
|
|
605
665
|
| `--tree-indent` | `20px` | Indentation per depth level |
|
|
606
|
-
| `--tree-item-height` | `30px` |
|
|
666
|
+
| `--tree-item-height` | `30px` | Content height of each row (padding and border add to the rendered box) |
|
|
667
|
+
| `--tree-item-gap` | `1px` | Margin above each row |
|
|
607
668
|
| `--tree-caret-size` | `30px` | Expand/collapse caret size |
|
|
608
669
|
| `--ixfx-list-item-move-duration` | `200ms` | Row enter / leave animation duration |
|
|
609
670
|
| `--tree-drop-line` | `var(--accent)` | Colour of the before/after insertion line + nub |
|
|
@@ -588,6 +588,8 @@ The element must be focused (click it or tab to it) before keyboard navigation w
|
|
|
588
588
|
|---|---|
|
|
589
589
|
| `Arrow Down` | Move cursor to next visible item |
|
|
590
590
|
| `Arrow Up` | Move cursor to previous visible item |
|
|
591
|
+
| `Home` / `End` | Move cursor to the first / last item (across the whole list, including pooled rows when virtualized) |
|
|
592
|
+
| `Page Down` / `Page Up` | Move cursor by one viewport of rows |
|
|
591
593
|
| `Enter` | Activate the cursor item — fires `list-item-click` then `list-activate`; selects the item (except in `manual` mode) |
|
|
592
594
|
| `Space` | Toggle selection on the cursor item |
|
|
593
595
|
| `Shift+Arrow Down/Up` | Extend range from anchor (`vscode` mode only) |
|
|
@@ -756,7 +758,13 @@ dispatch. Mirrors `ixfx-grid-list`'s `grid-list-viewport-change`.
|
|
|
756
758
|
|
|
757
759
|
- **Drag-select is disabled** — the window shifts under a drag, so the marquee band can't be tracked reliably.
|
|
758
760
|
- **Incremental-search `filter` mode is excluded** — it collapses rows via `hidden`, corrupting the fixed-stride geometry. `navigate` and `select` modes still work.
|
|
759
|
-
|
|
761
|
+
|
|
762
|
+
### Search, keyboard and accessibility while virtualized
|
|
763
|
+
|
|
764
|
+
- **Incremental search covers every row**, rendered or pooled. Matches in `navigate` / `select` mode don't change as you scroll, and highlights repaint as the window moves.
|
|
765
|
+
- **Keyboard navigation spans the whole list** (arrows, Home/End, Page Up/Down, `item-wrap="wrap"`). The cursor is scrolled to the nearest edge only when it would otherwise leave the viewport.
|
|
766
|
+
- **Native multi-row drag** includes selected rows that are currently pooled; `ListDragEntry.index` is the row's position in the full list.
|
|
767
|
+
- **Accessibility:** attached rows get `aria-setsize` / `aria-posinset` for the whole list. Rows without a `role` get `role="option"` and `aria-selected`; a role you set yourself is preserved. The inner list is `role="listbox"` unless the host has its own `role`.
|
|
760
768
|
|
|
761
769
|
---
|
|
762
770
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ixfx/components",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.13.
|
|
4
|
+
"version": "0.13.3",
|
|
5
5
|
"packageManager": "pnpm@10.0.0",
|
|
6
6
|
"description": "Web components for ixfx — Lit-based UI toolkit",
|
|
7
7
|
"author": "Clint Heyer",
|
|
@@ -216,6 +216,11 @@
|
|
|
216
216
|
"scripts": {
|
|
217
217
|
"typecheck": "tsc --noEmit",
|
|
218
218
|
"test": "vitest run",
|
|
219
|
+
"bench:grid-list": "node --experimental-strip-types ./scripts/bench/grid-list.ts",
|
|
220
|
+
"bench:miller": "node --experimental-strip-types ./scripts/bench/miller.ts",
|
|
221
|
+
"bench:data-grid": "node --experimental-strip-types ./scripts/bench/data-grid.ts",
|
|
222
|
+
"bench:detail-list": "node --experimental-strip-types ./scripts/bench/detail-list.ts",
|
|
223
|
+
"test:perf": "PERF_REPORT=1 vitest run --reporter=verbose .perf.test",
|
|
219
224
|
"start": "vite serve",
|
|
220
225
|
"dev": "vite serve",
|
|
221
226
|
"lint": "eslint",
|