@ds-mo/ui 15.1.0 → 15.2.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 +2 -2
- package/dist/agent.json +286 -4
- package/dist/angular/ds-map-cluster-marker.d.ts +18 -0
- package/dist/angular/ds-map-cluster-marker.js +43 -0
- package/dist/angular/ds-map-entity-marker.d.ts +18 -0
- package/dist/angular/ds-map-entity-marker.js +43 -0
- package/dist/angular/index.d.ts +1 -1
- package/dist/angular/index.js +2 -0
- package/dist/angular/proxies.d.ts +2 -0
- package/dist/angular/proxies.js +2 -0
- package/dist/components/ds-agent-activity.js +1 -1
- package/dist/components/ds-agent-questionnaire.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.js +1 -1
- package/dist/components/ds-bar-action.js +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-page-title.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-inverted.js +1 -1
- package/dist/components/ds-button-unfilled.js +1 -1
- package/dist/components/ds-calendar.js +1 -1
- package/dist/components/ds-card-action-center.js +1 -1
- package/dist/components/ds-card-chart.js +1 -1
- package/dist/components/ds-card-navigation.js +1 -1
- package/dist/components/ds-card-overview.js +1 -1
- package/dist/components/ds-card-setting.js +1 -1
- package/dist/components/ds-card-settings-scope.js +1 -1
- package/dist/components/ds-chart-legend.js +1 -1
- package/dist/components/ds-chart.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-data-customize.js +1 -1
- package/dist/components/ds-data-filter.js +1 -1
- package/dist/components/ds-data-group.js +1 -1
- package/dist/components/ds-data-preferences.js +1 -1
- package/dist/components/ds-data-saved-views.js +1 -1
- package/dist/components/ds-data-saved-views.js.map +1 -1
- package/dist/components/ds-data-search.js +1 -1
- package/dist/components/ds-data-search.js.map +1 -1
- package/dist/components/ds-data-sort.js +1 -1
- package/dist/components/ds-data-toolbar.js +1 -1
- package/dist/components/ds-empty-state.js +1 -1
- package/dist/components/ds-field.js +1 -1
- package/dist/components/ds-filter-menu.js +1 -1
- package/dist/components/ds-input-date.js +1 -1
- package/dist/components/ds-input-time.js +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-map-cluster-marker.d.ts +11 -0
- package/dist/components/ds-map-cluster-marker.js +2 -0
- package/dist/components/ds-map-cluster-marker.js.map +1 -0
- package/dist/components/ds-map-entity-marker.d.ts +11 -0
- package/dist/components/ds-map-entity-marker.js +2 -0
- package/dist/components/ds-map-entity-marker.js.map +1 -0
- 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-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-pagination.js +1 -1
- package/dist/components/ds-panel-nav.js +1 -1
- package/dist/components/ds-panel-sub-nav.js +1 -1
- package/dist/components/ds-panel-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-paper-texture.js +1 -1
- package/dist/components/ds-radio.js +1 -1
- package/dist/components/ds-score.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-shell-app.js +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-table.js +1 -1
- package/dist/components/ds-tag.js +1 -1
- package/dist/components/ds-text.js +1 -1
- package/dist/components/ds-textarea.js +1 -1
- package/dist/components/ds-time-picker.js +1 -1
- package/dist/components/ds-toast.js +1 -1
- package/dist/components/ds-tooltip-chart.js +1 -1
- package/dist/components/ds-tooltip.js +1 -1
- package/dist/components/ds-typing-indicator.js +1 -1
- package/dist/components/{p-CD4DZPy_.js → p-5SVmAjXY.js} +2 -2
- package/dist/components/{p-B_jdoM7e.js → p-B3CZvMXs.js} +2 -2
- package/dist/components/{p-Bn8I7iw6.js → p-B6qQaYkB.js} +2 -2
- package/dist/components/p-BCnzbfma.js +2 -0
- package/dist/components/{p-0qY_HDId.js.map → p-BCnzbfma.js.map} +1 -1
- package/dist/components/{p-D922f6Tb.js → p-BJJh-Bf7.js} +2 -2
- package/dist/components/{p-5URekSny.js → p-BLSSaVWm.js} +2 -2
- package/dist/components/{p-BciTtTwx.js → p-BLuqpHvp.js} +2 -2
- package/dist/components/{p-BciTtTwx.js.map → p-BLuqpHvp.js.map} +1 -1
- package/dist/components/{p-BeNa6tw5.js → p-BOhefRMt.js} +2 -2
- package/dist/components/{p-BKZdqJQB.js → p-BR5OC2PX.js} +2 -2
- package/dist/components/{p-CUcUHwI3.js → p-BX506yEW.js} +2 -2
- package/dist/components/{p-C6xVxqOn.js → p-BZ_e69zx.js} +2 -2
- package/dist/components/{p-DEW7r0U9.js → p-BfF4RZdp.js} +2 -2
- package/dist/components/{p-etifxiDc.js → p-Br5e83Fr.js} +2 -2
- package/dist/components/{p-8yBdCXCn.js → p-BtnTQp4K.js} +2 -2
- package/dist/components/{p-Di5FH7eh.js → p-Bv9rSkp0.js} +2 -2
- package/dist/components/{p-CiH4GG6r.js → p-BzYW1IqO.js} +2 -2
- package/dist/components/{p-DpoTwJAR.js → p-C0JkLR83.js} +2 -2
- package/dist/components/{p-CiJ1WR11.js → p-C1gaWjOm.js} +2 -2
- package/dist/components/{p-D0y7Sja6.js → p-C6Aavz9h.js} +2 -2
- package/dist/components/{p-DVOjjuXc.js → p-C6zycbhC.js} +2 -2
- package/dist/components/{p-BhJXmuRA.js → p-CNZ6-kWd.js} +2 -2
- package/dist/components/{p-DVINzOoK.js → p-CR-Jjdcn.js} +2 -2
- package/dist/components/{p-Dfs98tpL.js → p-CRk0TjQv.js} +2 -2
- package/dist/components/{p-DHICPS4n.js → p-CTGgXIXM.js} +2 -2
- package/dist/components/{p-Dw9sE6Iw.js → p-CZ7hxDy_.js} +2 -2
- package/dist/components/{p-CYzFfFcH.js → p-C_Ma6Xa-.js} +2 -2
- package/dist/components/{p-ZhP2tVmM.js → p-Cn6AfzeF.js} +2 -2
- package/dist/components/{p-Dn9QHL4O.js → p-Cu8o2rOz.js} +2 -2
- package/dist/components/{p-FIBCluwV.js → p-CuWPWC3t.js} +2 -2
- package/dist/components/{p-DOAybCp0.js → p-D5HxZrMA.js} +2 -2
- package/dist/components/{p-Dzndkdx5.js → p-D7K1ZHML.js} +2 -2
- package/dist/components/{p-CzIpquvw.js → p-DFO5lV_A.js} +2 -2
- package/dist/components/{p-CdGXzRP8.js → p-Dlp1HqgS.js} +2 -2
- package/dist/components/{p-0FYD2bev.js → p-Dqfc6Nvd.js} +2 -2
- package/dist/components/{p-0FYD2bev.js.map → p-Dqfc6Nvd.js.map} +1 -1
- package/dist/components/{p-CNxvnQ6A.js → p-DsLXlxt3.js} +2 -2
- package/dist/components/{p-ljbw3UMZ.js → p-ER5M085B.js} +2 -2
- package/dist/components/{p-xdmJCPCg.js → p-QWukusRL.js} +2 -2
- package/dist/components/{p-E5W27Fu_.js → p-UY63jmrO.js} +2 -2
- package/dist/components/{p-D1vQGJzc.js → p-YrnU2fM7.js} +2 -2
- package/dist/components/{p-Do2FJd1y.js → p-ZYWnJaye.js} +2 -2
- package/dist/components/{p-DwbcNDJF.js → p-ZwRLjD5M.js} +2 -2
- package/dist/components/p-lamoJZE4.js +2 -0
- package/dist/components/{p-BgQMEHRa.js.map → p-lamoJZE4.js.map} +1 -1
- package/dist/components/{p-CdgOLWX9.js → p-mV4vSjSK.js} +2 -2
- package/dist/components/{p-Cqc2U1ZQ.js → p-mux14tWZ.js} +2 -2
- package/dist/components/{p-C-3YqeTb.js → p-mxZoWfnS.js} +2 -2
- package/dist/components/{p-CmexIVnE.js → p-p0FVyEFa.js} +2 -2
- package/dist/components/{p-Bpd6YCo4.js → p-uADksYMl.js} +2 -2
- package/dist/components/{p-Bm7rqs3C.js → p-xhn1cZgU.js} +2 -2
- package/dist/components/{p-Bm7rqs3C.js.map → p-xhn1cZgU.js.map} +1 -1
- package/dist/docs/components.json +488 -1
- package/dist/lint/index.js +1 -1
- package/dist/mcp-data/registry/bar-title.json +2 -2
- package/dist/mcp-data/registry/calendar.json +1 -1
- package/dist/mcp-data/registry/data-search.json +1 -1
- package/dist/mcp-data/registry/map-cluster-marker.json +263 -0
- package/dist/mcp-data/registry/map-entity-marker.json +321 -0
- package/dist/mcp-data/registry/registry.json +291 -3
- package/dist/mcp-data/registry/shell-app.json +1 -1
- package/dist/mcp-data/registry/shell-page.json +3 -3
- package/dist/react/components.d.ts +2 -0
- package/dist/react/components.js +2 -0
- package/dist/react/ds-map-cluster-marker.d.ts +12 -0
- package/dist/react/ds-map-cluster-marker.js +12 -0
- package/dist/react/ds-map-entity-marker.d.ts +12 -0
- package/dist/react/ds-map-entity-marker.js +12 -0
- package/dist/types/components/MapClusterMarker/MapClusterMarker.d.ts +19 -0
- package/dist/types/components/MapEntityMarker/MapEntityMarker.d.ts +26 -0
- package/dist/types/components.d.ts +195 -0
- package/dist/vue/components.d.ts +2 -0
- package/dist/vue/components.js +2 -0
- package/dist/vue/ds-map-cluster-marker.d.ts +3 -0
- package/dist/vue/ds-map-cluster-marker.js +14 -0
- package/dist/vue/ds-map-entity-marker.d.ts +3 -0
- package/dist/vue/ds-map-entity-marker.js +17 -0
- package/package.json +1 -1
- package/dist/components/p-0qY_HDId.js +0 -2
- package/dist/components/p-BgQMEHRa.js +0 -2
- /package/dist/components/{p-CD4DZPy_.js.map → p-5SVmAjXY.js.map} +0 -0
- /package/dist/components/{p-B_jdoM7e.js.map → p-B3CZvMXs.js.map} +0 -0
- /package/dist/components/{p-Bn8I7iw6.js.map → p-B6qQaYkB.js.map} +0 -0
- /package/dist/components/{p-D922f6Tb.js.map → p-BJJh-Bf7.js.map} +0 -0
- /package/dist/components/{p-5URekSny.js.map → p-BLSSaVWm.js.map} +0 -0
- /package/dist/components/{p-BeNa6tw5.js.map → p-BOhefRMt.js.map} +0 -0
- /package/dist/components/{p-BKZdqJQB.js.map → p-BR5OC2PX.js.map} +0 -0
- /package/dist/components/{p-CUcUHwI3.js.map → p-BX506yEW.js.map} +0 -0
- /package/dist/components/{p-C6xVxqOn.js.map → p-BZ_e69zx.js.map} +0 -0
- /package/dist/components/{p-DEW7r0U9.js.map → p-BfF4RZdp.js.map} +0 -0
- /package/dist/components/{p-etifxiDc.js.map → p-Br5e83Fr.js.map} +0 -0
- /package/dist/components/{p-8yBdCXCn.js.map → p-BtnTQp4K.js.map} +0 -0
- /package/dist/components/{p-Di5FH7eh.js.map → p-Bv9rSkp0.js.map} +0 -0
- /package/dist/components/{p-CiH4GG6r.js.map → p-BzYW1IqO.js.map} +0 -0
- /package/dist/components/{p-DpoTwJAR.js.map → p-C0JkLR83.js.map} +0 -0
- /package/dist/components/{p-CiJ1WR11.js.map → p-C1gaWjOm.js.map} +0 -0
- /package/dist/components/{p-D0y7Sja6.js.map → p-C6Aavz9h.js.map} +0 -0
- /package/dist/components/{p-DVOjjuXc.js.map → p-C6zycbhC.js.map} +0 -0
- /package/dist/components/{p-BhJXmuRA.js.map → p-CNZ6-kWd.js.map} +0 -0
- /package/dist/components/{p-DVINzOoK.js.map → p-CR-Jjdcn.js.map} +0 -0
- /package/dist/components/{p-Dfs98tpL.js.map → p-CRk0TjQv.js.map} +0 -0
- /package/dist/components/{p-DHICPS4n.js.map → p-CTGgXIXM.js.map} +0 -0
- /package/dist/components/{p-Dw9sE6Iw.js.map → p-CZ7hxDy_.js.map} +0 -0
- /package/dist/components/{p-CYzFfFcH.js.map → p-C_Ma6Xa-.js.map} +0 -0
- /package/dist/components/{p-ZhP2tVmM.js.map → p-Cn6AfzeF.js.map} +0 -0
- /package/dist/components/{p-Dn9QHL4O.js.map → p-Cu8o2rOz.js.map} +0 -0
- /package/dist/components/{p-FIBCluwV.js.map → p-CuWPWC3t.js.map} +0 -0
- /package/dist/components/{p-DOAybCp0.js.map → p-D5HxZrMA.js.map} +0 -0
- /package/dist/components/{p-Dzndkdx5.js.map → p-D7K1ZHML.js.map} +0 -0
- /package/dist/components/{p-CzIpquvw.js.map → p-DFO5lV_A.js.map} +0 -0
- /package/dist/components/{p-CdGXzRP8.js.map → p-Dlp1HqgS.js.map} +0 -0
- /package/dist/components/{p-CNxvnQ6A.js.map → p-DsLXlxt3.js.map} +0 -0
- /package/dist/components/{p-ljbw3UMZ.js.map → p-ER5M085B.js.map} +0 -0
- /package/dist/components/{p-xdmJCPCg.js.map → p-QWukusRL.js.map} +0 -0
- /package/dist/components/{p-E5W27Fu_.js.map → p-UY63jmrO.js.map} +0 -0
- /package/dist/components/{p-D1vQGJzc.js.map → p-YrnU2fM7.js.map} +0 -0
- /package/dist/components/{p-Do2FJd1y.js.map → p-ZYWnJaye.js.map} +0 -0
- /package/dist/components/{p-DwbcNDJF.js.map → p-ZwRLjD5M.js.map} +0 -0
- /package/dist/components/{p-CdgOLWX9.js.map → p-mV4vSjSK.js.map} +0 -0
- /package/dist/components/{p-Cqc2U1ZQ.js.map → p-mux14tWZ.js.map} +0 -0
- /package/dist/components/{p-C-3YqeTb.js.map → p-mxZoWfnS.js.map} +0 -0
- /package/dist/components/{p-CmexIVnE.js.map → p-p0FVyEFa.js.map} +0 -0
- /package/dist/components/{p-Bpd6YCo4.js.map → p-uADksYMl.js.map} +0 -0
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"Dividers in sections or actions create separate Menu groups without becoming selectable rows.",
|
|
78
78
|
"The overflow trigger uses its bordered treatment whenever any visible action sits beside it; overflow-only presentations keep the trigger borderless.",
|
|
79
79
|
"Ordered actions preserve authored order, but only the first eligible filled button, unfilled button, icon button, labeled menu button, or split button renders beside the title. Remaining commands use the shared overflow, whose rows are text-only. Menu choices and overflowed split commands become separate rows and share the same flat action event.",
|
|
80
|
-
"expanded renders the larger heading, optional description, inset divider, and a full-width caption breadcrumb row above the title. The breadcrumb begins after
|
|
80
|
+
"expanded renders the larger heading, optional description, inset divider, and a full-width caption breadcrumb row above the title. The breadcrumb begins after 16px top padding; the following title and right-side actions share one top alignment. Compact renders 48px chrome with the existing icon Back action; constrained uses compact chrome and moves an auto-collapse primary action into overflow.",
|
|
81
81
|
"The one visible ordered action collapses by default in constrained presentation. Use never for an exceptional action whose hierarchy must remain visible; explicit overflow actions never leave the shared menu.",
|
|
82
82
|
"showDivider=false removes the bottom divider when the surrounding page composition supplies the boundary; showCompactDivider can independently restore or remove that boundary in compact and constrained variants.",
|
|
83
83
|
"placement=shell-bar remains compact slotted chrome for advanced ShellApp composition. Managed panel-mode ShellApp uses BarPageTitle instead. It omits description and breadcrumb presentation and preserves applicable Back, section, primary, and overflow controls.",
|
|
@@ -588,7 +588,7 @@
|
|
|
588
588
|
"files": [
|
|
589
589
|
{
|
|
590
590
|
"path": "src/wc/components/BarTitle/BarTitle.css",
|
|
591
|
-
"content": "@import '../../utils/radius.css';\n@import '../../utils/control-density.css';\n@import '../../utils/chrome-header.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: block;\n min-width: 0;\n box-sizing: border-box;\n background: var(--_shell-page-header-background, var(--color-background-primary));\n}\n\n:host([placement='shell-bar']) {\n --_shell-page-header-background: var(--color-background-secondary);\n}\n\n:host-context(ds-shell-app:not([gradient-preset='none']))[placement='shell-bar'],\n:host-context(ds-shell-app:not([gradient-preset='none']))[placement='shell-bar'] .bar-title,\n:host-context(ds-shell-app.shell-app--paper-texture)[placement='shell-bar'],\n:host-context(ds-shell-app.shell-app--paper-texture)[placement='shell-bar'] .bar-title {\n background: transparent;\n}\n\n:host([data-shell-page-syncing]) {\n visibility: hidden;\n}\n\n.bar-title {\n position: relative;\n min-width: 0;\n box-sizing: border-box;\n background: var(--_shell-page-header-background, var(--color-background-primary));\n}\n\n.bar-title::after {\n content: '';\n position: absolute;\n inset: auto var(--ds-bar-title-divider-inset, var(--dimension-space-
|
|
591
|
+
"content": "@import '../../utils/radius.css';\n@import '../../utils/control-density.css';\n@import '../../utils/chrome-header.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: block;\n min-width: 0;\n box-sizing: border-box;\n background: var(--_shell-page-header-background, var(--color-background-primary));\n}\n\n:host([placement='shell-bar']) {\n --_shell-page-header-background: var(--color-background-secondary);\n}\n\n:host-context(ds-shell-app:not([gradient-preset='none']))[placement='shell-bar'],\n:host-context(ds-shell-app:not([gradient-preset='none']))[placement='shell-bar'] .bar-title,\n:host-context(ds-shell-app.shell-app--paper-texture)[placement='shell-bar'],\n:host-context(ds-shell-app.shell-app--paper-texture)[placement='shell-bar'] .bar-title {\n background: transparent;\n}\n\n:host([data-shell-page-syncing]) {\n visibility: hidden;\n}\n\n.bar-title {\n position: relative;\n min-width: 0;\n box-sizing: border-box;\n background: var(--_shell-page-header-background, var(--color-background-primary));\n}\n\n.bar-title::after {\n content: '';\n position: absolute;\n inset: auto var(--ds-bar-title-divider-inset, var(--dimension-space-200)) 0;\n height: var(--dimension-stroke-width-012);\n background: var(--color-border-tertiary);\n pointer-events: none;\n}\n\n.bar-title--divider-hidden::after {\n display: none;\n}\n\n.bar-title__inner {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: var(--dimension-space-100);\n padding: var(--dimension-space-200);\n box-sizing: border-box;\n}\n\n.bar-title__row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--dimension-space-100);\n min-height: var(--dimension-size-400);\n min-width: 0;\n}\n\n:host(.bar-title-host--expanded) .bar-title__row {\n gap: var(--dimension-space-400);\n}\n\n.bar-title__leading {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--dimension-space-100);\n min-width: 0;\n}\n\n.bar-title__title-row {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-100);\n min-width: 0;\n min-height: var(--dimension-size-400);\n}\n\n.bar-title__identity {\n display: flex;\n flex: 0 1 auto;\n align-items: center;\n gap: var(--dimension-space-050);\n min-width: 0;\n}\n\n.bar-title__back,\n.bar-title__section-chevron {\n flex: 0 0 auto;\n}\n\n.bar-title__breadcrumb {\n display: block;\n width: 100%;\n min-width: 0;\n}\n\n.bar-title__heading {\n display: block;\n width: auto;\n min-width: 0;\n user-select: none;\n}\n\n.bar-title__divider {\n width: var(--dimension-stroke-width-012);\n height: var(--dimension-size-250);\n margin-inline-start: var(--dimension-space-100);\n flex: 0 0 var(--dimension-stroke-width-012);\n background: var(--color-border-tertiary);\n}\n\n.bar-title__section-selector {\n display: flex;\n flex: 0 1 auto;\n min-width: 0;\n max-width: var(--dimension-panel-width-sm);\n}\n\n.bar-title__section-trigger {\n --ds-control-radius: var(--dimension-radius-half);\n\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--ds-control-gap);\n height: var(--ds-control-height);\n min-width: 0;\n max-width: 100%;\n padding-inline: var(--ds-control-padding-inline);\n border: none;\n border-radius: var(--ds-control-radius);\n box-sizing: border-box;\n background: transparent;\n color: var(--color-foreground-secondary);\n cursor: default;\n font-family: inherit;\n user-select: none;\n}\n\n.bar-title__section-label {\n min-width: 0;\n padding-inline: var(--ds-control-label-inset);\n}\n\n.bar-title__actions {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-100);\n flex: 0 0 auto;\n}\n\n.bar-title__description {\n display: block;\n min-width: 0;\n max-width: 100%;\n}\n\n:host(.bar-title-host--compact) .bar-title {\n height: var(--dimension-size-600);\n}\n\n:host(.bar-title-host--compact) .bar-title::after {\n inset-inline: 0;\n}\n\n:host(.bar-title-host--compact) .bar-title__inner {\n flex: 1 1 auto;\n height: 100%;\n min-width: 0;\n padding: 0;\n gap: 0;\n}\n\n:host(.bar-title-host--compact) .bar-title__row {\n height: 100%;\n align-items: center;\n}\n\n:host(.bar-title-host--compact) .bar-title__identity {\n height: var(--dimension-size-400);\n}\n\n:host(.bar-title-host--compact) .bar-title__divider {\n margin-inline-start: 0;\n}\n\n:host(.bar-title-host--compact) .bar-title__heading {\n display: inline-flex;\n flex: 1 1 auto;\n align-items: center;\n height: var(--dimension-size-400);\n padding-inline: calc(var(--ds-control-padding-inline) + var(--ds-control-label-inset));\n box-sizing: border-box;\n}\n",
|
|
592
592
|
"type": "registry:ui"
|
|
593
593
|
},
|
|
594
594
|
{
|
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
},
|
|
275
275
|
{
|
|
276
276
|
"path": "src/wc/components/Calendar/Calendar.tsx",
|
|
277
|
-
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n Method,\n Prop,\n State,\n Watch,\n h,\n Host,\n} from '@stencil/core';\nimport {\n dateFilterRangeValue,\n isIsoCalendarDate,\n parseDateFilterValue,\n shiftIsoCalendarDate,\n} from '../../utils';\nimport {\n CALENDAR_WEEKDAYS,\n calendarDays,\n calendarMonth,\n calendarMonthLabel,\n calendarPaintedRange,\n calendarToday,\n shiftCalendarMonth,\n type CalendarDateRange,\n} from './calendar-grid';\n\nexport type CalendarSelectionMode = 'single' | 'range';\n\n@Component({\n tag: 'ds-calendar',\n styleUrl: 'Calendar.css',\n scoped: true,\n})\nexport class Calendar {\n @Element() el!: HTMLElement;\n\n /** Single date emits YYYY-MM-DD. Range emits the shared `range:start/end` string. */\n @Prop() selectionMode: CalendarSelectionMode = 'single';\n @Prop() value: string = '';\n @Prop() min: string | undefined;\n @Prop() max: string | undefined;\n @Prop() isInactive: boolean = false;\n /** Move keyboard focus into the grid after the calendar is rendered. */\n @Prop() autoFocus: boolean = false;\n\n @Event() dsChange!: EventEmitter<string>;\n\n @State() private month = calendarMonth(calendarToday());\n @State() private pendingStart = '';\n @State() private previewEnd = '';\n @State() private heldRange: CalendarDateRange | null = null;\n\n componentWillLoad() {\n this.month = calendarMonth(this.anchorDate());\n }\n\n componentDidLoad() {\n if (this.autoFocus) void this.setFocus();\n }\n\n @Watch('value')\n onValueChange() {\n this.month = calendarMonth(this.anchorDate());\n }\n\n @Method()\n async setFocus() {\n this.el.querySelector<HTMLElement>('.calendar-day[tabindex=\"0\"]')?.focus();\n }\n\n private anchorDate(): string {\n if (this.selectionMode === 'range') {\n const parsed = parseDateFilterValue(this.value);\n if (parsed?.kind === 'range') return parsed.start;\n return calendarToday();\n }\n return isIsoCalendarDate(this.value) ? this.value : calendarToday();\n }\n\n private isDisabled(day: string): boolean {\n if (this.isInactive) return true;\n if (this.min && isIsoCalendarDate(this.min) && day < this.min) return true;\n if (this.max && isIsoCalendarDate(this.max) && day > this.max) return true;\n return false;\n }\n\n private moveMonth = (offset: number) => {\n this.previewEnd = '';\n this.month = shiftCalendarMonth(this.month, offset);\n };\n\n private selectDate = (day: string) => {\n if (this.isDisabled(day)) return;\n this.previewEnd = '';\n if (this.selectionMode === 'single') {\n this.pendingStart = '';\n this.heldRange = null;\n this.dsChange.emit(day);\n return;\n }\n const pending = this.pendingStart;\n if (pending) {\n this.pendingStart = '';\n this.heldRange = null;\n this.dsChange.emit(dateFilterRangeValue(pending, day));\n return;\n }\n const current = parseDateFilterValue(this.value);\n this.heldRange = current?.kind === 'range' ? { start: current.start, end: current.end } : null;\n this.pendingStart = day;\n this.dsChange.emit(dateFilterRangeValue(day, day));\n };\n\n private previewRange = (day: string) => {\n if (!this.pendingStart || this.previewEnd === day) return;\n this.previewEnd = day;\n };\n\n private clearPreview = () => {\n if (!this.previewEnd) return;\n this.previewEnd = '';\n };\n\n private focusDate(day: string) {\n this.previewRange(day);\n const targetMonth = calendarMonth(day);\n if (this.month !== targetMonth) this.month = targetMonth;\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>(`[data-date-option=\"${day}\"]`)?.focus();\n });\n }\n\n private handleDayKeyDown = (event: KeyboardEvent, day: string) => {\n let offset: number | null = null;\n if (event.key === 'ArrowLeft') offset = -1;\n else if (event.key === 'ArrowRight') offset = 1;\n else if (event.key === 'ArrowUp') offset = -7;\n else if (event.key === 'ArrowDown') offset = 7;\n else if (event.key === 'Home') {\n offset = -new Date(`${day}T00:00:00Z`).getUTCDay();\n } else if (event.key === 'End') {\n offset = 6 - new Date(`${day}T00:00:00Z`).getUTCDay();\n } else if (event.key === 'PageUp' || event.key === 'PageDown') {\n const month = shiftCalendarMonth(calendarMonth(day), event.key === 'PageUp' ? -1 : 1);\n const candidate = `${month}-${day.slice(8, 10)}`;\n event.preventDefault();\n this.focusDate(isIsoCalendarDate(candidate) ? candidate : `${month}-01`);\n return;\n }\n if (offset === null) return;\n event.preventDefault();\n this.focusDate(shiftIsoCalendarDate(day, offset));\n };\n\n render() {\n const liveRange = this.selectionMode === 'range' ? parseDateFilterValue(this.value) : null;\n const liveRangeValue = liveRange?.kind === 'range' ? liveRange : null;\n const rangeValue = calendarPaintedRange(this.pendingStart, this.heldRange, liveRangeValue);\n const days = calendarDays(this.month);\n const weeks = Array.from({ length: 6 }, (_, index) => days.slice(index * 7, index * 7 + 7));\n const today = calendarToday();\n const previewStart =\n this.pendingStart && this.previewEnd ? [this.pendingStart, this.previewEnd].sort()[0] : null;\n const previewFinish =\n this.pendingStart && this.previewEnd ? [this.pendingStart, this.previewEnd].sort()[1] : null;\n const selectedSingle =\n this.selectionMode === 'single' && isIsoCalendarDate(this.value) ? this.value : '';\n const preferredFocus =\n this.pendingStart ||\n selectedSingle ||\n rangeValue?.end ||\n (this.month === today.slice(0, 7) ? today : '');\n const focusDate = days.some(day => day.value === preferredFocus)\n ? preferredFocus\n : (days.find(day => day.inMonth)?.value ?? days[0]?.value);\n\n return (\n <Host>\n <div class=\"calendar\">\n <div class=\"calendar-heading ds-control--md\">\n <ds-button-unfilled\n class=\"calendar-nav calendar-nav--previous\"\n variant=\"icon\"\n icon=\"ChevronLeft\"\n size=\"md\"\n hasBorder={false}\n isInactive={this.isInactive}\n ariaLabel=\"Previous month\"\n onDsClick={() => this.moveMonth(-1)}\n />\n <ds-text as=\"span\" variant=\"text-body-medium\" emphasis color=\"primary\">\n {calendarMonthLabel(this.month)}\n </ds-text>\n <ds-button-unfilled\n class=\"calendar-nav calendar-nav--next\"\n variant=\"icon\"\n icon=\"ChevronRight\"\n size=\"md\"\n hasBorder={false}\n isInactive={this.isInactive}\n ariaLabel=\"Next month\"\n onDsClick={() => this.moveMonth(1)}\n />\n </div>\n <div class=\"calendar-weekdays\" aria-hidden=\"true\">\n {CALENDAR_WEEKDAYS.map(day => (\n <ds-text as=\"span\" variant=\"text-body-small\" color=\"secondary\">\n {day}\n </ds-text>\n ))}\n </div>\n <div\n class=\"calendar-grid\"\n role=\"grid\"\n aria-label={calendarMonthLabel(this.month)}\n onMouseLeave={this.clearPreview}\n >\n {weeks.map((week, weekIndex) => (\n <div class=\"calendar-grid__row\" role=\"row\" key={`week-${weekIndex}`}>\n {week.map(day => {\n const selectedInRange = Boolean(\n liveRangeValue &&\n day.value >= liveRangeValue.start &&\n day.value <= liveRangeValue.end\n );\n const paintedInRange = Boolean(\n rangeValue && day.value >= rangeValue.start && day.value <= rangeValue.end\n );\n const pendingStartDay =\n this.selectionMode === 'range' &&\n Boolean(this.pendingStart) &&\n day.value === this.pendingStart;\n const previewInRange = Boolean(\n this.pendingStart &&\n previewStart &&\n previewFinish &&\n day.value >= previewStart &&\n day.value <= previewFinish &&\n day.value !== this.pendingStart\n );\n const inRange = this.selectionMode === 'range' && paintedInRange;\n const rangeEdge = Boolean(\n this.selectionMode === 'range' &&\n rangeValue &&\n (day.value === rangeValue.start || day.value === rangeValue.end)\n );\n const selected = this.selectionMode === 'single' && day.value === selectedSingle;\n const textColor = rangeEdge\n ? 'on-bold'\n : selected ||\n inRange ||\n pendingStartDay ||\n previewInRange ||\n day.value === today\n ? 'primary'\n : day.inMonth\n ? 'secondary'\n : 'tertiary';\n return (\n <button\n type=\"button\"\n role=\"gridcell\"\n data-date-option={day.value}\n class={{\n 'calendar-day': true,\n 'calendar-day--outside': !day.inMonth,\n 'calendar-day--today': day.value === today,\n 'calendar-day--in-range': inRange,\n 'calendar-day--range-preview': previewInRange,\n 'calendar-day--range-edge': rangeEdge,\n 'calendar-day--selected': selected,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--on-bold': rangeEdge,\n 'ds-interaction-fill--surface-open':\n pendingStartDay && !this.isDisabled(day.value),\n 'ds-interaction-fill--selected': selected && !this.isDisabled(day.value),\n }}\n disabled={this.isDisabled(day.value)}\n aria-label={day.label}\n aria-selected={selectedInRange || selected ? 'true' : 'false'}\n tabIndex={day.value === focusDate ? 0 : -1}\n onMouseEnter={() => this.previewRange(day.value)}\n onFocus={() => this.previewRange(day.value)}\n onClick={() => this.selectDate(day.value)}\n onKeyDown={event => this.handleDayKeyDown(event, day.value)}\n >\n <ds-text\n class=\"ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={textColor}\n emphasis={day.value === today}\n >\n {day.day}\n </ds-text>\n </button>\n );\n })}\n </div>\n ))}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
|
|
277
|
+
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n Method,\n Prop,\n State,\n Watch,\n h,\n Host,\n} from '@stencil/core';\nimport {\n dateFilterRangeValue,\n isIsoCalendarDate,\n parseDateFilterValue,\n shiftIsoCalendarDate,\n} from '../../utils';\nimport {\n CALENDAR_WEEKDAYS,\n calendarDays,\n calendarMonth,\n calendarMonthLabel,\n calendarPaintedRange,\n calendarToday,\n shiftCalendarMonth,\n type CalendarDateRange,\n} from './calendar-grid';\n\nexport type CalendarSelectionMode = 'single' | 'range';\n\n@Component({\n tag: 'ds-calendar',\n styleUrl: 'Calendar.css',\n scoped: true,\n})\nexport class Calendar {\n @Element() el!: HTMLElement;\n\n /** Single date emits YYYY-MM-DD. Range emits the shared `range:start/end` string. */\n @Prop() selectionMode: CalendarSelectionMode = 'single';\n @Prop() value: string = '';\n @Prop() min: string | undefined;\n @Prop() max: string | undefined;\n @Prop() isInactive: boolean = false;\n /** Move keyboard focus into the grid after the calendar is rendered. */\n @Prop() autoFocus: boolean = false;\n\n @Event() dsChange!: EventEmitter<string>;\n\n @State() private month = calendarMonth(calendarToday());\n @State() private pendingStart = '';\n @State() private previewEnd = '';\n @State() private heldRange: CalendarDateRange | null = null;\n\n componentWillLoad() {\n this.month = calendarMonth(this.anchorDate());\n }\n\n componentDidLoad() {\n if (this.autoFocus) void this.setFocus();\n }\n\n @Watch('value')\n onValueChange() {\n this.month = calendarMonth(this.anchorDate());\n }\n\n @Method()\n async setFocus() {\n this.el.querySelector<HTMLElement>('.calendar-day[tabindex=\"0\"]')?.focus();\n }\n\n private anchorDate(): string {\n if (this.selectionMode === 'range') {\n const parsed = parseDateFilterValue(this.value);\n if (parsed?.kind === 'range') return parsed.start;\n return calendarToday();\n }\n return isIsoCalendarDate(this.value) ? this.value : calendarToday();\n }\n\n private isDisabled(day: string): boolean {\n if (this.isInactive) return true;\n if (this.min && isIsoCalendarDate(this.min) && day < this.min) return true;\n if (this.max && isIsoCalendarDate(this.max) && day > this.max) return true;\n return false;\n }\n\n private moveMonth = (offset: number) => {\n this.previewEnd = '';\n this.month = shiftCalendarMonth(this.month, offset);\n };\n\n private selectDate = (day: string) => {\n if (this.isDisabled(day)) return;\n this.previewEnd = '';\n if (this.selectionMode === 'single') {\n this.pendingStart = '';\n this.heldRange = null;\n this.dsChange.emit(day);\n return;\n }\n const pending = this.pendingStart;\n if (pending) {\n this.pendingStart = '';\n this.heldRange = null;\n this.dsChange.emit(dateFilterRangeValue(pending, day));\n return;\n }\n const current = parseDateFilterValue(this.value);\n this.heldRange = current?.kind === 'range' ? { start: current.start, end: current.end } : null;\n this.pendingStart = day;\n this.dsChange.emit(dateFilterRangeValue(day, day));\n };\n\n private previewRange = (day: string) => {\n if (!this.pendingStart || this.previewEnd === day) return;\n this.previewEnd = day;\n };\n\n private clearPreview = () => {\n if (!this.previewEnd) return;\n this.previewEnd = '';\n };\n\n private focusDate(day: string) {\n this.previewRange(day);\n const targetMonth = calendarMonth(day);\n if (this.month !== targetMonth) this.month = targetMonth;\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>(`[data-date-option=\"${day}\"]`)?.focus();\n });\n }\n\n private handleDayKeyDown = (event: KeyboardEvent, day: string) => {\n let offset: number | null = null;\n if (event.key === 'ArrowLeft') offset = -1;\n else if (event.key === 'ArrowRight') offset = 1;\n else if (event.key === 'ArrowUp') offset = -7;\n else if (event.key === 'ArrowDown') offset = 7;\n else if (event.key === 'Home') {\n offset = -new Date(`${day}T00:00:00Z`).getUTCDay();\n } else if (event.key === 'End') {\n offset = 6 - new Date(`${day}T00:00:00Z`).getUTCDay();\n } else if (event.key === 'PageUp' || event.key === 'PageDown') {\n const month = shiftCalendarMonth(calendarMonth(day), event.key === 'PageUp' ? -1 : 1);\n const candidate = `${month}-${day.slice(8, 10)}`;\n event.preventDefault();\n this.focusDate(isIsoCalendarDate(candidate) ? candidate : `${month}-01`);\n return;\n }\n if (offset === null) return;\n event.preventDefault();\n this.focusDate(shiftIsoCalendarDate(day, offset));\n };\n\n render() {\n const liveRange = this.selectionMode === 'range' ? parseDateFilterValue(this.value) : null;\n const liveRangeValue = liveRange?.kind === 'range' ? liveRange : null;\n const rangeValue = calendarPaintedRange(this.pendingStart, this.heldRange, liveRangeValue);\n const days = calendarDays(this.month);\n const weeks = Array.from({ length: 6 }, (_, index) => days.slice(index * 7, index * 7 + 7));\n const today = calendarToday();\n const previewStart =\n this.pendingStart && this.previewEnd ? [this.pendingStart, this.previewEnd].sort()[0] : null;\n const previewFinish =\n this.pendingStart && this.previewEnd ? [this.pendingStart, this.previewEnd].sort()[1] : null;\n const selectedSingle =\n this.selectionMode === 'single' && isIsoCalendarDate(this.value) ? this.value : '';\n const preferredFocus =\n this.pendingStart ||\n selectedSingle ||\n rangeValue?.end ||\n (this.month === today.slice(0, 7) ? today : '');\n const focusDate = days.some(day => day.value === preferredFocus)\n ? preferredFocus\n : (days.find(day => day.inMonth)?.value ?? days[0]?.value);\n\n return (\n <Host>\n <div class=\"calendar\">\n <div class=\"calendar-heading ds-control--md\">\n <ds-button-unfilled\n class=\"calendar-nav calendar-nav--previous\"\n variant=\"icon\"\n icon=\"ChevronLeft\"\n size=\"md\"\n hasBorder={false}\n isInactive={this.isInactive}\n ariaLabel=\"Previous month\"\n onDsClick={() => this.moveMonth(-1)}\n />\n <ds-text as=\"span\" variant=\"text-body-medium\" emphasis color=\"primary\">\n {calendarMonthLabel(this.month)}\n </ds-text>\n <ds-button-unfilled\n class=\"calendar-nav calendar-nav--next\"\n variant=\"icon\"\n icon=\"ChevronRight\"\n size=\"md\"\n hasBorder={false}\n isInactive={this.isInactive}\n ariaLabel=\"Next month\"\n onDsClick={() => this.moveMonth(1)}\n />\n </div>\n <div class=\"calendar-weekdays\" aria-hidden=\"true\">\n {CALENDAR_WEEKDAYS.map(day => (\n <ds-text as=\"span\" variant=\"text-body-small\" color=\"secondary\">\n {day}\n </ds-text>\n ))}\n </div>\n <div\n class=\"calendar-grid\"\n role=\"grid\"\n aria-label={calendarMonthLabel(this.month)}\n onMouseLeave={this.clearPreview}\n >\n {weeks.map((week, weekIndex) => (\n <div class=\"calendar-grid__row\" role=\"row\" key={`week-${weekIndex}`}>\n {week.map(day => {\n const selectedInRange = Boolean(\n liveRangeValue &&\n day.value >= liveRangeValue.start &&\n day.value <= liveRangeValue.end\n );\n const paintedInRange = Boolean(\n rangeValue && day.value >= rangeValue.start && day.value <= rangeValue.end\n );\n const pendingStartDay =\n this.selectionMode === 'range' &&\n Boolean(this.pendingStart) &&\n day.value === this.pendingStart;\n const previewInRange = Boolean(\n this.pendingStart &&\n previewStart &&\n previewFinish &&\n day.value >= previewStart &&\n day.value <= previewFinish &&\n day.value !== this.pendingStart\n );\n const inRange = this.selectionMode === 'range' && paintedInRange;\n const rangeEdge = Boolean(\n this.selectionMode === 'range' &&\n rangeValue &&\n (day.value === rangeValue.start || day.value === rangeValue.end)\n );\n const selected = this.selectionMode === 'single' && day.value === selectedSingle;\n const textColor = rangeEdge\n ? 'on-bold'\n : selected ||\n inRange ||\n pendingStartDay ||\n previewInRange ||\n day.value === today\n ? 'primary'\n : day.inMonth\n ? 'secondary'\n : 'tertiary';\n return (\n <button\n type=\"button\"\n role=\"gridcell\"\n data-date-option={day.value}\n class={{\n 'calendar-day': true,\n 'calendar-day--outside': !day.inMonth,\n 'calendar-day--today': day.value === today,\n 'calendar-day--in-range': inRange,\n 'calendar-day--range-preview': previewInRange,\n 'calendar-day--range-edge': rangeEdge,\n 'calendar-day--selected': selected,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--on-bold': rangeEdge,\n 'ds-interaction-fill--surface-open':\n pendingStartDay && !this.isDisabled(day.value),\n 'ds-interaction-fill--selected': selected && !this.isDisabled(day.value),\n }}\n disabled={this.isDisabled(day.value)}\n aria-label={day.label}\n aria-selected={selectedInRange || selected ? 'true' : 'false'}\n tabIndex={day.value === focusDate ? 0 : -1}\n onMouseEnter={() => this.previewRange(day.value)}\n onFocus={() => this.previewRange(day.value)}\n onClick={() => this.selectDate(day.value)}\n onKeyDown={event => this.handleDayKeyDown(event, day.value)}\n >\n {/* eslint-disable compomo/no-selected-fill-emphasis-change -- Today emphasis is date-driven and remains unchanged when selection changes. */}\n <ds-text\n class=\"ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={textColor}\n emphasis={day.value === today}\n >\n {day.day}\n </ds-text>\n {/* eslint-enable compomo/no-selected-fill-emphasis-change */}\n </button>\n );\n })}\n </div>\n ))}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
|
|
278
278
|
"type": "registry:ui"
|
|
279
279
|
},
|
|
280
280
|
{
|
|
@@ -375,7 +375,7 @@
|
|
|
375
375
|
},
|
|
376
376
|
{
|
|
377
377
|
"path": "src/wc/components/DataSearch/DataSearch.tsx",
|
|
378
|
-
"content": "import type { DataField } from '../../utils/data-field';\nimport {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n choicePopupMinWidth,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n TOKEN_DEFAULTS,\n} from '../../utils';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport { AnchoredOverlayInteractionController } from '../../utils/anchored-overlay-interaction-controller';\nimport { resolveAnchoredOverlayBoundaryRect } from '../../utils/anchored-overlay-boundary';\nimport {\n availableTableSearchFields,\n filterTableSearchFields,\n nextTableSearchActiveIndex,\n selectedTableSearchFields,\n tableSearchFields,\n} from './data-search-model';\nimport type { DataSearchField, DataSearchFieldsChangeDetail } from './data-search-types';\n\nlet tableSearchSequence = 0;\n\n@Component({\n tag: 'ds-data-search',\n styleUrl: 'DataSearch.css',\n scoped: true,\n})\nexport class DataSearch {\n @Element() private el!: HTMLElement;\n\n /** Show the resting field border; keyboard focus remains visible. */\n @Prop({ reflect: true }) hasBorder: boolean = true;\n\n /** Controlled free-text query. */\n @Prop() value: string = '';\n /** Table column catalog used to derive searchable data points and their complete labels. */\n @Prop() fields: DataField[] = [];\n /** Controlled ordered field scopes rendered as Tags. */\n @Prop() selectedFieldIds: string[] = [];\n @Prop() placeholder: string = 'Search';\n @Prop({ attribute: 'aria-label' }) ariaLabel: string = 'Search table';\n @Prop() clearLabel: string = 'Clear search';\n @Prop() fieldMenuLabel: string = 'Choose search fields';\n @Prop() isInactive: boolean = false;\n\n @Event({ bubbles: false }) dsChange!: EventEmitter<string>;\n @Event({ bubbles: false }) dsFieldsChange!: EventEmitter<DataSearchFieldsChangeDetail>;\n @Event({ bubbles: false }) dsClear!: EventEmitter<void>;\n\n @State() private menuOpen = false;\n @State() private menuQuery = '';\n @State() private activeIndex = 0;\n @State() private focusRingVisible = false;\n @State() private position = { x: 0, y: 0 };\n @State() private positionReady = false;\n @State() private availableHeight: number | null = null;\n @State() private announcement = '';\n\n private readonly componentId = `ds-data-search-${++tableSearchSequence}`;\n private readonly inputId = `${this.componentId}-input`;\n private readonly listboxId = `${this.componentId}-listbox`;\n private readonly descriptionId = `${this.componentId}-description`;\n private inputEl: HTMLInputElement | null = null;\n private controlEl: HTMLElement | null = null;\n private popupEl: HTMLElement | null = null;\n\n private readonly positionController = new AnchoredPositionController({\n getAnchor: () => this.controlEl,\n getPopup: () => this.popupEl,\n getOwnerDocument: () => this.el.ownerDocument,\n measure: (anchor, popup) => {\n if (!this.menuOpen || !popup.isConnected) return null;\n const inset = resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n popup.style.minWidth = `max(var(--ds-choice-popup-min-inline-size, ${TOKEN_DEFAULTS.menuWidthXs}), ${choicePopupMinWidth(anchor.offsetWidth, inset)}px)`;\n const anchorRect = anchor.getBoundingClientRect();\n return {\n anchorRect,\n popupWidth: popup.offsetWidth,\n popupHeight: popup.offsetHeight,\n side: 'bottom',\n align: 'start',\n sideOffsetPx: inset,\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: 'start',\n alignOffsetPx: 0,\n sectionInsetPx: inset,\n }),\n viewportPadPx: inset,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n collisionRect: resolveAnchoredOverlayBoundaryRect(anchor),\n };\n },\n apply: ({ x, y, availableHeight }) => {\n this.position = { x, y };\n this.availableHeight = availableHeight;\n },\n onReady: () => {\n this.positionReady = true;\n },\n topLayer: true,\n liveUpdate: 'frame',\n observeResize: true,\n observeAnchorMotion: true,\n });\n\n private readonly interactionController = new AnchoredOverlayInteractionController({\n getAnchor: () => this.controlEl,\n getPopup: () => this.popupEl,\n getOwnerDocument: () => this.el.ownerDocument,\n onOutsideActivation: () => this.closeMenu(),\n });\n\n disconnectedCallback(): void {\n this.closeMenu();\n }\n\n @Watch('columns')\n @Watch('selectedFieldIds')\n syncAvailableFields(): void {\n const count = this.availableFields.length;\n if (count === 0) {\n this.closeMenu();\n return;\n }\n if (this.activeIndex >= count) this.activeIndex = count - 1;\n }\n\n @Watch('isInactive')\n syncInactive(isInactive: boolean): void {\n if (isInactive) this.closeMenu();\n }\n\n @Method()\n async setFocus(): Promise<void> {\n this.inputEl?.focus();\n }\n\n private get selectedFields(): DataSearchField[] {\n return selectedTableSearchFields(this.searchFields, this.selectedFieldIds);\n }\n\n private get availableFields(): DataSearchField[] {\n return availableTableSearchFields(this.searchFields, this.selectedFieldIds);\n }\n\n private get visibleFields(): DataSearchField[] {\n return filterTableSearchFields(this.availableFields, this.menuQuery);\n }\n\n private get searchFields(): DataSearchField[] {\n return tableSearchFields(this.fields);\n }\n\n private get activeOptionId(): string | undefined {\n return this.menuOpen && this.activeIndex >= 0\n ? `${this.componentId}-option-${this.activeIndex}`\n : undefined;\n }\n\n private openMenu(fromKeyboard: boolean): void {\n if (this.isInactive || this.menuOpen || this.availableFields.length === 0) return;\n this.menuQuery = '';\n // The native value can be ahead of the controlled prop when slash follows input\n // in the same frame, so clear it before the queued render can skip an equal value.\n if (this.inputEl) this.inputEl.value = '';\n this.activeIndex = 0;\n this.focusRingVisible = fromKeyboard;\n this.positionReady = false;\n this.menuOpen = true;\n requestAnimationFrame(() => {\n if (!this.menuOpen) return;\n this.interactionController.connect();\n this.positionController.observe();\n this.positionController.schedule();\n });\n }\n\n private closeMenu(): void {\n if (!this.menuOpen && !this.popupEl) return;\n if (this.popupEl?.matches(':popover-open')) this.popupEl.hidePopover();\n this.positionController.unobserve();\n this.interactionController.disconnect();\n this.popupEl = null;\n this.positionReady = false;\n this.availableHeight = null;\n this.menuOpen = false;\n this.menuQuery = '';\n }\n\n private handleInput(event: Event): void {\n const nextValue = (event.target as HTMLInputElement).value;\n if (this.menuOpen) {\n this.menuQuery = nextValue;\n this.activeIndex = filterTableSearchFields(this.availableFields, nextValue).length ? 0 : -1;\n this.focusRingVisible = false;\n this.positionController.schedule();\n return;\n }\n\n this.dsChange.emit(nextValue);\n }\n\n private selectField(field: DataSearchField): void {\n this.dsFieldsChange.emit({ selectedFieldIds: [...this.selectedFieldIds, field.id] });\n this.announcement = `${field.label} search field added.`;\n this.closeMenu();\n requestAnimationFrame(() => this.inputEl?.focus({ preventScroll: true }));\n }\n\n private removeLastField(): void {\n const selected = this.selectedFields;\n const removed = selected[selected.length - 1];\n if (!removed) return;\n this.dsFieldsChange.emit({\n selectedFieldIds: this.selectedFieldIds.filter(fieldId => fieldId !== removed.id),\n });\n this.announcement = `${removed.label} search field removed.`;\n }\n\n private removeField(field: DataSearchField): void {\n this.dsFieldsChange.emit({\n selectedFieldIds: this.selectedFieldIds.filter(fieldId => fieldId !== field.id),\n });\n this.announcement = `${field.label} search field removed.`;\n requestAnimationFrame(() => this.inputEl?.focus({ preventScroll: true }));\n }\n\n private clearSearch(): void {\n if (this.value) this.dsChange.emit('');\n if (this.selectedFieldIds.length) this.dsFieldsChange.emit({ selectedFieldIds: [] });\n this.dsClear.emit();\n this.announcement = 'Search cleared.';\n this.closeMenu();\n requestAnimationFrame(() => this.inputEl?.focus({ preventScroll: true }));\n }\n\n private handleKeyDown(event: KeyboardEvent): void {\n if (event.key === '/' && !this.menuOpen && !event.altKey && !event.ctrlKey && !event.metaKey) {\n event.preventDefault();\n this.openMenu(true);\n return;\n }\n\n if (event.key === 'Backspace' && this.menuOpen && this.menuQuery.length === 0) {\n event.preventDefault();\n this.closeMenu();\n return;\n }\n\n if (event.key === 'Backspace' && this.value.length === 0) {\n if (this.selectedFields.length) {\n event.preventDefault();\n this.removeLastField();\n }\n return;\n }\n\n if (!this.menuOpen) return;\n\n if (event.key === 'Escape') {\n event.preventDefault();\n this.closeMenu();\n return;\n }\n\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault();\n this.focusRingVisible = true;\n this.activeIndex = nextTableSearchActiveIndex(\n this.activeIndex,\n this.visibleFields.length,\n event.key === 'ArrowDown' ? 1 : -1\n );\n this.el\n .querySelector<HTMLElement>(`#${CSS.escape(this.activeOptionId ?? '')}`)\n ?.scrollIntoView({ block: 'nearest' });\n return;\n }\n\n if (event.key === 'Enter') {\n const field = this.visibleFields[this.activeIndex];\n if (!field) return;\n event.preventDefault();\n this.selectField(field);\n return;\n }\n\n if (event.key === 'Tab') this.closeMenu();\n }\n\n render() {\n const selected = this.selectedFields;\n const available = this.availableFields;\n const visible = this.visibleFields;\n const inputValue = this.menuOpen ? this.menuQuery : this.value;\n const hasValue = this.value.length > 0 || selected.length > 0;\n const description = selected.length\n ? `Search limited to ${selected.map(field => field.label).join(' or ')}.`\n : 'Search includes every available table field. Type slash to choose fields.';\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.position.x)}px, ${Math.round(this.position.y)}px)`,\n zIndex: 'var(--dimension-z-index-floating)',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n if (this.availableHeight !== null) {\n popupStyle['max-height'] = `${Math.floor(this.availableHeight)}px`;\n }\n\n return (\n <Host\n class={{\n 'data-search-host': true,\n 'data-search-host--borderless': !this.hasBorder,\n 'ds-control--md': true,\n 'ds-control-inactive': this.isInactive,\n }}\n data-filled={hasValue ? '' : undefined}\n data-expanded={this.menuOpen ? '' : undefined}\n >\n <div\n ref={element => {\n this.controlEl = element ?? null;\n }}\n class=\"data-search__control ds-control-frame ds-control--md ds-interaction-fill\"\n >\n <span\n class=\"data-search__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name=\"MagnifyingGlass\" size=\"md\" color=\"inherit\" />\n </span>\n <div class=\"data-search__editor ds-interaction-fill__content\">\n {selected.map(field => (\n <ds-chip\n class=\"data-search__chip\"\n key={field.id}\n label={field.label}\n size=\"md\"\n state=\"default\"\n isInset\n insetDepth=\"double\"\n isInactive={this.isInactive}\n onDsRemove={() => this.removeField(field)}\n />\n ))}\n <input\n ref={element => {\n this.inputEl = element ?? null;\n }}\n id={this.inputId}\n class=\"data-search__input ds-text--body-medium ds-text--regular\"\n type=\"search\"\n role=\"combobox\"\n value={inputValue}\n placeholder={this.placeholder}\n disabled={this.isInactive}\n aria-label={this.ariaLabel}\n aria-describedby={this.descriptionId}\n aria-haspopup=\"listbox\"\n aria-expanded={String(this.menuOpen)}\n aria-controls={this.menuOpen ? this.listboxId : undefined}\n aria-activedescendant={this.activeOptionId}\n aria-keyshortcuts=\"/\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n autoCorrect=\"off\"\n spellcheck={false}\n onInput={event => this.handleInput(event)}\n onKeyDown={event => this.handleKeyDown(event)}\n />\n </div>\n {inputValue.length > 0 && !this.isInactive && (\n <ds-button-unfilled\n class=\"data-search__clear ds-interaction-fill__content\"\n variant=\"icon\"\n size=\"sm\"\n icon=\"CrossCircle\"\n hasBorder={false}\n rounded\n ariaLabel={this.clearLabel}\n onDsClick={() => this.clearSearch()}\n />\n )}\n </div>\n <span id={this.descriptionId} class=\"ds-visually-hidden\">\n {description}\n </span>\n <span class=\"ds-visually-hidden\" aria-live=\"polite\">\n {this.announcement}\n </span>\n {this.menuOpen && (\n <div\n popover=\"manual\"\n ref={element => {\n this.popupEl = element ?? null;\n }}\n class=\"data-search__popup ds-choice-popup\"\n style={popupStyle}\n >\n <div\n id={this.listboxId}\n class=\"data-search__list ds-choice-list\"\n role=\"listbox\"\n aria-label={this.fieldMenuLabel}\n >\n {visible.map((field, index) => {\n const active = index === this.activeIndex;\n return (\n /* eslint-disable-next-line compomo/prefer-direct-ds-text -- The option owns listbox semantics and pointer interaction while ds-text remains its typography child. */\n <div\n id={`${this.componentId}-option-${index}`}\n key={field.id}\n class={{\n 'data-search__option': true,\n 'ds-choice-item': true,\n 'ds-control-frame': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': active && this.focusRingVisible,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': active,\n }}\n role=\"option\"\n aria-selected={String(active)}\n onMouseDown={event => event.preventDefault()}\n onMouseMove={() => {\n this.focusRingVisible = false;\n this.activeIndex = index;\n }}\n onClick={() => this.selectField(field)}\n >\n <ds-text\n class=\"ds-choice-item__label ds-control-label-box ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={active ? 'primary' : 'secondary'}\n >\n {field.label}\n </ds-text>\n </div>\n );\n })}\n {visible.length === 0 && (\n <div class=\"data-search__empty ds-choice-empty ds-empty-region\">\n <ds-empty-state body=\"No matching data points\" />\n </div>\n )}\n </div>\n </div>\n )}\n </Host>\n );\n }\n}\n",
|
|
378
|
+
"content": "import type { DataField } from '../../utils/data-field';\nimport {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n choicePopupMinWidth,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n TOKEN_DEFAULTS,\n} from '../../utils';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport { AnchoredOverlayInteractionController } from '../../utils/anchored-overlay-interaction-controller';\nimport { resolveAnchoredOverlayBoundaryRect } from '../../utils/anchored-overlay-boundary';\nimport {\n availableTableSearchFields,\n filterTableSearchFields,\n nextTableSearchActiveIndex,\n selectedTableSearchFields,\n tableSearchFields,\n} from './data-search-model';\nimport type { DataSearchField, DataSearchFieldsChangeDetail } from './data-search-types';\n\nlet tableSearchSequence = 0;\n\n@Component({\n tag: 'ds-data-search',\n styleUrl: 'DataSearch.css',\n scoped: true,\n})\nexport class DataSearch {\n @Element() private el!: HTMLElement;\n\n /** Show the resting field border; keyboard focus remains visible. */\n @Prop({ reflect: true }) hasBorder: boolean = true;\n\n /** Controlled free-text query. */\n @Prop() value: string = '';\n /** Table column catalog used to derive searchable data points and their complete labels. */\n @Prop() fields: DataField[] = [];\n /** Controlled ordered field scopes rendered as Tags. */\n @Prop() selectedFieldIds: string[] = [];\n @Prop() placeholder: string = 'Search';\n @Prop({ attribute: 'aria-label' }) ariaLabel: string = 'Search table';\n @Prop() clearLabel: string = 'Clear search';\n @Prop() fieldMenuLabel: string = 'Choose search fields';\n @Prop() isInactive: boolean = false;\n\n @Event({ bubbles: false }) dsChange!: EventEmitter<string>;\n @Event({ bubbles: false }) dsFieldsChange!: EventEmitter<DataSearchFieldsChangeDetail>;\n @Event({ bubbles: false }) dsClear!: EventEmitter<void>;\n\n @State() private menuOpen = false;\n @State() private menuQuery = '';\n @State() private activeIndex = 0;\n @State() private focusRingVisible = false;\n @State() private position = { x: 0, y: 0 };\n @State() private positionReady = false;\n @State() private availableHeight: number | null = null;\n @State() private announcement = '';\n\n private readonly componentId = `ds-data-search-${++tableSearchSequence}`;\n private readonly inputId = `${this.componentId}-input`;\n private readonly listboxId = `${this.componentId}-listbox`;\n private readonly descriptionId = `${this.componentId}-description`;\n private inputEl: HTMLInputElement | null = null;\n private controlEl: HTMLElement | null = null;\n private popupEl: HTMLElement | null = null;\n\n private readonly positionController = new AnchoredPositionController({\n getAnchor: () => this.controlEl,\n getPopup: () => this.popupEl,\n getOwnerDocument: () => this.el.ownerDocument,\n measure: (anchor, popup) => {\n if (!this.menuOpen || !popup.isConnected) return null;\n const inset = resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n popup.style.minWidth = `max(var(--ds-choice-popup-min-inline-size, ${TOKEN_DEFAULTS.menuWidthXs}), ${choicePopupMinWidth(anchor.offsetWidth, inset)}px)`;\n const anchorRect = anchor.getBoundingClientRect();\n return {\n anchorRect,\n popupWidth: popup.offsetWidth,\n popupHeight: popup.offsetHeight,\n side: 'bottom',\n align: 'start',\n sideOffsetPx: inset,\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: 'start',\n alignOffsetPx: 0,\n sectionInsetPx: inset,\n }),\n viewportPadPx: inset,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n collisionRect: resolveAnchoredOverlayBoundaryRect(anchor),\n };\n },\n apply: ({ x, y, availableHeight }) => {\n this.position = { x, y };\n this.availableHeight = availableHeight;\n },\n onReady: () => {\n this.positionReady = true;\n },\n topLayer: true,\n liveUpdate: 'frame',\n observeResize: true,\n observeAnchorMotion: true,\n });\n\n private readonly interactionController = new AnchoredOverlayInteractionController({\n getAnchor: () => this.controlEl,\n getPopup: () => this.popupEl,\n getOwnerDocument: () => this.el.ownerDocument,\n onOutsideActivation: () => this.closeMenu(),\n });\n\n disconnectedCallback(): void {\n this.closeMenu();\n }\n\n @Watch('columns')\n @Watch('selectedFieldIds')\n syncAvailableFields(): void {\n const count = this.availableFields.length;\n if (count === 0) {\n this.closeMenu();\n return;\n }\n if (this.activeIndex >= count) this.activeIndex = count - 1;\n }\n\n @Watch('isInactive')\n syncInactive(isInactive: boolean): void {\n if (isInactive) this.closeMenu();\n }\n\n @Method()\n async setFocus(): Promise<void> {\n this.inputEl?.focus();\n }\n\n private get selectedFields(): DataSearchField[] {\n return selectedTableSearchFields(this.searchFields, this.selectedFieldIds);\n }\n\n private get availableFields(): DataSearchField[] {\n return availableTableSearchFields(this.searchFields, this.selectedFieldIds);\n }\n\n private get visibleFields(): DataSearchField[] {\n return filterTableSearchFields(this.availableFields, this.menuQuery);\n }\n\n private get searchFields(): DataSearchField[] {\n return tableSearchFields(this.fields);\n }\n\n private get activeOptionId(): string | undefined {\n return this.menuOpen && this.activeIndex >= 0\n ? `${this.componentId}-option-${this.activeIndex}`\n : undefined;\n }\n\n private openMenu(fromKeyboard: boolean): void {\n if (this.isInactive || this.menuOpen || this.availableFields.length === 0) return;\n this.menuQuery = '';\n // The native value can be ahead of the controlled prop when slash follows input\n // in the same frame, so clear it before the queued render can skip an equal value.\n if (this.inputEl) this.inputEl.value = '';\n this.activeIndex = 0;\n this.focusRingVisible = fromKeyboard;\n this.positionReady = false;\n this.menuOpen = true;\n requestAnimationFrame(() => {\n if (!this.menuOpen) return;\n this.interactionController.connect();\n this.positionController.observe();\n this.positionController.schedule();\n });\n }\n\n private closeMenu(): void {\n if (!this.menuOpen && !this.popupEl) return;\n if (this.popupEl?.matches(':popover-open')) this.popupEl.hidePopover();\n this.positionController.unobserve();\n this.interactionController.disconnect();\n this.popupEl = null;\n this.positionReady = false;\n this.availableHeight = null;\n this.menuOpen = false;\n this.menuQuery = '';\n }\n\n private handleInput(event: Event): void {\n const nextValue = (event.target as HTMLInputElement).value;\n if (this.menuOpen) {\n this.menuQuery = nextValue;\n this.activeIndex = filterTableSearchFields(this.availableFields, nextValue).length ? 0 : -1;\n this.focusRingVisible = false;\n this.positionController.schedule();\n return;\n }\n\n this.dsChange.emit(nextValue);\n }\n\n private selectField(field: DataSearchField): void {\n this.dsFieldsChange.emit({ selectedFieldIds: [...this.selectedFieldIds, field.id] });\n this.announcement = `${field.label} search field added.`;\n this.closeMenu();\n requestAnimationFrame(() => this.inputEl?.focus({ preventScroll: true }));\n }\n\n private removeLastField(): void {\n const selected = this.selectedFields;\n const removed = selected[selected.length - 1];\n if (!removed) return;\n this.dsFieldsChange.emit({\n selectedFieldIds: this.selectedFieldIds.filter(fieldId => fieldId !== removed.id),\n });\n this.announcement = `${removed.label} search field removed.`;\n }\n\n private removeField(field: DataSearchField): void {\n this.dsFieldsChange.emit({\n selectedFieldIds: this.selectedFieldIds.filter(fieldId => fieldId !== field.id),\n });\n this.announcement = `${field.label} search field removed.`;\n requestAnimationFrame(() => this.inputEl?.focus({ preventScroll: true }));\n }\n\n private clearSearch(): void {\n if (this.value) this.dsChange.emit('');\n if (this.selectedFieldIds.length) this.dsFieldsChange.emit({ selectedFieldIds: [] });\n this.dsClear.emit();\n this.announcement = 'Search cleared.';\n this.closeMenu();\n requestAnimationFrame(() => this.inputEl?.focus({ preventScroll: true }));\n }\n\n private handleKeyDown(event: KeyboardEvent): void {\n if (event.key === '/' && !this.menuOpen && !event.altKey && !event.ctrlKey && !event.metaKey) {\n event.preventDefault();\n this.openMenu(true);\n return;\n }\n\n if (event.key === 'Backspace' && this.menuOpen && this.menuQuery.length === 0) {\n event.preventDefault();\n this.closeMenu();\n return;\n }\n\n if (event.key === 'Backspace' && this.value.length === 0) {\n if (this.selectedFields.length) {\n event.preventDefault();\n this.removeLastField();\n }\n return;\n }\n\n if (!this.menuOpen) return;\n\n if (event.key === 'Escape') {\n event.preventDefault();\n this.closeMenu();\n return;\n }\n\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault();\n this.focusRingVisible = true;\n this.activeIndex = nextTableSearchActiveIndex(\n this.activeIndex,\n this.visibleFields.length,\n event.key === 'ArrowDown' ? 1 : -1\n );\n this.el\n .querySelector<HTMLElement>(`#${CSS.escape(this.activeOptionId ?? '')}`)\n ?.scrollIntoView({ block: 'nearest' });\n return;\n }\n\n if (event.key === 'Enter') {\n const field = this.visibleFields[this.activeIndex];\n if (!field) return;\n event.preventDefault();\n this.selectField(field);\n return;\n }\n\n if (event.key === 'Tab') this.closeMenu();\n }\n\n render() {\n const selected = this.selectedFields;\n const visible = this.visibleFields;\n const inputValue = this.menuOpen ? this.menuQuery : this.value;\n const hasValue = this.value.length > 0 || selected.length > 0;\n const description = selected.length\n ? `Search limited to ${selected.map(field => field.label).join(' or ')}.`\n : 'Search includes every available table field. Type slash to choose fields.';\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.position.x)}px, ${Math.round(this.position.y)}px)`,\n zIndex: 'var(--dimension-z-index-floating)',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n if (this.availableHeight !== null) {\n popupStyle['max-height'] = `${Math.floor(this.availableHeight)}px`;\n }\n\n return (\n <Host\n class={{\n 'data-search-host': true,\n 'data-search-host--borderless': !this.hasBorder,\n 'ds-control--md': true,\n 'ds-control-inactive': this.isInactive,\n }}\n data-filled={hasValue ? '' : undefined}\n data-expanded={this.menuOpen ? '' : undefined}\n >\n <div\n ref={element => {\n this.controlEl = element ?? null;\n }}\n class=\"data-search__control ds-control-frame ds-control--md ds-interaction-fill\"\n >\n <span\n class=\"data-search__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name=\"MagnifyingGlass\" size=\"md\" color=\"inherit\" />\n </span>\n <div class=\"data-search__editor ds-interaction-fill__content\">\n {selected.map(field => (\n <ds-chip\n class=\"data-search__chip\"\n key={field.id}\n label={field.label}\n size=\"md\"\n state=\"default\"\n isInset\n insetDepth=\"double\"\n isInactive={this.isInactive}\n onDsRemove={() => this.removeField(field)}\n />\n ))}\n <input\n ref={element => {\n this.inputEl = element ?? null;\n }}\n id={this.inputId}\n class=\"data-search__input ds-text--body-medium ds-text--regular\"\n type=\"search\"\n role=\"combobox\"\n value={inputValue}\n placeholder={this.placeholder}\n disabled={this.isInactive}\n aria-label={this.ariaLabel}\n aria-describedby={this.descriptionId}\n aria-haspopup=\"listbox\"\n aria-expanded={String(this.menuOpen)}\n aria-controls={this.menuOpen ? this.listboxId : undefined}\n aria-activedescendant={this.activeOptionId}\n aria-keyshortcuts=\"/\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n autoCorrect=\"off\"\n spellcheck={false}\n onInput={event => this.handleInput(event)}\n onKeyDown={event => this.handleKeyDown(event)}\n />\n </div>\n {inputValue.length > 0 && !this.isInactive && (\n <ds-button-unfilled\n class=\"data-search__clear ds-interaction-fill__content\"\n variant=\"icon\"\n size=\"sm\"\n icon=\"CrossCircle\"\n hasBorder={false}\n rounded\n ariaLabel={this.clearLabel}\n onDsClick={() => this.clearSearch()}\n />\n )}\n </div>\n <span id={this.descriptionId} class=\"ds-visually-hidden\">\n {description}\n </span>\n <span class=\"ds-visually-hidden\" aria-live=\"polite\">\n {this.announcement}\n </span>\n {this.menuOpen && (\n <div\n popover=\"manual\"\n ref={element => {\n this.popupEl = element ?? null;\n }}\n class=\"data-search__popup ds-choice-popup\"\n style={popupStyle}\n >\n <div\n id={this.listboxId}\n class=\"data-search__list ds-choice-list\"\n role=\"listbox\"\n aria-label={this.fieldMenuLabel}\n >\n {visible.map((field, index) => {\n const active = index === this.activeIndex;\n return (\n /* eslint-disable-next-line compomo/prefer-direct-ds-text -- The option owns listbox semantics and pointer interaction while ds-text remains its typography child. */\n <div\n id={`${this.componentId}-option-${index}`}\n key={field.id}\n class={{\n 'data-search__option': true,\n 'ds-choice-item': true,\n 'ds-control-frame': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': active && this.focusRingVisible,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': active,\n }}\n role=\"option\"\n aria-selected={String(active)}\n onMouseDown={event => event.preventDefault()}\n onMouseMove={() => {\n this.focusRingVisible = false;\n this.activeIndex = index;\n }}\n onClick={() => this.selectField(field)}\n >\n <ds-text\n class=\"ds-choice-item__label ds-control-label-box ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={active ? 'primary' : 'secondary'}\n >\n {field.label}\n </ds-text>\n </div>\n );\n })}\n {visible.length === 0 && (\n <div class=\"data-search__empty ds-choice-empty ds-empty-region\">\n <ds-empty-state body=\"No matching data points\" />\n </div>\n )}\n </div>\n </div>\n )}\n </Host>\n );\n }\n}\n",
|
|
379
379
|
"type": "registry:ui"
|
|
380
380
|
},
|
|
381
381
|
{
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "map-cluster-marker",
|
|
4
|
+
"title": "MapClusterMarker",
|
|
5
|
+
"description": "Engine-independent map cluster button with compact count formatting and a hover or focus caption.",
|
|
6
|
+
"type": "registry:ui",
|
|
7
|
+
"meta": {
|
|
8
|
+
"library": "@ds-mo/ui",
|
|
9
|
+
"distribution": "npm",
|
|
10
|
+
"storybook": "https://zainadeel.github.io/compomo/?path=/story/map-cluster-marker",
|
|
11
|
+
"source": "src/wc/components/MapClusterMarker/MapClusterMarker.tsx",
|
|
12
|
+
"intentStatus": "complete",
|
|
13
|
+
"intent": {
|
|
14
|
+
"audience": "general",
|
|
15
|
+
"status": "experimental",
|
|
16
|
+
"summary": "Engine-independent map cluster button with compact count formatting and a hover or focus caption.",
|
|
17
|
+
"useWhen": [
|
|
18
|
+
"An application map needs one interactive marker to represent several nearby entities.",
|
|
19
|
+
"The map engine can mount a custom element and retain ownership of clustering, coordinates, anchoring, and camera behavior."
|
|
20
|
+
],
|
|
21
|
+
"avoidWhen": [
|
|
22
|
+
"The marker represents one vehicle, asset, person, or travel group; use MapEntityMarker.",
|
|
23
|
+
"The count is a notification attached to another control; use Badge.",
|
|
24
|
+
"The component would be asked to calculate clusters or own geographic and selected-entity business state."
|
|
25
|
+
],
|
|
26
|
+
"alternatives": [
|
|
27
|
+
{
|
|
28
|
+
"when": "One map entity needs operating state, heading, and stale treatment.",
|
|
29
|
+
"component": "component:ds-map-entity-marker",
|
|
30
|
+
"reason": "MapEntityMarker owns single-entity marker semantics and geometry."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"when": "A count supplements another non-map control.",
|
|
34
|
+
"component": "component:ds-badge",
|
|
35
|
+
"reason": "Badge owns compact notification and counter indicators."
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"commonlyComposedWith": [
|
|
39
|
+
"component:ds-map-entity-marker",
|
|
40
|
+
"component:ds-text"
|
|
41
|
+
],
|
|
42
|
+
"accessibility": [
|
|
43
|
+
"The component renders a native button and label is its accessible action name. Prefer a label that includes the full count and outcome, such as ‘Zoom to 24 vehicles’.",
|
|
44
|
+
"Caption is a supplemental visual identifier and is hidden from assistive technology to avoid duplicating label.",
|
|
45
|
+
"Keyboard focus reveals the same caption as hover and uses the shared visible focus ring.",
|
|
46
|
+
"The compact displayed count does not replace the full count in label."
|
|
47
|
+
],
|
|
48
|
+
"states": [
|
|
49
|
+
"count is rounded to a non-negative whole number and compacts with a k suffix at one thousand and above.",
|
|
50
|
+
"dimmed is visual de-emphasis only. The map owner decides when selection context should dim cluster peers and keeps every cluster operable. A dimmed cluster returns to full opacity while hovered or keyboard-focused so its revealed caption remains legible.",
|
|
51
|
+
"Activation emits dsClick without calculating a cluster expansion or changing camera state internally."
|
|
52
|
+
],
|
|
53
|
+
"responsiveBehavior": [
|
|
54
|
+
"Marker height and minimum width stay fixed across breakpoints so map-engine anchors remain stable; width grows only for the compact count.",
|
|
55
|
+
"The map owner controls collision handling, overlays, and viewport-aware caption placement."
|
|
56
|
+
],
|
|
57
|
+
"frameworkCaveats": {
|
|
58
|
+
"customElements": [
|
|
59
|
+
"A map engine that accepts an Element for a custom marker or div icon can mount ds-map-cluster-marker directly.",
|
|
60
|
+
"Listen for dsClick and keep cluster membership, expansion zoom, z-index, coordinates, and camera movement in application state."
|
|
61
|
+
],
|
|
62
|
+
"react": [
|
|
63
|
+
"Pass count as a number and handle dsClick through the generated event binding."
|
|
64
|
+
],
|
|
65
|
+
"angular": [
|
|
66
|
+
"Bind count as a number and keep map-engine marker lifecycle in the owning map adapter."
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"references": [
|
|
70
|
+
{
|
|
71
|
+
"label": "Storybook examples",
|
|
72
|
+
"path": "src/wc/components/MapClusterMarker/MapClusterMarker.stories.ts"
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"styling": {
|
|
76
|
+
"rationale": "The component owns stable map anchoring geometry and semantic cluster recipes; the map engine owns host placement and stacking.",
|
|
77
|
+
"protected": [
|
|
78
|
+
{
|
|
79
|
+
"properties": [
|
|
80
|
+
"height",
|
|
81
|
+
"min-width",
|
|
82
|
+
"padding",
|
|
83
|
+
"border-radius",
|
|
84
|
+
"background",
|
|
85
|
+
"color",
|
|
86
|
+
"opacity"
|
|
87
|
+
],
|
|
88
|
+
"reason": "Cluster geometry, count legibility, interaction treatment, and de-emphasis are one accessible visual recipe.",
|
|
89
|
+
"props": [
|
|
90
|
+
"count",
|
|
91
|
+
"dimmed"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"customProperties": []
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"api": {
|
|
99
|
+
"props": {
|
|
100
|
+
"caption": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"resolvedType": "string",
|
|
103
|
+
"attribute": "caption",
|
|
104
|
+
"default": "''",
|
|
105
|
+
"required": false,
|
|
106
|
+
"mutable": false,
|
|
107
|
+
"description": "Short visible cluster identifier revealed on hover or keyboard focus."
|
|
108
|
+
},
|
|
109
|
+
"count": {
|
|
110
|
+
"type": "number",
|
|
111
|
+
"resolvedType": "number",
|
|
112
|
+
"attribute": "count",
|
|
113
|
+
"default": "0",
|
|
114
|
+
"required": false,
|
|
115
|
+
"mutable": false,
|
|
116
|
+
"description": "Number of represented entities, compacted at one thousand and above."
|
|
117
|
+
},
|
|
118
|
+
"dimmed": {
|
|
119
|
+
"type": "boolean",
|
|
120
|
+
"resolvedType": "boolean",
|
|
121
|
+
"attribute": "dimmed",
|
|
122
|
+
"default": "false",
|
|
123
|
+
"required": false,
|
|
124
|
+
"mutable": false,
|
|
125
|
+
"description": "De-emphasizes this cluster when an owner-managed selection is active elsewhere."
|
|
126
|
+
},
|
|
127
|
+
"label": {
|
|
128
|
+
"type": "string",
|
|
129
|
+
"resolvedType": "string",
|
|
130
|
+
"attribute": "label",
|
|
131
|
+
"required": true,
|
|
132
|
+
"mutable": false,
|
|
133
|
+
"description": "Accessible action label. Describe the cluster and what activation does."
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"events": [
|
|
137
|
+
{
|
|
138
|
+
"name": "dsClick",
|
|
139
|
+
"detail": "MouseEvent",
|
|
140
|
+
"bubbles": true,
|
|
141
|
+
"cancelable": true,
|
|
142
|
+
"composed": true,
|
|
143
|
+
"description": "Fired when the cluster's native button is activated."
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
"methods": [
|
|
147
|
+
{
|
|
148
|
+
"name": "setFocus",
|
|
149
|
+
"signature": "setFocus() => Promise<void>",
|
|
150
|
+
"description": "Moves keyboard focus to the cluster button."
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
"slots": []
|
|
154
|
+
},
|
|
155
|
+
"props": {
|
|
156
|
+
"caption": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"resolvedType": "string",
|
|
159
|
+
"attribute": "caption",
|
|
160
|
+
"default": "''",
|
|
161
|
+
"required": false,
|
|
162
|
+
"mutable": false,
|
|
163
|
+
"description": "Short visible cluster identifier revealed on hover or keyboard focus."
|
|
164
|
+
},
|
|
165
|
+
"count": {
|
|
166
|
+
"type": "number",
|
|
167
|
+
"resolvedType": "number",
|
|
168
|
+
"attribute": "count",
|
|
169
|
+
"default": "0",
|
|
170
|
+
"required": false,
|
|
171
|
+
"mutable": false,
|
|
172
|
+
"description": "Number of represented entities, compacted at one thousand and above."
|
|
173
|
+
},
|
|
174
|
+
"dimmed": {
|
|
175
|
+
"type": "boolean",
|
|
176
|
+
"resolvedType": "boolean",
|
|
177
|
+
"attribute": "dimmed",
|
|
178
|
+
"default": "false",
|
|
179
|
+
"required": false,
|
|
180
|
+
"mutable": false,
|
|
181
|
+
"description": "De-emphasizes this cluster when an owner-managed selection is active elsewhere."
|
|
182
|
+
},
|
|
183
|
+
"label": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"resolvedType": "string",
|
|
186
|
+
"attribute": "label",
|
|
187
|
+
"required": true,
|
|
188
|
+
"mutable": false,
|
|
189
|
+
"description": "Accessible action label. Describe the cluster and what activation does."
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"events": [
|
|
193
|
+
{
|
|
194
|
+
"name": "dsClick",
|
|
195
|
+
"detail": "MouseEvent",
|
|
196
|
+
"bubbles": true,
|
|
197
|
+
"cancelable": true,
|
|
198
|
+
"composed": true,
|
|
199
|
+
"description": "Fired when the cluster's native button is activated."
|
|
200
|
+
}
|
|
201
|
+
],
|
|
202
|
+
"methods": [
|
|
203
|
+
{
|
|
204
|
+
"name": "setFocus",
|
|
205
|
+
"signature": "setFocus() => Promise<void>",
|
|
206
|
+
"description": "Moves keyboard focus to the cluster button."
|
|
207
|
+
}
|
|
208
|
+
],
|
|
209
|
+
"slots": [],
|
|
210
|
+
"exports": {
|
|
211
|
+
"customElement": "ds-map-cluster-marker",
|
|
212
|
+
"react": "DsMapClusterMarker",
|
|
213
|
+
"vue": "DsMapClusterMarker",
|
|
214
|
+
"angular": "DsMapClusterMarker"
|
|
215
|
+
},
|
|
216
|
+
"consumption": {
|
|
217
|
+
"install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
|
|
218
|
+
"cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
|
|
219
|
+
"customElements": {
|
|
220
|
+
"import": "import '@ds-mo/ui/dist/components/ds-map-cluster-marker.js';",
|
|
221
|
+
"example": "<ds-map-cluster-marker></ds-map-cluster-marker>"
|
|
222
|
+
},
|
|
223
|
+
"react": {
|
|
224
|
+
"import": "import { DsMapClusterMarker } from '@ds-mo/ui/react';",
|
|
225
|
+
"example": "<DsMapClusterMarker />"
|
|
226
|
+
},
|
|
227
|
+
"vue": {
|
|
228
|
+
"import": "import { DsMapClusterMarker } from '@ds-mo/ui/vue';",
|
|
229
|
+
"example": "<DsMapClusterMarker />"
|
|
230
|
+
},
|
|
231
|
+
"angular": {
|
|
232
|
+
"import": "import { DsMapClusterMarker } from '@ds-mo/ui/angular/ds-map-cluster-marker';",
|
|
233
|
+
"example": "<ds-map-cluster-marker></ds-map-cluster-marker>"
|
|
234
|
+
},
|
|
235
|
+
"peerDependencies": {
|
|
236
|
+
"required": [
|
|
237
|
+
"@ds-mo/tokens ^6.5.1",
|
|
238
|
+
"@ds-mo/icons ^8.1.0"
|
|
239
|
+
],
|
|
240
|
+
"frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
"dependencies": [
|
|
245
|
+
"@ds-mo/ui",
|
|
246
|
+
"@ds-mo/tokens"
|
|
247
|
+
],
|
|
248
|
+
"registryDependencies": [
|
|
249
|
+
"text"
|
|
250
|
+
],
|
|
251
|
+
"files": [
|
|
252
|
+
{
|
|
253
|
+
"path": "src/wc/components/MapClusterMarker/MapClusterMarker.css",
|
|
254
|
+
"content": "@import '../../utils/focus-ring.css';\n@import '../../utils/map-marker.css';\n\n:host {\n --_map-marker-background: var(--color-entity-cluster-marker-background);\n --_map-marker-foreground: var(--color-entity-cluster-marker-foreground);\n --_map-marker-interaction-hover: var(--color-entity-cluster-marker-interaction-hover);\n --_map-marker-interaction-pressed: var(--color-entity-cluster-marker-interaction-pressed);\n}\n\n.map-marker__button {\n width: max-content;\n padding-inline: 0;\n border-radius: var(--dimension-radius-half);\n}\n\n.map-marker__button,\n.map-marker__caption {\n -webkit-backdrop-filter: blur(var(--effect-blur-md));\n backdrop-filter: blur(var(--effect-blur-md));\n}\n\n.map-cluster-marker__button--padded {\n padding-inline: var(--dimension-space-100);\n}\n\n.map-cluster-marker__count {\n position: relative;\n z-index: 2;\n display: inline-flex;\n white-space: nowrap;\n}\n",
|
|
255
|
+
"type": "registry:ui"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"path": "src/wc/components/MapClusterMarker/MapClusterMarker.tsx",
|
|
259
|
+
"content": "import { Component, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\n\nconst formatCount = (count: number): string => {\n const normalizedCount = Math.max(0, Math.round(Number.isFinite(count) ? count : 0));\n if (normalizedCount < 1000) return String(normalizedCount);\n const compactCount = Math.round(normalizedCount / 100) / 10;\n return `${Number.isInteger(compactCount) ? compactCount.toFixed(0) : compactCount.toFixed(1)}k`;\n};\n\n@Component({\n tag: 'ds-map-cluster-marker',\n styleUrl: 'MapClusterMarker.css',\n scoped: true,\n})\nexport class MapClusterMarker {\n /** Accessible action label. Describe the cluster and what activation does. */\n @Prop() label!: string;\n\n /** Short visible cluster identifier revealed on hover or keyboard focus. */\n @Prop() caption: string = '';\n\n /** Number of represented entities, compacted at one thousand and above. */\n @Prop() count: number = 0;\n\n /** De-emphasizes this cluster when an owner-managed selection is active elsewhere. */\n @Prop({ reflect: true }) dimmed: boolean = false;\n\n /** Fired when the cluster's native button is activated. */\n @Event() dsClick!: EventEmitter<MouseEvent>;\n\n private buttonEl?: HTMLButtonElement;\n\n /** Moves keyboard focus to the cluster button. */\n @Method()\n async setFocus(): Promise<void> {\n this.buttonEl?.focus();\n }\n\n private handleClick = (event: MouseEvent) => {\n this.dsClick.emit(event);\n };\n\n render() {\n const caption = this.caption.trim();\n const countLabel = formatCount(this.count);\n\n return (\n <Host class={{ 'map-cluster-marker': true, 'map-marker--dimmed': this.dimmed }}>\n <button\n ref={element => (this.buttonEl = element)}\n type=\"button\"\n class={{\n 'map-marker__button': true,\n 'map-cluster-marker__button--padded': countLabel.length > 1,\n 'ds-focus-ring': true,\n }}\n aria-label={this.label}\n onClick={this.handleClick}\n >\n <ds-text\n class=\"map-cluster-marker__count\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"inherit\"\n emphasis\n >\n {countLabel}\n </ds-text>\n </button>\n {caption && (\n <ds-text\n class=\"map-marker__caption\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"inherit\"\n aria-hidden=\"true\"\n >\n {caption}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
|
|
260
|
+
"type": "registry:ui"
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
}
|