@ds-mo/ui 13.4.0 → 13.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.package-ready.json +1 -1
- package/dist/agent-patterns.json +8 -1
- package/dist/agent.json +187 -26
- package/dist/angular/ds-banner.d.ts +23 -0
- package/dist/angular/ds-banner.js +45 -0
- package/dist/angular/ds-modal.d.ts +1 -1
- package/dist/angular/ds-modal.js +3 -3
- package/dist/angular/index.d.ts +1 -1
- package/dist/angular/index.js +1 -0
- package/dist/angular/proxies.d.ts +1 -0
- package/dist/angular/proxies.js +1 -0
- package/dist/components/ds-agent-activity.js +1 -1
- package/dist/components/ds-agent-response.js +1 -1
- package/dist/components/ds-agent-source-list.js +1 -1
- package/dist/components/ds-agent-tool-call.js +1 -1
- package/dist/components/ds-attachment-list.js +1 -1
- package/dist/components/ds-badge.js +1 -1
- package/dist/components/ds-banner.d.ts +11 -0
- package/dist/components/ds-banner.js +2 -0
- package/dist/components/ds-banner.js.map +1 -0
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-title.js +1 -1
- package/dist/components/ds-bar-workflow.js +1 -1
- package/dist/components/ds-breadcrumb.js +1 -1
- package/dist/components/ds-button-filled.js +1 -1
- package/dist/components/ds-button-unfilled.js +1 -1
- package/dist/components/ds-card-data-viz.js +1 -1
- package/dist/components/ds-card-overview.js +1 -1
- package/dist/components/ds-card-overview.js.map +1 -1
- package/dist/components/ds-card-setting.js +1 -1
- package/dist/components/ds-card-setting.js.map +1 -1
- package/dist/components/ds-chart-bar.js +1 -1
- package/dist/components/ds-chart-donut.js +1 -1
- package/dist/components/ds-chart-legend.js +1 -1
- package/dist/components/ds-chart-line.js +1 -1
- package/dist/components/ds-checkbox.js +1 -1
- package/dist/components/ds-chip.js +1 -1
- package/dist/components/ds-code-block.js +1 -1
- package/dist/components/ds-conversation-list-item.js +1 -1
- package/dist/components/ds-conversation-list-section.js +1 -1
- package/dist/components/ds-conversation-list.js +1 -1
- package/dist/components/ds-divider.js +1 -1
- package/dist/components/ds-empty-state.js +1 -1
- package/dist/components/ds-field.js +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-input.js.map +1 -1
- package/dist/components/ds-markdown.js +1 -1
- package/dist/components/ds-menu.js +1 -1
- package/dist/components/ds-message-actions.js +1 -1
- package/dist/components/ds-message-actions.js.map +1 -1
- package/dist/components/ds-message-bubble.js +1 -1
- package/dist/components/ds-message-composer.js +1 -1
- package/dist/components/ds-message-scroller.js +1 -1
- package/dist/components/ds-message.js +1 -1
- package/dist/components/ds-mobile-bar-nav.js +1 -1
- package/dist/components/ds-mobile-header.js +1 -1
- package/dist/components/ds-mobile-section-switcher.js +1 -1
- package/dist/components/ds-mobile-sheet-nav.js +1 -1
- package/dist/components/ds-modal.js +1 -1
- package/dist/components/ds-modal.js.map +1 -1
- package/dist/components/ds-panel-nav.js +1 -1
- package/dist/components/ds-panel-sub-nav.js +1 -1
- package/dist/components/ds-panel-tool-header.js +1 -1
- package/dist/components/ds-panel-tool-search.js +1 -1
- package/dist/components/ds-panel-tools.js +1 -1
- package/dist/components/ds-radio.js +1 -1
- package/dist/components/ds-scroll-overlay.js +1 -1
- package/dist/components/ds-scroll-overlay.js.map +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-select.js.map +1 -1
- package/dist/components/ds-shell-app.js +1 -1
- package/dist/components/ds-shell-app.js.map +1 -1
- package/dist/components/ds-shell-page.js +1 -1
- package/dist/components/ds-shell-tools.js +1 -1
- package/dist/components/ds-skeleton.js +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-swatch-picker.js +1 -1
- package/dist/components/ds-switch.js +1 -1
- package/dist/components/ds-tab-group.js +1 -1
- package/dist/components/ds-tag.js +1 -1
- package/dist/components/ds-text.js +1 -1
- package/dist/components/ds-toast.js +1 -1
- package/dist/components/ds-tooltip-data-viz.js +1 -1
- package/dist/components/ds-tooltip.js +1 -1
- package/dist/components/ds-typing-indicator.js +1 -1
- package/dist/components/{p-BN9FZESH.js → p-1S9AW2MA.js} +2 -2
- package/dist/components/{p-D4bVE7e4.js → p-6B411hQA.js} +2 -2
- package/dist/components/{p-D5PN6zdD.js → p-B-kRByho.js} +2 -2
- package/dist/components/{p-CmUGA_q4.js → p-B4_0Ys4w.js} +2 -2
- package/dist/components/{p-BApNi_xA.js → p-B6RrZQqw.js} +2 -2
- package/dist/components/{p-CQ4vAZ9j.js → p-BBtjABBS.js} +2 -2
- package/dist/components/{p-CmMfRM1s.js → p-BFfHIu6y.js} +2 -2
- package/dist/components/{p-CBiAsFAZ.js → p-BPyymeuR.js} +2 -2
- package/dist/components/{p-Bgh0uG51.js → p-BR_oMaas.js} +2 -2
- package/dist/components/{p-w2kKeG_h.js → p-Bb4f5SuF.js} +2 -2
- package/dist/components/p-Bl6A5AJq.js +2 -0
- package/dist/components/{p-CIgag7g2.js.map → p-Bl6A5AJq.js.map} +1 -1
- package/dist/components/{p-B5snQKiP.js → p-BmNpPyzf.js} +2 -2
- package/dist/components/{p-CWItD0gL.js → p-Bmo4T96l.js} +2 -2
- package/dist/components/{p-DHAuidt1.js → p-Bsfpxto1.js} +2 -2
- package/dist/components/{p-a6cnCKWk.js → p-BtuYufL3.js} +2 -2
- package/dist/components/{p-BxEOlO5k.js → p-CKRJX0Y0.js} +2 -2
- package/dist/components/{p-1moOgMI-.js → p-Ca6s5fLR.js} +2 -2
- package/dist/components/{p-RT1mts9U.js → p-CbjFUpOj.js} +2 -2
- package/dist/components/{p-CqOswTy8.js → p-CuJ3J6OJ.js} +2 -2
- package/dist/components/{p-BQTJ7ePr.js → p-CxgMUvqg.js} +2 -2
- package/dist/components/{p-CWKrq35a.js → p-D0bM3cKj.js} +2 -2
- package/dist/components/{p-UtfErnpX.js → p-D2cjoj2r.js} +2 -2
- package/dist/components/{p-CGwVooB6.js → p-DHF5cLzr.js} +2 -2
- package/dist/components/{p-BgjtMMye.js → p-DVuxzrCC.js} +2 -2
- package/dist/components/p-DX4c3kZ1.js +2 -0
- package/dist/components/p-DX4c3kZ1.js.map +1 -0
- package/dist/components/p-DaqFuVKZ.js +2 -0
- package/dist/components/p-DaqFuVKZ.js.map +1 -0
- package/dist/components/{p-DXGKfXTh.js → p-DfvDNcoq.js} +2 -2
- package/dist/components/p-Di81y9Q2.js +2 -0
- package/dist/components/{p-C_3mZMFK.js → p-Eep5mxtv.js} +2 -2
- package/dist/components/{p-DoNdiEly.js → p-IYy-jODg.js} +2 -2
- package/dist/components/{p-DtV263qO.js → p-iCjUWcp7.js} +2 -2
- package/dist/components/{p-BgASe-BH.js → p-og48Kh2i.js} +2 -2
- package/dist/components/{p-BSocHH2X.js → p-wOOAzVMC.js} +2 -2
- package/dist/docs/components.json +445 -16
- package/dist/mcp-data/registry/agent-activity.json +1 -1
- package/dist/mcp-data/registry/agent-response.json +1 -1
- package/dist/mcp-data/registry/agent-source-list.json +1 -1
- package/dist/mcp-data/registry/agent-tool-call.json +1 -1
- package/dist/mcp-data/registry/attachment-list.json +1 -1
- package/dist/mcp-data/registry/avatar.json +1 -1
- package/dist/mcp-data/registry/badge.json +1 -1
- package/dist/mcp-data/registry/banner.json +333 -0
- package/dist/mcp-data/registry/bar-nav.json +1 -1
- package/dist/mcp-data/registry/bar-title.json +4 -4
- package/dist/mcp-data/registry/bar-workflow.json +1 -1
- package/dist/mcp-data/registry/breadcrumb.json +1 -1
- package/dist/mcp-data/registry/button-filled.json +1 -1
- package/dist/mcp-data/registry/button-unfilled.json +1 -1
- package/dist/mcp-data/registry/card-data-viz.json +1 -1
- package/dist/mcp-data/registry/card-overview.json +33 -35
- package/dist/mcp-data/registry/card-setting.json +1 -1
- package/dist/mcp-data/registry/chart-bar.json +1 -1
- package/dist/mcp-data/registry/chart-donut.json +1 -1
- package/dist/mcp-data/registry/chart-legend.json +1 -1
- package/dist/mcp-data/registry/chart-line.json +1 -1
- package/dist/mcp-data/registry/checkbox.json +1 -1
- package/dist/mcp-data/registry/chip.json +1 -1
- package/dist/mcp-data/registry/code-block.json +1 -1
- package/dist/mcp-data/registry/conversation-list-item.json +1 -1
- package/dist/mcp-data/registry/conversation-list-section.json +1 -1
- package/dist/mcp-data/registry/conversation-list.json +1 -1
- package/dist/mcp-data/registry/divider.json +1 -1
- package/dist/mcp-data/registry/empty-state.json +1 -1
- package/dist/mcp-data/registry/field.json +1 -1
- package/dist/mcp-data/registry/icon.json +1 -1
- package/dist/mcp-data/registry/input.json +6 -6
- package/dist/mcp-data/registry/loader.json +1 -1
- package/dist/mcp-data/registry/markdown.json +1 -1
- package/dist/mcp-data/registry/menu.json +1 -1
- package/dist/mcp-data/registry/message-actions.json +3 -2
- package/dist/mcp-data/registry/message-bubble.json +1 -1
- package/dist/mcp-data/registry/message-composer.json +1 -1
- package/dist/mcp-data/registry/message-scroller.json +1 -1
- package/dist/mcp-data/registry/message.json +1 -1
- package/dist/mcp-data/registry/mobile-bar-nav.json +1 -1
- package/dist/mcp-data/registry/mobile-header.json +1 -1
- package/dist/mcp-data/registry/mobile-section-switcher.json +1 -1
- package/dist/mcp-data/registry/mobile-sheet-nav.json +1 -1
- package/dist/mcp-data/registry/modal.json +26 -6
- package/dist/mcp-data/registry/panel-nav.json +1 -1
- package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
- package/dist/mcp-data/registry/panel-tool-header.json +4 -4
- package/dist/mcp-data/registry/panel-tool-search.json +1 -1
- package/dist/mcp-data/registry/panel-tools.json +4 -3
- package/dist/mcp-data/registry/radio.json +1 -1
- package/dist/mcp-data/registry/registry.json +190 -26
- package/dist/mcp-data/registry/scroll-overlay.json +1 -1
- package/dist/mcp-data/registry/select.json +5 -5
- package/dist/mcp-data/registry/shell-app.json +7 -3
- package/dist/mcp-data/registry/shell-page.json +1 -1
- package/dist/mcp-data/registry/shell-tools.json +1 -1
- package/dist/mcp-data/registry/skeleton.json +1 -1
- package/dist/mcp-data/registry/slider.json +1 -1
- package/dist/mcp-data/registry/swatch-picker.json +1 -1
- package/dist/mcp-data/registry/switch.json +1 -1
- package/dist/mcp-data/registry/tab-group.json +1 -1
- package/dist/mcp-data/registry/tag.json +1 -1
- package/dist/mcp-data/registry/text.json +1 -1
- package/dist/mcp-data/registry/toast.json +1 -1
- package/dist/mcp-data/registry/tooltip-data-viz.json +1 -1
- package/dist/mcp-data/registry/tooltip.json +1 -1
- package/dist/mcp-data/registry/typing-indicator.json +1 -1
- package/dist/react/components.d.ts +1 -0
- package/dist/react/components.js +1 -0
- package/dist/react/ds-banner.d.ts +13 -0
- package/dist/react/ds-banner.js +15 -0
- package/dist/types/components/Banner/Banner.d.ts +50 -0
- package/dist/types/components/CardOverview/CardOverview.d.ts +10 -9
- package/dist/types/components/CardOverview/card-overview-controller.d.ts +13 -1
- package/dist/types/components/CardOverview/card-overview-types.d.ts +8 -2
- package/dist/types/components/CardOverview/index.d.ts +1 -1
- package/dist/types/components/Input/Input.d.ts +1 -1
- package/dist/types/components/Modal/Modal.d.ts +4 -0
- package/dist/types/components/PanelTools/PanelTools.d.ts +7 -0
- package/dist/types/components/Select/Select.d.ts +1 -1
- package/dist/types/components.d.ts +154 -16
- package/package.json +3 -3
- package/dist/components/p-4scN0Mgn.js +0 -2
- package/dist/components/p-B3PdrMP0.js +0 -2
- package/dist/components/p-B3PdrMP0.js.map +0 -1
- package/dist/components/p-CIgag7g2.js +0 -2
- package/dist/components/p-CUSz0JFa.js +0 -2
- package/dist/components/p-CUSz0JFa.js.map +0 -1
- /package/dist/components/{p-BN9FZESH.js.map → p-1S9AW2MA.js.map} +0 -0
- /package/dist/components/{p-D4bVE7e4.js.map → p-6B411hQA.js.map} +0 -0
- /package/dist/components/{p-D5PN6zdD.js.map → p-B-kRByho.js.map} +0 -0
- /package/dist/components/{p-CmUGA_q4.js.map → p-B4_0Ys4w.js.map} +0 -0
- /package/dist/components/{p-BApNi_xA.js.map → p-B6RrZQqw.js.map} +0 -0
- /package/dist/components/{p-CQ4vAZ9j.js.map → p-BBtjABBS.js.map} +0 -0
- /package/dist/components/{p-CmMfRM1s.js.map → p-BFfHIu6y.js.map} +0 -0
- /package/dist/components/{p-CBiAsFAZ.js.map → p-BPyymeuR.js.map} +0 -0
- /package/dist/components/{p-Bgh0uG51.js.map → p-BR_oMaas.js.map} +0 -0
- /package/dist/components/{p-w2kKeG_h.js.map → p-Bb4f5SuF.js.map} +0 -0
- /package/dist/components/{p-B5snQKiP.js.map → p-BmNpPyzf.js.map} +0 -0
- /package/dist/components/{p-CWItD0gL.js.map → p-Bmo4T96l.js.map} +0 -0
- /package/dist/components/{p-DHAuidt1.js.map → p-Bsfpxto1.js.map} +0 -0
- /package/dist/components/{p-a6cnCKWk.js.map → p-BtuYufL3.js.map} +0 -0
- /package/dist/components/{p-BxEOlO5k.js.map → p-CKRJX0Y0.js.map} +0 -0
- /package/dist/components/{p-1moOgMI-.js.map → p-Ca6s5fLR.js.map} +0 -0
- /package/dist/components/{p-RT1mts9U.js.map → p-CbjFUpOj.js.map} +0 -0
- /package/dist/components/{p-CqOswTy8.js.map → p-CuJ3J6OJ.js.map} +0 -0
- /package/dist/components/{p-BQTJ7ePr.js.map → p-CxgMUvqg.js.map} +0 -0
- /package/dist/components/{p-CWKrq35a.js.map → p-D0bM3cKj.js.map} +0 -0
- /package/dist/components/{p-UtfErnpX.js.map → p-D2cjoj2r.js.map} +0 -0
- /package/dist/components/{p-CGwVooB6.js.map → p-DHF5cLzr.js.map} +0 -0
- /package/dist/components/{p-BgjtMMye.js.map → p-DVuxzrCC.js.map} +0 -0
- /package/dist/components/{p-DXGKfXTh.js.map → p-DfvDNcoq.js.map} +0 -0
- /package/dist/components/{p-4scN0Mgn.js.map → p-Di81y9Q2.js.map} +0 -0
- /package/dist/components/{p-C_3mZMFK.js.map → p-Eep5mxtv.js.map} +0 -0
- /package/dist/components/{p-DoNdiEly.js.map → p-IYy-jODg.js.map} +0 -0
- /package/dist/components/{p-DtV263qO.js.map → p-iCjUWcp7.js.map} +0 -0
- /package/dist/components/{p-BgASe-BH.js.map → p-og48Kh2i.js.map} +0 -0
- /package/dist/components/{p-BSocHH2X.js.map → p-wOOAzVMC.js.map} +0 -0
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
],
|
|
56
56
|
"states": [
|
|
57
57
|
"Every size applies one complete control-density recipe across frame height, inline padding, icon zones, text-container inset, gap, and radius; supporting error content follows the shared 4px field flow.",
|
|
58
|
-
"The normal field boundary uses the shared secondary inset border. Focus promotes it to the thicker bold-brand inset stroke, while error uses the thicker bold-negative inset stroke without changing geometry.",
|
|
58
|
+
"The normal field boundary uses the shared secondary inset border. Focus promotes it to the thicker bold-brand inset stroke, while error uses the thicker bold-negative inset stroke without changing geometry. Set hasBorder to false only in chrome that supplies enough surrounding structure; the control then remains borderless at rest, while focused, and while invalid, with semantics and supporting error copy unchanged.",
|
|
59
59
|
"Placeholder text is secondary at rest and quaternary while the editable field is focused because it becomes decorative once typing begins.",
|
|
60
60
|
"Entered values use the parent surface's primary foreground; prefix icons, suffix content, and resting placeholder text use its secondary foreground.",
|
|
61
61
|
"Search fields show a standard borderless CrossCircle icon action only while they contain a value. Clearing emits the empty value and restores focus to the native field.",
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
"default": "true",
|
|
187
187
|
"required": false,
|
|
188
188
|
"mutable": false,
|
|
189
|
-
"description": "Show the standard inset border."
|
|
189
|
+
"description": "Show the standard inset border, including focused and invalid strokes."
|
|
190
190
|
},
|
|
191
191
|
"icon": {
|
|
192
192
|
"type": "string | undefined",
|
|
@@ -412,7 +412,7 @@
|
|
|
412
412
|
"default": "true",
|
|
413
413
|
"required": false,
|
|
414
414
|
"mutable": false,
|
|
415
|
-
"description": "Show the standard inset border."
|
|
415
|
+
"description": "Show the standard inset border, including focused and invalid strokes."
|
|
416
416
|
},
|
|
417
417
|
"icon": {
|
|
418
418
|
"type": "string | undefined",
|
|
@@ -566,7 +566,7 @@
|
|
|
566
566
|
},
|
|
567
567
|
"peerDependencies": {
|
|
568
568
|
"required": [
|
|
569
|
-
"@ds-mo/tokens ^5.1.
|
|
569
|
+
"@ds-mo/tokens ^5.1.1",
|
|
570
570
|
"@ds-mo/icons ^6.4.0"
|
|
571
571
|
],
|
|
572
572
|
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -586,12 +586,12 @@
|
|
|
586
586
|
"files": [
|
|
587
587
|
{
|
|
588
588
|
"path": "src/wc/components/Input/Input.css",
|
|
589
|
-
"content": "@import '../../utils/interaction-fill.css';\n@import '../../utils/control-inactive.css';\n@import '../../utils/control-density.css';\n@import '../../utils/control-parts.css';\n@import '../../utils/control-width.css';\n@import '../../utils/field-stack.css';\n@import '../../utils/typography.css';\n@import '../../utils/forced-colors.css';\n\n:host(.input-host) {\n display: inline-flex;\n --ds-input-fg: var(--color-foreground-primary);\n --ds-input-placeholder: var(--color-foreground-secondary);\n --ds-input-placeholder-active: var(--color-foreground-quaternary);\n --ds-input-adornment-fg: var(--color-foreground-secondary);\n --ds-input-border: var(--color-border-secondary);\n}\n\n:host(.ds-control-width--fill),\n:host(.ds-control-width--fill) .input-control {\n width: 100%;\n}\n\n.input-control {\n overflow: hidden;\n background: transparent;\n border: none;\n border-radius: var(--ds-control-radius, var(--dimension-radius-025));\n color: var(--ds-input-fg);\n}\n\n.input-control--bordered {\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--ds-input-border);\n}\n\n.input-control--error {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-negative);\n}\n\n.input-control:focus-within:not(.input-control--error) {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-brand);\n}\n\n:host(.ds-control-inactive) .input-control::after {\n background: transparent;\n}\n\n.native-input {\n position: relative;\n z-index: 2;\n flex: 1 1 auto;\n min-width: 0;\n height: 100%;\n background: transparent;\n border: none;\n border-radius: inherit;\n outline: none;\n color: var(--ds-input-fg);\n font-family: inherit;\n -webkit-appearance: none;\n appearance: none;\n}\n\n.native-input:disabled {\n cursor: not-allowed;\n}\n\n.native-input:read-only:not(:disabled) {\n cursor: default;\n}\n\n.native-input::placeholder {\n color: var(--ds-input-placeholder);\n}\n\n.native-input:focus::placeholder {\n color: var(--ds-input-placeholder-active);\n}\n\n.native-input[type='search']::-webkit-search-cancel-button,\n.native-input[type='search']::-webkit-search-decoration {\n display: none;\n}\n\n.input-control__prefix,\n.input-control__suffix {\n position: relative;\n z-index: 2;\n color: var(--ds-input-adornment-fg);\n}\n\n.input-control__suffix--empty {\n display: none;\n}\n\n.input-control__clear {\n position: relative;\n z-index: 2;\n flex-shrink: 0;\n align-self: center;\n}\n\n.input-control.ds-control--lg:has(.input-control__clear),\n.input-control.ds-control--md:has(.input-control__clear),\n.input-control.ds-control--sm:has(.input-control__clear) {\n padding-right: var(--dimension-space-050);\n}\n\n.input-control.ds-control--xs:has(.input-control__clear) {\n padding-right: 0;\n}\n\n.error-text {\n width: 100%;\n}\n\n@media (forced-colors: active) {\n .input-control:focus-within:not(.input-control--error)::after {\n outline-color: var(--ds-forced-color-selected);\n }\n\n .input-control--error::after {\n outline-style: dashed;\n }\n}\n",
|
|
589
|
+
"content": "@import '../../utils/interaction-fill.css';\n@import '../../utils/control-inactive.css';\n@import '../../utils/control-density.css';\n@import '../../utils/control-parts.css';\n@import '../../utils/control-width.css';\n@import '../../utils/field-stack.css';\n@import '../../utils/typography.css';\n@import '../../utils/forced-colors.css';\n\n:host(.input-host) {\n display: inline-flex;\n --ds-input-fg: var(--color-foreground-primary);\n --ds-input-placeholder: var(--color-foreground-secondary);\n --ds-input-placeholder-active: var(--color-foreground-quaternary);\n --ds-input-adornment-fg: var(--color-foreground-secondary);\n --ds-input-border: var(--color-border-secondary);\n}\n\n:host(.ds-control-width--fill),\n:host(.ds-control-width--fill) .input-control {\n width: 100%;\n}\n\n.input-control {\n overflow: hidden;\n background: transparent;\n border: none;\n border-radius: var(--ds-control-radius, var(--dimension-radius-025));\n color: var(--ds-input-fg);\n}\n\n.input-control--bordered {\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--ds-input-border);\n}\n\n.input-control--error {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-negative);\n}\n\n.input-control--bordered:focus-within:not(.input-control--error) {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-brand);\n}\n\n:host(.ds-control-inactive) .input-control::after {\n background: transparent;\n}\n\n.native-input {\n position: relative;\n z-index: 2;\n flex: 1 1 auto;\n min-width: 0;\n height: 100%;\n background: transparent;\n border: none;\n border-radius: inherit;\n outline: none;\n color: var(--ds-input-fg);\n font-family: inherit;\n -webkit-appearance: none;\n appearance: none;\n}\n\n.native-input:disabled {\n cursor: not-allowed;\n}\n\n.native-input:read-only:not(:disabled) {\n cursor: default;\n}\n\n.native-input::placeholder {\n color: var(--ds-input-placeholder);\n}\n\n.native-input:focus::placeholder {\n color: var(--ds-input-placeholder-active);\n}\n\n.native-input[type='search']::-webkit-search-cancel-button,\n.native-input[type='search']::-webkit-search-decoration {\n display: none;\n}\n\n.input-control__prefix,\n.input-control__suffix {\n position: relative;\n z-index: 2;\n color: var(--ds-input-adornment-fg);\n}\n\n.input-control__suffix--empty {\n display: none;\n}\n\n.input-control__clear {\n position: relative;\n z-index: 2;\n flex-shrink: 0;\n align-self: center;\n}\n\n.input-control.ds-control--lg:has(.input-control__clear),\n.input-control.ds-control--md:has(.input-control__clear),\n.input-control.ds-control--sm:has(.input-control__clear) {\n padding-right: var(--dimension-space-050);\n}\n\n.input-control.ds-control--xs:has(.input-control__clear) {\n padding-right: 0;\n}\n\n.error-text {\n width: 100%;\n}\n\n@media (forced-colors: active) {\n .input-control--bordered:focus-within:not(.input-control--error)::after {\n outline-color: var(--ds-forced-color-selected);\n }\n\n .input-control--error::after {\n outline-style: dashed;\n }\n}\n",
|
|
590
590
|
"type": "registry:ui"
|
|
591
591
|
},
|
|
592
592
|
{
|
|
593
593
|
"path": "src/wc/components/Input/Input.tsx",
|
|
594
|
-
"content": "import { AttachInternals, Component, Prop, State, Event, EventEmitter, Element, Method, Watch, h, Host } from '@stencil/core';\nimport {\n controlWidthClass,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n restoreStringFormState,\n setFormControlValue,\n setRequiredValidity,\n type ControlWidth,\n} from '../../utils';\n\nexport type InputType = 'text' | 'email' | 'tel' | 'url' | 'search' | 'password';\nexport type InputSize = 'lg' | 'md' | 'sm' | 'xs';\nexport type InputWidth = ControlWidth;\n\nconst ICON_SIZE: Record<InputSize, 'lg' | 'md' | 'sm' | 'xs'> = {\n lg: 'lg',\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\nconst CLEAR_BUTTON_SIZE: Record<InputSize, 'md' | 'sm' | 'xs'> = {\n lg: 'md',\n md: 'sm',\n sm: 'xs',\n xs: 'xs',\n};\n\nlet idCounter = 0;\n\n@Component({\n tag: 'ds-input',\n styleUrl: 'Input.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Input {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n private generatedId = `ds-input-${++idCounter}`;\n private errorId = `${this.generatedId}-error`;\n\n @Prop({ mutable: true }) value: string = '';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Keeps the value focusable and submittable while preventing edits. */\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() clearLabel: string = 'Clear';\n @Prop() placeholder: string | undefined;\n @Prop() type: InputType = 'text';\n /** Native browser autofill hint. */\n @Prop({ attribute: 'autocomplete' }) autoComplete: string | undefined;\n /** Preferred virtual keyboard without changing the value semantics. */\n @Prop({ attribute: 'inputmode' }) inputMode: string = '';\n /** Preferred virtual-keyboard action label. */\n @Prop({ attribute: 'enterkeyhint' }) enterKeyHint: string = '';\n /** Control density. */\n @Prop() size: InputSize = 'md';\n /** Width fit — fill the parent (default) or hug the available content. */\n @Prop() width: InputWidth = 'fill';\n /** Show the standard inset border. */\n @Prop() hasBorder: boolean = true;\n /** Optional leading icon name. */\n @Prop() icon: string | undefined;\n @Prop() isInactive: boolean = false;\n @Prop() autoFocus: boolean = false;\n @Prop() error: boolean = false;\n @Prop() errorMessage: string | undefined;\n /** Associates the internal input with an external <label>. */\n @Prop() inputId: string | undefined;\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n\n @Event() dsChange!: EventEmitter<string>;\n @Event() dsClear!: EventEmitter<void>;\n\n private initialValue = '';\n @State() private formDisabled = false;\n @State() private hasSuffix = false;\n @State() private focused = false;\n @State() private touched = false;\n\n componentWillLoad() {\n this.initialValue = this.value;\n this.hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n this.syncFormValue();\n }\n\n componentDidRender() {\n const hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n if (hasSuffix !== this.hasSuffix) this.hasSuffix = hasSuffix;\n }\n\n @Watch('value')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n syncFormValue() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n setFormControlValue(this.internals, this.value, { inactive });\n const missing = this.required && !inactive && this.value.length === 0;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.initialValue;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.value = restoreStringFormState(state);\n }\n\n @Method()\n async setFocus() {\n this.el.querySelector('input')?.focus();\n }\n\n private handleInput = (e: Event) => {\n this.value = (e.target as HTMLInputElement).value;\n this.dsChange.emit(this.value);\n };\n\n private handleFocus = () => {\n this.focused = true;\n };\n\n private handleBlur = () => {\n this.focused = false;\n this.touched = true;\n };\n\n private handleClear = () => {\n this.value = '';\n this.dsChange.emit('');\n this.dsClear.emit();\n this.el.querySelector('input')?.focus();\n };\n\n render() {\n const inputId = this.inputId ?? this.generatedId;\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const filled = this.value.length > 0;\n const dirty = this.value !== this.initialValue;\n const showClear = this.type === 'search' && filled && !inactive && !this.readOnly;\n const showError = this.error && Boolean(this.errorMessage);\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n\n const describedBy = [\n this.ariaDescribedby,\n showError ? this.errorId : undefined,\n ].filter(Boolean).join(' ') || undefined;\n\n return (\n <Host\n class={{\n 'input-host': true,\n 'ds-field-stack': true,\n 'ds-control-inactive': inactive,\n [`ds-control--${this.size}`]: true,\n ...controlWidthClass(this.width),\n }}\n data-disabled={inactive ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-required={this.required ? '' : undefined}\n data-invalid={this.error ? '' : undefined}\n data-filled={filled ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n >\n <div\n class={{\n 'input-control': true,\n 'ds-control-frame': true,\n 'input-control--bordered': this.hasBorder
|
|
594
|
+
"content": "import { AttachInternals, Component, Prop, State, Event, EventEmitter, Element, Method, Watch, h, Host } from '@stencil/core';\nimport {\n controlWidthClass,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n restoreStringFormState,\n setFormControlValue,\n setRequiredValidity,\n type ControlWidth,\n} from '../../utils';\n\nexport type InputType = 'text' | 'email' | 'tel' | 'url' | 'search' | 'password';\nexport type InputSize = 'lg' | 'md' | 'sm' | 'xs';\nexport type InputWidth = ControlWidth;\n\nconst ICON_SIZE: Record<InputSize, 'lg' | 'md' | 'sm' | 'xs'> = {\n lg: 'lg',\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\nconst CLEAR_BUTTON_SIZE: Record<InputSize, 'md' | 'sm' | 'xs'> = {\n lg: 'md',\n md: 'sm',\n sm: 'xs',\n xs: 'xs',\n};\n\nlet idCounter = 0;\n\n@Component({\n tag: 'ds-input',\n styleUrl: 'Input.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Input {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n private generatedId = `ds-input-${++idCounter}`;\n private errorId = `${this.generatedId}-error`;\n\n @Prop({ mutable: true }) value: string = '';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Keeps the value focusable and submittable while preventing edits. */\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() clearLabel: string = 'Clear';\n @Prop() placeholder: string | undefined;\n @Prop() type: InputType = 'text';\n /** Native browser autofill hint. */\n @Prop({ attribute: 'autocomplete' }) autoComplete: string | undefined;\n /** Preferred virtual keyboard without changing the value semantics. */\n @Prop({ attribute: 'inputmode' }) inputMode: string = '';\n /** Preferred virtual-keyboard action label. */\n @Prop({ attribute: 'enterkeyhint' }) enterKeyHint: string = '';\n /** Control density. */\n @Prop() size: InputSize = 'md';\n /** Width fit — fill the parent (default) or hug the available content. */\n @Prop() width: InputWidth = 'fill';\n /** Show the standard inset border, including focused and invalid strokes. */\n @Prop() hasBorder: boolean = true;\n /** Optional leading icon name. */\n @Prop() icon: string | undefined;\n @Prop() isInactive: boolean = false;\n @Prop() autoFocus: boolean = false;\n @Prop() error: boolean = false;\n @Prop() errorMessage: string | undefined;\n /** Associates the internal input with an external <label>. */\n @Prop() inputId: string | undefined;\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n\n @Event() dsChange!: EventEmitter<string>;\n @Event() dsClear!: EventEmitter<void>;\n\n private initialValue = '';\n @State() private formDisabled = false;\n @State() private hasSuffix = false;\n @State() private focused = false;\n @State() private touched = false;\n\n componentWillLoad() {\n this.initialValue = this.value;\n this.hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n this.syncFormValue();\n }\n\n componentDidRender() {\n const hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n if (hasSuffix !== this.hasSuffix) this.hasSuffix = hasSuffix;\n }\n\n @Watch('value')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n syncFormValue() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n setFormControlValue(this.internals, this.value, { inactive });\n const missing = this.required && !inactive && this.value.length === 0;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.initialValue;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.value = restoreStringFormState(state);\n }\n\n @Method()\n async setFocus() {\n this.el.querySelector('input')?.focus();\n }\n\n private handleInput = (e: Event) => {\n this.value = (e.target as HTMLInputElement).value;\n this.dsChange.emit(this.value);\n };\n\n private handleFocus = () => {\n this.focused = true;\n };\n\n private handleBlur = () => {\n this.focused = false;\n this.touched = true;\n };\n\n private handleClear = () => {\n this.value = '';\n this.dsChange.emit('');\n this.dsClear.emit();\n this.el.querySelector('input')?.focus();\n };\n\n render() {\n const inputId = this.inputId ?? this.generatedId;\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const filled = this.value.length > 0;\n const dirty = this.value !== this.initialValue;\n const showClear = this.type === 'search' && filled && !inactive && !this.readOnly;\n const showError = this.error && Boolean(this.errorMessage);\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n\n const describedBy = [\n this.ariaDescribedby,\n showError ? this.errorId : undefined,\n ].filter(Boolean).join(' ') || undefined;\n\n return (\n <Host\n class={{\n 'input-host': true,\n 'ds-field-stack': true,\n 'ds-control-inactive': inactive,\n [`ds-control--${this.size}`]: true,\n ...controlWidthClass(this.width),\n }}\n data-disabled={inactive ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-required={this.required ? '' : undefined}\n data-invalid={this.error ? '' : undefined}\n data-filled={filled ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n >\n <div\n class={{\n 'input-control': true,\n 'ds-control-frame': true,\n 'input-control--bordered': this.hasBorder,\n 'input-control--error': this.hasBorder && this.error,\n 'ds-interaction-fill': true,\n [`ds-control--${this.size}`]: true,\n }}\n >\n {this.icon && (\n <span class=\"input-control__prefix ds-control-icon-box ds-interaction-fill__content\" aria-hidden=\"true\">\n <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n </span>\n )}\n <input\n type={this.type}\n id={inputId}\n value={this.value}\n placeholder={this.placeholder}\n disabled={inactive}\n readOnly={this.readOnly}\n required={this.required}\n autoFocus={this.autoFocus}\n autoComplete={this.autoComplete}\n inputMode={this.inputMode || undefined}\n enterKeyHint={this.enterKeyHint || undefined}\n class={`native-input ds-control-label-box ds-text--${textVariant.replace('text-', '')} ds-text--regular ds-interaction-fill__content`}\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-describedby={describedBy}\n aria-invalid={this.error ? 'true' : undefined}\n onInput={this.handleInput}\n onFocus={this.handleFocus}\n onBlur={this.handleBlur}\n />\n <span class={{ 'input-control__suffix': true, 'ds-control-icon-box': true, 'input-control__suffix--empty': !this.hasSuffix }}>\n <slot name=\"suffix\" />\n </span>\n {showClear && (\n <ds-button-unfilled\n class=\"input-control__clear\"\n variant=\"icon\"\n size={CLEAR_BUTTON_SIZE[this.size]}\n icon=\"CrossCircle\"\n hasBorder={false}\n rounded\n ariaLabel={this.clearLabel}\n onDsClick={this.handleClear}\n />\n )}\n </div>\n {showError && (\n <ds-text\n class=\"error-text\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.errorMessage}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
|
|
595
595
|
"type": "registry:ui"
|
|
596
596
|
}
|
|
597
597
|
]
|
|
@@ -453,7 +453,7 @@
|
|
|
453
453
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: anchor, items, sections.",
|
|
454
454
|
"peerDependencies": {
|
|
455
455
|
"required": [
|
|
456
|
-
"@ds-mo/tokens ^5.1.
|
|
456
|
+
"@ds-mo/tokens ^5.1.1",
|
|
457
457
|
"@ds-mo/icons ^6.4.0"
|
|
458
458
|
],
|
|
459
459
|
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"states": [
|
|
37
37
|
"The component owns clipboard execution and temporary copied feedback while the application supplies the exact copyable text.",
|
|
38
38
|
"Feedback is controlled: the component emits the requested positive, negative, or cleared value while the application owns persistence and consequences.",
|
|
39
|
+
"Selected feedback changes to the filled thumb icon without adding a persistent selected fill; hover and press feedback remain available.",
|
|
39
40
|
"Copy, positive feedback, and negative feedback retain one stable order."
|
|
40
41
|
],
|
|
41
42
|
"responsiveBehavior": [
|
|
@@ -156,7 +157,7 @@
|
|
|
156
157
|
},
|
|
157
158
|
"peerDependencies": {
|
|
158
159
|
"required": [
|
|
159
|
-
"@ds-mo/tokens ^5.1.
|
|
160
|
+
"@ds-mo/tokens ^5.1.1",
|
|
160
161
|
"@ds-mo/icons ^6.4.0"
|
|
161
162
|
],
|
|
162
163
|
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -179,7 +180,7 @@
|
|
|
179
180
|
},
|
|
180
181
|
{
|
|
181
182
|
"path": "src/wc/components/MessageActions/MessageActions.tsx",
|
|
182
|
-
"content": "import { Component, Event, EventEmitter, h, Host, Prop, State, Watch } from '@stencil/core';\nimport { ClipboardFeedbackController } from '../../utils/clipboard';\nimport type { MessageCopyResultEventDetail, MessageFeedback } from '../conversation-types';\n\n@Component({\n tag: 'ds-message-actions',\n styleUrl: 'MessageActions.css',\n scoped: true,\n})\nexport class MessageActions {\n @Prop() copyText: string | undefined;\n @Prop() feedbackEnabled: boolean = false;\n @Prop() feedback: MessageFeedback | undefined;\n\n @State() private copied: boolean = false;\n\n @Event() dsCopyResult!: EventEmitter<MessageCopyResultEventDetail>;\n @Event() dsFeedbackChange!: EventEmitter<MessageFeedback | undefined>;\n\n private readonly copyFeedback = new ClipboardFeedbackController(copied => {\n this.copied = copied;\n });\n\n connectedCallback() {\n this.copyFeedback.connect();\n }\n\n @Watch('copyText')\n handleCopyTextChange() {\n this.copyFeedback.reset();\n }\n\n disconnectedCallback() {\n this.copyFeedback.disconnect();\n }\n\n private copy = async () => {\n const success = await this.copyFeedback.copy(this.copyText ?? '');\n if (success === undefined) return;\n\n this.dsCopyResult.emit({ status: success ? 'success' : 'error' });\n };\n\n private changeFeedback(feedback: MessageFeedback, selected: boolean) {\n this.dsFeedbackChange.emit(selected ? feedback : undefined);\n }\n\n render() {\n const copyLabel = this.copied ? 'Copied' : 'Copy message';\n\n return (\n <Host>\n <div class=\"message-actions\" role=\"group\" aria-label=\"Message actions\">\n {this.copyText ? (\n <ds-tooltip label={copyLabel} side=\"top\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n icon={this.copied ? 'Check' : 'Copy'}\n size=\"xs\"\n aria-label={copyLabel}\n hasBorder={false}\n onDsClick={this.copy}\n />\n </ds-tooltip>\n ) : null}\n {this.feedbackEnabled ? (\n <ds-tooltip label=\"Good response\" side=\"top\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n icon={this.feedback === 'positive' ? 'ThumbsUpFilled' : 'ThumbsUp'}\n size=\"xs\"\n aria-label=\"Good response\"\n hasBorder={false}\n pressed={this.feedback === 'positive'}\n onDsChange={(event: CustomEvent<boolean>) =>\n this.changeFeedback('positive', event.detail)\n }\n />\n </ds-tooltip>\n ) : null}\n {this.feedbackEnabled ? (\n <ds-tooltip label=\"Bad response\" side=\"top\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n icon={this.feedback === 'negative' ? 'ThumbsDownFilled' : 'ThumbsDown'}\n size=\"xs\"\n aria-label=\"Bad response\"\n hasBorder={false}\n pressed={this.feedback === 'negative'}\n onDsChange={(event: CustomEvent<boolean>) =>\n this.changeFeedback('negative', event.detail)\n }\n />\n </ds-tooltip>\n ) : null}\n </div>\n </Host>\n );\n }\n}\n",
|
|
183
|
+
"content": "import { Component, Event, EventEmitter, h, Host, Prop, State, Watch } from '@stencil/core';\nimport { ClipboardFeedbackController } from '../../utils/clipboard';\nimport type { MessageCopyResultEventDetail, MessageFeedback } from '../conversation-types';\n\n@Component({\n tag: 'ds-message-actions',\n styleUrl: 'MessageActions.css',\n scoped: true,\n})\nexport class MessageActions {\n @Prop() copyText: string | undefined;\n @Prop() feedbackEnabled: boolean = false;\n @Prop() feedback: MessageFeedback | undefined;\n\n @State() private copied: boolean = false;\n\n @Event() dsCopyResult!: EventEmitter<MessageCopyResultEventDetail>;\n @Event() dsFeedbackChange!: EventEmitter<MessageFeedback | undefined>;\n\n private readonly copyFeedback = new ClipboardFeedbackController(copied => {\n this.copied = copied;\n });\n\n connectedCallback() {\n this.copyFeedback.connect();\n }\n\n @Watch('copyText')\n handleCopyTextChange() {\n this.copyFeedback.reset();\n }\n\n disconnectedCallback() {\n this.copyFeedback.disconnect();\n }\n\n private copy = async () => {\n const success = await this.copyFeedback.copy(this.copyText ?? '');\n if (success === undefined) return;\n\n this.dsCopyResult.emit({ status: success ? 'success' : 'error' });\n };\n\n private changeFeedback(feedback: MessageFeedback, selected: boolean) {\n this.dsFeedbackChange.emit(selected ? feedback : undefined);\n }\n\n render() {\n const copyLabel = this.copied ? 'Copied' : 'Copy message';\n\n return (\n <Host>\n <div class=\"message-actions\" role=\"group\" aria-label=\"Message actions\">\n {this.copyText ? (\n <ds-tooltip label={copyLabel} side=\"top\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n icon={this.copied ? 'Check' : 'Copy'}\n size=\"xs\"\n aria-label={copyLabel}\n hasBorder={false}\n onDsClick={this.copy}\n />\n </ds-tooltip>\n ) : null}\n {this.feedbackEnabled ? (\n <ds-tooltip label=\"Good response\" side=\"top\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n icon={this.feedback === 'positive' ? 'ThumbsUpFilled' : 'ThumbsUp'}\n size=\"xs\"\n aria-label=\"Good response\"\n hasBorder={false}\n activeFill={false}\n pressed={this.feedback === 'positive'}\n onDsChange={(event: CustomEvent<boolean>) =>\n this.changeFeedback('positive', event.detail)\n }\n />\n </ds-tooltip>\n ) : null}\n {this.feedbackEnabled ? (\n <ds-tooltip label=\"Bad response\" side=\"top\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n icon={this.feedback === 'negative' ? 'ThumbsDownFilled' : 'ThumbsDown'}\n size=\"xs\"\n aria-label=\"Bad response\"\n hasBorder={false}\n activeFill={false}\n pressed={this.feedback === 'negative'}\n onDsChange={(event: CustomEvent<boolean>) =>\n this.changeFeedback('negative', event.detail)\n }\n />\n </ds-tooltip>\n ) : null}\n </div>\n </Host>\n );\n }\n}\n",
|
|
183
184
|
"type": "registry:ui"
|
|
184
185
|
}
|
|
185
186
|
]
|
|
@@ -299,7 +299,7 @@
|
|
|
299
299
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: currentArea.",
|
|
300
300
|
"peerDependencies": {
|
|
301
301
|
"required": [
|
|
302
|
-
"@ds-mo/tokens ^5.1.
|
|
302
|
+
"@ds-mo/tokens ^5.1.1",
|
|
303
303
|
"@ds-mo/icons ^6.4.0"
|
|
304
304
|
],
|
|
305
305
|
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -280,7 +280,7 @@
|
|
|
280
280
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: sections, subsections.",
|
|
281
281
|
"peerDependencies": {
|
|
282
282
|
"required": [
|
|
283
|
-
"@ds-mo/tokens ^5.1.
|
|
283
|
+
"@ds-mo/tokens ^5.1.1",
|
|
284
284
|
"@ds-mo/icons ^6.4.0"
|
|
285
285
|
],
|
|
286
286
|
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: sections.",
|
|
154
154
|
"peerDependencies": {
|
|
155
155
|
"required": [
|
|
156
|
-
"@ds-mo/tokens ^5.1.
|
|
156
|
+
"@ds-mo/tokens ^5.1.1",
|
|
157
157
|
"@ds-mo/icons ^6.4.0"
|
|
158
158
|
],
|
|
159
159
|
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -284,7 +284,7 @@
|
|
|
284
284
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: dashboardGroups, settingsGroups.",
|
|
285
285
|
"peerDependencies": {
|
|
286
286
|
"required": [
|
|
287
|
-
"@ds-mo/tokens ^5.1.
|
|
287
|
+
"@ds-mo/tokens ^5.1.1",
|
|
288
288
|
"@ds-mo/icons ^6.4.0"
|
|
289
289
|
],
|
|
290
290
|
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
3
|
"name": "modal",
|
|
4
4
|
"title": "Modal",
|
|
5
|
-
"description": "Dismissible native top-layer dialog for a focused confirmation or short blocking task, with a
|
|
5
|
+
"description": "Dismissible native top-layer dialog for a focused confirmation or short blocking task, with a compact title-and-close header, optional supporting description, composable content, and an optional two-action footer.",
|
|
6
6
|
"type": "registry:ui",
|
|
7
7
|
"meta": {
|
|
8
8
|
"library": "@ds-mo/ui",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"intent": {
|
|
14
14
|
"audience": "general",
|
|
15
15
|
"status": "stable",
|
|
16
|
-
"summary": "Dismissible native top-layer dialog for a focused confirmation or short blocking task, with a
|
|
16
|
+
"summary": "Dismissible native top-layer dialog for a focused confirmation or short blocking task, with a compact title-and-close header, optional supporting description, composable content, and an optional two-action footer.",
|
|
17
17
|
"useWhen": [
|
|
18
18
|
"The user must confirm or cancel one focused decision before returning to the underlying interface.",
|
|
19
19
|
"A short blocking task or compact form needs dialog semantics, focus containment, and explicit completion actions.",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
],
|
|
52
52
|
"accessibility": [
|
|
53
53
|
"Provide a concise visible heading; the component uses it as the dialog's accessible name.",
|
|
54
|
+
"When concise supporting context belongs in the header, provide description; it stays below the title and is included in the dialog's accessible description.",
|
|
54
55
|
"Give the primary explanatory copy a stable id and pass it through aria-describedby so assistive technology receives the decision context.",
|
|
55
56
|
"Opening the native modal dialog moves focus to the header Close action, whose visible tooltip matches its accessible name, traps Tab navigation inside the top layer, and makes outside content inert.",
|
|
56
57
|
"Close, Escape, and backdrop activation emit dsClose with close-button, escape, or backdrop reason before beginning dismissal; use the component only when every dismissal path is safe.",
|
|
@@ -63,12 +64,13 @@
|
|
|
63
64
|
"The owner opens the modal through the open property and responds to the typed dsClose detail for application state synchronization or dismissal analytics.",
|
|
64
65
|
"The component mutates open to false for Close, Escape, and backdrop dismissal while retaining its rendered surface through exit motion.",
|
|
65
66
|
"The default slot owns task content; the named footer slot is optional and is hidden when no actions are assigned.",
|
|
67
|
+
"The optional header description remains stacked beneath the title with the shared compact-header rhythm; longer task content belongs in the default slot.",
|
|
66
68
|
"Primary actions use a bold filled button with semantic intent. Secondary and cancel actions use bordered label-only unfilled buttons.",
|
|
67
69
|
"The owner performs save, delete, cancel, and other product consequences before or while closing the modal."
|
|
68
70
|
],
|
|
69
71
|
"responsiveBehavior": [
|
|
70
72
|
"Choose the smallest sm, md, or lg width that fits the focused task; the top-layer dialog clamps to the viewport with token-based outer space.",
|
|
71
|
-
"Content scrolls independently when needed while the
|
|
73
|
+
"Content scrolls independently when needed while the header and optional 64px footer remain fixed. The title-only header uses the shared 48px compact geometry and grows when its optional description wraps.",
|
|
72
74
|
"Avoid layouts whose minimum content width exceeds the narrow viewport clamp.",
|
|
73
75
|
"The native top layer escapes transformed ancestors, clipping containers, and application stacking contexts without moving slotted content."
|
|
74
76
|
],
|
|
@@ -122,6 +124,15 @@
|
|
|
122
124
|
"required": false,
|
|
123
125
|
"mutable": false
|
|
124
126
|
},
|
|
127
|
+
"description": {
|
|
128
|
+
"type": "string",
|
|
129
|
+
"resolvedType": "string",
|
|
130
|
+
"attribute": "description",
|
|
131
|
+
"default": "''",
|
|
132
|
+
"required": false,
|
|
133
|
+
"mutable": false,
|
|
134
|
+
"description": "Optional supporting copy shown below the heading."
|
|
135
|
+
},
|
|
125
136
|
"heading": {
|
|
126
137
|
"type": "string",
|
|
127
138
|
"resolvedType": "string",
|
|
@@ -184,6 +195,15 @@
|
|
|
184
195
|
"required": false,
|
|
185
196
|
"mutable": false
|
|
186
197
|
},
|
|
198
|
+
"description": {
|
|
199
|
+
"type": "string",
|
|
200
|
+
"resolvedType": "string",
|
|
201
|
+
"attribute": "description",
|
|
202
|
+
"default": "''",
|
|
203
|
+
"required": false,
|
|
204
|
+
"mutable": false,
|
|
205
|
+
"description": "Optional supporting copy shown below the heading."
|
|
206
|
+
},
|
|
187
207
|
"heading": {
|
|
188
208
|
"type": "string",
|
|
189
209
|
"resolvedType": "string",
|
|
@@ -250,7 +270,7 @@
|
|
|
250
270
|
},
|
|
251
271
|
"peerDependencies": {
|
|
252
272
|
"required": [
|
|
253
|
-
"@ds-mo/tokens ^5.1.
|
|
273
|
+
"@ds-mo/tokens ^5.1.1",
|
|
254
274
|
"@ds-mo/icons ^6.4.0"
|
|
255
275
|
],
|
|
256
276
|
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -269,12 +289,12 @@
|
|
|
269
289
|
"files": [
|
|
270
290
|
{
|
|
271
291
|
"path": "src/wc/components/Modal/Modal.css",
|
|
272
|
-
"content": "@import '../../utils/chrome-layout.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n display: contents;\n}\n\n.modal-dialog {\n display: none;\n flex-direction: column;\n max-width: none;\n max-height: calc(100vh - 2 * var(--dimension-space-200));\n margin: auto;\n padding: 0;\n overflow: hidden;\n color: var(--color-foreground-primary);\n background-color: var(--color-background-primary);\n border: 0;\n border-radius: var(--dimension-radius-050);\n box-shadow: var(--effect-elevation-elevated-floating);\n outline: none;\n box-sizing: border-box;\n}\n\n.modal-dialog[open] {\n display: flex;\n animation: modalDialogIn var(--effect-motion-short-2) forwards;\n}\n\n.modal-dialog::backdrop {\n background-color: var(--color-background-shade);\n animation: modalBackdropIn var(--effect-motion-short-2) forwards;\n}\n\n.modal-dialog--closing {\n animation: modalDialogOut var(--effect-motion-short-3) forwards;\n}\n\n.modal-dialog--closing::backdrop {\n animation: modalBackdropOut var(--effect-motion-short-3) forwards;\n}\n\n@keyframes modalBackdropIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes modalBackdropOut {\n from { opacity: 1; }\n to { opacity: 0; }\n}\n\n@keyframes modalDialogIn {\n from { opacity: 0; transform: scale(0.97) translateY(4px); }\n to { opacity: 1; transform: scale(1) translateY(0); }\n}\n\n@keyframes modalDialogOut {\n from { opacity: 1; transform: scale(1) translateY(0); }\n to { opacity: 0; transform: scale(0.97) translateY(4px); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .modal-dialog[open],\n .modal-dialog--closing {\n animation: none;\n transform: none;\n }\n\n .modal-dialog::backdrop {\n animation: none;\n opacity: 1;\n }\n\n .modal-dialog--closing,\n .modal-dialog--closing::backdrop {\n opacity: 0;\n }\n}\n\n.modal-header {\n
|
|
292
|
+
"content": "@import '../../utils/chrome-header.css';\n@import '../../utils/chrome-layout.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n display: contents;\n}\n\n.modal-dialog {\n display: none;\n flex-direction: column;\n max-width: none;\n max-height: calc(100vh - 2 * var(--dimension-space-200));\n margin: auto;\n padding: 0;\n overflow: hidden;\n color: var(--color-foreground-primary);\n background-color: var(--color-background-primary);\n border: 0;\n border-radius: var(--dimension-radius-050);\n box-shadow: var(--effect-elevation-elevated-floating);\n outline: none;\n box-sizing: border-box;\n}\n\n.modal-dialog[open] {\n display: flex;\n animation: modalDialogIn var(--effect-motion-short-2) forwards;\n}\n\n.modal-dialog::backdrop {\n background-color: var(--color-background-shade);\n animation: modalBackdropIn var(--effect-motion-short-2) forwards;\n}\n\n.modal-dialog--closing {\n animation: modalDialogOut var(--effect-motion-short-3) forwards;\n}\n\n.modal-dialog--closing::backdrop {\n animation: modalBackdropOut var(--effect-motion-short-3) forwards;\n}\n\n@keyframes modalBackdropIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@keyframes modalBackdropOut {\n from { opacity: 1; }\n to { opacity: 0; }\n}\n\n@keyframes modalDialogIn {\n from { opacity: 0; transform: scale(0.97) translateY(4px); }\n to { opacity: 1; transform: scale(1) translateY(0); }\n}\n\n@keyframes modalDialogOut {\n from { opacity: 1; transform: scale(1) translateY(0); }\n to { opacity: 0; transform: scale(0.97) translateY(4px); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .modal-dialog[open],\n .modal-dialog--closing {\n animation: none;\n transform: none;\n }\n\n .modal-dialog::backdrop {\n animation: none;\n opacity: 1;\n }\n\n .modal-dialog--closing,\n .modal-dialog--closing::backdrop {\n opacity: 0;\n }\n}\n\n.modal-header {\n align-items: flex-start;\n flex: 0 0 auto;\n color: var(--color-foreground-primary);\n}\n\n.modal-copy,\nds-text.modal-heading,\nds-text.modal-description {\n width: auto;\n text-align: left;\n}\n\nds-text.modal-heading {\n -webkit-user-select: none;\n user-select: none;\n}\n\n.modal-close-tooltip {\n flex: 0 0 auto;\n}\n\n.modal-content {\n min-height: 0;\n padding: var(--dimension-space-200);\n overflow-y: auto;\n flex: 1 1 auto;\n box-sizing: border-box;\n}\n\n.modal-footer {\n justify-content: flex-start;\n height: var(--dimension-size-800);\n flex: 0 0 auto;\n border-top: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n}\n\n.modal-footer__actions {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-100);\n min-width: 0;\n}\n\n.modal-footer--empty {\n display: none;\n}\n\n.modal-footer__actions slot {\n display: contents;\n}\n\n@media (forced-colors: active) {\n .modal-dialog {\n box-shadow: none;\n outline: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n outline-offset: calc(-1 * var(--dimension-stroke-width-012));\n }\n}\n",
|
|
273
293
|
"type": "registry:ui"
|
|
274
294
|
},
|
|
275
295
|
{
|
|
276
296
|
"path": "src/wc/components/Modal/Modal.tsx",
|
|
277
|
-
"content": "import { Component, Prop, State, Event, EventEmitter, Element, Watch, h, Host } from '@stencil/core';\nimport { resolveMotionTimeMs, TOKEN_DEFAULTS } from '../../utils';\n\nexport type ModalWidth = 'sm' | 'md' | 'lg';\nexport type ModalCloseReason = 'close-button' | 'escape' | 'backdrop';\n\nexport interface ModalCloseDetail {\n reason: ModalCloseReason;\n originalEvent: Event;\n}\n\nconst WIDTH_MAP: Record<ModalWidth, string> = {\n sm: 'var(--dimension-modal-width-sm)',\n md: 'var(--dimension-modal-width-md)',\n lg: 'var(--dimension-modal-width-lg)',\n};\n\nconst FOCUSABLE_SEL = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n 'iframe',\n '[contenteditable=\"true\"]',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\nlet modalIdCounter = 0;\n\n@Component({\n tag: 'ds-modal',\n styleUrl: 'Modal.css',\n scoped: true,\n})\nexport class Modal {\n @Element() el!: HTMLElement;\n\n @Prop({ mutable: true }) open: boolean = false;\n @Prop() heading!: string;\n @Prop() closeAriaLabel: string = 'Close';\n @Prop() modalWidth: ModalWidth | string = 'md';\n /** Optional id reference for explanatory content in the default slot. */\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n\n @State() private closing: boolean = false;\n @State() private hasFooter: boolean = false;\n\n /** Emitted when an internal dismissal control requests that the modal close. */\n @Event() dsClose!: EventEmitter<ModalCloseDetail>;\n /** Emitted after exit motion completes, the top layer closes, and focus is restored. */\n @Event() dsAfterClose!: EventEmitter<void>;\n\n private titleId = `ds-modal-title-${++modalIdCounter}`;\n private dialogEl: HTMLDialogElement | null = null;\n private previousFocus: HTMLElement | null = null;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n\n componentDidLoad() {\n this.updateFooterPresence();\n if (this.open) this.onOpenChange(true);\n }\n\n disconnectedCallback() {\n this.clearCloseTimer();\n if (this.dialogEl?.open) this.dialogEl.close();\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (isOpen) {\n this.clearCloseTimer();\n const alreadyOpen = !!this.dialogEl?.open;\n this.closing = false;\n if (!this.dialogEl || alreadyOpen) return;\n this.previousFocus = document.activeElement as HTMLElement | null;\n this.dialogEl.showModal();\n requestAnimationFrame(() => {\n if (this.open && this.dialogEl?.open) this.focusClose();\n });\n } else if (this.dialogEl?.open) {\n this.closing = true;\n const closeAnimationMs = this.closeAnimationMs;\n if (closeAnimationMs <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => {\n this.closeTimer = null;\n this.finishClose();\n }, closeAnimationMs);\n }\n }\n\n private focusClose() {\n const close = this.el.querySelector('ds-button-unfilled.modal-close') as\n | (HTMLElement & { setFocus?: () => Promise<void> })\n | null;\n void close?.setFocus?.();\n }\n\n private clearCloseTimer() {\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private requestClose(reason: ModalCloseReason, originalEvent: Event) {\n if (!this.open || this.closing || !this.dialogEl?.open) return;\n this.dsClose.emit({ reason, originalEvent });\n this.open = false;\n }\n\n private updateFooterPresence(slot?: HTMLSlotElement) {\n const assignedNodes = slot?.assignedNodes() ?? [];\n this.hasFooter =\n assignedNodes.some(node => node.nodeType !== Node.TEXT_NODE || !!node.textContent?.trim()) ||\n this.el.querySelector('[slot=\"footer\"]') !== null;\n }\n\n private handleCancel(event: Event) {\n event.preventDefault();\n this.requestClose('escape', event);\n }\n\n private handleBackdropPointerDown(event: PointerEvent) {\n if (event.target === this.dialogEl) this.requestClose('backdrop', event);\n }\n\n private handleKeyDown(event: KeyboardEvent) {\n if (event.key !== 'Tab' || !this.dialogEl) return;\n const focusables = Array.from(\n this.dialogEl.querySelectorAll<HTMLElement>(FOCUSABLE_SEL)\n ).filter(element =>\n !element.hasAttribute('disabled') && element.getAttribute('aria-hidden') !== 'true'\n );\n if (!focusables.length) {\n event.preventDefault();\n this.dialogEl.focus();\n return;\n }\n const first = focusables[0];\n const last = focusables[focusables.length - 1];\n const active = document.activeElement;\n if (event.shiftKey && (active === first || active === this.dialogEl)) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first.focus();\n }\n }\n\n private get resolvedWidth(): string {\n if (this.modalWidth === 'sm' || this.modalWidth === 'md' || this.modalWidth === 'lg') {\n return WIDTH_MAP[this.modalWidth];\n }\n return this.modalWidth as string;\n }\n\n private get closeAnimationMs(): number {\n return resolveMotionTimeMs(TOKEN_DEFAULTS.motionShort3, TOKEN_DEFAULTS.animationDurationShort3);\n }\n\n private finishClose() {\n if (!this.dialogEl?.open) return;\n this.dialogEl.close();\n this.closing = false;\n this.clearCloseTimer();\n this.previousFocus?.focus?.();\n this.previousFocus = null;\n this.dsAfterClose.emit();\n }\n\n render() {\n return (\n <Host>\n <dialog\n ref={(element?: HTMLDialogElement) => {\n this.dialogEl = element ?? null;\n }}\n class={{ 'modal-dialog': true, 'modal-dialog--closing': this.closing }}\n aria-labelledby={this.titleId}\n aria-describedby={this.
|
|
297
|
+
"content": "import { Component, Prop, State, Event, EventEmitter, Element, Watch, h, Host } from '@stencil/core';\nimport { resolveMotionTimeMs, TOKEN_DEFAULTS } from '../../utils';\n\nexport type ModalWidth = 'sm' | 'md' | 'lg';\nexport type ModalCloseReason = 'close-button' | 'escape' | 'backdrop';\n\nexport interface ModalCloseDetail {\n reason: ModalCloseReason;\n originalEvent: Event;\n}\n\nconst WIDTH_MAP: Record<ModalWidth, string> = {\n sm: 'var(--dimension-modal-width-sm)',\n md: 'var(--dimension-modal-width-md)',\n lg: 'var(--dimension-modal-width-lg)',\n};\n\nconst FOCUSABLE_SEL = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n 'iframe',\n '[contenteditable=\"true\"]',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\nlet modalIdCounter = 0;\n\n@Component({\n tag: 'ds-modal',\n styleUrl: 'Modal.css',\n scoped: true,\n})\nexport class Modal {\n @Element() el!: HTMLElement;\n\n @Prop({ mutable: true }) open: boolean = false;\n @Prop() heading!: string;\n /** Optional supporting copy shown below the heading. */\n @Prop() description: string = '';\n @Prop() closeAriaLabel: string = 'Close';\n @Prop() modalWidth: ModalWidth | string = 'md';\n /** Optional id reference for explanatory content in the default slot. */\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n\n @State() private closing: boolean = false;\n @State() private hasFooter: boolean = false;\n\n /** Emitted when an internal dismissal control requests that the modal close. */\n @Event() dsClose!: EventEmitter<ModalCloseDetail>;\n /** Emitted after exit motion completes, the top layer closes, and focus is restored. */\n @Event() dsAfterClose!: EventEmitter<void>;\n\n private titleId = `ds-modal-title-${++modalIdCounter}`;\n private descriptionId = `${this.titleId}-description`;\n private dialogEl: HTMLDialogElement | null = null;\n private previousFocus: HTMLElement | null = null;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n\n componentDidLoad() {\n this.updateFooterPresence();\n if (this.open) this.onOpenChange(true);\n }\n\n disconnectedCallback() {\n this.clearCloseTimer();\n if (this.dialogEl?.open) this.dialogEl.close();\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (isOpen) {\n this.clearCloseTimer();\n const alreadyOpen = !!this.dialogEl?.open;\n this.closing = false;\n if (!this.dialogEl || alreadyOpen) return;\n this.previousFocus = document.activeElement as HTMLElement | null;\n this.dialogEl.showModal();\n requestAnimationFrame(() => {\n if (this.open && this.dialogEl?.open) this.focusClose();\n });\n } else if (this.dialogEl?.open) {\n this.closing = true;\n const closeAnimationMs = this.closeAnimationMs;\n if (closeAnimationMs <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => {\n this.closeTimer = null;\n this.finishClose();\n }, closeAnimationMs);\n }\n }\n\n private focusClose() {\n const close = this.el.querySelector('ds-button-unfilled.modal-close') as\n | (HTMLElement & { setFocus?: () => Promise<void> })\n | null;\n void close?.setFocus?.();\n }\n\n private clearCloseTimer() {\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private requestClose(reason: ModalCloseReason, originalEvent: Event) {\n if (!this.open || this.closing || !this.dialogEl?.open) return;\n this.dsClose.emit({ reason, originalEvent });\n this.open = false;\n }\n\n private updateFooterPresence(slot?: HTMLSlotElement) {\n const assignedNodes = slot?.assignedNodes() ?? [];\n this.hasFooter =\n assignedNodes.some(node => node.nodeType !== Node.TEXT_NODE || !!node.textContent?.trim()) ||\n this.el.querySelector('[slot=\"footer\"]') !== null;\n }\n\n private handleCancel(event: Event) {\n event.preventDefault();\n this.requestClose('escape', event);\n }\n\n private handleBackdropPointerDown(event: PointerEvent) {\n if (event.target === this.dialogEl) this.requestClose('backdrop', event);\n }\n\n private handleKeyDown(event: KeyboardEvent) {\n if (event.key !== 'Tab' || !this.dialogEl) return;\n const focusables = Array.from(\n this.dialogEl.querySelectorAll<HTMLElement>(FOCUSABLE_SEL)\n ).filter(element =>\n !element.hasAttribute('disabled') && element.getAttribute('aria-hidden') !== 'true'\n );\n if (!focusables.length) {\n event.preventDefault();\n this.dialogEl.focus();\n return;\n }\n const first = focusables[0];\n const last = focusables[focusables.length - 1];\n const active = document.activeElement;\n if (event.shiftKey && (active === first || active === this.dialogEl)) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first.focus();\n }\n }\n\n private get resolvedWidth(): string {\n if (this.modalWidth === 'sm' || this.modalWidth === 'md' || this.modalWidth === 'lg') {\n return WIDTH_MAP[this.modalWidth];\n }\n return this.modalWidth as string;\n }\n\n private get describedBy(): string | undefined {\n const ids = [this.description ? this.descriptionId : '', this.ariaDescribedby ?? ''].filter(\n Boolean,\n );\n return ids.length > 0 ? ids.join(' ') : undefined;\n }\n\n private get closeAnimationMs(): number {\n return resolveMotionTimeMs(TOKEN_DEFAULTS.motionShort3, TOKEN_DEFAULTS.animationDurationShort3);\n }\n\n private finishClose() {\n if (!this.dialogEl?.open) return;\n this.dialogEl.close();\n this.closing = false;\n this.clearCloseTimer();\n this.previousFocus?.focus?.();\n this.previousFocus = null;\n this.dsAfterClose.emit();\n }\n\n render() {\n return (\n <Host>\n <dialog\n ref={(element?: HTMLDialogElement) => {\n this.dialogEl = element ?? null;\n }}\n class={{ 'modal-dialog': true, 'modal-dialog--closing': this.closing }}\n aria-labelledby={this.titleId}\n aria-describedby={this.describedBy}\n onCancel={(event: Event) => this.handleCancel(event)}\n onPointerDown={(event: PointerEvent) => this.handleBackdropPointerDown(event)}\n onKeyDown={(event: KeyboardEvent) => this.handleKeyDown(event)}\n style={{\n width: `min(${this.resolvedWidth}, calc(100vw - 2 * var(--dimension-space-200)))`,\n }}\n >\n <div class=\"modal-header ds-chrome-header ds-chrome-header--bounded\">\n <div class=\"modal-copy ds-chrome-header__copy ds-chrome-header__copy--stacked ds-control--md\">\n <ds-text\n class=\"modal-heading ds-chrome-header__heading\"\n as=\"h2\"\n variant=\"text-title-small\"\n emphasis\n color=\"primary\"\n lineTruncation={1}\n textId={this.titleId}\n >\n {this.heading}\n </ds-text>\n {this.description ? (\n <ds-text\n class=\"modal-description ds-chrome-header__description\"\n as=\"p\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n wrap=\"wrap\"\n textId={this.descriptionId}\n >\n {this.description}\n </ds-text>\n ) : null}\n </div>\n <ds-tooltip class=\"modal-close-tooltip ds-chrome-header__trailing\" label={this.closeAriaLabel} side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n class=\"modal-close\"\n variant=\"icon\"\n icon=\"Cross\"\n size=\"md\"\n aria-label={this.closeAriaLabel}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) =>\n this.requestClose('close-button', event.detail)\n }\n />\n </ds-tooltip>\n </div>\n <div class=\"modal-content\">\n <slot />\n </div>\n <div\n class={{\n 'modal-footer': true,\n 'ds-chrome-row': true,\n 'ds-chrome-space--lg': true,\n 'modal-footer--empty': !this.hasFooter,\n }}\n >\n <div class=\"modal-footer__actions\">\n <slot\n name=\"footer\"\n onSlotchange={(event: Event) =>\n this.updateFooterPresence(event.currentTarget as HTMLSlotElement)\n }\n />\n </div>\n </div>\n </dialog>\n </Host>\n );\n }\n}\n",
|
|
278
298
|
"type": "registry:ui"
|
|
279
299
|
}
|
|
280
300
|
]
|
|
@@ -215,7 +215,7 @@
|
|
|
215
215
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: items.",
|
|
216
216
|
"peerDependencies": {
|
|
217
217
|
"required": [
|
|
218
|
-
"@ds-mo/tokens ^5.1.
|
|
218
|
+
"@ds-mo/tokens ^5.1.1",
|
|
219
219
|
"@ds-mo/icons ^6.4.0"
|
|
220
220
|
],
|
|
221
221
|
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
],
|
|
32
32
|
"states": [
|
|
33
33
|
"The title remains left aligned whether the header has no actions, Back, one trailing action, or several trailing actions.",
|
|
34
|
-
"Leading and trailing control zones never shrink; only the title consumes and truncates within the remaining width,
|
|
34
|
+
"Leading and trailing control zones never shrink; only the title consumes and truncates within the remaining width. The header uses the shared 8px compact-header zone gap, while adjacent trailing actions retain their 4px control gap.",
|
|
35
35
|
"Header chrome text is not selectable; actions remain keyboard focusable and interactive.",
|
|
36
36
|
"Adjacent trailing actions keep the shared 4px control gap in drawer and fullscreen presentations.",
|
|
37
37
|
"When fullscreen and overflow actions appear together, author fullscreen before overflow so Ellipses remains the rightmost action.",
|
|
@@ -312,7 +312,7 @@
|
|
|
312
312
|
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: actions.",
|
|
313
313
|
"peerDependencies": {
|
|
314
314
|
"required": [
|
|
315
|
-
"@ds-mo/tokens ^5.1.
|
|
315
|
+
"@ds-mo/tokens ^5.1.1",
|
|
316
316
|
"@ds-mo/icons ^6.4.0"
|
|
317
317
|
],
|
|
318
318
|
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
@@ -331,12 +331,12 @@
|
|
|
331
331
|
"files": [
|
|
332
332
|
{
|
|
333
333
|
"path": "src/wc/components/PanelToolHeader/PanelToolHeader.css",
|
|
334
|
-
"content": "@import '../../utils/control-density.css';\n@import '../../utils/chrome-
|
|
334
|
+
"content": "@import '../../utils/control-density.css';\n@import '../../utils/chrome-header.css';\n\n:host {\n display: block;\n flex: 0 0 auto;\n box-sizing: border-box;\n}\n\n.panel-tool-header {\n height: var(--dimension-size-600);\n color: var(--color-foreground-primary);\n -webkit-user-select: none;\n user-select: none;\n}\n\n.panel-tool-header__leading:empty,\n.panel-tool-header__trailing:empty {\n display: none;\n}\n\n.panel-tool-header__trailing {\n gap: var(--dimension-space-050);\n justify-content: flex-end;\n}\n\nds-text.panel-tool-header__heading {\n display: inline-flex;\n flex: 1 1 0;\n align-items: center;\n width: auto;\n min-width: 0;\n height: var(--ds-control-height);\n padding-inline: calc(var(--ds-control-padding-inline) + var(--ds-control-label-inset));\n box-sizing: border-box;\n text-align: left;\n -webkit-user-select: none;\n user-select: none;\n}\n",
|
|
335
335
|
"type": "registry:ui"
|
|
336
336
|
},
|
|
337
337
|
{
|
|
338
338
|
"path": "src/wc/components/PanelToolHeader/PanelToolHeader.tsx",
|
|
339
|
-
"content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\nimport type { PanelToolsHeaderAction } from '../PanelTools/panel-tools-types';\n\n@Component({\n tag: 'ds-panel-tool-header',\n styleUrl: 'PanelToolHeader.css',\n scoped: true,\n})\nexport class PanelToolHeader {\n @Element() el!: HTMLElement;\n\n /** Visible heading for the active tool view. */\n @Prop() heading: string = '';\n /** Show the leading Back action. */\n @Prop() showBack: boolean = false;\n /** Canonical icon for the leading navigation or dismiss action. */\n @Prop() backIcon: string = 'ChevronLeft';\n /** Accessible name for the leading navigation or dismiss action. */\n @Prop() backAriaLabel: string = 'Back';\n /** Show the trailing options-menu trigger. */\n @Prop() showMenu: boolean = true;\n /** Accessible name for the options-menu trigger. */\n @Prop() menuAriaLabel: string = 'More options';\n /** Stable id placed on the menu trigger host for ds-menu anchoring. */\n @Prop() menuTriggerId: string = '';\n /** Id of the menu controlled by the trigger. */\n @Prop() menuControls: string | undefined;\n /** Whether the controlled menu is open. */\n @Prop() menuExpanded: boolean = false;\n /** Trailing application-owned icon actions. When present, these replace the legacy menu action. */\n @Prop() actions: PanelToolsHeaderAction[] = [];\n\n @Event() dsBack!: EventEmitter<MouseEvent>;\n @Event() dsMenuToggle!: EventEmitter<MouseEvent>;\n @Event() dsAction!: EventEmitter<{ id: string; originalEvent: MouseEvent }>;\n\n @Method()\n async focusMenuTrigger() {\n const trigger = this.el.querySelector(\n 'ds-button-unfilled.panel-tool-header__menu, ds-button-unfilled[data-header-action-id=\"menu\"]'\n ) as (HTMLElement & { setFocus?: () => Promise<void> }) | null;\n await trigger?.setFocus?.();\n }\n\n render() {\n const authoredActions = this.actions.length > 0;\n const actions: PanelToolsHeaderAction[] = authoredActions\n ? this.actions\n : this.showMenu\n ? [\n {\n id: 'menu',\n icon: 'Ellipses',\n ariaLabel: this.menuAriaLabel,\n triggerId: this.menuTriggerId,\n controls: this.menuControls,\n expanded: this.menuExpanded,\n haspopup: 'menu',\n },\n ]\n : [];\n\n return (\n <Host>\n <header class=\"panel-tool-header ds-chrome-
|
|
339
|
+
"content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\nimport type { PanelToolsHeaderAction } from '../PanelTools/panel-tools-types';\n\n@Component({\n tag: 'ds-panel-tool-header',\n styleUrl: 'PanelToolHeader.css',\n scoped: true,\n})\nexport class PanelToolHeader {\n @Element() el!: HTMLElement;\n\n /** Visible heading for the active tool view. */\n @Prop() heading: string = '';\n /** Show the leading Back action. */\n @Prop() showBack: boolean = false;\n /** Canonical icon for the leading navigation or dismiss action. */\n @Prop() backIcon: string = 'ChevronLeft';\n /** Accessible name for the leading navigation or dismiss action. */\n @Prop() backAriaLabel: string = 'Back';\n /** Show the trailing options-menu trigger. */\n @Prop() showMenu: boolean = true;\n /** Accessible name for the options-menu trigger. */\n @Prop() menuAriaLabel: string = 'More options';\n /** Stable id placed on the menu trigger host for ds-menu anchoring. */\n @Prop() menuTriggerId: string = '';\n /** Id of the menu controlled by the trigger. */\n @Prop() menuControls: string | undefined;\n /** Whether the controlled menu is open. */\n @Prop() menuExpanded: boolean = false;\n /** Trailing application-owned icon actions. When present, these replace the legacy menu action. */\n @Prop() actions: PanelToolsHeaderAction[] = [];\n\n @Event() dsBack!: EventEmitter<MouseEvent>;\n @Event() dsMenuToggle!: EventEmitter<MouseEvent>;\n @Event() dsAction!: EventEmitter<{ id: string; originalEvent: MouseEvent }>;\n\n @Method()\n async focusMenuTrigger() {\n const trigger = this.el.querySelector(\n 'ds-button-unfilled.panel-tool-header__menu, ds-button-unfilled[data-header-action-id=\"menu\"]'\n ) as (HTMLElement & { setFocus?: () => Promise<void> }) | null;\n await trigger?.setFocus?.();\n }\n\n render() {\n const authoredActions = this.actions.length > 0;\n const actions: PanelToolsHeaderAction[] = authoredActions\n ? this.actions\n : this.showMenu\n ? [\n {\n id: 'menu',\n icon: 'Ellipses',\n ariaLabel: this.menuAriaLabel,\n triggerId: this.menuTriggerId,\n controls: this.menuControls,\n expanded: this.menuExpanded,\n haspopup: 'menu',\n },\n ]\n : [];\n\n return (\n <Host>\n <header class=\"panel-tool-header ds-chrome-header ds-chrome-header--bounded\">\n <div class=\"panel-tool-header__leading ds-chrome-header__leading\">\n {this.showBack ? (\n <ds-tooltip label={this.backAriaLabel} side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n class=\"panel-tool-header__back\"\n variant=\"icon\"\n icon={this.backIcon}\n size=\"md\"\n aria-label={this.backAriaLabel}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => this.dsBack.emit(event.detail)}\n />\n </ds-tooltip>\n ) : null}\n </div>\n <ds-text\n class=\"panel-tool-header__heading ds-chrome-header__heading ds-control--md\"\n as=\"h2\"\n variant=\"text-body-medium\"\n emphasis\n color=\"primary\"\n lineTruncation={1}\n >\n {this.heading}\n </ds-text>\n <div class=\"panel-tool-header__trailing ds-chrome-header__trailing\">\n {actions.map(action => (\n <ds-tooltip key={action.id} label={action.ariaLabel} side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n id={action.triggerId || undefined}\n data-header-action-id={action.id}\n class={{\n 'panel-tool-header__action': true,\n 'panel-tool-header__menu': action.id === 'menu',\n }}\n variant=\"icon\"\n icon={action.icon}\n size=\"md\"\n aria-label={action.ariaLabel}\n haspopup={action.haspopup}\n controls={action.controls}\n expanded={action.expanded}\n pressed={action.pressed}\n isActive={!!action.expanded}\n isInactive={action.isInactive}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n if (!authoredActions && action.id === 'menu')\n this.dsMenuToggle.emit(event.detail);\n this.dsAction.emit({ id: action.id, originalEvent: event.detail });\n }}\n />\n </ds-tooltip>\n ))}\n </div>\n </header>\n </Host>\n );\n }\n}\n",
|
|
340
340
|
"type": "registry:ui"
|
|
341
341
|
}
|
|
342
342
|
]
|