@ds-mo/ui 13.11.1 → 13.13.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.
Files changed (167) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +307 -10
  4. package/dist/angular/ds-button-filled.d.ts +1 -1
  5. package/dist/angular/ds-button-filled.js +3 -3
  6. package/dist/angular/ds-button-unfilled.d.ts +1 -1
  7. package/dist/angular/ds-button-unfilled.js +3 -3
  8. package/dist/angular/ds-mobile-header.d.ts +1 -1
  9. package/dist/angular/ds-mobile-header.js +3 -3
  10. package/dist/angular/ds-tab-group.d.ts +1 -1
  11. package/dist/angular/ds-tab-group.js +3 -3
  12. package/dist/angular/ds-table.d.ts +31 -0
  13. package/dist/angular/ds-table.js +57 -0
  14. package/dist/angular/ds-tag.d.ts +1 -1
  15. package/dist/angular/ds-tag.js +3 -3
  16. package/dist/angular/index.d.ts +1 -1
  17. package/dist/angular/index.js +1 -0
  18. package/dist/angular/proxies.d.ts +1 -0
  19. package/dist/angular/proxies.js +1 -0
  20. package/dist/components/ds-agent-response.js +1 -1
  21. package/dist/components/ds-banner.js +1 -1
  22. package/dist/components/ds-bar-nav.js +1 -1
  23. package/dist/components/ds-bar-title.js +1 -1
  24. package/dist/components/ds-bar-workflow.js +1 -1
  25. package/dist/components/ds-bar-workflow.js.map +1 -1
  26. package/dist/components/ds-button-filled.js +1 -1
  27. package/dist/components/ds-button-unfilled.js +1 -1
  28. package/dist/components/ds-card-chart.js +1 -1
  29. package/dist/components/ds-card-overview.js +1 -1
  30. package/dist/components/ds-card-overview.js.map +1 -1
  31. package/dist/components/ds-card-setting.js +1 -1
  32. package/dist/components/ds-chart-legend.js +1 -1
  33. package/dist/components/ds-chart-legend.js.map +1 -1
  34. package/dist/components/ds-checkbox.js +1 -1
  35. package/dist/components/ds-chip.js +1 -1
  36. package/dist/components/ds-chip.js.map +1 -1
  37. package/dist/components/ds-code-block.js +1 -1
  38. package/dist/components/ds-conversation-list-item.js +1 -1
  39. package/dist/components/ds-conversation-list-item.js.map +1 -1
  40. package/dist/components/ds-input.js +1 -1
  41. package/dist/components/ds-input.js.map +1 -1
  42. package/dist/components/ds-markdown.js +1 -1
  43. package/dist/components/ds-menu.js +1 -1
  44. package/dist/components/ds-message-actions.js +1 -1
  45. package/dist/components/ds-message-composer.js +1 -1
  46. package/dist/components/ds-message-scroller.js +1 -1
  47. package/dist/components/ds-mobile-bar-nav.js +1 -1
  48. package/dist/components/ds-mobile-header.js +1 -1
  49. package/dist/components/ds-mobile-section-switcher.js +1 -1
  50. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  51. package/dist/components/ds-modal.js +1 -1
  52. package/dist/components/ds-panel-nav.js +1 -1
  53. package/dist/components/ds-panel-sub-nav.js +1 -1
  54. package/dist/components/ds-panel-sub-nav.js.map +1 -1
  55. package/dist/components/ds-panel-tool-header.js +1 -1
  56. package/dist/components/ds-panel-tool-search.js +1 -1
  57. package/dist/components/ds-panel-tools.js +1 -1
  58. package/dist/components/ds-radio.js +1 -1
  59. package/dist/components/ds-radio.js.map +1 -1
  60. package/dist/components/ds-scroll-overlay.js +1 -1
  61. package/dist/components/ds-scroll-overlay.js.map +1 -1
  62. package/dist/components/ds-select.js +1 -1
  63. package/dist/components/ds-select.js.map +1 -1
  64. package/dist/components/ds-shell-app.js +1 -1
  65. package/dist/components/ds-shell-app.js.map +1 -1
  66. package/dist/components/ds-shell-tools.js +1 -1
  67. package/dist/components/ds-skeleton.js +1 -1
  68. package/dist/components/ds-slider.js +1 -1
  69. package/dist/components/ds-switch.js +1 -1
  70. package/dist/components/ds-tab-group.js +1 -1
  71. package/dist/components/ds-table.d.ts +11 -0
  72. package/dist/components/ds-table.js +2 -0
  73. package/dist/components/ds-table.js.map +1 -0
  74. package/dist/components/ds-tag.js +1 -1
  75. package/dist/components/ds-tag.js.map +1 -1
  76. package/dist/components/ds-toast.js +1 -1
  77. package/dist/components/ds-typing-indicator.js +1 -1
  78. package/dist/components/index.d.ts +30 -0
  79. package/dist/components/p-B1JB0Zn-.js +2 -0
  80. package/dist/components/{p-DgglLVCj.js.map → p-B1JB0Zn-.js.map} +1 -1
  81. package/dist/components/p-B2t8alvH.js +2 -0
  82. package/dist/components/{p-DK4PGrHm.js.map → p-B2t8alvH.js.map} +1 -1
  83. package/dist/components/p-BAGTb6cZ.js +2 -0
  84. package/dist/components/p-BAGTb6cZ.js.map +1 -0
  85. package/dist/components/p-BEEYhbIb.js +2 -0
  86. package/dist/components/p-BEEYhbIb.js.map +1 -0
  87. package/dist/components/{p-BRajO-Za.js → p-BFMV9TJO.js} +2 -2
  88. package/dist/components/p-BNHPCCw8.js +2 -0
  89. package/dist/components/{p-DCN04sgt.js.map → p-BNHPCCw8.js.map} +1 -1
  90. package/dist/components/{p-DJGdoPMG.js → p-BUZLhvs1.js} +2 -2
  91. package/dist/components/p-BgaF83uh.js +2 -0
  92. package/dist/components/{p-C5gZHZHU.js.map → p-BgaF83uh.js.map} +1 -1
  93. package/dist/components/p-Br0fwWUG.js +2 -0
  94. package/dist/components/{p-BHb3Ws8e.js.map → p-Br0fwWUG.js.map} +1 -1
  95. package/dist/components/{p-DcmqsAMP.js → p-BydCwx7x.js} +2 -2
  96. package/dist/components/p-C3Z01Glr.js +2 -0
  97. package/dist/components/p-C3Z01Glr.js.map +1 -0
  98. package/dist/components/{p-Dg9qiMDa.js → p-C9StMCWg.js} +2 -2
  99. package/dist/components/{p-BX57q1Lo.js → p-CSlTJWFG.js} +2 -2
  100. package/dist/components/{p-BQ7Nzx9A.js → p-Ch5yT3UA.js} +2 -2
  101. package/dist/components/p-Cw1bbiCG.js +2 -0
  102. package/dist/components/p-Cw1bbiCG.js.map +1 -0
  103. package/dist/components/p-CzV4gIxd.js +2 -0
  104. package/dist/components/p-CzV4gIxd.js.map +1 -0
  105. package/dist/components/p-D-F8eyXy.js +2 -0
  106. package/dist/components/p-D-F8eyXy.js.map +1 -0
  107. package/dist/components/p-DGvzGGcu.js +2 -0
  108. package/dist/components/{p-CIlO_j3q.js.map → p-DGvzGGcu.js.map} +1 -1
  109. package/dist/components/{p-DkhE3BsH.js → p-bLCbOPYa.js} +2 -2
  110. package/dist/components/p-cPh32CFB.js +2 -0
  111. package/dist/components/{p-BnGJrBPG.js.map → p-cPh32CFB.js.map} +1 -1
  112. package/dist/docs/components.json +1590 -59
  113. package/dist/lib/components/ButtonFilled/ButtonFilled.d.ts +2 -0
  114. package/dist/lib/components/MobileHeader/mobile-header-types.d.ts +3 -2
  115. package/dist/mcp-data/registry/button-filled.json +20 -1
  116. package/dist/mcp-data/registry/button-unfilled.json +20 -1
  117. package/dist/mcp-data/registry/mobile-header.json +48 -10
  118. package/dist/mcp-data/registry/mobile-sheet-nav.json +2 -2
  119. package/dist/mcp-data/registry/registry.json +309 -9
  120. package/dist/mcp-data/registry/shell-app.json +4 -2
  121. package/dist/mcp-data/registry/shell-tools.json +4 -4
  122. package/dist/mcp-data/registry/tab-group.json +22 -2
  123. package/dist/mcp-data/registry/table.json +777 -0
  124. package/dist/mcp-data/registry/tag.json +22 -3
  125. package/dist/react/components.d.ts +1 -0
  126. package/dist/react/components.js +1 -0
  127. package/dist/react/ds-table.d.ts +17 -0
  128. package/dist/react/ds-table.js +19 -0
  129. package/dist/styles/table.css +874 -0
  130. package/dist/types/components/ButtonFilled/ButtonFilled.d.ts +2 -0
  131. package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +2 -0
  132. package/dist/types/components/MobileHeader/MobileHeader.d.ts +8 -3
  133. package/dist/types/components/MobileHeader/index.d.ts +1 -1
  134. package/dist/types/components/MobileHeader/mobile-header-types.d.ts +3 -2
  135. package/dist/types/components/ShellApp/ShellApp.d.ts +8 -0
  136. package/dist/types/components/TabGroup/TabGroup.d.ts +3 -0
  137. package/dist/types/components/Table/Table.d.ts +126 -0
  138. package/dist/types/components/Table/index.d.ts +2 -0
  139. package/dist/types/components/Table/table-model.d.ts +29 -0
  140. package/dist/types/components/Table/table-types.d.ts +189 -0
  141. package/dist/types/components/Tag/Tag.d.ts +2 -0
  142. package/dist/types/components.d.ts +417 -6
  143. package/package.json +3 -2
  144. package/dist/components/p-BHb3Ws8e.js +0 -2
  145. package/dist/components/p-BnGJrBPG.js +0 -2
  146. package/dist/components/p-C5gZHZHU.js +0 -2
  147. package/dist/components/p-CIlO_j3q.js +0 -2
  148. package/dist/components/p-CVmtbj-6.js +0 -2
  149. package/dist/components/p-CVmtbj-6.js.map +0 -1
  150. package/dist/components/p-DAbaMtEX.js +0 -2
  151. package/dist/components/p-DAbaMtEX.js.map +0 -1
  152. package/dist/components/p-DCN04sgt.js +0 -2
  153. package/dist/components/p-DK4PGrHm.js +0 -2
  154. package/dist/components/p-DW9p068S.js +0 -2
  155. package/dist/components/p-DW9p068S.js.map +0 -1
  156. package/dist/components/p-DgglLVCj.js +0 -2
  157. package/dist/components/p-Xftomzsv.js +0 -2
  158. package/dist/components/p-Xftomzsv.js.map +0 -1
  159. package/dist/components/p-c1pY-Mqq.js +0 -2
  160. package/dist/components/p-c1pY-Mqq.js.map +0 -1
  161. /package/dist/components/{p-BRajO-Za.js.map → p-BFMV9TJO.js.map} +0 -0
  162. /package/dist/components/{p-DJGdoPMG.js.map → p-BUZLhvs1.js.map} +0 -0
  163. /package/dist/components/{p-DcmqsAMP.js.map → p-BydCwx7x.js.map} +0 -0
  164. /package/dist/components/{p-Dg9qiMDa.js.map → p-C9StMCWg.js.map} +0 -0
  165. /package/dist/components/{p-BX57q1Lo.js.map → p-CSlTJWFG.js.map} +0 -0
  166. /package/dist/components/{p-BQ7Nzx9A.js.map → p-Ch5yT3UA.js.map} +0 -0
  167. /package/dist/components/{p-DkhE3BsH.js.map → p-bLCbOPYa.js.map} +0 -0
@@ -17,6 +17,8 @@ export declare class ButtonFilled {
17
17
  variant: ButtonFilledVariant;
18
18
  /** Control density (height, padding, icon, type). */
19
19
  size: ButtonFilledSize;
20
+ /** Use reduced outer geometry when nested inside a control of the same size. */
21
+ isInset: boolean;
20
22
  /** Width fit — hug content (default) or fill the parent. */
21
23
  width: ButtonFilledWidth;
22
24
  /** Visible text for `label` / `icon-label` variants. */
@@ -1,4 +1,5 @@
1
- import type { TabItem } from '../TabGroup/tab-item-utils';
1
+ import type { TabGroupItem } from '../TabGroup/tab-item-utils';
2
2
  export type MobileHeaderTone = 'default' | 'brand';
3
3
  export type MobileHeaderHeadingLevel = 'h1' | 'h2';
4
- export type MobileHeaderSectionItem = TabItem;
4
+ export type MobileHeaderSectionsPresentation = 'switcher' | 'segmented';
5
+ export type MobileHeaderSectionItem = TabGroupItem;
@@ -63,6 +63,7 @@
63
63
  ],
64
64
  "states": [
65
65
  "Every size applies one complete control-density recipe across frame height, inline padding, icon zone, text-container inset, gap, and radius.",
66
+ "Set isInset only when the button is nested inside a same-size control; it reduces the outer geometry while preserving the size's icon, label inset, gap, radius, and typography.",
66
67
  "Use only one filled action per local decision area; render all secondary actions with ButtonUnfilled.",
67
68
  "Choose brand or neutral for general commands. Reserve positive, negative, warning, and caution for matching action consequences, and AI, guide, or walkthrough for actions that enter those feature experiences.",
68
69
  "Contrast serves both surface adaptation and hierarchy, but never permits a second filled action in the same decision area.",
@@ -194,6 +195,15 @@
194
195
  "mutable": false,
195
196
  "description": "Disables interaction."
196
197
  },
198
+ "isInset": {
199
+ "type": "boolean",
200
+ "resolvedType": "boolean",
201
+ "attribute": "is-inset",
202
+ "default": "false",
203
+ "required": false,
204
+ "mutable": false,
205
+ "description": "Use reduced outer geometry when nested inside a control of the same size."
206
+ },
197
207
  "isLoading": {
198
208
  "type": "boolean",
199
209
  "resolvedType": "boolean",
@@ -380,6 +390,15 @@
380
390
  "mutable": false,
381
391
  "description": "Disables interaction."
382
392
  },
393
+ "isInset": {
394
+ "type": "boolean",
395
+ "resolvedType": "boolean",
396
+ "attribute": "is-inset",
397
+ "default": "false",
398
+ "required": false,
399
+ "mutable": false,
400
+ "description": "Use reduced outer geometry when nested inside a control of the same size."
401
+ },
383
402
  "isLoading": {
384
403
  "type": "boolean",
385
404
  "resolvedType": "boolean",
@@ -517,7 +536,7 @@
517
536
  },
518
537
  {
519
538
  "path": "src/wc/components/ButtonFilled/ButtonFilled.tsx",
520
- "content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\nimport { controlWidthClass } from '../../utils';\nimport type { ChoiceBackground } from '../../utils/choice-list';\nimport { beginElevatedControlPress } from '../../utils/control-press';\nimport { renderButtonContent } from '../../utils/button-render';\nimport type {\n ButtonPopup,\n ButtonSize,\n ButtonVariant,\n ButtonWidth,\n} from '../../utils/button-types';\n\nexport type ButtonFilledIntent =\n | 'neutral'\n | 'brand'\n | 'ai'\n | 'negative'\n | 'warning'\n | 'caution'\n | 'positive'\n | 'guide'\n | 'walkthrough';\n\nexport type ButtonFilledContrast = 'bold' | 'strong' | 'medium' | 'faint';\n\nexport type ButtonFilledBackground = ChoiceBackground;\n\nexport type ButtonFilledVariant = ButtonVariant;\nexport type ButtonFilledSize = ButtonSize;\nexport type ButtonFilledWidth = ButtonWidth;\nexport type ButtonFilledPopup = ButtonPopup;\n\n@Component({\n tag: 'ds-button-filled',\n styleUrl: 'ButtonFilled.css',\n scoped: true,\n})\nexport class ButtonFilled {\n @Element() el!: HTMLElement;\n\n /**\n * Content layout. Default is label-only; pass `icon` for icon-only chrome\n * (nav / tool rails) or `icon-label` for leading icon + text.\n */\n @Prop() variant: ButtonFilledVariant = 'label';\n\n /** Control density (height, padding, icon, type). */\n @Prop() size: ButtonFilledSize = 'md';\n\n /** Width fit — hug content (default) or fill the parent. */\n @Prop() width: ButtonFilledWidth = 'hug';\n\n /** Visible text for `label` / `icon-label` variants. */\n @Prop() label: string = '';\n\n /** Icon name passed to <ds-icon> for `icon` / `icon-label` variants. */\n @Prop() icon: string = '';\n\n /** Semantic colour intent. */\n @Prop() intent: ButtonFilledIntent = 'brand';\n\n /**\n * Background fill weight. Foreground uses the paired contrast token:\n * bold → faint, strong → medium, medium → strong, faint → bold.\n */\n @Prop() contrast: ButtonFilledContrast = 'bold';\n\n /** Show a 1px secondary inset border. */\n @Prop() hasBorder: boolean = false;\n\n /**\n * Actual parent surface context for the optional inset border color only.\n * Omit on primary and secondary surfaces.\n */\n @Prop() background: ButtonFilledBackground | undefined;\n\n /** Use the half-radius treatment instead of the default control radius. */\n @Prop() rounded: boolean = false;\n\n /**\n * Scale down during a physical pointer press.\n * Disable when an owning composite requires fixed child or background geometry.\n */\n @Prop() pressScale: boolean = true;\n\n /** Disables interaction. */\n @Prop() isInactive: boolean = false;\n\n /** Shows an inline loader and prevents interaction without applying inactive opacity. */\n @Prop() isLoading: boolean = false;\n\n /** Native button type. */\n @Prop() type: 'button' | 'submit' | 'reset' = 'button';\n\n /** Accessible name override. Required for icon-only buttons. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n\n /** ID of the popup this button controls. */\n @Prop() controls: string | undefined;\n\n /**\n * Controlled open state of the popup this button triggers. Holds the pressed\n * wash for the popup's rendered lifecycle. ButtonFilled has no selected state,\n * so an open popup never promotes to an active treatment.\n */\n @Prop() expanded: boolean | undefined;\n\n /** Popup type exposed to assistive technology. */\n @Prop() haspopup: ButtonFilledPopup | undefined;\n\n /**\n * This action *has* a menu: implies `aria-haspopup=\"menu\"` and adds the trailing\n * chevron that carries the affordance.\n *\n * Only `label` and `icon-label` are supported. A filled button is never the\n * overflow / more-options control — that role belongs to ButtonUnfilled with an\n * `Ellipses` glyph, which conveys the menu without a chevron.\n *\n * Use `haspopup` directly for non-menu popups.\n */\n @Prop() hasMenu: boolean = false;\n\n @Event() dsClick!: EventEmitter<MouseEvent>;\n\n private buttonEl: HTMLButtonElement | null = null;\n\n @Method()\n async setFocus() {\n this.buttonEl?.focus();\n }\n\n private handleClick = (event: MouseEvent) => {\n if (this.isInactive || this.isLoading) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n this.dsClick.emit(event);\n };\n\n private get resolvedHaspopup(): ButtonFilledPopup | undefined {\n return this.haspopup ?? (this.hasMenu ? 'menu' : undefined);\n }\n\n private get accessibleName(): string | undefined {\n if (this.ariaLabel) return this.ariaLabel;\n if (this.isLoading && this.variant === 'label' && this.label) return this.label;\n return undefined;\n }\n\n render() {\n const cls: Record<string, boolean> = {\n 'button-filled': true,\n 'ds-button': true,\n 'ds-focus-ring-inset': true,\n 'ds-control-press-scale': this.pressScale,\n 'ds-interaction-fill': !this.isInactive,\n /* Bold is the default filled contrast — on-bold interaction tokens. */\n 'ds-interaction-fill--on-bold': this.contrast === 'bold',\n 'ds-interaction-fill--on-strong': this.contrast === 'strong',\n 'ds-interaction-fill--on-medium': this.contrast === 'medium',\n /* faint → default app interaction tokens (no --on-*). */\n 'button-filled--bordered': this.hasBorder,\n 'ds-button--bordered': this.hasBorder,\n 'button-filled--expanded': this.expanded === true && !this.isInactive,\n 'ds-button--expanded': this.expanded === true && !this.isInactive,\n 'ds-control-inactive': this.isInactive,\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n 'ds-control-frame': true,\n 'button-filled--icon': this.variant === 'icon',\n 'ds-button--icon': this.variant === 'icon',\n 'button-filled--label': this.variant === 'label',\n 'button-filled--icon-label': this.variant === 'icon-label',\n 'button-filled--rounded': this.rounded,\n 'ds-button--rounded': this.rounded,\n [`button-filled--background-${this.background}`]: this.background !== undefined,\n [`button-filled--intent-${this.intent}`]: true,\n [`button-filled--contrast-${this.contrast}`]: true,\n };\n\n return (\n <Host\n class={{\n 'button-filled-host': true,\n 'button-filled-host--icon': this.variant === 'icon',\n 'ds-button-host--icon': this.variant === 'icon',\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n ...controlWidthClass(this.width),\n }}\n tabIndex={-1}\n >\n <button\n ref={el => {\n this.buttonEl = el ?? null;\n }}\n type={this.type}\n class={cls}\n disabled={this.isInactive}\n aria-label={this.accessibleName}\n aria-busy={this.isLoading ? 'true' : undefined}\n aria-disabled={this.isLoading ? 'true' : undefined}\n aria-controls={this.controls}\n aria-expanded={this.expanded === undefined ? undefined : String(this.expanded)}\n aria-haspopup={this.resolvedHaspopup}\n onPointerDown={event =>\n beginElevatedControlPress(\n event,\n this.pressScale && !this.isInactive && !this.isLoading,\n )\n }\n onClick={this.handleClick}\n >\n {renderButtonContent({\n namespace: 'button-filled',\n variant: this.variant,\n size: this.size,\n label: this.label,\n icon: this.icon,\n hasMenu: this.hasMenu,\n isLoading: this.isLoading,\n })}\n </button>\n </Host>\n );\n }\n}\n",
539
+ "content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\nimport { controlWidthClass } from '../../utils';\nimport type { ChoiceBackground } from '../../utils/choice-list';\nimport { beginElevatedControlPress } from '../../utils/control-press';\nimport { renderButtonContent } from '../../utils/button-render';\nimport type {\n ButtonPopup,\n ButtonSize,\n ButtonVariant,\n ButtonWidth,\n} from '../../utils/button-types';\n\nexport type ButtonFilledIntent =\n | 'neutral'\n | 'brand'\n | 'ai'\n | 'negative'\n | 'warning'\n | 'caution'\n | 'positive'\n | 'guide'\n | 'walkthrough';\n\nexport type ButtonFilledContrast = 'bold' | 'strong' | 'medium' | 'faint';\n\nexport type ButtonFilledBackground = ChoiceBackground;\n\nexport type ButtonFilledVariant = ButtonVariant;\nexport type ButtonFilledSize = ButtonSize;\nexport type ButtonFilledWidth = ButtonWidth;\nexport type ButtonFilledPopup = ButtonPopup;\n\n@Component({\n tag: 'ds-button-filled',\n styleUrl: 'ButtonFilled.css',\n scoped: true,\n})\nexport class ButtonFilled {\n @Element() el!: HTMLElement;\n\n /**\n * Content layout. Default is label-only; pass `icon` for icon-only chrome\n * (nav / tool rails) or `icon-label` for leading icon + text.\n */\n @Prop() variant: ButtonFilledVariant = 'label';\n\n /** Control density (height, padding, icon, type). */\n @Prop() size: ButtonFilledSize = 'md';\n\n /** Use reduced outer geometry when nested inside a control of the same size. */\n @Prop() isInset: boolean = false;\n\n /** Width fit — hug content (default) or fill the parent. */\n @Prop() width: ButtonFilledWidth = 'hug';\n\n /** Visible text for `label` / `icon-label` variants. */\n @Prop() label: string = '';\n\n /** Icon name passed to <ds-icon> for `icon` / `icon-label` variants. */\n @Prop() icon: string = '';\n\n /** Semantic colour intent. */\n @Prop() intent: ButtonFilledIntent = 'brand';\n\n /**\n * Background fill weight. Foreground uses the paired contrast token:\n * bold → faint, strong → medium, medium → strong, faint → bold.\n */\n @Prop() contrast: ButtonFilledContrast = 'bold';\n\n /** Show a 1px secondary inset border. */\n @Prop() hasBorder: boolean = false;\n\n /**\n * Actual parent surface context for the optional inset border color only.\n * Omit on primary and secondary surfaces.\n */\n @Prop() background: ButtonFilledBackground | undefined;\n\n /** Use the half-radius treatment instead of the default control radius. */\n @Prop() rounded: boolean = false;\n\n /**\n * Scale down during a physical pointer press.\n * Disable when an owning composite requires fixed child or background geometry.\n */\n @Prop() pressScale: boolean = true;\n\n /** Disables interaction. */\n @Prop() isInactive: boolean = false;\n\n /** Shows an inline loader and prevents interaction without applying inactive opacity. */\n @Prop() isLoading: boolean = false;\n\n /** Native button type. */\n @Prop() type: 'button' | 'submit' | 'reset' = 'button';\n\n /** Accessible name override. Required for icon-only buttons. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n\n /** ID of the popup this button controls. */\n @Prop() controls: string | undefined;\n\n /**\n * Controlled open state of the popup this button triggers. Holds the pressed\n * wash for the popup's rendered lifecycle. ButtonFilled has no selected state,\n * so an open popup never promotes to an active treatment.\n */\n @Prop() expanded: boolean | undefined;\n\n /** Popup type exposed to assistive technology. */\n @Prop() haspopup: ButtonFilledPopup | undefined;\n\n /**\n * This action *has* a menu: implies `aria-haspopup=\"menu\"` and adds the trailing\n * chevron that carries the affordance.\n *\n * Only `label` and `icon-label` are supported. A filled button is never the\n * overflow / more-options control — that role belongs to ButtonUnfilled with an\n * `Ellipses` glyph, which conveys the menu without a chevron.\n *\n * Use `haspopup` directly for non-menu popups.\n */\n @Prop() hasMenu: boolean = false;\n\n @Event() dsClick!: EventEmitter<MouseEvent>;\n\n private buttonEl: HTMLButtonElement | null = null;\n\n @Method()\n async setFocus() {\n this.buttonEl?.focus();\n }\n\n private handleClick = (event: MouseEvent) => {\n if (this.isInactive || this.isLoading) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n this.dsClick.emit(event);\n };\n\n private get resolvedHaspopup(): ButtonFilledPopup | undefined {\n return this.haspopup ?? (this.hasMenu ? 'menu' : undefined);\n }\n\n private get accessibleName(): string | undefined {\n if (this.ariaLabel) return this.ariaLabel;\n if (this.isLoading && this.variant === 'label' && this.label) return this.label;\n return undefined;\n }\n\n render() {\n const cls: Record<string, boolean> = {\n 'button-filled': true,\n 'ds-button': true,\n 'ds-focus-ring-inset': true,\n 'ds-control-press-scale': this.pressScale,\n 'ds-interaction-fill': !this.isInactive,\n /* Bold is the default filled contrast — on-bold interaction tokens. */\n 'ds-interaction-fill--on-bold': this.contrast === 'bold',\n 'ds-interaction-fill--on-strong': this.contrast === 'strong',\n 'ds-interaction-fill--on-medium': this.contrast === 'medium',\n /* faint → default app interaction tokens (no --on-*). */\n 'button-filled--bordered': this.hasBorder,\n 'ds-button--bordered': this.hasBorder,\n 'button-filled--expanded': this.expanded === true && !this.isInactive,\n 'ds-button--expanded': this.expanded === true && !this.isInactive,\n 'ds-control-inactive': this.isInactive,\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n 'ds-control--inset': this.isInset,\n 'ds-control-frame': true,\n 'button-filled--icon': this.variant === 'icon',\n 'ds-button--icon': this.variant === 'icon',\n 'button-filled--label': this.variant === 'label',\n 'button-filled--icon-label': this.variant === 'icon-label',\n 'button-filled--rounded': this.rounded,\n 'ds-button--rounded': this.rounded,\n [`button-filled--background-${this.background}`]: this.background !== undefined,\n [`button-filled--intent-${this.intent}`]: true,\n [`button-filled--contrast-${this.contrast}`]: true,\n };\n\n return (\n <Host\n class={{\n 'button-filled-host': true,\n 'button-filled-host--icon': this.variant === 'icon',\n 'ds-button-host--icon': this.variant === 'icon',\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n 'ds-control--inset': this.isInset,\n ...controlWidthClass(this.width),\n }}\n tabIndex={-1}\n >\n <button\n ref={el => {\n this.buttonEl = el ?? null;\n }}\n type={this.type}\n class={cls}\n disabled={this.isInactive}\n aria-label={this.accessibleName}\n aria-busy={this.isLoading ? 'true' : undefined}\n aria-disabled={this.isLoading ? 'true' : undefined}\n aria-controls={this.controls}\n aria-expanded={this.expanded === undefined ? undefined : String(this.expanded)}\n aria-haspopup={this.resolvedHaspopup}\n onPointerDown={event =>\n beginElevatedControlPress(\n event,\n this.pressScale && !this.isInactive && !this.isLoading,\n )\n }\n onClick={this.handleClick}\n >\n {renderButtonContent({\n namespace: 'button-filled',\n variant: this.variant,\n size: this.size,\n label: this.label,\n icon: this.icon,\n hasMenu: this.hasMenu,\n isLoading: this.isLoading,\n })}\n </button>\n </Host>\n );\n }\n}\n",
521
540
  "type": "registry:ui"
522
541
  }
523
542
  ]
@@ -64,6 +64,7 @@
64
64
  ],
65
65
  "states": [
66
66
  "Every size applies one complete control-density recipe across frame height, inline padding, icon zone, text-container inset, gap, and radius.",
67
+ "Set isInset only when the button is nested inside a same-size control; it reduces the outer geometry while preserving the size's icon, label inset, gap, radius, and typography.",
67
68
  "A true pressed toggle and owner-controlled isActive emphasis share the selected visual treatment, but only pressed supplies toggle semantics and dsChange.",
68
69
  "General content UI keeps the selected interaction fill and border for pressed or active emphasis.",
69
70
  "Shell, navigation, and tool-rail composites use isActive with foreground-only emphasis and no selected fill or border.",
@@ -211,6 +212,15 @@
211
212
  "mutable": false,
212
213
  "description": "Disables interaction."
213
214
  },
215
+ "isInset": {
216
+ "type": "boolean",
217
+ "resolvedType": "boolean",
218
+ "attribute": "is-inset",
219
+ "default": "false",
220
+ "required": false,
221
+ "mutable": false,
222
+ "description": "Use reduced outer geometry when nested inside a control of the same size."
223
+ },
214
224
  "isLoading": {
215
225
  "type": "boolean",
216
226
  "resolvedType": "boolean",
@@ -426,6 +436,15 @@
426
436
  "mutable": false,
427
437
  "description": "Disables interaction."
428
438
  },
439
+ "isInset": {
440
+ "type": "boolean",
441
+ "resolvedType": "boolean",
442
+ "attribute": "is-inset",
443
+ "default": "false",
444
+ "required": false,
445
+ "mutable": false,
446
+ "description": "Use reduced outer geometry when nested inside a control of the same size."
447
+ },
429
448
  "isLoading": {
430
449
  "type": "boolean",
431
450
  "resolvedType": "boolean",
@@ -578,7 +597,7 @@
578
597
  },
579
598
  {
580
599
  "path": "src/wc/components/ButtonUnfilled/ButtonUnfilled.tsx",
581
- "content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\nimport { controlWidthClass } from '../../utils';\nimport { beginElevatedControlPress } from '../../utils/control-press';\nimport { renderButtonContent } from '../../utils/button-render';\nimport type {\n ButtonPopup,\n ButtonSize,\n ButtonVariant,\n ButtonWidth,\n} from '../../utils/button-types';\n\nexport type ButtonUnfilledBackground =\n | 'faint'\n | 'medium'\n | 'bold'\n | 'strong'\n | 'translucent'\n | 'inverted'\n | 'media'\n | 'always-dark';\n\nexport type ButtonUnfilledVariant = ButtonVariant;\nexport type ButtonUnfilledSize = ButtonSize;\nexport type ButtonUnfilledWidth = ButtonWidth;\nexport type ButtonUnfilledPopup = ButtonPopup;\n\n@Component({\n tag: 'ds-button-unfilled',\n styleUrl: 'ButtonUnfilled.css',\n scoped: true,\n})\nexport class ButtonUnfilled {\n @Element() el!: HTMLElement;\n\n /**\n * Content layout. Default is label-only; pass `icon` for icon-only chrome\n * (nav / tool rails) or `icon-label` for leading icon + text.\n */\n @Prop() variant: ButtonUnfilledVariant = 'label';\n\n /** Control density (height, padding, icon, type). */\n @Prop() size: ButtonUnfilledSize = 'md';\n\n /** Width fit — hug content (default) or fill the parent. */\n @Prop() width: ButtonUnfilledWidth = 'hug';\n\n /** Visible text for `label` / `icon-label` variants. */\n @Prop() label: string = '';\n\n /** Icon name passed to <ds-icon> for `icon` / `icon-label` variants. */\n @Prop() icon: string = '';\n\n /**\n * Owner-controlled visual emphasis inside a composite. This does not add\n * toggle semantics and does not emit dsChange.\n */\n @Prop() isActive: boolean = false;\n\n /**\n * When active, render the selected interaction fill.\n * Default `true` for general UI. Shell chrome (nav / tool rails) should pass\n * `false` so selection is foreground-only (primary color, no fill).\n */\n @Prop() activeFill: boolean = true;\n\n /** Show a 1px secondary inset border. Default on; shell chrome can pass `false`. */\n @Prop() hasBorder: boolean = true;\n\n /** Use the half-radius treatment instead of the default control radius. */\n @Prop() rounded: boolean = false;\n\n /**\n * Scale down during a physical pointer press.\n * Disable when an owning composite requires fixed child or background geometry.\n */\n @Prop() pressScale: boolean = true;\n\n /** Show a notification dot at the top-right of the icon zone (icon variant only). */\n @Prop() dot: boolean = false;\n\n /** Disables interaction. */\n @Prop() isInactive: boolean = false;\n\n /** Shows an inline loader and prevents interaction without applying inactive opacity. */\n @Prop() isLoading: boolean = false;\n\n /** Native button type. */\n @Prop() type: 'button' | 'submit' | 'reset' = 'button';\n\n /** Actual parent surface context. Omit on primary and secondary surfaces. */\n @Prop() background: ButtonUnfilledBackground | undefined;\n\n /** Accessible name override. Required for icon-only buttons. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop() controls: string | undefined;\n @Prop() expanded: boolean | undefined;\n @Prop() haspopup: ButtonUnfilledPopup | undefined;\n /**\n * Controlled state for a genuine toggle button. Adds aria-pressed, promotes\n * active styling, and makes activation emit dsChange with the requested state.\n */\n @Prop() pressed: boolean | undefined;\n\n /**\n * Mark the button as a Menu trigger. Implies `aria-haspopup=\"menu\"` and covers\n * both menu-button shapes:\n *\n * - `label` / `icon-label` — the action *has* a menu; a trailing chevron carries\n * the affordance.\n * - `icon` — the button *is* a menu, i.e. the overflow / more-options control.\n * No chevron is added, so the glyph must convey it on its own; use `Ellipses`.\n *\n * Use `haspopup` directly for non-menu popups.\n */\n @Prop() hasMenu: boolean = false;\n\n /**\n * Native `tabindex` for roving keyboard groups in shell chrome.\n * Omit for the default button tab stop (`0`).\n */\n @Prop({ attribute: 'tab-index' }) focusTabIndex?: number;\n\n @Event() dsClick!: EventEmitter<MouseEvent>;\n @Event() dsChange!: EventEmitter<boolean>;\n\n private buttonEl: HTMLButtonElement | null = null;\n\n @Method()\n async setFocus() {\n this.buttonEl?.focus();\n }\n\n private handleClick = (event: MouseEvent) => {\n if (this.isInactive || this.isLoading) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n this.dsClick.emit(event);\n if (this.pressed !== undefined) this.dsChange.emit(!this.pressed);\n };\n\n private get showDot(): boolean {\n return this.variant === 'icon' && this.dot && !this.isLoading;\n }\n\n private get resolvedHaspopup(): ButtonUnfilledPopup | undefined {\n return this.haspopup ?? (this.hasMenu ? 'menu' : undefined);\n }\n\n /** An expanded popup trigger is visually active even when selection is controlled separately. */\n private get visuallyActive(): boolean {\n return this.isActive || this.pressed === true || this.expanded === true;\n }\n\n /** Knock-out ring: selected fill → active wash; otherwise surface token. */\n private get dotRing(): string {\n if ((this.isActive || this.pressed === true) && this.activeFill) {\n return 'var(--ds-interaction-active)';\n }\n return 'var(--ds-button-unfilled-dot-ring)';\n }\n\n private get accessibleName(): string | undefined {\n if (this.ariaLabel) return this.ariaLabel;\n if (this.isLoading && this.variant === 'label' && this.label) return this.label;\n return undefined;\n }\n\n render() {\n const bg = this.background;\n const cls: Record<string, boolean> = {\n 'button-unfilled': true,\n 'ds-button': true,\n 'ds-focus-ring-inset': true,\n 'ds-control-press-scale': this.pressScale,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected':\n (this.isActive || this.pressed === true) &&\n this.activeFill &&\n !this.isInactive,\n 'ds-interaction-fill--on-faint': bg === 'faint',\n 'ds-interaction-fill--on-medium': bg === 'medium',\n 'ds-interaction-fill--on-bold': bg === 'bold',\n 'ds-interaction-fill--on-strong': bg === 'strong',\n 'ds-interaction-fill--on-translucent': bg === 'translucent',\n 'ds-interaction-fill--on-inverted': bg === 'inverted',\n 'ds-interaction-fill--on-media': bg === 'media',\n 'ds-interaction-fill--on-always-dark': bg === 'always-dark',\n 'button-unfilled--active': this.visuallyActive,\n 'button-unfilled--expanded': this.expanded === true && !this.isInactive,\n 'ds-button--expanded': this.expanded === true && !this.isInactive,\n 'button-unfilled--bordered': this.hasBorder,\n 'ds-button--bordered': this.hasBorder,\n 'button-unfilled--rounded': this.rounded,\n 'ds-button--rounded': this.rounded,\n 'ds-control-inactive': this.isInactive,\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n 'ds-control-frame': true,\n 'button-unfilled--icon': this.variant === 'icon',\n 'ds-button--icon': this.variant === 'icon',\n 'button-unfilled--label': this.variant === 'label',\n 'button-unfilled--icon-label': this.variant === 'icon-label',\n 'button-unfilled--background-faint': bg === 'faint',\n 'button-unfilled--background-medium': bg === 'medium',\n 'button-unfilled--background-bold': bg === 'bold',\n 'button-unfilled--background-strong': bg === 'strong',\n 'button-unfilled--background-translucent': bg === 'translucent',\n 'button-unfilled--background-inverted': bg === 'inverted',\n 'button-unfilled--background-media': bg === 'media',\n 'button-unfilled--on-always-dark': bg === 'always-dark',\n };\n\n return (\n <Host\n class={{\n 'button-unfilled-host': true,\n 'button-unfilled-host--icon': this.variant === 'icon',\n 'ds-button-host--icon': this.variant === 'icon',\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n ...controlWidthClass(this.width),\n }}\n tabIndex={-1}\n >\n <button\n ref={el => {\n this.buttonEl = el ?? null;\n }}\n type={this.type}\n class={cls}\n disabled={this.isInactive}\n tabIndex={this.focusTabIndex ?? 0}\n aria-label={this.accessibleName}\n aria-busy={this.isLoading ? 'true' : undefined}\n aria-disabled={this.isLoading ? 'true' : undefined}\n aria-controls={this.controls}\n aria-expanded={this.expanded === undefined ? undefined : String(this.expanded)}\n aria-haspopup={this.resolvedHaspopup}\n aria-pressed={this.pressed === undefined ? undefined : String(this.pressed)}\n onPointerDown={event =>\n beginElevatedControlPress(\n event,\n this.pressScale && !this.isInactive && !this.isLoading,\n )\n }\n onClick={this.handleClick}\n >\n {renderButtonContent({\n namespace: 'button-unfilled',\n variant: this.variant,\n size: this.size,\n label: this.label,\n icon: this.icon,\n hasMenu: this.hasMenu,\n isLoading: this.isLoading,\n dot: {\n visible: this.showDot,\n background: this.dotRing,\n },\n })}\n </button>\n </Host>\n );\n }\n}\n",
600
+ "content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\nimport { controlWidthClass } from '../../utils';\nimport { beginElevatedControlPress } from '../../utils/control-press';\nimport { renderButtonContent } from '../../utils/button-render';\nimport type {\n ButtonPopup,\n ButtonSize,\n ButtonVariant,\n ButtonWidth,\n} from '../../utils/button-types';\n\nexport type ButtonUnfilledBackground =\n | 'faint'\n | 'medium'\n | 'bold'\n | 'strong'\n | 'translucent'\n | 'inverted'\n | 'media'\n | 'always-dark';\n\nexport type ButtonUnfilledVariant = ButtonVariant;\nexport type ButtonUnfilledSize = ButtonSize;\nexport type ButtonUnfilledWidth = ButtonWidth;\nexport type ButtonUnfilledPopup = ButtonPopup;\n\n@Component({\n tag: 'ds-button-unfilled',\n styleUrl: 'ButtonUnfilled.css',\n scoped: true,\n})\nexport class ButtonUnfilled {\n @Element() el!: HTMLElement;\n\n /**\n * Content layout. Default is label-only; pass `icon` for icon-only chrome\n * (nav / tool rails) or `icon-label` for leading icon + text.\n */\n @Prop() variant: ButtonUnfilledVariant = 'label';\n\n /** Control density (height, padding, icon, type). */\n @Prop() size: ButtonUnfilledSize = 'md';\n\n /** Use reduced outer geometry when nested inside a control of the same size. */\n @Prop() isInset: boolean = false;\n\n /** Width fit — hug content (default) or fill the parent. */\n @Prop() width: ButtonUnfilledWidth = 'hug';\n\n /** Visible text for `label` / `icon-label` variants. */\n @Prop() label: string = '';\n\n /** Icon name passed to <ds-icon> for `icon` / `icon-label` variants. */\n @Prop() icon: string = '';\n\n /**\n * Owner-controlled visual emphasis inside a composite. This does not add\n * toggle semantics and does not emit dsChange.\n */\n @Prop() isActive: boolean = false;\n\n /**\n * When active, render the selected interaction fill.\n * Default `true` for general UI. Shell chrome (nav / tool rails) should pass\n * `false` so selection is foreground-only (primary color, no fill).\n */\n @Prop() activeFill: boolean = true;\n\n /** Show a 1px secondary inset border. Default on; shell chrome can pass `false`. */\n @Prop() hasBorder: boolean = true;\n\n /** Use the half-radius treatment instead of the default control radius. */\n @Prop() rounded: boolean = false;\n\n /**\n * Scale down during a physical pointer press.\n * Disable when an owning composite requires fixed child or background geometry.\n */\n @Prop() pressScale: boolean = true;\n\n /** Show a notification dot at the top-right of the icon zone (icon variant only). */\n @Prop() dot: boolean = false;\n\n /** Disables interaction. */\n @Prop() isInactive: boolean = false;\n\n /** Shows an inline loader and prevents interaction without applying inactive opacity. */\n @Prop() isLoading: boolean = false;\n\n /** Native button type. */\n @Prop() type: 'button' | 'submit' | 'reset' = 'button';\n\n /** Actual parent surface context. Omit on primary and secondary surfaces. */\n @Prop() background: ButtonUnfilledBackground | undefined;\n\n /** Accessible name override. Required for icon-only buttons. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop() controls: string | undefined;\n @Prop() expanded: boolean | undefined;\n @Prop() haspopup: ButtonUnfilledPopup | undefined;\n /**\n * Controlled state for a genuine toggle button. Adds aria-pressed, promotes\n * active styling, and makes activation emit dsChange with the requested state.\n */\n @Prop() pressed: boolean | undefined;\n\n /**\n * Mark the button as a Menu trigger. Implies `aria-haspopup=\"menu\"` and covers\n * both menu-button shapes:\n *\n * - `label` / `icon-label` — the action *has* a menu; a trailing chevron carries\n * the affordance.\n * - `icon` — the button *is* a menu, i.e. the overflow / more-options control.\n * No chevron is added, so the glyph must convey it on its own; use `Ellipses`.\n *\n * Use `haspopup` directly for non-menu popups.\n */\n @Prop() hasMenu: boolean = false;\n\n /**\n * Native `tabindex` for roving keyboard groups in shell chrome.\n * Omit for the default button tab stop (`0`).\n */\n @Prop({ attribute: 'tab-index' }) focusTabIndex?: number;\n\n @Event() dsClick!: EventEmitter<MouseEvent>;\n @Event() dsChange!: EventEmitter<boolean>;\n\n private buttonEl: HTMLButtonElement | null = null;\n\n @Method()\n async setFocus() {\n this.buttonEl?.focus();\n }\n\n private handleClick = (event: MouseEvent) => {\n if (this.isInactive || this.isLoading) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n this.dsClick.emit(event);\n if (this.pressed !== undefined) this.dsChange.emit(!this.pressed);\n };\n\n private get showDot(): boolean {\n return this.variant === 'icon' && this.dot && !this.isLoading;\n }\n\n private get resolvedHaspopup(): ButtonUnfilledPopup | undefined {\n return this.haspopup ?? (this.hasMenu ? 'menu' : undefined);\n }\n\n /** An expanded popup trigger is visually active even when selection is controlled separately. */\n private get visuallyActive(): boolean {\n return this.isActive || this.pressed === true || this.expanded === true;\n }\n\n /** Knock-out ring: selected fill → active wash; otherwise surface token. */\n private get dotRing(): string {\n if ((this.isActive || this.pressed === true) && this.activeFill) {\n return 'var(--ds-interaction-active)';\n }\n return 'var(--ds-button-unfilled-dot-ring)';\n }\n\n private get accessibleName(): string | undefined {\n if (this.ariaLabel) return this.ariaLabel;\n if (this.isLoading && this.variant === 'label' && this.label) return this.label;\n return undefined;\n }\n\n render() {\n const bg = this.background;\n const cls: Record<string, boolean> = {\n 'button-unfilled': true,\n 'ds-button': true,\n 'ds-focus-ring-inset': true,\n 'ds-control-press-scale': this.pressScale,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected':\n (this.isActive || this.pressed === true) &&\n this.activeFill &&\n !this.isInactive,\n 'ds-interaction-fill--on-faint': bg === 'faint',\n 'ds-interaction-fill--on-medium': bg === 'medium',\n 'ds-interaction-fill--on-bold': bg === 'bold',\n 'ds-interaction-fill--on-strong': bg === 'strong',\n 'ds-interaction-fill--on-translucent': bg === 'translucent',\n 'ds-interaction-fill--on-inverted': bg === 'inverted',\n 'ds-interaction-fill--on-media': bg === 'media',\n 'ds-interaction-fill--on-always-dark': bg === 'always-dark',\n 'button-unfilled--active': this.visuallyActive,\n 'button-unfilled--expanded': this.expanded === true && !this.isInactive,\n 'ds-button--expanded': this.expanded === true && !this.isInactive,\n 'button-unfilled--bordered': this.hasBorder,\n 'ds-button--bordered': this.hasBorder,\n 'button-unfilled--rounded': this.rounded,\n 'ds-button--rounded': this.rounded,\n 'ds-control-inactive': this.isInactive,\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n 'ds-control--inset': this.isInset,\n 'ds-control-frame': true,\n 'button-unfilled--icon': this.variant === 'icon',\n 'ds-button--icon': this.variant === 'icon',\n 'button-unfilled--label': this.variant === 'label',\n 'button-unfilled--icon-label': this.variant === 'icon-label',\n 'button-unfilled--background-faint': bg === 'faint',\n 'button-unfilled--background-medium': bg === 'medium',\n 'button-unfilled--background-bold': bg === 'bold',\n 'button-unfilled--background-strong': bg === 'strong',\n 'button-unfilled--background-translucent': bg === 'translucent',\n 'button-unfilled--background-inverted': bg === 'inverted',\n 'button-unfilled--background-media': bg === 'media',\n 'button-unfilled--on-always-dark': bg === 'always-dark',\n };\n\n return (\n <Host\n class={{\n 'button-unfilled-host': true,\n 'button-unfilled-host--icon': this.variant === 'icon',\n 'ds-button-host--icon': this.variant === 'icon',\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n 'ds-control--inset': this.isInset,\n ...controlWidthClass(this.width),\n }}\n tabIndex={-1}\n >\n <button\n ref={el => {\n this.buttonEl = el ?? null;\n }}\n type={this.type}\n class={cls}\n disabled={this.isInactive}\n tabIndex={this.focusTabIndex ?? 0}\n aria-label={this.accessibleName}\n aria-busy={this.isLoading ? 'true' : undefined}\n aria-disabled={this.isLoading ? 'true' : undefined}\n aria-controls={this.controls}\n aria-expanded={this.expanded === undefined ? undefined : String(this.expanded)}\n aria-haspopup={this.resolvedHaspopup}\n aria-pressed={this.pressed === undefined ? undefined : String(this.pressed)}\n onPointerDown={event =>\n beginElevatedControlPress(\n event,\n this.pressScale && !this.isInactive && !this.isLoading,\n )\n }\n onClick={this.handleClick}\n >\n {renderButtonContent({\n namespace: 'button-unfilled',\n variant: this.variant,\n size: this.size,\n label: this.label,\n icon: this.icon,\n hasMenu: this.hasMenu,\n isLoading: this.isLoading,\n dot: {\n visible: this.showDot,\n background: this.dotRing,\n },\n })}\n </button>\n </Host>\n );\n }\n}\n",
582
601
  "type": "registry:ui"
583
602
  }
584
603
  ]
@@ -23,6 +23,7 @@
23
23
  ],
24
24
  "commonlyComposedWith": [
25
25
  "component:ds-mobile-section-switcher",
26
+ "component:ds-tab-group",
26
27
  "component:ds-shell-page",
27
28
  "component:ds-shell-tools"
28
29
  ],
@@ -31,19 +32,19 @@
31
32
  ],
32
33
  "accessibility": [
33
34
  "Static screens render a real h1 or h2 according to headingLevel.",
34
- "Section-switching screens expose an equivalent semantic heading alongside the accessible popup trigger.",
35
+ "Section-switching screens expose an equivalent semantic heading alongside the accessible popup trigger or named segmented TabGroup.",
35
36
  "Subsections use a separately named navigation landmark and never replace the semantic page identity.",
36
37
  "Leading and trailing slotted icon actions retain their own accessible names and tooltips."
37
38
  ],
38
39
  "states": [
39
40
  "Without multiple sections the center renders heading.",
40
- "With multiple sections the center renders the controlled MobileSectionSwitcher.",
41
+ "With multiple sections the center renders the controlled MobileSectionSwitcher by default or an explicitly requested segmented TabGroup.",
41
42
  "Optional subsections render in a second row and emit dsSubsectionChange without changing the primary section.",
42
43
  "Sections represent peer destinations at the current navigation level; subsections represent local views beneath the selected page or detail identity.",
43
44
  "Brand tone changes surface and foreground without changing geometry or interaction ownership."
44
45
  ],
45
46
  "responsiveBehavior": [
46
- "The primary 48px lane is dedicated to the below-768px presentation; optional subsections add one subordinate 48px lane.",
47
+ "The primary 56px lane matches the Mobile Sheet Nav header and Mobile Bar Nav; optional subsections add one subordinate 48px lane.",
47
48
  "Equal flexible side lanes keep the center mathematically aligned when actions are asymmetric."
48
49
  ],
49
50
  "references": [
@@ -74,8 +75,8 @@
74
75
  "description": "Semantic heading level for the active mobile screen."
75
76
  },
76
77
  "sections": {
77
- "type": "TabItem[]",
78
- "resolvedType": "TabItem[]",
78
+ "type": "TabGroupItem[]",
79
+ "resolvedType": "TabGroupItem[]",
79
80
  "default": "[]",
80
81
  "required": false,
81
82
  "mutable": false,
@@ -90,6 +91,24 @@
90
91
  "mutable": false,
91
92
  "description": "Accessible name for the section chooser."
92
93
  },
94
+ "sectionsPresentation": {
95
+ "type": "MobileHeaderSectionsPresentation",
96
+ "resolvedType": "\"segmented\" | \"switcher\"",
97
+ "attribute": "sections-presentation",
98
+ "default": "'switcher'",
99
+ "required": false,
100
+ "mutable": false,
101
+ "description": "Popup switcher or an inline segmented TabGroup for peer sections."
102
+ },
103
+ "sectionsSize": {
104
+ "type": "TabGroupSize",
105
+ "resolvedType": "\"lg\" | \"md\" | \"sm\"",
106
+ "attribute": "sections-size",
107
+ "default": "'md'",
108
+ "required": false,
109
+ "mutable": false,
110
+ "description": "Density for the segmented sections presentation."
111
+ },
93
112
  "subsections": {
94
113
  "type": "TabItem[]",
95
114
  "resolvedType": "TabItem[]",
@@ -176,8 +195,8 @@
176
195
  "description": "Semantic heading level for the active mobile screen."
177
196
  },
178
197
  "sections": {
179
- "type": "TabItem[]",
180
- "resolvedType": "TabItem[]",
198
+ "type": "TabGroupItem[]",
199
+ "resolvedType": "TabGroupItem[]",
181
200
  "default": "[]",
182
201
  "required": false,
183
202
  "mutable": false,
@@ -192,6 +211,24 @@
192
211
  "mutable": false,
193
212
  "description": "Accessible name for the section chooser."
194
213
  },
214
+ "sectionsPresentation": {
215
+ "type": "MobileHeaderSectionsPresentation",
216
+ "resolvedType": "\"segmented\" | \"switcher\"",
217
+ "attribute": "sections-presentation",
218
+ "default": "'switcher'",
219
+ "required": false,
220
+ "mutable": false,
221
+ "description": "Popup switcher or an inline segmented TabGroup for peer sections."
222
+ },
223
+ "sectionsSize": {
224
+ "type": "TabGroupSize",
225
+ "resolvedType": "\"lg\" | \"md\" | \"sm\"",
226
+ "attribute": "sections-size",
227
+ "default": "'md'",
228
+ "required": false,
229
+ "mutable": false,
230
+ "description": "Density for the segmented sections presentation."
231
+ },
195
232
  "subsections": {
196
233
  "type": "TabItem[]",
197
234
  "resolvedType": "TabItem[]",
@@ -293,22 +330,23 @@
293
330
  ],
294
331
  "registryDependencies": [
295
332
  "mobile-section-switcher",
333
+ "tab-group",
296
334
  "text"
297
335
  ],
298
336
  "files": [
299
337
  {
300
338
  "path": "src/wc/components/MobileHeader/MobileHeader.css",
301
- "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: var(--dimension-size-600);\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",
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",
302
340
  "type": "registry:ui"
303
341
  },
304
342
  {
305
343
  "path": "src/wc/components/MobileHeader/MobileHeader.tsx",
306
- "content": "import { Component, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\nimport { getSelectableTabs, type TabItem } from '../TabGroup/tab-item-utils';\nimport type {\n MobileHeaderHeadingLevel,\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: TabItem[] = [];\n /** Controlled selected section id. */\n @Prop() value: string = '';\n /** Accessible name for the section chooser. */\n @Prop() sectionsAriaLabel: string = 'Change page section';\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(): TabItem[] {\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 class=\"mobile-header__primary ds-chrome-grid ds-chrome-space--md\">\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 <ds-mobile-section-switcher\n sections={this.resolvedSections}\n value={this.value}\n navigationLabel={this.sectionsAriaLabel}\n onDsChange={this.handleSectionChange}\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",
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 class=\"mobile-header__primary ds-chrome-grid ds-chrome-space--md\">\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 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",
307
345
  "type": "registry:ui"
308
346
  },
309
347
  {
310
348
  "path": "src/wc/components/MobileHeader/mobile-header-types.ts",
311
- "content": "import type { TabItem } from '../TabGroup/tab-item-utils';\n\nexport type MobileHeaderTone = 'default' | 'brand';\nexport type MobileHeaderHeadingLevel = 'h1' | 'h2';\nexport type MobileHeaderSectionItem = TabItem;\n\n",
349
+ "content": "import type { TabGroupItem } from '../TabGroup/tab-item-utils';\n\nexport type MobileHeaderTone = 'default' | 'brand';\nexport type MobileHeaderHeadingLevel = 'h1' | 'h2';\nexport type MobileHeaderSectionsPresentation = 'switcher' | 'segmented';\nexport type MobileHeaderSectionItem = TabGroupItem;\n",
312
350
  "type": "registry:ui"
313
351
  }
314
352
  ]
@@ -39,7 +39,7 @@
39
39
  ],
40
40
  "responsiveBehavior": [
41
41
  "The pane fills the mobile content stage while the bottom bar remains available.",
42
- "The sheet header and scrollable navigation body each use the shared 8px chrome inset; large destination controls retain their independent 40px density.",
42
+ "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.",
43
43
  "Dashboard and Settings preserve their supplied visual groups with an 8px gap between destinations inside a section and a 32px gap between sections."
44
44
  ],
45
45
  "references": [
@@ -312,7 +312,7 @@
312
312
  },
313
313
  {
314
314
  "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 class=\"mobile-sheet-nav__header ds-chrome-grid ds-chrome-space--md\">\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 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.renderHeaderDestination('account', 'Avatar', this.accountLabel)}\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",
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 class=\"mobile-sheet-nav__header ds-chrome-grid ds-chrome-space--md\">\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 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.renderHeaderDestination('account', 'Avatar', this.accountLabel)}\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
316
  "type": "registry:ui"
317
317
  }
318
318
  ]