@ds-mo/ui 14.0.0 → 14.1.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 +1 -1
- package/dist/agent.json +26 -6
- package/dist/angular/ds-mobile-sheet-nav.d.ts +1 -1
- package/dist/angular/ds-mobile-sheet-nav.js +3 -3
- package/dist/angular/ds-tab-group.d.ts +1 -1
- package/dist/angular/ds-tab-group.js +3 -3
- package/dist/components/ds-agent-questionnaire.js +1 -1
- package/dist/components/ds-agent-response.js +1 -1
- package/dist/components/ds-banner.js +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-title.js +1 -1
- package/dist/components/ds-bar-workflow.js +1 -1
- package/dist/components/ds-button-filled.js +1 -1
- package/dist/components/ds-button-unfilled.js +1 -1
- package/dist/components/ds-card-chart.js +1 -1
- package/dist/components/ds-card-overview.js +1 -1
- package/dist/components/ds-card-overview.js.map +1 -1
- package/dist/components/ds-card-setting.js +1 -1
- package/dist/components/ds-chart-legend.js +1 -1
- package/dist/components/ds-chart-legend.js.map +1 -1
- package/dist/components/ds-checkbox.js +1 -1
- package/dist/components/ds-chip.js +1 -1
- package/dist/components/ds-chip.js.map +1 -1
- package/dist/components/ds-code-block.js +1 -1
- package/dist/components/ds-conversation-list-item.js +1 -1
- package/dist/components/ds-conversation-list-item.js.map +1 -1
- package/dist/components/ds-filter-menu.js +1 -1
- package/dist/components/ds-filter-menu.js.map +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-input.js.map +1 -1
- package/dist/components/ds-markdown.js +1 -1
- package/dist/components/ds-menu.js +1 -1
- package/dist/components/ds-message-actions.js +1 -1
- package/dist/components/ds-message-composer.js +1 -1
- package/dist/components/ds-message-scroller.js +1 -1
- package/dist/components/ds-message-scroller.js.map +1 -1
- package/dist/components/ds-mobile-bar-nav.js +1 -1
- package/dist/components/ds-mobile-header.js +1 -1
- package/dist/components/ds-mobile-section-switcher.js +1 -1
- package/dist/components/ds-mobile-sheet-nav.js +1 -1
- package/dist/components/ds-modal.js +1 -1
- package/dist/components/ds-panel-nav.js +1 -1
- package/dist/components/ds-panel-sub-nav.js +1 -1
- package/dist/components/ds-panel-sub-nav.js.map +1 -1
- package/dist/components/ds-panel-tool-header.js +1 -1
- package/dist/components/ds-panel-tool-search.js +1 -1
- package/dist/components/ds-panel-tools.js +1 -1
- package/dist/components/ds-radio.js +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-select.js.map +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-page.js +1 -1
- package/dist/components/ds-shell-tools.js +1 -1
- package/dist/components/ds-tab-group.js +1 -1
- package/dist/components/ds-table.js +1 -1
- package/dist/components/ds-table.js.map +1 -1
- package/dist/components/ds-tag.js +1 -1
- package/dist/components/ds-toast.js +1 -1
- package/dist/components/p-1tOy4H51.js +2 -0
- package/dist/components/{p-DatlGTJp.js.map → p-1tOy4H51.js.map} +1 -1
- package/dist/components/{p-nMkoXLC0.js → p-BiOfbCke.js} +2 -2
- package/dist/components/{p-1KtrhAC7.js → p-BnPcWBA8.js} +2 -2
- package/dist/components/{p-D0i_J2rj.js → p-CRjpqqAI.js} +2 -2
- package/dist/components/{p-D0i_J2rj.js.map → p-CRjpqqAI.js.map} +1 -1
- package/dist/components/p-CTTDiW33.js +2 -0
- package/dist/components/{p--z-DWRkg.js.map → p-CTTDiW33.js.map} +1 -1
- package/dist/components/{p-DXgSSxyB.js → p-CZncU32j.js} +2 -2
- package/dist/components/p-CZncU32j.js.map +1 -0
- package/dist/components/{p-CQS5aWiC.js → p-ChlRsQYY.js} +2 -2
- package/dist/components/p-CrjsT3s7.js +2 -0
- package/dist/components/p-CrjsT3s7.js.map +1 -0
- package/dist/components/p-D4bL9H8v.js +2 -0
- package/dist/components/p-D4bL9H8v.js.map +1 -0
- package/dist/components/{p-BaiGt_o6.js → p-DD5OCVXx.js} +2 -2
- package/dist/components/{p-BaiGt_o6.js.map → p-DD5OCVXx.js.map} +1 -1
- package/dist/components/p-DJ_FsLCc.js +2 -0
- package/dist/components/{p-DofS_hVZ.js.map → p-DJ_FsLCc.js.map} +1 -1
- package/dist/components/{p-DXHq_AfZ.js → p-DT4WdAup.js} +2 -2
- package/dist/components/{p-DYoCb_sj.js → p-DYzjeIAT.js} +2 -2
- package/dist/components/{p-DYoCb_sj.js.map → p-DYzjeIAT.js.map} +1 -1
- package/dist/components/p-Dly2SR-q.js +2 -0
- package/dist/components/{p-BKsWDUnX.js.map → p-Dly2SR-q.js.map} +1 -1
- package/dist/components/p-DmZupC1-.js +2 -0
- package/dist/components/p-DmZupC1-.js.map +1 -0
- package/dist/components/p-DmwMiaWL.js +2 -0
- package/dist/components/p-DmwMiaWL.js.map +1 -0
- package/dist/components/p-Dr4AgJbV.js +2 -0
- package/dist/components/{p-DAW6G8Sl.js.map → p-Dr4AgJbV.js.map} +1 -1
- package/dist/components/p-DuFm-ieS.js +2 -0
- package/dist/components/p-DuFm-ieS.js.map +1 -0
- package/dist/components/p-DvJkkdIO.js +2 -0
- package/dist/components/{p-DY_sA9Jz.js.map → p-DvJkkdIO.js.map} +1 -1
- package/dist/components/p-NTU5jrG_.js +2 -0
- package/dist/components/{p-ZtJM0cZm.js.map → p-NTU5jrG_.js.map} +1 -1
- package/dist/components/p-NdahPypA.js.map +1 -1
- package/dist/components/{p-S9IjLCGj.js → p-c9QiNOc7.js} +2 -2
- package/dist/components/{p-CIhETsHv.js → p-eKARJmWs.js} +2 -2
- package/dist/components/p-jMCRaxQW.js +2 -0
- package/dist/components/{p-CUeuemsF.js.map → p-jMCRaxQW.js.map} +1 -1
- package/dist/docs/components.json +106 -3
- package/dist/lib/components/ShellApp/shell-app-types.d.ts +2 -0
- package/dist/mcp-data/registry/card-overview.json +1 -1
- package/dist/mcp-data/registry/input.json +1 -1
- package/dist/mcp-data/registry/message-scroller.json +1 -1
- package/dist/mcp-data/registry/mobile-header.json +4 -3
- package/dist/mcp-data/registry/mobile-sheet-nav.json +43 -4
- package/dist/mcp-data/registry/panel-nav.json +5 -4
- package/dist/mcp-data/registry/registry.json +25 -5
- package/dist/mcp-data/registry/shell-app.json +5 -3
- package/dist/mcp-data/registry/shell-page.json +2 -2
- package/dist/mcp-data/registry/tab-group.json +22 -3
- package/dist/types/components/MessageScroller/MessageScroller.d.ts +1 -0
- package/dist/types/components/MobileSheetNav/MobileSheetNav.d.ts +4 -0
- package/dist/types/components/PanelNav/PanelNav.d.ts +1 -1
- package/dist/types/components/ShellApp/ShellApp.d.ts +1 -0
- package/dist/types/components/ShellApp/shell-app-types.d.ts +2 -0
- package/dist/types/components/ShellPage/ShellPage.d.ts +3 -0
- package/dist/types/components/TabGroup/TabGroup.d.ts +4 -0
- package/dist/types/components.d.ts +37 -4
- package/package.json +2 -2
- package/dist/components/p--z-DWRkg.js +0 -2
- package/dist/components/p-BKsWDUnX.js +0 -2
- package/dist/components/p-CElfUmMp.js +0 -2
- package/dist/components/p-CElfUmMp.js.map +0 -1
- package/dist/components/p-CUeuemsF.js +0 -2
- package/dist/components/p-Cae9PFPE.js +0 -2
- package/dist/components/p-Cae9PFPE.js.map +0 -1
- package/dist/components/p-Cih1XCy3.js +0 -2
- package/dist/components/p-Cih1XCy3.js.map +0 -1
- package/dist/components/p-CoTD4Mmb.js +0 -2
- package/dist/components/p-CoTD4Mmb.js.map +0 -1
- package/dist/components/p-DAW6G8Sl.js +0 -2
- package/dist/components/p-DXgSSxyB.js.map +0 -1
- package/dist/components/p-DY_sA9Jz.js +0 -2
- package/dist/components/p-DatlGTJp.js +0 -2
- package/dist/components/p-DofS_hVZ.js +0 -2
- package/dist/components/p-ZtJM0cZm.js +0 -2
- /package/dist/components/{p-nMkoXLC0.js.map → p-BiOfbCke.js.map} +0 -0
- /package/dist/components/{p-1KtrhAC7.js.map → p-BnPcWBA8.js.map} +0 -0
- /package/dist/components/{p-CQS5aWiC.js.map → p-ChlRsQYY.js.map} +0 -0
- /package/dist/components/{p-DXHq_AfZ.js.map → p-DT4WdAup.js.map} +0 -0
- /package/dist/components/{p-S9IjLCGj.js.map → p-c9QiNOc7.js.map} +0 -0
- /package/dist/components/{p-CIhETsHv.js.map → p-eKARJmWs.js.map} +0 -0
|
@@ -222,7 +222,7 @@
|
|
|
222
222
|
},
|
|
223
223
|
{
|
|
224
224
|
"path": "src/wc/components/MessageScroller/MessageScroller.tsx",
|
|
225
|
-
"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 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.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.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.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 this.programmatic = false;\n this.programmaticTimer = undefined;\n if (this.following) {\n this.viewport!.scrollTop = top;\n this.showScrollToLatest = false;\n }\n }, behavior === 'smooth' ? 600 : 0);\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 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 (\n element.closest(\n '.message-scroller__interaction, .message-scroller__overlay-content',\n )\n ) {\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 (\n !element.matches('ds-message[scroll-anchor]') ||\n this.knownTurnAnchors.has(element)\n ) {\n return false;\n }\n const index = current.length - reverseIndex - 1;\n return current\n .slice(index + 1)\n .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 && 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(\n preservedElement,\n preservedTop,\n newTranscript,\n );\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 =\n 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 -\n 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 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 this.following = this.isAtLiveEdge();\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.following = false;\n this.followReleased = true;\n this.showScrollToLatest = true;\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.following = false;\n this.followReleased = true;\n this.showScrollToLatest = !this.isAtLiveEdge();\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",
|
|
225
|
+
"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 this.programmatic = false;\n this.programmaticTimer = undefined;\n if (this.following) {\n this.viewport!.scrollTop = top;\n this.showScrollToLatest = false;\n }\n }, behavior === 'smooth' ? 600 : 0);\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 (\n element.closest(\n '.message-scroller__interaction, .message-scroller__overlay-content',\n )\n ) {\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 (\n !element.matches('ds-message[scroll-anchor]') ||\n this.knownTurnAnchors.has(element)\n ) {\n return false;\n }\n const index = current.length - reverseIndex - 1;\n return current\n .slice(index + 1)\n .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 && 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(\n preservedElement,\n preservedTop,\n newTranscript,\n );\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 =\n 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 -\n 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",
|
|
226
226
|
"type": "registry:ui"
|
|
227
227
|
}
|
|
228
228
|
]
|
|
@@ -39,13 +39,14 @@
|
|
|
39
39
|
"states": [
|
|
40
40
|
"Without multiple sections the center renders heading.",
|
|
41
41
|
"With multiple sections the center renders the controlled MobileSectionSwitcher by default or an explicitly requested segmented TabGroup.",
|
|
42
|
+
"The segmented presentation uses the shared fill-width TabGroup contract and divides its centered two-thirds top-bar lane equally between options.",
|
|
42
43
|
"Optional subsections render in a second row and emit dsSubsectionChange without changing the primary section.",
|
|
43
44
|
"Sections represent peer destinations at the current navigation level; subsections represent local views beneath the selected page or detail identity.",
|
|
44
45
|
"Brand tone changes surface and foreground without changing geometry or interaction ownership."
|
|
45
46
|
],
|
|
46
47
|
"responsiveBehavior": [
|
|
47
48
|
"The primary 56px lane matches the Mobile Sheet Nav header and Mobile Bar Nav; optional subsections add one subordinate 48px lane.",
|
|
48
|
-
"Equal flexible side lanes keep the center mathematically aligned when actions are asymmetric."
|
|
49
|
+
"Equal flexible side lanes keep the center mathematically aligned when actions are asymmetric; segmented headers allocate the usable lane in a 1:4:1 ratio."
|
|
49
50
|
],
|
|
50
51
|
"references": [
|
|
51
52
|
{
|
|
@@ -336,12 +337,12 @@
|
|
|
336
337
|
"files": [
|
|
337
338
|
{
|
|
338
339
|
"path": "src/wc/components/MobileHeader/MobileHeader.css",
|
|
339
|
-
"content": "@import '../../utils/chrome-layout.css';\n\n:host {\n display: block;\n min-width: 0;\n box-sizing: border-box;\n background: var(--color-background-primary);\n color: var(--color-foreground-primary);\n}\n\n:host([tone='brand']) {\n background: var(--color-background-bold-brand);\n color: var(--color-foreground-on-bold);\n}\n\n.mobile-header {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n min-width: 0;\n box-sizing: border-box;\n background: inherit;\n color: inherit;\n}\n\n.mobile-header__primary {\n grid-template-columns: minmax(var(--dimension-size-400), 1fr) minmax(0, auto) minmax(var(--dimension-size-400), 1fr);\n width: 100%;\n height: calc(var(--dimension-size-500) + var(--dimension-space-200));\n min-width: 0;\n background: inherit;\n color: inherit;\n}\n\n:host(:not([tone='brand'])) .mobile-header::after {\n content: '';\n position: absolute;\n inset-inline: 0;\n inset-block-end: 0;\n height: var(--dimension-stroke-width-012);\n background: var(--color-border-tertiary);\n pointer-events: none;\n}\n\n.mobile-header__subsections {\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n height: var(--dimension-size-600);\n padding: var(--dimension-space-100) var(--dimension-space-200);\n box-sizing: border-box;\n}\n\n.mobile-header__subsections ds-mobile-section-switcher {\n min-width: 0;\n max-width: 100%;\n}\n\n.mobile-header__lane {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n min-width: 0;\n}\n\n.mobile-header__lane--leading {\n justify-content: flex-start;\n}\n\n.mobile-header__lane--trailing {\n justify-content: flex-end;\n}\n\n.mobile-header__center {\n position: relative;\n min-width: 0;\n max-width: 100%;\n justify-self: center;\n}\n\n.mobile-header__heading {\n display: block;\n min-width: 0;\n max-width: 100%;\n text-align: center;\n}\n\n.mobile-header__semantic-heading {\n position: absolute;\n width: var(--dimension-stroke-width-012);\n height: var(--dimension-stroke-width-012);\n padding: 0;\n margin: calc(var(--dimension-stroke-width-012) * -1);\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n::slotted([slot='leading']),\n::slotted([slot='trailing']) {\n flex: 0 0 auto;\n}\n",
|
|
340
|
+
"content": "@import '../../utils/chrome-layout.css';\n\n:host {\n display: block;\n min-width: 0;\n box-sizing: border-box;\n background: var(--color-background-primary);\n color: var(--color-foreground-primary);\n}\n\n:host([tone='brand']) {\n background: var(--color-background-bold-brand);\n color: var(--color-foreground-on-bold);\n}\n\n.mobile-header {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n min-width: 0;\n box-sizing: border-box;\n background: inherit;\n color: inherit;\n}\n\n.mobile-header__primary {\n grid-template-columns: minmax(var(--dimension-size-400), 1fr) minmax(0, auto) minmax(var(--dimension-size-400), 1fr);\n width: 100%;\n height: calc(var(--dimension-size-500) + var(--dimension-space-200));\n min-width: 0;\n background: inherit;\n color: inherit;\n}\n\n.mobile-header__primary--segmented {\n grid-template-columns:\n minmax(var(--dimension-size-500), 1fr)\n minmax(0, 4fr)\n minmax(var(--dimension-size-500), 1fr);\n}\n\n.mobile-header__primary--segmented .mobile-header__center,\n.mobile-header__primary--segmented ds-tab-group {\n width: 100%;\n}\n\n:host(:not([tone='brand'])) .mobile-header::after {\n content: '';\n position: absolute;\n inset-inline: 0;\n inset-block-end: 0;\n height: var(--dimension-stroke-width-012);\n background: var(--color-border-tertiary);\n pointer-events: none;\n}\n\n.mobile-header__subsections {\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n height: var(--dimension-size-600);\n padding: var(--dimension-space-100) var(--dimension-space-200);\n box-sizing: border-box;\n}\n\n.mobile-header__subsections ds-mobile-section-switcher {\n min-width: 0;\n max-width: 100%;\n}\n\n.mobile-header__lane {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n min-width: 0;\n}\n\n.mobile-header__lane--leading {\n justify-content: flex-start;\n}\n\n.mobile-header__lane--trailing {\n justify-content: flex-end;\n}\n\n.mobile-header__center {\n position: relative;\n min-width: 0;\n max-width: 100%;\n justify-self: center;\n}\n\n.mobile-header__heading {\n display: block;\n min-width: 0;\n max-width: 100%;\n text-align: center;\n}\n\n.mobile-header__semantic-heading {\n position: absolute;\n width: var(--dimension-stroke-width-012);\n height: var(--dimension-stroke-width-012);\n padding: 0;\n margin: calc(var(--dimension-stroke-width-012) * -1);\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n::slotted([slot='leading']),\n::slotted([slot='trailing']) {\n flex: 0 0 auto;\n}\n",
|
|
340
341
|
"type": "registry:ui"
|
|
341
342
|
},
|
|
342
343
|
{
|
|
343
344
|
"path": "src/wc/components/MobileHeader/MobileHeader.tsx",
|
|
344
|
-
"content": "import { Component, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\nimport {\n getSelectableTabs,\n type TabGroupItem,\n type TabItem,\n} from '../TabGroup/tab-item-utils';\nimport type { TabGroupSize } from '../TabGroup/TabGroup';\nimport type {\n MobileHeaderHeadingLevel,\n MobileHeaderSectionsPresentation,\n MobileHeaderTone,\n} from './mobile-header-types';\n\n@Component({\n tag: 'ds-mobile-header',\n styleUrl: 'MobileHeader.css',\n scoped: true,\n})\nexport class MobileHeader {\n /** Static title used when no peer sections are supplied. */\n @Prop() heading: string = '';\n /** Semantic heading level for the active mobile screen. */\n @Prop() headingLevel: MobileHeaderHeadingLevel = 'h1';\n /** Controlled peer sections. Their selected label replaces the static title. */\n @Prop() sections: TabGroupItem[] = [];\n /** Controlled selected section id. */\n @Prop() value: string = '';\n /** Accessible name for the section chooser. */\n @Prop() sectionsAriaLabel: string = 'Change page section';\n /** Popup switcher or an inline segmented TabGroup for peer sections. */\n @Prop() sectionsPresentation: MobileHeaderSectionsPresentation = 'switcher';\n /** Density for the segmented sections presentation. */\n @Prop() sectionsSize: TabGroupSize = 'md';\n /** Controlled child sections within the selected page or detail screen. */\n @Prop() subsections: TabItem[] = [];\n /** Controlled selected child-section id. */\n @Prop() subvalue: string = '';\n /** Accessible name for the child-section chooser. */\n @Prop() subsectionsAriaLabel: string = 'Change page subsection';\n /** Default page chrome or bold-brand workflow chrome. */\n @Prop({ reflect: true }) tone: MobileHeaderTone = 'default';\n\n /** Section selection intent. */\n @Event() dsSectionChange!: EventEmitter<string>;\n /** Child-section selection intent. */\n @Event() dsSubsectionChange!: EventEmitter<string>;\n\n private get resolvedSections(): TabGroupItem[] {\n return this.sections ?? [];\n }\n\n private get resolvedSubsections(): TabItem[] {\n return this.subsections ?? [];\n }\n\n private get selectedLabel(): string {\n const selectable = getSelectableTabs(this.resolvedSections);\n return selectable.find(section => section.id === this.value)?.label ??\n selectable[0]?.label ??\n this.heading;\n }\n\n private handleSectionChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsSectionChange.emit(event.detail);\n };\n\n private handleSubsectionChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsSubsectionChange.emit(event.detail);\n };\n\n render() {\n const Heading = this.headingLevel;\n const selectable = getSelectableTabs(this.resolvedSections);\n const hasSwitcher = selectable.length > 1;\n const hasSubsections = getSelectableTabs(this.resolvedSubsections).length > 1;\n const foreground = this.tone === 'brand' ? 'on-bold' : 'primary';\n\n return (\n <Host>\n <header class=\"mobile-header\">\n <div
|
|
345
|
+
"content": "import { Component, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\nimport {\n getSelectableTabs,\n type TabGroupItem,\n type TabItem,\n} from '../TabGroup/tab-item-utils';\nimport type { TabGroupSize } from '../TabGroup/TabGroup';\nimport type {\n MobileHeaderHeadingLevel,\n MobileHeaderSectionsPresentation,\n MobileHeaderTone,\n} from './mobile-header-types';\n\n@Component({\n tag: 'ds-mobile-header',\n styleUrl: 'MobileHeader.css',\n scoped: true,\n})\nexport class MobileHeader {\n /** Static title used when no peer sections are supplied. */\n @Prop() heading: string = '';\n /** Semantic heading level for the active mobile screen. */\n @Prop() headingLevel: MobileHeaderHeadingLevel = 'h1';\n /** Controlled peer sections. Their selected label replaces the static title. */\n @Prop() sections: TabGroupItem[] = [];\n /** Controlled selected section id. */\n @Prop() value: string = '';\n /** Accessible name for the section chooser. */\n @Prop() sectionsAriaLabel: string = 'Change page section';\n /** Popup switcher or an inline segmented TabGroup for peer sections. */\n @Prop() sectionsPresentation: MobileHeaderSectionsPresentation = 'switcher';\n /** Density for the segmented sections presentation. */\n @Prop() sectionsSize: TabGroupSize = 'md';\n /** Controlled child sections within the selected page or detail screen. */\n @Prop() subsections: TabItem[] = [];\n /** Controlled selected child-section id. */\n @Prop() subvalue: string = '';\n /** Accessible name for the child-section chooser. */\n @Prop() subsectionsAriaLabel: string = 'Change page subsection';\n /** Default page chrome or bold-brand workflow chrome. */\n @Prop({ reflect: true }) tone: MobileHeaderTone = 'default';\n\n /** Section selection intent. */\n @Event() dsSectionChange!: EventEmitter<string>;\n /** Child-section selection intent. */\n @Event() dsSubsectionChange!: EventEmitter<string>;\n\n private get resolvedSections(): TabGroupItem[] {\n return this.sections ?? [];\n }\n\n private get resolvedSubsections(): TabItem[] {\n return this.subsections ?? [];\n }\n\n private get selectedLabel(): string {\n const selectable = getSelectableTabs(this.resolvedSections);\n return selectable.find(section => section.id === this.value)?.label ??\n selectable[0]?.label ??\n this.heading;\n }\n\n private handleSectionChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsSectionChange.emit(event.detail);\n };\n\n private handleSubsectionChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsSubsectionChange.emit(event.detail);\n };\n\n render() {\n const Heading = this.headingLevel;\n const selectable = getSelectableTabs(this.resolvedSections);\n const hasSwitcher = selectable.length > 1;\n const hasSubsections = getSelectableTabs(this.resolvedSubsections).length > 1;\n const hasSegmentedSections = hasSwitcher && this.sectionsPresentation === 'segmented';\n const foreground = this.tone === 'brand' ? 'on-bold' : 'primary';\n\n return (\n <Host>\n <header class=\"mobile-header\">\n <div\n class={{\n 'mobile-header__primary': true,\n 'mobile-header__primary--segmented': hasSegmentedSections,\n 'ds-chrome-grid': true,\n 'ds-chrome-space--md': true,\n }}\n >\n <div class=\"mobile-header__lane mobile-header__lane--leading\">\n <slot name=\"leading\" />\n </div>\n <div class=\"mobile-header__center\">\n {hasSwitcher ? (\n [\n <span\n class=\"mobile-header__semantic-heading\"\n role=\"heading\"\n aria-level={this.headingLevel === 'h1' ? '1' : '2'}\n >\n {this.selectedLabel}\n </span>,\n this.sectionsPresentation === 'segmented' ? (\n <ds-tab-group\n tabs={this.resolvedSections}\n value={this.value}\n size={this.sectionsSize}\n width=\"fill\"\n ariaLabel={this.sectionsAriaLabel}\n onDsChange={this.handleSectionChange}\n />\n ) : (\n <ds-mobile-section-switcher\n sections={this.resolvedSections}\n value={this.value}\n navigationLabel={this.sectionsAriaLabel}\n onDsChange={this.handleSectionChange}\n />\n ),\n ]\n ) : (\n <ds-text\n class=\"mobile-header__heading\"\n as={Heading}\n variant=\"text-body-medium\"\n emphasis\n color={foreground}\n lineTruncation={1}\n >\n {this.selectedLabel}\n </ds-text>\n )}\n </div>\n <div class=\"mobile-header__lane mobile-header__lane--trailing\">\n <slot name=\"trailing\" />\n </div>\n </div>\n {hasSubsections ? (\n <nav class=\"mobile-header__subsections\" aria-label={this.subsectionsAriaLabel}>\n <ds-mobile-section-switcher\n sections={this.resolvedSubsections}\n value={this.subvalue}\n navigationLabel={this.subsectionsAriaLabel}\n onDsChange={this.handleSubsectionChange}\n />\n </nav>\n ) : null}\n </header>\n </Host>\n );\n }\n}\n",
|
|
345
346
|
"type": "registry:ui"
|
|
346
347
|
},
|
|
347
348
|
{
|
|
@@ -30,16 +30,19 @@
|
|
|
30
30
|
],
|
|
31
31
|
"accessibility": [
|
|
32
32
|
"The sheet is a named navigation landmark and retains visible labels for every primary destination.",
|
|
33
|
-
"Dashboard and Settings use icon-only tabs with localized accessible names; Help and Account use icon-only actions with matching tooltips.",
|
|
33
|
+
"Dashboard and Settings use icon-only tabs with localized accessible names; Help and the optional Account shortcut use icon-only actions with matching tooltips.",
|
|
34
34
|
"Escape requests dismissal and opening moves focus to the selected or first available area."
|
|
35
35
|
],
|
|
36
36
|
"states": [
|
|
37
37
|
"Dashboard and Settings change only the flat destination collection being browsed.",
|
|
38
|
-
"Area, Help, and Account events express intent while the application owns navigation and product behavior."
|
|
38
|
+
"Area, Help, and Account events express intent while the application owns navigation and product behavior.",
|
|
39
|
+
"Set routeSelectionActive to false while a global tool owns the mobile stage so the underlying router URL remains available without marking its area current.",
|
|
40
|
+
"Set showAccount to false when account and user preferences already live in the supplied Settings navigation."
|
|
39
41
|
],
|
|
40
42
|
"responsiveBehavior": [
|
|
41
43
|
"The pane fills the mobile content stage while the bottom bar remains available.",
|
|
42
44
|
"The sheet header and scrollable navigation body each use the shared 8px chrome inset; the context TabGroup and primary controls share a 40px height and 24px icon scale.",
|
|
45
|
+
"When the optional Account shortcut is hidden, the Dashboard and Settings TabGroup fills the centered two-thirds top-bar lane and gives both context options equal width.",
|
|
43
46
|
"Dashboard and Settings preserve their supplied visual groups with an 8px gap between destinations inside a section and a 32px gap between sections."
|
|
44
47
|
],
|
|
45
48
|
"references": [
|
|
@@ -114,6 +117,15 @@
|
|
|
114
117
|
"required": false,
|
|
115
118
|
"mutable": false
|
|
116
119
|
},
|
|
120
|
+
"routeSelectionActive": {
|
|
121
|
+
"type": "boolean",
|
|
122
|
+
"resolvedType": "boolean",
|
|
123
|
+
"attribute": "route-selection-active",
|
|
124
|
+
"default": "true",
|
|
125
|
+
"required": false,
|
|
126
|
+
"mutable": false,
|
|
127
|
+
"description": "Whether the router-derived current area owns the active mobile stage."
|
|
128
|
+
},
|
|
117
129
|
"settingsGroups": {
|
|
118
130
|
"type": "PanelNavGroup[]",
|
|
119
131
|
"resolvedType": "PanelNavGroup[]",
|
|
@@ -128,6 +140,15 @@
|
|
|
128
140
|
"default": "'Settings'",
|
|
129
141
|
"required": false,
|
|
130
142
|
"mutable": false
|
|
143
|
+
},
|
|
144
|
+
"showAccount": {
|
|
145
|
+
"type": "boolean",
|
|
146
|
+
"resolvedType": "boolean",
|
|
147
|
+
"attribute": "show-account",
|
|
148
|
+
"default": "true",
|
|
149
|
+
"required": false,
|
|
150
|
+
"mutable": false,
|
|
151
|
+
"description": "Show the optional Account shortcut in the sheet header."
|
|
131
152
|
}
|
|
132
153
|
},
|
|
133
154
|
"events": [
|
|
@@ -220,6 +241,15 @@
|
|
|
220
241
|
"required": false,
|
|
221
242
|
"mutable": false
|
|
222
243
|
},
|
|
244
|
+
"routeSelectionActive": {
|
|
245
|
+
"type": "boolean",
|
|
246
|
+
"resolvedType": "boolean",
|
|
247
|
+
"attribute": "route-selection-active",
|
|
248
|
+
"default": "true",
|
|
249
|
+
"required": false,
|
|
250
|
+
"mutable": false,
|
|
251
|
+
"description": "Whether the router-derived current area owns the active mobile stage."
|
|
252
|
+
},
|
|
223
253
|
"settingsGroups": {
|
|
224
254
|
"type": "PanelNavGroup[]",
|
|
225
255
|
"resolvedType": "PanelNavGroup[]",
|
|
@@ -234,6 +264,15 @@
|
|
|
234
264
|
"default": "'Settings'",
|
|
235
265
|
"required": false,
|
|
236
266
|
"mutable": false
|
|
267
|
+
},
|
|
268
|
+
"showAccount": {
|
|
269
|
+
"type": "boolean",
|
|
270
|
+
"resolvedType": "boolean",
|
|
271
|
+
"attribute": "show-account",
|
|
272
|
+
"default": "true",
|
|
273
|
+
"required": false,
|
|
274
|
+
"mutable": false,
|
|
275
|
+
"description": "Show the optional Account shortcut in the sheet header."
|
|
237
276
|
}
|
|
238
277
|
},
|
|
239
278
|
"events": [
|
|
@@ -307,12 +346,12 @@
|
|
|
307
346
|
"files": [
|
|
308
347
|
{
|
|
309
348
|
"path": "src/wc/components/MobileSheetNav/MobileSheetNav.css",
|
|
310
|
-
"content": "@import '../../utils/control-density.css';\n@import '../../utils/chrome-layout.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: block;\n min-width: 0;\n min-height: 0;\n height: 100%;\n background: var(--color-background-primary);\n}\n\n:host(:not([open])) {\n display: none;\n}\n\n.mobile-sheet-nav {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr);\n height: 100%;\n min-height: 0;\n background: var(--color-background-primary);\n}\n\n.mobile-sheet-nav__header {\n position: relative;\n grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);\n}\n\n.mobile-sheet-nav__header::after {\n content: '';\n position: absolute;\n inset-inline: 0;\n inset-block-end: 0;\n height: var(--dimension-stroke-width-012);\n background: var(--color-border-tertiary);\n pointer-events: none;\n}\n\n.mobile-sheet-nav__brand {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n min-width: 0;\n}\n\n.mobile-sheet-nav__logo {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--dimension-size-500);\n height: var(--dimension-size-500);\n padding: var(--dimension-space-100);\n box-sizing: border-box;\n color: var(--color-foreground-primary);\n}\n\n.mobile-sheet-nav__logo-mark {\n width: var(--dimension-iconography-lg);\n height: var(--dimension-iconography-lg);\n flex: 0 0 auto;\n}\n\n.mobile-sheet-nav__context {\n justify-self: center;\n}\n\n.mobile-sheet-nav__actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--dimension-space-050);\n min-width: 0;\n}\n\n.mobile-sheet-nav__body {\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n\n.mobile-sheet-nav__sections {\n display: grid;\n gap: var(--dimension-space-400);\n}\n\n.mobile-sheet-nav__items {\n display: grid;\n gap: var(--dimension-space-100);\n}\n\n.mobile-sheet-nav__item {\n appearance: none;\n width: 100%;\n height: var(--ds-control-height);\n min-height: var(--ds-control-height);\n padding-inline: var(--ds-control-padding-inline);\n border: 0;\n border-radius: var(--ds-control-radius);\n background: transparent;\n color: var(--color-foreground-secondary);\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: var(--ds-control-gap);\n cursor: default;\n text-align: start;\n box-sizing: border-box;\n}\n\n.mobile-sheet-nav__item--selected {\n color: var(--color-foreground-primary);\n}\n\n.mobile-sheet-nav__item-label {\n min-width: 0;\n padding-inline: var(--ds-control-label-inset);\n}\n\n.mobile-sheet-nav__dot {\n margin-inline-start: auto;\n}\n",
|
|
349
|
+
"content": "@import '../../utils/control-density.css';\n@import '../../utils/chrome-layout.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: block;\n min-width: 0;\n min-height: 0;\n height: 100%;\n background: var(--color-background-primary);\n}\n\n:host(:not([open])) {\n display: none;\n}\n\n.mobile-sheet-nav {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr);\n height: 100%;\n min-height: 0;\n background: var(--color-background-primary);\n}\n\n.mobile-sheet-nav__header {\n position: relative;\n grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);\n}\n\n.mobile-sheet-nav__header--expanded-context {\n grid-template-columns:\n minmax(var(--dimension-size-500), 1fr)\n minmax(0, 4fr)\n minmax(var(--dimension-size-500), 1fr);\n}\n\n.mobile-sheet-nav__header--expanded-context .mobile-sheet-nav__context {\n width: 100%;\n}\n\n.mobile-sheet-nav__header::after {\n content: '';\n position: absolute;\n inset-inline: 0;\n inset-block-end: 0;\n height: var(--dimension-stroke-width-012);\n background: var(--color-border-tertiary);\n pointer-events: none;\n}\n\n.mobile-sheet-nav__brand {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n min-width: 0;\n}\n\n.mobile-sheet-nav__logo {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--dimension-size-500);\n height: var(--dimension-size-500);\n padding: var(--dimension-space-100);\n box-sizing: border-box;\n color: var(--color-foreground-primary);\n}\n\n.mobile-sheet-nav__logo-mark {\n width: var(--dimension-iconography-lg);\n height: var(--dimension-iconography-lg);\n flex: 0 0 auto;\n}\n\n.mobile-sheet-nav__context {\n justify-self: center;\n}\n\n.mobile-sheet-nav__actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--dimension-space-050);\n min-width: 0;\n}\n\n.mobile-sheet-nav__body {\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n\n.mobile-sheet-nav__sections {\n display: grid;\n gap: var(--dimension-space-400);\n}\n\n.mobile-sheet-nav__items {\n display: grid;\n gap: var(--dimension-space-100);\n}\n\n.mobile-sheet-nav__item {\n appearance: none;\n width: 100%;\n height: var(--ds-control-height);\n min-height: var(--ds-control-height);\n padding-inline: var(--ds-control-padding-inline);\n border: 0;\n border-radius: var(--ds-control-radius);\n background: transparent;\n color: var(--color-foreground-secondary);\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: var(--ds-control-gap);\n cursor: default;\n text-align: start;\n box-sizing: border-box;\n}\n\n.mobile-sheet-nav__item--selected {\n color: var(--color-foreground-primary);\n}\n\n.mobile-sheet-nav__item-label {\n min-width: 0;\n padding-inline: var(--ds-control-label-inset);\n}\n\n.mobile-sheet-nav__dot {\n margin-inline-start: auto;\n}\n",
|
|
311
350
|
"type": "registry:ui"
|
|
312
351
|
},
|
|
313
352
|
{
|
|
314
353
|
"path": "src/wc/components/MobileSheetNav/MobileSheetNav.tsx",
|
|
315
|
-
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\n Prop,\n Watch,\n} from '@stencil/core';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport { deriveActiveIdFromUrl } from '../PanelNav/panel-nav-utils';\nimport type { PanelNavGroup, PanelNavItem } from '../PanelNav/panel-nav-types';\n\ntype MobileSheetNavHeaderDestinationId = 'account' | 'help';\n\nconst CONTEXT_TABS = [\n { id: 'dashboard', label: 'Dashboard', icon: 'Dashboard', variant: 'icon' as const },\n { id: 'settings', label: 'Settings', icon: 'Gear', variant: 'icon' as const },\n];\n\n@Component({\n tag: 'ds-mobile-sheet-nav',\n styleUrl: 'MobileSheetNav.css',\n scoped: true,\n})\nexport class MobileSheetNav {\n @Element() el!: HTMLElement;\n\n @Prop({ reflect: true }) open: boolean = false;\n @Prop() browseContext: NavChromeStyle = 'dashboard';\n @Prop() dashboardGroups: PanelNavGroup[] = [];\n @Prop() settingsGroups: PanelNavGroup[] = [];\n @Prop() currentUrl: string = '';\n @Prop() navigationLabel: string = 'Application navigation';\n @Prop() dashboardLabel: string = 'Dashboard';\n @Prop() settingsLabel: string = 'Settings';\n @Prop() accountLabel: string = 'Account';\n @Prop() helpLabel: string = 'Help & Support';\n\n @Event() dsAreaSelect!: EventEmitter<string>;\n @Event() dsBrowseContextChange!: EventEmitter<NavChromeStyle>;\n @Event() dsClose!: EventEmitter<void>;\n\n private get groups(): PanelNavGroup[] {\n return this.browseContext === 'settings' ? this.settingsGroups : this.dashboardGroups;\n }\n\n private get activeId(): string {\n return deriveActiveIdFromUrl(this.currentUrl, [\n ...this.dashboardGroups.flatMap(group => group.items),\n ...this.settingsGroups.flatMap(group => group.items),\n ]);\n }\n\n @Watch('open')\n handleOpenChange(open: boolean) {\n if (!open) return;\n requestAnimationFrame(() => {\n const selected = this.el.querySelector<HTMLElement>('[aria-current=\"page\"]');\n const first = this.el.querySelector<HTMLElement>('.mobile-sheet-nav__item');\n (selected ?? first)?.focus({ preventScroll: true });\n });\n }\n\n @Listen('keydown')\n handleKeyDown(event: KeyboardEvent) {\n if (!this.open || event.key !== 'Escape') return;\n event.preventDefault();\n this.dsClose.emit();\n }\n\n private handleContextChange = (event: CustomEvent<string>) => {\n const next = event.detail === 'settings' ? 'settings' : 'dashboard';\n if (next === this.browseContext) return;\n this.dsBrowseContextChange.emit(next);\n };\n\n private renderItem(item: PanelNavItem) {\n const selected = item.id === this.activeId;\n return (\n <button\n type=\"button\"\n class={{\n 'mobile-sheet-nav__item': true,\n 'mobile-sheet-nav__item--selected': selected,\n 'ds-control--lg': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n }}\n aria-current={selected ? 'page' : undefined}\n onClick={() => this.dsAreaSelect.emit(item.id)}\n >\n <ds-icon\n class=\"ds-interaction-fill__content\"\n name={item.icon}\n size=\"lg\"\n color=\"inherit\"\n />\n <ds-text\n class=\"mobile-sheet-nav__item-label ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-large\"\n emphasis={selected}\n color=\"inherit\"\n >\n {item.label}\n </ds-text>\n {item.dot && (\n <ds-badge\n class=\"mobile-sheet-nav__dot ds-interaction-fill__content\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n aria-hidden=\"true\"\n />\n )}\n </button>\n );\n }\n\n private renderLogo() {\n return (\n <span class=\"mobile-sheet-nav__logo\" aria-hidden=\"true\">\n {/* eslint-disable-next-line local/prefer-ds-icon -- Motive brand mark is not part of the IcoMo runtime catalog. */}\n <svg\n class=\"mobile-sheet-nav__logo-mark\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M11.1159 4.31537H7.67021L2.53401 13.0703H0V15.6875H4.02716L8.24319 8.49978V15.6846H11.6342L15.8289 8.47829V15.6846H18.7122V4.3125H15.2559L11.1159 11.3648V4.31537Z\"\n fill=\"currentColor\"\n />\n </svg>\n </span>\n );\n }\n\n private renderHeaderDestination(\n id: MobileSheetNavHeaderDestinationId,\n icon: string,\n label: string\n ) {\n return (\n <ds-tooltip label={label} side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n size=\"lg\"\n icon={icon}\n aria-label={label}\n activeFill={false}\n hasBorder={false}\n onDsClick={() => this.dsAreaSelect.emit(id)}\n />\n </ds-tooltip>\n );\n }\n\n render() {\n const contextTabs = [\n { ...CONTEXT_TABS[0], label: this.dashboardLabel },\n { ...CONTEXT_TABS[1], label: this.settingsLabel },\n ];\n\n return (\n <Host aria-hidden={this.open ? undefined : 'true'} inert={this.open ? undefined : true}>\n <section\n id=\"ds-mobile-sheet-nav\"\n class=\"mobile-sheet-nav\"\n >\n <header
|
|
354
|
+
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\n Prop,\n Watch,\n} from '@stencil/core';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport { deriveActiveIdFromUrl } from '../PanelNav/panel-nav-utils';\nimport type { PanelNavGroup, PanelNavItem } from '../PanelNav/panel-nav-types';\n\ntype MobileSheetNavHeaderDestinationId = 'account' | 'help';\n\nconst CONTEXT_TABS = [\n { id: 'dashboard', label: 'Dashboard', icon: 'Dashboard', variant: 'icon' as const },\n { id: 'settings', label: 'Settings', icon: 'Gear', variant: 'icon' as const },\n];\n\n@Component({\n tag: 'ds-mobile-sheet-nav',\n styleUrl: 'MobileSheetNav.css',\n scoped: true,\n})\nexport class MobileSheetNav {\n @Element() el!: HTMLElement;\n\n @Prop({ reflect: true }) open: boolean = false;\n @Prop() browseContext: NavChromeStyle = 'dashboard';\n @Prop() dashboardGroups: PanelNavGroup[] = [];\n @Prop() settingsGroups: PanelNavGroup[] = [];\n @Prop() currentUrl: string = '';\n /** Whether the router-derived current area owns the active mobile stage. */\n @Prop() routeSelectionActive: boolean = true;\n @Prop() navigationLabel: string = 'Application navigation';\n @Prop() dashboardLabel: string = 'Dashboard';\n @Prop() settingsLabel: string = 'Settings';\n @Prop() accountLabel: string = 'Account';\n @Prop() helpLabel: string = 'Help & Support';\n /** Show the optional Account shortcut in the sheet header. */\n @Prop() showAccount: boolean = true;\n\n @Event() dsAreaSelect!: EventEmitter<string>;\n @Event() dsBrowseContextChange!: EventEmitter<NavChromeStyle>;\n @Event() dsClose!: EventEmitter<void>;\n\n private get groups(): PanelNavGroup[] {\n return this.browseContext === 'settings' ? this.settingsGroups : this.dashboardGroups;\n }\n\n private get activeId(): string {\n if (!this.routeSelectionActive) return '';\n return deriveActiveIdFromUrl(this.currentUrl, [\n ...this.dashboardGroups.flatMap(group => group.items),\n ...this.settingsGroups.flatMap(group => group.items),\n ]);\n }\n\n @Watch('open')\n handleOpenChange(open: boolean) {\n if (!open) return;\n requestAnimationFrame(() => {\n const selected = this.el.querySelector<HTMLElement>('[aria-current=\"page\"]');\n const first = this.el.querySelector<HTMLElement>('.mobile-sheet-nav__item');\n (selected ?? first)?.focus({ preventScroll: true });\n });\n }\n\n @Listen('keydown')\n handleKeyDown(event: KeyboardEvent) {\n if (!this.open || event.key !== 'Escape') return;\n event.preventDefault();\n this.dsClose.emit();\n }\n\n private handleContextChange = (event: CustomEvent<string>) => {\n const next = event.detail === 'settings' ? 'settings' : 'dashboard';\n if (next === this.browseContext) return;\n this.dsBrowseContextChange.emit(next);\n };\n\n private renderItem(item: PanelNavItem) {\n const selected = item.id === this.activeId;\n return (\n <button\n type=\"button\"\n class={{\n 'mobile-sheet-nav__item': true,\n 'mobile-sheet-nav__item--selected': selected,\n 'ds-control--lg': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n }}\n aria-current={selected ? 'page' : undefined}\n onClick={() => this.dsAreaSelect.emit(item.id)}\n >\n <ds-icon\n class=\"ds-interaction-fill__content\"\n name={item.icon}\n size=\"lg\"\n color=\"inherit\"\n />\n <ds-text\n class=\"mobile-sheet-nav__item-label ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-large\"\n emphasis={selected}\n color=\"inherit\"\n >\n {item.label}\n </ds-text>\n {item.dot && (\n <ds-badge\n class=\"mobile-sheet-nav__dot ds-interaction-fill__content\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n aria-hidden=\"true\"\n />\n )}\n </button>\n );\n }\n\n private renderLogo() {\n return (\n <span class=\"mobile-sheet-nav__logo\" aria-hidden=\"true\">\n {/* eslint-disable-next-line local/prefer-ds-icon -- Motive brand mark is not part of the IcoMo runtime catalog. */}\n <svg\n class=\"mobile-sheet-nav__logo-mark\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M11.1159 4.31537H7.67021L2.53401 13.0703H0V15.6875H4.02716L8.24319 8.49978V15.6846H11.6342L15.8289 8.47829V15.6846H18.7122V4.3125H15.2559L11.1159 11.3648V4.31537Z\"\n fill=\"currentColor\"\n />\n </svg>\n </span>\n );\n }\n\n private renderHeaderDestination(\n id: MobileSheetNavHeaderDestinationId,\n icon: string,\n label: string\n ) {\n return (\n <ds-tooltip label={label} side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n size=\"lg\"\n icon={icon}\n aria-label={label}\n activeFill={false}\n hasBorder={false}\n onDsClick={() => this.dsAreaSelect.emit(id)}\n />\n </ds-tooltip>\n );\n }\n\n render() {\n const contextTabs = [\n { ...CONTEXT_TABS[0], label: this.dashboardLabel },\n { ...CONTEXT_TABS[1], label: this.settingsLabel },\n ];\n\n return (\n <Host aria-hidden={this.open ? undefined : 'true'} inert={this.open ? undefined : true}>\n <section\n id=\"ds-mobile-sheet-nav\"\n class=\"mobile-sheet-nav\"\n >\n <header\n class={{\n 'mobile-sheet-nav__header': true,\n 'mobile-sheet-nav__header--expanded-context': !this.showAccount,\n 'ds-chrome-grid': true,\n 'ds-chrome-space--md': true,\n }}\n >\n <div class=\"mobile-sheet-nav__brand\">{this.renderLogo()}</div>\n <ds-tab-group\n class=\"mobile-sheet-nav__context\"\n tabs={contextTabs}\n value={this.browseContext}\n size=\"lg\"\n width={this.showAccount ? 'hug' : 'fill'}\n aria-label=\"Browse context\"\n onDsChange={this.handleContextChange}\n />\n <div class=\"mobile-sheet-nav__actions\">\n {this.renderHeaderDestination('help', 'CircleQuestion', this.helpLabel)}\n {this.showAccount\n ? this.renderHeaderDestination('account', 'Avatar', this.accountLabel)\n : null}\n </div>\n </header>\n\n <nav\n class=\"mobile-sheet-nav__body ds-chrome-column ds-chrome-space--md\"\n aria-label={this.navigationLabel}\n >\n <div class=\"mobile-sheet-nav__sections\">\n {this.groups\n .filter(group => group.items.length > 0)\n .map(group => (\n <div class=\"mobile-sheet-nav__items\">\n {group.items.map(item => this.renderItem(item))}\n </div>\n ))}\n </div>\n </nav>\n </section>\n </Host>\n );\n }\n}\n",
|
|
316
355
|
"type": "registry:ui"
|
|
317
356
|
}
|
|
318
357
|
]
|