@ds-mo/ui 1.10.1 → 1.11.1

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.
@@ -1 +1 @@
1
- {"version":3,"names":["PANEL_TOOLS_PRIMARY_TOOL_ID","PANEL_TOOLS_LABELS","search","messages","stacks","activity","agents","parsePanelToolsItems","items","itemsJson","parseJsonArrayProp","panelToolsCss","PanelTools","__stencil_proxyCustomElement","HTMLElement","constructor","registerHost","this","open","activeTool","motion","readyForMotion","motionEnableGeneration","handleTransitionEnd","event","target","el","querySelector","propertyName","dsChromeTransitionEnd","emit","source","handleToolChange","id","selected","isRailSelected","dsToolChange","railItems","disconnectedCallback","removeEventListener","deferMotionEnable","generation","requestAnimationFrame","connectedCallback","addEventListener","openChanged","isOpen","wasOpen","undefined","dsChromeTransitionStart","activeToolChanged","componentDidLoad","itemsChanged","isDrawerPresent","isViewActive","headerLabel","renderRailAction","item","h","key","class","icon","dot","inactive","ariaLabel","onDsChange","render","headerItem","find","bodyItems","filter","showDrawerChrome","Host","role","hidden","name","map"],"sources":["src/wc/components/PanelTools/panel-tools-types.ts","src/wc/components/PanelTools/panel-tools-utils.ts","src/wc/components/PanelTools/PanelTools.css?tag=ds-panel-tools&encapsulation=scoped","src/wc/components/PanelTools/PanelTools.tsx"],"sourcesContent":["/** Tool ids for the `ds-panel-tools` rail and drawer views. */\nexport type PanelToolsToolId = 'search' | 'agents' | 'messages' | 'stacks' | 'activity';\n\nexport const PANEL_TOOLS_TOOL_IDS: PanelToolsToolId[] = [\n 'search',\n 'agents',\n 'messages',\n 'stacks',\n 'activity',\n];\n\n/** Rail header slot — mirrors panel-nav M logo row. */\nexport const PANEL_TOOLS_PRIMARY_TOOL_ID: PanelToolsToolId = 'search';\n\nexport const PANEL_TOOLS_LABELS: Record<PanelToolsToolId, string> = {\n search: 'Search',\n messages: 'Messages',\n stacks: 'Stacks',\n activity: 'Activity',\n agents: 'Agents',\n};\n\nexport interface PanelToolsItem {\n id: PanelToolsToolId;\n /** Icon name for <ds-icon>. */\n icon: string;\n /** Whether this rail button is currently pressed/active. */\n selected?: boolean;\n /** Show a notification dot. */\n dot?: boolean;\n inactive?: boolean;\n ariaLabel?: string;\n}\n","import { parseJsonArrayProp } from '../BarNav/bar-nav-utils';\nimport type { PanelToolsItem } from './panel-tools-types';\n\nexport function parsePanelToolsItems(\n items: PanelToolsItem[],\n itemsJson: string,\n): PanelToolsItem[] {\n if (itemsJson) {\n return parseJsonArrayProp<PanelToolsItem>(itemsJson, []);\n }\n return items ?? [];\n}\n\nexport function shouldResyncPanelToolsItems(\n prev: PanelToolsItem[],\n next: PanelToolsItem[],\n): boolean {\n if (prev.length !== next.length) return true;\n return next.some((item, index) => {\n const prior = prev[index];\n if (!prior) return true;\n return (\n prior.id !== item.id ||\n prior.icon !== item.icon ||\n prior.selected !== item.selected ||\n prior.dot !== item.dot ||\n prior.inactive !== item.inactive ||\n prior.ariaLabel !== item.ariaLabel\n );\n });\n}\n",":host {\n display: block;\n flex: 0 0 auto;\n box-sizing: border-box;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n --_panel-tools-rail-width: var(--dimension-size-600);\n --_panel-tools-drawer-width: var(--dimension-panel-width-sm);\n --_panel-tools-border: var(--dimension-stroke-width-012, 1px) solid var(--color-border-tertiary);\n /* Match panel-nav expand timing (medium-1); easing differs by direction. */\n --_panel-tools-expand-dur: var(--effect-animation-duration-medium-1, 300ms);\n}\n\n.panel-tools__layout {\n display: flex;\n flex-direction: row;\n align-items: stretch;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n}\n\n/* Clip frame — only max-width animates (slide reveal, same as pre-rail :host). */\n.panel-tools__drawer {\n flex: 0 0 auto;\n max-width: 0;\n min-width: 0;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n box-sizing: border-box;\n transition: none;\n}\n\n:host(.panel-tools--ready.panel-tools--motion-opening) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-out, ease-out);\n}\n\n:host(.panel-tools--ready.panel-tools--motion-closing) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-in, ease-in);\n}\n\n:host(.panel-tools--open) .panel-tools__drawer {\n max-width: var(--_panel-tools-drawer-width);\n}\n\n.panel-tools__drawer--visible {\n border-left: var(--_panel-tools-border);\n}\n\n/* Fixed-width surface — stays 400px while the clip frame opens/closes. */\n.panel-tools__drawer-surface {\n display: flex;\n flex-direction: column;\n width: var(--_panel-tools-drawer-width);\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n background-color: var(--color-background-primary);\n}\n\n/* Shell-owned chrome (bg + wash + grid) — drawer is a transparent frame. */\n:host-context(ds-app-shell[gradient]) .panel-tools__drawer-surface {\n background-color: transparent;\n}\n\n/* Aligns with ds-bar-nav row — same height, inset, border, and title token. */\n.panel-tools__header {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n height: var(--dimension-size-600);\n padding: var(--dimension-space-100);\n border-bottom: var(--_panel-tools-border);\n color: var(--color-foreground-primary);\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.panel-tools__title {\n display: inline-flex;\n align-items: center;\n height: var(--dimension-size-400);\n padding-inline: var(--dimension-space-100);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.panel-tools__body {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n position: relative;\n}\n\n.panel-tools__view {\n display: none;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n padding: var(--dimension-space-200);\n color: var(--color-foreground-secondary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n}\n\n.panel-tools__view ::slotted(*) {\n margin: 0;\n color: var(--color-foreground-secondary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n}\n\n.panel-tools__view--active {\n display: block;\n}\n\n/* Rail — mirrors collapsed panel-nav width, padding, and item rhythm. */\n.panel-tools__rail {\n flex: 0 0 var(--_panel-tools-rail-width);\n width: var(--_panel-tools-rail-width);\n min-width: var(--_panel-tools-rail-width);\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n height: 100%;\n min-height: 0;\n border-left: var(--_panel-tools-border);\n}\n\n.panel-tools__rail-header {\n display: flex;\n align-items: center;\n justify-content: center;\n height: var(--dimension-size-600);\n padding: var(--dimension-space-100);\n flex-shrink: 0;\n box-sizing: border-box;\n border-bottom: var(--_panel-tools-border);\n}\n\n.panel-tools__rail-body {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n padding: var(--dimension-space-100);\n box-sizing: border-box;\n scrollbar-width: none;\n}\n\n.panel-tools__rail-body::-webkit-scrollbar {\n display: none;\n}\n\n.panel-tools__rail-action {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: var(--dimension-size-400);\n flex-shrink: 0;\n}\n","import { Component, Prop, Event, EventEmitter, Element, State, Watch, h, Host } from '@stencil/core';\nimport type { ChromeTransitionDetail } from '../../nav/chrome-transition';\nimport {\n PANEL_TOOLS_LABELS,\n PANEL_TOOLS_PRIMARY_TOOL_ID,\n type PanelToolsItem,\n type PanelToolsToolId,\n} from './panel-tools-types';\nimport { parsePanelToolsItems } from './panel-tools-utils';\n\n@Component({\n tag: 'ds-panel-tools',\n styleUrl: 'PanelTools.css',\n scoped: true,\n})\nexport class PanelTools {\n @Element() el!: HTMLElement;\n\n /** When false, only the icon rail is shown. */\n @Prop({ mutable: true, reflect: true }) open: boolean = false;\n\n /** Active tool view — `search`, `agents`, `messages`, `stacks`, or `activity`. */\n @Prop({ mutable: true, attribute: 'active-tool', reflect: true })\n activeTool: PanelToolsToolId | '' = '';\n\n /**\n * Rail items rendered in the right column.\n * Set via JS property: `el.items = [...]`. Replace the array reference to update.\n */\n @Prop() items: PanelToolsItem[] = [];\n\n /** JSON fallback for `items` — useful when framework bindings don't propagate arrays. */\n @Prop({ attribute: 'items-json' }) itemsJson: string = '';\n\n /** Emitted when a rail button is toggled. Detail = { id, selected }. */\n @Event({ bubbles: true, composed: true }) dsToolChange!: EventEmitter<{\n id: PanelToolsToolId;\n selected: boolean;\n }>;\n\n /** Bubbling lifecycle — `ds-bar-nav` defers overflow checks during drawer motion. */\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionStart!: EventEmitter<ChromeTransitionDetail>;\n\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionEnd!: EventEmitter<ChromeTransitionDetail>;\n\n /** Arms open vs close easing for the in-flight width transition. */\n @State() private motion: 'opening' | 'closing' | 'idle' = 'idle';\n\n /** Suppresses width transition until the host has painted its initial open state. */\n @State() private readyForMotion = false;\n\n private motionEnableGeneration = 0;\n\n private get railItems(): PanelToolsItem[] {\n return parsePanelToolsItems(this.items, this.itemsJson);\n }\n\n disconnectedCallback() {\n this.el.removeEventListener('transitionend', this.handleTransitionEnd);\n this.motionEnableGeneration += 1;\n }\n\n private deferMotionEnable() {\n if (this.readyForMotion) return;\n const generation = ++this.motionEnableGeneration;\n requestAnimationFrame(() => {\n if (generation !== this.motionEnableGeneration) return;\n requestAnimationFrame(() => {\n if (generation !== this.motionEnableGeneration) return;\n this.readyForMotion = true;\n });\n });\n }\n\n connectedCallback() {\n this.el.addEventListener('transitionend', this.handleTransitionEnd);\n }\n\n @Watch('open')\n openChanged(isOpen: boolean, wasOpen?: boolean) {\n if (this.readyForMotion && wasOpen !== undefined && wasOpen !== isOpen) {\n this.motion = isOpen ? 'opening' : 'closing';\n this.dsChromeTransitionStart.emit({ source: 'panel-tools' });\n }\n this.deferMotionEnable();\n }\n\n @Watch('activeTool')\n activeToolChanged() {\n this.deferMotionEnable();\n }\n\n componentDidLoad() {\n this.deferMotionEnable();\n }\n\n @Watch('items')\n @Watch('itemsJson')\n itemsChanged() {\n this.deferMotionEnable();\n }\n\n private handleTransitionEnd = (event: TransitionEvent) => {\n if (event.target !== this.el.querySelector('.panel-tools__drawer')) return;\n if (event.propertyName !== 'max-width') return;\n this.motion = 'idle';\n this.dsChromeTransitionEnd.emit({ source: 'panel-tools' });\n };\n\n /** Rail selection follows `open` immediately — independent of the slide animation. */\n private isRailSelected(id: PanelToolsToolId): boolean {\n return this.open && this.activeTool === id;\n }\n\n /** Drawer body stays mounted while the clip frame animates closed. */\n private isDrawerPresent(): boolean {\n return this.open || this.motion === 'closing';\n }\n\n private isViewActive(id: PanelToolsToolId): boolean {\n return this.isDrawerPresent() && this.activeTool === id;\n }\n\n private headerLabel(): string {\n if (!this.isDrawerPresent() || !this.activeTool) return '';\n return PANEL_TOOLS_LABELS[this.activeTool as PanelToolsToolId] ?? '';\n }\n\n private handleToolChange = (id: PanelToolsToolId) => {\n const selected = !this.isRailSelected(id);\n if (selected) {\n this.open = true;\n this.activeTool = id;\n } else {\n this.open = false;\n }\n this.dsToolChange.emit({ id, selected });\n };\n\n private renderRailAction(item: PanelToolsItem) {\n return (\n <ds-bar-nav-action\n key={item.id}\n class=\"panel-tools__rail-action\"\n icon={item.icon}\n selected={this.isRailSelected(item.id)}\n dot={item.dot ?? false}\n inactive={item.inactive}\n aria-label={item.ariaLabel ?? PANEL_TOOLS_LABELS[item.id]}\n onDsChange={() => this.handleToolChange(item.id)}\n />\n );\n }\n\n render() {\n const headerLabel = this.headerLabel();\n const railItems = this.railItems;\n const headerItem = railItems.find(item => item.id === PANEL_TOOLS_PRIMARY_TOOL_ID);\n const bodyItems = railItems.filter(item => item.id !== PANEL_TOOLS_PRIMARY_TOOL_ID);\n const showDrawerChrome = this.isDrawerPresent();\n\n return (\n <Host\n class={{\n 'panel-tools': true,\n 'panel-tools--open': this.open,\n 'panel-tools--ready': this.readyForMotion,\n 'panel-tools--motion-opening': this.motion === 'opening',\n 'panel-tools--motion-closing': this.motion === 'closing',\n }}\n role=\"complementary\"\n aria-label=\"Tools\"\n >\n <div class=\"panel-tools__layout\">\n <div\n class={{\n 'panel-tools__drawer': true,\n 'panel-tools__drawer--visible': showDrawerChrome,\n }}\n aria-hidden={showDrawerChrome ? null : 'true'}\n >\n <div class=\"panel-tools__drawer-surface\">\n <header class=\"panel-tools__header\">\n <span class=\"panel-tools__title text-body-medium-emphasis\">{headerLabel}</span>\n </header>\n <div class=\"panel-tools__body\">\n <div\n class={{\n 'panel-tools__view': true,\n 'panel-tools__view--active': this.isViewActive('search'),\n 'text-body-medium': true,\n }}\n hidden={!this.isViewActive('search')}\n >\n <slot name=\"search\" />\n </div>\n <div\n class={{\n 'panel-tools__view': true,\n 'panel-tools__view--active': this.isViewActive('messages'),\n 'text-body-medium': true,\n }}\n hidden={!this.isViewActive('messages')}\n >\n <slot name=\"messages\" />\n </div>\n <div\n class={{\n 'panel-tools__view': true,\n 'panel-tools__view--active': this.isViewActive('stacks'),\n 'text-body-medium': true,\n }}\n hidden={!this.isViewActive('stacks')}\n >\n <slot name=\"stacks\" />\n </div>\n <div\n class={{\n 'panel-tools__view': true,\n 'panel-tools__view--active': this.isViewActive('activity'),\n 'text-body-medium': true,\n }}\n hidden={!this.isViewActive('activity')}\n >\n <slot name=\"activity\" />\n </div>\n <div\n class={{\n 'panel-tools__view': true,\n 'panel-tools__view--active': this.isViewActive('agents'),\n 'text-body-medium': true,\n }}\n hidden={!this.isViewActive('agents')}\n >\n <slot name=\"agents\" />\n </div>\n </div>\n </div>\n </div>\n\n <nav class=\"panel-tools__rail\" aria-label=\"Tool shortcuts\">\n {headerItem ? (\n <div class=\"panel-tools__rail-header\">{this.renderRailAction(headerItem)}</div>\n ) : null}\n <div class=\"panel-tools__rail-body\">\n {bodyItems.map(item => this.renderRailAction(item))}\n </div>\n </nav>\n </div>\n </Host>\n );\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["PANEL_TOOLS_PRIMARY_TOOL_ID","PANEL_TOOLS_LABELS","search","messages","stacks","activity","agents","parsePanelToolsItems","items","itemsJson","parseJsonArrayProp","panelToolsDrawerResting","open","motion","panelToolsCss","PanelTools","__stencil_proxyCustomElement","HTMLElement","constructor","registerHost","this","activeTool","readyForMotion","motionEnableGeneration","handleTransitionEnd","event","target","el","querySelector","propertyName","dsChromeTransitionEnd","emit","source","handleToolChange","id","selected","isRailSelected","dsToolChange","railItems","disconnectedCallback","removeEventListener","deferMotionEnable","generation","requestAnimationFrame","connectedCallback","addEventListener","openChanged","isOpen","wasOpen","undefined","dsChromeTransitionStart","activeToolChanged","componentDidLoad","itemsChanged","isDrawerPresent","isViewActive","headerLabel","renderRailAction","item","h","key","class","icon","dot","inactive","ariaLabel","onDsChange","render","headerItem","find","bodyItems","filter","showDrawerChrome","drawerResting","Host","role","hidden","name","map"],"sources":["src/wc/components/PanelTools/panel-tools-types.ts","src/wc/components/PanelTools/panel-tools-utils.ts","src/wc/components/PanelTools/PanelTools.css?tag=ds-panel-tools&encapsulation=scoped","src/wc/components/PanelTools/PanelTools.tsx"],"sourcesContent":["/** Tool ids for the `ds-panel-tools` rail and drawer views. */\nexport type PanelToolsToolId = 'search' | 'agents' | 'messages' | 'stacks' | 'activity';\n\nexport const PANEL_TOOLS_TOOL_IDS: PanelToolsToolId[] = [\n 'search',\n 'agents',\n 'messages',\n 'stacks',\n 'activity',\n];\n\n/** Rail header slot — mirrors panel-nav M logo row. */\nexport const PANEL_TOOLS_PRIMARY_TOOL_ID: PanelToolsToolId = 'search';\n\nexport const PANEL_TOOLS_LABELS: Record<PanelToolsToolId, string> = {\n search: 'Search',\n messages: 'Messages',\n stacks: 'Stacks',\n activity: 'Activity',\n agents: 'Agents',\n};\n\nexport interface PanelToolsItem {\n id: PanelToolsToolId;\n /** Icon name for <ds-icon>. */\n icon: string;\n /** Whether this rail button is currently pressed/active. */\n selected?: boolean;\n /** Show a notification dot. */\n dot?: boolean;\n inactive?: boolean;\n ariaLabel?: string;\n}\n","import { parseJsonArrayProp } from '../BarNav/bar-nav-utils';\nimport type { PanelToolsItem } from './panel-tools-types';\n\nexport function parsePanelToolsItems(\n items: PanelToolsItem[],\n itemsJson: string,\n): PanelToolsItem[] {\n if (itemsJson) {\n return parseJsonArrayProp<PanelToolsItem>(itemsJson, []);\n }\n return items ?? [];\n}\n\nexport function shouldResyncPanelToolsItems(\n prev: PanelToolsItem[],\n next: PanelToolsItem[],\n): boolean {\n if (prev.length !== next.length) return true;\n return next.some((item, index) => {\n const prior = prev[index];\n if (!prior) return true;\n return (\n prior.id !== item.id ||\n prior.icon !== item.icon ||\n prior.selected !== item.selected ||\n prior.dot !== item.dot ||\n prior.inactive !== item.inactive ||\n prior.ariaLabel !== item.ariaLabel\n );\n });\n}\n\nexport type PanelToolsMotion = 'opening' | 'closing' | 'idle';\n\n/** True when the drawer clip is fully closed — safe to skip painting slot content. */\nexport function panelToolsDrawerResting(open: boolean, motion: PanelToolsMotion): boolean {\n return !open && motion === 'idle';\n}\n",":host {\n display: block;\n flex: 0 0 auto;\n box-sizing: border-box;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n --_panel-tools-rail-width: var(--dimension-size-600);\n --_panel-tools-drawer-width: var(--dimension-panel-width-sm);\n --_panel-tools-border: var(--dimension-stroke-width-012, 1px) solid var(--color-border-tertiary);\n --_panel-tools-surface-bg: var(--color-background-secondary);\n /* Match panel-nav expand timing (medium-1); easing differs by direction. */\n --_panel-tools-expand-dur: var(--effect-animation-duration-medium-1, 300ms);\n}\n\n.panel-tools__layout {\n display: flex;\n flex-direction: row;\n align-items: stretch;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n}\n\n/* Clip frame — only max-width animates (slide reveal, same as pre-rail :host). */\n.panel-tools__drawer {\n flex: 0 0 auto;\n max-width: 0;\n min-width: 0;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n box-sizing: border-box;\n transition: none;\n}\n\n:host(.panel-tools--ready.panel-tools--motion-opening) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-out, ease-out);\n}\n\n:host(.panel-tools--ready.panel-tools--motion-closing) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-in, ease-in);\n}\n\n:host(.panel-tools--open) .panel-tools__drawer {\n max-width: var(--_panel-tools-drawer-width);\n}\n\n.panel-tools__drawer--visible {\n border-left: var(--_panel-tools-border);\n}\n\n/* Fixed-width surface — stays 400px while the clip frame opens/closes. */\n.panel-tools__drawer-surface {\n display: flex;\n flex-direction: column;\n width: var(--_panel-tools-drawer-width);\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n background-color: var(--_panel-tools-surface-bg);\n}\n\n/* Shell-owned chrome (bg + wash +/or grid) — drawer + rail are transparent frames. */\n:host-context(ds-app-shell[gradient]) .panel-tools__drawer-surface,\n:host-context(ds-app-shell[grid]) .panel-tools__drawer-surface,\n:host-context(ds-app-shell[gradient]) .panel-tools__rail,\n:host-context(ds-app-shell[grid]) .panel-tools__rail {\n background-color: transparent;\n}\n\n/* Aligns with ds-bar-nav row — same height, inset, border, and title token. */\n.panel-tools__header {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n height: var(--dimension-size-600);\n padding: var(--dimension-space-100);\n border-bottom: var(--_panel-tools-border);\n color: var(--color-foreground-primary);\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.panel-tools__title {\n display: inline-flex;\n align-items: center;\n height: var(--dimension-size-400);\n padding-inline: var(--dimension-space-100);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.panel-tools__body {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n position: relative;\n}\n\n/* Skip paint while fully closed; cleared during open/close motion for WYSIWYG slide. */\n:host(.panel-tools--drawer-resting) .panel-tools__body {\n content-visibility: auto;\n contain-intrinsic-size: auto 200px;\n}\n\n.panel-tools__view {\n display: none;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n padding: var(--dimension-space-200);\n color: var(--color-foreground-secondary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n}\n\n.panel-tools__view ::slotted(*) {\n margin: 0;\n color: var(--color-foreground-secondary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n}\n\n.panel-tools__view--active {\n display: block;\n}\n\n/* Rail — mirrors collapsed panel-nav width, padding, and item rhythm. */\n.panel-tools__rail {\n flex: 0 0 var(--_panel-tools-rail-width);\n width: var(--_panel-tools-rail-width);\n min-width: var(--_panel-tools-rail-width);\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n height: 100%;\n min-height: 0;\n border-left: var(--_panel-tools-border);\n background-color: var(--_panel-tools-surface-bg);\n}\n\n.panel-tools__rail-header {\n display: flex;\n align-items: center;\n justify-content: center;\n height: var(--dimension-size-600);\n padding: var(--dimension-space-100);\n flex-shrink: 0;\n box-sizing: border-box;\n border-bottom: var(--_panel-tools-border);\n}\n\n.panel-tools__rail-body {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n padding: var(--dimension-space-100);\n box-sizing: border-box;\n scrollbar-width: none;\n}\n\n.panel-tools__rail-body::-webkit-scrollbar {\n display: none;\n}\n\n.panel-tools__rail-action {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: var(--dimension-size-400);\n flex-shrink: 0;\n}\n","import { Component, Prop, Event, EventEmitter, Element, State, Watch, h, Host } from '@stencil/core';\nimport type { ChromeTransitionDetail } from '../../nav/chrome-transition';\nimport {\n PANEL_TOOLS_LABELS,\n PANEL_TOOLS_PRIMARY_TOOL_ID,\n type PanelToolsItem,\n type PanelToolsToolId,\n} from './panel-tools-types';\nimport { parsePanelToolsItems, panelToolsDrawerResting } from './panel-tools-utils';\n\n@Component({\n tag: 'ds-panel-tools',\n styleUrl: 'PanelTools.css',\n scoped: true,\n})\nexport class PanelTools {\n @Element() el!: HTMLElement;\n\n /** When false, only the icon rail is shown. */\n @Prop({ mutable: true, reflect: true }) open: boolean = false;\n\n /** Active tool view — `search`, `agents`, `messages`, `stacks`, or `activity`. */\n @Prop({ mutable: true, attribute: 'active-tool', reflect: true })\n activeTool: PanelToolsToolId | '' = '';\n\n /**\n * Rail items rendered in the right column.\n * Set via JS property: `el.items = [...]`. Replace the array reference to update.\n */\n @Prop() items: PanelToolsItem[] = [];\n\n /** JSON fallback for `items` — useful when framework bindings don't propagate arrays. */\n @Prop({ attribute: 'items-json' }) itemsJson: string = '';\n\n /** Emitted when a rail button is toggled. Detail = { id, selected }. */\n @Event({ bubbles: true, composed: true }) dsToolChange!: EventEmitter<{\n id: PanelToolsToolId;\n selected: boolean;\n }>;\n\n /** Bubbling lifecycle — `ds-bar-nav` defers overflow checks during drawer motion. */\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionStart!: EventEmitter<ChromeTransitionDetail>;\n\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionEnd!: EventEmitter<ChromeTransitionDetail>;\n\n /** Arms open vs close easing for the in-flight width transition. */\n @State() private motion: 'opening' | 'closing' | 'idle' = 'idle';\n\n /** Suppresses width transition until the host has painted its initial open state. */\n @State() private readyForMotion = false;\n\n private motionEnableGeneration = 0;\n\n private get railItems(): PanelToolsItem[] {\n return parsePanelToolsItems(this.items, this.itemsJson);\n }\n\n disconnectedCallback() {\n this.el.removeEventListener('transitionend', this.handleTransitionEnd);\n this.motionEnableGeneration += 1;\n }\n\n private deferMotionEnable() {\n if (this.readyForMotion) return;\n const generation = ++this.motionEnableGeneration;\n requestAnimationFrame(() => {\n if (generation !== this.motionEnableGeneration) return;\n requestAnimationFrame(() => {\n if (generation !== this.motionEnableGeneration) return;\n this.readyForMotion = true;\n });\n });\n }\n\n connectedCallback() {\n this.el.addEventListener('transitionend', this.handleTransitionEnd);\n }\n\n @Watch('open')\n openChanged(isOpen: boolean, wasOpen?: boolean) {\n if (this.readyForMotion && wasOpen !== undefined && wasOpen !== isOpen) {\n this.motion = isOpen ? 'opening' : 'closing';\n this.dsChromeTransitionStart.emit({ source: 'panel-tools' });\n }\n this.deferMotionEnable();\n }\n\n @Watch('activeTool')\n activeToolChanged() {\n this.deferMotionEnable();\n }\n\n componentDidLoad() {\n this.deferMotionEnable();\n }\n\n @Watch('items')\n @Watch('itemsJson')\n itemsChanged() {\n this.deferMotionEnable();\n }\n\n private handleTransitionEnd = (event: TransitionEvent) => {\n if (event.target !== this.el.querySelector('.panel-tools__drawer')) return;\n if (event.propertyName !== 'max-width') return;\n this.motion = 'idle';\n this.dsChromeTransitionEnd.emit({ source: 'panel-tools' });\n };\n\n /** Rail selection follows `open` immediately — independent of the slide animation. */\n private isRailSelected(id: PanelToolsToolId): boolean {\n return this.open && this.activeTool === id;\n }\n\n /** Drawer body stays mounted while the clip frame animates closed. */\n private isDrawerPresent(): boolean {\n return this.open || this.motion === 'closing';\n }\n\n private isViewActive(id: PanelToolsToolId): boolean {\n return this.isDrawerPresent() && this.activeTool === id;\n }\n\n private headerLabel(): string {\n if (!this.isDrawerPresent() || !this.activeTool) return '';\n return PANEL_TOOLS_LABELS[this.activeTool as PanelToolsToolId] ?? '';\n }\n\n private handleToolChange = (id: PanelToolsToolId) => {\n const selected = !this.isRailSelected(id);\n if (selected) {\n this.open = true;\n this.activeTool = id;\n } else {\n this.open = false;\n }\n this.dsToolChange.emit({ id, selected });\n };\n\n private renderRailAction(item: PanelToolsItem) {\n return (\n <ds-bar-nav-action\n key={item.id}\n class=\"panel-tools__rail-action\"\n icon={item.icon}\n selected={this.isRailSelected(item.id)}\n dot={item.dot ?? false}\n inactive={item.inactive}\n aria-label={item.ariaLabel ?? PANEL_TOOLS_LABELS[item.id]}\n onDsChange={() => this.handleToolChange(item.id)}\n />\n );\n }\n\n render() {\n const headerLabel = this.headerLabel();\n const railItems = this.railItems;\n const headerItem = railItems.find(item => item.id === PANEL_TOOLS_PRIMARY_TOOL_ID);\n const bodyItems = railItems.filter(item => item.id !== PANEL_TOOLS_PRIMARY_TOOL_ID);\n const showDrawerChrome = this.isDrawerPresent();\n const drawerResting = panelToolsDrawerResting(this.open, this.motion);\n\n return (\n <Host\n class={{\n 'panel-tools': true,\n 'panel-tools--open': this.open,\n 'panel-tools--ready': this.readyForMotion,\n 'panel-tools--motion-opening': this.motion === 'opening',\n 'panel-tools--motion-closing': this.motion === 'closing',\n 'panel-tools--drawer-resting': drawerResting,\n }}\n role=\"complementary\"\n aria-label=\"Tools\"\n >\n <div class=\"panel-tools__layout\">\n <div\n class={{\n 'panel-tools__drawer': true,\n 'panel-tools__drawer--visible': showDrawerChrome,\n }}\n aria-hidden={showDrawerChrome ? null : 'true'}\n >\n <div class=\"panel-tools__drawer-surface\">\n <header class=\"panel-tools__header\">\n <span class=\"panel-tools__title text-body-medium-emphasis\">{headerLabel}</span>\n </header>\n <div class=\"panel-tools__body\">\n <div\n class={{\n 'panel-tools__view': true,\n 'panel-tools__view--active': this.isViewActive('search'),\n 'text-body-medium': true,\n }}\n hidden={!this.isViewActive('search')}\n >\n <slot name=\"search\" />\n </div>\n <div\n class={{\n 'panel-tools__view': true,\n 'panel-tools__view--active': this.isViewActive('messages'),\n 'text-body-medium': true,\n }}\n hidden={!this.isViewActive('messages')}\n >\n <slot name=\"messages\" />\n </div>\n <div\n class={{\n 'panel-tools__view': true,\n 'panel-tools__view--active': this.isViewActive('stacks'),\n 'text-body-medium': true,\n }}\n hidden={!this.isViewActive('stacks')}\n >\n <slot name=\"stacks\" />\n </div>\n <div\n class={{\n 'panel-tools__view': true,\n 'panel-tools__view--active': this.isViewActive('activity'),\n 'text-body-medium': true,\n }}\n hidden={!this.isViewActive('activity')}\n >\n <slot name=\"activity\" />\n </div>\n <div\n class={{\n 'panel-tools__view': true,\n 'panel-tools__view--active': this.isViewActive('agents'),\n 'text-body-medium': true,\n }}\n hidden={!this.isViewActive('agents')}\n >\n <slot name=\"agents\" />\n </div>\n </div>\n </div>\n </div>\n\n <nav class=\"panel-tools__rail\" aria-label=\"Tool shortcuts\">\n {headerItem ? (\n <div class=\"panel-tools__rail-header\">{this.renderRailAction(headerItem)}</div>\n ) : null}\n <div class=\"panel-tools__rail-body\">\n {bodyItems.map(item => this.renderRailAction(item))}\n </div>\n </nav>\n </div>\n </Host>\n );\n }\n}\n"],"mappings":"wMAYO,MAAMA,EAAgD,SAEtD,MAAMC,EAAuD,CAClEC,OAAQ,SACRC,SAAU,WACVC,OAAQ,SACRC,SAAU,WACVC,OAAQ,UChBJ,SAAUC,EACdC,EACAC,GAEA,GAAIA,EAAW,CACb,OAAOC,EAAmCD,EAAW,G,CAEvD,OAAOD,GAAS,EAClB,CAwBM,SAAUG,EAAwBC,EAAeC,GACrD,OAAQD,GAAQC,IAAW,MAC7B,CCrCA,MAAMC,EAAgB,IAAM,ozJ,MCefC,EAAUC,EAAA,MAAAD,UAAAE,EALvB,WAAAC,CAAAC,G,+MAS0CC,KAAAR,KAAgB,MAIxDQ,KAAAC,WAAoC,GAM5BD,KAAAZ,MAA0B,GAGCY,KAAAX,UAAoB,GAgBtCW,KAAAP,OAAyC,OAGzCO,KAAAE,eAAiB,MAE1BF,KAAAG,uBAAyB,EAmDzBH,KAAAI,oBAAuBC,IAC7B,GAAIA,EAAMC,SAAWN,KAAKO,GAAGC,cAAc,wBAAyB,OACpE,GAAIH,EAAMI,eAAiB,YAAa,OACxCT,KAAKP,OAAS,OACdO,KAAKU,sBAAsBC,KAAK,CAAEC,OAAQ,eAAgB,EAsBpDZ,KAAAa,iBAAoBC,IAC1B,MAAMC,GAAYf,KAAKgB,eAAeF,GACtC,GAAIC,EAAU,CACZf,KAAKR,KAAO,KACZQ,KAAKC,WAAaa,C,KACb,CACLd,KAAKR,KAAO,K,CAEdQ,KAAKiB,aAAaN,KAAK,CAAEG,KAAIC,YAAW,CAsH3C,CAzMC,aAAYG,GACV,OAAO/B,EAAqBa,KAAKZ,MAAOY,KAAKX,U,CAG/C,oBAAA8B,GACEnB,KAAKO,GAAGa,oBAAoB,gBAAiBpB,KAAKI,qBAClDJ,KAAKG,wBAA0B,C,CAGzB,iBAAAkB,GACN,GAAIrB,KAAKE,eAAgB,OACzB,MAAMoB,IAAetB,KAAKG,uBAC1BoB,uBAAsB,KACpB,GAAID,IAAetB,KAAKG,uBAAwB,OAChDoB,uBAAsB,KACpB,GAAID,IAAetB,KAAKG,uBAAwB,OAChDH,KAAKE,eAAiB,IAAI,GAC1B,G,CAIN,iBAAAsB,GACExB,KAAKO,GAAGkB,iBAAiB,gBAAiBzB,KAAKI,oB,CAIjD,WAAAsB,CAAYC,EAAiBC,GAC3B,GAAI5B,KAAKE,gBAAkB0B,IAAYC,WAAaD,IAAYD,EAAQ,CACtE3B,KAAKP,OAASkC,EAAS,UAAY,UACnC3B,KAAK8B,wBAAwBnB,KAAK,CAAEC,OAAQ,e,CAE9CZ,KAAKqB,mB,CAIP,iBAAAU,GACE/B,KAAKqB,mB,CAGP,gBAAAW,GACEhC,KAAKqB,mB,CAKP,YAAAY,GACEjC,KAAKqB,mB,CAWC,cAAAL,CAAeF,GACrB,OAAOd,KAAKR,MAAQQ,KAAKC,aAAea,C,CAIlC,eAAAoB,GACN,OAAOlC,KAAKR,MAAQQ,KAAKP,SAAW,S,CAG9B,YAAA0C,CAAarB,GACnB,OAAOd,KAAKkC,mBAAqBlC,KAAKC,aAAea,C,CAG/C,WAAAsB,GACN,IAAKpC,KAAKkC,oBAAsBlC,KAAKC,WAAY,MAAO,GACxD,OAAOpB,EAAmBmB,KAAKC,aAAmC,E,CAc5D,gBAAAoC,CAAiBC,GACvB,OACEC,EAAA,qBACEC,IAAKF,EAAKxB,GACV2B,MAAM,2BACNC,KAAMJ,EAAKI,KACX3B,SAAUf,KAAKgB,eAAesB,EAAKxB,IACnC6B,IAAKL,EAAKK,KAAO,MACjBC,SAAUN,EAAKM,SAAQ,aACXN,EAAKO,WAAahE,EAAmByD,EAAKxB,IACtDgC,WAAY,IAAM9C,KAAKa,iBAAiByB,EAAKxB,K,CAKnD,MAAAiC,GACE,MAAMX,EAAcpC,KAAKoC,cACzB,MAAMlB,EAAYlB,KAAKkB,UACvB,MAAM8B,EAAa9B,EAAU+B,MAAKX,GAAQA,EAAKxB,KAAOlC,IACtD,MAAMsE,EAAYhC,EAAUiC,QAAOb,GAAQA,EAAKxB,KAAOlC,IACvD,MAAMwE,EAAmBpD,KAAKkC,kBAC9B,MAAMmB,EAAgB9D,EAAwBS,KAAKR,KAAMQ,KAAKP,QAE9D,OACE8C,EAACe,EAAI,CAAAd,IAAA,2CACHC,MAAO,CACL,cAAe,KACf,oBAAqBzC,KAAKR,KAC1B,qBAAsBQ,KAAKE,eAC3B,8BAA+BF,KAAKP,SAAW,UAC/C,8BAA+BO,KAAKP,SAAW,UAC/C,8BAA+B4D,GAEjCE,KAAK,gBAAe,aACT,SAEXhB,EAAA,OAAAC,IAAA,2CAAKC,MAAM,uBACTF,EAAA,OAAAC,IAAA,2CACEC,MAAO,CACL,sBAAuB,KACvB,+BAAgCW,GACjC,cACYA,EAAmB,KAAO,QAEvCb,EAAA,OAAAC,IAAA,2CAAKC,MAAM,+BACTF,EAAA,UAAAC,IAAA,2CAAQC,MAAM,uBACZF,EAAA,QAAAC,IAAA,2CAAMC,MAAM,gDAAgDL,IAE9DG,EAAA,OAAAC,IAAA,2CAAKC,MAAM,qBACTF,EAAA,OAAAC,IAAA,2CACEC,MAAO,CACL,oBAAqB,KACrB,4BAA6BzC,KAAKmC,aAAa,UAC/C,mBAAoB,MAEtBqB,QAASxD,KAAKmC,aAAa,WAE3BI,EAAA,QAAAC,IAAA,2CAAMiB,KAAK,YAEblB,EAAA,OAAAC,IAAA,2CACEC,MAAO,CACL,oBAAqB,KACrB,4BAA6BzC,KAAKmC,aAAa,YAC/C,mBAAoB,MAEtBqB,QAASxD,KAAKmC,aAAa,aAE3BI,EAAA,QAAAC,IAAA,2CAAMiB,KAAK,cAEblB,EAAA,OAAAC,IAAA,2CACEC,MAAO,CACL,oBAAqB,KACrB,4BAA6BzC,KAAKmC,aAAa,UAC/C,mBAAoB,MAEtBqB,QAASxD,KAAKmC,aAAa,WAE3BI,EAAA,QAAAC,IAAA,2CAAMiB,KAAK,YAEblB,EAAA,OAAAC,IAAA,2CACEC,MAAO,CACL,oBAAqB,KACrB,4BAA6BzC,KAAKmC,aAAa,YAC/C,mBAAoB,MAEtBqB,QAASxD,KAAKmC,aAAa,aAE3BI,EAAA,QAAAC,IAAA,2CAAMiB,KAAK,cAEblB,EAAA,OAAAC,IAAA,2CACEC,MAAO,CACL,oBAAqB,KACrB,4BAA6BzC,KAAKmC,aAAa,UAC/C,mBAAoB,MAEtBqB,QAASxD,KAAKmC,aAAa,WAE3BI,EAAA,QAAAC,IAAA,2CAAMiB,KAAK,eAMnBlB,EAAA,OAAAC,IAAA,2CAAKC,MAAM,oBAAmB,aAAY,kBACvCO,EACCT,EAAA,OAAKE,MAAM,4BAA4BzC,KAAKqC,iBAAiBW,IAC3D,KACJT,EAAA,OAAAC,IAAA,2CAAKC,MAAM,0BACRS,EAAUQ,KAAIpB,GAAQtC,KAAKqC,iBAAiBC,Q","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ const t="dsChromeTransitionStart";const n="dsChromeTransitionEnd";class e{constructor(){this.depth=0}enter(){this.depth+=1}exit(){this.depth=Math.max(0,this.depth-1)}get isActive(){return this.depth>0}}function i(t){let n=0;return{schedule(){if(n!==0)return;n=requestAnimationFrame((()=>{n=0;t()}))},cancel(){if(n!==0){cancelAnimationFrame(n);n=0}}}}function r(t){return t.detail?.source}function s(t,n){if(typeof document==="undefined")return 0;const e=document.createElement("div");e.style.cssText="position:absolute;visibility:hidden;pointer-events:none;height:0;width:var("+n+");";t.appendChild(e);const i=e.getBoundingClientRect().width;t.removeChild(e);return i}function a(t){return{expandedPx:s(t,"--_nav-width"),collapsedPx:s(t,"--_nav-width-collapsed")}}function o(t,n){if(n?.classList.contains("panel-nav--collapsed"))return true;const e=t?.collapsed;return e===true}function c(t,n){return n?t.collapsedPx:t.expandedPx}export{e as C,t as a,n as b,i as c,a as d,s as e,o as i,c as p,r};
2
+ //# sourceMappingURL=p-1HlgCxGN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["CHROME_TRANSITION_START","CHROME_TRANSITION_END","ChromeTransitionDepth","constructor","this","depth","enter","exit","Math","max","isActive","createRafCoalescer","onFrame","rafId","schedule","requestAnimationFrame","cancel","cancelAnimationFrame","readChromeTransitionSource","event","detail","source","readCssVarWidthPx","context","cssVarName","document","probe","createElement","style","cssText","appendChild","width","getBoundingClientRect","removeChild","readPanelNavWidthTokens","navRoot","expandedPx","collapsedPx","isPanelNavCollapsed","panelNavHost","classList","contains","collapsedProp","collapsed","panelWidthPxFromTokens","tokens"],"sources":["src/wc/nav/chrome-transition.ts","src/wc/nav/shell-chrome-metrics.ts"],"sourcesContent":["/** Bubbling/composed events — `ds-app-shell` and `ds-bar-nav` coordinate during width motion. */\n\nexport const CHROME_TRANSITION_START = 'dsChromeTransitionStart';\nexport const CHROME_TRANSITION_END = 'dsChromeTransitionEnd';\n\nexport type ChromeTransitionSource = 'panel-nav' | 'panel-tools';\n\nexport interface ChromeTransitionDetail {\n source: ChromeTransitionSource;\n}\n\n/** Reference-counted gate — used while panel-nav width is transitioning. */\nexport class ChromeTransitionDepth {\n private depth = 0;\n\n enter(): void {\n this.depth += 1;\n }\n\n exit(): void {\n this.depth = Math.max(0, this.depth - 1);\n }\n\n get isActive(): boolean {\n return this.depth > 0;\n }\n}\n\nexport interface RafCoalescer {\n schedule(): void;\n cancel(): void;\n}\n\n/** Coalesce bursty layout work (ResizeObserver, prop churn) to one callback per frame. */\nexport function createRafCoalescer(onFrame: () => void): RafCoalescer {\n let rafId = 0;\n return {\n schedule() {\n if (rafId !== 0) return;\n rafId = requestAnimationFrame(() => {\n rafId = 0;\n onFrame();\n });\n },\n cancel() {\n if (rafId !== 0) {\n cancelAnimationFrame(rafId);\n rafId = 0;\n }\n },\n };\n}\n\nexport function readChromeTransitionSource(event: Event): ChromeTransitionSource | undefined {\n return (event as CustomEvent<ChromeTransitionDetail>).detail?.source;\n}\n","import { shellGradientPositionBar } from './shell-gradient';\n\n/** Read a `var(--token)` width in px using a hidden probe (works with calc() tokens). */\nexport function readCssVarWidthPx(context: HTMLElement, cssVarName: string): number {\n if (typeof document === 'undefined') return 0;\n\n const probe = document.createElement('div');\n probe.style.cssText =\n 'position:absolute;visibility:hidden;pointer-events:none;height:0;width:var(' +\n cssVarName +\n ');';\n context.appendChild(probe);\n const width = probe.getBoundingClientRect().width;\n context.removeChild(probe);\n return width;\n}\n\nexport interface PanelNavWidthTokens {\n expandedPx: number;\n collapsedPx: number;\n}\n\n/** Resolve panel-nav expanded/collapsed widths from scoped CSS vars on `.panel-nav`. */\nexport function readPanelNavWidthTokens(navRoot: HTMLElement): PanelNavWidthTokens {\n return {\n expandedPx: readCssVarWidthPx(navRoot, '--_nav-width'),\n collapsedPx: readCssVarWidthPx(navRoot, '--_nav-width-collapsed'),\n };\n}\n\nexport function isPanelNavCollapsed(\n panelNavHost: HTMLElement | null,\n navRoot: HTMLElement | null,\n): boolean {\n if (navRoot?.classList.contains('panel-nav--collapsed')) return true;\n const collapsedProp = (panelNavHost as (HTMLElement & { collapsed?: boolean }) | null)?.collapsed;\n return collapsedProp === true;\n}\n\nexport function panelWidthPxFromTokens(\n tokens: PanelNavWidthTokens,\n collapsed: boolean,\n): number {\n return collapsed ? tokens.collapsedPx : tokens.expandedPx;\n}\n\nexport function barGradientPositionFromPanelWidth(panelWidthPx: number): string {\n return shellGradientPositionBar(panelWidthPx);\n}\n"],"mappings":"AAEO,MAAMA,EAA0B,0BAChC,MAAMC,EAAwB,wB,MASxBC,EAAb,WAAAC,GACUC,KAAAC,MAAQ,C,CAEhB,KAAAC,GACEF,KAAKC,OAAS,C,CAGhB,IAAAE,GACEH,KAAKC,MAAQG,KAAKC,IAAI,EAAGL,KAAKC,MAAQ,E,CAGxC,YAAIK,GACF,OAAON,KAAKC,MAAQ,C,EAUlB,SAAUM,EAAmBC,GACjC,IAAIC,EAAQ,EACZ,MAAO,CACL,QAAAC,GACE,GAAID,IAAU,EAAG,OACjBA,EAAQE,uBAAsB,KAC5BF,EAAQ,EACRD,GAAS,G,EAGb,MAAAI,GACE,GAAIH,IAAU,EAAG,CACfI,qBAAqBJ,GACrBA,EAAQ,C,GAIhB,CAEM,SAAUK,EAA2BC,GACzC,OAAQA,EAA8CC,QAAQC,MAChE,CCpDM,SAAUC,EAAkBC,EAAsBC,GACtD,UAAWC,WAAa,YAAa,OAAO,EAE5C,MAAMC,EAAQD,SAASE,cAAc,OACrCD,EAAME,MAAMC,QACV,8EACAL,EACA,KACFD,EAAQO,YAAYJ,GACpB,MAAMK,EAAQL,EAAMM,wBAAwBD,MAC5CR,EAAQU,YAAYP,GACpB,OAAOK,CACT,CAQM,SAAUG,EAAwBC,GACtC,MAAO,CACLC,WAAYd,EAAkBa,EAAS,gBACvCE,YAAaf,EAAkBa,EAAS,0BAE5C,CAEM,SAAUG,EACdC,EACAJ,GAEA,GAAIA,GAASK,UAAUC,SAAS,wBAAyB,OAAO,KAChE,MAAMC,EAAiBH,GAAiEI,UACxF,OAAOD,IAAkB,IAC3B,CAEM,SAAUE,EACdC,EACAF,GAEA,OAAOA,EAAYE,EAAOR,YAAcQ,EAAOT,UACjD,Q","ignoreList":[]}
@@ -2,8 +2,10 @@ import type { NavChromeStyle } from '../../nav/nav-chrome';
2
2
  export declare class AppShell {
3
3
  /** Chrome style propagated to slotted `ds-panel-nav` and `ds-bar-nav`. */
4
4
  navStyle: NavChromeStyle;
5
- /** When `true`, paints the shared chrome surface (bg + wash + grid) behind panel, bar, and tools. */
5
+ /** When `true`, paints the radial wash behind panel, bar, and tools (synced to shell layout). */
6
6
  gradient: boolean;
7
+ /** When `true`, paints the diagonal grid overlay on the shared chrome layer. Independent of `gradient`. */
8
+ grid: boolean;
7
9
  /**
8
10
  * Optional custom gradient for `background-image` (e.g. SVG URL).
9
11
  * When set, overrides the built-in radial wash.
@@ -11,8 +13,8 @@ export declare class AppShell {
11
13
  gradientSrc: string;
12
14
  el: HTMLElement;
13
15
  private resizeObserver;
14
- private chromeSyncRafId;
15
- private chromeTransitionDepth;
16
+ private readonly panelNavTransition;
17
+ private readonly chromeSyncCoalescer;
16
18
  private panelWidthTokens;
17
19
  private cachedShellWidth;
18
20
  private cachedShellHeight;
@@ -32,6 +34,7 @@ export declare class AppShell {
32
34
  private syncChromeLayoutVars;
33
35
  private resolvePanelWidthPx;
34
36
  private resolveShellDimensions;
37
+ private chromeLayerActive;
35
38
  private syncChrome;
36
39
  render(): any;
37
40
  }
@@ -57,9 +57,9 @@ export declare class BarNav {
57
57
  private visibleTabGroupEl;
58
58
  private probeTabGroupEl;
59
59
  private resizeObserver;
60
- private overflowCheckScheduled;
61
60
  private intrinsicWidthRetryCount;
62
- private chromeTransitionDepth;
61
+ private readonly panelNavTransition;
62
+ private readonly overflowCoalescer;
63
63
  private chromeTransitionShell;
64
64
  private toolsDrawerOpening;
65
65
  /** Matches `basePath` once tabs + URL are reconciled for the active section. */
@@ -1,4 +1,7 @@
1
1
  import type { PanelToolsItem } from './panel-tools-types';
2
2
  export declare function parsePanelToolsItems(items: PanelToolsItem[], itemsJson: string): PanelToolsItem[];
3
3
  export declare function shouldResyncPanelToolsItems(prev: PanelToolsItem[], next: PanelToolsItem[]): boolean;
4
+ export type PanelToolsMotion = 'opening' | 'closing' | 'idle';
5
+ /** True when the drawer clip is fully closed — safe to skip painting slot content. */
6
+ export declare function panelToolsDrawerResting(open: boolean, motion: PanelToolsMotion): boolean;
4
7
  //# sourceMappingURL=panel-tools-utils.d.ts.map
@@ -99,7 +99,7 @@ export namespace Components {
99
99
  }
100
100
  interface DsAppShell {
101
101
  /**
102
- * When `true`, paints the shared chrome surface (bg + wash + grid) behind panel, bar, and tools.
102
+ * When `true`, paints the radial wash behind panel, bar, and tools (synced to shell layout).
103
103
  * @default false
104
104
  */
105
105
  "gradient": boolean;
@@ -108,6 +108,11 @@ export namespace Components {
108
108
  * @default ''
109
109
  */
110
110
  "gradientSrc": string;
111
+ /**
112
+ * When `true`, paints the diagonal grid overlay on the shared chrome layer. Independent of `gradient`.
113
+ * @default false
114
+ */
115
+ "grid": boolean;
111
116
  /**
112
117
  * Chrome style propagated to slotted `ds-panel-nav` and `ds-bar-nav`.
113
118
  * @default 'dashboard'
@@ -1781,7 +1786,7 @@ declare namespace LocalJSX {
1781
1786
  }
1782
1787
  interface DsAppShell {
1783
1788
  /**
1784
- * When `true`, paints the shared chrome surface (bg + wash + grid) behind panel, bar, and tools.
1789
+ * When `true`, paints the radial wash behind panel, bar, and tools (synced to shell layout).
1785
1790
  * @default false
1786
1791
  */
1787
1792
  "gradient"?: boolean;
@@ -1790,6 +1795,11 @@ declare namespace LocalJSX {
1790
1795
  * @default ''
1791
1796
  */
1792
1797
  "gradientSrc"?: string;
1798
+ /**
1799
+ * When `true`, paints the diagonal grid overlay on the shared chrome layer. Independent of `gradient`.
1800
+ * @default false
1801
+ */
1802
+ "grid"?: boolean;
1793
1803
  /**
1794
1804
  * Chrome style propagated to slotted `ds-panel-nav` and `ds-bar-nav`.
1795
1805
  * @default 'dashboard'
@@ -2871,6 +2881,7 @@ declare namespace LocalJSX {
2871
2881
  interface DsAppShellAttributes {
2872
2882
  "navStyle": NavChromeStyle;
2873
2883
  "gradient": boolean;
2884
+ "grid": boolean;
2874
2885
  "gradientSrc": string;
2875
2886
  }
2876
2887
  interface DsBadgeAttributes {
@@ -5,4 +5,18 @@ export type ChromeTransitionSource = 'panel-nav' | 'panel-tools';
5
5
  export interface ChromeTransitionDetail {
6
6
  source: ChromeTransitionSource;
7
7
  }
8
+ /** Reference-counted gate — used while panel-nav width is transitioning. */
9
+ export declare class ChromeTransitionDepth {
10
+ private depth;
11
+ enter(): void;
12
+ exit(): void;
13
+ get isActive(): boolean;
14
+ }
15
+ export interface RafCoalescer {
16
+ schedule(): void;
17
+ cancel(): void;
18
+ }
19
+ /** Coalesce bursty layout work (ResizeObserver, prop churn) to one callback per frame. */
20
+ export declare function createRafCoalescer(onFrame: () => void): RafCoalescer;
21
+ export declare function readChromeTransitionSource(event: Event): ChromeTransitionSource | undefined;
8
22
  //# sourceMappingURL=chrome-transition.d.ts.map
@@ -2,10 +2,10 @@ export type { NavChromeStyle } from './nav-chrome';
2
2
  export { NAV_STYLE_HINT_ATTR, readNavStyleAttr, setNavStyleHint, clearNavStyleHint, resolveNavChromeStyle, shouldResyncNavChromeStyle, } from './nav-chrome';
3
3
  export { SHELL_BAR_NAV_VT_NAME, SHELL_NAV_REVEAL_DURATION_VAR, SHELL_NAV_REVEAL_EASING_VAR, parseCssTimeMs, ensureShellNavVtStyle, resolveShellNavRevealOrigin, setShellNavRevealOriginVars, animateShellNavRadialReveal, runShellNavStyleRevealOnReady, } from './shell-view-transition';
4
4
  export type { ShellNavRevealOrigin } from './shell-view-transition';
5
- export { SHELL_GRADIENT_IMAGE_VAR, SHELL_GRADIENT_SIZE_VAR, SHELL_GRADIENT_POSITION_PANEL_VAR, SHELL_GRADIENT_POSITION_BAR_VAR, SHELL_GRADIENT_OPACITY_VAR, SHELL_CHROME_SURFACE_POSITION_VAR, SHELL_GRADIENT_OPACITY, buildShellRadialGradient, shellGradientImage, shellGradientSize, shellGradientPositionPanel, shellGradientPositionBar, shellChromeSurfacePosition, } from './shell-gradient';
5
+ export { SHELL_GRADIENT_IMAGE_VAR, SHELL_GRADIENT_SIZE_VAR, SHELL_GRADIENT_POSITION_PANEL_VAR, SHELL_GRADIENT_POSITION_BAR_VAR, SHELL_GRADIENT_OPACITY_VAR, SHELL_CHROME_SURFACE_POSITION_VAR, SHELL_GRADIENT_OPACITY, buildShellRadialGradient, shellGradientImage, shellGradientSize, shellGradientPositionPanel, shellGradientPositionBar, shellChromeSurfacePosition, shellChromeLayerActive, } from './shell-gradient';
6
6
  export type { ShellGradientLayout } from './shell-gradient';
7
7
  export type { ChromeTransitionDetail, ChromeTransitionSource } from './chrome-transition';
8
- export { CHROME_TRANSITION_END, CHROME_TRANSITION_START, } from './chrome-transition';
8
+ export { CHROME_TRANSITION_END, CHROME_TRANSITION_START, ChromeTransitionDepth, createRafCoalescer, readChromeTransitionSource, } from './chrome-transition';
9
9
  export { barGradientPositionFromPanelWidth, isPanelNavCollapsed, panelWidthPxFromTokens, readCssVarWidthPx, readPanelNavWidthTokens, } from './shell-chrome-metrics';
10
10
  export type { PanelNavWidthTokens } from './shell-chrome-metrics';
11
11
  export { BADGE_GRADIENT_POSITION_VAR, badgeGradientPosition, findGradientSurface, isShellGradientActive, parseCssBackgroundPosition, readShellGradientPosition, syncBadgeGradientPosition, } from './badge-gradient-ring';
@@ -8,6 +8,8 @@ export declare const SHELL_GRADIENT_OPACITY_VAR = "--ds-shell-gradient-opacity";
8
8
  export declare const SHELL_CHROME_SURFACE_POSITION_VAR = "--ds-shell-chrome-surface-position";
9
9
  /** Layer opacity for the nav gradient wash. */
10
10
  export declare const SHELL_GRADIENT_OPACITY = "0.1";
11
+ /** Whether the shared chrome layer (bg + optional wash/grid) should mount. */
12
+ export declare function shellChromeLayerActive(gradient: boolean, grid: boolean): boolean;
11
13
  /**
12
14
  * Shell radial wash — same for panel and bar nav.
13
15
  * Tokens follow `data-theme` (light/dark blue intent).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ds-mo/ui",
3
- "version": "1.10.1",
3
+ "version": "1.11.1",
4
4
  "description": "CompoMo — composable web components styled with TokoMo design tokens",
5
5
  "keywords": [
6
6
  "web-components",
@@ -69,7 +69,7 @@
69
69
  "pretest": "node scripts/generate-icon-catalog.mjs",
70
70
  "build": "stencil build && node scripts/patch-index-types.mjs && node scripts/verify-icons-externalized.mjs && node scripts/write-build-stamp.mjs",
71
71
  "verify:pack": "node scripts/verify-npm-pack.mjs && node scripts/verify-nav-import.mjs",
72
- "test": "node --import tsx/esm --test tests/panel-nav-utils.test.ts tests/panel-tools.test.ts tests/shell-view-transition.test.ts tests/shell-gradient.test.ts tests/shell-chrome-metrics.test.ts tests/bar-nav-overflow-hysteresis.test.ts tests/badge-gradient-ring.test.ts tests/scroll-edge-fade.test.ts tests/resolve-css-length-px.test.ts tests/resolve-css-time-ms.test.ts tests/overlay-positioning.test.ts tests/menu-position.test.ts tests/bar-nav-utils.test.ts tests/bar-nav-tabs-menu-utils.test.ts tests/bar-nav-dom-utils.test.ts tests/icon-catalog.test.ts",
72
+ "test": "node --import tsx/esm --test tests/panel-nav-utils.test.ts tests/panel-tools.test.ts tests/shell-view-transition.test.ts tests/shell-gradient.test.ts tests/shell-chrome-metrics.test.ts tests/shell-chrome-layer.test.ts tests/chrome-transition.test.ts tests/bar-nav-overflow-hysteresis.test.ts tests/badge-gradient-ring.test.ts tests/scroll-edge-fade.test.ts tests/resolve-css-length-px.test.ts tests/resolve-css-time-ms.test.ts tests/overlay-positioning.test.ts tests/menu-position.test.ts tests/bar-nav-utils.test.ts tests/bar-nav-tabs-menu-utils.test.ts tests/bar-nav-dom-utils.test.ts tests/icon-catalog.test.ts",
73
73
  "test:e2e": "npm run build && playwright test",
74
74
  "test:e2e:install": "playwright install chromium",
75
75
  "build:docs": "stencil build --docs",
@@ -36,14 +36,14 @@ export declare interface DsAccordion extends Components.DsAccordion {
36
36
 
37
37
 
38
38
  @ProxyCmp({
39
- inputs: ['gradient', 'gradientSrc', 'navStyle']
39
+ inputs: ['gradient', 'gradientSrc', 'grid', 'navStyle']
40
40
  })
41
41
  @Component({
42
42
  selector: 'ds-app-shell',
43
43
  changeDetection: ChangeDetectionStrategy.OnPush,
44
44
  template: '<ng-content></ng-content>',
45
45
  // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
46
- inputs: ['gradient', 'gradientSrc', 'navStyle'],
46
+ inputs: ['gradient', 'gradientSrc', 'grid', 'navStyle'],
47
47
  standalone: false
48
48
  })
49
49
  export class DsAppShell {
@@ -3,7 +3,9 @@ import type { NavChromeStyle } from '../../nav/nav-chrome';
3
3
  import {
4
4
  CHROME_TRANSITION_END,
5
5
  CHROME_TRANSITION_START,
6
- type ChromeTransitionDetail,
6
+ ChromeTransitionDepth,
7
+ createRafCoalescer,
8
+ readChromeTransitionSource,
7
9
  } from '../../nav/chrome-transition';
8
10
  import {
9
11
  isPanelNavCollapsed,
@@ -33,9 +35,12 @@ export class AppShell {
33
35
  /** Chrome style propagated to slotted `ds-panel-nav` and `ds-bar-nav`. */
34
36
  @Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'dashboard';
35
37
 
36
- /** When `true`, paints the shared chrome surface (bg + wash + grid) behind panel, bar, and tools. */
38
+ /** When `true`, paints the radial wash behind panel, bar, and tools (synced to shell layout). */
37
39
  @Prop({ reflect: true }) gradient: boolean = false;
38
40
 
41
+ /** When `true`, paints the diagonal grid overlay on the shared chrome layer. Independent of `gradient`. */
42
+ @Prop({ reflect: true }) grid: boolean = false;
43
+
39
44
  /**
40
45
  * Optional custom gradient for `background-image` (e.g. SVG URL).
41
46
  * When set, overrides the built-in radial wash.
@@ -45,8 +50,8 @@ export class AppShell {
45
50
  @Element() el!: HTMLElement;
46
51
 
47
52
  private resizeObserver: ResizeObserver | null = null;
48
- private chromeSyncRafId = 0;
49
- private chromeTransitionDepth = 0;
53
+ private readonly panelNavTransition = new ChromeTransitionDepth();
54
+ private readonly chromeSyncCoalescer = createRafCoalescer(() => this.syncChrome());
50
55
  private panelWidthTokens: PanelNavWidthTokens = { expandedPx: 0, collapsedPx: 0 };
51
56
  private cachedShellWidth = 0;
52
57
  private cachedShellHeight = 0;
@@ -65,16 +70,14 @@ export class AppShell {
65
70
  disconnectedCallback() {
66
71
  this.resizeObserver?.disconnect();
67
72
  this.resizeObserver = null;
68
- if (this.chromeSyncRafId) {
69
- cancelAnimationFrame(this.chromeSyncRafId);
70
- this.chromeSyncRafId = 0;
71
- }
73
+ this.chromeSyncCoalescer.cancel();
72
74
  this.el.removeEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);
73
75
  this.el.removeEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);
74
76
  }
75
77
 
76
78
  @Watch('navStyle')
77
79
  @Watch('gradient')
80
+ @Watch('grid')
78
81
  @Watch('gradientSrc')
79
82
  onShellPropsChange() {
80
83
  this.syncSlottedNavStyle();
@@ -82,32 +85,26 @@ export class AppShell {
82
85
  }
83
86
 
84
87
  private onChromeTransitionStart = (event: Event) => {
85
- const source = (event as CustomEvent<ChromeTransitionDetail>).detail?.source;
86
- if (source === 'panel-nav') {
87
- this.chromeTransitionDepth += 1;
88
- this.cachedShellWidth = this.el.clientWidth;
89
- this.cachedShellHeight = this.el.clientHeight;
90
- this.syncChrome();
91
- }
88
+ if (readChromeTransitionSource(event) !== 'panel-nav') return;
89
+
90
+ this.panelNavTransition.enter();
91
+ this.cachedShellWidth = this.el.clientWidth;
92
+ this.cachedShellHeight = this.el.clientHeight;
93
+ this.syncChrome();
92
94
  };
93
95
 
94
96
  private onChromeTransitionEnd = (event: Event) => {
95
- const source = (event as CustomEvent<ChromeTransitionDetail>).detail?.source;
96
- if (source !== 'panel-nav') return;
97
+ if (readChromeTransitionSource(event) !== 'panel-nav') return;
97
98
 
98
- this.chromeTransitionDepth = Math.max(0, this.chromeTransitionDepth - 1);
99
- if (this.chromeTransitionDepth === 0) {
99
+ this.panelNavTransition.exit();
100
+ if (!this.panelNavTransition.isActive) {
100
101
  this.scheduleChromeSync();
101
102
  }
102
103
  };
103
104
 
104
105
  /** Coalesce ResizeObserver bursts to one layout read per frame. */
105
106
  private scheduleChromeSync() {
106
- if (this.chromeSyncRafId) return;
107
- this.chromeSyncRafId = requestAnimationFrame(() => {
108
- this.chromeSyncRafId = 0;
109
- this.syncChrome();
110
- });
107
+ this.chromeSyncCoalescer.schedule();
111
108
  }
112
109
 
113
110
  private syncSlottedNavStyle() {
@@ -125,7 +122,7 @@ export class AppShell {
125
122
 
126
123
  private connectMetricsObserver() {
127
124
  this.resizeObserver = new ResizeObserver(() => {
128
- if (this.chromeTransitionDepth > 0) return;
125
+ if (this.panelNavTransition.isActive) return;
129
126
  this.scheduleChromeSync();
130
127
  });
131
128
  this.resizeObserver.observe(this.el);
@@ -176,7 +173,7 @@ export class AppShell {
176
173
  private resolvePanelWidthPx(panelNav: HTMLElement | null): number {
177
174
  const navRoot = panelNav?.querySelector('.panel-nav') as HTMLElement | null;
178
175
 
179
- if (this.chromeTransitionDepth > 0 && this.panelWidthTokens.expandedPx > 0) {
176
+ if (this.panelNavTransition.isActive && this.panelWidthTokens.expandedPx > 0) {
180
177
  const collapsed = isPanelNavCollapsed(panelNav, navRoot);
181
178
  return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);
182
179
  }
@@ -194,7 +191,7 @@ export class AppShell {
194
191
  }
195
192
 
196
193
  private resolveShellDimensions(): { width: number; height: number } {
197
- if (this.chromeTransitionDepth > 0 && this.cachedShellWidth > 0) {
194
+ if (this.panelNavTransition.isActive && this.cachedShellWidth > 0) {
198
195
  return {
199
196
  width: this.cachedShellWidth,
200
197
  height: this.cachedShellHeight || this.el.clientHeight,
@@ -205,11 +202,32 @@ export class AppShell {
205
202
  return { width: rect.width, height: rect.height };
206
203
  }
207
204
 
205
+ private chromeLayerActive(): boolean {
206
+ return this.gradient || this.grid;
207
+ }
208
+
208
209
  private syncChrome() {
209
210
  const panelNav = this.el.querySelector('ds-panel-nav') as HTMLElement | null;
210
211
  const bar = this.el.querySelector('ds-bar-nav') as HTMLElement | null;
211
212
  const targets = [this.el, panelNav, bar].filter((el): el is HTMLElement => el !== null);
212
213
 
214
+ const clearLayoutVars = () => {
215
+ if (panelNav) panelNav.style.removeProperty(SHELL_GRADIENT_POSITION_PANEL_VAR);
216
+ if (bar) bar.style.removeProperty(SHELL_GRADIENT_POSITION_BAR_VAR);
217
+ };
218
+
219
+ if (!this.chromeLayerActive()) {
220
+ this.clearGradientPaintVars(targets);
221
+ clearLayoutVars();
222
+ return;
223
+ }
224
+
225
+ if (!this.gradient) {
226
+ this.clearGradientPaintVars(targets);
227
+ clearLayoutVars();
228
+ return;
229
+ }
230
+
213
231
  const { width: shellWidth, height: shellHeight } = this.resolveShellDimensions();
214
232
  const panelWidth = this.resolvePanelWidthPx(panelNav);
215
233
 
@@ -218,11 +236,6 @@ export class AppShell {
218
236
 
219
237
  this.syncChromeLayoutVars(panelNav, bar, panelPosition, barPosition);
220
238
 
221
- if (!this.gradient) {
222
- this.clearGradientPaintVars(targets);
223
- return;
224
- }
225
-
226
239
  const image = this.gradientSrc.trim()
227
240
  ? `url(${this.gradientSrc.trim()})`
228
241
  : buildShellRadialGradient();
@@ -258,16 +271,18 @@ export class AppShell {
258
271
  }
259
272
 
260
273
  render() {
274
+ const chromeActive = this.chromeLayerActive();
261
275
  const shellCls: Record<string, boolean> = {
262
276
  'app-shell': true,
263
277
  'app-shell--gradient': this.gradient,
278
+ 'app-shell--grid': this.grid,
264
279
  [`app-shell--${this.navStyle}`]: true,
265
280
  };
266
281
 
267
282
  return (
268
283
  <Host class={shellCls}>
269
284
  <div class="app-shell__row">
270
- {this.gradient ? <div class="app-shell__chrome" aria-hidden="true" /> : null}
285
+ {chromeActive ? <div class="app-shell__chrome" aria-hidden="true" /> : null}
271
286
  <div class="app-shell__panel">
272
287
  <slot name="panel" />
273
288
  </div>
@@ -31,7 +31,9 @@ import {
31
31
  import {
32
32
  CHROME_TRANSITION_END,
33
33
  CHROME_TRANSITION_START,
34
- type ChromeTransitionDetail,
34
+ ChromeTransitionDepth,
35
+ createRafCoalescer,
36
+ readChromeTransitionSource,
35
37
  } from '../../nav/chrome-transition';
36
38
  import { readCssVarWidthPx } from '../../nav/shell-chrome-metrics';
37
39
 
@@ -107,9 +109,12 @@ export class BarNav {
107
109
  private visibleTabGroupEl: QueryableHost | null = null;
108
110
  private probeTabGroupEl: QueryableHost | null = null;
109
111
  private resizeObserver: ResizeObserver | null = null;
110
- private overflowCheckScheduled = false;
111
112
  private intrinsicWidthRetryCount = 0;
112
- private chromeTransitionDepth = 0;
113
+ private readonly panelNavTransition = new ChromeTransitionDepth();
114
+ private readonly overflowCoalescer = createRafCoalescer(() => {
115
+ this.updateTabsCollapsed();
116
+ this.updateTriggerLabelTruncation();
117
+ });
113
118
  private chromeTransitionShell: HTMLElement | null = null;
114
119
  private toolsDrawerOpening = false;
115
120
  /** Matches `basePath` once tabs + URL are reconciled for the active section. */
@@ -189,7 +194,7 @@ export class BarNav {
189
194
  }
190
195
 
191
196
  componentDidRender() {
192
- if (this.chromeTransitionDepth > 0) {
197
+ if (this.panelNavTransition.isActive) {
193
198
  this.syncMenuAnchor();
194
199
  return;
195
200
  }
@@ -203,6 +208,7 @@ export class BarNav {
203
208
  disconnectedCallback() {
204
209
  this.resizeObserver?.disconnect();
205
210
  this.resizeObserver = null;
211
+ this.overflowCoalescer.cancel();
206
212
  this.syncTriggerLabelObserver(true);
207
213
  this.unbindChromeTransitionListeners();
208
214
  }
@@ -229,9 +235,9 @@ export class BarNav {
229
235
  }
230
236
 
231
237
  private onChromeTransitionStart = (event: Event) => {
232
- const source = (event as CustomEvent<ChromeTransitionDetail>).detail?.source;
238
+ const source = readChromeTransitionSource(event);
233
239
  if (source === 'panel-nav') {
234
- this.chromeTransitionDepth += 1;
240
+ this.panelNavTransition.enter();
235
241
  return;
236
242
  }
237
243
  if (source === 'panel-tools') {
@@ -246,10 +252,10 @@ export class BarNav {
246
252
  };
247
253
 
248
254
  private onChromeTransitionEnd = (event: Event) => {
249
- const source = (event as CustomEvent<ChromeTransitionDetail>).detail?.source;
255
+ const source = readChromeTransitionSource(event);
250
256
  if (source === 'panel-nav') {
251
- this.chromeTransitionDepth = Math.max(0, this.chromeTransitionDepth - 1);
252
- if (this.chromeTransitionDepth === 0) {
257
+ this.panelNavTransition.exit();
258
+ if (!this.panelNavTransition.isActive) {
253
259
  this.scheduleOverflowCheck();
254
260
  }
255
261
  return;
@@ -295,22 +301,15 @@ export class BarNav {
295
301
  if (typeof ResizeObserver === 'undefined' || !this.headerEl) return;
296
302
  this.resizeObserver?.disconnect();
297
303
  this.resizeObserver = new ResizeObserver(() => {
298
- if (this.chromeTransitionDepth > 0) return;
304
+ if (this.panelNavTransition.isActive) return;
299
305
  this.scheduleOverflowCheck();
300
306
  });
301
307
  this.resizeObserver.observe(this.headerEl);
302
308
  }
303
309
 
304
310
  private scheduleOverflowCheck() {
305
- if (this.chromeTransitionDepth > 0) return;
306
- if (this.overflowCheckScheduled) return;
307
- this.overflowCheckScheduled = true;
308
- requestAnimationFrame(() => {
309
- this.overflowCheckScheduled = false;
310
- if (this.chromeTransitionDepth > 0) return;
311
- this.updateTabsCollapsed();
312
- this.updateTriggerLabelTruncation();
313
- });
311
+ if (this.panelNavTransition.isActive) return;
312
+ this.overflowCoalescer.schedule();
314
313
  }
315
314
 
316
315
  private syncTriggerLabelObserver(disconnectOnly = false) {
@@ -6,7 +6,7 @@ import {
6
6
  type PanelToolsItem,
7
7
  type PanelToolsToolId,
8
8
  } from './panel-tools-types';
9
- import { parsePanelToolsItems } from './panel-tools-utils';
9
+ import { parsePanelToolsItems, panelToolsDrawerResting } from './panel-tools-utils';
10
10
 
11
11
  @Component({
12
12
  tag: 'ds-panel-tools',
@@ -160,6 +160,7 @@ export class PanelTools {
160
160
  const headerItem = railItems.find(item => item.id === PANEL_TOOLS_PRIMARY_TOOL_ID);
161
161
  const bodyItems = railItems.filter(item => item.id !== PANEL_TOOLS_PRIMARY_TOOL_ID);
162
162
  const showDrawerChrome = this.isDrawerPresent();
163
+ const drawerResting = panelToolsDrawerResting(this.open, this.motion);
163
164
 
164
165
  return (
165
166
  <Host
@@ -169,6 +170,7 @@ export class PanelTools {
169
170
  'panel-tools--ready': this.readyForMotion,
170
171
  'panel-tools--motion-opening': this.motion === 'opening',
171
172
  'panel-tools--motion-closing': this.motion === 'closing',
173
+ 'panel-tools--drawer-resting': drawerResting,
172
174
  }}
173
175
  role="complementary"
174
176
  aria-label="Tools"
@@ -29,3 +29,10 @@ export function shouldResyncPanelToolsItems(
29
29
  );
30
30
  });
31
31
  }
32
+
33
+ export type PanelToolsMotion = 'opening' | 'closing' | 'idle';
34
+
35
+ /** True when the drawer clip is fully closed — safe to skip painting slot content. */
36
+ export function panelToolsDrawerResting(open: boolean, motion: PanelToolsMotion): boolean {
37
+ return !open && motion === 'idle';
38
+ }