@ds-mo/ui 14.18.1 → 14.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.package-ready.json +1 -1
- package/dist/agent-patterns.json +1 -1
- package/dist/agent.json +16 -9
- package/dist/angular/ds-pagination.d.ts +1 -1
- package/dist/angular/ds-pagination.js +3 -3
- 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-avatar.js +1 -1
- package/dist/components/ds-badge.js +1 -1
- package/dist/components/ds-banner.js +1 -1
- package/dist/components/ds-banner.js.map +1 -1
- package/dist/components/ds-bar-action.js +1 -1
- package/dist/components/ds-bar-action.js.map +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-title.js +1 -1
- package/dist/components/ds-bar-workflow.js +1 -1
- package/dist/components/ds-bar-workflow.js.map +1 -1
- package/dist/components/ds-breadcrumb.js +1 -1
- package/dist/components/ds-button-filled.js +1 -1
- package/dist/components/ds-button-unfilled.js +1 -1
- package/dist/components/ds-card-chart.js +1 -1
- package/dist/components/ds-card-chart.js.map +1 -1
- package/dist/components/ds-card-overview.js +1 -1
- package/dist/components/ds-card-overview.js.map +1 -1
- package/dist/components/ds-card-setting.js +1 -1
- package/dist/components/ds-card-setting.js.map +1 -1
- package/dist/components/ds-chart-legend.js +1 -1
- package/dist/components/ds-chart-legend.js.map +1 -1
- package/dist/components/ds-chart.js +1 -1
- package/dist/components/ds-chart.js.map +1 -1
- package/dist/components/ds-checkbox.js +1 -1
- package/dist/components/ds-chip.js +1 -1
- package/dist/components/ds-chip.js.map +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.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-filter-menu.js +1 -1
- package/dist/components/ds-icon.js +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-loader.js +1 -1
- package/dist/components/ds-markdown.js +1 -1
- package/dist/components/ds-menu.js +1 -1
- package/dist/components/ds-message-actions.js +1 -1
- package/dist/components/ds-message-bubble.js +1 -1
- package/dist/components/ds-message-bubble.js.map +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-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-sub-nav.js.map +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-tool-search.js.map +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-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-app.js.map +1 -1
- package/dist/components/ds-shell-page.js +1 -1
- package/dist/components/ds-shell-tools.js +1 -1
- package/dist/components/ds-skeleton.js +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-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-table-filter.js +1 -1
- package/dist/components/ds-table-saved-views.js +1 -1
- package/dist/components/ds-table-sort.js +1 -1
- package/dist/components/ds-table-sort.js.map +1 -1
- package/dist/components/ds-table-toolbar.js +1 -1
- package/dist/components/ds-table.js +1 -1
- package/dist/components/ds-table.js.map +1 -1
- package/dist/components/ds-tag.js +1 -1
- package/dist/components/ds-text.js +1 -1
- package/dist/components/ds-toast.js +1 -1
- package/dist/components/ds-toast.js.map +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-0XTyQNRG.js +2 -0
- package/dist/components/{p-JPTkZ3Ji.js.map → p-0XTyQNRG.js.map} +1 -1
- package/dist/components/{p-DFQ9mcbK.js → p-1YHFQeI7.js} +1 -1
- package/dist/components/p-1YHFQeI7.js.map +1 -0
- package/dist/components/p-B-HsfQOK.js +2 -0
- package/dist/components/p-B-HsfQOK.js.map +1 -0
- package/dist/components/p-B4wTnsUL.js +2 -0
- package/dist/components/p-B4wTnsUL.js.map +1 -0
- package/dist/components/p-B6WQre5D.js +2 -0
- package/dist/components/p-B6WQre5D.js.map +1 -0
- package/dist/components/{p-B1JRCDHS.js → p-BHhmB9-4.js} +2 -2
- package/dist/components/{p-B1JRCDHS.js.map → p-BHhmB9-4.js.map} +1 -1
- package/dist/components/{p-CqQFowZd.js → p-BNq9fr9S.js} +2 -2
- package/dist/components/p-BNq9fr9S.js.map +1 -0
- package/dist/components/{p-BKp__CQX.js → p-BWwiCvep.js} +2 -2
- package/dist/components/p-BWwiCvep.js.map +1 -0
- package/dist/components/{p-BBG6Sswb.js → p-BYFS2qWb.js} +2 -2
- package/dist/components/{p-BBG6Sswb.js.map → p-BYFS2qWb.js.map} +1 -1
- package/dist/components/{p-uI6z53BE.js → p-Bb4KnRy0.js} +2 -2
- package/dist/components/{p-HB_cf_aR.js → p-BeA6Jv-3.js} +2 -2
- package/dist/components/{p-HB_cf_aR.js.map → p-BeA6Jv-3.js.map} +1 -1
- package/dist/components/{p-1Hu9jlju.js → p-Bf7PZJPT.js} +2 -2
- package/dist/components/{p-Bbb9A3NV.js → p-BjRD57w3.js} +2 -2
- package/dist/components/p-BjRD57w3.js.map +1 -0
- package/dist/components/{p-CKVHrOEp.js → p-BmlGeaY8.js} +2 -2
- package/dist/components/p-BmlGeaY8.js.map +1 -0
- package/dist/components/{p-CLVxgTme.js → p-BubgDQlb.js} +2 -2
- package/dist/components/p-BubgDQlb.js.map +1 -0
- package/dist/components/{p-BNkU9dcc.js → p-Bv6FSDPR.js} +2 -2
- package/dist/components/{p-BNkU9dcc.js.map → p-Bv6FSDPR.js.map} +1 -1
- package/dist/components/p-Bx1IHqJb.js +2 -0
- package/dist/components/p-Bx1IHqJb.js.map +1 -0
- package/dist/components/{p-D3fMaUJ3.js → p-Bx5BSd6F.js} +2 -2
- package/dist/components/p-Bx5BSd6F.js.map +1 -0
- package/dist/components/{p-B8q-6a03.js → p-C0qN3PSb.js} +1 -1
- package/dist/components/{p-B8q-6a03.js.map → p-C0qN3PSb.js.map} +1 -1
- package/dist/components/{p-H5T258T-.js → p-CFsLF-ts.js} +2 -2
- package/dist/components/p-CFsLF-ts.js.map +1 -0
- package/dist/components/p-CKtJ6FHM.js +2 -0
- package/dist/components/p-CKtJ6FHM.js.map +1 -0
- package/dist/components/p-CRSwNbPk.js +2 -0
- package/dist/components/p-CRSwNbPk.js.map +1 -0
- package/dist/components/{p-BFfLvMZd.js → p-Cea7rROU.js} +2 -2
- package/dist/components/{p-BFfLvMZd.js.map → p-Cea7rROU.js.map} +1 -1
- package/dist/components/{p-B0Byw6OX.js → p-CgGcgMjs.js} +1 -1
- package/dist/components/{p-B0Byw6OX.js.map → p-CgGcgMjs.js.map} +1 -1
- package/dist/components/{p-C-Y7gYgz.js → p-ChfVRPHw.js} +2 -2
- package/dist/components/p-ChfVRPHw.js.map +1 -0
- package/dist/components/{p-CJq3n5kA.js → p-Ck1hgD6g.js} +2 -2
- package/dist/components/p-Ck1hgD6g.js.map +1 -0
- package/dist/components/{p-BMc0DE59.js → p-CochmPyg.js} +1 -1
- package/dist/components/p-CochmPyg.js.map +1 -0
- package/dist/components/{p-iMuFgSoM.js → p-Cp5Hr7Vz.js} +2 -2
- package/dist/components/p-Cp5Hr7Vz.js.map +1 -0
- package/dist/components/p-CpUXubax.js +2 -0
- package/dist/components/{p-DtndC9f1.js.map → p-CpUXubax.js.map} +1 -1
- package/dist/components/p-Crz606uD.js +2 -0
- package/dist/components/p-Crz606uD.js.map +1 -0
- package/dist/components/p-Cxr0IQjC.js.map +1 -1
- package/dist/components/p-D-MWvpg6.js +2 -0
- package/dist/components/p-D-MWvpg6.js.map +1 -0
- package/dist/components/{p-Bu3dOjpW.js → p-D7v6Na7U.js} +2 -2
- package/dist/components/{p-Bu3dOjpW.js.map → p-D7v6Na7U.js.map} +1 -1
- package/dist/components/{p-C6DNhlES.js → p-DA5H_Iq_.js} +1 -1
- package/dist/components/p-DA5H_Iq_.js.map +1 -0
- package/dist/components/p-DBYCwq9L.js +2 -0
- package/dist/components/{p-BXN-e7vF.js.map → p-DBYCwq9L.js.map} +1 -1
- package/dist/components/{p-CY4YFAT3.js → p-DCbKsABt.js} +1 -1
- package/dist/components/p-DCbKsABt.js.map +1 -0
- package/dist/components/{p-C6R5Lft0.js → p-DFC0sRM-.js} +2 -2
- package/dist/components/p-DFC0sRM-.js.map +1 -0
- package/dist/components/{p-CsBICYmt.js → p-DGkn8FwV.js} +2 -2
- package/dist/components/p-DGkn8FwV.js.map +1 -0
- package/dist/components/p-DIqT1m2r.js +2 -0
- package/dist/components/p-DIqT1m2r.js.map +1 -0
- package/dist/components/p-DKt_8s-U.js +2 -0
- package/dist/components/p-DKt_8s-U.js.map +1 -0
- package/dist/components/p-DOUk4KoT.js.map +1 -1
- package/dist/components/p-DQBT1Jkf.js +2 -0
- package/dist/components/p-DQBT1Jkf.js.map +1 -0
- package/dist/components/p-DW5Ita-P.js +2 -0
- package/dist/components/{p-DUdOmSFR.js.map → p-DW5Ita-P.js.map} +1 -1
- package/dist/components/{p-DNpLDbWY.js → p-DZ58Sg5a.js} +1 -1
- package/dist/components/p-DZ58Sg5a.js.map +1 -0
- package/dist/components/{p-DDgeXGbB.js → p-DZv2g9Pe.js} +2 -2
- package/dist/components/p-DZv2g9Pe.js.map +1 -0
- package/dist/components/{p-BknXYAEc.js → p-DdH7b4JF.js} +2 -2
- package/dist/components/p-DdH7b4JF.js.map +1 -0
- package/dist/components/{p-DBue3ISk.js → p-Demfba7T.js} +2 -2
- package/dist/components/p-Demfba7T.js.map +1 -0
- package/dist/components/p-Djxw2FjZ.js +2 -0
- package/dist/components/{p-CwjVgUqy.js.map → p-Djxw2FjZ.js.map} +1 -1
- package/dist/components/p-DmWoFSGs.js +2 -0
- package/dist/components/p-DmWoFSGs.js.map +1 -0
- package/dist/components/p-Do8wbFff.js +2 -0
- package/dist/components/p-Do8wbFff.js.map +1 -0
- package/dist/components/p-Drnyq8z7.js +2 -0
- package/dist/components/p-Drnyq8z7.js.map +1 -0
- package/dist/components/p-DsgEY6mZ.js +2 -0
- package/dist/components/p-DsgEY6mZ.js.map +1 -0
- package/dist/components/p-Is1lFmwp.js +2 -0
- package/dist/components/p-Is1lFmwp.js.map +1 -0
- package/dist/components/{p-BUDAwJVV.js → p-caLOcQUK.js} +2 -2
- package/dist/components/p-caLOcQUK.js.map +1 -0
- package/dist/components/{p-D1eEgwcP.js → p-etg-Uk1u.js} +1 -1
- package/dist/components/{p-D1eEgwcP.js.map → p-etg-Uk1u.js.map} +1 -1
- package/dist/components/{p-C9gC4haA.js → p-fRDzpfi9.js} +2 -2
- package/dist/components/p-fRDzpfi9.js.map +1 -0
- package/dist/components/{p-C-vRkDyn.js → p-rCeztgVn.js} +1 -1
- package/dist/components/{p-C-vRkDyn.js.map → p-rCeztgVn.js.map} +1 -1
- package/dist/components/{p-B_jZIC9R.js → p-vrotWZxt.js} +2 -2
- package/dist/components/{p-B_jZIC9R.js.map → p-vrotWZxt.js.map} +1 -1
- package/dist/docs/components.json +35 -6
- package/dist/lib/shell/index.d.ts +2 -2
- package/dist/lib/shell/index.js +8 -6
- package/dist/lib/shell/index.js.map +2 -2
- package/dist/lib/toast/index.js +1 -4
- package/dist/lib/toast/index.js.map +2 -2
- package/dist/lib/utils/index.d.ts +1 -1
- package/dist/lib/utils/index.js +104 -22
- package/dist/lib/utils/index.js.map +2 -2
- package/dist/mcp/cli.js +70 -31
- package/dist/mcp-data/registry/agent-activity.json +1 -1
- package/dist/mcp-data/registry/agent-questionnaire.json +3 -3
- package/dist/mcp-data/registry/agent-source-list.json +2 -2
- package/dist/mcp-data/registry/agent-tool-call.json +2 -2
- package/dist/mcp-data/registry/attachment-list.json +1 -1
- package/dist/mcp-data/registry/badge.json +1 -1
- package/dist/mcp-data/registry/banner.json +2 -2
- package/dist/mcp-data/registry/bar-action.json +1 -1
- package/dist/mcp-data/registry/bar-nav.json +3 -3
- package/dist/mcp-data/registry/bar-workflow.json +1 -1
- package/dist/mcp-data/registry/breadcrumb.json +1 -1
- package/dist/mcp-data/registry/button-filled.json +1 -1
- package/dist/mcp-data/registry/button-unfilled.json +1 -1
- package/dist/mcp-data/registry/card-chart.json +2 -2
- package/dist/mcp-data/registry/card-overview.json +3 -3
- package/dist/mcp-data/registry/card-setting.json +1 -1
- package/dist/mcp-data/registry/chart-legend.json +1 -1
- package/dist/mcp-data/registry/chart.json +1 -1
- package/dist/mcp-data/registry/checkbox.json +1 -1
- package/dist/mcp-data/registry/chip.json +1 -1
- package/dist/mcp-data/registry/code-block.json +1 -1
- package/dist/mcp-data/registry/conversation-list-item.json +1 -1
- package/dist/mcp-data/registry/conversation-list.json +2 -2
- package/dist/mcp-data/registry/divider.json +1 -1
- package/dist/mcp-data/registry/empty-state.json +1 -1
- package/dist/mcp-data/registry/field.json +1 -1
- package/dist/mcp-data/registry/filter-menu.json +1 -1
- package/dist/mcp-data/registry/icon.json +2 -2
- package/dist/mcp-data/registry/input.json +2 -2
- package/dist/mcp-data/registry/loader.json +1 -1
- package/dist/mcp-data/registry/menu.json +3 -3
- package/dist/mcp-data/registry/message-bubble.json +1 -1
- package/dist/mcp-data/registry/message-composer.json +2 -2
- package/dist/mcp-data/registry/message-scroller.json +3 -3
- package/dist/mcp-data/registry/mobile-bar-nav.json +1 -1
- package/dist/mcp-data/registry/mobile-header.json +2 -2
- package/dist/mcp-data/registry/mobile-section-switcher.json +2 -2
- package/dist/mcp-data/registry/mobile-sheet-nav.json +1 -1
- package/dist/mcp-data/registry/modal.json +2 -2
- package/dist/mcp-data/registry/pagination.json +32 -11
- package/dist/mcp-data/registry/panel-nav.json +2 -2
- package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
- package/dist/mcp-data/registry/panel-tool-header.json +1 -1
- 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 +15 -8
- package/dist/mcp-data/registry/select.json +1 -1
- package/dist/mcp-data/registry/shell-app.json +2 -2
- package/dist/mcp-data/registry/shell-page.json +1 -1
- package/dist/mcp-data/registry/shell-tools.json +1 -1
- package/dist/mcp-data/registry/skeleton.json +2 -2
- package/dist/mcp-data/registry/slider.json +2 -2
- package/dist/mcp-data/registry/swatch-picker.json +3 -3
- package/dist/mcp-data/registry/switch.json +2 -2
- package/dist/mcp-data/registry/tab-group.json +2 -2
- package/dist/mcp-data/registry/table-sort.json +1 -1
- package/dist/mcp-data/registry/table.json +18 -18
- package/dist/mcp-data/registry/tag.json +2 -2
- package/dist/mcp-data/registry/text.json +4 -4
- package/dist/mcp-data/registry/toast.json +2 -2
- package/dist/mcp-data/registry/tooltip-chart.json +2 -2
- package/dist/mcp-data/registry/tooltip.json +2 -2
- package/dist/styles/prose.css +42 -38
- package/dist/styles/table.css +39 -67
- package/dist/types/components/CardSetting/index.d.ts +1 -1
- package/dist/types/components/Pagination/Pagination.d.ts +2 -0
- package/dist/types/components/Pagination/pagination-types.d.ts +2 -0
- package/dist/types/components/TooltipChart/index.d.ts +1 -1
- package/dist/types/components.d.ts +11 -0
- package/dist/types/shell/index.d.ts +2 -2
- package/dist/types/utils/index.d.ts +1 -1
- package/dist/vue/ds-pagination.js +1 -0
- package/package.json +11 -1
- package/dist/components/p-B00PEE6t.js +0 -2
- package/dist/components/p-B00PEE6t.js.map +0 -1
- package/dist/components/p-BCOl1x0_.js +0 -2
- package/dist/components/p-BCOl1x0_.js.map +0 -1
- package/dist/components/p-BHVOiWz5.js +0 -2
- package/dist/components/p-BHVOiWz5.js.map +0 -1
- package/dist/components/p-BKp__CQX.js.map +0 -1
- package/dist/components/p-BMc0DE59.js.map +0 -1
- package/dist/components/p-BPG0r_mC.js +0 -2
- package/dist/components/p-BPG0r_mC.js.map +0 -1
- package/dist/components/p-BUDAwJVV.js.map +0 -1
- package/dist/components/p-BXN-e7vF.js +0 -2
- package/dist/components/p-Bbb9A3NV.js.map +0 -1
- package/dist/components/p-Bece5KH3.js +0 -2
- package/dist/components/p-Bece5KH3.js.map +0 -1
- package/dist/components/p-BhYS_Ump.js +0 -2
- package/dist/components/p-BhYS_Ump.js.map +0 -1
- package/dist/components/p-Bkh7scMQ.js +0 -2
- package/dist/components/p-Bkh7scMQ.js.map +0 -1
- package/dist/components/p-BknXYAEc.js.map +0 -1
- package/dist/components/p-C-Y7gYgz.js.map +0 -1
- package/dist/components/p-C6DNhlES.js.map +0 -1
- package/dist/components/p-C6R5Lft0.js.map +0 -1
- package/dist/components/p-C9gC4haA.js.map +0 -1
- package/dist/components/p-CAnnQL1F.js +0 -2
- package/dist/components/p-CAnnQL1F.js.map +0 -1
- package/dist/components/p-CE2IKOsy.js +0 -2
- package/dist/components/p-CE2IKOsy.js.map +0 -1
- package/dist/components/p-CJq3n5kA.js.map +0 -1
- package/dist/components/p-CKVHrOEp.js.map +0 -1
- package/dist/components/p-CLVxgTme.js.map +0 -1
- package/dist/components/p-CY4YFAT3.js.map +0 -1
- package/dist/components/p-Cg0kimXZ.js +0 -2
- package/dist/components/p-Cg0kimXZ.js.map +0 -1
- package/dist/components/p-CqQFowZd.js.map +0 -1
- package/dist/components/p-CsBICYmt.js.map +0 -1
- package/dist/components/p-CwjVgUqy.js +0 -2
- package/dist/components/p-D123Jvp-.js +0 -2
- package/dist/components/p-D123Jvp-.js.map +0 -1
- package/dist/components/p-D3fMaUJ3.js.map +0 -1
- package/dist/components/p-D7R4yE2C.js +0 -2
- package/dist/components/p-D7R4yE2C.js.map +0 -1
- package/dist/components/p-D7haeL5y.js +0 -2
- package/dist/components/p-D7haeL5y.js.map +0 -1
- package/dist/components/p-DAvh38E4.js +0 -2
- package/dist/components/p-DAvh38E4.js.map +0 -1
- package/dist/components/p-DBue3ISk.js.map +0 -1
- package/dist/components/p-DDgeXGbB.js.map +0 -1
- package/dist/components/p-DFQ9mcbK.js.map +0 -1
- package/dist/components/p-DNpLDbWY.js.map +0 -1
- package/dist/components/p-DUdOmSFR.js +0 -2
- package/dist/components/p-DtndC9f1.js +0 -2
- package/dist/components/p-H5T258T-.js.map +0 -1
- package/dist/components/p-JPTkZ3Ji.js +0 -2
- package/dist/components/p-LK-jpxwq.js +0 -2
- package/dist/components/p-LK-jpxwq.js.map +0 -1
- package/dist/components/p-ag6Io-kL.js +0 -2
- package/dist/components/p-ag6Io-kL.js.map +0 -1
- package/dist/components/p-iMuFgSoM.js.map +0 -1
- /package/dist/components/{p-uI6z53BE.js.map → p-Bb4KnRy0.js.map} +0 -0
- /package/dist/components/{p-1Hu9jlju.js.map → p-Bf7PZJPT.js.map} +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
3
|
"name": "pagination",
|
|
4
4
|
"title": "Pagination",
|
|
5
|
-
"description": "Controlled fixed or fitted page-size, compact range, and first/
|
|
5
|
+
"description": "Controlled fixed or fitted page-size, compact range, adjacent-page navigation, and optional first/last controls for application-owned data windows.",
|
|
6
6
|
"type": "registry:ui",
|
|
7
7
|
"meta": {
|
|
8
8
|
"library": "@ds-mo/ui",
|
|
@@ -13,14 +13,15 @@
|
|
|
13
13
|
"intent": {
|
|
14
14
|
"audience": "general",
|
|
15
15
|
"status": "stable",
|
|
16
|
-
"summary": "Controlled fixed or fitted page-size, compact range, and first/
|
|
16
|
+
"summary": "Controlled fixed or fitted page-size, compact range, adjacent-page navigation, and optional first/last controls for application-owned data windows.",
|
|
17
17
|
"useWhen": [
|
|
18
18
|
"A result set is divided into discrete pages and the application owns fetching or slicing each page.",
|
|
19
|
-
"A table or list needs a compact page-size selector, result range, and boundary-aware navigation."
|
|
19
|
+
"A table or list needs a compact page-size selector, result range, and boundary-aware navigation.",
|
|
20
|
+
"Direct first-page and last-page navigation is useful enough to opt into alongside the default Previous and Next controls."
|
|
20
21
|
],
|
|
21
22
|
"avoidWhen": [
|
|
22
23
|
"More results append to one continuous list; use the owning component's incremental-loading behavior instead.",
|
|
23
|
-
"
|
|
24
|
+
"The total result count is unknown; use an incremental-loading pattern instead."
|
|
24
25
|
],
|
|
25
26
|
"commonlyComposedWith": [
|
|
26
27
|
"component:ds-table",
|
|
@@ -31,18 +32,20 @@
|
|
|
31
32
|
"The component renders a named navigation region, gives its compact page-size noun a complete accessible label, and gives every icon-only boundary control an explicit page action name.",
|
|
32
33
|
"The visible page-size cluster states the localized noun, selected size, and current-page count of total, for example Rows: [25] 25 of 500. The page position uses compact ‘of’ phrasing, for example 1 of 20. A polite atomic status update retains the complete current range and ‘Page’ phrasing with the localized item noun.",
|
|
33
34
|
"Left and Right Arrow request the adjacent page while focus is within pagination navigation controls. The page-size combobox retains its own arrow-key behavior.",
|
|
34
|
-
"Inactive boundary controls and every control during loading remain non-interactive while retaining their accessible names."
|
|
35
|
+
"Inactive boundary controls and every control during loading remain non-interactive while retaining their accessible names.",
|
|
36
|
+
"showFirstLastButtons adds explicitly named First page and Last page controls; Previous and Next remain present in either mode."
|
|
35
37
|
],
|
|
36
38
|
"states": [
|
|
37
39
|
"pageIndex, numeric effective pageSize, pageSizeMode, totalItems, and pageSizeOptions are controlled inputs. dsChange reports the complete next pagination state, previous index, size, and mode, and whether the request changed the page, page size, or fitted capacity; it never mutates controlled inputs.",
|
|
38
40
|
"Page-size changes request the first page. Out-of-range page indexes and invalid numeric inputs normalize only for presentation; the owner remains authoritative.",
|
|
39
41
|
"Zero results display 0–0 of 0 and Page 1 of 1. A partial final page caps the visible range at totalItems.",
|
|
40
42
|
"loading preserves the full pagination surface, applies aria-busy to the navigation region, and prevents page or page-size requests.",
|
|
41
|
-
"fitToPage appends a Fit to page choice. fitPageSize supplies its effective whole-item capacity; the closed trigger says Fit while the range continues to expose the resulting count. fitToPageInactive keeps that choice visible but unavailable when fitting is not meaningful for the current data shape. Pagination does not measure layouts itself."
|
|
43
|
+
"fitToPage appends a Fit to page choice. fitPageSize supplies its effective whole-item capacity; the closed trigger says Fit while the range continues to expose the resulting count. fitToPageInactive keeps that choice visible but unavailable when fitting is not meaningful for the current data shape. Pagination does not measure layouts itself.",
|
|
44
|
+
"showFirstLastButtons defaults to false. Enable it when direct jumps to both result boundaries materially help the workflow."
|
|
42
45
|
],
|
|
43
46
|
"responsiveBehavior": [
|
|
44
|
-
"The full-width controls remain on one line with noun, selected size, current-page count of total, a vertical divider, page position, and
|
|
45
|
-
"Inside a ds-table container narrower than 900px, pagination hides the visible current-page count and first/last
|
|
47
|
+
"The full-width controls remain on one line with noun, selected size, current-page count of total, a vertical divider, page position, Previous and Next, plus optional First and Last navigation.",
|
|
48
|
+
"Inside a ds-table container narrower than 900px, pagination hides the visible current-page count and any opted-in first/last controls while retaining the page-size noun and selector plus Previous, page position, and Next. The complete current range remains available to assistive technology."
|
|
46
49
|
],
|
|
47
50
|
"frameworkCaveats": {
|
|
48
51
|
"customElements": [
|
|
@@ -187,6 +190,15 @@
|
|
|
187
190
|
"mutable": false,
|
|
188
191
|
"description": "Available page sizes. Assign arrays through JavaScript."
|
|
189
192
|
},
|
|
193
|
+
"showFirstLastButtons": {
|
|
194
|
+
"type": "boolean",
|
|
195
|
+
"resolvedType": "boolean",
|
|
196
|
+
"attribute": "show-first-last-buttons",
|
|
197
|
+
"default": "false",
|
|
198
|
+
"required": false,
|
|
199
|
+
"mutable": false,
|
|
200
|
+
"description": "Include direct first-page and last-page controls."
|
|
201
|
+
},
|
|
190
202
|
"totalItems": {
|
|
191
203
|
"type": "number",
|
|
192
204
|
"resolvedType": "number",
|
|
@@ -334,6 +346,15 @@
|
|
|
334
346
|
"mutable": false,
|
|
335
347
|
"description": "Available page sizes. Assign arrays through JavaScript."
|
|
336
348
|
},
|
|
349
|
+
"showFirstLastButtons": {
|
|
350
|
+
"type": "boolean",
|
|
351
|
+
"resolvedType": "boolean",
|
|
352
|
+
"attribute": "show-first-last-buttons",
|
|
353
|
+
"default": "false",
|
|
354
|
+
"required": false,
|
|
355
|
+
"mutable": false,
|
|
356
|
+
"description": "Include direct first-page and last-page controls."
|
|
357
|
+
},
|
|
337
358
|
"totalItems": {
|
|
338
359
|
"type": "number",
|
|
339
360
|
"resolvedType": "number",
|
|
@@ -409,17 +430,17 @@
|
|
|
409
430
|
},
|
|
410
431
|
{
|
|
411
432
|
"path": "src/wc/components/Pagination/Pagination.tsx",
|
|
412
|
-
"content": "import { Component, Element, Event, EventEmitter, h, Host, Prop, State } from '@stencil/core';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\nimport { resolvePaginationState } from './pagination-model';\nimport type {\n PaginationChangeDetail,\n PaginationPageSizeMode,\n} from './pagination-types';\n\nlet paginationId = 0;\n\n@Component({\n tag: 'ds-pagination',\n styleUrl: 'Pagination.css',\n scoped: true,\n})\nexport class Pagination {\n @Element() el!: HTMLElement;\n\n /** Controlled zero-based page index. */\n @Prop() pageIndex: number = 0;\n /** Controlled number of top-level items on a full page. */\n @Prop() pageSize: number = 25;\n /** Whether pageSize is a fixed choice or a measured Fit snapshot. */\n @Prop() pageSizeMode: PaginationPageSizeMode = 'fixed';\n /** Controlled total number of top-level items across every page. */\n @Prop() totalItems: number = 0;\n /** Available page sizes. Assign arrays through JavaScript. */\n @Prop() pageSizeOptions: number[] = [25, 50, 100, 200];\n /** Include the Fit to page choice. */\n @Prop() fitToPage: boolean = false;\n /** Keep the Fit to page choice visible but unavailable. */\n @Prop() fitToPageInactive: boolean = false;\n /** Effective whole-item capacity to request when Fit is selected. */\n @Prop() fitPageSize: number | undefined;\n /** Full choice-list label for Fit. */\n @Prop() fitPageSizeLabel: string = 'Fit to page';\n /** Compact closed-trigger label while Fit is selected. */\n @Prop() fitPageSizeTriggerLabel: string = 'Fit';\n /** Localized plural noun used by assistive range announcements. */\n @Prop() itemLabel: string = 'items';\n /** Visible page-size control label. */\n @Prop() pageSizeLabel: string = 'Items';\n /** Accessible page-size control label. Defaults to “{pageSizeLabel} per page”. */\n @Prop() pageSizeAriaLabel: string | undefined;\n /** Accessible name for the pagination navigation region. */\n @Prop() label: string = 'Pagination';\n /** Prevent interaction while the owner replaces the current data page. */\n @Prop() loading: boolean = false;\n\n /** Emits the complete next controlled state after a page or page-size request. */\n @Event() dsChange!: EventEmitter<PaginationChangeDetail>;\n\n @State() private tableCompact = false;\n\n private readonly pageSizeControlId = `ds-pagination-${++paginationId}-page-size`;\n private tableCompactDisconnect: (() => void) | undefined;\n private hasLoaded = false;\n\n componentDidLoad(): void {\n this.hasLoaded = true;\n this.syncTableCompactObserver();\n }\n\n connectedCallback(): void {\n if (this.hasLoaded) this.syncTableCompactObserver();\n }\n\n disconnectedCallback(): void {\n this.tableCompactDisconnect?.();\n this.tableCompactDisconnect = undefined;\n }\n\n private syncTableCompactObserver(): void {\n this.tableCompactDisconnect?.();\n this.tableCompactDisconnect = observeTableCaptionCompact(this.el, compact => {\n if (this.tableCompact !== compact) this.tableCompact = compact;\n });\n }\n\n private requestPage(pageIndex: number): void {\n const state = this.resolvedState;\n if (this.loading || pageIndex === state.pageIndex || pageIndex < 0 || pageIndex >= state.totalPages) {\n return;\n }\n this.dsChange.emit({\n pageIndex,\n pageSize: state.pageSize,\n pageSizeMode: state.pageSizeMode,\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n fitToPage: this.fitToPage,\n fitToPageInactive: this.fitToPageInactive,\n fitPageSize: this.resolvedFitPageSize,\n fitPageSizeLabel: this.fitPageSizeLabel,\n fitPageSizeTriggerLabel: this.fitPageSizeTriggerLabel,\n itemLabel: this.itemLabel,\n pageSizeLabel: this.pageSizeLabel,\n ariaLabel: this.label,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n previousPageSizeMode: state.pageSizeMode,\n reason: 'page',\n });\n }\n\n private requestPageSize(value: string | string[]): void {\n if (\n this.loading ||\n typeof value !== 'string' ||\n (value === 'fit' && this.fitToPageInactive)\n ) return;\n const state = this.resolvedState;\n const nextMode: PaginationPageSizeMode = value === 'fit' ? 'fit' : 'fixed';\n const pageSize = nextMode === 'fit' ? this.resolvedFitPageSize : Number(value);\n if (!pageSize || (pageSize === state.pageSize && nextMode === state.pageSizeMode)) return;\n this.dsChange.emit({\n pageIndex: 0,\n pageSize: Math.trunc(pageSize),\n pageSizeMode: nextMode,\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n fitToPage: this.fitToPage,\n fitToPageInactive: this.fitToPageInactive,\n fitPageSize: this.resolvedFitPageSize,\n fitPageSizeLabel: this.fitPageSizeLabel,\n fitPageSizeTriggerLabel: this.fitPageSizeTriggerLabel,\n itemLabel: this.itemLabel,\n pageSizeLabel: this.pageSizeLabel,\n ariaLabel: this.label,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n previousPageSizeMode: state.pageSizeMode,\n reason: 'page-size',\n });\n }\n\n private handleKeyDown(event: KeyboardEvent): void {\n if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {\n return;\n }\n const fromChoiceControl = event.composedPath().some(node =>\n node instanceof HTMLElement &&\n (node.tagName === 'DS-SELECT' || ['INPUT', 'SELECT', 'TEXTAREA'].includes(node.tagName)),\n );\n if (fromChoiceControl || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')) return;\n const state = this.resolvedState;\n const pageIndex = event.key === 'ArrowLeft' ? state.pageIndex - 1 : state.pageIndex + 1;\n if (this.loading || pageIndex < 0 || pageIndex >= state.totalPages) return;\n event.preventDefault();\n this.requestPage(pageIndex);\n }\n\n private get resolvedFitPageSize(): number | undefined {\n return Number.isFinite(this.fitPageSize) && Number(this.fitPageSize) > 0\n ? Math.trunc(Number(this.fitPageSize))\n : undefined;\n }\n\n private get resolvedState() {\n return resolvePaginationState({\n pageIndex: this.pageIndex,\n pageSize: this.pageSize,\n pageSizeMode: this.pageSizeMode,\n totalItems: this.totalItems,\n pageSizeOptions: this.pageSizeOptions,\n });\n }\n\n render() {\n const state = this.resolvedState;\n const atStart = state.pageIndex === 0;\n const atEnd = state.pageIndex === state.totalPages - 1;\n const visibleItems = state.totalItems === 0 ? 0 : state.lastItem - state.firstItem + 1;\n const total = `${visibleItems} of ${state.totalItems}`;\n const page = `${state.pageIndex + 1} of ${state.totalPages}`;\n const announcedRange = `${state.firstItem}–${state.lastItem} of ${state.totalItems}`;\n const announcedPage = `Page ${state.pageIndex + 1} of ${state.totalPages}`;\n const pageSizeAriaLabel = this.pageSizeAriaLabel ?? `${this.pageSizeLabel} per page`;\n const options: Array<{ label: string; value: string; isInactive?: boolean }> =\n state.pageSizeOptions.map(value => ({ label: String(value), value: String(value) }));\n if (this.fitToPage) {\n options.push({\n label: this.fitPageSizeLabel,\n value: 'fit',\n isInactive: this.fitToPageInactive || this.resolvedFitPageSize === undefined,\n });\n }\n\n return (\n <Host>\n <nav\n class={{\n pagination: true,\n 'pagination--table-compact': this.tableCompact,\n }}\n aria-label={this.label}\n aria-busy={this.loading ? 'true' : undefined}\n onKeyDown={event => this.handleKeyDown(event)}\n >\n <div class=\"pagination__page-size\">\n <ds-text\n class=\"pagination__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n {this.pageSizeLabel}:\n </ds-text>\n <span id={`${this.pageSizeControlId}-label`} class=\"ds-visually-hidden\">\n {pageSizeAriaLabel}\n </span>\n <ds-select\n class=\"pagination__page-size-select\"\n size=\"md\"\n inputId={this.pageSizeControlId}\n ariaLabelledby={`${this.pageSizeControlId}-label`}\n options={options}\n value={state.pageSizeMode === 'fit' ? 'fit' : String(state.pageSize)}\n triggerLabel={state.pageSizeMode === 'fit' ? this.fitPageSizeTriggerLabel : undefined}\n indicator=\"up-down\"\n allowClear={false}\n activeFill={false}\n hasBorder={false}\n isInactive={this.loading}\n onDsChange={event => {\n event.stopPropagation();\n this.requestPageSize(event.detail);\n }}\n />\n <ds-text\n class=\"pagination__total\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n fontFeature=\"tabular-nums\"\n >\n {total}\n </ds-text>\n </div>\n <ds-divider\n class=\"pagination__divider\"\n orientation=\"vertical\"\n length=\"var(--dimension-size-250)\"\n />\n <div class=\"pagination__navigation\">\n <ds-button-unfilled\n class=\"pagination__boundary\"\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronLeftDouble\"\n ariaLabel=\"First page\"\n hasBorder={false}\n isInactive={this.loading || atStart}\n onDsClick={() => this.requestPage(0)}\n />\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronLeft\"\n ariaLabel=\"Previous page\"\n hasBorder={false}\n isInactive={this.loading || atStart}\n onDsClick={() => this.requestPage(state.pageIndex - 1)}\n />\n <ds-text\n class=\"pagination__page\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n fontFeature=\"tabular-nums\"\n >\n {page}\n </ds-text>\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronRight\"\n ariaLabel=\"Next page\"\n hasBorder={false}\n isInactive={this.loading || atEnd}\n onDsClick={() => this.requestPage(state.pageIndex + 1)}\n />\n <ds-button-unfilled\n class=\"pagination__boundary\"\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronRightDouble\"\n ariaLabel=\"Last page\"\n hasBorder={false}\n isInactive={this.loading || atEnd}\n onDsClick={() => this.requestPage(state.totalPages - 1)}\n />\n </div>\n <span class=\"ds-visually-hidden\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {announcedRange} {this.itemLabel}. {announcedPage}.\n </span>\n </nav>\n </Host>\n );\n }\n}\n",
|
|
433
|
+
"content": "import { Component, Element, Event, EventEmitter, h, Host, Prop, State } from '@stencil/core';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\nimport { resolvePaginationState } from './pagination-model';\nimport type { PaginationChangeDetail, PaginationPageSizeMode } from './pagination-types';\n\nlet paginationId = 0;\n\n@Component({\n tag: 'ds-pagination',\n styleUrl: 'Pagination.css',\n scoped: true,\n})\nexport class Pagination {\n @Element() el!: HTMLElement;\n\n /** Controlled zero-based page index. */\n @Prop() pageIndex: number = 0;\n /** Controlled number of top-level items on a full page. */\n @Prop() pageSize: number = 25;\n /** Whether pageSize is a fixed choice or a measured Fit snapshot. */\n @Prop() pageSizeMode: PaginationPageSizeMode = 'fixed';\n /** Controlled total number of top-level items across every page. */\n @Prop() totalItems: number = 0;\n /** Available page sizes. Assign arrays through JavaScript. */\n @Prop() pageSizeOptions: number[] = [25, 50, 100, 200];\n /** Include the Fit to page choice. */\n @Prop() fitToPage: boolean = false;\n /** Keep the Fit to page choice visible but unavailable. */\n @Prop() fitToPageInactive: boolean = false;\n /** Effective whole-item capacity to request when Fit is selected. */\n @Prop() fitPageSize: number | undefined;\n /** Full choice-list label for Fit. */\n @Prop() fitPageSizeLabel: string = 'Fit to page';\n /** Compact closed-trigger label while Fit is selected. */\n @Prop() fitPageSizeTriggerLabel: string = 'Fit';\n /** Localized plural noun used by assistive range announcements. */\n @Prop() itemLabel: string = 'items';\n /** Visible page-size control label. */\n @Prop() pageSizeLabel: string = 'Items';\n /** Accessible page-size control label. Defaults to “{pageSizeLabel} per page”. */\n @Prop() pageSizeAriaLabel: string | undefined;\n /** Accessible name for the pagination navigation region. */\n @Prop() label: string = 'Pagination';\n /** Include direct first-page and last-page controls. */\n @Prop() showFirstLastButtons: boolean = false;\n /** Prevent interaction while the owner replaces the current data page. */\n @Prop() loading: boolean = false;\n\n /** Emits the complete next controlled state after a page or page-size request. */\n @Event() dsChange!: EventEmitter<PaginationChangeDetail>;\n\n @State() private tableCompact = false;\n\n private readonly pageSizeControlId = `ds-pagination-${++paginationId}-page-size`;\n private tableCompactDisconnect: (() => void) | undefined;\n private hasLoaded = false;\n\n componentDidLoad(): void {\n this.hasLoaded = true;\n this.syncTableCompactObserver();\n }\n\n connectedCallback(): void {\n if (this.hasLoaded) this.syncTableCompactObserver();\n }\n\n disconnectedCallback(): void {\n this.tableCompactDisconnect?.();\n this.tableCompactDisconnect = undefined;\n }\n\n private syncTableCompactObserver(): void {\n this.tableCompactDisconnect?.();\n this.tableCompactDisconnect = observeTableCaptionCompact(this.el, compact => {\n if (this.tableCompact !== compact) this.tableCompact = compact;\n });\n }\n\n private requestPage(pageIndex: number): void {\n const state = this.resolvedState;\n if (\n this.loading ||\n pageIndex === state.pageIndex ||\n pageIndex < 0 ||\n pageIndex >= state.totalPages\n ) {\n return;\n }\n this.dsChange.emit({\n pageIndex,\n pageSize: state.pageSize,\n pageSizeMode: state.pageSizeMode,\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n fitToPage: this.fitToPage,\n fitToPageInactive: this.fitToPageInactive,\n fitPageSize: this.resolvedFitPageSize,\n fitPageSizeLabel: this.fitPageSizeLabel,\n fitPageSizeTriggerLabel: this.fitPageSizeTriggerLabel,\n itemLabel: this.itemLabel,\n pageSizeLabel: this.pageSizeLabel,\n ariaLabel: this.label,\n showFirstLastButtons: this.showFirstLastButtons,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n previousPageSizeMode: state.pageSizeMode,\n reason: 'page',\n });\n }\n\n private requestPageSize(value: string | string[]): void {\n if (this.loading || typeof value !== 'string' || (value === 'fit' && this.fitToPageInactive))\n return;\n const state = this.resolvedState;\n const nextMode: PaginationPageSizeMode = value === 'fit' ? 'fit' : 'fixed';\n const pageSize = nextMode === 'fit' ? this.resolvedFitPageSize : Number(value);\n if (!pageSize || (pageSize === state.pageSize && nextMode === state.pageSizeMode)) return;\n this.dsChange.emit({\n pageIndex: 0,\n pageSize: Math.trunc(pageSize),\n pageSizeMode: nextMode,\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n fitToPage: this.fitToPage,\n fitToPageInactive: this.fitToPageInactive,\n fitPageSize: this.resolvedFitPageSize,\n fitPageSizeLabel: this.fitPageSizeLabel,\n fitPageSizeTriggerLabel: this.fitPageSizeTriggerLabel,\n itemLabel: this.itemLabel,\n pageSizeLabel: this.pageSizeLabel,\n ariaLabel: this.label,\n showFirstLastButtons: this.showFirstLastButtons,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n previousPageSizeMode: state.pageSizeMode,\n reason: 'page-size',\n });\n }\n\n private handleKeyDown(event: KeyboardEvent): void {\n if (\n event.defaultPrevented ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey ||\n event.shiftKey\n ) {\n return;\n }\n const fromChoiceControl = event\n .composedPath()\n .some(\n node =>\n node instanceof HTMLElement &&\n (node.tagName === 'DS-SELECT' || ['INPUT', 'SELECT', 'TEXTAREA'].includes(node.tagName))\n );\n if (fromChoiceControl || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')) return;\n const state = this.resolvedState;\n const pageIndex = event.key === 'ArrowLeft' ? state.pageIndex - 1 : state.pageIndex + 1;\n if (this.loading || pageIndex < 0 || pageIndex >= state.totalPages) return;\n event.preventDefault();\n this.requestPage(pageIndex);\n }\n\n private get resolvedFitPageSize(): number | undefined {\n return Number.isFinite(this.fitPageSize) && Number(this.fitPageSize) > 0\n ? Math.trunc(Number(this.fitPageSize))\n : undefined;\n }\n\n private get resolvedState() {\n return resolvePaginationState({\n pageIndex: this.pageIndex,\n pageSize: this.pageSize,\n pageSizeMode: this.pageSizeMode,\n totalItems: this.totalItems,\n pageSizeOptions: this.pageSizeOptions,\n });\n }\n\n render() {\n const state = this.resolvedState;\n const atStart = state.pageIndex === 0;\n const atEnd = state.pageIndex === state.totalPages - 1;\n const visibleItems = state.totalItems === 0 ? 0 : state.lastItem - state.firstItem + 1;\n const total = `${visibleItems} of ${state.totalItems}`;\n const page = `${state.pageIndex + 1} of ${state.totalPages}`;\n const announcedRange = `${state.firstItem}–${state.lastItem} of ${state.totalItems}`;\n const announcedPage = `Page ${state.pageIndex + 1} of ${state.totalPages}`;\n const pageSizeAriaLabel = this.pageSizeAriaLabel ?? `${this.pageSizeLabel} per page`;\n const options: Array<{ label: string; value: string; isInactive?: boolean }> =\n state.pageSizeOptions.map(value => ({ label: String(value), value: String(value) }));\n if (this.fitToPage) {\n options.push({\n label: this.fitPageSizeLabel,\n value: 'fit',\n isInactive: this.fitToPageInactive || this.resolvedFitPageSize === undefined,\n });\n }\n\n return (\n <Host>\n <nav\n class={{\n pagination: true,\n 'pagination--table-compact': this.tableCompact,\n }}\n aria-label={this.label}\n aria-busy={this.loading ? 'true' : undefined}\n onKeyDown={event => this.handleKeyDown(event)}\n >\n <div class=\"pagination__page-size\">\n <ds-text\n class=\"pagination__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n {this.pageSizeLabel}:\n </ds-text>\n <span id={`${this.pageSizeControlId}-label`} class=\"ds-visually-hidden\">\n {pageSizeAriaLabel}\n </span>\n <ds-select\n class=\"pagination__page-size-select\"\n size=\"md\"\n inputId={this.pageSizeControlId}\n ariaLabelledby={`${this.pageSizeControlId}-label`}\n options={options}\n value={state.pageSizeMode === 'fit' ? 'fit' : String(state.pageSize)}\n triggerLabel={state.pageSizeMode === 'fit' ? this.fitPageSizeTriggerLabel : undefined}\n indicator=\"up-down\"\n allowClear={false}\n activeFill={false}\n hasBorder={false}\n isInactive={this.loading}\n onDsChange={event => {\n event.stopPropagation();\n this.requestPageSize(event.detail);\n }}\n />\n <ds-text\n class=\"pagination__total\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n fontFeature=\"tabular-nums\"\n >\n {total}\n </ds-text>\n </div>\n <ds-divider\n class=\"pagination__divider\"\n orientation=\"vertical\"\n length=\"var(--dimension-size-250)\"\n />\n <div class=\"pagination__navigation\">\n {this.showFirstLastButtons ? (\n <ds-button-unfilled\n class=\"pagination__boundary\"\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronLeftDouble\"\n ariaLabel=\"First page\"\n hasBorder={false}\n isInactive={this.loading || atStart}\n onDsClick={() => this.requestPage(0)}\n />\n ) : null}\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronLeft\"\n ariaLabel=\"Previous page\"\n hasBorder={false}\n isInactive={this.loading || atStart}\n onDsClick={() => this.requestPage(state.pageIndex - 1)}\n />\n <ds-text\n class=\"pagination__page\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n fontFeature=\"tabular-nums\"\n >\n {page}\n </ds-text>\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronRight\"\n ariaLabel=\"Next page\"\n hasBorder={false}\n isInactive={this.loading || atEnd}\n onDsClick={() => this.requestPage(state.pageIndex + 1)}\n />\n {this.showFirstLastButtons ? (\n <ds-button-unfilled\n class=\"pagination__boundary\"\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronRightDouble\"\n ariaLabel=\"Last page\"\n hasBorder={false}\n isInactive={this.loading || atEnd}\n onDsClick={() => this.requestPage(state.totalPages - 1)}\n />\n ) : null}\n </div>\n <span class=\"ds-visually-hidden\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {announcedRange} {this.itemLabel}. {announcedPage}.\n </span>\n </nav>\n </Host>\n );\n }\n}\n",
|
|
413
434
|
"type": "registry:ui"
|
|
414
435
|
},
|
|
415
436
|
{
|
|
416
437
|
"path": "src/wc/components/Pagination/pagination-model.ts",
|
|
417
|
-
"content": "import type { PaginationPageSizeMode, PaginationState } from './pagination-types';\n\nexport const DEFAULT_PAGE_SIZE_OPTIONS = [25, 50, 100, 200] as const;\n\nexport interface ResolvedPaginationState {\n pageIndex: number;\n pageSize: number;\n pageSizeMode: PaginationPageSizeMode;\n totalItems: number;\n totalPages: number;\n firstItem: number;\n lastItem: number;\n pageSizeOptions: number[];\n}\n\nfunction nonNegativeInteger(value: number): number {\n return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;\n}\n\nfunction positiveInteger(value: number, fallback: number): number {\n return Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallback;\n}\n\nexport function resolvePaginationState(\n state: Pick<\n PaginationState,\n 'pageIndex' | 'pageSize' | 'pageSizeMode' | 'totalItems' | 'pageSizeOptions'\n
|
|
438
|
+
"content": "import type { PaginationPageSizeMode, PaginationState } from './pagination-types';\n\nexport const DEFAULT_PAGE_SIZE_OPTIONS = [25, 50, 100, 200] as const;\n\nexport interface ResolvedPaginationState {\n pageIndex: number;\n pageSize: number;\n pageSizeMode: PaginationPageSizeMode;\n totalItems: number;\n totalPages: number;\n firstItem: number;\n lastItem: number;\n pageSizeOptions: number[];\n}\n\nfunction nonNegativeInteger(value: number): number {\n return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;\n}\n\nfunction positiveInteger(value: number, fallback: number): number {\n return Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallback;\n}\n\nexport function resolvePaginationState(\n state: Pick<\n PaginationState,\n 'pageIndex' | 'pageSize' | 'pageSizeMode' | 'totalItems' | 'pageSizeOptions'\n >\n): ResolvedPaginationState {\n const pageSize = positiveInteger(state.pageSize, DEFAULT_PAGE_SIZE_OPTIONS[0]);\n const pageSizeMode = state.pageSizeMode === 'fit' ? 'fit' : 'fixed';\n const totalItems = nonNegativeInteger(state.totalItems);\n const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));\n const pageIndex = Math.min(nonNegativeInteger(state.pageIndex), totalPages - 1);\n const options = (\n state.pageSizeOptions?.length ? state.pageSizeOptions : DEFAULT_PAGE_SIZE_OPTIONS\n )\n .map(value => positiveInteger(value, 0))\n .filter(value => value > 0);\n const pageSizeOptions = [\n ...new Set(pageSizeMode === 'fixed' ? [...options, pageSize] : options),\n ].sort((left, right) => left - right);\n const firstItem = totalItems === 0 ? 0 : pageIndex * pageSize + 1;\n const lastItem = totalItems === 0 ? 0 : Math.min(totalItems, firstItem + pageSize - 1);\n\n return {\n pageIndex,\n pageSize,\n pageSizeMode,\n totalItems,\n totalPages,\n firstItem,\n lastItem,\n pageSizeOptions,\n };\n}\n",
|
|
418
439
|
"type": "registry:ui"
|
|
419
440
|
},
|
|
420
441
|
{
|
|
421
442
|
"path": "src/wc/components/Pagination/pagination-types.ts",
|
|
422
|
-
"content": "export type PaginationChangeReason = 'page' | 'page-size' | 'fit';\nexport type PaginationPageSizeMode = 'fixed' | 'fit';\n\n/** Controlled pagination state shared by Pagination and pagination-aware consumers. */\nexport interface PaginationState {\n /** Zero-based page index. */\n pageIndex: number;\n /** Number of top-level items on a full page. */\n pageSize: number;\n /** Whether pageSize is a chosen fixed value or a measured Fit snapshot. */\n pageSizeMode?: PaginationPageSizeMode;\n /** Total number of top-level items across every page. */\n totalItems: number;\n /** Available page sizes. Defaults to 25, 50, 100, and 200. */\n pageSizeOptions?: number[];\n /** Include a Fit to page option. The owner supplies the effective fitted size. */\n fitToPage?: boolean;\n /** Keep the Fit to page option visible but unavailable for the current data shape. */\n fitToPageInactive?: boolean;\n /** Effective page size proposed when Fit to page is selected. */\n fitPageSize?: number;\n /** Full option label for Fit in the open choice list. */\n fitPageSizeLabel?: string;\n /** Compact trigger label while Fit is selected. */\n fitPageSizeTriggerLabel?: string;\n /** Localized plural noun used by assistive range announcements. */\n itemLabel?: string;\n /** Visible label for the page-size selector. */\n pageSizeLabel?: string;\n /** Accessible name for the pagination navigation region. */\n ariaLabel?: string;\n}\n\n/** Complete next controlled state emitted for a page or page-size request. */\nexport interface PaginationChangeDetail extends PaginationState {\n previousPageIndex: number;\n previousPageSize: number;\n previousPageSizeMode: PaginationPageSizeMode;\n reason: PaginationChangeReason;\n}\n",
|
|
443
|
+
"content": "export type PaginationChangeReason = 'page' | 'page-size' | 'fit';\nexport type PaginationPageSizeMode = 'fixed' | 'fit';\n\n/** Controlled pagination state shared by Pagination and pagination-aware consumers. */\nexport interface PaginationState {\n /** Zero-based page index. */\n pageIndex: number;\n /** Number of top-level items on a full page. */\n pageSize: number;\n /** Whether pageSize is a chosen fixed value or a measured Fit snapshot. */\n pageSizeMode?: PaginationPageSizeMode;\n /** Total number of top-level items across every page. */\n totalItems: number;\n /** Available page sizes. Defaults to 25, 50, 100, and 200. */\n pageSizeOptions?: number[];\n /** Include a Fit to page option. The owner supplies the effective fitted size. */\n fitToPage?: boolean;\n /** Keep the Fit to page option visible but unavailable for the current data shape. */\n fitToPageInactive?: boolean;\n /** Effective page size proposed when Fit to page is selected. */\n fitPageSize?: number;\n /** Full option label for Fit in the open choice list. */\n fitPageSizeLabel?: string;\n /** Compact trigger label while Fit is selected. */\n fitPageSizeTriggerLabel?: string;\n /** Localized plural noun used by assistive range announcements. */\n itemLabel?: string;\n /** Visible label for the page-size selector. */\n pageSizeLabel?: string;\n /** Accessible name for the pagination navigation region. */\n ariaLabel?: string;\n /** Include direct first-page and last-page controls around Previous and Next. */\n showFirstLastButtons?: boolean;\n}\n\n/** Complete next controlled state emitted for a page or page-size request. */\nexport interface PaginationChangeDetail extends PaginationState {\n previousPageIndex: number;\n previousPageSize: number;\n previousPageSizeMode: PaginationPageSizeMode;\n reason: PaginationChangeReason;\n}\n",
|
|
423
444
|
"type": "registry:ui"
|
|
424
445
|
}
|
|
425
446
|
]
|
|
@@ -625,7 +625,7 @@
|
|
|
625
625
|
"files": [
|
|
626
626
|
{
|
|
627
627
|
"path": "src/wc/components/PanelNav/PanelNav.css",
|
|
628
|
-
"content": "@import '../../utils/control-density.css';\n@import '../../utils/chrome-layout.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/scrollbar-hidden.css';\n\n/* ── Theme variables ──────────────────────────────────────────────────────── */\n\n.panel-nav {\n /* ─ Sizes ─ */\n --_nav-width: var(--dimension-panel-width-2xs);\n --_nav-width-collapsed: var(--dimension-size-600);\n --_nav-item-height: var(--dimension-size-400, 32px);\n --_nav-padding-x: var(--dimension-space-100, 8px);\n --_nav-padding-y: var(--dimension-space-100, 8px);\n /* gear button outer width = padding*2 + icon */\n --_nav-footer-btn-w: calc(var(--dimension-space-075, 6px) * 2 + var(--dimension-iconography-md, 20px));\n\n /* ─ Motion ─ */\n /* --ds-panel-nav-speed: unitless multiplier; override in Storybook to slow-mo (e.g. 10 = 10×).\n *-dur vars are duration-only (needed for calc); *-motion vars add easing for use in transitions. */\n --_nav-speed: var(--ds-panel-nav-speed, 1);\n --_nav-expand-dur: calc(var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms));\n --_nav-expand-motion: var(--_nav-expand-dur) var(--effect-animation-easing-ease-in-out, ease);\n --_nav-label-fade-dur: calc(var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms));\n --_nav-label-fade-motion: var(--_nav-label-fade-dur) var(--effect-animation-easing-ease-in-out, ease);\n --_nav-label-fade-out-dur: calc(var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms));\n --_nav-label-fade-out-motion: var(--_nav-label-fade-out-dur) var(--effect-animation-easing-ease-in-out, ease);\n\n /* ─ Colours – default chrome (app surfaces) ─ */\n --_nav-bg: var(--color-background-secondary);\n --_nav-border: var(--color-border-tertiary);\n --_nav-divider: var(--color-border-tertiary);\n --_nav-fg-primary: var(--color-foreground-primary);\n --_nav-fg-secondary: var(--color-foreground-secondary);\n --_nav-fg-tertiary: var(--color-foreground-tertiary);\n --_nav-hover: var(--color-interaction-hover);\n --_nav-pressed: var(--color-interaction-pressed);\n --_nav-focus: var(--color-interaction-focus);\n --_nav-dot: var(--color-foreground-bold-brand);\n --ds-interaction-hover: var(--_nav-hover);\n --ds-interaction-pressed: var(--_nav-pressed);\n --ds-focus-ring-color: var(--_nav-focus);\n}\n\n/* ── Default chrome overrides ────────────────────────────────────────────── */\n\n/* Shell-owned chrome (bg + wash) — panel is a transparent frame. */\n:host-context(ds-shell-app:not([gradient-preset='none'])) .panel-nav,\n:host-context(ds-shell-app.shell-app--paper-texture) .panel-nav {\n background-color: transparent;\n}\n\n/* Style slots — same app tokens for now; hook texture/glyph layers on these classes later. */\n.panel-nav--dashboard,\n.panel-nav--settings {\n --_nav-bg: var(--color-background-secondary);\n --_nav-border: var(--color-border-tertiary);\n --_nav-divider: var(--color-border-tertiary);\n --_nav-fg-primary: var(--color-foreground-primary);\n --_nav-fg-secondary: var(--color-foreground-secondary);\n --_nav-fg-tertiary: var(--color-foreground-tertiary);\n --_nav-hover: var(--color-interaction-hover);\n --_nav-pressed: var(--color-interaction-pressed);\n --_nav-focus: var(--color-interaction-focus);\n --_nav-dot: var(--color-foreground-bold-brand);\n --ds-interaction-hover: var(--_nav-hover);\n --ds-interaction-pressed: var(--_nav-pressed);\n --ds-focus-ring-color: var(--_nav-focus);\n}\n\n/* ── Shell ───────────────────────────────────────────────────────────────── */\n\n.panel-nav {\n display: flex;\n flex-direction: column;\n position: relative;\n width: var(--_nav-width);\n min-width: var(--_nav-width);\n height: 100%;\n background-color: var(--_nav-bg);\n border-right: var(--dimension-stroke-width-012, 1px) solid var(--_nav-border);\n box-sizing: border-box;\n overflow: visible;\n transition:\n width var(--_nav-expand-motion),\n min-width var(--_nav-expand-motion),\n background-color var(--effect-motion-short-3, 200ms ease),\n border-color var(--effect-motion-short-3, 200ms ease);\n font-family: var(--typography-font-family-ui, system-ui);\n isolation: isolate;\n}\n\n.panel-nav__header,\n.panel-nav__body,\n.panel-nav__footer {\n position: relative;\n z-index: 3;\n}\n\n.panel-nav--collapsed {\n width: var(--_nav-width-collapsed);\n min-width: var(--_nav-width-collapsed);\n}\n\n/* ── Header ──────────────────────────────────────────────────────────────── */\n\n.panel-nav__header {\n height: var(--dimension-size-600, 48px);\n flex-shrink: 0;\n border-bottom: var(--dimension-stroke-width-012, 1px) solid var(--_nav-divider);\n transition: border-color var(--effect-motion-short-3, 200ms ease);\n}\n\n.panel-nav__header-btn {\n all: unset;\n /* Re-assert after unset — required for `.ds-interaction-fill` stacking. */\n position: relative;\n z-index: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--dimension-space-075, 6px);\n border-radius: var(--ds-control-radius, var(--dimension-radius-025, 2px));\n cursor: default;\n color: var(--_nav-fg-primary);\n flex-shrink: 0;\n box-sizing: border-box;\n transition: color var(--effect-motion-short-3, 200ms ease);\n /* Hover/press: `.ds-interaction-fill` (tokens from --_nav-* via --ds-interaction-*). */\n}\n\n.panel-nav--breakpoint-locked .panel-nav__header-btn {\n cursor: default;\n}\n\n/* Logo — visible by default; instant swap to toggle on hover (no crossfade overlap) */\n.panel-nav__header-logo {\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 2;\n opacity: 1;\n}\n\n.panel-nav__m-mark {\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n display: block;\n}\n\n/* Toggle icon — stacked on logo; under hover wash (::after z-index 3) */\n.panel-nav__header-toggle {\n display: flex;\n align-items: center;\n justify-content: center;\n position: absolute;\n inset: 0;\n z-index: 2;\n opacity: 0;\n pointer-events: none;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .panel-nav:not(.panel-nav--breakpoint-locked)\n .panel-nav__header-btn:hover\n .panel-nav__header-logo {\n opacity: 0;\n }\n\n .panel-nav:not(.panel-nav--breakpoint-locked)\n .panel-nav__header-btn:hover\n .panel-nav__header-toggle {\n opacity: 1;\n }\n}\n\n/* ── Scrollable body ─────────────────────────────────────────────────────── */\n\n.panel-nav__body {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n}\n\n.panel-nav__sections {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-200, 16px);\n width: 100%;\n}\n\n/* ── Group ───────────────────────────────────────────────────────────────── */\n\n.panel-nav__group {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050, 4px);\n}\n\n/*\n * Section headers — typography via ds-text (text-caption + emphasis).\n * Element selector so display:flex beats Text's inline-block, and padding\n * isn't clobbered by sharing `.panel-nav__item-label-text` on the same host.\n */\nds-text.panel-nav__group-label {\n display: flex;\n align-items: center;\n box-sizing: border-box;\n text-transform: uppercase;\n color: var(--_nav-fg-secondary);\n user-select: none;\n height: var(--dimension-size-300, 24px);\n /* Former nested pad: group-label (075) + item-label-text (025). */\n padding: 0 calc(var(--dimension-space-075, 6px) + var(--ds-control-label-inset));\n white-space: nowrap;\n overflow: hidden;\n max-height: var(--dimension-size-300, 24px);\n opacity: 1;\n transition:\n opacity var(--_nav-label-fade-motion) 0ms,\n max-height var(--_nav-label-fade-motion) 0ms,\n padding var(--_nav-label-fade-motion) 0ms,\n color var(--effect-motion-short-3, 200ms ease);\n}\n\n/* ── Item ────────────────────────────────────────────────────────────────── */\n\n.panel-nav__item {\n all: unset;\n /* Re-assert after unset — required for `.ds-interaction-fill` stacking. */\n position: relative;\n z-index: 0;\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050, 4px);\n padding: 0 var(--dimension-space-075, 6px);\n height: var(--dimension-size-400, 32px);\n border-radius: var(--ds-control-radius, var(--dimension-radius-025, 2px));\n cursor: default;\n color: var(--_nav-fg-secondary);\n box-sizing: border-box;\n width: 100%;\n user-select: none;\n transition: color var(--effect-motion-short-1, 50ms ease-in-out);\n /* Hover/press: `.ds-interaction-fill` — no --selected (icon/type weight only). */\n}\n\n/* ── Active item ─────────────────────────────────────────────────────────── */\n\n.panel-nav__item--active {\n color: var(--_nav-fg-primary);\n /* Bold typography via ds-text (text-body-medium + emphasis) on the label */\n}\n\n/* ── Item icon ───────────────────────────────────────────────────────────── */\n\n.panel-nav__item-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n position: relative;\n z-index: 2;\n}\n\n/* ── Item label ──────────────────────────────────────────────────────────── */\n\n.panel-nav__item-label {\n position: relative;\n z-index: 2;\n flex: 1;\n min-width: 0;\n height: var(--typography-lineheight-md, 20px);\n opacity: 1;\n /* Gradient wipe: mask sweeps left-to-right on expand, right-to-left on collapse */\n /* stylelint-disable-next-line color-named -- mask luminance stops, not theme colors */\n mask-image: linear-gradient(to right, black 50%, transparent 100%);\n /* stylelint-disable-next-line color-named -- mask luminance stops, not theme colors */\n -webkit-mask-image: linear-gradient(to right, black 50%, transparent 100%);\n mask-size: 200% 100%;\n -webkit-mask-size: 200% 100%;\n mask-position: 0% 0;\n -webkit-mask-position: 0% 0;\n transition:\n opacity var(--_nav-label-fade-motion) 0ms,\n mask-position var(--_nav-label-fade-motion) 0ms,\n -webkit-mask-position var(--_nav-label-fade-motion) 0ms;\n}\n\n.panel-nav__item-label-text {\n padding: 0 var(--ds-control-label-inset);\n}\n\n/* ── Item notification dot — 20px suffix zone matches the icon geometry ──── */\n\n.panel-nav__item-dot-box {\n position: absolute;\n z-index: 2;\n right: var(--dimension-space-075, 6px);\n top: var(--dimension-space-075, 6px);\n display: flex;\n align-items: center;\n justify-content: center;\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n pointer-events: none;\n}\n\n.panel-nav__item-dot {\n transition:\n transform var(--_nav-expand-motion);\n}\n\n.panel-nav--collapsed .panel-nav__item-dot {\n transform: translate(\n calc(var(--dimension-space-075, 6px) + var(--dimension-stroke-width-012, 1px)),\n calc(0px - var(--dimension-space-075, 6px) - var(--dimension-stroke-width-012, 1px))\n );\n}\n\n.panel-nav--animating .panel-nav__item-label {\n text-overflow: clip;\n}\n\n.panel-nav--collapsed .panel-nav__item-label {\n opacity: 0;\n pointer-events: none;\n text-overflow: clip;\n mask-position: 100% 0;\n -webkit-mask-position: 100% 0;\n transition:\n opacity var(--_nav-label-fade-out-motion) 0ms,\n mask-position var(--_nav-label-fade-out-motion) 0ms,\n -webkit-mask-position var(--_nav-label-fade-out-motion) 0ms;\n}\n\n.panel-nav--collapsed ds-text.panel-nav__group-label {\n opacity: 0;\n max-height: 0;\n padding: 0 calc(var(--dimension-space-075, 6px) + var(--ds-control-label-inset));\n pointer-events: none;\n transition:\n opacity var(--_nav-label-fade-out-motion) 0ms,\n max-height var(--_nav-label-fade-out-motion) 0ms,\n padding var(--_nav-label-fade-out-motion) 0ms,\n color var(--effect-motion-short-3, 200ms ease);\n}\n\n/* ── Footer ──────────────────────────────────────────────────────────────── */\n\n.panel-nav__footer {\n position: relative;\n flex-shrink: 0;\n /* single row: padding*2 + item */\n height: calc(var(--_nav-padding-y) * 2 + var(--_nav-item-height));\n transition: height var(--_nav-expand-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer {\n /* two rows: padding*2 + item*2 + gap */\n height: calc(var(--_nav-padding-y) * 2 + var(--_nav-item-height) * 2 + var(--dimension-space-050, 4px));\n}\n\n/* ── Footer icon button (gear / dashboard) ───────────────────────────────── */\n\n.panel-nav__footer-btn {\n position: absolute;\n left: var(--_nav-padding-x);\n top: var(--_nav-padding-y);\n --ds-button-unfilled-fg: var(--_nav-fg-secondary);\n --ds-button-unfilled-fg-active: var(--_nav-fg-primary);\n --ds-interaction-hover: var(--_nav-hover);\n --ds-interaction-pressed: var(--_nav-pressed);\n --ds-focus-ring-color: var(--_nav-focus);\n}\n\n/* ── Footer user — absolutely positioned, slides from row to below gear ───── */\n\n.panel-nav__footer-user {\n position: absolute;\n /* Expanded row: balanced shell inset with an 8px gear-to-user gap. */\n left: calc(var(--_nav-padding-x) + var(--_nav-footer-btn-w) + var(--dimension-space-100, 8px));\n right: var(--_nav-padding-x);\n top: var(--_nav-padding-y);\n width: auto;\n min-width: 0;\n transition:\n left var(--_nav-expand-motion),\n right var(--_nav-expand-motion),\n top var(--_nav-expand-motion);\n}\n\n/* Popup-open state keeps the native footer trigger visibly pressed. */\n.panel-nav__footer-user--menu-expanded::after {\n background: var(--_nav-pressed);\n}\n\n.panel-nav__footer-user--menu-expanded {\n --ds-interaction-hover: var(--_nav-pressed);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user {\n /* Resolve one shared square control while keeping both inline edges animatable. */\n left: var(--_nav-padding-x);\n right: calc(100% - var(--_nav-padding-x) - var(--_nav-item-height));\n top: calc(var(--_nav-padding-y) + var(--_nav-item-height) + var(--dimension-space-050, 4px));\n}\n\n/* Label: absolute so it stays in place and only fades — no mask slide */\n.panel-nav__footer-user-label {\n text-align: right;\n position: absolute;\n left: var(--dimension-space-075, 6px);\n right: calc(var(--dimension-space-075, 6px) + var(--dimension-iconography-md, 20px) + var(--dimension-space-050, 4px));\n top: 50%;\n transform: translateY(-50%);\n flex: none;\n height: var(--typography-lineheight-md, 20px);\n mask-image: none;\n -webkit-mask-image: none;\n transition: opacity var(--_nav-label-fade-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user-label {\n opacity: 0;\n pointer-events: none;\n mask-position: 0% 0;\n -webkit-mask-position: 0% 0;\n}\n\n/* ── Footer user icon — cross-fades chevron ↔ circle+initial ────────────── */\n\n/* Absolute — right-anchored in expanded, left-anchored in collapsed to match regular item icon position */\n.panel-nav__footer-user-icon {\n position: absolute;\n left: calc(100% - var(--dimension-iconography-md, 20px) - var(--dimension-space-075, 6px));\n top: 0;\n bottom: 0;\n height: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: left var(--_nav-expand-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user-icon {\n left: var(--dimension-space-075, 6px);\n}\n\n.panel-nav__footer-icon-expanded,\n.panel-nav__footer-icon-collapsed {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.panel-nav__footer-icon-expanded {\n opacity: 1;\n transition: opacity var(--_nav-label-fade-motion);\n}\n\n.panel-nav__footer-icon-collapsed {\n opacity: 0;\n transition: opacity var(--_nav-label-fade-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-icon-expanded {\n opacity: 0;\n transition: opacity var(--_nav-label-fade-out-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-icon-collapsed {\n opacity: 1;\n transition: opacity var(--_nav-label-fade-out-motion);\n}\n\n/* ── User initial — overlaid on the circle icon ─────────────────────────── */\n\n/*\n * Center the caption glyph on the Circle icon. Don't use inset:0 + flex — the\n * inner `.ds-text__element { width: 100% }` stretches and left-aligns the letter.\n * Translate-centering keeps the complete caption metric box on the icon center;\n * the caption variant remains the sole owner of its line-height.\n * (Do not set `inset: auto` after top/left — it resets them.)\n */\nds-text.panel-nav__user-initial,\nds-text.panel-nav__user-initial.ds-text--caption {\n position: absolute;\n top: 50%;\n left: 50%;\n right: auto;\n bottom: auto;\n transform: translate(-50%, -50%);\n display: block;\n width: max-content;\n max-width: none;\n min-width: 0;\n text-align: center;\n color: inherit;\n pointer-events: none;\n}\n\n/* ── Drag-to-resize handle ───────────────────────────────────────────────── */\n\n/* The resize overlay line — extends past the right edge; keep above workspace content */\n.panel-nav__resize-handle {\n position: absolute;\n top: 0;\n /* Centered on the right edge: --dimension-space-050 outside + inside = --dimension-space-100 hit zone */\n right: var(--dimension-space-n050, -4px);\n bottom: 0;\n width: var(--dimension-space-100, 8px);\n z-index: 20;\n cursor: ew-resize;\n touch-action: none;\n /* Needs a real background for Safari hit-testing */\n background: transparent;\n}\n\n/* Safari requires non-transparent fill for hit testing */\n.panel-nav__resize-handle::before {\n content: '';\n position: absolute;\n inset: 0;\n /* stylelint-disable-next-line color-named -- near-invisible hit-test fill; not a theme color */\n background: rgba(0, 0, 0, 0.001);\n}\n\n/* Resize overlay line */\n.panel-nav__resize-handle::after {\n content: '';\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n width: var(--dimension-stroke-width-025, 2px);\n background: var(--color-foreground-primary);\n opacity: 0;\n pointer-events: none;\n transition: opacity var(--effect-motion-short-2, 100ms ease-in-out);\n}\n\n.panel-nav__resize-handle--overlay::after {\n opacity: 1;\n}\n\n/* ── Reduced motion ──────────────────────────────────────────────────────── */\n\n@media (prefers-reduced-motion: reduce) {\n .panel-nav,\n .panel-nav *,\n .panel-nav *::before,\n .panel-nav *::after,\n .panel-nav__resize-handle,\n .panel-nav__resize-handle::before,\n .panel-nav__resize-handle::after {\n /* Near-instant — browsers treat 0 as \"no transition\" and can skip the end state. */\n transition-duration: var(--effect-animation-duration-instant) !important;\n animation-duration: var(--effect-animation-duration-instant) !important;\n transition-delay: var(--effect-animation-duration-instant) !important;\n }\n}\n",
|
|
628
|
+
"content": "@import '../../utils/control-density.css';\n@import '../../utils/chrome-layout.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/scrollbar-hidden.css';\n\n/* ── Theme variables ──────────────────────────────────────────────────────── */\n\n.panel-nav {\n /* ─ Sizes ─ */\n --_nav-width: var(--dimension-panel-width-2xs);\n --_nav-width-collapsed: var(--dimension-size-600);\n --_nav-item-height: var(--dimension-size-400, 32px);\n --_nav-padding-x: var(--dimension-space-100, 8px);\n --_nav-padding-y: var(--dimension-space-100, 8px);\n /* gear button outer width = padding*2 + icon */\n --_nav-footer-btn-w: calc(\n var(--dimension-space-075, 6px) * 2 + var(--dimension-iconography-md, 20px)\n );\n\n /* ─ Motion ─ */\n /* --ds-panel-nav-speed: unitless multiplier; override in Storybook to slow-mo (e.g. 10 = 10×).\n *-dur vars are duration-only (needed for calc); *-motion vars add easing for use in transitions. */\n --_nav-speed: var(--ds-panel-nav-speed, 1);\n --_nav-expand-dur: calc(var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms));\n --_nav-expand-motion: var(--_nav-expand-dur) var(--effect-animation-easing-ease-in-out, ease);\n --_nav-label-fade-dur: calc(var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms));\n --_nav-label-fade-motion: var(--_nav-label-fade-dur)\n var(--effect-animation-easing-ease-in-out, ease);\n --_nav-label-fade-out-dur: calc(\n var(--_nav-speed) * var(--effect-animation-duration-medium-1, 300ms)\n );\n --_nav-label-fade-out-motion: var(--_nav-label-fade-out-dur)\n var(--effect-animation-easing-ease-in-out, ease);\n\n /* ─ Colours – default chrome (app surfaces) ─ */\n --_nav-bg: var(--color-background-secondary);\n --_nav-border: var(--color-border-tertiary);\n --_nav-divider: var(--color-border-tertiary);\n --_nav-fg-primary: var(--color-foreground-primary);\n --_nav-fg-secondary: var(--color-foreground-secondary);\n --_nav-fg-tertiary: var(--color-foreground-tertiary);\n --_nav-hover: var(--color-interaction-hover);\n --_nav-pressed: var(--color-interaction-pressed);\n --_nav-focus: var(--color-interaction-focus);\n --_nav-dot: var(--color-foreground-bold-brand);\n --ds-interaction-hover: var(--_nav-hover);\n --ds-interaction-pressed: var(--_nav-pressed);\n --ds-focus-ring-color: var(--_nav-focus);\n}\n\n/* ── Default chrome overrides ────────────────────────────────────────────── */\n\n/* Shell-owned chrome (bg + wash) — panel is a transparent frame. */\n:host-context(ds-shell-app:not([gradient-preset='none'])) .panel-nav,\n:host-context(ds-shell-app.shell-app--paper-texture) .panel-nav {\n background-color: transparent;\n}\n\n/* Style slots — same app tokens for now; hook texture/glyph layers on these classes later. */\n.panel-nav--dashboard,\n.panel-nav--settings {\n --_nav-bg: var(--color-background-secondary);\n --_nav-border: var(--color-border-tertiary);\n --_nav-divider: var(--color-border-tertiary);\n --_nav-fg-primary: var(--color-foreground-primary);\n --_nav-fg-secondary: var(--color-foreground-secondary);\n --_nav-fg-tertiary: var(--color-foreground-tertiary);\n --_nav-hover: var(--color-interaction-hover);\n --_nav-pressed: var(--color-interaction-pressed);\n --_nav-focus: var(--color-interaction-focus);\n --_nav-dot: var(--color-foreground-bold-brand);\n --ds-interaction-hover: var(--_nav-hover);\n --ds-interaction-pressed: var(--_nav-pressed);\n --ds-focus-ring-color: var(--_nav-focus);\n}\n\n/* ── Shell ───────────────────────────────────────────────────────────────── */\n\n.panel-nav {\n display: flex;\n flex-direction: column;\n position: relative;\n width: var(--_nav-width);\n min-width: var(--_nav-width);\n height: 100%;\n background-color: var(--_nav-bg);\n border-right: var(--dimension-stroke-width-012, 1px) solid var(--_nav-border);\n box-sizing: border-box;\n overflow: visible;\n transition:\n width var(--_nav-expand-motion),\n min-width var(--_nav-expand-motion),\n background-color var(--effect-motion-short-3, 200ms ease),\n border-color var(--effect-motion-short-3, 200ms ease);\n font-family: var(--typography-font-family-ui, system-ui);\n isolation: isolate;\n}\n\n.panel-nav__header,\n.panel-nav__body,\n.panel-nav__footer {\n position: relative;\n z-index: 3;\n}\n\n.panel-nav--collapsed {\n width: var(--_nav-width-collapsed);\n min-width: var(--_nav-width-collapsed);\n}\n\n/* ── Header ──────────────────────────────────────────────────────────────── */\n\n.panel-nav__header {\n height: var(--dimension-size-600, 48px);\n flex-shrink: 0;\n border-bottom: var(--dimension-stroke-width-012, 1px) solid var(--_nav-divider);\n transition: border-color var(--effect-motion-short-3, 200ms ease);\n}\n\n.panel-nav__header-btn {\n all: unset;\n /* Re-assert after unset — required for `.ds-interaction-fill` stacking. */\n position: relative;\n z-index: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--dimension-space-075, 6px);\n border-radius: var(--ds-control-radius, var(--dimension-radius-025, 2px));\n cursor: default;\n color: var(--_nav-fg-primary);\n flex-shrink: 0;\n box-sizing: border-box;\n transition: color var(--effect-motion-short-3, 200ms ease);\n /* Hover/press: `.ds-interaction-fill` (tokens from --_nav-* via --ds-interaction-*). */\n}\n\n.panel-nav--breakpoint-locked .panel-nav__header-btn {\n cursor: default;\n}\n\n/* Logo — visible by default; instant swap to toggle on hover (no crossfade overlap) */\n.panel-nav__header-logo {\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 2;\n opacity: 1;\n}\n\n.panel-nav__m-mark {\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n display: block;\n}\n\n/* Toggle icon — stacked on logo; under hover wash (::after z-index 3) */\n.panel-nav__header-toggle {\n display: flex;\n align-items: center;\n justify-content: center;\n position: absolute;\n inset: 0;\n z-index: 2;\n opacity: 0;\n pointer-events: none;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .panel-nav:not(.panel-nav--breakpoint-locked)\n .panel-nav__header-btn:hover\n .panel-nav__header-logo {\n opacity: 0;\n }\n\n .panel-nav:not(.panel-nav--breakpoint-locked)\n .panel-nav__header-btn:hover\n .panel-nav__header-toggle {\n opacity: 1;\n }\n}\n\n/* ── Scrollable body ─────────────────────────────────────────────────────── */\n\n.panel-nav__body {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n}\n\n.panel-nav__sections {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-200, 16px);\n width: 100%;\n}\n\n/* ── Group ───────────────────────────────────────────────────────────────── */\n\n.panel-nav__group {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050, 4px);\n}\n\n/*\n * Section headers — typography via ds-text (text-caption + emphasis).\n * Element selector so display:flex beats Text's inline-block, and padding\n * isn't clobbered by sharing `.panel-nav__item-label-text` on the same host.\n */\nds-text.panel-nav__group-label {\n display: flex;\n align-items: center;\n box-sizing: border-box;\n text-transform: uppercase;\n color: var(--_nav-fg-secondary);\n user-select: none;\n height: var(--dimension-size-300, 24px);\n /* Former nested pad: group-label (075) + item-label-text (025). */\n padding: 0 calc(var(--dimension-space-075, 6px) + var(--ds-control-label-inset));\n white-space: nowrap;\n overflow: hidden;\n max-height: var(--dimension-size-300, 24px);\n opacity: 1;\n transition:\n opacity var(--_nav-label-fade-motion) 0ms,\n max-height var(--_nav-label-fade-motion) 0ms,\n padding var(--_nav-label-fade-motion) 0ms,\n color var(--effect-motion-short-3, 200ms ease);\n}\n\n/* ── Item ────────────────────────────────────────────────────────────────── */\n\n.panel-nav__item {\n all: unset;\n /* Re-assert after unset — required for `.ds-interaction-fill` stacking. */\n position: relative;\n z-index: 0;\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050, 4px);\n padding: 0 var(--dimension-space-075, 6px);\n height: var(--dimension-size-400, 32px);\n border-radius: var(--ds-control-radius, var(--dimension-radius-025, 2px));\n cursor: default;\n color: var(--_nav-fg-secondary);\n box-sizing: border-box;\n width: 100%;\n user-select: none;\n transition: color var(--effect-motion-short-1, 50ms ease-in-out);\n /* Hover/press: `.ds-interaction-fill` — no --selected (icon/type weight only). */\n}\n\n/* ── Active item ─────────────────────────────────────────────────────────── */\n\n.panel-nav__item--active {\n color: var(--_nav-fg-primary);\n /* Bold typography via ds-text (text-body-medium + emphasis) on the label */\n}\n\n/* ── Item icon ───────────────────────────────────────────────────────────── */\n\n.panel-nav__item-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n position: relative;\n z-index: 2;\n}\n\n/* ── Item label ──────────────────────────────────────────────────────────── */\n\n.panel-nav__item-label {\n position: relative;\n z-index: 2;\n flex: 1;\n min-width: 0;\n height: var(--typography-lineheight-md, 20px);\n opacity: 1;\n /* Gradient wipe: mask sweeps left-to-right on expand, right-to-left on collapse */\n /* stylelint-disable-next-line color-named -- mask luminance stops, not theme colors */\n mask-image: linear-gradient(to right, black 50%, transparent 100%);\n /* stylelint-disable-next-line color-named -- mask luminance stops, not theme colors */\n -webkit-mask-image: linear-gradient(to right, black 50%, transparent 100%);\n mask-size: 200% 100%;\n -webkit-mask-size: 200% 100%;\n mask-position: 0% 0;\n -webkit-mask-position: 0% 0;\n transition:\n opacity var(--_nav-label-fade-motion) 0ms,\n mask-position var(--_nav-label-fade-motion) 0ms,\n -webkit-mask-position var(--_nav-label-fade-motion) 0ms;\n}\n\n.panel-nav__item-label-text {\n padding: 0 var(--ds-control-label-inset);\n}\n\n/* ── Item notification dot — 20px suffix zone matches the icon geometry ──── */\n\n.panel-nav__item-dot-box {\n position: absolute;\n z-index: 2;\n right: var(--dimension-space-075, 6px);\n top: var(--dimension-space-075, 6px);\n display: flex;\n align-items: center;\n justify-content: center;\n width: var(--dimension-iconography-md, 20px);\n height: var(--dimension-iconography-md, 20px);\n pointer-events: none;\n}\n\n.panel-nav__item-dot {\n transition: transform var(--_nav-expand-motion);\n}\n\n.panel-nav--collapsed .panel-nav__item-dot {\n transform: translate(\n calc(var(--dimension-space-075, 6px) + var(--dimension-stroke-width-012, 1px)),\n calc(0px - var(--dimension-space-075, 6px) - var(--dimension-stroke-width-012, 1px))\n );\n}\n\n.panel-nav--animating .panel-nav__item-label {\n text-overflow: clip;\n}\n\n.panel-nav--collapsed .panel-nav__item-label {\n opacity: 0;\n pointer-events: none;\n text-overflow: clip;\n mask-position: 100% 0;\n -webkit-mask-position: 100% 0;\n transition:\n opacity var(--_nav-label-fade-out-motion) 0ms,\n mask-position var(--_nav-label-fade-out-motion) 0ms,\n -webkit-mask-position var(--_nav-label-fade-out-motion) 0ms;\n}\n\n.panel-nav--collapsed ds-text.panel-nav__group-label {\n opacity: 0;\n max-height: 0;\n padding: 0 calc(var(--dimension-space-075, 6px) + var(--ds-control-label-inset));\n pointer-events: none;\n transition:\n opacity var(--_nav-label-fade-out-motion) 0ms,\n max-height var(--_nav-label-fade-out-motion) 0ms,\n padding var(--_nav-label-fade-out-motion) 0ms,\n color var(--effect-motion-short-3, 200ms ease);\n}\n\n/* ── Footer ──────────────────────────────────────────────────────────────── */\n\n.panel-nav__footer {\n position: relative;\n flex-shrink: 0;\n /* single row: padding*2 + item */\n height: calc(var(--_nav-padding-y) * 2 + var(--_nav-item-height));\n transition: height var(--_nav-expand-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer {\n /* two rows: padding*2 + item*2 + gap */\n height: calc(\n var(--_nav-padding-y) * 2 + var(--_nav-item-height) * 2 + var(--dimension-space-050, 4px)\n );\n}\n\n/* ── Footer icon button (gear / dashboard) ───────────────────────────────── */\n\n.panel-nav__footer-btn {\n position: absolute;\n left: var(--_nav-padding-x);\n top: var(--_nav-padding-y);\n --ds-button-unfilled-fg: var(--_nav-fg-secondary);\n --ds-button-unfilled-fg-active: var(--_nav-fg-primary);\n --ds-interaction-hover: var(--_nav-hover);\n --ds-interaction-pressed: var(--_nav-pressed);\n --ds-focus-ring-color: var(--_nav-focus);\n}\n\n/* ── Footer user — absolutely positioned, slides from row to below gear ───── */\n\n.panel-nav__footer-user {\n position: absolute;\n /* Expanded row: balanced shell inset with an 8px gear-to-user gap. */\n left: calc(var(--_nav-padding-x) + var(--_nav-footer-btn-w) + var(--dimension-space-100, 8px));\n right: var(--_nav-padding-x);\n top: var(--_nav-padding-y);\n width: auto;\n min-width: 0;\n transition:\n left var(--_nav-expand-motion),\n right var(--_nav-expand-motion),\n top var(--_nav-expand-motion);\n}\n\n/* Popup-open state keeps the native footer trigger visibly pressed. */\n.panel-nav__footer-user--menu-expanded::after {\n background: var(--_nav-pressed);\n}\n\n.panel-nav__footer-user--menu-expanded {\n --ds-interaction-hover: var(--_nav-pressed);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user {\n /* Resolve one shared square control while keeping both inline edges animatable. */\n left: var(--_nav-padding-x);\n right: calc(100% - var(--_nav-padding-x) - var(--_nav-item-height));\n top: calc(var(--_nav-padding-y) + var(--_nav-item-height) + var(--dimension-space-050, 4px));\n}\n\n/* Label: absolute so it stays in place and only fades — no mask slide */\n.panel-nav__footer-user-label {\n text-align: right;\n position: absolute;\n left: var(--dimension-space-075, 6px);\n right: calc(\n var(--dimension-space-075, 6px) + var(--dimension-iconography-md, 20px) +\n var(--dimension-space-050, 4px)\n );\n top: 50%;\n transform: translateY(-50%);\n flex: none;\n height: var(--typography-lineheight-md, 20px);\n mask-image: none;\n -webkit-mask-image: none;\n transition: opacity var(--_nav-label-fade-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user-label {\n opacity: 0;\n pointer-events: none;\n mask-position: 0% 0;\n -webkit-mask-position: 0% 0;\n}\n\n/* ── Footer user icon — cross-fades chevron ↔ circle+initial ────────────── */\n\n/* Absolute — right-anchored in expanded, left-anchored in collapsed to match regular item icon position */\n.panel-nav__footer-user-icon {\n position: absolute;\n left: calc(100% - var(--dimension-iconography-md, 20px) - var(--dimension-space-075, 6px));\n top: 0;\n bottom: 0;\n height: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: left var(--_nav-expand-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-user-icon {\n left: var(--dimension-space-075, 6px);\n}\n\n.panel-nav__footer-icon-expanded,\n.panel-nav__footer-icon-collapsed {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.panel-nav__footer-icon-expanded {\n opacity: 1;\n transition: opacity var(--_nav-label-fade-motion);\n}\n\n.panel-nav__footer-icon-collapsed {\n opacity: 0;\n transition: opacity var(--_nav-label-fade-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-icon-expanded {\n opacity: 0;\n transition: opacity var(--_nav-label-fade-out-motion);\n}\n\n.panel-nav--collapsed .panel-nav__footer-icon-collapsed {\n opacity: 1;\n transition: opacity var(--_nav-label-fade-out-motion);\n}\n\n/* ── User initial — overlaid on the circle icon ─────────────────────────── */\n\n/*\n * Center the caption glyph on the Circle icon. Don't use inset:0 + flex — the\n * inner `.ds-text__element { width: 100% }` stretches and left-aligns the letter.\n * Translate-centering keeps the complete caption metric box on the icon center;\n * the caption variant remains the sole owner of its line-height.\n * (Do not set `inset: auto` after top/left — it resets them.)\n */\nds-text.panel-nav__user-initial,\nds-text.panel-nav__user-initial.ds-text--caption {\n position: absolute;\n top: 50%;\n left: 50%;\n right: auto;\n bottom: auto;\n transform: translate(-50%, -50%);\n display: block;\n width: max-content;\n max-width: none;\n min-width: 0;\n text-align: center;\n color: inherit;\n pointer-events: none;\n}\n\n/* ── Drag-to-resize handle ───────────────────────────────────────────────── */\n\n/* The resize overlay line — extends past the right edge; keep above workspace content */\n.panel-nav__resize-handle {\n position: absolute;\n top: 0;\n /* Centered on the right edge: --dimension-space-050 outside + inside = --dimension-space-100 hit zone */\n right: var(--dimension-space-n050, -4px);\n bottom: 0;\n width: var(--dimension-space-100, 8px);\n z-index: 20;\n cursor: ew-resize;\n touch-action: none;\n /* Needs a real background for Safari hit-testing */\n background: transparent;\n}\n\n/* Safari requires non-transparent fill for hit testing */\n.panel-nav__resize-handle::before {\n content: '';\n position: absolute;\n inset: 0;\n /* stylelint-disable-next-line color-named -- near-invisible hit-test fill; not a theme color */\n background: rgba(0, 0, 0, 0.001);\n}\n\n/* Resize overlay line */\n.panel-nav__resize-handle::after {\n content: '';\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n width: var(--dimension-stroke-width-025, 2px);\n background: var(--color-foreground-primary);\n opacity: 0;\n pointer-events: none;\n transition: opacity var(--effect-motion-short-2, 100ms ease-in-out);\n}\n\n.panel-nav__resize-handle--overlay::after {\n opacity: 1;\n}\n\n/* ── Reduced motion ──────────────────────────────────────────────────────── */\n\n@media (prefers-reduced-motion: reduce) {\n .panel-nav,\n .panel-nav *,\n .panel-nav *::before,\n .panel-nav *::after,\n .panel-nav__resize-handle,\n .panel-nav__resize-handle::before,\n .panel-nav__resize-handle::after {\n /* Near-instant — browsers treat 0 as \"no transition\" and can skip the end state. */\n transition-duration: var(--effect-animation-duration-instant) !important;\n animation-duration: var(--effect-animation-duration-instant) !important;\n transition-delay: var(--effect-animation-duration-instant) !important;\n }\n}\n",
|
|
629
629
|
"type": "registry:ui"
|
|
630
630
|
},
|
|
631
631
|
{
|
|
@@ -640,7 +640,7 @@
|
|
|
640
640
|
},
|
|
641
641
|
{
|
|
642
642
|
"path": "src/wc/components/PanelNav/panel-nav-utils.ts",
|
|
643
|
-
"content": "import type { PanelNavGroup, PanelNavItem } from './panel-nav-types';\nimport { parseCssTimeMs } from '../../utils/resolve-css-time-ms';\nimport {\n NAV_STYLE_HINT_ATTR,\n clearNavStyleHint,\n readNavStyleAttr,\n resolveNavChromeStyle,\n setNavStyleHint,\n shouldResyncNavChromeStyle,\n} from '../../shell/nav-chrome';\n\nexport type { NavChromeStyle } from '../../shell/nav-chrome';\nexport {\n NAV_STYLE_HINT_ATTR,\n readNavStyleAttr,\n setNavStyleHint,\n clearNavStyleHint,\n resolveNavChromeStyle as resolvePanelNavStyle,\n shouldResyncNavChromeStyle as shouldResyncPanelNavStyle,\n};\n\n/** Whether `path` matches `href` at a segment boundary (exact or child path). */\nexport function hrefMatchesPath(path: string, href: string): boolean {\n return path === href || path.startsWith(`${href}/`);\n}\n\n/** Derive the active nav item id from a URL using longest segment-boundary prefix match. */\nexport function deriveActiveIdFromUrl(path: string, items: PanelNavItem[]): string {\n if (!path) return '';\n\n let bestId = '';\n let bestLen = 0;\n\n for (const item of items) {\n if (!item.href) continue;\n if (hrefMatchesPath(path, item.href) && item.href.length > bestLen) {\n bestId = item.id;\n bestLen = item.href.length;\n }\n }\n\n return bestId;\n}\n\n/** Total nav item count across all groups. */\nexport function countPanelNavItems(groups: PanelNavGroup[]): number {\n return groups.reduce((sum, g) => sum + g.items.length, 0);\n}\n\n/** Resolve a user toggle without mutating the persisted desktop preference while breakpoint-locked. */\nexport function resolvePanelNavToggle(
|
|
643
|
+
"content": "import type { PanelNavGroup, PanelNavItem } from './panel-nav-types';\nimport { parseCssTimeMs } from '../../utils/resolve-css-time-ms';\nimport {\n NAV_STYLE_HINT_ATTR,\n clearNavStyleHint,\n readNavStyleAttr,\n resolveNavChromeStyle,\n setNavStyleHint,\n shouldResyncNavChromeStyle,\n} from '../../shell/nav-chrome';\n\nexport type { NavChromeStyle } from '../../shell/nav-chrome';\nexport {\n NAV_STYLE_HINT_ATTR,\n readNavStyleAttr,\n setNavStyleHint,\n clearNavStyleHint,\n resolveNavChromeStyle as resolvePanelNavStyle,\n shouldResyncNavChromeStyle as shouldResyncPanelNavStyle,\n};\n\n/** Whether `path` matches `href` at a segment boundary (exact or child path). */\nexport function hrefMatchesPath(path: string, href: string): boolean {\n return path === href || path.startsWith(`${href}/`);\n}\n\n/** Derive the active nav item id from a URL using longest segment-boundary prefix match. */\nexport function deriveActiveIdFromUrl(path: string, items: PanelNavItem[]): string {\n if (!path) return '';\n\n let bestId = '';\n let bestLen = 0;\n\n for (const item of items) {\n if (!item.href) continue;\n if (hrefMatchesPath(path, item.href) && item.href.length > bestLen) {\n bestId = item.id;\n bestLen = item.href.length;\n }\n }\n\n return bestId;\n}\n\n/** Total nav item count across all groups. */\nexport function countPanelNavItems(groups: PanelNavGroup[]): number {\n return groups.reduce((sum, g) => sum + g.items.length, 0);\n}\n\n/** Resolve a user toggle without mutating the persisted desktop preference while breakpoint-locked. */\nexport function resolvePanelNavToggle(collapsed: boolean, viewportNarrow: boolean): boolean | null {\n return viewportNarrow ? null : !collapsed;\n}\n\nexport interface PanelNavTransitionStyle {\n transitionProperty: string;\n transitionDuration: string;\n transitionDelay: string;\n}\n\nfunction parseCssTimeList(value: string): number[] {\n return value.split(',').map(item => parseCssTimeMs(item.trim(), 0));\n}\n\n/**\n * Maximum computed time for PanelNav's width transition. CSS repeats shorter\n * duration/delay lists to match the property list, so mirror that behavior.\n */\nexport function panelNavWidthTransitionMs(style: PanelNavTransitionStyle): number {\n const properties = style.transitionProperty.split(',').map(item => item.trim());\n const durations = parseCssTimeList(style.transitionDuration);\n const delays = parseCssTimeList(style.transitionDelay);\n\n return properties.reduce((max, property, index) => {\n if (property !== 'all' && property !== 'width' && property !== 'min-width') return max;\n const duration = durations[index % durations.length] ?? 0;\n const delay = delays[index % delays.length] ?? 0;\n return Math.max(max, duration + delay, 0);\n }, 0);\n}\n\n/** Parse `groups` from either a JSON attribute string or a JS property array. */\nexport function parsePanelNavGroups(groups: string | unknown): PanelNavGroup[] {\n if (Array.isArray(groups)) return groups;\n if (typeof groups === 'string') {\n try {\n const parsed = JSON.parse(groups);\n return Array.isArray(parsed) ? parsed : [];\n } catch {\n return [];\n }\n }\n return [];\n}\n\n/** Resolve whether VT is disabled from prop and/or host attribute. */\nexport function resolvePanelNavDisableVt(prop: boolean, attr: string | null): boolean {\n if (prop) return true;\n if (attr === null) return false;\n return attr !== 'false';\n}\n\n/** True when host `groups` has items but internal parsed state is still empty. */\nexport function shouldResyncPanelNavGroups(\n parsedGroups: PanelNavGroup[],\n groups: string | unknown\n): boolean {\n return (\n countPanelNavItems(parsedGroups) === 0 && countPanelNavItems(parsePanelNavGroups(groups)) > 0\n );\n}\n",
|
|
644
644
|
"type": "registry:ui"
|
|
645
645
|
}
|
|
646
646
|
]
|
|
@@ -242,7 +242,7 @@
|
|
|
242
242
|
},
|
|
243
243
|
{
|
|
244
244
|
"path": "src/wc/components/PanelSubNav/PanelSubNav.tsx",
|
|
245
|
-
"content": "import {
|
|
245
|
+
"content": "import {\n Component,\n Prop,\n Event,\n EventEmitter,\n Element,\n Listen,\n Watch,\n h,\n Host,\n} from '@stencil/core';\nimport type { PanelSubNavItem } from './panel-sub-nav-types';\n\nexport type PanelSubNavBackground =\n | 'faint'\n | 'medium'\n | 'bold'\n | 'strong'\n | 'translucent'\n | 'inverted'\n | 'media'\n | 'always-dark';\n\n@Component({\n tag: 'ds-panel-sub-nav',\n styleUrl: 'PanelSubNav.css',\n scoped: true,\n})\nexport class PanelSubNav {\n @Element() el!: HTMLElement;\n\n /** Required selected item id when enabled items exist. Keep synchronized with the visible panel. */\n @Prop({ mutable: true }) value: string = '';\n /**\n * Local panel tabs. Assign this array through the JavaScript property.\n * Replace the array reference when items change.\n */\n @Prop() items: PanelSubNavItem[] = [];\n /** Actual parent surface context. Omit on primary and secondary surfaces. */\n @Prop() background: PanelSubNavBackground | undefined;\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n\n @Event() dsChange!: EventEmitter<string>;\n\n private get enabledItems(): PanelSubNavItem[] {\n return this.items.filter(item => !item.isInactive);\n }\n\n private get focusableId(): string {\n return this.enabledItems.some(item => item.id === this.value)\n ? this.value\n : (this.enabledItems[0]?.id ?? '');\n }\n\n private selectItem(id: string) {\n this.value = id;\n this.dsChange.emit(id);\n this.focusItem(id);\n }\n\n private focusItem(id: string) {\n const item = this.el.querySelector(`[data-panel-sub-nav-id=\"${id}\"]`) as HTMLElement | null;\n item?.focus({ preventScroll: true });\n }\n\n @Watch('value')\n onValueChange(next: string, previous: string | undefined) {\n if (previous === undefined || next === previous) return;\n\n requestAnimationFrame(() => {\n const active = document.activeElement as HTMLElement | null;\n if (!active || !this.el.contains(active)) return;\n this.focusItem(this.enabledItems.some(item => item.id === next) ? next : this.focusableId);\n });\n }\n\n private findEnabled(from: number, step: 1 | -1): number {\n const length = this.items.length;\n for (let offset = 1; offset <= length; offset++) {\n const index = (from + step * offset + length * offset) % length;\n if (!this.items[index]?.isInactive) return index;\n }\n return from;\n }\n\n @Listen('keydown')\n handleKeyDown(event: KeyboardEvent) {\n if (!this.enabledItems.length) return;\n\n const focusedId = (event.target as Element | null)?.closest<HTMLElement>(\n '[data-panel-sub-nav-id]'\n )?.dataset['panelSubNavId'];\n const focusedIndex = this.items.findIndex(item => item.id === focusedId);\n const selectedIndex = this.items.findIndex(item => item.id === this.value && !item.isInactive);\n const currentIndex =\n focusedIndex >= 0\n ? focusedIndex\n : selectedIndex >= 0\n ? selectedIndex\n : event.key === 'ArrowUp'\n ? 0\n : -1;\n let nextIndex: number | null = null;\n\n if (event.key === 'ArrowDown') {\n nextIndex = this.findEnabled(currentIndex, 1);\n } else if (event.key === 'ArrowUp') {\n nextIndex = this.findEnabled(currentIndex, -1);\n } else if (event.key === 'Home') {\n nextIndex = this.items.findIndex(item => !item.isInactive);\n } else if (event.key === 'End') {\n const reversedIndex = [...this.items].reverse().findIndex(item => !item.isInactive);\n nextIndex = reversedIndex < 0 ? null : this.items.length - 1 - reversedIndex;\n }\n\n if (nextIndex === null || nextIndex < 0) return;\n const next = this.items[nextIndex];\n if (!next || next.isInactive) return;\n\n event.preventDefault();\n this.selectItem(next.id);\n }\n\n render() {\n const focusableId = this.focusableId;\n const background = this.background;\n\n return (\n <Host>\n <div\n class=\"panel-sub-nav\"\n role=\"tablist\"\n aria-orientation=\"vertical\"\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n >\n {this.items.map(item => {\n const isSelected = item.id === this.value;\n return (\n <button\n id={item.id}\n type=\"button\"\n role=\"tab\"\n data-panel-sub-nav-id={item.id}\n class={{\n 'panel-sub-nav__item': true,\n 'panel-sub-nav__item--selected': isSelected,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': !item.isInactive,\n 'ds-interaction-fill--selected': isSelected && !item.isInactive,\n 'ds-interaction-fill--on-faint': background === 'faint',\n 'ds-interaction-fill--on-medium': background === 'medium',\n 'ds-interaction-fill--on-bold': background === 'bold',\n 'ds-interaction-fill--on-strong': background === 'strong',\n 'ds-interaction-fill--on-translucent': background === 'translucent',\n 'ds-interaction-fill--on-inverted': background === 'inverted',\n 'ds-interaction-fill--on-media': background === 'media',\n 'ds-interaction-fill--on-always-dark': background === 'always-dark',\n [`panel-sub-nav__item--on-${background}`]: !!background,\n 'ds-control-inactive': !!item.isInactive,\n }}\n aria-selected={isSelected ? 'true' : 'false'}\n aria-controls={item.panelId}\n aria-disabled={item.isInactive ? 'true' : undefined}\n disabled={item.isInactive}\n tabIndex={item.id === focusableId ? 0 : -1}\n onClick={() => !item.isInactive && this.selectItem(item.id)}\n >\n <ds-text\n class=\"panel-sub-nav__label ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"inherit\"\n >\n {item.label}\n </ds-text>\n </button>\n );\n })}\n </div>\n </Host>\n );\n }\n}\n",
|
|
246
246
|
"type": "registry:ui"
|
|
247
247
|
},
|
|
248
248
|
{
|
|
@@ -341,7 +341,7 @@
|
|
|
341
341
|
},
|
|
342
342
|
{
|
|
343
343
|
"path": "src/wc/components/PanelToolHeader/PanelToolHeader.tsx",
|
|
344
|
-
"content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\nimport type {\n PanelToolHeaderActionDetail,\n PanelToolsHeaderAction,\n} from '../PanelTools/panel-tools-types';\n\n@Component({\n tag: 'ds-panel-tool-header',\n styleUrl: 'PanelToolHeader.css',\n scoped: true,\n})\nexport class PanelToolHeader {\n @Element() el!: HTMLElement;\n\n /** Visible heading for the active tool view. */\n @Prop() heading: string = '';\n /** Show the leading Back action. */\n @Prop() showBack: boolean = false;\n /** Canonical icon for the leading navigation or dismiss action. */\n @Prop() backIcon: string = 'ChevronLeft';\n /** Accessible name for the leading navigation or dismiss action. */\n @Prop() backAriaLabel: string = 'Back';\n /** Show the trailing options-menu trigger. */\n @Prop() showMenu: boolean = true;\n /** Accessible name for the options-menu trigger. */\n @Prop() menuAriaLabel: string = 'More options';\n /** Stable id placed on the menu trigger host for ds-menu anchoring. */\n @Prop() menuTriggerId: string = '';\n /** Id of the menu controlled by the trigger. */\n @Prop() menuControls: string | undefined;\n /** Whether the controlled menu is open. */\n @Prop() menuExpanded: boolean = false;\n /** Trailing application-owned icon actions. When present, these replace the legacy menu action. */\n @Prop() actions: PanelToolsHeaderAction[] = [];\n\n @Event() dsBack!: EventEmitter<MouseEvent>;\n @Event() dsMenuToggle!: EventEmitter<MouseEvent>;\n @Event() dsAction!: EventEmitter<PanelToolHeaderActionDetail>;\n\n @Method()\n async focusMenuTrigger() {\n const trigger = this.el.querySelector(\n 'ds-button-unfilled.panel-tool-header__menu, ds-button-unfilled[data-header-action-id=\"menu\"]'\n ) as (HTMLElement & { setFocus?: () => Promise<void> }) | null;\n await trigger?.setFocus?.();\n }\n\n render() {\n const authoredActions = this.actions.length > 0;\n const actions: PanelToolsHeaderAction[] = authoredActions\n ? this.actions\n : this.showMenu\n
|
|
344
|
+
"content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\nimport type {\n PanelToolHeaderActionDetail,\n PanelToolsHeaderAction,\n} from '../PanelTools/panel-tools-types';\n\n@Component({\n tag: 'ds-panel-tool-header',\n styleUrl: 'PanelToolHeader.css',\n scoped: true,\n})\nexport class PanelToolHeader {\n @Element() el!: HTMLElement;\n\n /** Visible heading for the active tool view. */\n @Prop() heading: string = '';\n /** Show the leading Back action. */\n @Prop() showBack: boolean = false;\n /** Canonical icon for the leading navigation or dismiss action. */\n @Prop() backIcon: string = 'ChevronLeft';\n /** Accessible name for the leading navigation or dismiss action. */\n @Prop() backAriaLabel: string = 'Back';\n /** Show the trailing options-menu trigger. */\n @Prop() showMenu: boolean = true;\n /** Accessible name for the options-menu trigger. */\n @Prop() menuAriaLabel: string = 'More options';\n /** Stable id placed on the menu trigger host for ds-menu anchoring. */\n @Prop() menuTriggerId: string = '';\n /** Id of the menu controlled by the trigger. */\n @Prop() menuControls: string | undefined;\n /** Whether the controlled menu is open. */\n @Prop() menuExpanded: boolean = false;\n /** Trailing application-owned icon actions. When present, these replace the legacy menu action. */\n @Prop() actions: PanelToolsHeaderAction[] = [];\n\n @Event() dsBack!: EventEmitter<MouseEvent>;\n @Event() dsMenuToggle!: EventEmitter<MouseEvent>;\n @Event() dsAction!: EventEmitter<PanelToolHeaderActionDetail>;\n\n @Method()\n async focusMenuTrigger() {\n const trigger = this.el.querySelector(\n 'ds-button-unfilled.panel-tool-header__menu, ds-button-unfilled[data-header-action-id=\"menu\"]'\n ) as (HTMLElement & { setFocus?: () => Promise<void> }) | null;\n await trigger?.setFocus?.();\n }\n\n render() {\n const authoredActions = this.actions.length > 0;\n const actions: PanelToolsHeaderAction[] = authoredActions\n ? this.actions\n : this.showMenu\n ? [\n {\n id: 'menu',\n icon: 'Ellipses',\n ariaLabel: this.menuAriaLabel,\n triggerId: this.menuTriggerId,\n controls: this.menuControls,\n expanded: this.menuExpanded,\n haspopup: 'menu',\n },\n ]\n : [];\n\n return (\n <Host>\n <header class=\"panel-tool-header ds-chrome-header ds-chrome-header--bounded\">\n <div class=\"panel-tool-header__leading ds-chrome-header__leading\">\n {this.showBack ? (\n <ds-tooltip label={this.backAriaLabel} side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n class=\"panel-tool-header__back\"\n variant=\"icon\"\n icon={this.backIcon}\n size=\"md\"\n aria-label={this.backAriaLabel}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => this.dsBack.emit(event.detail)}\n />\n </ds-tooltip>\n ) : null}\n </div>\n <ds-text\n class=\"panel-tool-header__heading ds-chrome-header__heading ds-control--md\"\n as=\"h2\"\n variant=\"text-body-medium\"\n emphasis\n color=\"primary\"\n lineTruncation={1}\n >\n {this.heading}\n </ds-text>\n <div class=\"panel-tool-header__trailing ds-chrome-header__trailing\">\n {actions.map(action => (\n <ds-tooltip key={action.id} label={action.ariaLabel} side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n id={action.triggerId || undefined}\n data-header-action-id={action.id}\n class={{\n 'panel-tool-header__action': true,\n 'panel-tool-header__menu': action.id === 'menu',\n }}\n variant=\"icon\"\n icon={action.icon}\n size=\"md\"\n aria-label={action.ariaLabel}\n haspopup={action.haspopup}\n controls={action.controls}\n expanded={action.expanded}\n pressed={action.pressed}\n isInactive={action.isInactive}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n if (!authoredActions && action.id === 'menu')\n this.dsMenuToggle.emit(event.detail);\n this.dsAction.emit({\n id: action.id,\n originalEvent: event.detail,\n anchor: event.currentTarget as HTMLElement,\n });\n }}\n />\n </ds-tooltip>\n ))}\n </div>\n </header>\n </Host>\n );\n }\n}\n",
|
|
345
345
|
"type": "registry:ui"
|
|
346
346
|
}
|
|
347
347
|
]
|
|
@@ -386,7 +386,7 @@
|
|
|
386
386
|
},
|
|
387
387
|
{
|
|
388
388
|
"path": "src/wc/components/PanelToolSearch/PanelToolSearch.tsx",
|
|
389
|
-
"content": "import {
|
|
389
|
+
"content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\nimport { ChoiceSearch } from '../../utils/choice-list-parts';\n\n@Component({\n tag: 'ds-panel-tool-search',\n styleUrl: 'PanelToolSearch.css',\n scoped: true,\n})\nexport class PanelToolSearch {\n @Element() el!: HTMLElement;\n\n /** Current search query. */\n @Prop({ mutable: true }) value: string = '';\n /** Search guidance shown while the query is empty. */\n @Prop() placeholder: string = 'Search';\n /** Accessible name for the search field. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string = 'Search';\n /** Optional ID of the results region filtered by this search. */\n @Prop() controls: string | undefined;\n /** Prevents search interaction while keeping the row visible. */\n @Prop() isInactive: boolean = false;\n /** Show the optional trailing filter-menu trigger. */\n @Prop() showFilter: boolean = false;\n /** Accessible name for the filter-menu trigger. */\n @Prop() filterAriaLabel: string = 'Filter';\n /** Stable id placed on the filter trigger for ds-menu anchoring. */\n @Prop() filterTriggerId: string = '';\n /** Id of the menu controlled by the filter trigger. */\n @Prop() filterControls: string | undefined;\n /** Whether the controlled filter menu is open. */\n @Prop() filterExpanded: boolean = false;\n /** Promote the trigger foreground when one or more filters are applied. */\n @Prop() filterActive: boolean = false;\n\n @Event() dsChange!: EventEmitter<string>;\n @Event() dsClear!: EventEmitter<void>;\n @Event() dsFilterToggle!: EventEmitter<MouseEvent>;\n\n componentDidRender() {\n const input = this.el.querySelector('input');\n if (input && input.value !== this.value) input.value = this.value;\n }\n\n @Method()\n async setFocus() {\n this.el.querySelector('input')?.focus();\n }\n\n @Method()\n async focusFilterTrigger() {\n const trigger = this.el.querySelector('ds-button-unfilled.panel-tool-search__filter') as\n | (HTMLElement & { setFocus?: () => Promise<void> })\n | null;\n await trigger?.setFocus?.();\n }\n\n private handleChange = (value: string) => {\n this.value = value;\n this.dsChange.emit(value);\n };\n\n render() {\n return (\n <Host>\n <div class=\"panel-tool-search ds-chrome-row ds-chrome-space--md\">\n <ChoiceSearch\n value={this.value}\n placeholder={this.placeholder}\n ariaLabel={this.ariaLabel}\n controls={this.controls}\n disabled={this.isInactive}\n inputRef={() => undefined}\n clearLabel=\"Clear\"\n onValueChange={this.handleChange}\n onClear={() => this.dsClear.emit()}\n onKeyDown={() => undefined}\n />\n {this.showFilter ? (\n <div class=\"panel-tool-search__filter-group\">\n <ds-divider\n class=\"panel-tool-search__divider\"\n orientation=\"vertical\"\n length=\"var(--dimension-size-200)\"\n />\n <ds-tooltip label={this.filterAriaLabel} side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n id={this.filterTriggerId || undefined}\n class=\"panel-tool-search__filter\"\n variant=\"icon\"\n icon=\"Filters\"\n size=\"md\"\n aria-label={this.filterAriaLabel}\n haspopup=\"menu\"\n controls={this.filterControls}\n expanded={this.filterExpanded}\n isActive={this.filterActive}\n isInactive={this.isInactive}\n activeFill={false}\n hasBorder={false}\n onDsChange={(event: CustomEvent<boolean>) => {\n event.stopPropagation();\n }}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n event.stopPropagation();\n this.dsFilterToggle.emit(event.detail);\n }}\n />\n </ds-tooltip>\n </div>\n ) : null}\n </div>\n </Host>\n );\n }\n}\n",
|
|
390
390
|
"type": "registry:ui"
|
|
391
391
|
}
|
|
392
392
|
]
|
|
@@ -469,7 +469,7 @@
|
|
|
469
469
|
},
|
|
470
470
|
{
|
|
471
471
|
"path": "src/wc/components/PanelTools/PanelTools.tsx",
|
|
472
|
-
"content": "import {\n Component,\n Prop,\n Event,\n EventEmitter,\n Element,\n State,\n Watch,\n Method,\n Listen,\n h,\n Host,\n} from '@stencil/core';\nimport type { ChromeTransitionDetail } from '../../shell/chrome-transition';\nimport {\n PANEL_TOOLS_LABELS,\n PANEL_TOOLS_SHORTCUTS,\n type PanelToolsHeaderAction,\n type PanelToolsHeaderActionDetail,\n type PanelToolsHeaderConfig,\n type PanelToolsHeaders,\n type PanelToolsItem,\n type PanelToolsToolId,\n} from './panel-tools-types';\nimport {\n isPanelToolsToolId,\n orderPanelToolsItems,\n panelToolsDrawerAtTerminal,\n panelToolsDrawerResting,\n panelToolsDrawerTransitionMs,\n panelToolsRailPlacement,\n reconcilePanelToolsAvailability,\n resolvePanelToolActivation,\n} from './panel-tools-utils';\n\n@Component({\n tag: 'ds-panel-tools',\n styleUrl: 'PanelTools.css',\n shadow: true,\n})\nexport class PanelTools {\n @Element() el!: HTMLElement;\n\n /** When false, only the icon rail is shown. */\n @Prop({ mutable: true, reflect: true }) open: boolean = false;\n\n /** Drawer presentation or viewport-covering presentation for the active tool. */\n @Prop({ mutable: true, reflect: true }) presentation: 'drawer' | 'fullscreen' = 'drawer';\n\n /** Let a fullscreen product supply independent master/detail headers inside its view. */\n @Prop({ attribute: 'fullscreen-header-mode', reflect: true })\n fullscreenHeaderMode: 'shared' | 'split' = 'shared';\n\n /** Active tool view — `search`, `agents`, `messages`, `stacks`, `activity`, or `help`. */\n @Prop({ mutable: true, attribute: 'active-tool', reflect: true })\n activeTool: PanelToolsToolId | '' = '';\n\n /**\n * Rail items rendered in the right column.\n * Set via JS property: `el.items = [...]`. Replace the array reference to update.\n */\n @Prop() items: PanelToolsItem[] = [];\n\n @Prop() toolsLabel: string = 'Tools';\n @Prop() toolShortcutsLabel: string = 'Tool shortcuts';\n\n /** Active header state per tool. Replace the object when title, depth, or actions change. */\n @Prop() headers: PanelToolsHeaders = {};\n\n /**\n * Optional localStorage key for the last active tool. The drawer always starts\n * closed; only the tool identity is restored for continuity within this browser.\n */\n @Prop({ attribute: 'storage-key' }) storageKey: string = '';\n\n /** Emitted when a rail button is toggled. Detail = { id, selected }. */\n @Event({ bubbles: true, composed: true }) dsToolChange!: EventEmitter<{\n id: PanelToolsToolId;\n selected: boolean;\n }>;\n\n /** Emitted when fullscreen changes, including Escape-initiated exits. */\n @Event({ bubbles: true, composed: true }) dsPresentationChange!: EventEmitter<{\n presentation: 'drawer' | 'fullscreen';\n }>;\n\n /** Requests navigation to the active tool's parent view. */\n @Event({ bubbles: true, composed: true }) dsHeaderBack!: EventEmitter<{\n tool: PanelToolsToolId;\n }>;\n\n /** Requests one application-owned action from the active tool header. */\n @Event({ bubbles: true, composed: true })\n dsHeaderAction!: EventEmitter<PanelToolsHeaderActionDetail>;\n\n /** Bubbling lifecycle — `ds-bar-nav` defers overflow checks during drawer motion. */\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionStart!: EventEmitter<ChromeTransitionDetail>;\n\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionEnd!: EventEmitter<ChromeTransitionDetail>;\n\n /** Arms open vs close easing for the in-flight width transition. */\n @State() private motion: 'opening' | 'closing' | 'idle' = 'idle';\n\n /** Keeps header and slotted content painted until the closing clip transition completes. */\n @State() private drawerSurfaceRetained = false;\n\n /** Suppresses width transition until the host has painted its initial open state. */\n @State() private readyForMotion = false;\n\n /** Fullscreen presentation changes snap without borrowing drawer width motion. */\n @State() private presentationMotionSuppressed = false;\n\n @State() private rovingIndex = 0;\n @State() private fullViewToolIds: PanelToolsToolId[] = [];\n @State() private railBodyScrollable = false;\n\n private motionEnableGeneration = 0;\n private presentationMotionGeneration = 0;\n private transitionGeneration = 0;\n private transitionFallbackTimer: number | null = null;\n /** Ignores the old transition's terminal event when reversing direction. */\n private ignoreReplacementTerminal = false;\n private focusBeforeFullscreen: HTMLElement | null = null;\n private fullViewObserver: MutationObserver | null = null;\n private railBodyEl?: HTMLElement;\n private railBodyObserver?: ResizeObserver;\n\n private get railItems(): PanelToolsItem[] {\n return this.items ?? [];\n }\n\n private get orderedRailItems(): PanelToolsItem[] {\n return orderPanelToolsItems(this.railItems);\n }\n\n disconnectedCallback() {\n this.el.removeEventListener('transitionend', this.handleTransitionEnd);\n this.el.removeEventListener('transitioncancel', this.handleTransitionEnd);\n this.el.removeEventListener('dsAction', this.handleComposedHeaderAction);\n this.clearTransitionCompletion();\n this.motionEnableGeneration += 1;\n this.fullViewObserver?.disconnect();\n this.fullViewObserver = null;\n this.disconnectRailBodyObserver();\n }\n\n @Watch('presentation')\n presentationChanged(next: 'drawer' | 'fullscreen', previous?: 'drawer' | 'fullscreen') {\n if (next === previous) return;\n // Framework property bindings can update slotted master/detail content in\n // the same render that requests fullscreen. Reflect the host selector\n // immediately so that content cannot paint once in the drawer geometry\n // while Stencil waits to commit its next render.\n if (this.el.getAttribute('presentation') !== next) {\n this.el.setAttribute('presentation', next);\n }\n const presentationGeneration = ++this.presentationMotionGeneration;\n this.presentationMotionSuppressed = true;\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (presentationGeneration !== this.presentationMotionGeneration) return;\n this.presentationMotionSuppressed = false;\n });\n });\n if (next === 'fullscreen') {\n this.focusBeforeFullscreen =\n document.activeElement instanceof HTMLElement ? document.activeElement : null;\n this.open = true;\n } else if (previous === 'fullscreen') {\n const focusBeforeFullscreen = this.focusBeforeFullscreen;\n const fallbackActionId = this.activeHeader().actions?.[0]?.id;\n requestAnimationFrame(async () => {\n if (focusBeforeFullscreen?.isConnected) {\n focusBeforeFullscreen.focus({ preventScroll: true });\n }\n const focusRestored = focusBeforeFullscreen\n ? document.activeElement === focusBeforeFullscreen ||\n focusBeforeFullscreen.contains(document.activeElement)\n : false;\n if (!focusRestored && fallbackActionId) await this.focusHeaderAction(fallbackActionId);\n this.focusBeforeFullscreen = null;\n });\n }\n this.dsPresentationChange.emit({ presentation: next });\n }\n\n @Listen('keydown', { target: 'window', capture: true })\n handleFullscreenEscape(event: KeyboardEvent) {\n if (this.presentation !== 'fullscreen' || event.key !== 'Escape') return;\n event.preventDefault();\n event.stopPropagation();\n this.presentation = 'drawer';\n }\n\n private deferMotionEnable() {\n if (this.readyForMotion) return;\n const generation = ++this.motionEnableGeneration;\n requestAnimationFrame(() => {\n if (generation !== this.motionEnableGeneration) return;\n requestAnimationFrame(() => {\n if (generation !== this.motionEnableGeneration) return;\n this.readyForMotion = true;\n });\n });\n }\n\n connectedCallback() {\n this.el.addEventListener('transitionend', this.handleTransitionEnd);\n this.el.addEventListener('transitioncancel', this.handleTransitionEnd);\n this.el.addEventListener('dsAction', this.handleComposedHeaderAction);\n }\n\n private handleComposedHeaderAction = (event: Event) => {\n const detail = (event as CustomEvent<{ id?: string; anchor?: HTMLElement }>).detail;\n if (!this.activeTool || !detail?.id) return;\n event.stopPropagation();\n this.dsHeaderAction.emit({\n tool: this.activeTool,\n id: detail.id,\n anchor: detail.anchor,\n });\n };\n\n componentWillLoad() {\n this.restoreLastActiveTool();\n this.drawerSurfaceRetained = this.open;\n }\n\n @Watch('open')\n openChanged(isOpen: boolean, wasOpen?: boolean) {\n if (isOpen) this.drawerSurfaceRetained = true;\n if (this.readyForMotion && wasOpen !== undefined && wasOpen !== isOpen) {\n this.startDrawerTransition(isOpen ? 'opening' : 'closing');\n } else if (!isOpen) {\n this.drawerSurfaceRetained = false;\n }\n this.deferMotionEnable();\n }\n\n @Watch('activeTool')\n activeToolChanged() {\n this.persistLastActiveTool();\n this.deferMotionEnable();\n }\n\n @Watch('storageKey')\n storageKeyChanged() {\n this.restoreLastActiveTool();\n }\n\n componentDidLoad() {\n this.deferMotionEnable();\n this.connectRailBodyObserver();\n this.updateRailBodyScrollable();\n this.syncFullViewTools();\n this.fullViewObserver = new MutationObserver(() => this.syncFullViewTools());\n this.fullViewObserver.observe(this.el, {\n childList: true,\n attributes: true,\n attributeFilter: ['slot'],\n });\n }\n\n componentDidRender() {\n this.updateRailBodyScrollable();\n }\n\n private connectRailBodyObserver() {\n this.disconnectRailBodyObserver();\n if (!this.railBodyEl || typeof ResizeObserver === 'undefined') return;\n this.railBodyObserver = new ResizeObserver(() => this.updateRailBodyScrollable());\n this.railBodyObserver.observe(this.railBodyEl);\n }\n\n private disconnectRailBodyObserver() {\n this.railBodyObserver?.disconnect();\n this.railBodyObserver = undefined;\n }\n\n private updateRailBodyScrollable() {\n const next = Boolean(\n this.railBodyEl && this.railBodyEl.scrollHeight > this.railBodyEl.clientHeight + 1\n );\n if (next !== this.railBodyScrollable) this.railBodyScrollable = next;\n }\n\n @Watch('items')\n itemsChanged() {\n this.rovingIndex = 0;\n this.reconcileActiveTool();\n this.deferMotionEnable();\n }\n\n private restoreLastActiveTool() {\n if (!this.storageKey || this.activeTool) return;\n try {\n const stored = localStorage.getItem(this.storageKey);\n if (isPanelToolsToolId(stored)) this.activeTool = stored;\n } catch {\n /* localStorage unavailable */\n }\n }\n\n private persistLastActiveTool() {\n if (!this.storageKey || !this.activeTool) return;\n try {\n localStorage.setItem(this.storageKey, this.activeTool);\n } catch {\n /* unavailable */\n }\n }\n\n private clearPersistedTool() {\n if (!this.storageKey) return;\n try {\n localStorage.removeItem(this.storageKey);\n } catch {\n /* unavailable */\n }\n }\n\n private reconcileActiveTool() {\n const next = reconcilePanelToolsAvailability(this.railItems, this.open, this.activeTool);\n if (!next.removedTool) return;\n this.clearPersistedTool();\n this.open = next.open;\n this.activeTool = next.activeTool;\n this.dsToolChange.emit({ id: next.removedTool, selected: false });\n }\n\n private startDrawerTransition(phase: 'opening' | 'closing') {\n const drawer = this.renderRoot.querySelector('.panel-tools__drawer') as HTMLElement | null;\n if (this.motion !== 'idle') {\n this.finishDrawerTransition();\n this.ignoreReplacementTerminal = true;\n }\n this.motion = phase;\n this.dsChromeTransitionStart.emit({ source: 'panel-tools', phase });\n const generation = ++this.transitionGeneration;\n\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (generation !== this.transitionGeneration || this.motion !== phase) return;\n if (!drawer) {\n this.finishDrawerTransition();\n return;\n }\n\n if (this.drawerTransitionAtTerminal(drawer, phase)) {\n this.finishDrawerTransition();\n return;\n }\n\n const fallbackMs = panelToolsDrawerTransitionMs(getComputedStyle(drawer));\n if (fallbackMs <= 0) {\n this.finishDrawerTransition();\n return;\n }\n this.transitionFallbackTimer = window.setTimeout(() => {\n this.finishDrawerTransition();\n }, fallbackMs);\n });\n });\n }\n\n private handleTransitionEnd = (event: TransitionEvent) => {\n const drawer = this.renderRoot.querySelector('.panel-tools__drawer') as HTMLElement | null;\n if (event.target !== drawer) return;\n if (event.propertyName !== 'max-width') return;\n if (this.ignoreReplacementTerminal) {\n this.ignoreReplacementTerminal = false;\n return;\n }\n // Changing from the neutral transition recipe to the direction-specific\n // easing can cancel the transition that the reflected `open` class began.\n // The replacement transition and its watchdog still own completion; ending\n // motion here would hide the fixed-width tool surface while it is visible.\n if (event.type === 'transitioncancel') return;\n if (drawer && !this.drawerTransitionAtTerminal(drawer, this.motion)) return;\n this.finishDrawerTransition();\n };\n\n private drawerTransitionAtTerminal(\n drawer: HTMLElement,\n phase: 'opening' | 'closing' | 'idle'\n ): boolean {\n const width = drawer.getBoundingClientRect().width;\n const surface = drawer.querySelector('.panel-tools__drawer-surface') as HTMLElement | null;\n const targetWidth = surface?.getBoundingClientRect().width ?? 0;\n return panelToolsDrawerAtTerminal(width, targetWidth, phase);\n }\n\n private finishDrawerTransition() {\n const completedPhase = this.motion;\n const wasTransitioning = completedPhase !== 'idle';\n this.clearTransitionCompletion();\n if (!wasTransitioning) return;\n this.ignoreReplacementTerminal = false;\n this.motion = 'idle';\n if (completedPhase === 'closing' && !this.open) {\n this.drawerSurfaceRetained = false;\n }\n this.dsChromeTransitionEnd.emit({ source: 'panel-tools' });\n }\n\n private clearTransitionCompletion() {\n this.transitionGeneration += 1;\n if (this.transitionFallbackTimer !== null) {\n window.clearTimeout(this.transitionFallbackTimer);\n this.transitionFallbackTimer = null;\n }\n }\n\n /** Rail selection follows `open` immediately — independent of the slide animation. */\n private isRailSelected(id: PanelToolsToolId): boolean {\n return this.open && this.activeTool === id;\n }\n\n /** Drawer body stays mounted while the clip frame animates closed. */\n private isDrawerPresent(): boolean {\n return this.open || this.drawerSurfaceRetained;\n }\n\n private isViewActive(id: PanelToolsToolId): boolean {\n return this.isDrawerPresent() && this.activeTool === id;\n }\n\n private hasFullView(id: PanelToolsToolId): boolean {\n return this.fullViewToolIds.includes(id);\n }\n\n private syncFullViewTools() {\n const ids = this.orderedRailItems.map(item => item.id);\n const next = ids.filter(id => Boolean(this.el.querySelector(`[slot=\"${id}-view\"]`)));\n if (next.join('|') !== this.fullViewToolIds.join('|')) this.fullViewToolIds = next;\n }\n\n private get renderRoot(): ParentNode {\n return this.el.shadowRoot ?? this.el;\n }\n\n private headerLabel(): string {\n if (!this.isDrawerPresent() || !this.activeTool) return '';\n const item = this.railItems.find(candidate => candidate.id === this.activeTool);\n return (\n item?.label ??\n PANEL_TOOLS_LABELS[this.activeTool as keyof typeof PANEL_TOOLS_LABELS] ??\n this.activeTool\n );\n }\n\n private resolvedHeaders(): PanelToolsHeaders {\n return this.headers ?? {};\n }\n\n private activeHeader(): PanelToolsHeaderConfig {\n if (!this.activeTool) return {};\n return this.resolvedHeaders()[this.activeTool] ?? {};\n }\n\n private handleToolChange = (id: PanelToolsToolId) => {\n const next = resolvePanelToolActivation(this.open, this.activeTool, id);\n this.open = next.open;\n this.activeTool = next.activeTool;\n this.dsToolChange.emit({ id, selected: next.selected });\n };\n\n /** Toggle any rail tool open/closed — shell shortcuts K/A/S/M/N call this. */\n @Method()\n async activateTool(id: PanelToolsToolId) {\n const item = this.railItems.find(entry => entry.id === id);\n if (!item || item.isInactive) return;\n this.handleToolChange(id);\n }\n\n /** Close the tools drawer when open — used by shell keyboard shortcuts. */\n @Method()\n async closeDrawer() {\n if (!this.open) return;\n const id = this.activeTool;\n this.open = false;\n if (id) {\n this.dsToolChange.emit({ id, selected: false });\n }\n }\n\n /** Focus an active tool-header action by its application-owned id. */\n @Method()\n async focusHeaderAction(id: string) {\n const action = this.renderRoot.querySelector(`[data-header-action-id=\"${CSS.escape(id)}\"]`) as\n | (HTMLElement & { setFocus?: () => Promise<void> })\n | null;\n await action?.setFocus?.();\n }\n\n private focusRailAt(index: number) {\n const items = this.orderedRailItems;\n if (!items.length) return;\n const bounded = Math.max(0, Math.min(index, items.length - 1));\n if (bounded === this.rovingIndex) return;\n this.rovingIndex = bounded;\n const actions = Array.from(\n this.renderRoot.querySelectorAll<HTMLElement>('.panel-tools__rail-action .button-icon')\n );\n actions[bounded]?.focus({ preventScroll: true });\n }\n\n private handleRailKeyDown = (e: KeyboardEvent, index: number) => {\n const items = this.orderedRailItems;\n if (!items.length) return;\n\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n this.handleToolChange(items[index].id);\n return;\n }\n\n if (e.key === 'ArrowDown') {\n if (index >= items.length - 1) return;\n e.preventDefault();\n this.focusRailAt(index + 1);\n return;\n }\n\n if (e.key === 'ArrowUp') {\n if (index <= 0) return;\n e.preventDefault();\n this.focusRailAt(index - 1);\n }\n };\n\n private renderRailAction(item: PanelToolsItem, index: number) {\n const label =\n item.ariaLabel ??\n item.label ??\n PANEL_TOOLS_LABELS[item.id as keyof typeof PANEL_TOOLS_LABELS] ??\n item.id;\n return (\n <ds-tooltip\n key={item.id}\n label={label}\n shortcutKey={\n item.shortcutKey ??\n PANEL_TOOLS_SHORTCUTS[item.id as keyof typeof PANEL_TOOLS_SHORTCUTS]\n }\n side=\"left\"\n size=\"sm\"\n boundary=\"viewport\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n class=\"panel-tools__rail-action\"\n icon={item.icon}\n isActive={this.isRailSelected(item.id)}\n activeFill={false}\n hasBorder={false}\n pressScale={false}\n dot={item.dot ?? false}\n isInactive={item.isInactive}\n focusTabIndex={index === this.rovingIndex ? 0 : -1}\n aria-label={label}\n pressed={this.isRailSelected(item.id)}\n onFocusin={() => {\n this.rovingIndex = index;\n }}\n onKeyDown={(e: KeyboardEvent) => this.handleRailKeyDown(e, index)}\n onDsClick={() => this.handleToolChange(item.id)}\n />\n </ds-tooltip>\n );\n }\n\n render() {\n const headerLabel = this.headerLabel();\n const header = this.activeHeader();\n const headerTitle = header.title?.trim() || headerLabel;\n const headerActions = header.actions ?? [];\n const orderedRailItems = this.orderedRailItems;\n const headerItems = orderedRailItems.filter(\n item => panelToolsRailPlacement(item) === 'header'\n );\n const bodyItems = orderedRailItems.filter(\n item => panelToolsRailPlacement(item) === 'body'\n );\n const footerItems = orderedRailItems.filter(\n item => panelToolsRailPlacement(item) === 'footer'\n );\n const headerCount = headerItems.length;\n const footerStart = headerCount + bodyItems.length;\n const showDrawerChrome = this.isDrawerPresent();\n const drawerResting = panelToolsDrawerResting(this.open, this.motion);\n const activeFullView = Boolean(\n showDrawerChrome && this.activeTool && this.hasFullView(this.activeTool)\n );\n const toolIds = orderedRailItems.map(item => item.id);\n\n return (\n <Host\n class={{\n 'panel-tools': true,\n 'panel-tools--open': this.open,\n 'panel-tools--ready': this.readyForMotion,\n 'panel-tools--motion-opening': this.motion === 'opening',\n 'panel-tools--motion-closing': this.motion === 'closing',\n 'panel-tools--drawer-resting': drawerResting,\n 'panel-tools--fullscreen': this.presentation === 'fullscreen',\n 'panel-tools--presentation-snap': this.presentationMotionSuppressed,\n }}\n role={this.presentation === 'fullscreen' ? 'dialog' : 'complementary'}\n aria-modal={this.presentation === 'fullscreen' ? 'true' : undefined}\n aria-label={this.toolsLabel}\n >\n <div class=\"panel-tools__layout\">\n <nav class=\"panel-tools__rail\" aria-label={this.toolShortcutsLabel}>\n {headerItems.length ? (\n <div class=\"panel-tools__rail-header ds-chrome-row ds-chrome-space--md\">\n {headerItems.map((item, index) => this.renderRailAction(item, index))}\n </div>\n ) : null}\n <div\n class={{\n 'panel-tools__rail-body': true,\n 'ds-chrome-column': true,\n 'ds-chrome-space--md': true,\n 'ds-scrollbar-hidden': true,\n 'ds-focus-ring': this.railBodyScrollable,\n }}\n ref={element => {\n const next = (element as HTMLElement) ?? undefined;\n if (next === this.railBodyEl) return;\n this.railBodyEl = next;\n this.connectRailBodyObserver();\n }}\n role={this.railBodyScrollable ? 'region' : undefined}\n aria-label={this.railBodyScrollable ? this.toolShortcutsLabel : undefined}\n tabIndex={this.railBodyScrollable ? 0 : undefined}\n >\n <div class=\"panel-tools__rail-actions\">\n {bodyItems.map((item, bodyIdx) =>\n this.renderRailAction(item, headerCount + bodyIdx)\n )}\n </div>\n </div>\n {footerItems.length ? (\n <div class=\"panel-tools__rail-footer ds-chrome-row ds-chrome-space--md\">\n {footerItems.map((item, footerIdx) =>\n this.renderRailAction(item, footerStart + footerIdx)\n )}\n </div>\n ) : null}\n </nav>\n\n <div\n class={{\n 'panel-tools__drawer': true,\n 'panel-tools__drawer--visible': showDrawerChrome,\n }}\n aria-hidden={showDrawerChrome ? null : 'true'}\n inert={showDrawerChrome ? undefined : true}\n >\n <div class=\"panel-tools__drawer-surface\">\n {this.presentation !== 'fullscreen' || this.fullscreenHeaderMode === 'shared' ? (\n <ds-panel-tool-header\n class=\"panel-tools__header\"\n heading={headerTitle}\n showBack={header.showBack ?? false}\n backIcon={header.backIcon || 'ChevronLeft'}\n backAriaLabel={header.backAriaLabel || 'Back'}\n showMenu={false}\n ref={element => {\n const toolHeader = element as\n | (HTMLDsPanelToolHeaderElement & {\n actions: PanelToolsHeaderAction[];\n })\n | undefined;\n if (toolHeader) toolHeader.actions = headerActions;\n }}\n onDsBack={() => {\n if (this.activeTool) this.dsHeaderBack.emit({ tool: this.activeTool });\n }}\n />\n ) : null}\n <div class=\"panel-tools__body\" hidden={activeFullView}>\n {toolIds.map(id => {\n const active = this.isViewActive(id);\n return (\n <div\n class={{\n 'panel-tools__view': true,\n 'panel-tools__view--active': active,\n }}\n hidden={!active}\n >\n <slot name={id} />\n </div>\n );\n })}\n </div>\n <div class=\"panel-tools__full-views\" hidden={!activeFullView}>\n {toolIds.map(id => (\n <div\n class={{\n 'panel-tools__full-view': true,\n 'panel-tools__full-view--active':\n this.isViewActive(id) && this.hasFullView(id),\n }}\n hidden={!(this.isViewActive(id) && this.hasFullView(id))}\n >\n <slot name={`${id}-view`} />\n </div>\n ))}\n </div>\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
|
|
472
|
+
"content": "import {\n Component,\n Prop,\n Event,\n EventEmitter,\n Element,\n State,\n Watch,\n Method,\n Listen,\n h,\n Host,\n} from '@stencil/core';\nimport type { ChromeTransitionDetail } from '../../shell/chrome-transition';\nimport {\n PANEL_TOOLS_LABELS,\n PANEL_TOOLS_SHORTCUTS,\n type PanelToolsHeaderAction,\n type PanelToolsHeaderActionDetail,\n type PanelToolsHeaderConfig,\n type PanelToolsHeaders,\n type PanelToolsItem,\n type PanelToolsToolId,\n} from './panel-tools-types';\nimport {\n isPanelToolsToolId,\n orderPanelToolsItems,\n panelToolsDrawerAtTerminal,\n panelToolsDrawerResting,\n panelToolsDrawerTransitionMs,\n panelToolsRailPlacement,\n reconcilePanelToolsAvailability,\n resolvePanelToolActivation,\n} from './panel-tools-utils';\n\n@Component({\n tag: 'ds-panel-tools',\n styleUrl: 'PanelTools.css',\n shadow: true,\n})\nexport class PanelTools {\n @Element() el!: HTMLElement;\n\n /** When false, only the icon rail is shown. */\n @Prop({ mutable: true, reflect: true }) open: boolean = false;\n\n /** Drawer presentation or viewport-covering presentation for the active tool. */\n @Prop({ mutable: true, reflect: true }) presentation: 'drawer' | 'fullscreen' = 'drawer';\n\n /** Let a fullscreen product supply independent master/detail headers inside its view. */\n @Prop({ attribute: 'fullscreen-header-mode', reflect: true })\n fullscreenHeaderMode: 'shared' | 'split' = 'shared';\n\n /** Active tool view — `search`, `agents`, `messages`, `stacks`, `activity`, or `help`. */\n @Prop({ mutable: true, attribute: 'active-tool', reflect: true })\n activeTool: PanelToolsToolId | '' = '';\n\n /**\n * Rail items rendered in the right column.\n * Set via JS property: `el.items = [...]`. Replace the array reference to update.\n */\n @Prop() items: PanelToolsItem[] = [];\n\n @Prop() toolsLabel: string = 'Tools';\n @Prop() toolShortcutsLabel: string = 'Tool shortcuts';\n\n /** Active header state per tool. Replace the object when title, depth, or actions change. */\n @Prop() headers: PanelToolsHeaders = {};\n\n /**\n * Optional localStorage key for the last active tool. The drawer always starts\n * closed; only the tool identity is restored for continuity within this browser.\n */\n @Prop({ attribute: 'storage-key' }) storageKey: string = '';\n\n /** Emitted when a rail button is toggled. Detail = { id, selected }. */\n @Event({ bubbles: true, composed: true }) dsToolChange!: EventEmitter<{\n id: PanelToolsToolId;\n selected: boolean;\n }>;\n\n /** Emitted when fullscreen changes, including Escape-initiated exits. */\n @Event({ bubbles: true, composed: true }) dsPresentationChange!: EventEmitter<{\n presentation: 'drawer' | 'fullscreen';\n }>;\n\n /** Requests navigation to the active tool's parent view. */\n @Event({ bubbles: true, composed: true }) dsHeaderBack!: EventEmitter<{\n tool: PanelToolsToolId;\n }>;\n\n /** Requests one application-owned action from the active tool header. */\n @Event({ bubbles: true, composed: true })\n dsHeaderAction!: EventEmitter<PanelToolsHeaderActionDetail>;\n\n /** Bubbling lifecycle — `ds-bar-nav` defers overflow checks during drawer motion. */\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionStart!: EventEmitter<ChromeTransitionDetail>;\n\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionEnd!: EventEmitter<ChromeTransitionDetail>;\n\n /** Arms open vs close easing for the in-flight width transition. */\n @State() private motion: 'opening' | 'closing' | 'idle' = 'idle';\n\n /** Keeps header and slotted content painted until the closing clip transition completes. */\n @State() private drawerSurfaceRetained = false;\n\n /** Suppresses width transition until the host has painted its initial open state. */\n @State() private readyForMotion = false;\n\n /** Fullscreen presentation changes snap without borrowing drawer width motion. */\n @State() private presentationMotionSuppressed = false;\n\n @State() private rovingIndex = 0;\n @State() private fullViewToolIds: PanelToolsToolId[] = [];\n @State() private railBodyScrollable = false;\n\n private motionEnableGeneration = 0;\n private presentationMotionGeneration = 0;\n private transitionGeneration = 0;\n private transitionFallbackTimer: number | null = null;\n /** Ignores the old transition's terminal event when reversing direction. */\n private ignoreReplacementTerminal = false;\n private focusBeforeFullscreen: HTMLElement | null = null;\n private fullViewObserver: MutationObserver | null = null;\n private railBodyEl?: HTMLElement;\n private railBodyObserver?: ResizeObserver;\n\n private get railItems(): PanelToolsItem[] {\n return this.items ?? [];\n }\n\n private get orderedRailItems(): PanelToolsItem[] {\n return orderPanelToolsItems(this.railItems);\n }\n\n disconnectedCallback() {\n this.el.removeEventListener('transitionend', this.handleTransitionEnd);\n this.el.removeEventListener('transitioncancel', this.handleTransitionEnd);\n this.el.removeEventListener('dsAction', this.handleComposedHeaderAction);\n this.clearTransitionCompletion();\n this.motionEnableGeneration += 1;\n this.fullViewObserver?.disconnect();\n this.fullViewObserver = null;\n this.disconnectRailBodyObserver();\n }\n\n @Watch('presentation')\n presentationChanged(next: 'drawer' | 'fullscreen', previous?: 'drawer' | 'fullscreen') {\n if (next === previous) return;\n // Framework property bindings can update slotted master/detail content in\n // the same render that requests fullscreen. Reflect the host selector\n // immediately so that content cannot paint once in the drawer geometry\n // while Stencil waits to commit its next render.\n if (this.el.getAttribute('presentation') !== next) {\n this.el.setAttribute('presentation', next);\n }\n const presentationGeneration = ++this.presentationMotionGeneration;\n this.presentationMotionSuppressed = true;\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (presentationGeneration !== this.presentationMotionGeneration) return;\n this.presentationMotionSuppressed = false;\n });\n });\n if (next === 'fullscreen') {\n this.focusBeforeFullscreen =\n document.activeElement instanceof HTMLElement ? document.activeElement : null;\n this.open = true;\n } else if (previous === 'fullscreen') {\n const focusBeforeFullscreen = this.focusBeforeFullscreen;\n const fallbackActionId = this.activeHeader().actions?.[0]?.id;\n requestAnimationFrame(async () => {\n if (focusBeforeFullscreen?.isConnected) {\n focusBeforeFullscreen.focus({ preventScroll: true });\n }\n const focusRestored = focusBeforeFullscreen\n ? document.activeElement === focusBeforeFullscreen ||\n focusBeforeFullscreen.contains(document.activeElement)\n : false;\n if (!focusRestored && fallbackActionId) await this.focusHeaderAction(fallbackActionId);\n this.focusBeforeFullscreen = null;\n });\n }\n this.dsPresentationChange.emit({ presentation: next });\n }\n\n @Listen('keydown', { target: 'window', capture: true })\n handleFullscreenEscape(event: KeyboardEvent) {\n if (this.presentation !== 'fullscreen' || event.key !== 'Escape') return;\n event.preventDefault();\n event.stopPropagation();\n this.presentation = 'drawer';\n }\n\n private deferMotionEnable() {\n if (this.readyForMotion) return;\n const generation = ++this.motionEnableGeneration;\n requestAnimationFrame(() => {\n if (generation !== this.motionEnableGeneration) return;\n requestAnimationFrame(() => {\n if (generation !== this.motionEnableGeneration) return;\n this.readyForMotion = true;\n });\n });\n }\n\n connectedCallback() {\n this.el.addEventListener('transitionend', this.handleTransitionEnd);\n this.el.addEventListener('transitioncancel', this.handleTransitionEnd);\n this.el.addEventListener('dsAction', this.handleComposedHeaderAction);\n }\n\n private handleComposedHeaderAction = (event: Event) => {\n const detail = (event as CustomEvent<{ id?: string; anchor?: HTMLElement }>).detail;\n if (!this.activeTool || !detail?.id) return;\n event.stopPropagation();\n this.dsHeaderAction.emit({\n tool: this.activeTool,\n id: detail.id,\n anchor: detail.anchor,\n });\n };\n\n componentWillLoad() {\n this.restoreLastActiveTool();\n this.drawerSurfaceRetained = this.open;\n }\n\n @Watch('open')\n openChanged(isOpen: boolean, wasOpen?: boolean) {\n if (isOpen) this.drawerSurfaceRetained = true;\n if (this.readyForMotion && wasOpen !== undefined && wasOpen !== isOpen) {\n this.startDrawerTransition(isOpen ? 'opening' : 'closing');\n } else if (!isOpen) {\n this.drawerSurfaceRetained = false;\n }\n this.deferMotionEnable();\n }\n\n @Watch('activeTool')\n activeToolChanged() {\n this.persistLastActiveTool();\n this.deferMotionEnable();\n }\n\n @Watch('storageKey')\n storageKeyChanged() {\n this.restoreLastActiveTool();\n }\n\n componentDidLoad() {\n this.deferMotionEnable();\n this.connectRailBodyObserver();\n this.updateRailBodyScrollable();\n this.syncFullViewTools();\n this.fullViewObserver = new MutationObserver(() => this.syncFullViewTools());\n this.fullViewObserver.observe(this.el, {\n childList: true,\n attributes: true,\n attributeFilter: ['slot'],\n });\n }\n\n componentDidRender() {\n this.updateRailBodyScrollable();\n }\n\n private connectRailBodyObserver() {\n this.disconnectRailBodyObserver();\n if (!this.railBodyEl || typeof ResizeObserver === 'undefined') return;\n this.railBodyObserver = new ResizeObserver(() => this.updateRailBodyScrollable());\n this.railBodyObserver.observe(this.railBodyEl);\n }\n\n private disconnectRailBodyObserver() {\n this.railBodyObserver?.disconnect();\n this.railBodyObserver = undefined;\n }\n\n private updateRailBodyScrollable() {\n const next = Boolean(\n this.railBodyEl && this.railBodyEl.scrollHeight > this.railBodyEl.clientHeight + 1\n );\n if (next !== this.railBodyScrollable) this.railBodyScrollable = next;\n }\n\n @Watch('items')\n itemsChanged() {\n this.rovingIndex = 0;\n this.reconcileActiveTool();\n this.deferMotionEnable();\n }\n\n private restoreLastActiveTool() {\n if (!this.storageKey || this.activeTool) return;\n try {\n const stored = localStorage.getItem(this.storageKey);\n if (isPanelToolsToolId(stored)) this.activeTool = stored;\n } catch {\n /* localStorage unavailable */\n }\n }\n\n private persistLastActiveTool() {\n if (!this.storageKey || !this.activeTool) return;\n try {\n localStorage.setItem(this.storageKey, this.activeTool);\n } catch {\n /* unavailable */\n }\n }\n\n private clearPersistedTool() {\n if (!this.storageKey) return;\n try {\n localStorage.removeItem(this.storageKey);\n } catch {\n /* unavailable */\n }\n }\n\n private reconcileActiveTool() {\n const next = reconcilePanelToolsAvailability(this.railItems, this.open, this.activeTool);\n if (!next.removedTool) return;\n this.clearPersistedTool();\n this.open = next.open;\n this.activeTool = next.activeTool;\n this.dsToolChange.emit({ id: next.removedTool, selected: false });\n }\n\n private startDrawerTransition(phase: 'opening' | 'closing') {\n const drawer = this.renderRoot.querySelector('.panel-tools__drawer') as HTMLElement | null;\n if (this.motion !== 'idle') {\n this.finishDrawerTransition();\n this.ignoreReplacementTerminal = true;\n }\n this.motion = phase;\n this.dsChromeTransitionStart.emit({ source: 'panel-tools', phase });\n const generation = ++this.transitionGeneration;\n\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (generation !== this.transitionGeneration || this.motion !== phase) return;\n if (!drawer) {\n this.finishDrawerTransition();\n return;\n }\n\n if (this.drawerTransitionAtTerminal(drawer, phase)) {\n this.finishDrawerTransition();\n return;\n }\n\n const fallbackMs = panelToolsDrawerTransitionMs(getComputedStyle(drawer));\n if (fallbackMs <= 0) {\n this.finishDrawerTransition();\n return;\n }\n this.transitionFallbackTimer = window.setTimeout(() => {\n this.finishDrawerTransition();\n }, fallbackMs);\n });\n });\n }\n\n private handleTransitionEnd = (event: TransitionEvent) => {\n const drawer = this.renderRoot.querySelector('.panel-tools__drawer') as HTMLElement | null;\n if (event.target !== drawer) return;\n if (event.propertyName !== 'max-width') return;\n if (this.ignoreReplacementTerminal) {\n this.ignoreReplacementTerminal = false;\n return;\n }\n // Changing from the neutral transition recipe to the direction-specific\n // easing can cancel the transition that the reflected `open` class began.\n // The replacement transition and its watchdog still own completion; ending\n // motion here would hide the fixed-width tool surface while it is visible.\n if (event.type === 'transitioncancel') return;\n if (drawer && !this.drawerTransitionAtTerminal(drawer, this.motion)) return;\n this.finishDrawerTransition();\n };\n\n private drawerTransitionAtTerminal(\n drawer: HTMLElement,\n phase: 'opening' | 'closing' | 'idle'\n ): boolean {\n const width = drawer.getBoundingClientRect().width;\n const surface = drawer.querySelector('.panel-tools__drawer-surface') as HTMLElement | null;\n const targetWidth = surface?.getBoundingClientRect().width ?? 0;\n return panelToolsDrawerAtTerminal(width, targetWidth, phase);\n }\n\n private finishDrawerTransition() {\n const completedPhase = this.motion;\n const wasTransitioning = completedPhase !== 'idle';\n this.clearTransitionCompletion();\n if (!wasTransitioning) return;\n this.ignoreReplacementTerminal = false;\n this.motion = 'idle';\n if (completedPhase === 'closing' && !this.open) {\n this.drawerSurfaceRetained = false;\n }\n this.dsChromeTransitionEnd.emit({ source: 'panel-tools' });\n }\n\n private clearTransitionCompletion() {\n this.transitionGeneration += 1;\n if (this.transitionFallbackTimer !== null) {\n window.clearTimeout(this.transitionFallbackTimer);\n this.transitionFallbackTimer = null;\n }\n }\n\n /** Rail selection follows `open` immediately — independent of the slide animation. */\n private isRailSelected(id: PanelToolsToolId): boolean {\n return this.open && this.activeTool === id;\n }\n\n /** Drawer body stays mounted while the clip frame animates closed. */\n private isDrawerPresent(): boolean {\n return this.open || this.drawerSurfaceRetained;\n }\n\n private isViewActive(id: PanelToolsToolId): boolean {\n return this.isDrawerPresent() && this.activeTool === id;\n }\n\n private hasFullView(id: PanelToolsToolId): boolean {\n return this.fullViewToolIds.includes(id);\n }\n\n private syncFullViewTools() {\n const ids = this.orderedRailItems.map(item => item.id);\n const next = ids.filter(id => Boolean(this.el.querySelector(`[slot=\"${id}-view\"]`)));\n if (next.join('|') !== this.fullViewToolIds.join('|')) this.fullViewToolIds = next;\n }\n\n private get renderRoot(): ParentNode {\n return this.el.shadowRoot ?? this.el;\n }\n\n private headerLabel(): string {\n if (!this.isDrawerPresent() || !this.activeTool) return '';\n const item = this.railItems.find(candidate => candidate.id === this.activeTool);\n return (\n item?.label ??\n PANEL_TOOLS_LABELS[this.activeTool as keyof typeof PANEL_TOOLS_LABELS] ??\n this.activeTool\n );\n }\n\n private resolvedHeaders(): PanelToolsHeaders {\n return this.headers ?? {};\n }\n\n private activeHeader(): PanelToolsHeaderConfig {\n if (!this.activeTool) return {};\n return this.resolvedHeaders()[this.activeTool] ?? {};\n }\n\n private handleToolChange = (id: PanelToolsToolId) => {\n const next = resolvePanelToolActivation(this.open, this.activeTool, id);\n this.open = next.open;\n this.activeTool = next.activeTool;\n this.dsToolChange.emit({ id, selected: next.selected });\n };\n\n /** Toggle any rail tool open/closed — shell shortcuts K/A/S/M/N call this. */\n @Method()\n async activateTool(id: PanelToolsToolId) {\n const item = this.railItems.find(entry => entry.id === id);\n if (!item || item.isInactive) return;\n this.handleToolChange(id);\n }\n\n /** Close the tools drawer when open — used by shell keyboard shortcuts. */\n @Method()\n async closeDrawer() {\n if (!this.open) return;\n const id = this.activeTool;\n this.open = false;\n if (id) {\n this.dsToolChange.emit({ id, selected: false });\n }\n }\n\n /** Focus an active tool-header action by its application-owned id. */\n @Method()\n async focusHeaderAction(id: string) {\n const action = this.renderRoot.querySelector(`[data-header-action-id=\"${CSS.escape(id)}\"]`) as\n | (HTMLElement & { setFocus?: () => Promise<void> })\n | null;\n await action?.setFocus?.();\n }\n\n private focusRailAt(index: number) {\n const items = this.orderedRailItems;\n if (!items.length) return;\n const bounded = Math.max(0, Math.min(index, items.length - 1));\n if (bounded === this.rovingIndex) return;\n this.rovingIndex = bounded;\n const actions = Array.from(\n this.renderRoot.querySelectorAll<HTMLElement>('.panel-tools__rail-action .button-icon')\n );\n actions[bounded]?.focus({ preventScroll: true });\n }\n\n private handleRailKeyDown = (e: KeyboardEvent, index: number) => {\n const items = this.orderedRailItems;\n if (!items.length) return;\n\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n this.handleToolChange(items[index].id);\n return;\n }\n\n if (e.key === 'ArrowDown') {\n if (index >= items.length - 1) return;\n e.preventDefault();\n this.focusRailAt(index + 1);\n return;\n }\n\n if (e.key === 'ArrowUp') {\n if (index <= 0) return;\n e.preventDefault();\n this.focusRailAt(index - 1);\n }\n };\n\n private renderRailAction(item: PanelToolsItem, index: number) {\n const label =\n item.ariaLabel ??\n item.label ??\n PANEL_TOOLS_LABELS[item.id as keyof typeof PANEL_TOOLS_LABELS] ??\n item.id;\n return (\n <ds-tooltip\n key={item.id}\n label={label}\n shortcutKey={\n item.shortcutKey ?? PANEL_TOOLS_SHORTCUTS[item.id as keyof typeof PANEL_TOOLS_SHORTCUTS]\n }\n side=\"left\"\n size=\"sm\"\n boundary=\"viewport\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n class=\"panel-tools__rail-action\"\n icon={item.icon}\n isActive={this.isRailSelected(item.id)}\n activeFill={false}\n hasBorder={false}\n pressScale={false}\n dot={item.dot ?? false}\n isInactive={item.isInactive}\n focusTabIndex={index === this.rovingIndex ? 0 : -1}\n aria-label={label}\n pressed={this.isRailSelected(item.id)}\n onFocusin={() => {\n this.rovingIndex = index;\n }}\n onKeyDown={(e: KeyboardEvent) => this.handleRailKeyDown(e, index)}\n onDsClick={() => this.handleToolChange(item.id)}\n />\n </ds-tooltip>\n );\n }\n\n render() {\n const headerLabel = this.headerLabel();\n const header = this.activeHeader();\n const headerTitle = header.title?.trim() || headerLabel;\n const headerActions = header.actions ?? [];\n const orderedRailItems = this.orderedRailItems;\n const headerItems = orderedRailItems.filter(item => panelToolsRailPlacement(item) === 'header');\n const bodyItems = orderedRailItems.filter(item => panelToolsRailPlacement(item) === 'body');\n const footerItems = orderedRailItems.filter(item => panelToolsRailPlacement(item) === 'footer');\n const headerCount = headerItems.length;\n const footerStart = headerCount + bodyItems.length;\n const showDrawerChrome = this.isDrawerPresent();\n const drawerResting = panelToolsDrawerResting(this.open, this.motion);\n const activeFullView = Boolean(\n showDrawerChrome && this.activeTool && this.hasFullView(this.activeTool)\n );\n const toolIds = orderedRailItems.map(item => item.id);\n\n return (\n <Host\n class={{\n 'panel-tools': true,\n 'panel-tools--open': this.open,\n 'panel-tools--ready': this.readyForMotion,\n 'panel-tools--motion-opening': this.motion === 'opening',\n 'panel-tools--motion-closing': this.motion === 'closing',\n 'panel-tools--drawer-resting': drawerResting,\n 'panel-tools--fullscreen': this.presentation === 'fullscreen',\n 'panel-tools--presentation-snap': this.presentationMotionSuppressed,\n }}\n role={this.presentation === 'fullscreen' ? 'dialog' : 'complementary'}\n aria-modal={this.presentation === 'fullscreen' ? 'true' : undefined}\n aria-label={this.toolsLabel}\n >\n <div class=\"panel-tools__layout\">\n <nav class=\"panel-tools__rail\" aria-label={this.toolShortcutsLabel}>\n {headerItems.length ? (\n <div class=\"panel-tools__rail-header ds-chrome-row ds-chrome-space--md\">\n {headerItems.map((item, index) => this.renderRailAction(item, index))}\n </div>\n ) : null}\n <div\n class={{\n 'panel-tools__rail-body': true,\n 'ds-chrome-column': true,\n 'ds-chrome-space--md': true,\n 'ds-scrollbar-hidden': true,\n 'ds-focus-ring': this.railBodyScrollable,\n }}\n ref={element => {\n const next = (element as HTMLElement) ?? undefined;\n if (next === this.railBodyEl) return;\n this.railBodyEl = next;\n this.connectRailBodyObserver();\n }}\n role={this.railBodyScrollable ? 'region' : undefined}\n aria-label={this.railBodyScrollable ? this.toolShortcutsLabel : undefined}\n tabIndex={this.railBodyScrollable ? 0 : undefined}\n >\n <div class=\"panel-tools__rail-actions\">\n {bodyItems.map((item, bodyIdx) =>\n this.renderRailAction(item, headerCount + bodyIdx)\n )}\n </div>\n </div>\n {footerItems.length ? (\n <div class=\"panel-tools__rail-footer ds-chrome-row ds-chrome-space--md\">\n {footerItems.map((item, footerIdx) =>\n this.renderRailAction(item, footerStart + footerIdx)\n )}\n </div>\n ) : null}\n </nav>\n\n <div\n class={{\n 'panel-tools__drawer': true,\n 'panel-tools__drawer--visible': showDrawerChrome,\n }}\n aria-hidden={showDrawerChrome ? null : 'true'}\n inert={showDrawerChrome ? undefined : true}\n >\n <div class=\"panel-tools__drawer-surface\">\n {this.presentation !== 'fullscreen' || this.fullscreenHeaderMode === 'shared' ? (\n <ds-panel-tool-header\n class=\"panel-tools__header\"\n heading={headerTitle}\n showBack={header.showBack ?? false}\n backIcon={header.backIcon || 'ChevronLeft'}\n backAriaLabel={header.backAriaLabel || 'Back'}\n showMenu={false}\n ref={element => {\n const toolHeader = element as\n | (HTMLDsPanelToolHeaderElement & {\n actions: PanelToolsHeaderAction[];\n })\n | undefined;\n if (toolHeader) toolHeader.actions = headerActions;\n }}\n onDsBack={() => {\n if (this.activeTool) this.dsHeaderBack.emit({ tool: this.activeTool });\n }}\n />\n ) : null}\n <div class=\"panel-tools__body\" hidden={activeFullView}>\n {toolIds.map(id => {\n const active = this.isViewActive(id);\n return (\n <div\n class={{\n 'panel-tools__view': true,\n 'panel-tools__view--active': active,\n }}\n hidden={!active}\n >\n <slot name={id} />\n </div>\n );\n })}\n </div>\n <div class=\"panel-tools__full-views\" hidden={!activeFullView}>\n {toolIds.map(id => (\n <div\n class={{\n 'panel-tools__full-view': true,\n 'panel-tools__full-view--active':\n this.isViewActive(id) && this.hasFullView(id),\n }}\n hidden={!(this.isViewActive(id) && this.hasFullView(id))}\n >\n <slot name={`${id}-view`} />\n </div>\n ))}\n </div>\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
|
|
473
473
|
"type": "registry:ui"
|
|
474
474
|
},
|
|
475
475
|
{
|
|
@@ -479,7 +479,7 @@
|
|
|
479
479
|
},
|
|
480
480
|
{
|
|
481
481
|
"path": "src/wc/components/PanelTools/panel-tools-utils.ts",
|
|
482
|
-
"content": "import { parseCssTimeMs } from '../../utils/resolve-css-time-ms';\nimport {\n PANEL_TOOLS_PRIMARY_TOOL_ID,\n PANEL_TOOLS_TOOL_IDS,\n type PanelToolsItem,\n type PanelToolsToolId,\n} from './panel-tools-types';\n\nexport function orderPanelToolsItems(items: PanelToolsItem[]): PanelToolsItem[] {\n const firstById = new Map<PanelToolsToolId, PanelToolsItem>();\n for (const item of items) {\n if (item.id.trim() && !firstById.has(item.id)) firstById.set(item.id, item);\n }\n const regionOrder = { header: 0, body: 1, footer: 2 } as const;\n return Array.from(firstById.values()).sort((left, right) => {\n const leftRegion = panelToolsRailPlacement(left);\n const rightRegion = panelToolsRailPlacement(right);\n const regionDelta = regionOrder[leftRegion] - regionOrder[rightRegion];\n if (regionDelta !== 0) return regionDelta;\n\n const leftCanonical = PANEL_TOOLS_TOOL_IDS.findIndex(id => id === left.id);\n const rightCanonical = PANEL_TOOLS_TOOL_IDS.findIndex(id => id === right.id);\n const leftOrder = left.order ?? (leftCanonical < 0 ? Number.MAX_SAFE_INTEGER : leftCanonical);\n const rightOrder =\n right.order ?? (rightCanonical < 0 ? Number.MAX_SAFE_INTEGER : rightCanonical);\n return leftOrder - rightOrder;\n });\n}\n\nexport function isPanelToolsToolId(value: string | null): value is PanelToolsToolId {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\nexport function panelToolsRailPlacement(
|
|
482
|
+
"content": "import { parseCssTimeMs } from '../../utils/resolve-css-time-ms';\nimport {\n PANEL_TOOLS_PRIMARY_TOOL_ID,\n PANEL_TOOLS_TOOL_IDS,\n type PanelToolsItem,\n type PanelToolsToolId,\n} from './panel-tools-types';\n\nexport function orderPanelToolsItems(items: PanelToolsItem[]): PanelToolsItem[] {\n const firstById = new Map<PanelToolsToolId, PanelToolsItem>();\n for (const item of items) {\n if (item.id.trim() && !firstById.has(item.id)) firstById.set(item.id, item);\n }\n const regionOrder = { header: 0, body: 1, footer: 2 } as const;\n return Array.from(firstById.values()).sort((left, right) => {\n const leftRegion = panelToolsRailPlacement(left);\n const rightRegion = panelToolsRailPlacement(right);\n const regionDelta = regionOrder[leftRegion] - regionOrder[rightRegion];\n if (regionDelta !== 0) return regionDelta;\n\n const leftCanonical = PANEL_TOOLS_TOOL_IDS.findIndex(id => id === left.id);\n const rightCanonical = PANEL_TOOLS_TOOL_IDS.findIndex(id => id === right.id);\n const leftOrder = left.order ?? (leftCanonical < 0 ? Number.MAX_SAFE_INTEGER : leftCanonical);\n const rightOrder =\n right.order ?? (rightCanonical < 0 ? Number.MAX_SAFE_INTEGER : rightCanonical);\n return leftOrder - rightOrder;\n });\n}\n\nexport function isPanelToolsToolId(value: string | null): value is PanelToolsToolId {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\nexport function panelToolsRailPlacement(item: PanelToolsItem): 'header' | 'body' | 'footer' {\n if (item.railPlacement) return item.railPlacement;\n if (item.id === PANEL_TOOLS_PRIMARY_TOOL_ID) return 'header';\n if (item.id === 'help') return 'footer';\n return 'body';\n}\n\nexport function reconcilePanelToolsAvailability(\n items: PanelToolsItem[],\n open: boolean,\n activeTool: PanelToolsToolId | ''\n): { open: boolean; activeTool: PanelToolsToolId | ''; removedTool: PanelToolsToolId | '' } {\n if (!activeTool || items.some(item => item.id === activeTool)) {\n return { open, activeTool, removedTool: '' };\n }\n return { open: false, activeTool: '', removedTool: activeTool };\n}\n\nexport function shouldResyncPanelToolsItems(\n prev: PanelToolsItem[],\n next: PanelToolsItem[]\n): boolean {\n if (prev.length !== next.length) return true;\n return next.some((item, index) => {\n const prior = prev[index];\n if (!prior) return true;\n return (\n prior.id !== item.id ||\n prior.icon !== item.icon ||\n prior.label !== item.label ||\n prior.railPlacement !== item.railPlacement ||\n prior.order !== item.order ||\n prior.shortcutKey !== item.shortcutKey ||\n prior.mobileDestination !== item.mobileDestination ||\n prior.selected !== item.selected ||\n prior.dot !== item.dot ||\n prior.isInactive !== item.isInactive ||\n prior.ariaLabel !== item.ariaLabel\n );\n });\n}\n\nexport type PanelToolsMotion = 'opening' | 'closing' | 'idle';\n\nexport interface PanelToolsTransitionStyle {\n transitionProperty: string;\n transitionDuration: string;\n transitionDelay: string;\n}\n\n/** Maximum computed time for the drawer's max-width transition, including delay. */\nexport function panelToolsDrawerTransitionMs(style: PanelToolsTransitionStyle): number {\n const properties = style.transitionProperty.split(',').map(item => item.trim());\n const durations = style.transitionDuration.split(',').map(item => parseCssTimeMs(item.trim(), 0));\n const delays = style.transitionDelay.split(',').map(item => parseCssTimeMs(item.trim(), 0));\n\n return properties.reduce((max, property, index) => {\n if (property !== 'all' && property !== 'max-width') return max;\n const duration = durations[index % durations.length] ?? 0;\n const delay = delays[index % delays.length] ?? 0;\n return Math.max(max, duration + delay, 0);\n }, 0);\n}\n\n/** True when the drawer clip is fully closed — safe to skip painting slot content. */\nexport function panelToolsDrawerResting(open: boolean, motion: PanelToolsMotion): boolean {\n return !open && motion === 'idle';\n}\n\n/** True only when the animated clip has reached the terminal width for its motion phase. */\nexport function panelToolsDrawerAtTerminal(\n width: number,\n targetWidth: number,\n motion: PanelToolsMotion\n): boolean {\n if (motion === 'closing') return width <= 0.5;\n if (motion === 'opening') return targetWidth > 0 && width >= targetWidth - 0.5;\n return true;\n}\n\n/** Toggle or switch rail tool selection — repeat activation closes the active tool. */\nexport function resolvePanelToolActivation(\n open: boolean,\n activeTool: PanelToolsToolId | '',\n id: PanelToolsToolId\n): { open: boolean; activeTool: PanelToolsToolId; selected: boolean } {\n if (open && activeTool === id) {\n return { open: false, activeTool: id, selected: false };\n }\n return { open: true, activeTool: id, selected: true };\n}\n",
|
|
483
483
|
"type": "registry:ui"
|
|
484
484
|
}
|
|
485
485
|
]
|