snice 7.8.0 → 7.9.0
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 +1 -1
- package/adapters/react/SniceProvider.js +1 -1
- package/adapters/react/SniceRouter.js +1 -1
- package/adapters/react/matchRoute.js +1 -1
- package/adapters/react/table.d.ts +1 -0
- package/adapters/react/table.d.ts.map +1 -1
- package/adapters/react/table.js +1 -1
- package/adapters/react/table.js.map +1 -1
- package/adapters/react/table.tsx +2 -1
- package/adapters/react/useRequestHandler.js +1 -1
- package/bin/analyzer-contracts.js +17 -1
- package/custom-elements.json +57 -3
- package/dist/cdn/accordion/snice-accordion.js +1 -1
- package/dist/cdn/action-bar/snice-action-bar.js +1 -1
- package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
- package/dist/cdn/alert/snice-alert.js +1 -1
- package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
- package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
- package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
- package/dist/cdn/availability/snice-availability.js +1 -1
- package/dist/cdn/avatar/snice-avatar.js +1 -1
- package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
- package/dist/cdn/badge/snice-badge.js +1 -1
- package/dist/cdn/banner/snice-banner.js +1 -1
- package/dist/cdn/binpack/snice-binpack.js +1 -1
- package/dist/cdn/book/snice-book.js +1 -1
- package/dist/cdn/booking/snice-booking.js +1 -1
- package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
- package/dist/cdn/button/snice-button.js +1 -1
- package/dist/cdn/calendar/README.md +2 -2
- package/dist/cdn/calendar/snice-calendar.js +537 -44
- package/dist/cdn/calendar/snice-calendar.js.map +1 -1
- package/dist/cdn/calendar/snice-calendar.min.js +2 -2
- package/dist/cdn/calendar/snice-calendar.min.js.map +1 -1
- package/dist/cdn/camera/snice-camera.js +1 -1
- package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
- package/dist/cdn/candlestick/snice-candlestick.js +1 -1
- package/dist/cdn/card/snice-card.js +1 -1
- package/dist/cdn/carousel/snice-carousel.js +1 -1
- package/dist/cdn/cart/snice-cart.js +1 -1
- package/dist/cdn/chart/snice-chart.js +1 -1
- package/dist/cdn/chat/snice-chat.js +1 -1
- package/dist/cdn/checkbox/snice-checkbox.js +1 -1
- package/dist/cdn/chip/snice-chip.js +1 -1
- package/dist/cdn/code-block/README.md +2 -2
- package/dist/cdn/code-block/snice-code-block.js +177 -167
- package/dist/cdn/code-block/snice-code-block.js.map +1 -1
- package/dist/cdn/code-block/snice-code-block.min.js +2 -2
- package/dist/cdn/code-block/snice-code-block.min.js.map +1 -1
- package/dist/cdn/color-display/snice-color-display.js +1 -1
- package/dist/cdn/color-picker/snice-color-picker.js +1 -1
- package/dist/cdn/command-palette/README.md +1 -1
- package/dist/cdn/command-palette/snice-command-palette.js +2 -2
- package/dist/cdn/command-palette/snice-command-palette.js.map +1 -1
- package/dist/cdn/command-palette/snice-command-palette.min.js +1 -1
- package/dist/cdn/command-palette/snice-command-palette.min.js.map +1 -1
- package/dist/cdn/comments/snice-comments.js +1 -1
- package/dist/cdn/countdown/snice-countdown.js +1 -1
- package/dist/cdn/cropper/snice-cropper.js +1 -1
- package/dist/cdn/data-card/snice-data-card.js +1 -1
- package/dist/cdn/date-picker/snice-date-picker.js +1 -1
- package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
- package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
- package/dist/cdn/diff/snice-diff.js +1 -1
- package/dist/cdn/divider/snice-divider.js +1 -1
- package/dist/cdn/doc/snice-doc.js +1 -1
- package/dist/cdn/draw/snice-draw.js +1 -1
- package/dist/cdn/drawer/snice-drawer.js +1 -1
- package/dist/cdn/empty-state/snice-empty-state.js +1 -1
- package/dist/cdn/estimate/snice-estimate.js +1 -1
- package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
- package/dist/cdn/file-upload/snice-file-upload.js +1 -1
- package/dist/cdn/flip-card/snice-flip-card.js +1 -1
- package/dist/cdn/flow/snice-flow.js +1 -1
- package/dist/cdn/form-layout/snice-form-layout.js +1 -1
- package/dist/cdn/funnel/snice-funnel.js +1 -1
- package/dist/cdn/gantt/snice-gantt.js +1 -1
- package/dist/cdn/gauge/snice-gauge.js +1 -1
- package/dist/cdn/grid/snice-grid.js +1 -1
- package/dist/cdn/heatmap/snice-heatmap.js +1 -1
- package/dist/cdn/image/snice-image.js +1 -1
- package/dist/cdn/input/snice-input.js +1 -1
- package/dist/cdn/invoice/snice-invoice.js +1 -1
- package/dist/cdn/kanban/snice-kanban.js +1 -1
- package/dist/cdn/key-value/snice-key-value.js +1 -1
- package/dist/cdn/kpi/snice-kpi.js +1 -1
- package/dist/cdn/layout/snice-layout.js +1 -1
- package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
- package/dist/cdn/link/snice-link.js +1 -1
- package/dist/cdn/link-preview/snice-link-preview.js +1 -1
- package/dist/cdn/list/snice-list.js +1 -1
- package/dist/cdn/location/snice-location.js +1 -1
- package/dist/cdn/login/snice-login.js +1 -1
- package/dist/cdn/map/snice-map.js +1 -1
- package/dist/cdn/markdown/snice-markdown.js +1 -1
- package/dist/cdn/masonry/snice-masonry.js +1 -1
- package/dist/cdn/menu/snice-menu.js +1 -1
- package/dist/cdn/message-strip/snice-message-strip.js +1 -1
- package/dist/cdn/modal/snice-modal.js +1 -1
- package/dist/cdn/music-player/snice-music-player.js +1 -1
- package/dist/cdn/nav/snice-nav.js +1 -1
- package/dist/cdn/network-graph/snice-network-graph.js +1 -1
- package/dist/cdn/notification-center/snice-notification-center.js +1 -1
- package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
- package/dist/cdn/org-chart/snice-org-chart.js +1 -1
- package/dist/cdn/pagination/snice-pagination.js +1 -1
- package/dist/cdn/paint/snice-paint.js +1 -1
- package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
- package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
- package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
- package/dist/cdn/popover/snice-popover.js +1 -1
- package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
- package/dist/cdn/product-card/snice-product-card.js +1 -1
- package/dist/cdn/progress/snice-progress.js +2 -2
- package/dist/cdn/progress/snice-progress.js.map +1 -1
- package/dist/cdn/progress/snice-progress.min.js +1 -1
- package/dist/cdn/progress/snice-progress.min.js.map +1 -1
- package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
- package/dist/cdn/qr-code/README.md +2 -2
- package/dist/cdn/qr-code/snice-qr-code.js +19 -3
- package/dist/cdn/qr-code/snice-qr-code.js.map +1 -1
- package/dist/cdn/qr-code/snice-qr-code.min.js +2 -2
- package/dist/cdn/qr-code/snice-qr-code.min.js.map +1 -1
- package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
- package/dist/cdn/radio/snice-radio.js +1 -1
- package/dist/cdn/range-slider/snice-range-slider.js +1 -1
- package/dist/cdn/rating/snice-rating.js +1 -1
- package/dist/cdn/receipt/snice-receipt.js +5 -2
- package/dist/cdn/receipt/snice-receipt.js.map +1 -1
- package/dist/cdn/receipt/snice-receipt.min.js +1 -1
- package/dist/cdn/receipt/snice-receipt.min.js.map +1 -1
- package/dist/cdn/recipe/snice-recipe.js +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.js +4 -4
- package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.js +4 -4
- package/dist/cdn/runtime/snice-runtime.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
- package/dist/cdn/sankey/snice-sankey.js +1 -1
- package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
- package/dist/cdn/select/README.md +2 -2
- package/dist/cdn/select/snice-select.js +6 -9
- package/dist/cdn/select/snice-select.js.map +1 -1
- package/dist/cdn/select/snice-select.min.js +2 -2
- package/dist/cdn/select/snice-select.min.js.map +1 -1
- package/dist/cdn/skeleton/snice-skeleton.js +1 -1
- package/dist/cdn/slider/snice-slider.js +1 -1
- package/dist/cdn/sortable/snice-sortable.js +1 -1
- package/dist/cdn/sparkline/snice-sparkline.js +1 -1
- package/dist/cdn/spinner/snice-spinner.js +1 -1
- package/dist/cdn/split-button/snice-split-button.js +1 -1
- package/dist/cdn/split-pane/snice-split-pane.js +1 -1
- package/dist/cdn/spotlight/snice-spotlight.js +1 -1
- package/dist/cdn/stat-group/snice-stat-group.js +1 -1
- package/dist/cdn/step-input/snice-step-input.js +1 -1
- package/dist/cdn/stepper/snice-stepper.js +1 -1
- package/dist/cdn/switch/snice-switch.js +1 -1
- package/dist/cdn/table/README.md +2 -2
- package/dist/cdn/table/snice-table.esm.js +1940 -407
- package/dist/cdn/table/snice-table.esm.js.map +1 -1
- package/dist/cdn/table/snice-table.esm.min.js +242 -53
- package/dist/cdn/table/snice-table.esm.min.js.map +1 -1
- package/dist/cdn/table/snice-table.js +1940 -407
- package/dist/cdn/table/snice-table.js.map +1 -1
- package/dist/cdn/table/snice-table.min.js +234 -45
- package/dist/cdn/table/snice-table.min.js.map +1 -1
- package/dist/cdn/tabs/snice-tabs.js +1 -1
- package/dist/cdn/tag/snice-tag.js +1 -1
- package/dist/cdn/tag-input/snice-tag-input.js +1 -1
- package/dist/cdn/terminal/snice-terminal.js +1 -1
- package/dist/cdn/testimonial/snice-testimonial.js +1 -1
- package/dist/cdn/textarea/snice-textarea.js +1 -1
- package/dist/cdn/time-picker/snice-time-picker.js +1 -1
- package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
- package/dist/cdn/timeline/snice-timeline.js +1 -1
- package/dist/cdn/timer/snice-timer.js +1 -1
- package/dist/cdn/toast/snice-toast.js +1 -1
- package/dist/cdn/tooltip/snice-tooltip.js +1 -1
- package/dist/cdn/tree/snice-tree.js +1 -1
- package/dist/cdn/treemap/snice-treemap.js +1 -1
- package/dist/cdn/user-card/snice-user-card.js +1 -1
- package/dist/cdn/video-player/snice-video-player.js +1 -1
- package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
- package/dist/cdn/waterfall/snice-waterfall.js +1 -1
- package/dist/cdn/weather/snice-weather.js +1 -1
- package/dist/cdn/work-order/snice-work-order.js +1 -1
- package/dist/components/calendar/snice-calendar.d.ts +187 -5
- package/dist/components/calendar/snice-calendar.js +537 -44
- package/dist/components/calendar/snice-calendar.js.map +1 -1
- package/dist/components/calendar/snice-calendar.types.d.ts +2 -1
- package/dist/components/code-block/formatter.d.ts +6 -0
- package/dist/components/code-block/formatter.js +174 -164
- package/dist/components/code-block/formatter.js.map +1 -1
- package/dist/components/code-block/grammars/snice.json +1 -1
- package/dist/components/code-block/grammars/typescript.json +1 -1
- package/dist/components/command-palette/snice-command-palette.js +1 -1
- package/dist/components/command-palette/snice-command-palette.js.map +1 -1
- package/dist/components/progress/snice-progress.js +1 -1
- package/dist/components/progress/snice-progress.js.map +1 -1
- package/dist/components/qr-code/qrcode.d.ts +1 -1
- package/dist/components/qr-code/qrcode.js +18 -2
- package/dist/components/qr-code/qrcode.js.map +1 -1
- package/dist/components/receipt/snice-receipt.js +4 -1
- package/dist/components/receipt/snice-receipt.js.map +1 -1
- package/dist/components/select/snice-select.d.ts +1 -1
- package/dist/components/select/snice-select.js +5 -8
- package/dist/components/select/snice-select.js.map +1 -1
- package/dist/components/table/snice-cell-actions.js +1 -1
- package/dist/components/table/snice-cell-actions.js.map +1 -1
- package/dist/components/table/snice-cell-color.js +1 -1
- package/dist/components/table/snice-cell-color.js.map +1 -1
- package/dist/components/table/snice-cell-date.js +20 -8
- package/dist/components/table/snice-cell-date.js.map +1 -1
- package/dist/components/table/snice-cell-email.js +1 -1
- package/dist/components/table/snice-cell-email.js.map +1 -1
- package/dist/components/table/snice-cell-image.js +1 -1
- package/dist/components/table/snice-cell-image.js.map +1 -1
- package/dist/components/table/snice-cell-json.js +1 -1
- package/dist/components/table/snice-cell-json.js.map +1 -1
- package/dist/components/table/snice-cell-link.js +1 -1
- package/dist/components/table/snice-cell-link.js.map +1 -1
- package/dist/components/table/snice-cell-location.js +1 -1
- package/dist/components/table/snice-cell-location.js.map +1 -1
- package/dist/components/table/snice-cell-phone.js +1 -1
- package/dist/components/table/snice-cell-phone.js.map +1 -1
- package/dist/components/table/snice-cell-status.js +1 -1
- package/dist/components/table/snice-cell-status.js.map +1 -1
- package/dist/components/table/snice-cell.d.ts +10 -0
- package/dist/components/table/snice-cell.js +59 -13
- package/dist/components/table/snice-cell.js.map +1 -1
- package/dist/components/table/snice-row.d.ts +0 -1
- package/dist/components/table/snice-row.js +4 -10
- package/dist/components/table/snice-row.js.map +1 -1
- package/dist/components/table/snice-table.d.ts +170 -2
- package/dist/components/table/snice-table.js +868 -189
- package/dist/components/table/snice-table.js.map +1 -1
- package/dist/components/table/snice-table.types.d.ts +17 -0
- package/dist/components/table/table-cell-presentation.d.ts +17 -4
- package/dist/components/table/table-cell-presentation.js +37 -11
- package/dist/components/table/table-cell-presentation.js.map +1 -1
- package/dist/components/table/table-column-manager.d.ts +153 -2
- package/dist/components/table/table-column-manager.js +364 -43
- package/dist/components/table/table-column-manager.js.map +1 -1
- package/dist/components/table/table-date.d.ts +29 -0
- package/dist/components/table/table-date.js +78 -0
- package/dist/components/table/table-date.js.map +1 -0
- package/dist/components/table/table-export.js +6 -2
- package/dist/components/table/table-export.js.map +1 -1
- package/dist/components/table/table-filter-engine.d.ts +9 -0
- package/dist/components/table/table-filter-engine.js +58 -14
- package/dist/components/table/table-filter-engine.js.map +1 -1
- package/dist/components/table/table-keyboard.d.ts +8 -0
- package/dist/components/table/table-keyboard.js +14 -3
- package/dist/components/table/table-keyboard.js.map +1 -1
- package/dist/components/table/table-toolbar.js +1 -0
- package/dist/components/table/table-toolbar.js.map +1 -1
- package/dist/components/table/table-tree-data.d.ts +2 -0
- package/dist/components/table/table-tree-data.js +35 -9
- package/dist/components/table/table-tree-data.js.map +1 -1
- package/dist/components/table/table-virtualizer.d.ts +44 -7
- package/dist/components/table/table-virtualizer.js +101 -54
- package/dist/components/table/table-virtualizer.js.map +1 -1
- package/dist/components/theme/theme.css +9 -0
- package/dist/index.cjs +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.iife.js +1 -1
- package/dist/react/SniceProvider.js +1 -1
- package/dist/react/SniceRouter.js +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/matchRoute.js +1 -1
- package/dist/react/useRequestHandler.js +1 -1
- package/dist/symbols.cjs +1 -1
- package/dist/symbols.esm.js +1 -1
- package/dist/testing-dom.cjs +1 -1
- package/dist/testing-dom.esm.js +1 -1
- package/dist/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/docs/ai/DEVELOPMENT.md +93 -2
- package/docs/ai/components/calendar.md +113 -10
- package/docs/ai/components/code-block.md +4 -0
- package/docs/ai/components/doc.md +1 -1
- package/docs/ai/components/progress.md +1 -0
- package/docs/ai/components/receipt.md +1 -1
- package/docs/ai/components/table.md +29 -30
- package/docs/ai/components/theme.md +1 -0
- package/docs/ai/events.md +30 -0
- package/docs/ai/theme.md +1 -0
- package/docs/components/calendar.md +171 -11
- package/docs/components/code-block.md +2 -0
- package/docs/components/doc.md +1 -1
- package/docs/components/progress.md +13 -0
- package/docs/components/receipt.md +1 -0
- package/docs/components/table.md +274 -15
- package/docs/components/theme.md +5 -0
- package/docs/events.md +49 -0
- package/docs/plans/2026-03-06-date-range-picker-plan.md +4 -4
- package/docs/plans/2026-03-07-table-mui-parity-design.md +1 -1
- package/docs/plans/2026-03-09-action-bar-plan.md +11 -11
- package/docs/superpowers/specs/2026-07-09-table-incredible-design.md +1 -1
- package/docs/superpowers/specs/2026-07-09-table-phase0-implementation.md +2 -2
- package/package.json +3 -1
- package/vscode.html-custom-data.json +13 -1
|
@@ -41,11 +41,24 @@ class TableVirtualizer {
|
|
|
41
41
|
window.removeEventListener('resize', this.scrollHandler);
|
|
42
42
|
cancelAnimationFrame(this.rafId);
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Publish a new row count. `repaint` false updates the model only — the
|
|
46
|
+
* pending re-render is still armed (lastStartIndex is reset), so the caller's
|
|
47
|
+
* own paint (scrollToIndex / refresh) performs it. Callers that resolve a
|
|
48
|
+
* display index and then scroll MUST use that form: painting here runs
|
|
49
|
+
* `afterRender`, and a host whose afterRender restores keyboard focus would
|
|
50
|
+
* re-enter this method from inside its own update.
|
|
51
|
+
*/
|
|
52
|
+
setTotalRows(total, repaint = true) {
|
|
45
53
|
this.options.totalRows = total;
|
|
46
54
|
this.invalidateOffsets();
|
|
47
55
|
this.lastStartIndex = -1; // Force re-render
|
|
48
|
-
|
|
56
|
+
if (repaint)
|
|
57
|
+
this.update();
|
|
58
|
+
}
|
|
59
|
+
/** Row count the virtualizer is currently windowing over. */
|
|
60
|
+
getTotalRows() {
|
|
61
|
+
return this.options.totalRows ?? 0;
|
|
49
62
|
}
|
|
50
63
|
/**
|
|
51
64
|
* Force a full re-render of the visible range.
|
|
@@ -56,11 +69,30 @@ class TableVirtualizer {
|
|
|
56
69
|
this.lastEndIndex = -1;
|
|
57
70
|
this.update();
|
|
58
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* The element that scrolls RIGHT NOW. A structural re-render of the host
|
|
74
|
+
* swaps the node captured at `attach()`; keep the listener and every lookup
|
|
75
|
+
* (notably the `<tbody>` we paint into) on the live one.
|
|
76
|
+
*/
|
|
77
|
+
resolveContainer() {
|
|
78
|
+
const live = this.options.resolveScrollContainer?.() ?? null;
|
|
79
|
+
const current = this.options.scrollContainer;
|
|
80
|
+
if (live && live !== current) {
|
|
81
|
+
current?.removeEventListener('scroll', this.scrollHandler);
|
|
82
|
+
live.addEventListener('scroll', this.scrollHandler, { passive: true });
|
|
83
|
+
this.options.scrollContainer = live;
|
|
84
|
+
// The new container's tbody is empty — the range we believed painted is
|
|
85
|
+
// not on screen anymore.
|
|
86
|
+
this.lastStartIndex = -1;
|
|
87
|
+
this.lastEndIndex = -1;
|
|
88
|
+
}
|
|
89
|
+
return this.options.scrollContainer ?? null;
|
|
90
|
+
}
|
|
59
91
|
/**
|
|
60
92
|
* Calculate visible range and render if changed.
|
|
61
93
|
*/
|
|
62
94
|
update() {
|
|
63
|
-
if (!this.enabled || !this.
|
|
95
|
+
if (!this.enabled || !this.resolveContainer())
|
|
64
96
|
return;
|
|
65
97
|
const { bufferPx, totalRows, scrollContainer, renderRange } = this.options;
|
|
66
98
|
const scrollTop = scrollContainer.scrollTop;
|
|
@@ -74,11 +106,12 @@ class TableVirtualizer {
|
|
|
74
106
|
// Filtering, pagination, or collapsing a group can shrink the flattened
|
|
75
107
|
// model while the container is scrolled near the old end. Clamp before
|
|
76
108
|
// deriving indices so startIndex never sits beyond totalRows and blanks the
|
|
77
|
-
// body.
|
|
78
|
-
//
|
|
109
|
+
// body. Only an offset that overruns the whole content is written back to
|
|
110
|
+
// the element — a browser already clamps its own scrollTop, and rewriting a
|
|
111
|
+
// deliberate offset (scrollToRow) would undo the caller's request.
|
|
79
112
|
const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
|
|
80
113
|
const effectiveScrollTop = Math.min(Math.max(0, scrollTop), maxScrollTop);
|
|
81
|
-
if (scrollTop
|
|
114
|
+
if (scrollTop > totalHeight || scrollTop < 0)
|
|
82
115
|
scrollContainer.scrollTop = effectiveScrollTop;
|
|
83
116
|
// Calculate visible range with buffer
|
|
84
117
|
const startPx = Math.max(0, effectiveScrollTop - bufferPx);
|
|
@@ -88,75 +121,89 @@ class TableVirtualizer {
|
|
|
88
121
|
// Skip if range hasn't changed
|
|
89
122
|
if (startIndex === this.lastStartIndex && endIndex === this.lastEndIndex)
|
|
90
123
|
return;
|
|
91
|
-
|
|
92
|
-
|
|
124
|
+
// Nothing to paint into yet (the host template has not committed its table,
|
|
125
|
+
// or a structural branch flip is mid-flight). Return WITHOUT recording the
|
|
126
|
+
// range: a dropped paint that looks like a completed one makes the guard
|
|
127
|
+
// above suppress every retry, and then only a viewport-height change (a
|
|
128
|
+
// window resize) ever repaints the rows.
|
|
129
|
+
const tbody = scrollContainer.querySelector('tbody');
|
|
130
|
+
if (!tbody)
|
|
131
|
+
return;
|
|
93
132
|
// Update spacers
|
|
94
133
|
const topHeight = offsets[startIndex] || 0;
|
|
95
134
|
const bottomHeight = Math.max(0, totalHeight - (offsets[endIndex] || totalHeight));
|
|
96
135
|
this.topSpacer.style.height = `${topHeight}px`;
|
|
97
136
|
this.bottomSpacer.style.height = `${bottomHeight}px`;
|
|
137
|
+
// Span the columns that exist, never more (see resolveColumnSpan).
|
|
138
|
+
const columnSpan = Math.max(1, Math.round(this.options.resolveColumnSpan?.() ?? 1));
|
|
98
139
|
// Render the visible range
|
|
99
140
|
const fragment = renderRange(startIndex, endIndex);
|
|
100
141
|
// Build the tbody content
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
bottomTd.style.padding = '0';
|
|
125
|
-
bottomTd.style.border = 'none';
|
|
126
|
-
this.bottomSpacer.innerHTML = '';
|
|
127
|
-
this.bottomSpacer.appendChild(bottomTd);
|
|
128
|
-
tbody.appendChild(this.bottomSpacer);
|
|
129
|
-
}
|
|
130
|
-
// Pinned-bottom rows render outside (below) the windowed range.
|
|
131
|
-
const pinnedBottom = this.options.renderPinnedBottom?.();
|
|
132
|
-
if (pinnedBottom)
|
|
133
|
-
tbody.appendChild(pinnedBottom);
|
|
134
|
-
this.options.afterRender?.();
|
|
142
|
+
tbody.innerHTML = '';
|
|
143
|
+
// Pinned-top rows render outside (above) the windowed range so they are
|
|
144
|
+
// always present. Rebuilt every window recompute since tbody is wiped.
|
|
145
|
+
const pinnedTop = this.options.renderPinnedTop?.();
|
|
146
|
+
if (pinnedTop)
|
|
147
|
+
tbody.appendChild(pinnedTop);
|
|
148
|
+
if (topHeight > 0) {
|
|
149
|
+
const topTd = document.createElement('td');
|
|
150
|
+
topTd.colSpan = columnSpan;
|
|
151
|
+
topTd.style.height = `${topHeight}px`;
|
|
152
|
+
topTd.style.padding = '0';
|
|
153
|
+
topTd.style.border = 'none';
|
|
154
|
+
this.topSpacer.innerHTML = '';
|
|
155
|
+
this.topSpacer.appendChild(topTd);
|
|
156
|
+
tbody.appendChild(this.topSpacer);
|
|
157
|
+
}
|
|
158
|
+
tbody.appendChild(fragment);
|
|
159
|
+
// Nothing to window over: hand the body back its empty/loading/error row
|
|
160
|
+
// instead of leaving the user staring at a blank frame.
|
|
161
|
+
if (totalRows === 0) {
|
|
162
|
+
const emptyBody = this.options.renderEmptyBody?.();
|
|
163
|
+
if (emptyBody)
|
|
164
|
+
tbody.appendChild(emptyBody);
|
|
135
165
|
}
|
|
166
|
+
if (bottomHeight > 0) {
|
|
167
|
+
const bottomTd = document.createElement('td');
|
|
168
|
+
bottomTd.colSpan = columnSpan;
|
|
169
|
+
bottomTd.style.height = `${bottomHeight}px`;
|
|
170
|
+
bottomTd.style.padding = '0';
|
|
171
|
+
bottomTd.style.border = 'none';
|
|
172
|
+
this.bottomSpacer.innerHTML = '';
|
|
173
|
+
this.bottomSpacer.appendChild(bottomTd);
|
|
174
|
+
tbody.appendChild(this.bottomSpacer);
|
|
175
|
+
}
|
|
176
|
+
// Pinned-bottom rows render outside (below) the windowed range.
|
|
177
|
+
const pinnedBottom = this.options.renderPinnedBottom?.();
|
|
178
|
+
if (pinnedBottom)
|
|
179
|
+
tbody.appendChild(pinnedBottom);
|
|
180
|
+
// The window is on screen — only NOW does this range count as painted.
|
|
181
|
+
// Committed before afterRender so a re-entrant update() from that hook
|
|
182
|
+
// (keyboard focus restoration scrolls) sees the range it is looking at.
|
|
183
|
+
this.lastStartIndex = startIndex;
|
|
184
|
+
this.lastEndIndex = endIndex;
|
|
185
|
+
this.options.afterRender?.();
|
|
136
186
|
}
|
|
137
187
|
/** Get the currently visible row index range */
|
|
138
188
|
getVisibleRange() {
|
|
139
189
|
return { start: this.lastStartIndex, end: this.lastEndIndex };
|
|
140
190
|
}
|
|
141
|
-
/** Scroll to bring a specific row into view */
|
|
142
|
-
scrollToRow(index) {
|
|
143
|
-
if (!this.options.scrollContainer)
|
|
144
|
-
return;
|
|
145
|
-
const top = this.offsetForIndex(index);
|
|
146
|
-
this.options.scrollContainer.scrollTop = top;
|
|
147
|
-
}
|
|
148
191
|
/**
|
|
149
|
-
* Scroll a
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
192
|
+
* Scroll a row into view AND synchronously render the window around it.
|
|
193
|
+
* A programmatic `scrollTop` assignment fires no scroll event, so the window
|
|
194
|
+
* has to be recomputed here — otherwise the viewport lands on bare spacer and
|
|
195
|
+
* the requested row is never in the DOM.
|
|
153
196
|
*/
|
|
154
|
-
|
|
197
|
+
scrollToRow(index) {
|
|
155
198
|
if (!this.options.scrollContainer)
|
|
156
199
|
return;
|
|
157
200
|
this.options.scrollContainer.scrollTop = this.offsetForIndex(index);
|
|
158
201
|
this.refresh();
|
|
159
202
|
}
|
|
203
|
+
/** Scroll to a logical (flattened display) row index. */
|
|
204
|
+
scrollToIndex(index) {
|
|
205
|
+
this.scrollToRow(index);
|
|
206
|
+
}
|
|
160
207
|
/** Get current scroll position */
|
|
161
208
|
getScrollPosition() {
|
|
162
209
|
const el = this.options.scrollContainer;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table-virtualizer.js","sources":["../../../../components/table/table-virtualizer.ts"],"sourcesContent":["/**\n * Row virtualization engine for snice-table.\n * Only renders visible rows + configurable buffer.\n * Uses spacer elements to maintain scroll height.\n */\n\nexport interface VirtualizerOptions {\n /** Row height in pixels, or a per-logical-row height resolver. */\n rowHeight: number | ((index: number) => number);\n /** Buffer size in pixels above and below viewport */\n bufferPx: number;\n /** Total number of rows in the dataset */\n totalRows: number;\n /** Container element that scrolls */\n scrollContainer: HTMLElement;\n /** Callback to render a range of rows */\n renderRange: (startIndex: number, endIndex: number) => DocumentFragment;\n /**\n * Optional: render pinned-top rows. These live OUTSIDE the windowed range\n * (before the top spacer) so they are always present regardless of scroll\n * position — the same guarantee the non-virtual path provides.\n */\n renderPinnedTop?: () => DocumentFragment;\n /** Optional: render pinned-bottom rows (after the bottom spacer). */\n renderPinnedBottom?: () => DocumentFragment;\n /** Called after the new window has been inserted into tbody. */\n afterRender?: () => void;\n}\n\nexport class TableVirtualizer {\n private options: VirtualizerOptions;\n private topSpacer: HTMLElement;\n private bottomSpacer: HTMLElement;\n private contentContainer: HTMLElement;\n private scrollHandler: () => void;\n private rafId = 0;\n private lastStartIndex = -1;\n private lastEndIndex = -1;\n private enabled = false;\n private offsets: number[] = [];\n private offsetsTotalRows = -1;\n\n constructor() {\n this.topSpacer = document.createElement('tr');\n this.topSpacer.className = 'virtual-spacer virtual-spacer--top';\n this.topSpacer.style.display = 'table-row';\n\n this.bottomSpacer = document.createElement('tr');\n this.bottomSpacer.className = 'virtual-spacer virtual-spacer--bottom';\n this.bottomSpacer.style.display = 'table-row';\n\n this.contentContainer = document.createElement('tbody');\n this.options = {} as VirtualizerOptions;\n\n this.scrollHandler = () => {\n if (this.rafId) cancelAnimationFrame(this.rafId);\n this.rafId = requestAnimationFrame(() => this.update());\n };\n }\n\n attach(options: VirtualizerOptions) {\n this.options = options;\n this.enabled = true;\n this.lastStartIndex = -1;\n this.lastEndIndex = -1;\n this.invalidateOffsets();\n this.options.scrollContainer.addEventListener('scroll', this.scrollHandler, { passive: true });\n window.addEventListener('resize', this.scrollHandler, { passive: true });\n this.update();\n }\n\n detach() {\n this.enabled = false;\n this.options.scrollContainer?.removeEventListener('scroll', this.scrollHandler);\n window.removeEventListener('resize', this.scrollHandler);\n cancelAnimationFrame(this.rafId);\n }\n\n setTotalRows(total: number) {\n this.options.totalRows = total;\n this.invalidateOffsets();\n this.lastStartIndex = -1; // Force re-render\n this.update();\n }\n\n /**\n * Force a full re-render of the visible range.\n */\n refresh() {\n this.invalidateOffsets();\n this.lastStartIndex = -1;\n this.lastEndIndex = -1;\n this.update();\n }\n\n /**\n * Calculate visible range and render if changed.\n */\n private update() {\n if (!this.enabled || !this.options.scrollContainer) return;\n\n const { bufferPx, totalRows, scrollContainer, renderRange } = this.options;\n const scrollTop = scrollContainer.scrollTop;\n const offsets = this.buildOffsets(totalRows);\n const minimumViewport = totalRows > 0 ? Math.max(1, offsets[1] - offsets[0]) : 1;\n // Layoutless DOMs report a zero-height viewport; treating one row as the\n // minimum keeps range math valid and mirrors the smallest useful browser\n // viewport.\n const viewportHeight = Math.max(minimumViewport, scrollContainer.clientHeight);\n\n const totalHeight = offsets[totalRows] || 0;\n\n // Filtering, pagination, or collapsing a group can shrink the flattened\n // model while the container is scrolled near the old end. Clamp before\n // deriving indices so startIndex never sits beyond totalRows and blanks the\n // body. Reflect the clamp to the element as well so its next scroll event\n // observes the same state.\n const maxScrollTop = Math.max(0, totalHeight - viewportHeight);\n const effectiveScrollTop = Math.min(Math.max(0, scrollTop), maxScrollTop);\n if (scrollTop !== effectiveScrollTop) scrollContainer.scrollTop = effectiveScrollTop;\n\n // Calculate visible range with buffer\n const startPx = Math.max(0, effectiveScrollTop - bufferPx);\n const endPx = Math.min(totalHeight, effectiveScrollTop + viewportHeight + bufferPx);\n\n const startIndex = this.findRowAtOffset(offsets, startPx);\n const endIndex = Math.min(totalRows, this.findFirstOffsetAtOrAfter(offsets, endPx));\n\n // Skip if range hasn't changed\n if (startIndex === this.lastStartIndex && endIndex === this.lastEndIndex) return;\n\n this.lastStartIndex = startIndex;\n this.lastEndIndex = endIndex;\n\n // Update spacers\n const topHeight = offsets[startIndex] || 0;\n const bottomHeight = Math.max(0, totalHeight - (offsets[endIndex] || totalHeight));\n\n this.topSpacer.style.height = `${topHeight}px`;\n this.bottomSpacer.style.height = `${bottomHeight}px`;\n\n // Render the visible range\n const fragment = renderRange(startIndex, endIndex);\n\n // Build the tbody content\n const tbody = this.options.scrollContainer.querySelector('tbody');\n if (tbody) {\n tbody.innerHTML = '';\n // Pinned-top rows render outside (above) the windowed range so they are\n // always present. Rebuilt every window recompute since tbody is wiped.\n const pinnedTop = this.options.renderPinnedTop?.();\n if (pinnedTop) tbody.appendChild(pinnedTop);\n if (topHeight > 0) {\n const topTd = document.createElement('td');\n topTd.colSpan = 999;\n topTd.style.height = `${topHeight}px`;\n topTd.style.padding = '0';\n topTd.style.border = 'none';\n this.topSpacer.innerHTML = '';\n this.topSpacer.appendChild(topTd);\n tbody.appendChild(this.topSpacer);\n }\n tbody.appendChild(fragment);\n if (bottomHeight > 0) {\n const bottomTd = document.createElement('td');\n bottomTd.colSpan = 999;\n bottomTd.style.height = `${bottomHeight}px`;\n bottomTd.style.padding = '0';\n bottomTd.style.border = 'none';\n this.bottomSpacer.innerHTML = '';\n this.bottomSpacer.appendChild(bottomTd);\n tbody.appendChild(this.bottomSpacer);\n }\n // Pinned-bottom rows render outside (below) the windowed range.\n const pinnedBottom = this.options.renderPinnedBottom?.();\n if (pinnedBottom) tbody.appendChild(pinnedBottom);\n this.options.afterRender?.();\n }\n }\n\n /** Get the currently visible row index range */\n getVisibleRange(): { start: number; end: number } {\n return { start: this.lastStartIndex, end: this.lastEndIndex };\n }\n\n /** Scroll to bring a specific row into view */\n scrollToRow(index: number) {\n if (!this.options.scrollContainer) return;\n const top = this.offsetForIndex(index);\n this.options.scrollContainer.scrollTop = top;\n }\n\n /**\n * Scroll a logical row index into view AND synchronously render its range.\n * Unlike `scrollToRow`, this does not rely on a scroll event (a programmatic\n * scrollTop assignment does not fire one) — it forces the window recompute\n * immediately so the row is in the DOM by the time the caller focuses it.\n */\n scrollToIndex(index: number) {\n if (!this.options.scrollContainer) return;\n this.options.scrollContainer.scrollTop = this.offsetForIndex(index);\n this.refresh();\n }\n\n /** Get current scroll position */\n getScrollPosition(): { top: number; left: number } {\n const el = this.options.scrollContainer;\n return { top: el?.scrollTop ?? 0, left: el?.scrollLeft ?? 0 };\n }\n\n isEnabled() {\n return this.enabled;\n }\n\n private heightForIndex(index: number): number {\n const configured = typeof this.options.rowHeight === 'function'\n ? this.options.rowHeight(index)\n : this.options.rowHeight;\n return Number.isFinite(configured) && configured > 0 ? configured : 1;\n }\n\n private buildOffsets(totalRows: number): number[] {\n if (this.offsetsTotalRows === totalRows && this.offsets.length === totalRows + 1) {\n return this.offsets;\n }\n this.offsets = new Array(totalRows + 1);\n this.offsets[0] = 0;\n for (let index = 0; index < totalRows; index++) {\n this.offsets[index + 1] = this.offsets[index] + this.heightForIndex(index);\n }\n this.offsetsTotalRows = totalRows;\n return this.offsets;\n }\n\n private invalidateOffsets() {\n this.offsets = [];\n this.offsetsTotalRows = -1;\n }\n\n private offsetForIndex(index: number): number {\n const clamped = Math.max(0, Math.min(index, this.options.totalRows));\n return this.buildOffsets(this.options.totalRows)[clamped] || 0;\n }\n\n /** Index of the logical row containing the offset. */\n private findRowAtOffset(offsets: number[], offset: number): number {\n const totalRows = offsets.length - 1;\n if (totalRows <= 0) return 0;\n let low = 0;\n let high = totalRows;\n while (low < high) {\n const mid = Math.floor((low + high) / 2);\n if (offsets[mid + 1] <= offset) low = mid + 1;\n else high = mid;\n }\n return Math.min(totalRows, low);\n }\n\n /** First prefix offset at or after the requested pixel. */\n private findFirstOffsetAtOrAfter(offsets: number[], offset: number): number {\n let low = 0;\n let high = offsets.length - 1;\n while (low < high) {\n const mid = Math.floor((low + high) / 2);\n if (offsets[mid] < offset) low = mid + 1;\n else high = mid;\n }\n return low;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"table-virtualizer.js","sources":["../../../../components/table/table-virtualizer.ts"],"sourcesContent":["/**\n * Row virtualization engine for snice-table.\n * Only renders visible rows + configurable buffer.\n * Uses spacer elements to maintain scroll height.\n */\n\nexport interface VirtualizerOptions {\n /** Row height in pixels, or a per-logical-row height resolver. */\n rowHeight: number | ((index: number) => number);\n /** Buffer size in pixels above and below viewport */\n bufferPx: number;\n /** Total number of rows in the dataset */\n totalRows: number;\n /** Container element that scrolls */\n scrollContainer: HTMLElement;\n /**\n * Optional: re-resolve the scrolling container on every update. A host whose\n * template flips structural branches replaces the element captured at\n * `attach()`; without this the virtualizer keeps painting into the DETACHED\n * subtree's orphaned `<tbody>` while the live one stays empty.\n */\n resolveScrollContainer?: () => HTMLElement | null;\n /** Callback to render a range of rows */\n renderRange: (startIndex: number, endIndex: number) => DocumentFragment;\n /**\n * How many columns a spacer row spans. Required in practice: a spacer that\n * over-spans invents columns, and `table-layout: fixed` then hands the\n * frame's spare width to the phantom ones — the real columns stop short of\n * the right edge with a dead strip beside them.\n */\n resolveColumnSpan?: () => number;\n /**\n * Optional: render pinned-top rows. These live OUTSIDE the windowed range\n * (before the top spacer) so they are always present regardless of scroll\n * position — the same guarantee the non-virtual path provides.\n */\n renderPinnedTop?: () => DocumentFragment;\n /** Optional: render pinned-bottom rows (after the bottom spacer). */\n renderPinnedBottom?: () => DocumentFragment;\n /**\n * Optional: the single message row a zero-row body shows (loading, error, or\n * empty state). Called only when `totalRows` is 0, so the virtualized body\n * carries the same states the ordinary render path does instead of being\n * wiped to nothing.\n */\n renderEmptyBody?: () => Node | null;\n /** Called after the new window has been inserted into tbody. */\n afterRender?: () => void;\n}\n\nexport class TableVirtualizer {\n private options: VirtualizerOptions;\n private topSpacer: HTMLElement;\n private bottomSpacer: HTMLElement;\n private contentContainer: HTMLElement;\n private scrollHandler: () => void;\n private rafId = 0;\n private lastStartIndex = -1;\n private lastEndIndex = -1;\n private enabled = false;\n private offsets: number[] = [];\n private offsetsTotalRows = -1;\n\n constructor() {\n this.topSpacer = document.createElement('tr');\n this.topSpacer.className = 'virtual-spacer virtual-spacer--top';\n this.topSpacer.style.display = 'table-row';\n\n this.bottomSpacer = document.createElement('tr');\n this.bottomSpacer.className = 'virtual-spacer virtual-spacer--bottom';\n this.bottomSpacer.style.display = 'table-row';\n\n this.contentContainer = document.createElement('tbody');\n this.options = {} as VirtualizerOptions;\n\n this.scrollHandler = () => {\n if (this.rafId) cancelAnimationFrame(this.rafId);\n this.rafId = requestAnimationFrame(() => this.update());\n };\n }\n\n attach(options: VirtualizerOptions) {\n this.options = options;\n this.enabled = true;\n this.lastStartIndex = -1;\n this.lastEndIndex = -1;\n this.invalidateOffsets();\n this.options.scrollContainer.addEventListener('scroll', this.scrollHandler, { passive: true });\n window.addEventListener('resize', this.scrollHandler, { passive: true });\n this.update();\n }\n\n detach() {\n this.enabled = false;\n this.options.scrollContainer?.removeEventListener('scroll', this.scrollHandler);\n window.removeEventListener('resize', this.scrollHandler);\n cancelAnimationFrame(this.rafId);\n }\n\n /**\n * Publish a new row count. `repaint` false updates the model only — the\n * pending re-render is still armed (lastStartIndex is reset), so the caller's\n * own paint (scrollToIndex / refresh) performs it. Callers that resolve a\n * display index and then scroll MUST use that form: painting here runs\n * `afterRender`, and a host whose afterRender restores keyboard focus would\n * re-enter this method from inside its own update.\n */\n setTotalRows(total: number, repaint = true) {\n this.options.totalRows = total;\n this.invalidateOffsets();\n this.lastStartIndex = -1; // Force re-render\n if (repaint) this.update();\n }\n\n /** Row count the virtualizer is currently windowing over. */\n getTotalRows() {\n return this.options.totalRows ?? 0;\n }\n\n /**\n * Force a full re-render of the visible range.\n */\n refresh() {\n this.invalidateOffsets();\n this.lastStartIndex = -1;\n this.lastEndIndex = -1;\n this.update();\n }\n\n /**\n * The element that scrolls RIGHT NOW. A structural re-render of the host\n * swaps the node captured at `attach()`; keep the listener and every lookup\n * (notably the `<tbody>` we paint into) on the live one.\n */\n private resolveContainer(): HTMLElement | null {\n const live = this.options.resolveScrollContainer?.() ?? null;\n const current = this.options.scrollContainer;\n if (live && live !== current) {\n current?.removeEventListener('scroll', this.scrollHandler);\n live.addEventListener('scroll', this.scrollHandler, { passive: true });\n this.options.scrollContainer = live;\n // The new container's tbody is empty — the range we believed painted is\n // not on screen anymore.\n this.lastStartIndex = -1;\n this.lastEndIndex = -1;\n }\n return this.options.scrollContainer ?? null;\n }\n\n /**\n * Calculate visible range and render if changed.\n */\n private update() {\n if (!this.enabled || !this.resolveContainer()) return;\n\n const { bufferPx, totalRows, scrollContainer, renderRange } = this.options;\n const scrollTop = scrollContainer.scrollTop;\n const offsets = this.buildOffsets(totalRows);\n const minimumViewport = totalRows > 0 ? Math.max(1, offsets[1] - offsets[0]) : 1;\n // Layoutless DOMs report a zero-height viewport; treating one row as the\n // minimum keeps range math valid and mirrors the smallest useful browser\n // viewport.\n const viewportHeight = Math.max(minimumViewport, scrollContainer.clientHeight);\n\n const totalHeight = offsets[totalRows] || 0;\n\n // Filtering, pagination, or collapsing a group can shrink the flattened\n // model while the container is scrolled near the old end. Clamp before\n // deriving indices so startIndex never sits beyond totalRows and blanks the\n // body. Only an offset that overruns the whole content is written back to\n // the element — a browser already clamps its own scrollTop, and rewriting a\n // deliberate offset (scrollToRow) would undo the caller's request.\n const maxScrollTop = Math.max(0, totalHeight - viewportHeight);\n const effectiveScrollTop = Math.min(Math.max(0, scrollTop), maxScrollTop);\n if (scrollTop > totalHeight || scrollTop < 0) scrollContainer.scrollTop = effectiveScrollTop;\n\n // Calculate visible range with buffer\n const startPx = Math.max(0, effectiveScrollTop - bufferPx);\n const endPx = Math.min(totalHeight, effectiveScrollTop + viewportHeight + bufferPx);\n\n const startIndex = this.findRowAtOffset(offsets, startPx);\n const endIndex = Math.min(totalRows, this.findFirstOffsetAtOrAfter(offsets, endPx));\n\n // Skip if range hasn't changed\n if (startIndex === this.lastStartIndex && endIndex === this.lastEndIndex) return;\n\n // Nothing to paint into yet (the host template has not committed its table,\n // or a structural branch flip is mid-flight). Return WITHOUT recording the\n // range: a dropped paint that looks like a completed one makes the guard\n // above suppress every retry, and then only a viewport-height change (a\n // window resize) ever repaints the rows.\n const tbody = scrollContainer.querySelector('tbody');\n if (!tbody) return;\n\n // Update spacers\n const topHeight = offsets[startIndex] || 0;\n const bottomHeight = Math.max(0, totalHeight - (offsets[endIndex] || totalHeight));\n\n this.topSpacer.style.height = `${topHeight}px`;\n this.bottomSpacer.style.height = `${bottomHeight}px`;\n\n // Span the columns that exist, never more (see resolveColumnSpan).\n const columnSpan = Math.max(1, Math.round(this.options.resolveColumnSpan?.() ?? 1));\n\n // Render the visible range\n const fragment = renderRange(startIndex, endIndex);\n\n // Build the tbody content\n tbody.innerHTML = '';\n // Pinned-top rows render outside (above) the windowed range so they are\n // always present. Rebuilt every window recompute since tbody is wiped.\n const pinnedTop = this.options.renderPinnedTop?.();\n if (pinnedTop) tbody.appendChild(pinnedTop);\n if (topHeight > 0) {\n const topTd = document.createElement('td');\n topTd.colSpan = columnSpan;\n topTd.style.height = `${topHeight}px`;\n topTd.style.padding = '0';\n topTd.style.border = 'none';\n this.topSpacer.innerHTML = '';\n this.topSpacer.appendChild(topTd);\n tbody.appendChild(this.topSpacer);\n }\n tbody.appendChild(fragment);\n // Nothing to window over: hand the body back its empty/loading/error row\n // instead of leaving the user staring at a blank frame.\n if (totalRows === 0) {\n const emptyBody = this.options.renderEmptyBody?.();\n if (emptyBody) tbody.appendChild(emptyBody);\n }\n if (bottomHeight > 0) {\n const bottomTd = document.createElement('td');\n bottomTd.colSpan = columnSpan;\n bottomTd.style.height = `${bottomHeight}px`;\n bottomTd.style.padding = '0';\n bottomTd.style.border = 'none';\n this.bottomSpacer.innerHTML = '';\n this.bottomSpacer.appendChild(bottomTd);\n tbody.appendChild(this.bottomSpacer);\n }\n // Pinned-bottom rows render outside (below) the windowed range.\n const pinnedBottom = this.options.renderPinnedBottom?.();\n if (pinnedBottom) tbody.appendChild(pinnedBottom);\n\n // The window is on screen — only NOW does this range count as painted.\n // Committed before afterRender so a re-entrant update() from that hook\n // (keyboard focus restoration scrolls) sees the range it is looking at.\n this.lastStartIndex = startIndex;\n this.lastEndIndex = endIndex;\n this.options.afterRender?.();\n }\n\n /** Get the currently visible row index range */\n getVisibleRange(): { start: number; end: number } {\n return { start: this.lastStartIndex, end: this.lastEndIndex };\n }\n\n /**\n * Scroll a row into view AND synchronously render the window around it.\n * A programmatic `scrollTop` assignment fires no scroll event, so the window\n * has to be recomputed here — otherwise the viewport lands on bare spacer and\n * the requested row is never in the DOM.\n */\n scrollToRow(index: number) {\n if (!this.options.scrollContainer) return;\n this.options.scrollContainer.scrollTop = this.offsetForIndex(index);\n this.refresh();\n }\n\n /** Scroll to a logical (flattened display) row index. */\n scrollToIndex(index: number) {\n this.scrollToRow(index);\n }\n\n /** Get current scroll position */\n getScrollPosition(): { top: number; left: number } {\n const el = this.options.scrollContainer;\n return { top: el?.scrollTop ?? 0, left: el?.scrollLeft ?? 0 };\n }\n\n isEnabled() {\n return this.enabled;\n }\n\n private heightForIndex(index: number): number {\n const configured = typeof this.options.rowHeight === 'function'\n ? this.options.rowHeight(index)\n : this.options.rowHeight;\n return Number.isFinite(configured) && configured > 0 ? configured : 1;\n }\n\n private buildOffsets(totalRows: number): number[] {\n if (this.offsetsTotalRows === totalRows && this.offsets.length === totalRows + 1) {\n return this.offsets;\n }\n this.offsets = new Array(totalRows + 1);\n this.offsets[0] = 0;\n for (let index = 0; index < totalRows; index++) {\n this.offsets[index + 1] = this.offsets[index] + this.heightForIndex(index);\n }\n this.offsetsTotalRows = totalRows;\n return this.offsets;\n }\n\n private invalidateOffsets() {\n this.offsets = [];\n this.offsetsTotalRows = -1;\n }\n\n private offsetForIndex(index: number): number {\n const clamped = Math.max(0, Math.min(index, this.options.totalRows));\n return this.buildOffsets(this.options.totalRows)[clamped] || 0;\n }\n\n /** Index of the logical row containing the offset. */\n private findRowAtOffset(offsets: number[], offset: number): number {\n const totalRows = offsets.length - 1;\n if (totalRows <= 0) return 0;\n let low = 0;\n let high = totalRows;\n while (low < high) {\n const mid = Math.floor((low + high) / 2);\n if (offsets[mid + 1] <= offset) low = mid + 1;\n else high = mid;\n }\n return Math.min(totalRows, low);\n }\n\n /** First prefix offset at or after the requested pixel. */\n private findFirstOffsetAtOrAfter(offsets: number[], offset: number): number {\n let low = 0;\n let high = offsets.length - 1;\n while (low < high) {\n const mid = Math.floor((low + high) / 2);\n if (offsets[mid] < offset) low = mid + 1;\n else high = mid;\n }\n return low;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -129,6 +129,11 @@
|
|
|
129
129
|
--snice-color-overlay-hover: hsl(0 0% 0% / 0.04);
|
|
130
130
|
--snice-color-overlay-selected: hsl(var(--snice-color-blue-500) / 0.08);
|
|
131
131
|
--snice-color-overlay-selected-hover: hsl(var(--snice-color-blue-500) / 0.12);
|
|
132
|
+
/* Zebra tint for alternating rows. Not a surface step: a solid surface token
|
|
133
|
+
lands within a couple of luminance points of the body on ONE of the two
|
|
134
|
+
themes and the striping disappears there. An alpha tint is tuned per
|
|
135
|
+
theme, and composites over pinned/selected/tinted cells alike. */
|
|
136
|
+
--snice-color-overlay-stripe: hsl(0 0% 0% / 0.02);
|
|
132
137
|
|
|
133
138
|
--snice-color-primary-subtle: hsl(var(--snice-color-blue-50));
|
|
134
139
|
--snice-color-primary-subtle-hover: hsl(var(--snice-color-blue-100));
|
|
@@ -342,6 +347,9 @@
|
|
|
342
347
|
--snice-color-overlay-hover: hsl(0 0% 100% / 0.06);
|
|
343
348
|
--snice-color-overlay-selected: hsl(var(--snice-color-blue-400) / 0.16);
|
|
344
349
|
--snice-color-overlay-selected-hover: hsl(var(--snice-color-blue-400) / 0.22);
|
|
350
|
+
/* Dark surfaces need a much larger step than light ones: the same tint
|
|
351
|
+
that reads as a stripe on white is invisible on near-black. */
|
|
352
|
+
--snice-color-overlay-stripe: hsl(0 0% 100% / 0.11);
|
|
345
353
|
|
|
346
354
|
--snice-color-primary-subtle: hsl(var(--snice-color-blue-950));
|
|
347
355
|
--snice-color-primary-subtle-hover: hsl(var(--snice-color-blue-900));
|
|
@@ -405,6 +413,7 @@
|
|
|
405
413
|
--snice-color-border-focus: hsl(var(--snice-color-blue-400));
|
|
406
414
|
|
|
407
415
|
--snice-color-surface-hover: hsl(var(--snice-color-gray-700));
|
|
416
|
+
--snice-color-overlay-stripe: hsl(0 0% 100% / 0.11);
|
|
408
417
|
--snice-color-surface-active: hsl(var(--snice-color-gray-600));
|
|
409
418
|
--snice-color-surface-disabled: hsl(var(--snice-color-gray-800));
|
|
410
419
|
--snice-color-text-disabled: hsl(var(--snice-color-gray-600));
|
package/dist/index.cjs
CHANGED
package/dist/index.esm.js
CHANGED
package/dist/index.iife.js
CHANGED
package/dist/react/index.js
CHANGED
package/dist/react/matchRoute.js
CHANGED
package/dist/symbols.cjs
CHANGED
package/dist/symbols.esm.js
CHANGED
package/dist/testing-dom.cjs
CHANGED
package/dist/testing-dom.esm.js
CHANGED
package/dist/transitions.cjs
CHANGED
package/dist/transitions.esm.js
CHANGED
package/docs/ai/DEVELOPMENT.md
CHANGED
|
@@ -35,6 +35,8 @@ npm run generate:react-tests # Generate test files
|
|
|
35
35
|
```bash
|
|
36
36
|
npm test # Required complete gate (source+built+browser+site)
|
|
37
37
|
npm run test:source # Tests importing package sources
|
|
38
|
+
npm run test:matrix # Component feature-combination matrices (fuzz tier, opt-in)
|
|
39
|
+
npm run test:matrix:visual # Same matrices in a real browser (on demand, chromium)
|
|
38
40
|
npm run test:distribution # Fresh build, then same tests against dist/
|
|
39
41
|
npm run test:cdn # Fresh CDN build + artifact/runtime tests
|
|
40
42
|
npm run test:react # Fresh adapters + React tests
|
|
@@ -52,6 +54,95 @@ npm run gauntlet # Blind local-model checker gauntlet
|
|
|
52
54
|
`repeat.ts`, `snice-element.ts`, and `template.ts`; every aggregate metric must
|
|
53
55
|
be strictly greater than 90%. Browser commands manage their own local servers.
|
|
54
56
|
|
|
57
|
+
## Test Tiers
|
|
58
|
+
|
|
59
|
+
| Tier | Command | Cost | When |
|
|
60
|
+
|------|---------|------|------|
|
|
61
|
+
| Everyday | `vitest run` / `npm run test:source` | ~26s for `tests/components` | Every change |
|
|
62
|
+
| Matrix (fuzz) | `npm run test:matrix` | Minutes; all components | Any component rendering change; once inside `npm test` |
|
|
63
|
+
| Visual | Playwright (`tests/live`) | Slowest | On request |
|
|
64
|
+
| Visual matrix | `npm run test:matrix:visual` | Minutes; every component's specs in `tests/live/matrix/` | On demand only; never in the gate |
|
|
65
|
+
|
|
66
|
+
- Canonical layout: `tests/matrix/<component>/*.test.ts` (matrix),
|
|
67
|
+
`tests/matrix/<component>/smoke.test.ts` (default-loop slice),
|
|
68
|
+
`tests/matrix/*.ts` (shared oracles),
|
|
69
|
+
`tests/live/matrix/<component>/<component>-visual.spec.ts` (visual tier),
|
|
70
|
+
`tests/live/fixtures/<component>/` (fixtures).
|
|
71
|
+
- `vitest.config.ts` `test.exclude` drops `tests/matrix/**/!(smoke).test.ts`
|
|
72
|
+
from the default include, the same way `tests/live` is dropped. Plain
|
|
73
|
+
`vitest run` pays for the smoke slices only.
|
|
74
|
+
- `npm run test:matrix` = `vitest run --config vitest.matrix.config.ts`. That
|
|
75
|
+
config inherits `vitest.config.ts` and replaces `exclude` with the base list
|
|
76
|
+
minus the matrix pattern (mergeConfig concatenates arrays, so inheriting it
|
|
77
|
+
would keep the entry that hides the suites). It throws if the base config
|
|
78
|
+
stops excluding the pattern. Smoke slices run in both tiers — intended.
|
|
79
|
+
- Each `smoke.test.ts` stays IN the default include: one combo per feature
|
|
80
|
+
family, plus the marquee regressions. Budget a few seconds per component —
|
|
81
|
+
add new combos to the matrix, not to the smoke file.
|
|
82
|
+
- Helper modules under `tests/matrix/` (`matrix-utils.ts`, the per-slice
|
|
83
|
+
`*-support.ts`) stay importable from outside; the exclusion removes files
|
|
84
|
+
from test COLLECTION, not from module resolution.
|
|
85
|
+
- `npm test` runs the matrices as their own `matrix suite` stage, exactly
|
|
86
|
+
once, source-flavoured (see `tooling/testing/run-full-tests.js`).
|
|
87
|
+
|
|
88
|
+
## True-Visual Matrix (on demand)
|
|
89
|
+
|
|
90
|
+
`tests/live/matrix/` — the real-browser twin of `tests/matrix/`.
|
|
91
|
+
happy-dom does no layout, so the DOM matrix owns VALUE truth only; this tier
|
|
92
|
+
owns VISUAL truth.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm run test:matrix:visual # chromium only (default)
|
|
96
|
+
npm run test:matrix:visual -- --all-engines # chromium + firefox + webkit
|
|
97
|
+
npm run test:matrix:visual -- --project=webkit # explicit engine wins
|
|
98
|
+
npm run test:matrix:visual -- --grep squish # all other flags pass through
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- ON DEMAND. Not in `npm test`; `tests/playwright.config.ts` sets
|
|
102
|
+
`testIgnore: ['live/matrix/**']` so `test:browser:framework` (targets all of
|
|
103
|
+
`tests/live`) cannot sweep it in. Own config: `tests/playwright.matrix.config.ts`.
|
|
104
|
+
- Runner `tooling/testing/run-visual-matrix-tests.js` manages vite on :5566 —
|
|
105
|
+
probes `127.0.0.1` AND `[::1]` (servers bind v6), reuses a listener, kills
|
|
106
|
+
only a server it started. It then requires the fixture URL to return 200
|
|
107
|
+
before starting Playwright: a listener is not a server, and a wedged :5566
|
|
108
|
+
(vite rebuilds components synchronously on source change) otherwise surfaces
|
|
109
|
+
as hundreds of `net::ERR_ABORTED` specs instead of one clear message.
|
|
110
|
+
- Layout: `tests/live/matrix/<component>/<component>-visual.spec.ts` (the table
|
|
111
|
+
alone splits into `matrix-{local,remote,marquee}.spec.ts`), each driving
|
|
112
|
+
`tests/live/fixtures/<component>/matrix.html` (fixtures, NOT showcases). A
|
|
113
|
+
fixture exposes `window.matrix.mount(combo)` and owns anything
|
|
114
|
+
carrying functions — column definitions, callbacks — which cannot cross the
|
|
115
|
+
Playwright boundary.
|
|
116
|
+
- Size the specs to the component: the table is the ceiling, not the template.
|
|
117
|
+
Its combos are the FULL product `{local,remote}` x 6 pipelines x 3 delivery
|
|
118
|
+
patterns x 2^5 `{virtualize,height,squish,striped,selectable}` = 1152,
|
|
119
|
+
generated by `generateCombos()` in `tests/live/matrix/matrix-harness.ts`; a
|
|
120
|
+
divider's are two dozen. `--all-engines` triples the wall time.
|
|
121
|
+
|
|
122
|
+
**Two layers per component** (the split is documented in the table harness
|
|
123
|
+
header; every component's specs follow it):
|
|
124
|
+
|
|
125
|
+
| Layer | Scope | What it asserts |
|
|
126
|
+
|-------|-------|-----------------|
|
|
127
|
+
| 1. geometry + computed style + `elementFromPoint` | every combo, one `page.evaluate` each on a shared open page | the component's own documented visual contract. Table: row/cell boxes disjoint + ascending + contained; squish right edge on the frame's inner edge and ellipsis not mid-glyph clip; sized host filled to bottom; virtual window contiguous between spacers; `striped` = 2 row colours, unstriped = 1; selection checkbox painted at non-zero size; three hit-test probes per visible cell must land inside that cell |
|
|
128
|
+
| 2. real screenshots | a small pinned marquee set (table: 6 combos x 2 colour schemes) | table: striping, loading spinner, empty state, squished right edge, fill row, density — decoded in-browser, judged on WCAG contrast between painted samples; PNGs written to `test-results/matrix-visual/` |
|
|
129
|
+
|
|
130
|
+
Layer 2 exists because layer 1 cannot tell a colour that DIFFERS from one that
|
|
131
|
+
differs VISIBLY (`striped` once applied correctly while painting two luminance
|
|
132
|
+
points from the dark surface). Keep every marquee set small — screenshots are
|
|
133
|
+
the expensive layer. New combinations go in the layer-1 generator.
|
|
134
|
+
|
|
135
|
+
**Known defects are pinned, never softened** (`.ai/fuzzing.md` policy). A
|
|
136
|
+
component's specs pin theirs with `test.fail()` and the finding written above
|
|
137
|
+
it — the Playwright counterpart of the DOM matrix's `it.fails`. The table
|
|
138
|
+
harness uses `WAIVERS` in `matrix-harness.ts`, stricter still because one combo reports many
|
|
139
|
+
problems: a waiver names the EXACT message it excuses, every other problem in
|
|
140
|
+
the same combo still fails, and a waiver whose message stops appearing fails
|
|
141
|
+
itself so it must be deleted. Currently pinned there:
|
|
142
|
+
`VISUAL-MATRIX-fill-1` (96 combos) — a local, sized, non-virtualized table that
|
|
143
|
+
renders its body a second time appends an inert filler even though the content
|
|
144
|
+
already overflows the frame.
|
|
145
|
+
|
|
55
146
|
## Dumb-Agent Gauntlet
|
|
56
147
|
|
|
57
148
|
Internal checker hardening: `npm run gauntlet` uses the comprehensive multi-file
|
|
@@ -63,7 +154,7 @@ application sample. Focused probes remain available with
|
|
|
63
154
|
- Auto-downloads pinned llama.cpp + size/SHA-256-pinned GGUFs into `.local/`.
|
|
64
155
|
- Produces blind raw output plus checker, TypeScript, and Vite logs per model.
|
|
65
156
|
- No token/time cap; exact-output repetition is preserved and classified.
|
|
66
|
-
- Full workflow/classification rules: `.ai/
|
|
157
|
+
- Full workflow/classification rules: `.ai/testing.md`.
|
|
67
158
|
|
|
68
159
|
## File Structure
|
|
69
160
|
|
|
@@ -140,7 +231,7 @@ All CDN builds use the shared runtime (external `snice` imports). Load `snice-ru
|
|
|
140
231
|
- IIFE: `.js` + `.min.js`
|
|
141
232
|
- Sourcemaps + README
|
|
142
233
|
|
|
143
|
-
**Size:** Runtime ~34KB gzip, components ~1-
|
|
234
|
+
**Size:** Runtime ~34KB gzip, components ~1-118KB each
|
|
144
235
|
|
|
145
236
|
**Features:**
|
|
146
237
|
- Shared runtime (external snice imports)
|