@ds-mo/ui 14.22.2 → 14.23.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/dist/.package-ready.json +1 -1
- package/dist/agent-patterns.json +5 -1
- package/dist/agent.json +56 -13
- package/dist/angular/ds-panel-tools.d.ts +7 -1
- package/dist/angular/ds-panel-tools.js +7 -4
- package/dist/angular/ds-shell-app.d.ts +7 -1
- package/dist/angular/ds-shell-app.js +5 -2
- package/dist/angular/ds-shell-tools.d.ts +4 -1
- package/dist/angular/ds-shell-tools.js +7 -4
- package/dist/components/ds-message-scroller.js +1 -1
- package/dist/components/ds-message-scroller.js.map +1 -1
- package/dist/components/ds-panel-tools.js +1 -1
- package/dist/components/ds-shell-app.js +1 -1
- package/dist/components/ds-shell-app.js.map +1 -1
- package/dist/components/ds-shell-tools.js +1 -1
- package/dist/components/p-Bd-Kpekh.js +2 -0
- package/dist/components/p-Bd-Kpekh.js.map +1 -0
- package/dist/components/p-C13fWp2x.js +2 -0
- package/dist/components/p-C13fWp2x.js.map +1 -0
- package/dist/docs/components.json +178 -3
- package/dist/lib/components/PanelTools/panel-tools-types.d.ts +76 -1
- package/dist/lib/components/ShellApp/shell-app-types.d.ts +3 -1
- package/dist/lib/shell/index.d.ts +1 -1
- package/dist/lib/shell/index.js.map +2 -2
- package/dist/mcp-data/registry/agent-activity.json +1 -1
- package/dist/mcp-data/registry/agent-questionnaire.json +1 -1
- package/dist/mcp-data/registry/agent-response.json +1 -1
- package/dist/mcp-data/registry/agent-source-list.json +1 -1
- package/dist/mcp-data/registry/agent-tool-call.json +1 -1
- package/dist/mcp-data/registry/attachment-list.json +1 -1
- package/dist/mcp-data/registry/avatar.json +1 -1
- package/dist/mcp-data/registry/badge.json +1 -1
- package/dist/mcp-data/registry/banner.json +1 -1
- package/dist/mcp-data/registry/bar-action.json +1 -1
- package/dist/mcp-data/registry/bar-nav.json +1 -1
- package/dist/mcp-data/registry/bar-title.json +1 -1
- package/dist/mcp-data/registry/bar-workflow.json +1 -1
- package/dist/mcp-data/registry/breadcrumb.json +1 -1
- package/dist/mcp-data/registry/button-filled.json +1 -1
- package/dist/mcp-data/registry/button-unfilled.json +1 -1
- package/dist/mcp-data/registry/card-chart.json +1 -1
- package/dist/mcp-data/registry/card-overview.json +1 -1
- package/dist/mcp-data/registry/card-setting.json +1 -1
- package/dist/mcp-data/registry/chart-legend.json +1 -1
- package/dist/mcp-data/registry/chart.json +1 -1
- package/dist/mcp-data/registry/checkbox.json +1 -1
- package/dist/mcp-data/registry/chip.json +1 -1
- package/dist/mcp-data/registry/code-block.json +1 -1
- package/dist/mcp-data/registry/conversation-list-item.json +1 -1
- package/dist/mcp-data/registry/conversation-list-section.json +1 -1
- package/dist/mcp-data/registry/conversation-list.json +1 -1
- package/dist/mcp-data/registry/divider.json +1 -1
- package/dist/mcp-data/registry/empty-state.json +1 -1
- package/dist/mcp-data/registry/field.json +1 -1
- package/dist/mcp-data/registry/filter-menu.json +1 -1
- package/dist/mcp-data/registry/icon.json +1 -1
- package/dist/mcp-data/registry/input.json +1 -1
- package/dist/mcp-data/registry/loader.json +1 -1
- package/dist/mcp-data/registry/markdown.json +1 -1
- package/dist/mcp-data/registry/menu.json +1 -1
- package/dist/mcp-data/registry/message-actions.json +1 -1
- package/dist/mcp-data/registry/message-bubble.json +1 -1
- package/dist/mcp-data/registry/message-composer.json +1 -1
- package/dist/mcp-data/registry/message-scroller.json +9 -2
- package/dist/mcp-data/registry/message.json +1 -1
- package/dist/mcp-data/registry/mobile-bar-nav.json +1 -1
- package/dist/mcp-data/registry/mobile-header.json +1 -1
- package/dist/mcp-data/registry/mobile-section-switcher.json +1 -1
- package/dist/mcp-data/registry/mobile-sheet-nav.json +1 -1
- package/dist/mcp-data/registry/modal.json +1 -1
- package/dist/mcp-data/registry/pagination.json +1 -1
- package/dist/mcp-data/registry/panel-nav.json +1 -1
- package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
- package/dist/mcp-data/registry/panel-tool-header.json +1 -1
- package/dist/mcp-data/registry/panel-tool-search.json +1 -1
- package/dist/mcp-data/registry/panel-tools.json +62 -15
- package/dist/mcp-data/registry/paper-texture.json +1 -1
- package/dist/mcp-data/registry/radio.json +1 -1
- package/dist/mcp-data/registry/registry.json +56 -13
- package/dist/mcp-data/registry/scroll-overlay.json +1 -1
- package/dist/mcp-data/registry/select.json +1 -1
- package/dist/mcp-data/registry/shell-app.json +22 -3
- package/dist/mcp-data/registry/shell-page.json +1 -1
- package/dist/mcp-data/registry/shell-tools.json +39 -6
- package/dist/mcp-data/registry/skeleton.json +1 -1
- package/dist/mcp-data/registry/slider.json +1 -1
- package/dist/mcp-data/registry/swatch-picker.json +1 -1
- package/dist/mcp-data/registry/switch.json +1 -1
- package/dist/mcp-data/registry/tab-group.json +1 -1
- package/dist/mcp-data/registry/table-filter.json +1 -1
- package/dist/mcp-data/registry/table-group.json +1 -1
- package/dist/mcp-data/registry/table-saved-views.json +1 -1
- package/dist/mcp-data/registry/table-search.json +1 -1
- package/dist/mcp-data/registry/table-sort.json +1 -1
- package/dist/mcp-data/registry/table-toolbar.json +1 -1
- package/dist/mcp-data/registry/table.json +1 -1
- package/dist/mcp-data/registry/tag.json +1 -1
- package/dist/mcp-data/registry/text.json +1 -1
- package/dist/mcp-data/registry/toast.json +1 -1
- package/dist/mcp-data/registry/tooltip-chart.json +1 -1
- package/dist/mcp-data/registry/tooltip.json +1 -1
- package/dist/mcp-data/registry/typing-indicator.json +1 -1
- package/dist/react/ds-panel-tools.d.ts +2 -1
- package/dist/react/ds-panel-tools.js +1 -0
- package/dist/react/ds-shell-app.d.ts +2 -1
- package/dist/react/ds-shell-app.js +1 -0
- package/dist/react/ds-shell-tools.d.ts +2 -1
- package/dist/react/ds-shell-tools.js +2 -1
- package/dist/types/components/MessageScroller/MessageScroller.d.ts +14 -0
- package/dist/types/components/MessageScroller/message-scroller-transcript.d.ts +9 -0
- package/dist/types/components/PanelTools/PanelTools.d.ts +17 -1
- package/dist/types/components/PanelTools/index.d.ts +1 -1
- package/dist/types/components/PanelTools/panel-tools-types.d.ts +76 -1
- package/dist/types/components/PanelTools/panel-tools-utils.d.ts +17 -1
- package/dist/types/components/ShellApp/ShellApp.d.ts +4 -1
- package/dist/types/components/ShellApp/shell-app-types.d.ts +3 -1
- package/dist/types/components/ShellTools/ShellTools.d.ts +5 -1
- package/dist/types/components/ShellTools/index.d.ts +1 -1
- package/dist/types/components.d.ts +34 -2
- package/dist/types/shell/index.d.ts +1 -1
- package/dist/vue/ds-panel-tools.js +3 -0
- package/dist/vue/ds-shell-app.js +2 -0
- package/dist/vue/ds-shell-tools.js +5 -2
- package/package.json +3 -3
- package/dist/components/p-COFw_fcj.js +0 -2
- package/dist/components/p-COFw_fcj.js.map +0 -1
- package/dist/components/p-CvHrCyuz.js +0 -2
- package/dist/components/p-CvHrCyuz.js.map +0 -1
|
@@ -281,7 +281,7 @@
|
|
|
281
281
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: tabs.",
|
|
282
282
|
"peerDependencies": {
|
|
283
283
|
"required": [
|
|
284
|
-
"@ds-mo/tokens ^6.5.
|
|
284
|
+
"@ds-mo/tokens ^6.5.1",
|
|
285
285
|
"@ds-mo/icons ^7.0.0"
|
|
286
286
|
],
|
|
287
287
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -511,7 +511,7 @@
|
|
|
511
511
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: actions, breadcrumbs, primaryAction, sections.",
|
|
512
512
|
"peerDependencies": {
|
|
513
513
|
"required": [
|
|
514
|
-
"@ds-mo/tokens ^6.5.
|
|
514
|
+
"@ds-mo/tokens ^6.5.1",
|
|
515
515
|
"@ds-mo/icons ^7.0.0"
|
|
516
516
|
],
|
|
517
517
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -356,7 +356,7 @@
|
|
|
356
356
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: steps, submitAction.",
|
|
357
357
|
"peerDependencies": {
|
|
358
358
|
"required": [
|
|
359
|
-
"@ds-mo/tokens ^6.5.
|
|
359
|
+
"@ds-mo/tokens ^6.5.1",
|
|
360
360
|
"@ds-mo/icons ^7.0.0"
|
|
361
361
|
],
|
|
362
362
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: items.",
|
|
177
177
|
"peerDependencies": {
|
|
178
178
|
"required": [
|
|
179
|
-
"@ds-mo/tokens ^6.5.
|
|
179
|
+
"@ds-mo/tokens ^6.5.1",
|
|
180
180
|
"@ds-mo/icons ^7.0.0"
|
|
181
181
|
],
|
|
182
182
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -350,7 +350,7 @@
|
|
|
350
350
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: metrics, score.",
|
|
351
351
|
"peerDependencies": {
|
|
352
352
|
"required": [
|
|
353
|
-
"@ds-mo/tokens ^6.5.
|
|
353
|
+
"@ds-mo/tokens ^6.5.1",
|
|
354
354
|
"@ds-mo/icons ^7.0.0"
|
|
355
355
|
],
|
|
356
356
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -272,7 +272,7 @@
|
|
|
272
272
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: items.",
|
|
273
273
|
"peerDependencies": {
|
|
274
274
|
"required": [
|
|
275
|
-
"@ds-mo/tokens ^6.5.
|
|
275
|
+
"@ds-mo/tokens ^6.5.1",
|
|
276
276
|
"@ds-mo/icons ^7.0.0"
|
|
277
277
|
],
|
|
278
278
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -234,7 +234,7 @@
|
|
|
234
234
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: definition.",
|
|
235
235
|
"peerDependencies": {
|
|
236
236
|
"required": [
|
|
237
|
-
"@ds-mo/tokens ^6.5.
|
|
237
|
+
"@ds-mo/tokens ^6.5.1",
|
|
238
238
|
"@ds-mo/icons ^7.0.0"
|
|
239
239
|
],
|
|
240
240
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -759,7 +759,7 @@
|
|
|
759
759
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: anchor, boundary, filters, matchModes, values.",
|
|
760
760
|
"peerDependencies": {
|
|
761
761
|
"required": [
|
|
762
|
-
"@ds-mo/tokens ^6.5.
|
|
762
|
+
"@ds-mo/tokens ^6.5.1",
|
|
763
763
|
"@ds-mo/icons ^7.0.0"
|
|
764
764
|
],
|
|
765
765
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -500,7 +500,7 @@
|
|
|
500
500
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: anchor, boundary, items, sections.",
|
|
501
501
|
"peerDependencies": {
|
|
502
502
|
"required": [
|
|
503
|
-
"@ds-mo/tokens ^6.5.
|
|
503
|
+
"@ds-mo/tokens ^6.5.1",
|
|
504
504
|
"@ds-mo/icons ^7.0.0"
|
|
505
505
|
],
|
|
506
506
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"Wheel, touch, keyboard navigation, selection, and transcript interaction release automatic following; Scroll to latest explicitly restores it.",
|
|
39
39
|
"A newly appended outgoing Message marked scroll-anchor starts a turn near the top with the configurable previous-turn peek; mutations inside existing messages and prepended history do not re-anchor.",
|
|
40
40
|
"Opening saved history, message jumps, and prepended history preserve deliberate position.",
|
|
41
|
+
"When a persistent scroller receives a cleared or wholly replaced transcript, it releases internal references to the previous message elements, invalidates their pending anchor work, and treats the replacement as a new baseline at defaultPosition rather than as an appended turn.",
|
|
42
|
+
"MessageScroller bounds its internal element tracking to the currently rendered transcript. The application still owns transcript data and must cap or window the rendered message collection when conversation history can grow without limit.",
|
|
41
43
|
"The scroll-to-latest action is absent at the live edge, appears only after the reader moves away, and disappears again after returning to the newest message.",
|
|
42
44
|
"Scroll to latest uses the shared md control-elevation wrapper with a pointer-transparent top highlight, preserving its transparent backdrop blur and borderless unfilled action while the press-scale modifier moves the complete elevated surface.",
|
|
43
45
|
"Place a pending human-input surface in interaction and a composer container with 8px padding on every side in overlay; the shared scroll-overlay controller measures them as one stack, tracks every growth step, reserves matching transcript clearance, begins the fade before the stack, and keeps scroll-to-latest above it."
|
|
@@ -199,7 +201,7 @@
|
|
|
199
201
|
},
|
|
200
202
|
"peerDependencies": {
|
|
201
203
|
"required": [
|
|
202
|
-
"@ds-mo/tokens ^6.5.
|
|
204
|
+
"@ds-mo/tokens ^6.5.1",
|
|
203
205
|
"@ds-mo/icons ^7.0.0"
|
|
204
206
|
],
|
|
205
207
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -227,7 +229,12 @@
|
|
|
227
229
|
},
|
|
228
230
|
{
|
|
229
231
|
"path": "src/wc/components/MessageScroller/MessageScroller.tsx",
|
|
230
|
-
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n} from '@stencil/core';\nimport type { MessageScrollerPosition } from '../conversation-types';\nimport { ScrollOverlayController } from '../../utils/scroll-overlay-controller';\nimport { resolveCssLengthPx } from '../../utils/resolve-css-length-px';\n\nconst LIVE_EDGE_THRESHOLD = 24;\n\n@Component({\n tag: 'ds-message-scroller',\n styleUrl: 'MessageScroller.css',\n styleUrls: ['../../utils/scroll-edge-fade.css'],\n scoped: true,\n})\nexport class MessageScroller {\n @Element() el!: HTMLElement;\n\n @Prop() busy: boolean = false;\n @Prop() autoFollow: boolean = true;\n @Prop() defaultPosition: MessageScrollerPosition = 'last-anchor';\n @Prop() messagesLabel: string = 'Messages';\n\n @Event() dsReachStart!: EventEmitter<void>;\n\n @State() private showScrollToLatest = false;\n\n private viewport?: HTMLElement;\n private content?: HTMLElement;\n private overlay?: HTMLElement;\n private transcriptSlot?: HTMLSlotElement;\n private scrollOverlayController?: ScrollOverlayController;\n private mutationObserver?: MutationObserver;\n private following = true;\n private followReleased = false;\n private releasePendingAtLiveEdge = false;\n private atStart = false;\n private programmatic = false;\n private programmaticTimer?: ReturnType<typeof setTimeout>;\n private transcriptChildren: HTMLElement[] = [];\n private knownTranscriptElements = new Set<HTMLElement>();\n private knownTurnAnchors = new Set<HTMLElement>();\n private firstChildViewportTop?: number;\n private activeTurnAnchor?: HTMLElement;\n private activeTurnAnchorViewportTop?: number;\n private turnClearance = 0;\n private prependReconciliation = 0;\n\n componentDidLoad() {\n this.transcriptChildren = this.getTranscriptChildren();\n this.transcriptChildren.forEach(element => {\n this.knownTranscriptElements.add(element);\n if (element.matches('ds-message[scroll-anchor]')) {\n this.knownTurnAnchors.add(element);\n }\n });\n this.rememberFirstChildTop();\n this.connectObservers();\n requestAnimationFrame(() => this.applyDefaultPosition());\n document.addEventListener('selectionchange', this.handleSelectionChange);\n }\n\n disconnectedCallback() {\n this.prependReconciliation += 1;\n this.scrollOverlayController?.disconnect();\n this.mutationObserver?.disconnect();\n if (this.programmaticTimer) clearTimeout(this.programmaticTimer);\n document.removeEventListener('selectionchange', this.handleSelectionChange);\n }\n\n @Method()\n async scrollToMessage(id: string): Promise<boolean> {\n const message = Array.from(\n this.el.querySelectorAll<HTMLElement & { messageId?: string }>('ds-message')\n ).find(item => item.messageId === id || item.getAttribute('message-id') === id);\n if (!message) return false;\n this.releasePendingAtLiveEdge = false;\n this.following = false;\n this.followReleased = true;\n this.showScrollToLatest = true;\n message.scrollIntoView({ block: 'start', behavior: this.motionBehavior() });\n return true;\n }\n\n @Method()\n async scrollToStart() {\n this.releasePendingAtLiveEdge = false;\n this.scrollTo(0);\n this.following = false;\n this.followReleased = true;\n this.showScrollToLatest = Boolean(\n this.viewport && this.viewport.scrollHeight > this.viewport.clientHeight\n );\n }\n\n @Method()\n async scrollToEnd() {\n this.releasePendingAtLiveEdge = false;\n this.following = true;\n this.followReleased = false;\n this.showScrollToLatest = false;\n this.scrollTo(this.viewport?.scrollHeight ?? 0);\n }\n\n private motionBehavior(): ScrollBehavior {\n return matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';\n }\n\n private scrollTo(top: number) {\n if (!this.viewport) return;\n this.programmatic = true;\n const behavior = this.motionBehavior();\n this.viewport.scrollTo({ top, behavior });\n if (this.programmaticTimer) clearTimeout(this.programmaticTimer);\n this.programmaticTimer = setTimeout(\n () => {\n this.programmatic = false;\n this.programmaticTimer = undefined;\n if (this.following) {\n this.viewport!.scrollTop = top;\n this.showScrollToLatest = false;\n }\n },\n behavior === 'smooth' ? 600 : 0\n );\n }\n\n private connectObservers() {\n if (!this.viewport || !this.content || !this.overlay) return;\n this.scrollOverlayController = new ScrollOverlayController({\n host: this.el,\n viewport: this.viewport,\n content: this.content,\n overlay: this.overlay,\n overlayContentSelector: '.message-scroller__overlay-stack',\n onGeometryChange: () => this.handleContentGrowth(),\n });\n this.scrollOverlayController.connect();\n this.mutationObserver = new MutationObserver(() => {\n this.handleTranscriptChange();\n this.handleContentGrowth();\n });\n this.mutationObserver.observe(this.el, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n\n private handleContentGrowth() {\n if (!this.viewport) return;\n this.syncTurnClearance();\n const nextHeight = this.viewport.scrollHeight;\n if (this.autoFollow && this.following && !this.followReleased) {\n this.viewport.scrollTop = nextHeight;\n }\n if (this.releasePendingAtLiveEdge && !this.isAtLiveEdge()) {\n this.releasePendingAtLiveEdge = false;\n }\n this.showScrollToLatest = !this.following && !this.isAtLiveEdge();\n this.scrollOverlayController?.sync();\n }\n\n private rememberFirstChildTop() {\n if (!this.viewport) return;\n const first = this.getTranscriptChildren()[0];\n this.firstChildViewportTop = first\n ? first.getBoundingClientRect().top - this.viewport.getBoundingClientRect().top\n : undefined;\n }\n\n private getTranscriptChildren(): HTMLElement[] {\n const assigned = this.transcriptSlot?.assignedElements() ?? [];\n const assignedTranscript = assigned.filter(\n (element): element is HTMLElement =>\n element instanceof HTMLElement &&\n (element.matches('ds-message') || element.matches('ds-agent-response'))\n );\n const queriedTranscript = Array.from(\n this.el.querySelectorAll<HTMLElement>('ds-message, ds-agent-response')\n ).filter(element => {\n if (element.closest('.message-scroller__interaction, .message-scroller__overlay-content')) {\n return false;\n }\n if (element.matches('ds-agent-response')) return true;\n return !element.closest('ds-agent-response');\n });\n return queriedTranscript.length > assignedTranscript.length\n ? queriedTranscript\n : assignedTranscript;\n }\n\n private handleTranscriptChange = () => {\n const current = this.getTranscriptChildren();\n const previous = this.transcriptChildren;\n const hadKnownTranscript = this.knownTranscriptElements.size > 0;\n const previouslyKnown = new Set(this.knownTranscriptElements);\n const appendedAnchor = [...current].reverse().find((element, reverseIndex) => {\n if (!element.matches('ds-message[scroll-anchor]') || this.knownTurnAnchors.has(element)) {\n return false;\n }\n const index = current.length - reverseIndex - 1;\n return current.slice(index + 1).every(next => !this.knownTranscriptElements.has(next));\n });\n current.forEach(element => {\n this.knownTranscriptElements.add(element);\n if (element.matches('ds-message[scroll-anchor]')) {\n this.knownTurnAnchors.add(element);\n }\n });\n if (!previous.length) {\n this.transcriptChildren = current;\n if (appendedAnchor && hadKnownTranscript) {\n requestAnimationFrame(() => this.positionNewTurn(appendedAnchor));\n }\n return;\n }\n\n const firstKnownIndex = current.findIndex(element => previouslyKnown.has(element));\n const newTranscript = current.filter(element => !previouslyKnown.has(element));\n const prepended =\n (current.length > previous.length &&\n previous.every(\n (element, index) => current[current.length - previous.length + index] === element\n )) ||\n (!appendedAnchor &&\n newTranscript.length > 0 &&\n firstKnownIndex > 0 &&\n newTranscript.every(element => current.indexOf(element) < firstKnownIndex));\n\n this.transcriptChildren = current;\n if (prepended) {\n if (this.viewport) {\n const preservedElement = this.activeTurnAnchor?.isConnected\n ? this.activeTurnAnchor\n : previous[0];\n const preservedTop = this.activeTurnAnchor?.isConnected\n ? this.activeTurnAnchorViewportTop\n : this.firstChildViewportTop;\n if (preservedElement && preservedTop !== undefined) {\n this.reconcilePrependPosition(preservedElement, preservedTop);\n this.schedulePrependReconciliation(preservedElement, preservedTop, newTranscript);\n }\n }\n this.rememberFirstChildTop();\n this.rememberActiveTurnAnchorTop();\n this.scrollOverlayController?.sync();\n return;\n }\n if (appendedAnchor) {\n requestAnimationFrame(() => this.positionNewTurn(appendedAnchor));\n }\n };\n\n private reconcilePrependPosition(element: HTMLElement, viewportTop: number) {\n if (!this.viewport || !element.isConnected) return;\n const nextTop = element.getBoundingClientRect().top - this.viewport.getBoundingClientRect().top;\n this.viewport.scrollTop += nextTop - viewportTop;\n }\n\n private schedulePrependReconciliation(\n element: HTMLElement,\n viewportTop: number,\n prepended: HTMLElement[]\n ) {\n const generation = ++this.prependReconciliation;\n // Prepended custom elements can gain height after their child-list mutation.\n // Reconcile against the original coordinate after their layouts settle.\n const pending = prepended\n .flatMap(item => [item, ...Array.from(item.querySelectorAll<HTMLElement>('*'))])\n .map(item => {\n const componentOnReady = (\n item as HTMLElement & { componentOnReady?: () => Promise<unknown> }\n ).componentOnReady;\n return componentOnReady ? componentOnReady.call(item).catch(() => undefined) : null;\n })\n .filter((promise): promise is Promise<unknown> => Boolean(promise));\n\n void Promise.all(pending).then(() => {\n requestAnimationFrame(() => {\n if (generation !== this.prependReconciliation) return;\n this.reconcilePrependPosition(element, viewportTop);\n requestAnimationFrame(() => {\n if (generation !== this.prependReconciliation) return;\n this.reconcilePrependPosition(element, viewportTop);\n this.rememberFirstChildTop();\n this.rememberActiveTurnAnchorTop();\n this.scrollOverlayController?.sync();\n });\n });\n });\n }\n\n private handleTranscriptSlotChange = () => this.handleTranscriptChange();\n\n private positionNewTurn(anchor: HTMLElement) {\n if (!this.viewport) return;\n this.activeTurnAnchor = anchor;\n this.syncTurnClearance();\n requestAnimationFrame(() => this.finishPositionNewTurn(anchor));\n }\n\n private finishPositionNewTurn(anchor: HTMLElement) {\n if (!this.viewport || !anchor.isConnected) return;\n const viewportRect = this.viewport.getBoundingClientRect();\n const anchorTop =\n anchor.getBoundingClientRect().top - viewportRect.top + this.viewport.scrollTop;\n const customOffset = getComputedStyle(this.el)\n .getPropertyValue('--ds-message-scroller-anchor-offset')\n .trim();\n const offset = resolveCssLengthPx(customOffset, 'var(--dimension-space-800)');\n const max = Math.max(this.viewport.scrollHeight - this.viewport.clientHeight, 0);\n const target = Math.min(Math.max(anchorTop - offset, 0), max);\n this.following = max - target <= LIVE_EDGE_THRESHOLD;\n this.followReleased = !this.following;\n this.showScrollToLatest = !this.following;\n this.programmatic = true;\n this.viewport.scrollTop = target;\n requestAnimationFrame(() => {\n this.programmatic = false;\n this.following = this.isAtLiveEdge();\n this.followReleased = !this.following;\n this.showScrollToLatest = !this.following;\n this.rememberFirstChildTop();\n this.rememberActiveTurnAnchorTop();\n });\n this.scrollOverlayController?.sync();\n }\n\n private rememberActiveTurnAnchorTop() {\n if (!this.viewport || !this.activeTurnAnchor?.isConnected) return;\n this.activeTurnAnchorViewportTop =\n this.activeTurnAnchor.getBoundingClientRect().top - this.viewport.getBoundingClientRect().top;\n }\n\n private syncTurnClearance() {\n if (!this.viewport || !this.activeTurnAnchor?.isConnected) return;\n const children = this.getTranscriptChildren();\n const last = children[children.length - 1];\n if (!last) return;\n const anchorRect = this.activeTurnAnchor.getBoundingClientRect();\n const lastRect = last.getBoundingClientRect();\n const customOffset = getComputedStyle(this.el)\n .getPropertyValue('--ds-message-scroller-anchor-offset')\n .trim();\n const offset = resolveCssLengthPx(customOffset, 'var(--dimension-space-800)');\n const contentAfterAnchor = Math.max(lastRect.bottom - anchorRect.top, 0);\n const clearance = Math.max(\n Math.ceil(this.viewport.clientHeight - offset - contentAfterAnchor),\n 0\n );\n if (clearance === this.turnClearance) return;\n this.turnClearance = clearance;\n this.el.style.setProperty('--ds-message-scroller-turn-clearance', `${clearance}px`);\n }\n\n private applyDefaultPosition() {\n if (!this.viewport) return;\n this.releasePendingAtLiveEdge = false;\n if (this.defaultPosition === 'start') {\n this.viewport.scrollTop = 0;\n this.following = false;\n this.followReleased = true;\n this.showScrollToLatest = this.viewport.scrollHeight > this.viewport.clientHeight;\n this.scrollOverlayController?.sync();\n return;\n }\n if (this.defaultPosition === 'last-anchor') {\n const anchors = this.el.querySelectorAll<HTMLElement>('ds-message[scroll-anchor]');\n const last = anchors.item(anchors.length - 1);\n if (last) {\n last.scrollIntoView({ block: 'start' });\n this.following = this.isAtLiveEdge();\n this.followReleased = !this.following;\n this.showScrollToLatest = !this.following;\n this.scrollOverlayController?.sync();\n return;\n }\n }\n this.viewport.scrollTop = this.viewport.scrollHeight;\n this.following = true;\n this.followReleased = false;\n this.showScrollToLatest = false;\n this.scrollOverlayController?.sync();\n }\n\n private isAtLiveEdge(): boolean {\n if (!this.viewport) return true;\n return (\n this.viewport.scrollHeight - this.viewport.clientHeight - this.viewport.scrollTop <=\n LIVE_EDGE_THRESHOLD\n );\n }\n\n private handleScroll = () => {\n if (!this.viewport) return;\n this.scrollOverlayController?.sync();\n this.rememberFirstChildTop();\n this.rememberActiveTurnAnchorTop();\n if (this.programmatic) return;\n const atLiveEdge = this.isAtLiveEdge();\n if (this.releasePendingAtLiveEdge && atLiveEdge) {\n // A scroll event queued by the previous auto-follow can arrive after\n // reader input. Keep that stale event from restoring follow mode.\n this.following = false;\n this.followReleased = true;\n this.showScrollToLatest = false;\n return;\n }\n this.releasePendingAtLiveEdge = false;\n this.following = atLiveEdge;\n this.followReleased = !this.following;\n this.showScrollToLatest = !this.following;\n const nextAtStart = this.viewport.scrollTop <= LIVE_EDGE_THRESHOLD;\n if (nextAtStart && !this.atStart) this.dsReachStart.emit();\n this.atStart = nextAtStart;\n };\n\n private releaseFollow = () => {\n this.prependReconciliation += 1;\n this.releasePendingAtLiveEdge = this.isAtLiveEdge();\n this.following = false;\n this.followReleased = true;\n this.showScrollToLatest = !this.releasePendingAtLiveEdge;\n };\n\n private handleSelectionChange = () => {\n const selection = document.getSelection();\n if (\n selection &&\n !selection.isCollapsed &&\n selection.anchorNode &&\n this.el.contains(selection.anchorNode)\n ) {\n this.releaseFollow();\n }\n };\n\n render() {\n return (\n <Host>\n <div class=\"message-scroller\">\n <div\n class=\"message-scroller__viewport\"\n ref={element => {\n this.viewport = element;\n }}\n role=\"region\"\n aria-label={this.messagesLabel}\n tabIndex={0}\n onScroll={this.handleScroll}\n onPointerDown={(event: PointerEvent) => {\n if (event.pointerType === 'touch') this.releaseFollow();\n }}\n onWheel={this.releaseFollow}\n onKeyDown={(event: KeyboardEvent) => {\n if (\n ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', ' '].includes(\n event.key\n )\n ) {\n this.releaseFollow();\n }\n }}\n >\n <div\n class=\"message-scroller__content scroll-edge-fade--block-window\"\n ref={element => {\n this.content = element;\n }}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions\"\n aria-busy={this.busy ? 'true' : undefined}\n >\n <slot\n ref={element => {\n this.transcriptSlot = element;\n }}\n onSlotchange={this.handleTranscriptSlotChange}\n />\n </div>\n </div>\n <div\n class=\"message-scroller__overlay\"\n ref={element => {\n this.overlay = element;\n }}\n >\n <div class=\"message-scroller__overlay-stack\">\n <div class=\"message-scroller__interaction\">\n <slot name=\"interaction\" />\n </div>\n <div class=\"message-scroller__overlay-content\">\n <slot name=\"overlay\" />\n </div>\n </div>\n </div>\n {this.showScrollToLatest ? (\n <div class=\"message-scroller__control ds-control-elevation ds-control-elevation--md ds-control-elevation--press-scale\">\n <ds-tooltip label=\"Scroll to latest message\" side=\"top\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n icon=\"ChevronDown\"\n size=\"md\"\n rounded\n hasBorder={false}\n aria-label=\"Scroll to latest message\"\n onDsClick={() => void this.scrollToEnd()}\n />\n </ds-tooltip>\n </div>\n ) : null}\n </div>\n </Host>\n );\n }\n}\n",
|
|
232
|
+
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n} from '@stencil/core';\nimport type { MessageScrollerPosition } from '../conversation-types';\nimport { ScrollOverlayController } from '../../utils/scroll-overlay-controller';\nimport { resolveCssLengthPx } from '../../utils/resolve-css-length-px';\nimport {\n isMessageScrollerTranscriptReset,\n pruneMessageScrollerTranscriptSet,\n} from './message-scroller-transcript';\n\nconst LIVE_EDGE_THRESHOLD = 24;\n\n@Component({\n tag: 'ds-message-scroller',\n styleUrl: 'MessageScroller.css',\n styleUrls: ['../../utils/scroll-edge-fade.css'],\n scoped: true,\n})\nexport class MessageScroller {\n @Element() el!: HTMLElement;\n\n @Prop() busy: boolean = false;\n @Prop() autoFollow: boolean = true;\n @Prop() defaultPosition: MessageScrollerPosition = 'last-anchor';\n @Prop() messagesLabel: string = 'Messages';\n\n @Event() dsReachStart!: EventEmitter<void>;\n\n @State() private showScrollToLatest = false;\n\n private viewport?: HTMLElement;\n private content?: HTMLElement;\n private overlay?: HTMLElement;\n private transcriptSlot?: HTMLSlotElement;\n private scrollOverlayController?: ScrollOverlayController;\n private mutationObserver?: MutationObserver;\n private following = true;\n private followReleased = false;\n private releasePendingAtLiveEdge = false;\n private atStart = false;\n private programmatic = false;\n private programmaticTimer?: ReturnType<typeof setTimeout>;\n private transcriptChildren: HTMLElement[] = [];\n private knownTranscriptElements = new Set<HTMLElement>();\n private knownTurnAnchors = new Set<HTMLElement>();\n private firstChildViewportTop?: number;\n private activeTurnAnchor?: HTMLElement;\n private activeTurnAnchorViewportTop?: number;\n private turnClearance = 0;\n private prependReconciliation = 0;\n private turnPositionGeneration = 0;\n private defaultPositionGeneration = 0;\n private transcriptResetPending = false;\n private hasLoaded = false;\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.seedTranscriptBaseline();\n this.connectObservers();\n this.scheduleDefaultPosition();\n document.removeEventListener('selectionchange', this.handleSelectionChange);\n document.addEventListener('selectionchange', this.handleSelectionChange);\n }\n\n connectedCallback() {\n if (!this.hasLoaded) return;\n this.seedTranscriptBaseline();\n this.connectObservers();\n this.scheduleDefaultPosition();\n document.removeEventListener('selectionchange', this.handleSelectionChange);\n document.addEventListener('selectionchange', this.handleSelectionChange);\n }\n\n disconnectedCallback() {\n this.invalidateTranscriptWork();\n this.scrollOverlayController?.disconnect();\n this.scrollOverlayController = undefined;\n this.mutationObserver?.disconnect();\n this.mutationObserver = undefined;\n this.resetTranscriptTracking();\n this.resetTranscriptPositionState();\n this.transcriptResetPending = false;\n document.removeEventListener('selectionchange', this.handleSelectionChange);\n }\n\n @Method()\n async scrollToMessage(id: string): Promise<boolean> {\n const message = Array.from(\n this.el.querySelectorAll<HTMLElement & { messageId?: string }>('ds-message')\n ).find(item => item.messageId === id || item.getAttribute('message-id') === id);\n if (!message) return false;\n this.releasePendingAtLiveEdge = false;\n this.following = false;\n this.followReleased = true;\n this.showScrollToLatest = true;\n message.scrollIntoView({ block: 'start', behavior: this.motionBehavior() });\n return true;\n }\n\n @Method()\n async scrollToStart() {\n this.releasePendingAtLiveEdge = false;\n this.scrollTo(0);\n this.following = false;\n this.followReleased = true;\n this.showScrollToLatest = Boolean(\n this.viewport && this.viewport.scrollHeight > this.viewport.clientHeight\n );\n }\n\n @Method()\n async scrollToEnd() {\n this.releasePendingAtLiveEdge = false;\n this.following = true;\n this.followReleased = false;\n this.showScrollToLatest = false;\n this.scrollTo(this.viewport?.scrollHeight ?? 0);\n }\n\n private motionBehavior(): ScrollBehavior {\n return matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';\n }\n\n private scrollTo(top: number) {\n if (!this.viewport) return;\n this.programmatic = true;\n const behavior = this.motionBehavior();\n this.viewport.scrollTo({ top, behavior });\n if (this.programmaticTimer) clearTimeout(this.programmaticTimer);\n this.programmaticTimer = setTimeout(\n () => {\n this.programmatic = false;\n this.programmaticTimer = undefined;\n if (this.following) {\n this.viewport!.scrollTop = top;\n this.showScrollToLatest = false;\n }\n },\n behavior === 'smooth' ? 600 : 0\n );\n }\n\n private invalidateTranscriptWork() {\n this.prependReconciliation += 1;\n this.turnPositionGeneration += 1;\n this.defaultPositionGeneration += 1;\n if (this.programmaticTimer) clearTimeout(this.programmaticTimer);\n this.programmaticTimer = undefined;\n this.programmatic = false;\n }\n\n private clearActiveTurnAnchor() {\n this.activeTurnAnchor = undefined;\n this.activeTurnAnchorViewportTop = undefined;\n this.turnClearance = 0;\n this.el.style.removeProperty('--ds-message-scroller-turn-clearance');\n }\n\n private resetTranscriptTracking() {\n this.transcriptChildren = [];\n this.knownTranscriptElements.clear();\n this.knownTurnAnchors.clear();\n this.firstChildViewportTop = undefined;\n this.clearActiveTurnAnchor();\n }\n\n private resetTranscriptPositionState() {\n this.following = true;\n this.followReleased = false;\n this.releasePendingAtLiveEdge = false;\n this.atStart = false;\n this.showScrollToLatest = false;\n }\n\n private trackTranscript(elements: readonly HTMLElement[]) {\n elements.forEach(element => {\n this.knownTranscriptElements.add(element);\n if (element.matches('ds-message[scroll-anchor]')) {\n this.knownTurnAnchors.add(element);\n }\n });\n }\n\n private seedTranscriptBaseline() {\n this.resetTranscriptTracking();\n this.transcriptChildren = this.getTranscriptChildren();\n this.trackTranscript(this.transcriptChildren);\n this.transcriptResetPending = false;\n this.rememberFirstChildTop();\n }\n\n private pruneTranscriptTracking(current: readonly HTMLElement[]) {\n pruneMessageScrollerTranscriptSet(this.knownTranscriptElements, current);\n pruneMessageScrollerTranscriptSet(this.knownTurnAnchors, current);\n if (this.activeTurnAnchor && !current.includes(this.activeTurnAnchor)) {\n this.clearActiveTurnAnchor();\n }\n }\n\n private scheduleDefaultPosition() {\n const generation = ++this.defaultPositionGeneration;\n requestAnimationFrame(() => {\n if (generation !== this.defaultPositionGeneration || !this.el.isConnected) return;\n this.applyDefaultPosition();\n });\n }\n\n private connectObservers() {\n if (!this.viewport || !this.content || !this.overlay) return;\n this.scrollOverlayController?.disconnect();\n this.mutationObserver?.disconnect();\n this.scrollOverlayController = new ScrollOverlayController({\n host: this.el,\n viewport: this.viewport,\n content: this.content,\n overlay: this.overlay,\n overlayContentSelector: '.message-scroller__overlay-stack',\n onGeometryChange: () => this.handleContentGrowth(),\n });\n this.scrollOverlayController.connect();\n this.mutationObserver = new MutationObserver(() => {\n this.handleTranscriptChange();\n this.handleContentGrowth();\n });\n this.mutationObserver.observe(this.el, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n\n private handleContentGrowth() {\n if (!this.viewport) return;\n this.syncTurnClearance();\n const nextHeight = this.viewport.scrollHeight;\n if (this.autoFollow && this.following && !this.followReleased) {\n this.viewport.scrollTop = nextHeight;\n }\n if (this.releasePendingAtLiveEdge && !this.isAtLiveEdge()) {\n this.releasePendingAtLiveEdge = false;\n }\n this.showScrollToLatest = !this.following && !this.isAtLiveEdge();\n this.scrollOverlayController?.sync();\n }\n\n private rememberFirstChildTop() {\n if (!this.viewport) return;\n const first = this.getTranscriptChildren()[0];\n this.firstChildViewportTop = first\n ? first.getBoundingClientRect().top - this.viewport.getBoundingClientRect().top\n : undefined;\n }\n\n private getTranscriptChildren(): HTMLElement[] {\n const assigned = this.transcriptSlot?.assignedElements() ?? [];\n const assignedTranscript = assigned.filter(\n (element): element is HTMLElement =>\n element instanceof HTMLElement &&\n (element.matches('ds-message') || element.matches('ds-agent-response'))\n );\n const queriedTranscript = Array.from(\n this.el.querySelectorAll<HTMLElement>('ds-message, ds-agent-response')\n ).filter(element => {\n if (element.closest('.message-scroller__interaction, .message-scroller__overlay-content')) {\n return false;\n }\n if (element.matches('ds-agent-response')) return true;\n return !element.closest('ds-agent-response');\n });\n return queriedTranscript.length > assignedTranscript.length\n ? queriedTranscript\n : assignedTranscript;\n }\n\n private handleTranscriptChange = () => {\n const current = this.getTranscriptChildren();\n const previous = this.transcriptChildren;\n const transcriptReset =\n isMessageScrollerTranscriptReset(previous, current) ||\n (this.transcriptResetPending && current.length > 0);\n if (transcriptReset) {\n this.invalidateTranscriptWork();\n this.resetTranscriptTracking();\n this.resetTranscriptPositionState();\n this.transcriptChildren = current;\n this.trackTranscript(current);\n this.transcriptResetPending = current.length === 0;\n this.rememberFirstChildTop();\n this.scrollOverlayController?.sync();\n if (current.length) this.scheduleDefaultPosition();\n return;\n }\n\n const hadKnownTranscript = this.knownTranscriptElements.size > 0;\n const previouslyKnown = new Set(this.knownTranscriptElements);\n const appendedAnchor = [...current].reverse().find((element, reverseIndex) => {\n if (!element.matches('ds-message[scroll-anchor]') || this.knownTurnAnchors.has(element)) {\n return false;\n }\n const index = current.length - reverseIndex - 1;\n return current.slice(index + 1).every(next => !this.knownTranscriptElements.has(next));\n });\n this.trackTranscript(current);\n this.pruneTranscriptTracking(current);\n if (!previous.length) {\n this.transcriptChildren = current;\n if (appendedAnchor && hadKnownTranscript) {\n this.scheduleNewTurnPosition(appendedAnchor);\n }\n return;\n }\n\n const firstKnownIndex = current.findIndex(element => previouslyKnown.has(element));\n const newTranscript = current.filter(element => !previouslyKnown.has(element));\n const prepended =\n (current.length > previous.length &&\n previous.every(\n (element, index) => current[current.length - previous.length + index] === element\n )) ||\n (!appendedAnchor &&\n newTranscript.length > 0 &&\n firstKnownIndex > 0 &&\n newTranscript.every(element => current.indexOf(element) < firstKnownIndex));\n\n this.transcriptChildren = current;\n if (prepended) {\n if (this.viewport) {\n const preservedElement = this.activeTurnAnchor?.isConnected\n ? this.activeTurnAnchor\n : previous[0];\n const preservedTop = this.activeTurnAnchor?.isConnected\n ? this.activeTurnAnchorViewportTop\n : this.firstChildViewportTop;\n if (preservedElement && preservedTop !== undefined) {\n this.reconcilePrependPosition(preservedElement, preservedTop);\n this.schedulePrependReconciliation(preservedElement, preservedTop, newTranscript);\n }\n }\n this.rememberFirstChildTop();\n this.rememberActiveTurnAnchorTop();\n this.scrollOverlayController?.sync();\n return;\n }\n if (appendedAnchor) {\n this.scheduleNewTurnPosition(appendedAnchor);\n }\n };\n\n private reconcilePrependPosition(element: HTMLElement, viewportTop: number) {\n if (!this.viewport || !element.isConnected) return;\n const nextTop = element.getBoundingClientRect().top - this.viewport.getBoundingClientRect().top;\n this.viewport.scrollTop += nextTop - viewportTop;\n }\n\n private schedulePrependReconciliation(\n element: HTMLElement,\n viewportTop: number,\n prepended: HTMLElement[]\n ) {\n const generation = ++this.prependReconciliation;\n // Prepended custom elements can gain height after their child-list mutation.\n // Reconcile against the original coordinate after their layouts settle.\n const pending = prepended\n .flatMap(item => [item, ...Array.from(item.querySelectorAll<HTMLElement>('*'))])\n .map(item => {\n const componentOnReady = (\n item as HTMLElement & { componentOnReady?: () => Promise<unknown> }\n ).componentOnReady;\n return componentOnReady ? componentOnReady.call(item).catch(() => undefined) : null;\n })\n .filter((promise): promise is Promise<unknown> => Boolean(promise));\n\n void Promise.all(pending).then(() => {\n requestAnimationFrame(() => {\n if (generation !== this.prependReconciliation) return;\n this.reconcilePrependPosition(element, viewportTop);\n requestAnimationFrame(() => {\n if (generation !== this.prependReconciliation) return;\n this.reconcilePrependPosition(element, viewportTop);\n this.rememberFirstChildTop();\n this.rememberActiveTurnAnchorTop();\n this.scrollOverlayController?.sync();\n });\n });\n });\n }\n\n private handleTranscriptSlotChange = () => this.handleTranscriptChange();\n\n private scheduleNewTurnPosition(anchor: HTMLElement) {\n const generation = ++this.turnPositionGeneration;\n requestAnimationFrame(() => {\n if (generation !== this.turnPositionGeneration || !anchor.isConnected || !this.viewport) {\n return;\n }\n this.positionNewTurn(anchor, generation);\n });\n }\n\n private positionNewTurn(anchor: HTMLElement, generation: number) {\n if (generation !== this.turnPositionGeneration || !this.viewport || !anchor.isConnected) return;\n this.activeTurnAnchor = anchor;\n this.syncTurnClearance();\n requestAnimationFrame(() => {\n if (generation !== this.turnPositionGeneration) return;\n this.finishPositionNewTurn(anchor, generation);\n });\n }\n\n private finishPositionNewTurn(anchor: HTMLElement, generation: number) {\n if (generation !== this.turnPositionGeneration || !this.viewport || !anchor.isConnected) {\n return;\n }\n const viewportRect = this.viewport.getBoundingClientRect();\n const anchorTop =\n anchor.getBoundingClientRect().top - viewportRect.top + this.viewport.scrollTop;\n const customOffset = getComputedStyle(this.el)\n .getPropertyValue('--ds-message-scroller-anchor-offset')\n .trim();\n const offset = resolveCssLengthPx(customOffset, 'var(--dimension-space-800)');\n const max = Math.max(this.viewport.scrollHeight - this.viewport.clientHeight, 0);\n const target = Math.min(Math.max(anchorTop - offset, 0), max);\n this.following = max - target <= LIVE_EDGE_THRESHOLD;\n this.followReleased = !this.following;\n this.showScrollToLatest = !this.following;\n this.programmatic = true;\n this.viewport.scrollTop = target;\n requestAnimationFrame(() => {\n if (generation !== this.turnPositionGeneration) return;\n this.programmatic = false;\n this.following = this.isAtLiveEdge();\n this.followReleased = !this.following;\n this.showScrollToLatest = !this.following;\n this.rememberFirstChildTop();\n this.rememberActiveTurnAnchorTop();\n });\n this.scrollOverlayController?.sync();\n }\n\n private rememberActiveTurnAnchorTop() {\n if (!this.viewport || !this.activeTurnAnchor?.isConnected) return;\n this.activeTurnAnchorViewportTop =\n this.activeTurnAnchor.getBoundingClientRect().top - this.viewport.getBoundingClientRect().top;\n }\n\n private syncTurnClearance() {\n if (!this.viewport || !this.activeTurnAnchor?.isConnected) return;\n const children = this.getTranscriptChildren();\n const last = children[children.length - 1];\n if (!last) return;\n const anchorRect = this.activeTurnAnchor.getBoundingClientRect();\n const lastRect = last.getBoundingClientRect();\n const customOffset = getComputedStyle(this.el)\n .getPropertyValue('--ds-message-scroller-anchor-offset')\n .trim();\n const offset = resolveCssLengthPx(customOffset, 'var(--dimension-space-800)');\n const contentAfterAnchor = Math.max(lastRect.bottom - anchorRect.top, 0);\n const clearance = Math.max(\n Math.ceil(this.viewport.clientHeight - offset - contentAfterAnchor),\n 0\n );\n if (clearance === this.turnClearance) return;\n this.turnClearance = clearance;\n this.el.style.setProperty('--ds-message-scroller-turn-clearance', `${clearance}px`);\n }\n\n private applyDefaultPosition() {\n if (!this.viewport) return;\n this.releasePendingAtLiveEdge = false;\n if (this.defaultPosition === 'start') {\n this.viewport.scrollTop = 0;\n this.following = false;\n this.followReleased = true;\n this.showScrollToLatest = this.viewport.scrollHeight > this.viewport.clientHeight;\n this.scrollOverlayController?.sync();\n return;\n }\n if (this.defaultPosition === 'last-anchor') {\n const anchors = this.el.querySelectorAll<HTMLElement>('ds-message[scroll-anchor]');\n const last = anchors.item(anchors.length - 1);\n if (last) {\n last.scrollIntoView({ block: 'start' });\n this.following = this.isAtLiveEdge();\n this.followReleased = !this.following;\n this.showScrollToLatest = !this.following;\n this.scrollOverlayController?.sync();\n return;\n }\n }\n this.viewport.scrollTop = this.viewport.scrollHeight;\n this.following = true;\n this.followReleased = false;\n this.showScrollToLatest = false;\n this.scrollOverlayController?.sync();\n }\n\n private isAtLiveEdge(): boolean {\n if (!this.viewport) return true;\n return (\n this.viewport.scrollHeight - this.viewport.clientHeight - this.viewport.scrollTop <=\n LIVE_EDGE_THRESHOLD\n );\n }\n\n private handleScroll = () => {\n if (!this.viewport) return;\n this.scrollOverlayController?.sync();\n this.rememberFirstChildTop();\n this.rememberActiveTurnAnchorTop();\n if (this.programmatic) return;\n const atLiveEdge = this.isAtLiveEdge();\n if (this.releasePendingAtLiveEdge && atLiveEdge) {\n // A scroll event queued by the previous auto-follow can arrive after\n // reader input. Keep that stale event from restoring follow mode.\n this.following = false;\n this.followReleased = true;\n this.showScrollToLatest = false;\n return;\n }\n this.releasePendingAtLiveEdge = false;\n this.following = atLiveEdge;\n this.followReleased = !this.following;\n this.showScrollToLatest = !this.following;\n const nextAtStart = this.viewport.scrollTop <= LIVE_EDGE_THRESHOLD;\n if (nextAtStart && !this.atStart) this.dsReachStart.emit();\n this.atStart = nextAtStart;\n };\n\n private releaseFollow = () => {\n this.prependReconciliation += 1;\n this.releasePendingAtLiveEdge = this.isAtLiveEdge();\n this.following = false;\n this.followReleased = true;\n this.showScrollToLatest = !this.releasePendingAtLiveEdge;\n };\n\n private handleSelectionChange = () => {\n const selection = document.getSelection();\n if (\n selection &&\n !selection.isCollapsed &&\n selection.anchorNode &&\n this.el.contains(selection.anchorNode)\n ) {\n this.releaseFollow();\n }\n };\n\n render() {\n return (\n <Host>\n <div class=\"message-scroller\">\n <div\n class=\"message-scroller__viewport\"\n ref={element => {\n this.viewport = element;\n }}\n role=\"region\"\n aria-label={this.messagesLabel}\n tabIndex={0}\n onScroll={this.handleScroll}\n onPointerDown={(event: PointerEvent) => {\n if (event.pointerType === 'touch') this.releaseFollow();\n }}\n onWheel={this.releaseFollow}\n onKeyDown={(event: KeyboardEvent) => {\n if (\n ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', ' '].includes(\n event.key\n )\n ) {\n this.releaseFollow();\n }\n }}\n >\n <div\n class=\"message-scroller__content scroll-edge-fade--block-window\"\n ref={element => {\n this.content = element;\n }}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions\"\n aria-busy={this.busy ? 'true' : undefined}\n >\n <slot\n ref={element => {\n this.transcriptSlot = element;\n }}\n onSlotchange={this.handleTranscriptSlotChange}\n />\n </div>\n </div>\n <div\n class=\"message-scroller__overlay\"\n ref={element => {\n this.overlay = element;\n }}\n >\n <div class=\"message-scroller__overlay-stack\">\n <div class=\"message-scroller__interaction\">\n <slot name=\"interaction\" />\n </div>\n <div class=\"message-scroller__overlay-content\">\n <slot name=\"overlay\" />\n </div>\n </div>\n </div>\n {this.showScrollToLatest ? (\n <div class=\"message-scroller__control ds-control-elevation ds-control-elevation--md ds-control-elevation--press-scale\">\n <ds-tooltip label=\"Scroll to latest message\" side=\"top\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n icon=\"ChevronDown\"\n size=\"md\"\n rounded\n hasBorder={false}\n aria-label=\"Scroll to latest message\"\n onDsClick={() => void this.scrollToEnd()}\n />\n </ds-tooltip>\n </div>\n ) : null}\n </div>\n </Host>\n );\n }\n}\n",
|
|
233
|
+
"type": "registry:ui"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"path": "src/wc/components/MessageScroller/message-scroller-transcript.ts",
|
|
237
|
+
"content": "/**\n * A transcript reset replaces the current baseline instead of appending or\n * prepending around surviving nodes. Emptying a populated transcript also\n * starts a reset that completes when its next baseline is supplied.\n */\nexport function isMessageScrollerTranscriptReset<T>(\n previous: readonly T[],\n current: readonly T[]\n): boolean {\n if (!previous.length) return false;\n if (!current.length) return true;\n const previousElements = new Set(previous);\n return !current.some(element => previousElements.has(element));\n}\n\n/** Keep identity tracking bounded to nodes in the current transcript. */\nexport function pruneMessageScrollerTranscriptSet<T>(known: Set<T>, current: readonly T[]): void {\n const currentElements = new Set(current);\n for (const element of known) {\n if (!currentElements.has(element)) known.delete(element);\n }\n}\n",
|
|
231
238
|
"type": "registry:ui"
|
|
232
239
|
}
|
|
233
240
|
]
|
|
@@ -387,7 +387,7 @@
|
|
|
387
387
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: currentArea.",
|
|
388
388
|
"peerDependencies": {
|
|
389
389
|
"required": [
|
|
390
|
-
"@ds-mo/tokens ^6.5.
|
|
390
|
+
"@ds-mo/tokens ^6.5.1",
|
|
391
391
|
"@ds-mo/icons ^7.0.0"
|
|
392
392
|
],
|
|
393
393
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -323,7 +323,7 @@
|
|
|
323
323
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: sections, subsections.",
|
|
324
324
|
"peerDependencies": {
|
|
325
325
|
"required": [
|
|
326
|
-
"@ds-mo/tokens ^6.5.
|
|
326
|
+
"@ds-mo/tokens ^6.5.1",
|
|
327
327
|
"@ds-mo/icons ^7.0.0"
|
|
328
328
|
],
|
|
329
329
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: sections.",
|
|
159
159
|
"peerDependencies": {
|
|
160
160
|
"required": [
|
|
161
|
-
"@ds-mo/tokens ^6.5.
|
|
161
|
+
"@ds-mo/tokens ^6.5.1",
|
|
162
162
|
"@ds-mo/icons ^7.0.0"
|
|
163
163
|
],
|
|
164
164
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -328,7 +328,7 @@
|
|
|
328
328
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: dashboardGroups, settingsGroups.",
|
|
329
329
|
"peerDependencies": {
|
|
330
330
|
"required": [
|
|
331
|
-
"@ds-mo/tokens ^6.5.
|
|
331
|
+
"@ds-mo/tokens ^6.5.1",
|
|
332
332
|
"@ds-mo/icons ^7.0.0"
|
|
333
333
|
],
|
|
334
334
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -405,7 +405,7 @@
|
|
|
405
405
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: pageSizeOptions.",
|
|
406
406
|
"peerDependencies": {
|
|
407
407
|
"required": [
|
|
408
|
-
"@ds-mo/tokens ^6.5.
|
|
408
|
+
"@ds-mo/tokens ^6.5.1",
|
|
409
409
|
"@ds-mo/icons ^7.0.0"
|
|
410
410
|
],
|
|
411
411
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: items.",
|
|
221
221
|
"peerDependencies": {
|
|
222
222
|
"required": [
|
|
223
|
-
"@ds-mo/tokens ^6.5.
|
|
223
|
+
"@ds-mo/tokens ^6.5.1",
|
|
224
224
|
"@ds-mo/icons ^7.0.0"
|
|
225
225
|
],
|
|
226
226
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -317,7 +317,7 @@
|
|
|
317
317
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: actions.",
|
|
318
318
|
"peerDependencies": {
|
|
319
319
|
"required": [
|
|
320
|
-
"@ds-mo/tokens ^6.5.
|
|
320
|
+
"@ds-mo/tokens ^6.5.1",
|
|
321
321
|
"@ds-mo/icons ^7.0.0"
|
|
322
322
|
],
|
|
323
323
|
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|