@aceshooting/lyra-ui 1.2.0 → 2.0.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 +11 -0
- package/custom-elements.json +23341 -11202
- package/dist/components/app-rail/app-rail.d.ts +171 -0
- package/dist/components/app-rail/app-rail.js +483 -0
- package/dist/components/app-rail/app-rail.styles.d.ts +1 -0
- package/dist/components/app-rail/app-rail.styles.js +125 -0
- package/dist/components/attachment-chip/attachment-chip.d.ts +114 -0
- package/dist/components/attachment-chip/attachment-chip.js +312 -0
- package/dist/components/attachment-chip/attachment-chip.styles.d.ts +1 -0
- package/dist/components/attachment-chip/attachment-chip.styles.js +184 -0
- package/dist/components/attachment-trigger/attachment-trigger.d.ts +83 -0
- package/dist/components/attachment-trigger/attachment-trigger.js +258 -0
- package/dist/components/attachment-trigger/attachment-trigger.styles.d.ts +1 -0
- package/dist/components/attachment-trigger/attachment-trigger.styles.js +68 -0
- package/dist/components/chart/box-plot.js +11 -1
- package/dist/components/chart/chart-loader.d.ts +44 -13
- package/dist/components/chart/chart-loader.js +65 -18
- package/dist/components/chart/chart.d.ts +1 -2
- package/dist/components/chart/chart.js +67 -12
- package/dist/components/chart/histogram-bin.d.ts +4 -1
- package/dist/components/chart/histogram-bin.js +24 -14
- package/dist/components/chart/lite-chart.d.ts +54 -0
- package/dist/components/chart/lite-chart.js +140 -22
- package/dist/components/chart/lite-chart.styles.js +9 -0
- package/dist/components/chat-composer/chat-composer.d.ts +92 -0
- package/dist/components/chat-composer/chat-composer.js +285 -0
- package/dist/components/chat-composer/chat-composer.styles.d.ts +1 -0
- package/dist/components/chat-composer/chat-composer.styles.js +135 -0
- package/dist/components/chat-message/chat-message.d.ts +113 -0
- package/dist/components/chat-message/chat-message.js +291 -0
- package/dist/components/chat-message/chat-message.styles.d.ts +1 -0
- package/dist/components/chat-message/chat-message.styles.js +203 -0
- package/dist/components/checkbox/checkbox.d.ts +59 -0
- package/dist/components/checkbox/checkbox.js +230 -0
- package/dist/components/checkbox/checkbox.styles.d.ts +1 -0
- package/dist/components/checkbox/checkbox.styles.js +70 -0
- package/dist/components/chip/chip-group.d.ts +56 -0
- package/dist/components/chip/chip-group.js +124 -0
- package/dist/components/chip/chip-group.styles.d.ts +1 -0
- package/dist/components/chip/chip-group.styles.js +57 -0
- package/dist/components/chip/chip.d.ts +64 -0
- package/dist/components/chip/chip.js +115 -0
- package/dist/components/chip/chip.styles.d.ts +1 -0
- package/dist/components/chip/chip.styles.js +115 -0
- package/dist/components/citation-badge/citation-badge.d.ts +113 -0
- package/dist/components/citation-badge/citation-badge.js +300 -0
- package/dist/components/citation-badge/citation-badge.styles.d.ts +1 -0
- package/dist/components/citation-badge/citation-badge.styles.js +96 -0
- package/dist/components/code-block/code-block.d.ts +89 -0
- package/dist/components/code-block/code-block.js +317 -0
- package/dist/components/code-block/code-block.styles.d.ts +1 -0
- package/dist/components/code-block/code-block.styles.js +187 -0
- package/dist/components/code-block/code-loader.d.ts +44 -0
- package/dist/components/code-block/code-loader.js +79 -0
- package/dist/components/combobox/combobox.d.ts +19 -1
- package/dist/components/combobox/combobox.js +84 -14
- package/dist/components/combobox/option.d.ts +5 -1
- package/dist/components/combobox/option.js +35 -0
- package/dist/components/context-meter/context-meter.d.ts +53 -0
- package/dist/components/context-meter/context-meter.js +150 -0
- package/dist/components/context-meter/context-meter.styles.d.ts +1 -0
- package/dist/components/context-meter/context-meter.styles.js +120 -0
- package/dist/components/conversation-item/conversation-item.d.ts +128 -0
- package/dist/components/conversation-item/conversation-item.js +322 -0
- package/dist/components/conversation-item/conversation-item.styles.d.ts +1 -0
- package/dist/components/conversation-item/conversation-item.styles.js +155 -0
- package/dist/components/date-picker/date-input.d.ts +24 -0
- package/dist/components/date-picker/date-input.js +134 -22
- package/dist/components/date-picker/date-picker.d.ts +7 -0
- package/dist/components/date-picker/date-picker.js +85 -16
- package/dist/components/dialog/confirm.d.ts +40 -0
- package/dist/components/dialog/confirm.js +79 -0
- package/dist/components/dialog/dialog.d.ts +85 -0
- package/dist/components/dialog/dialog.js +272 -0
- package/dist/components/dialog/dialog.styles.d.ts +1 -0
- package/dist/components/dialog/dialog.styles.js +52 -0
- package/dist/components/dock-panel/dock-panel.d.ts +134 -0
- package/dist/components/dock-panel/dock-panel.js +345 -0
- package/dist/components/dock-panel/dock-panel.styles.d.ts +1 -0
- package/dist/components/dock-panel/dock-panel.styles.js +145 -0
- package/dist/components/document-preview/document-preview.d.ts +135 -0
- package/dist/components/document-preview/document-preview.js +372 -0
- package/dist/components/document-preview/document-preview.styles.d.ts +1 -0
- package/dist/components/document-preview/document-preview.styles.js +191 -0
- package/dist/components/export-button/csv.js +1 -1
- package/dist/components/export-button/export-button.js +2 -0
- package/dist/components/flag/flag.d.ts +29 -13
- package/dist/components/flag/flag.js +39 -13
- package/dist/components/gauge/gauge.js +19 -10
- package/dist/components/generation-status/generation-status.d.ts +109 -0
- package/dist/components/generation-status/generation-status.js +273 -0
- package/dist/components/generation-status/generation-status.styles.d.ts +1 -0
- package/dist/components/generation-status/generation-status.styles.js +84 -0
- package/dist/components/heatmap/calendar-grid.d.ts +4 -1
- package/dist/components/heatmap/calendar-grid.js +25 -12
- package/dist/components/heatmap/heatmap-scale.d.ts +6 -0
- package/dist/components/heatmap/heatmap-scale.js +7 -1
- package/dist/components/heatmap/heatmap.d.ts +74 -0
- package/dist/components/heatmap/heatmap.js +135 -23
- package/dist/components/json-viewer/json-viewer.d.ts +53 -0
- package/dist/components/json-viewer/json-viewer.js +283 -0
- package/dist/components/json-viewer/json-viewer.styles.d.ts +1 -0
- package/dist/components/json-viewer/json-viewer.styles.js +176 -0
- package/dist/components/kbd/kbd.d.ts +77 -0
- package/dist/components/kbd/kbd.js +214 -0
- package/dist/components/kbd/kbd.styles.d.ts +1 -0
- package/dist/components/kbd/kbd.styles.js +52 -0
- package/dist/components/live-region/live-region.d.ts +81 -0
- package/dist/components/live-region/live-region.js +153 -0
- package/dist/components/live-region/live-region.styles.d.ts +1 -0
- package/dist/components/live-region/live-region.styles.js +10 -0
- package/dist/components/map/map.d.ts +8 -6
- package/dist/components/map/map.js +44 -8
- package/dist/components/markdown/markdown-loader.d.ts +34 -0
- package/dist/components/markdown/markdown-loader.js +43 -0
- package/dist/components/markdown/markdown.d.ts +81 -0
- package/dist/components/markdown/markdown.js +284 -0
- package/dist/components/markdown/markdown.styles.d.ts +1 -0
- package/dist/components/markdown/markdown.styles.js +82 -0
- package/dist/components/media-card/media-card.d.ts +122 -0
- package/dist/components/media-card/media-card.js +295 -0
- package/dist/components/media-card/media-card.styles.d.ts +1 -0
- package/dist/components/media-card/media-card.styles.js +132 -0
- package/dist/components/mention-popover/mention-popover.d.ts +200 -0
- package/dist/components/mention-popover/mention-popover.js +475 -0
- package/dist/components/mention-popover/mention-popover.styles.d.ts +1 -0
- package/dist/components/mention-popover/mention-popover.styles.js +101 -0
- package/dist/components/menu/menu-item.d.ts +62 -0
- package/dist/components/menu/menu-item.js +119 -0
- package/dist/components/menu/menu-item.styles.d.ts +1 -0
- package/dist/components/menu/menu-item.styles.js +67 -0
- package/dist/components/menu/menu.d.ts +143 -0
- package/dist/components/menu/menu.js +365 -0
- package/dist/components/menu/menu.styles.d.ts +1 -0
- package/dist/components/menu/menu.styles.js +66 -0
- package/dist/components/model-select/model-select.d.ts +125 -0
- package/dist/components/model-select/model-select.js +490 -0
- package/dist/components/model-select/model-select.styles.d.ts +1 -0
- package/dist/components/model-select/model-select.styles.js +189 -0
- package/dist/components/model-settings-panel/model-settings-panel.d.ts +69 -0
- package/dist/components/model-settings-panel/model-settings-panel.js +145 -0
- package/dist/components/model-settings-panel/model-settings-panel.styles.d.ts +1 -0
- package/dist/components/model-settings-panel/model-settings-panel.styles.js +82 -0
- package/dist/components/playback/playback.d.ts +13 -1
- package/dist/components/playback/playback.js +47 -14
- package/dist/components/responsive-panel/responsive-panel.d.ts +150 -0
- package/dist/components/responsive-panel/responsive-panel.js +365 -0
- package/dist/components/responsive-panel/responsive-panel.styles.d.ts +1 -0
- package/dist/components/responsive-panel/responsive-panel.styles.js +103 -0
- package/dist/components/result-card/result-card.d.ts +44 -0
- package/dist/components/result-card/result-card.js +83 -0
- package/dist/components/result-card/result-card.styles.d.ts +1 -0
- package/dist/components/result-card/result-card.styles.js +50 -0
- package/dist/components/result-card/result-field.d.ts +36 -0
- package/dist/components/result-card/result-field.js +87 -0
- package/dist/components/result-card/result-field.styles.d.ts +1 -0
- package/dist/components/result-card/result-field.styles.js +32 -0
- package/dist/components/select/select.d.ts +43 -2
- package/dist/components/select/select.js +150 -17
- package/dist/components/slider/slider.d.ts +89 -0
- package/dist/components/slider/slider.js +352 -0
- package/dist/components/slider/slider.styles.d.ts +1 -0
- package/dist/components/slider/slider.styles.js +87 -0
- package/dist/components/source-card/source-card.d.ts +92 -0
- package/dist/components/source-card/source-card.js +148 -0
- package/dist/components/source-card/source-card.styles.d.ts +1 -0
- package/dist/components/source-card/source-card.styles.js +69 -0
- package/dist/components/source-list/source-list.d.ts +63 -0
- package/dist/components/source-list/source-list.js +122 -0
- package/dist/components/source-list/source-list.styles.d.ts +1 -0
- package/dist/components/source-list/source-list.styles.js +59 -0
- package/dist/components/sparkline/sparkline.js +27 -13
- package/dist/components/split/split.d.ts +93 -1
- package/dist/components/split/split.js +284 -15
- package/dist/components/split/split.styles.js +30 -0
- package/dist/components/stat/stat.d.ts +6 -1
- package/dist/components/stat/stat.js +8 -2
- package/dist/components/stream-status/stream-status.d.ts +110 -0
- package/dist/components/stream-status/stream-status.js +210 -0
- package/dist/components/stream-status/stream-status.styles.d.ts +1 -0
- package/dist/components/stream-status/stream-status.styles.js +103 -0
- package/dist/components/streaming-text/streaming-text.d.ts +98 -0
- package/dist/components/streaming-text/streaming-text.js +209 -0
- package/dist/components/streaming-text/streaming-text.styles.d.ts +1 -0
- package/dist/components/streaming-text/streaming-text.styles.js +62 -0
- package/dist/components/switch/switch.d.ts +58 -0
- package/dist/components/switch/switch.js +179 -0
- package/dist/components/switch/switch.styles.d.ts +1 -0
- package/dist/components/switch/switch.styles.js +75 -0
- package/dist/components/table/table.d.ts +73 -4
- package/dist/components/table/table.js +169 -20
- package/dist/components/table/table.styles.js +6 -1
- package/dist/components/tabs/tabs.d.ts +51 -0
- package/dist/components/tabs/tabs.js +190 -0
- package/dist/components/tabs/tabs.styles.d.ts +1 -0
- package/dist/components/tabs/tabs.styles.js +63 -0
- package/dist/components/thinking-panel/thinking-panel.d.ts +121 -0
- package/dist/components/thinking-panel/thinking-panel.js +235 -0
- package/dist/components/thinking-panel/thinking-panel.styles.d.ts +1 -0
- package/dist/components/thinking-panel/thinking-panel.styles.js +117 -0
- package/dist/components/toast/toast-item.d.ts +1 -0
- package/dist/components/toast/toast-item.js +31 -6
- package/dist/components/tool-approval-dialog/tool-approval-dialog.d.ts +130 -0
- package/dist/components/tool-approval-dialog/tool-approval-dialog.js +403 -0
- package/dist/components/tool-approval-dialog/tool-approval-dialog.styles.d.ts +1 -0
- package/dist/components/tool-approval-dialog/tool-approval-dialog.styles.js +160 -0
- package/dist/components/tool-call-chip/tool-call-chip.d.ts +89 -0
- package/dist/components/tool-call-chip/tool-call-chip.js +288 -0
- package/dist/components/tool-call-chip/tool-call-chip.styles.d.ts +1 -0
- package/dist/components/tool-call-chip/tool-call-chip.styles.js +203 -0
- package/dist/components/tool-param-form/tool-param-form.d.ts +145 -0
- package/dist/components/tool-param-form/tool-param-form.js +366 -0
- package/dist/components/tool-param-form/tool-param-form.styles.d.ts +1 -0
- package/dist/components/tool-param-form/tool-param-form.styles.js +75 -0
- package/dist/components/tool-result-dialog/tool-result-dialog.d.ts +102 -0
- package/dist/components/tool-result-dialog/tool-result-dialog.js +393 -0
- package/dist/components/tool-result-dialog/tool-result-dialog.styles.d.ts +1 -0
- package/dist/components/tool-result-dialog/tool-result-dialog.styles.js +176 -0
- package/dist/components/tool-result-view/registry.d.ts +81 -0
- package/dist/components/tool-result-view/registry.js +84 -0
- package/dist/components/tool-result-view/tool-result-view.d.ts +55 -0
- package/dist/components/tool-result-view/tool-result-view.js +138 -0
- package/dist/components/tool-result-view/tool-result-view.styles.d.ts +1 -0
- package/dist/components/tool-result-view/tool-result-view.styles.js +13 -0
- package/dist/components/tool-select-dialog/tool-select-dialog.d.ts +151 -0
- package/dist/components/tool-select-dialog/tool-select-dialog.js +439 -0
- package/dist/components/tool-select-dialog/tool-select-dialog.styles.d.ts +1 -0
- package/dist/components/tool-select-dialog/tool-select-dialog.styles.js +182 -0
- package/dist/components/tree/tree-node.d.ts +11 -2
- package/dist/components/tree/tree-node.js +16 -6
- package/dist/components/tree/tree.d.ts +8 -2
- package/dist/components/tree/tree.js +18 -11
- package/dist/components/typing-indicator/typing-indicator.d.ts +60 -0
- package/dist/components/typing-indicator/typing-indicator.js +96 -0
- package/dist/components/typing-indicator/typing-indicator.styles.d.ts +1 -0
- package/dist/components/typing-indicator/typing-indicator.styles.js +132 -0
- package/dist/components/virtual-list/virtual-list.d.ts +173 -0
- package/dist/components/virtual-list/virtual-list.js +461 -0
- package/dist/components/virtual-list/virtual-list.styles.d.ts +1 -0
- package/dist/components/virtual-list/virtual-list.styles.js +44 -0
- package/dist/components/word-cloud/word-cloud-layout.js +14 -1
- package/dist/components/word-cloud/word-cloud.d.ts +6 -0
- package/dist/components/word-cloud/word-cloud.js +25 -14
- package/dist/components/word-cloud/word-cloud.styles.js +3 -2
- package/dist/internal/announcer.d.ts +57 -0
- package/dist/internal/announcer.js +71 -0
- package/dist/internal/form-associated.d.ts +1 -0
- package/dist/internal/form-associated.js +55 -11
- package/dist/internal/group-by-recency.d.ts +50 -0
- package/dist/internal/group-by-recency.js +80 -0
- package/dist/internal/rtl.d.ts +23 -0
- package/dist/internal/rtl.js +28 -0
- package/dist/internal/scroll-lock.d.ts +10 -5
- package/dist/internal/scroll-lock.js +30 -14
- package/dist/internal/tokens.styles.js +6 -0
- package/dist/lyra.d.ts +130 -31
- package/dist/lyra.js +94 -26
- package/llms-full.txt +3836 -17
- package/llms.txt +16 -1
- package/package.json +23 -5
- package/dist/components/chart/bar-chart.stories.d.ts +0 -5
- package/dist/components/chart/bar-chart.stories.js +0 -20
- package/dist/components/chart/box-plot.stories.d.ts +0 -5
- package/dist/components/chart/box-plot.stories.js +0 -29
- package/dist/components/chart/bubble-chart.stories.d.ts +0 -5
- package/dist/components/chart/bubble-chart.stories.js +0 -26
- package/dist/components/chart/chart.stories.d.ts +0 -32
- package/dist/components/chart/chart.stories.js +0 -141
- package/dist/components/chart/doughnut-chart.stories.d.ts +0 -5
- package/dist/components/chart/doughnut-chart.stories.js +0 -26
- package/dist/components/chart/histogram.stories.d.ts +0 -5
- package/dist/components/chart/histogram.stories.js +0 -17
- package/dist/components/chart/line-chart.stories.d.ts +0 -22
- package/dist/components/chart/line-chart.stories.js +0 -99
- package/dist/components/chart/lite-chart.stories.d.ts +0 -14
- package/dist/components/chart/lite-chart.stories.js +0 -114
- package/dist/components/chart/pie-chart.stories.d.ts +0 -5
- package/dist/components/chart/pie-chart.stories.js +0 -26
- package/dist/components/chart/polar-area-chart.stories.d.ts +0 -5
- package/dist/components/chart/polar-area-chart.stories.js +0 -20
- package/dist/components/chart/radar-chart.stories.d.ts +0 -5
- package/dist/components/chart/radar-chart.stories.js +0 -20
- package/dist/components/chart/scatter-chart.stories.d.ts +0 -5
- package/dist/components/chart/scatter-chart.stories.js +0 -31
- package/dist/components/combobox/combobox.stories.d.ts +0 -24
- package/dist/components/combobox/combobox.stories.js +0 -102
- package/dist/components/date-picker/date-input.stories.d.ts +0 -10
- package/dist/components/date-picker/date-input.stories.js +0 -48
- package/dist/components/date-picker/date-picker.stories.d.ts +0 -10
- package/dist/components/date-picker/date-picker.stories.js +0 -25
- package/dist/components/empty/empty.stories.d.ts +0 -7
- package/dist/components/empty/empty.stories.js +0 -34
- package/dist/components/export-button/export-button.stories.d.ts +0 -7
- package/dist/components/export-button/export-button.stories.js +0 -37
- package/dist/components/file-input/file-input.stories.d.ts +0 -7
- package/dist/components/file-input/file-input.stories.js +0 -19
- package/dist/components/flag/flag.stories.d.ts +0 -6
- package/dist/components/flag/flag.stories.js +0 -31
- package/dist/components/gauge/gauge.stories.d.ts +0 -8
- package/dist/components/gauge/gauge.stories.js +0 -19
- package/dist/components/graph/graph.stories.d.ts +0 -8
- package/dist/components/graph/graph.stories.js +0 -68
- package/dist/components/heatmap/heatmap.stories.d.ts +0 -27
- package/dist/components/heatmap/heatmap.stories.js +0 -179
- package/dist/components/map/map.stories.d.ts +0 -15
- package/dist/components/map/map.stories.js +0 -135
- package/dist/components/playback/playback.stories.d.ts +0 -7
- package/dist/components/playback/playback.stories.js +0 -16
- package/dist/components/select/select.stories.d.ts +0 -21
- package/dist/components/select/select.stories.js +0 -91
- package/dist/components/skeleton/skeleton.stories.d.ts +0 -5
- package/dist/components/skeleton/skeleton.stories.js +0 -16
- package/dist/components/sparkline/sparkline.stories.d.ts +0 -11
- package/dist/components/sparkline/sparkline.stories.js +0 -39
- package/dist/components/split/split.stories.d.ts +0 -7
- package/dist/components/split/split.stories.js +0 -36
- package/dist/components/stat/stat.stories.d.ts +0 -9
- package/dist/components/stat/stat.stories.js +0 -81
- package/dist/components/table/table.stories.d.ts +0 -12
- package/dist/components/table/table.stories.js +0 -67
- package/dist/components/time-range/time-range.stories.d.ts +0 -8
- package/dist/components/time-range/time-range.stories.js +0 -26
- package/dist/components/toast/toast.stories.d.ts +0 -8
- package/dist/components/toast/toast.stories.js +0 -72
- package/dist/components/tree/tree.stories.d.ts +0 -9
- package/dist/components/tree/tree.stories.js +0 -52
- package/dist/components/widget/widget.stories.d.ts +0 -9
- package/dist/components/widget/widget.stories.js +0 -74
- package/dist/components/word-cloud/word-cloud.stories.d.ts +0 -10
- package/dist/components/word-cloud/word-cloud.stories.js +0 -55
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { html, nothing } from 'lit';
|
|
8
|
+
import { property, state } from 'lit/decorators.js';
|
|
9
|
+
import { repeat } from 'lit/directives/repeat.js';
|
|
10
|
+
import { styleMap } from 'lit/directives/style-map.js';
|
|
11
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
12
|
+
import { defineElement } from '../../internal/prefix.js';
|
|
13
|
+
import { styles } from './virtual-list.styles.js';
|
|
14
|
+
/** Fallback per-row height (px) used for any row that hasn't been measured
|
|
15
|
+
* yet in `row-height="auto"` mode -- close enough to a typical single-line
|
|
16
|
+
* chat-list row that the initial scrollbar/spacer size doesn't jump wildly
|
|
17
|
+
* once real measurements arrive. Irrelevant in fixed-`row-height` mode. */
|
|
18
|
+
const DEFAULT_ROW_ESTIMATE_PX = 48;
|
|
19
|
+
function prefersReducedMotion() {
|
|
20
|
+
return typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `<lyra-virtual-list>` — a generic windowed/virtualized list host. Renders
|
|
24
|
+
* only the items within the current viewport (plus `overscan` padding rows
|
|
25
|
+
* on each side) as real DOM, regardless of how large `items` is, so a
|
|
26
|
+
* multi-thousand-row chat history sidebar (or, in a later phase, a long
|
|
27
|
+
* message thread) stays cheap to scroll.
|
|
28
|
+
*
|
|
29
|
+
* Content is entirely caller-supplied: `renderItem(item, index)` returns
|
|
30
|
+
* whatever `lit-html` value should represent that row (typically a
|
|
31
|
+
* `TemplateResult`), and `keyFunction(item, index)` gives it a stable
|
|
32
|
+
* identity for `repeat()`'s DOM-reconciliation key, so scroll position and
|
|
33
|
+
* any per-row state (e.g. an `<audio>` element's playback position) survive
|
|
34
|
+
* an `items` mutation instead of every row remounting from scratch.
|
|
35
|
+
*
|
|
36
|
+
* **Windowing math.** Every row — in both `row-height` modes — is positioned
|
|
37
|
+
* by a `transform: translateY(offset)` computed from a single cumulative
|
|
38
|
+
* `offsets` array (`offsets[i]` = the pixel top of row `i`), rather than by
|
|
39
|
+
* page flow. This is what lets only a small DOM window exist while the
|
|
40
|
+
* scrollbar still reflects the *true* total content height:
|
|
41
|
+
* - **`row-height="auto"` (default).** Each currently-rendered row is
|
|
42
|
+
* watched by a `ResizeObserver`; its real height lands in a per-key
|
|
43
|
+
* `Map`, and any row not yet measured contributes `DEFAULT_ROW_ESTIMATE_PX`
|
|
44
|
+
* to the offsets array until it has been. `offsets` is *not* the same
|
|
45
|
+
* thing as a page-count-based `padding-top`/`padding-bottom` spacer pair —
|
|
46
|
+
* that approach reflows every unmeasured row's position on every new
|
|
47
|
+
* measurement, which is exactly what per-row transform offsets avoid: only
|
|
48
|
+
* the rows *after* a newly-measured one shift, and even that shift is a
|
|
49
|
+
* cheap style recompute, not a layout-affecting padding change.
|
|
50
|
+
* - **Fixed numeric `row-height`.** No measurement needed — `offsets[i]` is
|
|
51
|
+
* just `i * rowHeightPx`, computed by the exact same cumulative-array code
|
|
52
|
+
* path (a dedicated "single spacer + start-index" special case was
|
|
53
|
+
* considered and would work too, but reusing one code path for both modes
|
|
54
|
+
* avoids maintaining two parallel rendering strategies for a difference
|
|
55
|
+
* that's otherwise just "how is row *i*'s height looked up").
|
|
56
|
+
*
|
|
57
|
+
* The `offsets` array is rebuilt on every update. For the list sizes this
|
|
58
|
+
* component is meant for (a scrollable history sidebar, realistically
|
|
59
|
+
* hundreds to a few thousand rows) that's a trivial `O(n)` arithmetic loop,
|
|
60
|
+
* even at scroll-driven ~60fps; it is *not* the right approach for a
|
|
61
|
+
* hundred-thousand-row list without further work (e.g. a Fenwick/segment
|
|
62
|
+
* tree for `O(log n)` offset queries+updates), which is out of scope here.
|
|
63
|
+
*
|
|
64
|
+
* **Accessibility.** The scroll container is `role="list"` and each rendered
|
|
65
|
+
* row is `role="listitem"`, deliberately *not* `listbox`/`option` — this
|
|
66
|
+
* component only provides windowing, not the roving-tabindex/
|
|
67
|
+
* `aria-activedescendant` keyboard-interaction contract ARIA requires
|
|
68
|
+
* alongside a real `listbox`. A consumer that wants full single-select
|
|
69
|
+
* listbox semantics on top of this should compose that behavior itself (see
|
|
70
|
+
* `<lyra-select>`'s pattern), the same way this component's `active-id`
|
|
71
|
+
* only *scrolls* the matching row into view and marks it `aria-current` —
|
|
72
|
+
* it never claims to be a selection widget. `aria-setsize`/`aria-posinset`
|
|
73
|
+
* are computed from the row's real index in the full `items` array (not its
|
|
74
|
+
* position among the currently-rendered DOM window), so a screen reader
|
|
75
|
+
* still announces e.g. "item 12 of 340" correctly. `[part="base"]` itself
|
|
76
|
+
* carries `tabindex="0"` — `renderItem`'s content is caller-supplied and not
|
|
77
|
+
* guaranteed to contain a focusable element, and a scrollable region with no
|
|
78
|
+
* focusable content of its own is otherwise unreachable by keyboard (native
|
|
79
|
+
* arrow/Page Up/Page Down scrolling included).
|
|
80
|
+
*
|
|
81
|
+
* **Grouping.** `groups` is accepted for forward API compatibility with a
|
|
82
|
+
* later phase's sticky-header sectioning feature (paired with a
|
|
83
|
+
* `groupByRecency()` helper that doesn't exist yet) but is not read or
|
|
84
|
+
* rendered by this pass — every row still renders flat, in `items` order.
|
|
85
|
+
*
|
|
86
|
+
* @customElement lyra-virtual-list
|
|
87
|
+
* @event lyra-load-more - Fired once per approach to the bottom of the list
|
|
88
|
+
* while `has-more` is true and `loading` is false. Deliberately does not
|
|
89
|
+
* refire on every scroll tick while still near the bottom (`loading`
|
|
90
|
+
* gates the in-flight case; scrolling back away from the bottom and
|
|
91
|
+
* returning, or `items` growing enough to move the window away from the
|
|
92
|
+
* end, re-arms it) — a consumer wanting an automatic retry after a failed
|
|
93
|
+
* fetch should surface its own retry affordance rather than relying on
|
|
94
|
+
* this firing again unprompted.
|
|
95
|
+
* @event lyra-visible-range-changed - `detail: { start, end }` (see
|
|
96
|
+
* `VirtualListRange`) — the current visible (non-overscanned) item index
|
|
97
|
+
* range, fired only when it actually changes.
|
|
98
|
+
* @csspart base - The scrollable container (`role="list"`).
|
|
99
|
+
* @csspart spacer - The full-content-height inner element that gives the
|
|
100
|
+
* container its true scrollable extent.
|
|
101
|
+
* @csspart row - One rendered row's absolutely-positioned wrapper
|
|
102
|
+
* (`role="listitem"`); `renderItem`'s return value renders inside it.
|
|
103
|
+
*/
|
|
104
|
+
export class LyraVirtualList extends LyraElement {
|
|
105
|
+
constructor() {
|
|
106
|
+
super(...arguments);
|
|
107
|
+
/** The full (non-windowed) item collection. */
|
|
108
|
+
this.items = [];
|
|
109
|
+
/** Renders one row's content — typically returns a `lit-html` `TemplateResult`. */
|
|
110
|
+
this.renderItem = () => nothing;
|
|
111
|
+
/** `'auto'` (default) measures each row's real height via `ResizeObserver`;
|
|
112
|
+
* a numeric string fixes every row to that many pixels. */
|
|
113
|
+
this.rowHeight = 'auto';
|
|
114
|
+
/** Extra rows rendered beyond the visible viewport on each side, to reduce blank-frame risk during fast scrolling. */
|
|
115
|
+
this.overscan = 6;
|
|
116
|
+
/** When set and it matches a row's `keyFunction` result, that row is smoothly scrolled into view whenever this changes. */
|
|
117
|
+
this.activeId = '';
|
|
118
|
+
this.loading = false;
|
|
119
|
+
/** When true, scrolling near the bottom fires `lyra-load-more`. */
|
|
120
|
+
this.hasMore = false;
|
|
121
|
+
// Named distinctly from the inherited DOM `scrollTop` (a `HTMLElement`
|
|
122
|
+
// property this class would otherwise shadow) -- this tracks the *scroll
|
|
123
|
+
// container's* scrollTop, not the host element's own (the host never
|
|
124
|
+
// scrolls itself; [part="base"] does).
|
|
125
|
+
this.containerScrollTop = 0;
|
|
126
|
+
this.viewportHeight = 0;
|
|
127
|
+
/** `offsets[i]` = row `i`'s pixel top; `offsets[items.length]` = total content height. Rebuilt every update. */
|
|
128
|
+
this.offsets = [0];
|
|
129
|
+
/** Parsed `rowHeight`: a positive pixel number, or `null` for `'auto'` (measured) mode. */
|
|
130
|
+
this.fixedRowHeight = null;
|
|
131
|
+
/** `row-height="auto"` per-row measured heights, keyed by `String(keyFunction(item, index))`. */
|
|
132
|
+
this.measuredHeights = new Map();
|
|
133
|
+
this.renderStart = 0;
|
|
134
|
+
this.renderEnd = -1;
|
|
135
|
+
this.visibleStart = 0;
|
|
136
|
+
this.visibleEnd = -1;
|
|
137
|
+
this.lastEmittedStart = -1;
|
|
138
|
+
this.lastEmittedEnd = -1;
|
|
139
|
+
/** Re-armed whenever the window moves away from the end of `items` -- see the `lyra-load-more` event doc. */
|
|
140
|
+
this.loadMoreArmed = true;
|
|
141
|
+
this.isFirstUpdate = true;
|
|
142
|
+
this.observedRows = new Map();
|
|
143
|
+
this.pendingScrollTop = null;
|
|
144
|
+
this.onScroll = (e) => {
|
|
145
|
+
this.pendingScrollTop = e.currentTarget.scrollTop;
|
|
146
|
+
if (this.scrollRafId !== undefined)
|
|
147
|
+
return;
|
|
148
|
+
// Coalesce to one recompute per animation frame -- native `scroll`
|
|
149
|
+
// events can fire far faster than that under a fast trackpad/touch
|
|
150
|
+
// fling, and each recompute is a full Lit update.
|
|
151
|
+
this.scrollRafId = requestAnimationFrame(() => {
|
|
152
|
+
this.scrollRafId = undefined;
|
|
153
|
+
if (this.pendingScrollTop !== null) {
|
|
154
|
+
this.containerScrollTop = this.pendingScrollTop;
|
|
155
|
+
this.pendingScrollTop = null;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
this.onRowsResized = (entries) => {
|
|
160
|
+
if (this.fixedRowHeight != null)
|
|
161
|
+
return;
|
|
162
|
+
let changed = false;
|
|
163
|
+
for (const entry of entries) {
|
|
164
|
+
const key = entry.target.dataset.rowKey;
|
|
165
|
+
if (key === undefined)
|
|
166
|
+
continue;
|
|
167
|
+
const height = entry.borderBoxSize?.[0]?.blockSize ?? entry.target.getBoundingClientRect().height;
|
|
168
|
+
const prev = this.measuredHeights.get(key);
|
|
169
|
+
// A sub-pixel-only difference (common with fractional layout/zoom)
|
|
170
|
+
// isn't worth a full offsets rebuild + re-render.
|
|
171
|
+
if (prev === undefined || Math.abs(prev - height) > 0.5) {
|
|
172
|
+
this.measuredHeights.set(key, height);
|
|
173
|
+
changed = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (changed)
|
|
177
|
+
this.requestUpdate();
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
static { this.styles = [LyraElement.styles, styles]; }
|
|
181
|
+
connectedCallback() {
|
|
182
|
+
super.connectedCallback();
|
|
183
|
+
this.rowResizeObserver = new ResizeObserver(this.onRowsResized);
|
|
184
|
+
// firstUpdated() only ever fires once per element instance -- a
|
|
185
|
+
// disconnect/reconnect (e.g. a reparenting drag) needs its own
|
|
186
|
+
// re-attach here, since the container observer/scroll listener were
|
|
187
|
+
// torn down in disconnectedCallback below.
|
|
188
|
+
if (this.hasUpdated)
|
|
189
|
+
this.attachContainerListeners();
|
|
190
|
+
}
|
|
191
|
+
disconnectedCallback() {
|
|
192
|
+
super.disconnectedCallback();
|
|
193
|
+
this.rowResizeObserver?.disconnect();
|
|
194
|
+
this.rowResizeObserver = undefined;
|
|
195
|
+
this.observedRows.clear();
|
|
196
|
+
this.containerResizeObserver?.disconnect();
|
|
197
|
+
this.containerResizeObserver = undefined;
|
|
198
|
+
if (this.scrollRafId !== undefined) {
|
|
199
|
+
cancelAnimationFrame(this.scrollRafId);
|
|
200
|
+
this.scrollRafId = undefined;
|
|
201
|
+
}
|
|
202
|
+
this.renderRoot.querySelector('[part="base"]')?.removeEventListener('scroll', this.onScroll);
|
|
203
|
+
}
|
|
204
|
+
firstUpdated() {
|
|
205
|
+
this.attachContainerListeners();
|
|
206
|
+
}
|
|
207
|
+
willUpdate(changed) {
|
|
208
|
+
this.isFirstUpdate = !this.hasUpdated;
|
|
209
|
+
if (changed.has('rowHeight')) {
|
|
210
|
+
this.fixedRowHeight = this.parseRowHeight(this.rowHeight);
|
|
211
|
+
}
|
|
212
|
+
this.recomputeOffsets();
|
|
213
|
+
this.computeRange();
|
|
214
|
+
}
|
|
215
|
+
updated(changed) {
|
|
216
|
+
super.updated(changed);
|
|
217
|
+
this.syncRowObservers();
|
|
218
|
+
if (changed.has('activeId') && !this.isFirstUpdate)
|
|
219
|
+
this.scrollActiveIntoView();
|
|
220
|
+
this.emitRangeChangeIfNeeded();
|
|
221
|
+
this.maybeFireLoadMore();
|
|
222
|
+
}
|
|
223
|
+
parseRowHeight(value) {
|
|
224
|
+
if (value === 'auto')
|
|
225
|
+
return null;
|
|
226
|
+
const n = Number(value);
|
|
227
|
+
// Anything that isn't 'auto' or a positive finite number falls back to
|
|
228
|
+
// auto mode rather than throwing or producing a zero/negative-height
|
|
229
|
+
// row -- matches this library's generally-permissive attribute parsing.
|
|
230
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
231
|
+
}
|
|
232
|
+
keyOf(item, index) {
|
|
233
|
+
return this.keyFunction ? this.keyFunction(item, index) : index;
|
|
234
|
+
}
|
|
235
|
+
recomputeOffsets() {
|
|
236
|
+
const n = this.items.length;
|
|
237
|
+
const offsets = new Array(n + 1);
|
|
238
|
+
offsets[0] = 0;
|
|
239
|
+
for (let i = 0; i < n; i++) {
|
|
240
|
+
const h = this.fixedRowHeight ??
|
|
241
|
+
this.measuredHeights.get(String(this.keyOf(this.items[i], i))) ??
|
|
242
|
+
DEFAULT_ROW_ESTIMATE_PX;
|
|
243
|
+
offsets[i + 1] = offsets[i] + h;
|
|
244
|
+
}
|
|
245
|
+
this.offsets = offsets;
|
|
246
|
+
}
|
|
247
|
+
/** First item index whose bottom edge is at/after `offset`. */
|
|
248
|
+
findIndexAtOrAfter(offset) {
|
|
249
|
+
const offsets = this.offsets;
|
|
250
|
+
let lo = 0;
|
|
251
|
+
let hi = offsets.length - 2;
|
|
252
|
+
while (lo < hi) {
|
|
253
|
+
const mid = (lo + hi) >> 1;
|
|
254
|
+
if (offsets[mid + 1] <= offset)
|
|
255
|
+
lo = mid + 1;
|
|
256
|
+
else
|
|
257
|
+
hi = mid;
|
|
258
|
+
}
|
|
259
|
+
return lo;
|
|
260
|
+
}
|
|
261
|
+
/** Last item index whose top edge is at/before `offset`. */
|
|
262
|
+
findIndexAtOrBefore(offset) {
|
|
263
|
+
const offsets = this.offsets;
|
|
264
|
+
let lo = 0;
|
|
265
|
+
let hi = offsets.length - 2;
|
|
266
|
+
while (lo < hi) {
|
|
267
|
+
const mid = (lo + hi + 1) >> 1;
|
|
268
|
+
if (offsets[mid] < offset)
|
|
269
|
+
lo = mid;
|
|
270
|
+
else
|
|
271
|
+
hi = mid - 1;
|
|
272
|
+
}
|
|
273
|
+
return lo;
|
|
274
|
+
}
|
|
275
|
+
computeRange() {
|
|
276
|
+
const n = this.items.length;
|
|
277
|
+
if (n === 0 || this.viewportHeight <= 0) {
|
|
278
|
+
this.visibleStart = 0;
|
|
279
|
+
this.visibleEnd = -1;
|
|
280
|
+
this.renderStart = 0;
|
|
281
|
+
this.renderEnd = -1;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const viewTop = this.containerScrollTop;
|
|
285
|
+
const viewBottom = viewTop + this.viewportHeight;
|
|
286
|
+
this.visibleStart = this.findIndexAtOrAfter(viewTop);
|
|
287
|
+
this.visibleEnd = this.findIndexAtOrBefore(viewBottom);
|
|
288
|
+
this.renderStart = Math.max(0, this.visibleStart - this.overscan);
|
|
289
|
+
this.renderEnd = Math.min(n - 1, this.visibleEnd + this.overscan);
|
|
290
|
+
}
|
|
291
|
+
attachContainerListeners() {
|
|
292
|
+
const base = this.renderRoot.querySelector('[part="base"]');
|
|
293
|
+
if (!base)
|
|
294
|
+
return;
|
|
295
|
+
this.containerResizeObserver = new ResizeObserver((entries) => {
|
|
296
|
+
const entry = entries[0];
|
|
297
|
+
if (!entry)
|
|
298
|
+
return;
|
|
299
|
+
this.viewportHeight = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;
|
|
300
|
+
});
|
|
301
|
+
this.containerResizeObserver.observe(base);
|
|
302
|
+
base.addEventListener('scroll', this.onScroll, { passive: true });
|
|
303
|
+
// Read synchronously too -- ResizeObserver's first callback is
|
|
304
|
+
// asynchronous, so without this the very first render would otherwise
|
|
305
|
+
// sit at viewportHeight=0 (rendering no rows at all) until that callback
|
|
306
|
+
// eventually fires. This is called from firstUpdated(), so setting a
|
|
307
|
+
// reactive property here schedules a second update on top of the one
|
|
308
|
+
// that just finished -- Lit's dev-mode console flags that pattern, but
|
|
309
|
+
// it's the documented exception ("the next update can only be scheduled
|
|
310
|
+
// as a side effect of the previous update"): the container's real size
|
|
311
|
+
// genuinely isn't knowable until after the first layout.
|
|
312
|
+
this.viewportHeight = base.clientHeight;
|
|
313
|
+
this.containerScrollTop = base.scrollTop;
|
|
314
|
+
}
|
|
315
|
+
/** Keeps the row `ResizeObserver` watching exactly the currently-rendered
|
|
316
|
+
* rows in `row-height="auto"` mode -- rows that scroll out of the
|
|
317
|
+
* rendered window are unobserved so the observer doesn't accumulate
|
|
318
|
+
* references to detached elements it can never usefully report on again. */
|
|
319
|
+
syncRowObservers() {
|
|
320
|
+
const ro = this.rowResizeObserver;
|
|
321
|
+
if (!ro)
|
|
322
|
+
return;
|
|
323
|
+
if (this.fixedRowHeight != null) {
|
|
324
|
+
for (const el of this.observedRows.values())
|
|
325
|
+
ro.unobserve(el);
|
|
326
|
+
this.observedRows.clear();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const current = new Map();
|
|
330
|
+
this.renderRoot.querySelectorAll('[part="row"]').forEach((el) => {
|
|
331
|
+
const key = el.dataset.rowKey;
|
|
332
|
+
if (key !== undefined)
|
|
333
|
+
current.set(key, el);
|
|
334
|
+
});
|
|
335
|
+
for (const [key, el] of this.observedRows) {
|
|
336
|
+
if (current.get(key) !== el) {
|
|
337
|
+
ro.unobserve(el);
|
|
338
|
+
this.observedRows.delete(key);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
for (const [key, el] of current) {
|
|
342
|
+
if (!this.observedRows.has(key)) {
|
|
343
|
+
this.observedRows.set(key, el);
|
|
344
|
+
ro.observe(el);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
scrollActiveIntoView() {
|
|
349
|
+
if (!this.activeId)
|
|
350
|
+
return;
|
|
351
|
+
const index = this.items.findIndex((item, i) => String(this.keyOf(item, i)) === this.activeId);
|
|
352
|
+
if (index < 0)
|
|
353
|
+
return;
|
|
354
|
+
const base = this.renderRoot.querySelector('[part="base"]');
|
|
355
|
+
if (!base)
|
|
356
|
+
return;
|
|
357
|
+
const top = this.offsets[index] ?? 0;
|
|
358
|
+
const bottom = this.offsets[index + 1] ?? top;
|
|
359
|
+
const viewTop = base.scrollTop;
|
|
360
|
+
const viewBottom = viewTop + base.clientHeight;
|
|
361
|
+
let target = null;
|
|
362
|
+
if (top < viewTop)
|
|
363
|
+
target = top;
|
|
364
|
+
else if (bottom > viewBottom)
|
|
365
|
+
target = bottom - base.clientHeight;
|
|
366
|
+
if (target === null)
|
|
367
|
+
return;
|
|
368
|
+
base.scrollTo({ top: Math.max(0, target), behavior: prefersReducedMotion() ? 'auto' : 'smooth' });
|
|
369
|
+
}
|
|
370
|
+
emitRangeChangeIfNeeded() {
|
|
371
|
+
if (this.visibleEnd < this.visibleStart)
|
|
372
|
+
return;
|
|
373
|
+
if (this.visibleStart === this.lastEmittedStart && this.visibleEnd === this.lastEmittedEnd)
|
|
374
|
+
return;
|
|
375
|
+
this.lastEmittedStart = this.visibleStart;
|
|
376
|
+
this.lastEmittedEnd = this.visibleEnd;
|
|
377
|
+
this.emit('lyra-visible-range-changed', {
|
|
378
|
+
start: this.visibleStart,
|
|
379
|
+
end: this.visibleEnd,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
maybeFireLoadMore() {
|
|
383
|
+
const n = this.items.length;
|
|
384
|
+
const nearBottom = n > 0 && this.visibleEnd >= n - 1;
|
|
385
|
+
if (!nearBottom) {
|
|
386
|
+
this.loadMoreArmed = true;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (!this.hasMore || this.loading || !this.loadMoreArmed)
|
|
390
|
+
return;
|
|
391
|
+
this.loadMoreArmed = false;
|
|
392
|
+
this.emit('lyra-load-more');
|
|
393
|
+
}
|
|
394
|
+
renderRow(item, index, total) {
|
|
395
|
+
const key = String(this.keyOf(item, index));
|
|
396
|
+
const top = this.offsets[index] ?? 0;
|
|
397
|
+
const isActive = this.activeId !== '' && key === this.activeId;
|
|
398
|
+
return html `
|
|
399
|
+
<div
|
|
400
|
+
part="row"
|
|
401
|
+
role="listitem"
|
|
402
|
+
data-row-key=${key}
|
|
403
|
+
aria-setsize=${total}
|
|
404
|
+
aria-posinset=${index + 1}
|
|
405
|
+
aria-current=${isActive ? 'true' : nothing}
|
|
406
|
+
style=${styleMap({ transform: `translateY(${top}px)` })}
|
|
407
|
+
>
|
|
408
|
+
${this.renderItem(item, index)}
|
|
409
|
+
</div>
|
|
410
|
+
`;
|
|
411
|
+
}
|
|
412
|
+
render() {
|
|
413
|
+
const n = this.items.length;
|
|
414
|
+
const totalHeight = this.offsets[n] ?? 0;
|
|
415
|
+
const windowed = [];
|
|
416
|
+
for (let i = this.renderStart; i <= this.renderEnd; i++) {
|
|
417
|
+
windowed.push({ item: this.items[i], index: i });
|
|
418
|
+
}
|
|
419
|
+
return html `
|
|
420
|
+
<div part="base" role="list" tabindex="0" aria-busy=${this.loading ? 'true' : nothing}>
|
|
421
|
+
<div part="spacer" style=${styleMap({ height: `${totalHeight}px` })}>
|
|
422
|
+
${repeat(windowed, (w) => this.keyOf(w.item, w.index), (w) => this.renderRow(w.item, w.index, n))}
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
`;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
__decorate([
|
|
429
|
+
property({ attribute: false })
|
|
430
|
+
], LyraVirtualList.prototype, "items", void 0);
|
|
431
|
+
__decorate([
|
|
432
|
+
property({ attribute: false })
|
|
433
|
+
], LyraVirtualList.prototype, "renderItem", void 0);
|
|
434
|
+
__decorate([
|
|
435
|
+
property({ attribute: false })
|
|
436
|
+
], LyraVirtualList.prototype, "keyFunction", void 0);
|
|
437
|
+
__decorate([
|
|
438
|
+
property({ attribute: false })
|
|
439
|
+
], LyraVirtualList.prototype, "groups", void 0);
|
|
440
|
+
__decorate([
|
|
441
|
+
property({ attribute: 'row-height' })
|
|
442
|
+
], LyraVirtualList.prototype, "rowHeight", void 0);
|
|
443
|
+
__decorate([
|
|
444
|
+
property({ type: Number })
|
|
445
|
+
], LyraVirtualList.prototype, "overscan", void 0);
|
|
446
|
+
__decorate([
|
|
447
|
+
property({ attribute: 'active-id' })
|
|
448
|
+
], LyraVirtualList.prototype, "activeId", void 0);
|
|
449
|
+
__decorate([
|
|
450
|
+
property({ type: Boolean, reflect: true })
|
|
451
|
+
], LyraVirtualList.prototype, "loading", void 0);
|
|
452
|
+
__decorate([
|
|
453
|
+
property({ type: Boolean, attribute: 'has-more', reflect: true })
|
|
454
|
+
], LyraVirtualList.prototype, "hasMore", void 0);
|
|
455
|
+
__decorate([
|
|
456
|
+
state()
|
|
457
|
+
], LyraVirtualList.prototype, "containerScrollTop", void 0);
|
|
458
|
+
__decorate([
|
|
459
|
+
state()
|
|
460
|
+
], LyraVirtualList.prototype, "viewportHeight", void 0);
|
|
461
|
+
defineElement('virtual-list', LyraVirtualList);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
export const styles = css `
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
/* Consumer-tunable viewport height, same pattern as --lyra-chart-height --
|
|
6
|
+
a virtualized list is meaningless without a bounded scroll extent, so
|
|
7
|
+
this ships a sane default rather than collapsing to 0 when a caller
|
|
8
|
+
forgets to size the host. */
|
|
9
|
+
--lyra-virtual-list-height: 24rem;
|
|
10
|
+
}
|
|
11
|
+
[part='base'] {
|
|
12
|
+
position: relative;
|
|
13
|
+
block-size: var(--lyra-virtual-list-height);
|
|
14
|
+
overflow-x: hidden;
|
|
15
|
+
overflow-y: auto;
|
|
16
|
+
/* A fast fling shouldn't also scroll the page behind this list once it
|
|
17
|
+
hits either end. */
|
|
18
|
+
overscroll-behavior: contain;
|
|
19
|
+
}
|
|
20
|
+
[part='base']:focus-visible {
|
|
21
|
+
outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
|
|
22
|
+
/* Negative (inward) so the ring isn't clipped by this element's own
|
|
23
|
+
overflow:auto -- an outward ring (every other component's convention)
|
|
24
|
+
would otherwise be cut off along the scrolling edges. */
|
|
25
|
+
outline-offset: calc(-1 * var(--lyra-focus-ring-offset));
|
|
26
|
+
}
|
|
27
|
+
[part='spacer'] {
|
|
28
|
+
position: relative;
|
|
29
|
+
inline-size: 100%;
|
|
30
|
+
}
|
|
31
|
+
[part='row'] {
|
|
32
|
+
position: absolute;
|
|
33
|
+
inset-inline-start: 0;
|
|
34
|
+
inset-block-start: 0;
|
|
35
|
+
inline-size: 100%;
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
/* Every row's position updates via this transform on every scroll-driven
|
|
38
|
+
re-render -- hinting the compositor avoids a full repaint per frame. */
|
|
39
|
+
will-change: transform;
|
|
40
|
+
}
|
|
41
|
+
:host([loading]) [part='base'] {
|
|
42
|
+
cursor: progress;
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
@@ -9,6 +9,18 @@ const RADIUS_STEP_PER_RADIAN = 3;
|
|
|
9
9
|
* generous enough that only a pathological input (e.g. one giant word
|
|
10
10
|
* repeated many times) ever exhausts it. */
|
|
11
11
|
const MAX_RADIUS_AREA_FACTOR = 6;
|
|
12
|
+
/** Fallback used by `resolveFontSizeBounds()` for any non-finite/non-positive
|
|
13
|
+
* `minFontSize`/`maxFontSize` input. */
|
|
14
|
+
const MIN_SANE_FONT_SIZE = 1;
|
|
15
|
+
/** Normalizes possibly-invalid minFontSize/maxFontSize into a finite,
|
|
16
|
+
* positive, correctly-ordered pair — non-finite/negative inputs fall back
|
|
17
|
+
* to MIN_SANE_FONT_SIZE, and a reversed pair (max < min) is swapped rather
|
|
18
|
+
* than left to invert the weight-to-size mapping. */
|
|
19
|
+
function resolveFontSizeBounds(minFontSize, maxFontSize) {
|
|
20
|
+
const min = Number.isFinite(minFontSize) && minFontSize > 0 ? minFontSize : MIN_SANE_FONT_SIZE;
|
|
21
|
+
const max = Number.isFinite(maxFontSize) && maxFontSize > 0 ? maxFontSize : MIN_SANE_FONT_SIZE;
|
|
22
|
+
return min <= max ? [min, max] : [max, min];
|
|
23
|
+
}
|
|
12
24
|
/** Clamps a (possibly negative/non-finite) input weight for scale math only —
|
|
13
25
|
* never fed back to callers, who should still see their own original `weight`. */
|
|
14
26
|
function effectiveWeight(weight) {
|
|
@@ -29,7 +41,8 @@ function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
|
|
|
29
41
|
* any word already placed). Deterministic given a deterministic `random`.
|
|
30
42
|
*/
|
|
31
43
|
export function layoutWordCloud(words, options) {
|
|
32
|
-
const {
|
|
44
|
+
const { scale, orientations, measureText } = options;
|
|
45
|
+
const [minFontSize, maxFontSize] = resolveFontSizeBounds(options.minFontSize, options.maxFontSize);
|
|
33
46
|
const random = options.random ?? Math.random;
|
|
34
47
|
const skipped = [];
|
|
35
48
|
const decorated = words
|
|
@@ -50,6 +50,12 @@ export declare class LyraWordCloud extends LyraElement {
|
|
|
50
50
|
* independent of the weight-sorted placement order. */
|
|
51
51
|
private navOrder;
|
|
52
52
|
protected willUpdate(changed: PropertyValues): void;
|
|
53
|
+
private relayout;
|
|
54
|
+
/** Forces a relayout so the font-family theme token (`--lyra-font`) is
|
|
55
|
+
* re-read from computed style — mirrors `lyra-chart`'s `refreshTheme()`.
|
|
56
|
+
* No global theme-broadcast event exists in lyra-ui to subscribe to
|
|
57
|
+
* automatically; call this from a consumer's own theme-toggle handler. */
|
|
58
|
+
refreshTheme(): void;
|
|
53
59
|
private activate;
|
|
54
60
|
private announce;
|
|
55
61
|
private onWordClick;
|
|
@@ -150,22 +150,33 @@ export class LyraWordCloud extends LyraElement {
|
|
|
150
150
|
changed.has('maxFontSize') ||
|
|
151
151
|
changed.has('scale') ||
|
|
152
152
|
changed.has('orientations')) {
|
|
153
|
-
this.
|
|
154
|
-
minFontSize: this.minFontSize,
|
|
155
|
-
maxFontSize: this.maxFontSize,
|
|
156
|
-
scale: this.scale,
|
|
157
|
-
orientations: this.orientations,
|
|
158
|
-
measureText: this.measureText,
|
|
159
|
-
});
|
|
160
|
-
if (this.cachedLayout.skipped.length > 0)
|
|
161
|
-
warnSkippedWords(this.cachedLayout.skipped.length);
|
|
162
|
-
// The previous focus cursor may no longer address a real word once the
|
|
163
|
-
// data changes out from under it.
|
|
164
|
-
this.focusedIndex = null;
|
|
165
|
-
this.liveText = '';
|
|
153
|
+
this.relayout();
|
|
166
154
|
}
|
|
167
155
|
this.setAttribute('role', 'group');
|
|
168
|
-
this.setAttribute('aria-label', `Word cloud of ${this.
|
|
156
|
+
this.setAttribute('aria-label', `Word cloud of ${this.cachedLayout.placed.length} word${this.cachedLayout.placed.length === 1 ? '' : 's'}`);
|
|
157
|
+
}
|
|
158
|
+
relayout() {
|
|
159
|
+
this.cachedLayout = layoutWordCloud(this.words, {
|
|
160
|
+
minFontSize: this.minFontSize,
|
|
161
|
+
maxFontSize: this.maxFontSize,
|
|
162
|
+
scale: this.scale,
|
|
163
|
+
orientations: this.orientations,
|
|
164
|
+
measureText: this.measureText,
|
|
165
|
+
});
|
|
166
|
+
if (this.cachedLayout.skipped.length > 0)
|
|
167
|
+
warnSkippedWords(this.cachedLayout.skipped.length);
|
|
168
|
+
// The previous focus cursor may no longer address a real word once the
|
|
169
|
+
// data changes out from under it.
|
|
170
|
+
this.focusedIndex = null;
|
|
171
|
+
this.liveText = '';
|
|
172
|
+
}
|
|
173
|
+
/** Forces a relayout so the font-family theme token (`--lyra-font`) is
|
|
174
|
+
* re-read from computed style — mirrors `lyra-chart`'s `refreshTheme()`.
|
|
175
|
+
* No global theme-broadcast event exists in lyra-ui to subscribe to
|
|
176
|
+
* automatically; call this from a consumer's own theme-toggle handler. */
|
|
177
|
+
refreshTheme() {
|
|
178
|
+
this.relayout();
|
|
179
|
+
this.requestUpdate();
|
|
169
180
|
}
|
|
170
181
|
activate(word) {
|
|
171
182
|
this.emit('lyra-word-click', { text: word.text, weight: word.weight, group: word.group });
|
|
@@ -37,10 +37,11 @@ export const styles = css `
|
|
|
37
37
|
font-weight: 600;
|
|
38
38
|
text-anchor: middle;
|
|
39
39
|
dominant-baseline: middle;
|
|
40
|
-
transition:
|
|
40
|
+
transition: text-decoration-color var(--lyra-transition-fast);
|
|
41
|
+
text-decoration: underline transparent;
|
|
41
42
|
}
|
|
42
43
|
[part='word']:hover {
|
|
43
|
-
|
|
44
|
+
text-decoration-color: currentColor;
|
|
44
45
|
}
|
|
45
46
|
[part='focus-ring'] {
|
|
46
47
|
fill: none;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Options for a single `Announcer.announce()` call. */
|
|
2
|
+
export interface AnnounceOptions {
|
|
3
|
+
/** Bypass any in-progress throttle window and flush this text immediately. */
|
|
4
|
+
force?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface AnnouncerOptions {
|
|
7
|
+
/** Throttle window in ms. Repeated `announce()` calls arriving within this
|
|
8
|
+
* window of the first call in a burst collapse to one trailing-edge flush
|
|
9
|
+
* of the latest text. Defaults to 500. */
|
|
10
|
+
throttleMs?: number;
|
|
11
|
+
/** Invoked with the coalesced text whenever a burst flushes (on the
|
|
12
|
+
* trailing edge, or immediately for a `{ force: true }` call). */
|
|
13
|
+
onFlush: (text: string) => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Trailing-edge debounce/coalesce for screen-reader announcements.
|
|
17
|
+
*
|
|
18
|
+
* Streaming UIs (token-by-token chat responses, progress ticks, etc.)
|
|
19
|
+
* naturally produce far more candidate announcements than a screen-reader
|
|
20
|
+
* user can usefully absorb — reading every incremental chunk aloud is spam,
|
|
21
|
+
* not information. `Announcer` collapses a burst of `announce()` calls
|
|
22
|
+
* arriving within `throttleMs` of the *first* call in that burst down to a
|
|
23
|
+
* single flush of the latest text: superseded intermediate text is dropped
|
|
24
|
+
* outright, never queued or concatenated. Passing `{ force: true }` always
|
|
25
|
+
* flushes immediately regardless of any window in progress, so a final or
|
|
26
|
+
* terminal message (e.g. "response complete") is never swallowed mid-burst.
|
|
27
|
+
*
|
|
28
|
+
* This is pure timing/state logic with no DOM dependency — it knows nothing
|
|
29
|
+
* about ARIA, elements, or Lit. `<lyra-live-region>`
|
|
30
|
+
* (`../components/live-region/live-region.js`) is the DOM-facing wrapper
|
|
31
|
+
* that composes one of these with an actual live-region element. Other
|
|
32
|
+
* components that need throttled announcements (a stream-status indicator,
|
|
33
|
+
* a tool-call chip's status transitions, a chat message's streaming state)
|
|
34
|
+
* should reuse that wrapper rather than instantiating `Announcer` directly.
|
|
35
|
+
*/
|
|
36
|
+
export declare class Announcer {
|
|
37
|
+
/** Throttle window in ms. Safe to change between bursts; a flush already
|
|
38
|
+
* scheduled keeps the deadline it was scheduled with. */
|
|
39
|
+
throttleMs: number;
|
|
40
|
+
private readonly onFlush;
|
|
41
|
+
private timer?;
|
|
42
|
+
private pending?;
|
|
43
|
+
constructor(options: AnnouncerOptions);
|
|
44
|
+
/** The latest text awaiting flush, if a burst is currently in progress. */
|
|
45
|
+
get pendingText(): string | undefined;
|
|
46
|
+
/** Whether a flush is currently scheduled (a burst is in progress). */
|
|
47
|
+
get isPending(): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Queue `text` for announcement. Within a single throttle window, only the
|
|
50
|
+
* latest text queued survives — this call always overwrites whatever an
|
|
51
|
+
* earlier call in the same burst queued.
|
|
52
|
+
*/
|
|
53
|
+
announce(text: string, options?: AnnounceOptions): void;
|
|
54
|
+
/** Cancel any pending (not yet flushed) announcement without flushing it. */
|
|
55
|
+
cancel(): void;
|
|
56
|
+
private flush;
|
|
57
|
+
}
|