@ds-mo/ui 9.2.0 → 10.0.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/README.md +2 -81
- package/dist/.build-stamp +1 -1
- package/dist/agent-patterns.json +1 -1
- package/dist/agent.json +400 -135
- package/dist/angular/boolean-value-accessor.js +3 -3
- package/dist/angular/ds-agent-activity.js +3 -3
- package/dist/angular/ds-agent-response.js +3 -3
- package/dist/angular/ds-agent-source-list.js +3 -3
- package/dist/angular/ds-agent-tool-call.js +3 -3
- package/dist/angular/ds-app-shell.js +3 -3
- package/dist/angular/ds-attachment-list.js +3 -3
- package/dist/angular/ds-avatar.d.ts +1 -1
- package/dist/angular/ds-avatar.js +5 -5
- package/dist/angular/ds-badge.js +3 -3
- package/dist/angular/ds-bar-nav.js +3 -3
- package/dist/angular/ds-button-filled.js +3 -3
- package/dist/angular/ds-button-unfilled.js +3 -3
- package/dist/angular/ds-card-data-viz-donut.js +3 -3
- package/dist/angular/ds-card-setting.d.ts +5 -4
- package/dist/angular/ds-card-setting.js +6 -6
- package/dist/angular/ds-card-shell-data-viz.js +3 -3
- package/dist/angular/ds-chart-bar.js +3 -3
- package/dist/angular/ds-chart-donut.js +3 -3
- package/dist/angular/ds-chart-legend.js +3 -3
- package/dist/angular/ds-chart-line.js +3 -3
- package/dist/angular/ds-checkbox.js +3 -3
- package/dist/angular/ds-chip.js +3 -3
- package/dist/angular/ds-code-block.js +3 -3
- package/dist/angular/ds-conversation-list-item.d.ts +1 -1
- package/dist/angular/ds-conversation-list-item.js +5 -5
- package/dist/angular/ds-conversation-list-section.js +3 -3
- package/dist/angular/ds-conversation-list.js +3 -3
- package/dist/angular/ds-divider.js +3 -3
- package/dist/angular/ds-empty-state.js +3 -3
- package/dist/angular/ds-field.js +3 -3
- package/dist/angular/ds-icon.js +3 -3
- package/dist/angular/ds-input.js +3 -3
- package/dist/angular/ds-loader.js +3 -3
- package/dist/angular/ds-markdown.js +3 -3
- package/dist/angular/ds-menu.d.ts +6 -1
- package/dist/angular/ds-menu.js +7 -4
- package/dist/angular/ds-message-bubble.js +3 -3
- package/dist/angular/ds-message-composer.js +3 -3
- package/dist/angular/ds-message-scroller.js +3 -3
- package/dist/angular/ds-message.js +3 -3
- package/dist/angular/ds-modal.d.ts +12 -3
- package/dist/angular/ds-modal.js +9 -6
- package/dist/angular/ds-panel-nav.d.ts +1 -1
- package/dist/angular/ds-panel-nav.js +5 -5
- package/dist/angular/ds-panel-sub-nav.js +3 -3
- package/dist/angular/ds-panel-tool-header.js +3 -3
- package/dist/angular/ds-panel-tool-search.js +3 -3
- package/dist/angular/ds-panel-tools.js +3 -3
- package/dist/angular/ds-radio.js +3 -3
- package/dist/angular/ds-select-multi.js +3 -3
- package/dist/angular/ds-select.js +3 -3
- package/dist/angular/ds-shell-gradient-picker.js +3 -3
- package/dist/angular/ds-shell-gradient-swatch.js +3 -3
- package/dist/angular/ds-skeleton.js +3 -3
- package/dist/angular/ds-slider.js +3 -3
- package/dist/angular/ds-swatch-picker.js +3 -3
- package/dist/angular/ds-switch.js +3 -3
- package/dist/angular/ds-tab-group.js +3 -3
- package/dist/angular/ds-tag.js +3 -3
- package/dist/angular/ds-text.js +3 -3
- package/dist/angular/ds-toast.d.ts +22 -0
- package/dist/angular/{ds-banner.js → ds-toast.js} +21 -15
- package/dist/angular/ds-tooltip-data-viz.js +3 -3
- package/dist/angular/ds-tooltip.js +3 -3
- package/dist/angular/index.d.ts +1 -1
- package/dist/angular/index.js +1 -2
- package/dist/angular/proxies.d.ts +1 -2
- package/dist/angular/proxies.js +1 -2
- package/dist/angular/radio-value-accessor.js +3 -3
- package/dist/angular/select-value-accessor.js +3 -3
- package/dist/angular/text-value-accessor.js +3 -3
- package/dist/angular/value-accessor.js +3 -3
- package/dist/components/ds-agent-activity.js +1 -1
- package/dist/components/ds-agent-response.js +1 -1
- package/dist/components/ds-agent-response.js.map +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-app-shell.js +1 -1
- package/dist/components/ds-attachment-list.js +1 -1
- package/dist/components/ds-avatar.js +1 -1
- package/dist/components/ds-avatar.js.map +1 -1
- package/dist/components/ds-badge.js +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-nav.js.map +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-donut.js +1 -1
- package/dist/components/ds-card-data-viz-donut.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-card-shell-data-viz.js +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-item.js.map +1 -1
- package/dist/components/ds-conversation-list-section.js +1 -1
- package/dist/components/ds-conversation-list-section.js.map +1 -1
- package/dist/components/ds-conversation-list.js +1 -1
- package/dist/components/ds-conversation-list.js.map +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-icon.js +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-markdown.js +1 -1
- package/dist/components/ds-menu.js +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-composer.js.map +1 -1
- package/dist/components/ds-message-scroller.js +1 -1
- package/dist/components/ds-message-scroller.js.map +1 -1
- package/dist/components/ds-message.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-nav.js.map +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-panel-tools.js.map +1 -1
- package/dist/components/ds-radio.js +1 -1
- package/dist/components/ds-select-multi.js +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-shell-gradient-picker.js +1 -1
- package/dist/components/ds-shell-gradient-picker.js.map +1 -1
- package/dist/components/ds-shell-gradient-swatch.js +1 -1
- package/dist/components/ds-skeleton.js +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-slider.js.map +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-tag.js.map +1 -1
- package/dist/components/ds-text.js +1 -1
- package/dist/components/{ds-card.d.ts → ds-toast.d.ts} +4 -4
- package/dist/components/ds-toast.js +2 -0
- package/dist/components/ds-toast.js.map +1 -0
- package/dist/components/ds-tooltip-data-viz.js +1 -1
- package/dist/components/ds-tooltip.js +1 -1
- package/dist/components/{p-Bo5wikia.js → p-0kEg-Z4f.js} +2 -2
- package/dist/components/p-B4RQoNmL.js +2 -0
- package/dist/components/p-B4RQoNmL.js.map +1 -0
- package/dist/components/p-B8C_yfLQ.js +2 -0
- package/dist/components/p-B8C_yfLQ.js.map +1 -0
- package/dist/components/{p-Di-ThR90.js → p-BJUcsBHt.js} +2 -2
- package/dist/components/p-BJUcsBHt.js.map +1 -0
- package/dist/components/{p-C0sq948W.js → p-BTo4pNdp.js} +2 -2
- package/dist/components/p-Bet_UjML.js +2 -0
- package/dist/components/{p-BFWC17em.js.map → p-Bet_UjML.js.map} +1 -1
- package/dist/components/{p-2br9YDbG.js → p-BkwBtZs3.js} +2 -2
- package/dist/components/{p-CZMEhdlA.js → p-CKnoWZd9.js} +2 -2
- package/dist/components/p-CKnoWZd9.js.map +1 -0
- package/dist/components/p-CLn7QnE0.js +2 -0
- package/dist/components/p-CLn7QnE0.js.map +1 -0
- package/dist/components/{p-Bx1G7P6W.js → p-CPPAndX2.js} +2 -2
- package/dist/components/{p-Di4HBckb.js → p-CS12e52R.js} +2 -2
- package/dist/components/{p-BSOZXagj.js → p-CZZYGqKz.js} +2 -2
- package/dist/components/p-C_0Q8Cnp.js +2 -0
- package/dist/components/p-C_0Q8Cnp.js.map +1 -0
- package/dist/components/{p-t9Ei1kHP.js → p-CxyjoTN1.js} +2 -2
- package/dist/components/p-CyoHRqy6.js +2 -0
- package/dist/components/p-CyoHRqy6.js.map +1 -0
- package/dist/components/p-DBJ2I3mH.js +2 -0
- package/dist/components/p-DBJ2I3mH.js.map +1 -0
- package/dist/components/p-DM-95BX1.js +2 -0
- package/dist/components/p-DM-95BX1.js.map +1 -0
- package/dist/components/p-DZAtgSZo.js +2 -0
- package/dist/components/p-DZAtgSZo.js.map +1 -0
- package/dist/components/{p--KbMQblZ.js → p-Dh4q5TSS.js} +2 -2
- package/dist/components/{p-DIsDiGcp.js → p-DhsO0939.js} +2 -2
- package/dist/components/p-DhsO0939.js.map +1 -0
- package/dist/components/{p-CiwdnKKp.js → p-DrDHC7V6.js} +2 -2
- package/dist/components/{p-BZAEZENv.js → p-DsbhBb5F.js} +2 -2
- package/dist/components/p-DuNZGFkV.js +2 -0
- package/dist/components/p-DuNZGFkV.js.map +1 -0
- package/dist/components/{p-B1pWGQn1.js → p-P_4vE1ub.js} +2 -2
- package/dist/docs/components.json +1647 -1435
- package/dist/framework/select-multi-value-accessor.js +3 -3
- package/dist/lib/toast/index.d.ts +2 -0
- package/dist/lib/toast/index.js +158 -0
- package/dist/lib/toast/index.js.map +7 -0
- package/dist/lib/toast/toast-manager.d.ts +3 -0
- package/dist/lib/toast/toast-types.d.ts +85 -0
- package/dist/mcp-data/registry/agent-activity.json +1 -1
- package/dist/mcp-data/registry/agent-response.json +3 -3
- 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/app-shell.json +1 -1
- package/dist/mcp-data/registry/attachment-list.json +1 -1
- package/dist/mcp-data/registry/avatar.json +20 -2
- package/dist/mcp-data/registry/badge.json +1 -1
- package/dist/mcp-data/registry/bar-nav.json +2 -2
- package/dist/mcp-data/registry/button-filled.json +1 -1
- package/dist/mcp-data/registry/button-unfilled.json +3 -3
- package/dist/mcp-data/registry/card-data-viz-donut.json +2 -2
- package/dist/mcp-data/registry/card-setting.json +99 -13
- package/dist/mcp-data/registry/card-shell-data-viz.json +8 -8
- 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 +2 -2
- package/dist/mcp-data/registry/conversation-list-item.json +22 -4
- package/dist/mcp-data/registry/conversation-list-section.json +2 -2
- package/dist/mcp-data/registry/conversation-list.json +2 -2
- package/dist/mcp-data/registry/divider.json +5 -5
- 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 +2 -2
- package/dist/mcp-data/registry/input.json +1 -1
- package/dist/mcp-data/registry/loader.json +1 -1
- package/dist/mcp-data/registry/markdown.json +2 -2
- package/dist/mcp-data/registry/menu.json +18 -2
- package/dist/mcp-data/registry/message-bubble.json +7 -7
- package/dist/mcp-data/registry/message-composer.json +3 -3
- package/dist/mcp-data/registry/message-scroller.json +3 -3
- package/dist/mcp-data/registry/message.json +7 -6
- package/dist/mcp-data/registry/modal.json +154 -24
- package/dist/mcp-data/registry/panel-nav.json +21 -3
- package/dist/mcp-data/registry/panel-sub-nav.json +2 -2
- package/dist/mcp-data/registry/panel-tool-header.json +2 -2
- package/dist/mcp-data/registry/panel-tool-search.json +1 -1
- package/dist/mcp-data/registry/panel-tools.json +2 -2
- package/dist/mcp-data/registry/radio.json +1 -1
- package/dist/mcp-data/registry/registry.json +403 -141
- package/dist/mcp-data/registry/select-multi.json +1 -1
- package/dist/mcp-data/registry/select.json +1 -1
- package/dist/mcp-data/registry/shell-gradient-picker.json +1 -1
- package/dist/mcp-data/registry/shell-gradient-swatch.json +1 -1
- package/dist/mcp-data/registry/skeleton.json +2 -2
- package/dist/mcp-data/registry/slider.json +2 -2
- package/dist/mcp-data/registry/swatch-picker.json +1 -1
- package/dist/mcp-data/registry/switch.json +2 -2
- package/dist/mcp-data/registry/tab-group.json +1 -1
- package/dist/mcp-data/registry/tag.json +4 -4
- package/dist/mcp-data/registry/text.json +2 -2
- package/dist/mcp-data/registry/toast.json +318 -0
- package/dist/mcp-data/registry/tooltip-data-viz.json +1 -1
- package/dist/mcp-data/registry/tooltip.json +1 -1
- package/dist/react/components.d.ts +1 -2
- package/dist/react/components.js +1 -2
- package/dist/react/ds-card-setting.d.ts +2 -2
- package/dist/react/ds-card-setting.js +1 -1
- package/dist/react/ds-menu.d.ts +1 -0
- package/dist/react/ds-menu.js +1 -0
- package/dist/react/ds-modal.d.ts +3 -2
- package/dist/react/ds-modal.js +4 -1
- package/dist/react/ds-toast.d.ts +14 -0
- package/dist/react/ds-toast.js +16 -0
- package/dist/types/components/Avatar/Avatar.d.ts +3 -0
- package/dist/types/components/BarNav/BarNav.d.ts +2 -0
- package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +2 -0
- package/dist/types/components/CardSetting/CardSetting.d.ts +9 -6
- package/dist/types/components/CardSetting/index.d.ts +1 -1
- package/dist/types/components/ConversationListItem/ConversationListItem.d.ts +2 -0
- package/dist/types/components/Menu/Menu.d.ts +6 -0
- package/dist/types/components/MessageBubble/MessageBubble.d.ts +1 -1
- package/dist/types/components/MessageScroller/MessageScroller.d.ts +4 -0
- package/dist/types/components/Modal/Modal.d.ts +21 -18
- package/dist/types/components/PanelNav/PanelNav.d.ts +2 -0
- package/dist/types/components/Toast/Toast.d.ts +106 -0
- package/dist/types/components/Toast/index.d.ts +2 -0
- package/dist/types/components.d.ts +180 -186
- package/dist/types/toast/index.d.ts +3 -0
- package/dist/types/toast/toast-manager.d.ts +4 -0
- package/dist/types/toast/toast-types.d.ts +86 -0
- package/package.json +19 -18
- package/dist/angular/ds-banner.d.ts +0 -15
- package/dist/angular/ds-card.d.ts +0 -12
- package/dist/angular/ds-card.js +0 -38
- package/dist/components/ds-banner.d.ts +0 -11
- package/dist/components/ds-banner.js +0 -2
- package/dist/components/ds-banner.js.map +0 -1
- package/dist/components/ds-card.js +0 -2
- package/dist/components/ds-card.js.map +0 -1
- package/dist/components/p-41UAx2eq.js +0 -2
- package/dist/components/p-41UAx2eq.js.map +0 -1
- package/dist/components/p-B7NbIYjS.js +0 -2
- package/dist/components/p-B7NbIYjS.js.map +0 -1
- package/dist/components/p-BEQB8EnB.js +0 -2
- package/dist/components/p-BEQB8EnB.js.map +0 -1
- package/dist/components/p-BFWC17em.js +0 -2
- package/dist/components/p-BO6C8abO.js +0 -2
- package/dist/components/p-BO6C8abO.js.map +0 -1
- package/dist/components/p-CC1C8s6m.js +0 -2
- package/dist/components/p-CC1C8s6m.js.map +0 -1
- package/dist/components/p-CZMEhdlA.js.map +0 -1
- package/dist/components/p-CnM9SW_8.js +0 -2
- package/dist/components/p-CnM9SW_8.js.map +0 -1
- package/dist/components/p-CuKn9I5f.js +0 -2
- package/dist/components/p-CuKn9I5f.js.map +0 -1
- package/dist/components/p-DFMM60My.js +0 -2
- package/dist/components/p-DFMM60My.js.map +0 -1
- package/dist/components/p-DIsDiGcp.js.map +0 -1
- package/dist/components/p-Di-ThR90.js.map +0 -1
- package/dist/components/p-drBA73M_.js +0 -2
- package/dist/components/p-drBA73M_.js.map +0 -1
- package/dist/mcp-data/registry/banner.json +0 -206
- package/dist/mcp-data/registry/card.json +0 -126
- package/dist/react/ds-banner.d.ts +0 -12
- package/dist/react/ds-banner.js +0 -12
- package/dist/react/ds-card.d.ts +0 -9
- package/dist/react/ds-card.js +0 -12
- package/dist/types/components/Banner/Banner.d.ts +0 -31
- package/dist/types/components/Card/Card.d.ts +0 -21
- package/dist/types/components/Card/index.d.ts +0 -2
- /package/dist/components/{p-Bo5wikia.js.map → p-0kEg-Z4f.js.map} +0 -0
- /package/dist/components/{p-C0sq948W.js.map → p-BTo4pNdp.js.map} +0 -0
- /package/dist/components/{p-2br9YDbG.js.map → p-BkwBtZs3.js.map} +0 -0
- /package/dist/components/{p-Bx1G7P6W.js.map → p-CPPAndX2.js.map} +0 -0
- /package/dist/components/{p-Di4HBckb.js.map → p-CS12e52R.js.map} +0 -0
- /package/dist/components/{p-BSOZXagj.js.map → p-CZZYGqKz.js.map} +0 -0
- /package/dist/components/{p-t9Ei1kHP.js.map → p-CxyjoTN1.js.map} +0 -0
- /package/dist/components/{p--KbMQblZ.js.map → p-Dh4q5TSS.js.map} +0 -0
- /package/dist/components/{p-CiwdnKKp.js.map → p-DrDHC7V6.js.map} +0 -0
- /package/dist/components/{p-BZAEZENv.js.map → p-DsbhBb5F.js.map} +0 -0
- /package/dist/components/{p-B1pWGQn1.js.map → p-P_4vE1ub.js.map} +0 -0
|
@@ -237,7 +237,7 @@
|
|
|
237
237
|
"peerDependencies": {
|
|
238
238
|
"required": [
|
|
239
239
|
"@ds-mo/tokens ^3.1.1",
|
|
240
|
-
"@ds-mo/icons ^6.
|
|
240
|
+
"@ds-mo/icons ^6.3.0"
|
|
241
241
|
],
|
|
242
242
|
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
243
243
|
}
|
|
@@ -253,12 +253,12 @@
|
|
|
253
253
|
"files": [
|
|
254
254
|
{
|
|
255
255
|
"path": "src/wc/components/MessageComposer/MessageComposer.css",
|
|
256
|
-
"content": ":host {\n display: block;\n width: 100%;\n box-sizing: border-box;\n}\n\n.message-composer {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n width: 100%;\n margin: 0;\n}\n\n.message-composer__attachments:empty,\n.message-composer__tools:empty {\n display: none;\n}\n\n.message-composer__field {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n padding: var(--dimension-space-100);\n border-radius: var(--dimension-radius-
|
|
256
|
+
"content": ":host {\n display: block;\n width: 100%;\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.message-composer {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n width: 100%;\n margin: 0;\n}\n\n.message-composer__attachments:empty,\n.message-composer__tools:empty {\n display: none;\n}\n\n.message-composer__field {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n padding: var(--dimension-space-100);\n border-radius: var(--dimension-radius-050);\n background: var(--color-background-primary);\n box-shadow: var(--effect-elevation-elevated-md);\n overflow: hidden;\n}\n\n.message-composer--error .message-composer__field {\n box-shadow: var(--effect-elevation-elevated-md),\n inset 0 0 0 var(--dimension-stroke-width-025) var(--color-border-bold-negative);\n}\n\ntextarea {\n width: 100%;\n min-height: calc(var(--typography-lineheight-md) * 2 + var(--dimension-space-150));\n max-height: calc(var(--typography-lineheight-md) * 6 + var(--dimension-space-150));\n padding: var(--dimension-space-075);\n border: none;\n outline: none;\n resize: none;\n overflow-y: auto;\n box-sizing: border-box;\n background: transparent;\n color: var(--color-foreground-primary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n -webkit-user-select: text;\n user-select: text;\n}\n\ntextarea::placeholder {\n color: var(--color-foreground-secondary);\n}\n\ntextarea:focus::placeholder {\n color: var(--color-foreground-quaternary);\n}\n\n.message-composer__tools {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n min-height: var(--dimension-size-400);\n}\n\n.message-composer__controls {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--dimension-space-050);\n width: 100%;\n min-width: 0;\n}\n\n.message-composer__actions {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n flex: 0 0 auto;\n margin-inline-start: auto;\n}\n",
|
|
257
257
|
"type": "registry:ui"
|
|
258
258
|
},
|
|
259
259
|
{
|
|
260
260
|
"path": "src/wc/components/MessageComposer/MessageComposer.tsx",
|
|
261
|
-
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n Watch,\n} from '@stencil/core';\nimport type { ButtonFilledIntent } from '../ButtonFilled/ButtonFilled';\nimport type { MessageComposerStatus } from '../conversation-types';\n\n@Component({\n tag: 'ds-message-composer',\n styleUrl: 'MessageComposer.css',\n scoped: true,\n})\nexport class MessageComposer {\n @Element() el!: HTMLElement;\n\n @Prop({ mutable: true }) value: string = '';\n @Prop() placeholder: string = 'Write a message';\n @Prop() label: string = 'Message';\n @Prop() status: MessageComposerStatus = 'ready';\n @Prop() isInactive: boolean = false;\n @Prop() submitIntent: ButtonFilledIntent = 'brand';\n\n @Event() dsInput!: EventEmitter<string>;\n @Event() dsSubmit!: EventEmitter<{ text: string }>;\n @Event() dsStop!: EventEmitter<void>;\n\n private textarea?: HTMLTextAreaElement;\n\n componentDidLoad() {\n this.resize();\n }\n\n @Watch('value')\n onValueChange() {\n requestAnimationFrame(() => this.resize());\n }\n\n @Method()\n async setFocus() {\n this.textarea?.focus();\n }\n\n private get streaming(): boolean {\n return this.status === 'submitted' || this.status === 'streaming';\n }\n\n private resize() {\n if (!this.textarea) return;\n this.textarea.style.height = 'auto';\n this.textarea.style.height = `${this.textarea.scrollHeight}px`;\n }\n\n private handleInput = (event: Event) => {\n this.value = (event.target as HTMLTextAreaElement).value;\n this.resize();\n this.dsInput.emit(this.value);\n };\n\n private submit() {\n const text = this.value.trim();\n if (!text || this.isInactive || this.streaming) return;\n this.dsSubmit.emit({ text });\n }\n\n private handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Enter' || event.shiftKey || event.isComposing) return;\n event.preventDefault();\n this.submit();\n };\n\n private handleAction = () => {\n if (this.streaming) this.dsStop.emit();\n else this.submit();\n };\n\n render() {\n const error = this.status === 'error';\n return (\n <Host>\n <form\n class={{ 'message-composer': true, 'message-composer--error': error }}\n aria-busy={this.streaming ? 'true' : undefined}\n onSubmit={(event: Event) => {\n event.preventDefault();\n this.submit();\n }}\n >\n <div class=\"message-composer__attachments\">\n <slot name=\"attachments\" />\n </div>\n <div class=\"message-composer__field\">\n <textarea\n ref={element => {\n this.textarea = element;\n }}\n value={this.value}\n placeholder={this.placeholder}\n aria-label={this.label}\n aria-invalid={error ? 'true' : undefined}\n disabled={this.isInactive}\n rows={2}\n onInput={this.handleInput}\n onKeyDown={this.handleKeyDown}\n />\n <div class=\"message-composer__controls\">\n <div class=\"message-composer__tools\">\n <slot name=\"tools\" />\n </div>\n <div class=\"message-composer__actions\">\n <slot name=\"actions\" />\n <ds-button-filled\n class=\"message-composer__action\"\n variant=\"icon\"\n icon={this.streaming ? '
|
|
261
|
+
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n Watch,\n} from '@stencil/core';\nimport type { ButtonFilledIntent } from '../ButtonFilled/ButtonFilled';\nimport type { MessageComposerStatus } from '../conversation-types';\n\n@Component({\n tag: 'ds-message-composer',\n styleUrl: 'MessageComposer.css',\n scoped: true,\n})\nexport class MessageComposer {\n @Element() el!: HTMLElement;\n\n @Prop({ mutable: true }) value: string = '';\n @Prop() placeholder: string = 'Write a message';\n @Prop() label: string = 'Message';\n @Prop() status: MessageComposerStatus = 'ready';\n @Prop() isInactive: boolean = false;\n @Prop() submitIntent: ButtonFilledIntent = 'brand';\n\n @Event() dsInput!: EventEmitter<string>;\n @Event() dsSubmit!: EventEmitter<{ text: string }>;\n @Event() dsStop!: EventEmitter<void>;\n\n private textarea?: HTMLTextAreaElement;\n\n componentDidLoad() {\n this.resize();\n }\n\n @Watch('value')\n onValueChange() {\n requestAnimationFrame(() => this.resize());\n }\n\n @Method()\n async setFocus() {\n this.textarea?.focus();\n }\n\n private get streaming(): boolean {\n return this.status === 'submitted' || this.status === 'streaming';\n }\n\n private resize() {\n if (!this.textarea) return;\n this.textarea.style.height = 'auto';\n this.textarea.style.height = `${this.textarea.scrollHeight}px`;\n }\n\n private handleInput = (event: Event) => {\n this.value = (event.target as HTMLTextAreaElement).value;\n this.resize();\n this.dsInput.emit(this.value);\n };\n\n private submit() {\n const text = this.value.trim();\n if (!text || this.isInactive || this.streaming) return;\n this.dsSubmit.emit({ text });\n }\n\n private handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Enter' || event.shiftKey || event.isComposing) return;\n event.preventDefault();\n this.submit();\n };\n\n private handleAction = () => {\n if (this.streaming) this.dsStop.emit();\n else this.submit();\n };\n\n render() {\n const error = this.status === 'error';\n return (\n <Host>\n <form\n class={{ 'message-composer': true, 'message-composer--error': error }}\n aria-busy={this.streaming ? 'true' : undefined}\n onSubmit={(event: Event) => {\n event.preventDefault();\n this.submit();\n }}\n >\n <div class=\"message-composer__attachments\">\n <slot name=\"attachments\" />\n </div>\n <div class=\"message-composer__field\">\n <textarea\n ref={element => {\n this.textarea = element;\n }}\n value={this.value}\n placeholder={this.placeholder}\n aria-label={this.label}\n aria-invalid={error ? 'true' : undefined}\n disabled={this.isInactive}\n rows={2}\n onInput={this.handleInput}\n onKeyDown={this.handleKeyDown}\n />\n <div class=\"message-composer__controls\">\n <div class=\"message-composer__tools\">\n <slot name=\"tools\" />\n </div>\n <div class=\"message-composer__actions\">\n <slot name=\"actions\" />\n <ds-button-filled\n class=\"message-composer__action\"\n variant=\"icon\"\n icon={this.streaming ? 'SquareFilled' : 'ArrowUp'}\n intent={this.streaming ? 'neutral' : this.submitIntent}\n size=\"md\"\n rounded\n aria-label={this.streaming ? 'Stop response' : 'Send message'}\n isInactive={this.isInactive || (!this.streaming && !this.value.trim())}\n onDsClick={this.handleAction}\n />\n </div>\n </div>\n </div>\n </form>\n </Host>\n );\n }\n}\n",
|
|
262
262
|
"type": "registry:ui"
|
|
263
263
|
}
|
|
264
264
|
]
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
"peerDependencies": {
|
|
190
190
|
"required": [
|
|
191
191
|
"@ds-mo/tokens ^3.1.1",
|
|
192
|
-
"@ds-mo/icons ^6.
|
|
192
|
+
"@ds-mo/icons ^6.3.0"
|
|
193
193
|
],
|
|
194
194
|
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
195
195
|
}
|
|
@@ -205,12 +205,12 @@
|
|
|
205
205
|
"files": [
|
|
206
206
|
{
|
|
207
207
|
"path": "src/wc/components/MessageScroller/MessageScroller.css",
|
|
208
|
-
"content": ":host {\n display: block;\n width: 100%;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n}\n\n.message-scroller {\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 0;\n}\n\n.message-scroller__viewport {\n width: 100%;\n height: 100%;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n box-sizing: border-box;\n}\n\n.message-scroller__viewport:focus-visible {\n outline: var(--dimension-stroke-width-025) solid var(--color-border-bold-brand);\n outline-offset: calc(-1 * var(--dimension-stroke-width-025));\n}\n\n.message-scroller__content {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-
|
|
208
|
+
"content": ":host {\n display: block;\n width: 100%;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n}\n\n.message-scroller {\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 0;\n}\n\n.message-scroller__viewport {\n width: 100%;\n height: 100%;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n box-sizing: border-box;\n}\n\n.message-scroller__viewport:focus-visible {\n outline: var(--dimension-stroke-width-025) solid var(--color-border-bold-brand);\n outline-offset: calc(-1 * var(--dimension-stroke-width-025));\n}\n\n.message-scroller__content {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-300);\n width: 100%;\n max-width: var(--dimension-panel-width-2xl);\n min-height: 100%;\n margin-inline: auto;\n padding-block-start: var(--dimension-space-200);\n padding-inline: var(--dimension-space-200);\n padding-block-end: calc(\n var(--dimension-space-200) + var(--ds-message-scroller-overlay-block-size, 0px)\n );\n box-sizing: border-box;\n}\n\n.message-scroller__overlay {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: var(--dimension-z-index-floating);\n}\n\n.message-scroller__control {\n position: absolute;\n bottom: calc(\n var(--ds-message-scroller-overlay-content-block-size, 0px) + var(--dimension-space-200)\n );\n left: 50%;\n z-index: var(--dimension-z-index-floating);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--dimension-radius-half);\n background: var(--color-background-transparent);\n box-shadow: var(--effect-elevation-elevated-md);\n transform: translateX(-50%);\n -webkit-backdrop-filter: blur(var(--effect-blur-sm));\n backdrop-filter: blur(var(--effect-blur-sm));\n}\n",
|
|
209
209
|
"type": "registry:ui"
|
|
210
210
|
},
|
|
211
211
|
{
|
|
212
212
|
"path": "src/wc/components/MessageScroller/MessageScroller.tsx",
|
|
213
|
-
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n} from '@stencil/core';\nimport type { MessageScrollerPosition } from '../conversation-types';\n\nconst LIVE_EDGE_THRESHOLD = 24;\n\n@Component({\n tag: 'ds-message-scroller',\n styleUrl: 'MessageScroller.css',\n scoped: true,\n})\nexport class MessageScroller {\n @Element() el!: HTMLElement;\n\n @Prop() busy: boolean = false;\n @Prop() autoFollow: boolean = true;\n @Prop() defaultPosition: MessageScrollerPosition = 'last-anchor';\n @Prop() messagesLabel: string = 'Messages';\n\n @Event() dsReachStart!: EventEmitter<void>;\n\n @State() private showScrollToLatest = false;\n\n private viewport?: HTMLElement;\n private content?: HTMLElement;\n private overlay?: HTMLElement;\n private resizeObserver?: ResizeObserver;\n private overlayResizeObserver?: ResizeObserver;\n private mutationObserver?: MutationObserver;\n private following = true;\n private atStart = false;\n private programmatic = false;\n private previousScrollHeight = 0;\n\n componentDidLoad() {\n this.connectObservers();\n requestAnimationFrame(() => this.applyDefaultPosition());\n document.addEventListener('selectionchange', this.handleSelectionChange);\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect();\n this.overlayResizeObserver?.disconnect();\n this.mutationObserver?.disconnect();\n document.removeEventListener('selectionchange', this.handleSelectionChange);\n }\n\n @Method()\n async scrollToMessage(id: string): Promise<boolean> {\n const message = Array.from(\n this.el.querySelectorAll<HTMLElement & { messageId?: string }>('ds-message')\n ).find(item => item.messageId === id || item.getAttribute('message-id') === id);\n if (!message) return false;\n this.following = false;\n this.showScrollToLatest = true;\n message.scrollIntoView({ block: 'start', behavior: this.motionBehavior() });\n return true;\n }\n\n @Method()\n async scrollToStart() {\n this.scrollTo(0);\n this.following = false;\n this.showScrollToLatest = Boolean(\n this.viewport && this.viewport.scrollHeight > this.viewport.clientHeight\n );\n }\n\n @Method()\n async scrollToEnd() {\n this.following = true;\n this.showScrollToLatest = false;\n this.scrollTo(this.viewport?.scrollHeight ?? 0);\n }\n\n private motionBehavior(): ScrollBehavior {\n return matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';\n }\n\n private scrollTo(top: number) {\n if (!this.viewport) return;\n this.programmatic = true;\n this.viewport.scrollTo({ top, behavior: this.motionBehavior() });\n requestAnimationFrame(() => {\n this.programmatic = false;\n });\n }\n\n private connectObservers() {\n if (!this.viewport || !this.content) return;\n this.previousScrollHeight = this.viewport.scrollHeight;\n this.resizeObserver = new ResizeObserver(() => this.handleContentGrowth());\n this.resizeObserver.observe(this.content);\n if (this.overlay) {\n this.overlayResizeObserver = new ResizeObserver(entries => {\n const blockSize = Math.ceil(entries[0]?.contentRect.height ?? 0);\n this.el.style.setProperty('--ds-message-scroller-overlay-block-size', `${blockSize}px`);\n this.handleContentGrowth();\n });\n this.overlayResizeObserver.observe(this.overlay);\n }\n this.mutationObserver = new MutationObserver(() => this.handleContentGrowth());\n this.mutationObserver.observe(this.content, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n\n private handleContentGrowth() {\n if (!this.viewport) return;\n const nextHeight = this.viewport.scrollHeight;\n const delta = nextHeight - this.previousScrollHeight;\n if (delta > 0 && this.viewport.scrollTop <= LIVE_EDGE_THRESHOLD && !this.following) {\n this.viewport.scrollTop += delta;\n } else if (this.autoFollow && this.following) {\n this.viewport.scrollTop = nextHeight;\n }\n this.previousScrollHeight = nextHeight;\n this.showScrollToLatest = !this.following && !this.isAtLiveEdge();\n }\n\n private applyDefaultPosition() {\n if (!this.viewport) return;\n if (this.defaultPosition === 'start') {\n this.viewport.scrollTop = 0;\n this.following = false;\n this.showScrollToLatest = this.viewport.scrollHeight > this.viewport.clientHeight;\n return;\n }\n if (this.defaultPosition === 'last-anchor') {\n const anchors = this.el.querySelectorAll<HTMLElement>('ds-message[scroll-anchor]');\n const last = anchors.item(anchors.length - 1);\n if (last) {\n last.scrollIntoView({ block: 'start' });\n this.following = this.isAtLiveEdge();\n this.showScrollToLatest = !this.following;\n return;\n }\n }\n this.viewport.scrollTop = this.viewport.scrollHeight;\n this.following = true;\n this.showScrollToLatest = false;\n }\n\n private isAtLiveEdge(): boolean {\n if (!this.viewport) return true;\n return (\n this.viewport.scrollHeight - this.viewport.clientHeight - this.viewport.scrollTop <=\n LIVE_EDGE_THRESHOLD\n );\n }\n\n private handleScroll = () => {\n if (!this.viewport
|
|
213
|
+
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n} from '@stencil/core';\nimport type { MessageScrollerPosition } from '../conversation-types';\n\nconst LIVE_EDGE_THRESHOLD = 24;\n\n@Component({\n tag: 'ds-message-scroller',\n styleUrl: 'MessageScroller.css',\n styleUrls: ['../../utils/scroll-edge-fade.css'],\n scoped: true,\n})\nexport class MessageScroller {\n @Element() el!: HTMLElement;\n\n @Prop() busy: boolean = false;\n @Prop() autoFollow: boolean = true;\n @Prop() defaultPosition: MessageScrollerPosition = 'last-anchor';\n @Prop() messagesLabel: string = 'Messages';\n\n @Event() dsReachStart!: EventEmitter<void>;\n\n @State() private showScrollToLatest = false;\n\n private viewport?: HTMLElement;\n private content?: HTMLElement;\n private overlay?: HTMLElement;\n private resizeObserver?: ResizeObserver;\n private overlayResizeObserver?: ResizeObserver;\n private mutationObserver?: MutationObserver;\n private following = true;\n private atStart = false;\n private programmatic = false;\n private previousScrollHeight = 0;\n private overlayBlockSize = 0;\n private overlayContentBlockSize = 0;\n\n componentDidLoad() {\n this.connectObservers();\n requestAnimationFrame(() => this.applyDefaultPosition());\n document.addEventListener('selectionchange', this.handleSelectionChange);\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect();\n this.overlayResizeObserver?.disconnect();\n this.mutationObserver?.disconnect();\n document.removeEventListener('selectionchange', this.handleSelectionChange);\n }\n\n @Method()\n async scrollToMessage(id: string): Promise<boolean> {\n const message = Array.from(\n this.el.querySelectorAll<HTMLElement & { messageId?: string }>('ds-message')\n ).find(item => item.messageId === id || item.getAttribute('message-id') === id);\n if (!message) return false;\n this.following = false;\n this.showScrollToLatest = true;\n message.scrollIntoView({ block: 'start', behavior: this.motionBehavior() });\n return true;\n }\n\n @Method()\n async scrollToStart() {\n this.scrollTo(0);\n this.following = false;\n this.showScrollToLatest = Boolean(\n this.viewport && this.viewport.scrollHeight > this.viewport.clientHeight\n );\n }\n\n @Method()\n async scrollToEnd() {\n this.following = true;\n this.showScrollToLatest = false;\n this.scrollTo(this.viewport?.scrollHeight ?? 0);\n }\n\n private motionBehavior(): ScrollBehavior {\n return matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';\n }\n\n private scrollTo(top: number) {\n if (!this.viewport) return;\n this.programmatic = true;\n this.viewport.scrollTo({ top, behavior: this.motionBehavior() });\n requestAnimationFrame(() => {\n this.programmatic = false;\n });\n }\n\n private connectObservers() {\n if (!this.viewport || !this.content) return;\n this.previousScrollHeight = this.viewport.scrollHeight;\n this.resizeObserver = new ResizeObserver(() => this.handleContentGrowth());\n this.resizeObserver.observe(this.content);\n this.resizeObserver.observe(this.viewport);\n if (this.overlay) {\n this.overlayResizeObserver = new ResizeObserver(entries => {\n const blockSize = Math.ceil(entries[0]?.contentRect.height ?? 0);\n this.overlayBlockSize = blockSize;\n this.el.style.setProperty('--ds-message-scroller-overlay-block-size', `${blockSize}px`);\n this.syncOverlayContentBlockSize();\n this.handleContentGrowth();\n });\n this.overlayResizeObserver.observe(this.overlay);\n }\n this.mutationObserver = new MutationObserver(() => this.handleContentGrowth());\n this.mutationObserver.observe(this.content, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n\n private handleContentGrowth() {\n if (!this.viewport) return;\n const nextHeight = this.viewport.scrollHeight;\n const delta = nextHeight - this.previousScrollHeight;\n if (delta > 0 && this.viewport.scrollTop <= LIVE_EDGE_THRESHOLD && !this.following) {\n this.viewport.scrollTop += delta;\n } else if (this.autoFollow && this.following) {\n this.viewport.scrollTop = nextHeight;\n }\n this.previousScrollHeight = nextHeight;\n this.showScrollToLatest = !this.following && !this.isAtLiveEdge();\n this.syncContentFade();\n }\n\n private syncContentFade() {\n if (!this.viewport || !this.content) return;\n const fadeEnd = Math.max(this.viewport.scrollTop + this.viewport.clientHeight, 0);\n const fadeStart = Math.max(fadeEnd - this.overlayContentBlockSize, 0);\n this.content.style.setProperty('--ds-scroll-edge-fade-window-start', `${fadeStart}px`);\n this.content.style.setProperty('--ds-scroll-edge-fade-window-end', `${fadeEnd}px`);\n }\n\n private syncOverlayContentBlockSize() {\n if (!this.overlay) return;\n const assigned = this.el.querySelector<HTMLElement>('[slot=\"overlay\"]');\n if (!(assigned instanceof HTMLElement)) {\n this.overlayContentBlockSize = this.overlayBlockSize;\n } else {\n const visibleContent =\n assigned.firstElementChild instanceof HTMLElement ? assigned.firstElementChild : assigned;\n const topInset = Math.max(\n Math.ceil(visibleContent.getBoundingClientRect().top - this.overlay.getBoundingClientRect().top),\n 0\n );\n this.overlayContentBlockSize = Math.max(this.overlayBlockSize - topInset, 0);\n }\n this.el.style.setProperty(\n '--ds-message-scroller-overlay-content-block-size',\n `${this.overlayContentBlockSize}px`\n );\n }\n\n private applyDefaultPosition() {\n if (!this.viewport) return;\n if (this.defaultPosition === 'start') {\n this.viewport.scrollTop = 0;\n this.following = false;\n this.showScrollToLatest = this.viewport.scrollHeight > this.viewport.clientHeight;\n this.syncContentFade();\n return;\n }\n if (this.defaultPosition === 'last-anchor') {\n const anchors = this.el.querySelectorAll<HTMLElement>('ds-message[scroll-anchor]');\n const last = anchors.item(anchors.length - 1);\n if (last) {\n last.scrollIntoView({ block: 'start' });\n this.following = this.isAtLiveEdge();\n this.showScrollToLatest = !this.following;\n this.syncContentFade();\n return;\n }\n }\n this.viewport.scrollTop = this.viewport.scrollHeight;\n this.following = true;\n this.showScrollToLatest = false;\n this.syncContentFade();\n }\n\n private isAtLiveEdge(): boolean {\n if (!this.viewport) return true;\n return (\n this.viewport.scrollHeight - this.viewport.clientHeight - this.viewport.scrollTop <=\n LIVE_EDGE_THRESHOLD\n );\n }\n\n private handleScroll = () => {\n if (!this.viewport) return;\n this.syncContentFade();\n if (this.programmatic) return;\n this.following = this.isAtLiveEdge();\n this.showScrollToLatest = !this.following;\n const nextAtStart = this.viewport.scrollTop <= LIVE_EDGE_THRESHOLD;\n if (nextAtStart && !this.atStart) this.dsReachStart.emit();\n this.atStart = nextAtStart;\n };\n\n private releaseFollow = () => {\n this.following = false;\n this.showScrollToLatest = !this.isAtLiveEdge();\n };\n\n private handleSelectionChange = () => {\n const selection = document.getSelection();\n if (\n selection &&\n !selection.isCollapsed &&\n selection.anchorNode &&\n this.el.contains(selection.anchorNode)\n ) {\n this.following = false;\n this.showScrollToLatest = !this.isAtLiveEdge();\n }\n };\n\n render() {\n return (\n <Host>\n <div class=\"message-scroller\">\n <div\n class=\"message-scroller__viewport\"\n ref={element => {\n this.viewport = element;\n }}\n role=\"region\"\n aria-label={this.messagesLabel}\n tabIndex={0}\n onScroll={this.handleScroll}\n onPointerDown={this.releaseFollow}\n onKeyDown={(event: KeyboardEvent) => {\n if (['ArrowUp', 'PageUp', 'Home'].includes(event.key)) this.releaseFollow();\n }}\n >\n <div\n class=\"message-scroller__content scroll-edge-fade--block-window\"\n ref={element => {\n this.content = element;\n }}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions\"\n aria-busy={this.busy ? 'true' : undefined}\n >\n <slot />\n </div>\n </div>\n <div\n class=\"message-scroller__overlay\"\n ref={element => {\n this.overlay = element;\n }}\n >\n <slot name=\"overlay\" />\n </div>\n {this.showScrollToLatest ? (\n <div class=\"message-scroller__control\">\n <ds-button-unfilled\n variant=\"icon\"\n icon=\"ChevronDown\"\n size=\"md\"\n rounded\n hasBorder={false}\n aria-label=\"Scroll to latest message\"\n onDsClick={() => void this.scrollToEnd()}\n />\n </div>\n ) : null}\n </div>\n </Host>\n );\n }\n}\n",
|
|
214
214
|
"type": "registry:ui"
|
|
215
215
|
}
|
|
216
216
|
]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
3
|
"name": "message",
|
|
4
4
|
"title": "Message",
|
|
5
|
-
"description": "Channel-neutral message row owning direction, grouping, author, time, delivery metadata, and accessory regions.",
|
|
5
|
+
"description": "Channel-neutral message row owning direction, grouping, author, time, delivery metadata, and header, footer, or action accessory regions.",
|
|
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": "experimental",
|
|
16
|
-
"summary": "Channel-neutral message row owning direction, grouping, author, time, delivery metadata, and accessory regions.",
|
|
16
|
+
"summary": "Channel-neutral message row owning direction, grouping, author, time, delivery metadata, and header, footer, or action accessory regions.",
|
|
17
17
|
"useWhen": [
|
|
18
18
|
"A transcript contains messages that need consistent incoming, outgoing, or system layout."
|
|
19
19
|
],
|
|
@@ -29,10 +29,11 @@
|
|
|
29
29
|
],
|
|
30
30
|
"accessibility": [
|
|
31
31
|
"Author and time metadata remain textual when they are necessary to interpret the transcript.",
|
|
32
|
-
"Delivery failure
|
|
32
|
+
"Delivery failure uses negative Failed to send text in the footer while the message bubble keeps its normal outgoing treatment."
|
|
33
33
|
],
|
|
34
34
|
"states": [
|
|
35
35
|
"Pass an ISO timestamp. Message keeps the full value in dateTime and formats the visible value as a localized time; applications must not render a raw ISO string as visible transcript metadata.",
|
|
36
|
+
"Visible author, bubble content, and timestamp or delivery footer use consistent 4px vertical gaps.",
|
|
36
37
|
"Grouping joins adjacent bubble corners, shows the incoming author only at the start, and shows delivery/time metadata only at the end. Outgoing author remains in the accessible message label without repeating a visible You label.",
|
|
37
38
|
"Set showAuthor false only when the product makes participant and assistant roles visually self-evident; continue passing author so the accessible message label remains specific."
|
|
38
39
|
],
|
|
@@ -227,7 +228,7 @@
|
|
|
227
228
|
"peerDependencies": {
|
|
228
229
|
"required": [
|
|
229
230
|
"@ds-mo/tokens ^3.1.1",
|
|
230
|
-
"@ds-mo/icons ^6.
|
|
231
|
+
"@ds-mo/icons ^6.3.0"
|
|
231
232
|
],
|
|
232
233
|
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
233
234
|
}
|
|
@@ -243,12 +244,12 @@
|
|
|
243
244
|
"files": [
|
|
244
245
|
{
|
|
245
246
|
"path": "src/wc/components/Message/Message.css",
|
|
246
|
-
"content": ":host {\n display: block;\n box-sizing: border-box;\n content-visibility: auto;\n contain-intrinsic-size: auto var(--dimension-size-800);\n}\n\n:host(.message-host--group-middle),\n:host(.message-host--group-last) {\n margin-block-start: calc(-
|
|
247
|
+
"content": ":host {\n display: block;\n box-sizing: border-box;\n content-visibility: auto;\n contain-intrinsic-size: auto var(--dimension-size-800);\n -webkit-user-select: none;\n user-select: none;\n}\n\n:host(.message-host--group-middle),\n:host(.message-host--group-last) {\n margin-block-start: calc(var(--dimension-space-050) - var(--dimension-space-300));\n}\n\n.message {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n align-items: end;\n gap: 0;\n width: 100%;\n box-sizing: border-box;\n}\n\n.message--outgoing {\n grid-template-columns: minmax(0, 1fr) auto;\n}\n\n.message__header:empty,\n.message__footer:empty,\n.message__actions:empty {\n display: none;\n}\n\n.message__actions:not(:empty) {\n margin-inline-start: var(--dimension-space-075);\n}\n\n.message__body {\n display: flex;\n flex-direction: column;\n min-width: 0;\n gap: var(--dimension-space-050);\n}\n\n.message--incoming .message__body {\n width: 100%;\n}\n\n.message--outgoing .message__body {\n align-items: flex-end;\n}\n\n.message--outgoing .message__header {\n display: none;\n}\n\n.message--system .message__body {\n align-items: center;\n}\n\n.message__header,\n.message__footer,\n.message__actions {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n}\n\n.message__content {\n width: 100%;\n min-width: 0;\n -webkit-user-select: text;\n user-select: text;\n}\n\n.message--outgoing .message__content {\n display: flex;\n justify-content: flex-end;\n}\n\n.message__footer time {\n display: inline-flex;\n}\n\n.message--group-middle .message__header,\n.message--group-last .message__header {\n display: none;\n}\n\n.message--group-first .message__footer,\n.message--group-middle .message__footer {\n display: none;\n}\n",
|
|
247
248
|
"type": "registry:ui"
|
|
248
249
|
},
|
|
249
250
|
{
|
|
250
251
|
"path": "src/wc/components/Message/Message.tsx",
|
|
251
|
-
"content": "import { Component, h, Host, Prop } from '@stencil/core';\nimport type {\n MessageDeliveryState,\n MessageDirection,\n MessageGroupPosition,\n} from '../conversation-types';\n\nconst DELIVERY_LABELS: Record<MessageDeliveryState, string> = {\n sending: 'Sending',\n sent: 'Sent',\n delivered: 'Delivered',\n read: 'Read',\n failed: 'Failed to send',\n};\n\n@Component({\n tag: 'ds-message',\n styleUrl: 'Message.css',\n scoped: true,\n})\nexport class Message {\n @Prop({ reflect: true }) messageId: string = '';\n @Prop({ reflect: true }) direction: MessageDirection = 'incoming';\n @Prop() groupPosition: MessageGroupPosition = 'single';\n @Prop() author: string = '';\n /** Keep author semantics while allowing products with self-evident roles to hide the visible label. */\n @Prop() showAuthor: boolean = true;\n /** ISO timestamp. */\n @Prop() timestamp: string = '';\n @Prop() deliveryState: MessageDeliveryState | undefined;\n @Prop() streaming: boolean = false;\n @Prop({ reflect: true }) scrollAnchor: boolean = false;\n\n private formattedTimestamp(): string {\n if (!this.timestamp) return '';\n const parsed = Date.parse(this.timestamp);\n if (Number.isNaN(parsed)) return this.timestamp;\n return new Intl.DateTimeFormat(undefined, {\n hour: 'numeric',\n minute: '2-digit',\n }).format(new Date(parsed));\n }\n\n render() {\n const delivery = this.deliveryState ? DELIVERY_LABELS[this.deliveryState] : '';\n const formattedTimestamp = this.formattedTimestamp();\n return (\n <Host\n class={`message-host--group-${this.groupPosition}`}\n data-message-id={this.messageId || undefined}\n data-scroll-anchor={this.scrollAnchor ? 'true' : undefined}\n aria-busy={this.streaming ? 'true' : undefined}\n >\n <article\n class={{\n message: true,\n [`message--${this.direction}`]: true,\n [`message--group-${this.groupPosition}`]: true,\n }}\n aria-label={this.author ? `Message from ${this.author}` : 'Message'}\n >\n <div class=\"
|
|
252
|
+
"content": "import { Component, h, Host, Prop } from '@stencil/core';\nimport type {\n MessageDeliveryState,\n MessageDirection,\n MessageGroupPosition,\n} from '../conversation-types';\n\nconst DELIVERY_LABELS: Record<MessageDeliveryState, string> = {\n sending: 'Sending',\n sent: 'Sent',\n delivered: 'Delivered',\n read: 'Read',\n failed: 'Failed to send',\n};\n\n@Component({\n tag: 'ds-message',\n styleUrl: 'Message.css',\n scoped: true,\n})\nexport class Message {\n @Prop({ reflect: true }) messageId: string = '';\n @Prop({ reflect: true }) direction: MessageDirection = 'incoming';\n @Prop() groupPosition: MessageGroupPosition = 'single';\n @Prop() author: string = '';\n /** Keep author semantics while allowing products with self-evident roles to hide the visible label. */\n @Prop() showAuthor: boolean = true;\n /** ISO timestamp. */\n @Prop() timestamp: string = '';\n @Prop() deliveryState: MessageDeliveryState | undefined;\n @Prop() streaming: boolean = false;\n @Prop({ reflect: true }) scrollAnchor: boolean = false;\n\n private formattedTimestamp(): string {\n if (!this.timestamp) return '';\n const parsed = Date.parse(this.timestamp);\n if (Number.isNaN(parsed)) return this.timestamp;\n return new Intl.DateTimeFormat(undefined, {\n hour: 'numeric',\n minute: '2-digit',\n }).format(new Date(parsed));\n }\n\n render() {\n const delivery = this.deliveryState ? DELIVERY_LABELS[this.deliveryState] : '';\n const formattedTimestamp = this.formattedTimestamp();\n return (\n <Host\n class={`message-host--group-${this.groupPosition}`}\n data-message-id={this.messageId || undefined}\n data-scroll-anchor={this.scrollAnchor ? 'true' : undefined}\n aria-busy={this.streaming ? 'true' : undefined}\n >\n <article\n class={{\n message: true,\n [`message--${this.direction}`]: true,\n [`message--group-${this.groupPosition}`]: true,\n }}\n aria-label={this.author ? `Message from ${this.author}` : 'Message'}\n >\n <div class=\"message__body\">\n <div class=\"message__header\">\n <slot name=\"header\" />\n {this.author && this.showAuthor ? (\n <ds-text as=\"span\" variant=\"text-caption\" emphasis color=\"primary\">\n {this.author}\n </ds-text>\n ) : null}\n </div>\n <div class=\"message__content\">\n <slot />\n </div>\n <div class=\"message__footer\">\n <slot name=\"footer\" />\n {delivery ? (\n <ds-text\n as=\"span\"\n variant=\"text-caption\"\n color={this.deliveryState === 'failed' ? 'negative' : 'secondary'}\n >\n {delivery}\n </ds-text>\n ) : null}\n {formattedTimestamp ? (\n <time dateTime={this.timestamp}>\n <ds-text as=\"span\" variant=\"text-caption\" color=\"secondary\">\n {formattedTimestamp}\n </ds-text>\n </time>\n ) : null}\n </div>\n </div>\n <div class=\"message__actions\">\n <slot name=\"actions\" />\n </div>\n </article>\n </Host>\n );\n }\n}\n",
|
|
252
253
|
"type": "registry:ui"
|
|
253
254
|
}
|
|
254
255
|
]
|
|
@@ -2,17 +2,126 @@
|
|
|
2
2
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
3
|
"name": "modal",
|
|
4
4
|
"title": "Modal",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Dismissible native top-layer dialog for a focused confirmation or short blocking task, with a fixed title-and-close header, composable content, and an optional two-action footer.",
|
|
6
6
|
"type": "registry:ui",
|
|
7
7
|
"meta": {
|
|
8
8
|
"library": "@ds-mo/ui",
|
|
9
9
|
"distribution": "npm",
|
|
10
10
|
"storybook": "https://zainadeel.github.io/compomo/?path=/story/modal",
|
|
11
11
|
"source": "src/wc/components/Modal/Modal.tsx",
|
|
12
|
-
"intentStatus": "
|
|
13
|
-
"intent":
|
|
12
|
+
"intentStatus": "complete",
|
|
13
|
+
"intent": {
|
|
14
|
+
"audience": "general",
|
|
15
|
+
"status": "stable",
|
|
16
|
+
"summary": "Dismissible native top-layer dialog for a focused confirmation or short blocking task, with a fixed title-and-close header, composable content, and an optional two-action footer.",
|
|
17
|
+
"useWhen": [
|
|
18
|
+
"The user must confirm or cancel one focused decision before returning to the underlying interface.",
|
|
19
|
+
"A short blocking task or compact form needs dialog semantics, focus containment, and explicit completion actions.",
|
|
20
|
+
"The content can be safely dismissed through Close, Escape, or the backdrop."
|
|
21
|
+
],
|
|
22
|
+
"avoidWhen": [
|
|
23
|
+
"The message is non-blocking feedback or a transient status update.",
|
|
24
|
+
"The interaction is a contextual list of commands anchored to a trigger.",
|
|
25
|
+
"The workflow is multi-step, long-form, or requires substantial navigation.",
|
|
26
|
+
"Dismissal would lose work or leave an irreversible operation in an unsafe state.",
|
|
27
|
+
"A second modal would be opened over an existing modal."
|
|
28
|
+
],
|
|
29
|
+
"alternatives": [
|
|
30
|
+
{
|
|
31
|
+
"when": "The user needs non-blocking feedback or a transient status message.",
|
|
32
|
+
"component": "component:ds-toast",
|
|
33
|
+
"reason": "Toast communicates transient status without trapping focus or blocking the page."
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"when": "A trigger reveals a short contextual list of commands.",
|
|
37
|
+
"component": "component:ds-menu",
|
|
38
|
+
"reason": "Menu provides anchored command semantics without creating a blocking dialog."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"when": "Editable settings should remain visible without blocking the rest of the interface.",
|
|
42
|
+
"component": "component:ds-card-setting",
|
|
43
|
+
"reason": "CardSetting provides a persistent controlled settings workflow without modal focus containment."
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"commonlyComposedWith": [
|
|
47
|
+
"component:ds-button-filled",
|
|
48
|
+
"component:ds-button-unfilled",
|
|
49
|
+
"component:ds-field",
|
|
50
|
+
"component:ds-text"
|
|
51
|
+
],
|
|
52
|
+
"accessibility": [
|
|
53
|
+
"Provide a concise visible heading; the component uses it as the dialog's accessible name.",
|
|
54
|
+
"Give the primary explanatory copy a stable id and pass it through aria-describedby so assistive technology receives the decision context.",
|
|
55
|
+
"Opening the native modal dialog moves focus to the header Close action, traps Tab navigation inside the top layer, and makes outside content inert.",
|
|
56
|
+
"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.",
|
|
57
|
+
"After exit motion completes, the native dialog leaves the top layer, focus returns to the previously focused element, and dsAfterClose emits.",
|
|
58
|
+
"Use visible label-only footer buttons. Keep at most two actions: one bold filled primary action followed by one bordered unfilled action, or two bordered unfilled actions.",
|
|
59
|
+
"Use negative intent for a filled destructive action and state the irreversible consequence in the content.",
|
|
60
|
+
"Do not open a modal from another modal; keep one blocking decision in focus at a time."
|
|
61
|
+
],
|
|
62
|
+
"states": [
|
|
63
|
+
"The owner opens the modal through the open property and responds to the typed dsClose detail for application state synchronization or dismissal analytics.",
|
|
64
|
+
"The component mutates open to false for Close, Escape, and backdrop dismissal while retaining its rendered surface through exit motion.",
|
|
65
|
+
"The default slot owns task content; the named footer slot is optional and is hidden when no actions are assigned.",
|
|
66
|
+
"Primary actions use a bold filled button with semantic intent. Secondary and cancel actions use bordered label-only unfilled buttons.",
|
|
67
|
+
"The owner performs save, delete, cancel, and other product consequences before or while closing the modal."
|
|
68
|
+
],
|
|
69
|
+
"responsiveBehavior": [
|
|
70
|
+
"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 64px header and optional 64px footer remain fixed.",
|
|
72
|
+
"Avoid layouts whose minimum content width exceeds the narrow viewport clamp.",
|
|
73
|
+
"The native top layer escapes transformed ancestors, clipping containers, and application stacking contexts without moving slotted content."
|
|
74
|
+
],
|
|
75
|
+
"frameworkCaveats": {
|
|
76
|
+
"customElements": [
|
|
77
|
+
"Set open as a JavaScript property when controlling the modal; inspect dsClose.detail.reason and use dsAfterClose for cleanup that must wait for exit motion and focus restoration.",
|
|
78
|
+
"Connect the most relevant slotted explanatory text through aria-describedby.",
|
|
79
|
+
"Footer actions are slotted application-owned controls and must implement their own product consequence."
|
|
80
|
+
],
|
|
81
|
+
"react": [
|
|
82
|
+
"Pass open to DsModal, inspect onDsClose event detail, and use onDsAfterClose only for cleanup after exit motion and focus restoration.",
|
|
83
|
+
"Connect the most relevant slotted explanatory text through aria-describedby.",
|
|
84
|
+
"Assign footer controls with slot=\"footer\" and handle each action before updating open."
|
|
85
|
+
],
|
|
86
|
+
"angular": [
|
|
87
|
+
"Bind [open], inspect $event.detail from (dsClose), and use (dsAfterClose) only for cleanup after exit motion and focus restoration.",
|
|
88
|
+
"Connect the most relevant slotted explanatory text through aria-describedby.",
|
|
89
|
+
"Assign footer controls with slot=\"footer\" and handle each action before updating open."
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
"references": [
|
|
93
|
+
{
|
|
94
|
+
"label": "Storybook confirmation and footer examples",
|
|
95
|
+
"path": "src/wc/components/Modal/Modal.stories.ts"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"label": "Cross-browser focus, dismissal, geometry, and accessibility coverage",
|
|
99
|
+
"path": "tests/e2e/accessibility-overlays.spec.ts"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"label": "Reduced-motion overlay coverage",
|
|
103
|
+
"path": "tests/e2e/reduced-motion.spec.ts"
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
},
|
|
14
107
|
"api": {
|
|
15
108
|
"props": {
|
|
109
|
+
"ariaDescribedby": {
|
|
110
|
+
"type": "string | undefined",
|
|
111
|
+
"resolvedType": "string | undefined",
|
|
112
|
+
"attribute": "aria-describedby",
|
|
113
|
+
"required": false,
|
|
114
|
+
"mutable": false,
|
|
115
|
+
"description": "Optional id reference for explanatory content in the default slot."
|
|
116
|
+
},
|
|
117
|
+
"closeAriaLabel": {
|
|
118
|
+
"type": "string",
|
|
119
|
+
"resolvedType": "string",
|
|
120
|
+
"attribute": "close-aria-label",
|
|
121
|
+
"default": "'Close'",
|
|
122
|
+
"required": false,
|
|
123
|
+
"mutable": false
|
|
124
|
+
},
|
|
16
125
|
"heading": {
|
|
17
126
|
"type": "string",
|
|
18
127
|
"resolvedType": "string",
|
|
@@ -35,28 +144,46 @@
|
|
|
35
144
|
"default": "false",
|
|
36
145
|
"required": false,
|
|
37
146
|
"mutable": true
|
|
38
|
-
},
|
|
39
|
-
"subtitle": {
|
|
40
|
-
"type": "string | undefined",
|
|
41
|
-
"resolvedType": "string | undefined",
|
|
42
|
-
"attribute": "subtitle",
|
|
43
|
-
"required": false,
|
|
44
|
-
"mutable": false
|
|
45
147
|
}
|
|
46
148
|
},
|
|
47
149
|
"events": [
|
|
48
150
|
{
|
|
49
|
-
"name": "
|
|
151
|
+
"name": "dsAfterClose",
|
|
50
152
|
"detail": "void",
|
|
51
153
|
"bubbles": true,
|
|
52
154
|
"cancelable": true,
|
|
53
|
-
"composed": true
|
|
155
|
+
"composed": true,
|
|
156
|
+
"description": "Emitted after exit motion completes, the top layer closes, and focus is restored."
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"name": "dsClose",
|
|
160
|
+
"detail": "ModalCloseDetail",
|
|
161
|
+
"bubbles": true,
|
|
162
|
+
"cancelable": true,
|
|
163
|
+
"composed": true,
|
|
164
|
+
"description": "Emitted when an internal dismissal control requests that the modal close."
|
|
54
165
|
}
|
|
55
166
|
],
|
|
56
167
|
"methods": [],
|
|
57
168
|
"slots": []
|
|
58
169
|
},
|
|
59
170
|
"props": {
|
|
171
|
+
"ariaDescribedby": {
|
|
172
|
+
"type": "string | undefined",
|
|
173
|
+
"resolvedType": "string | undefined",
|
|
174
|
+
"attribute": "aria-describedby",
|
|
175
|
+
"required": false,
|
|
176
|
+
"mutable": false,
|
|
177
|
+
"description": "Optional id reference for explanatory content in the default slot."
|
|
178
|
+
},
|
|
179
|
+
"closeAriaLabel": {
|
|
180
|
+
"type": "string",
|
|
181
|
+
"resolvedType": "string",
|
|
182
|
+
"attribute": "close-aria-label",
|
|
183
|
+
"default": "'Close'",
|
|
184
|
+
"required": false,
|
|
185
|
+
"mutable": false
|
|
186
|
+
},
|
|
60
187
|
"heading": {
|
|
61
188
|
"type": "string",
|
|
62
189
|
"resolvedType": "string",
|
|
@@ -79,22 +206,24 @@
|
|
|
79
206
|
"default": "false",
|
|
80
207
|
"required": false,
|
|
81
208
|
"mutable": true
|
|
82
|
-
},
|
|
83
|
-
"subtitle": {
|
|
84
|
-
"type": "string | undefined",
|
|
85
|
-
"resolvedType": "string | undefined",
|
|
86
|
-
"attribute": "subtitle",
|
|
87
|
-
"required": false,
|
|
88
|
-
"mutable": false
|
|
89
209
|
}
|
|
90
210
|
},
|
|
91
211
|
"events": [
|
|
92
212
|
{
|
|
93
|
-
"name": "
|
|
213
|
+
"name": "dsAfterClose",
|
|
94
214
|
"detail": "void",
|
|
95
215
|
"bubbles": true,
|
|
96
216
|
"cancelable": true,
|
|
97
|
-
"composed": true
|
|
217
|
+
"composed": true,
|
|
218
|
+
"description": "Emitted after exit motion completes, the top layer closes, and focus is restored."
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"name": "dsClose",
|
|
222
|
+
"detail": "ModalCloseDetail",
|
|
223
|
+
"bubbles": true,
|
|
224
|
+
"cancelable": true,
|
|
225
|
+
"composed": true,
|
|
226
|
+
"description": "Emitted when an internal dismissal control requests that the modal close."
|
|
98
227
|
}
|
|
99
228
|
],
|
|
100
229
|
"methods": [],
|
|
@@ -122,7 +251,7 @@
|
|
|
122
251
|
"peerDependencies": {
|
|
123
252
|
"required": [
|
|
124
253
|
"@ds-mo/tokens ^3.1.1",
|
|
125
|
-
"@ds-mo/icons ^6.
|
|
254
|
+
"@ds-mo/icons ^6.3.0"
|
|
126
255
|
],
|
|
127
256
|
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
128
257
|
}
|
|
@@ -133,17 +262,18 @@
|
|
|
133
262
|
"@ds-mo/tokens"
|
|
134
263
|
],
|
|
135
264
|
"registryDependencies": [
|
|
265
|
+
"button-unfilled",
|
|
136
266
|
"text"
|
|
137
267
|
],
|
|
138
268
|
"files": [
|
|
139
269
|
{
|
|
140
270
|
"path": "src/wc/components/Modal/Modal.css",
|
|
141
|
-
"content": ".modal-
|
|
271
|
+
"content": ":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 display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n height: var(--dimension-size-800);\n padding: var(--dimension-space-200);\n flex: 0 0 auto;\n box-sizing: border-box;\n border-bottom: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n color: var(--color-foreground-primary);\n}\n\nds-text.modal-heading {\n flex: 1 1 0;\n width: auto;\n min-width: 0;\n text-align: left;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.modal-close {\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 display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: var(--dimension-space-100);\n height: var(--dimension-size-800);\n padding: var(--dimension-space-200);\n flex: 0 0 auto;\n box-sizing: border-box;\n border-top: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n}\n\n.modal-footer--empty {\n display: none;\n}\n\n.modal-footer slot {\n display: contents;\n}\n",
|
|
142
272
|
"type": "registry:ui"
|
|
143
273
|
},
|
|
144
274
|
{
|
|
145
275
|
"path": "src/wc/components/Modal/Modal.tsx",
|
|
146
|
-
"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';\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;\
|
|
276
|
+
"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({ flatten: true }) ?? [];\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.ariaDescribedby}\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\">\n <ds-text\n class=\"modal-heading\"\n as=\"h2\"\n variant=\"text-title-medium\"\n emphasis\n color=\"primary\"\n lineTruncation={1}\n textId={this.titleId}\n >\n {this.heading}\n </ds-text>\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 </div>\n <div class=\"modal-content\">\n <slot />\n </div>\n <div\n class={{\n 'modal-footer': true,\n 'modal-footer--empty': !this.hasFooter,\n }}\n >\n <slot\n name=\"footer\"\n onSlotchange={(event: Event) =>\n this.updateFooterPresence(event.currentTarget as HTMLSlotElement)\n }\n />\n </div>\n </dialog>\n </Host>\n );\n }\n}\n",
|
|
147
277
|
"type": "registry:ui"
|
|
148
278
|
}
|
|
149
279
|
]
|