@morscherlab/mint-sdk 1.4.0-beta.8 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/dist/{ActionMenu-CScINahU.js → AlertBox-DwgNTEqf.js} +40 -209
- package/dist/AlertBox-DwgNTEqf.js.map +1 -0
- package/dist/{AppSidebar-BmiiPfz9.js → AppSidebar-CZZaxni0.js} +1249 -239
- package/dist/AppSidebar-CZZaxni0.js.map +1 -0
- package/dist/{ConfirmDialog-CJ7hqYUP.js → ConfirmDialog-CjWoxAD1.js} +2 -2
- package/dist/{ConfirmDialog-CJ7hqYUP.js.map → ConfirmDialog-CjWoxAD1.js.map} +1 -1
- package/dist/ExperimentPopover-DujiuQIO.js +337 -0
- package/dist/ExperimentPopover-DujiuQIO.js.map +1 -0
- package/dist/{ExperimentSelectorModal-D-9z7cNx.js → ExperimentSelectorModal-B4j4eUOJ.js} +244 -211
- package/dist/ExperimentSelectorModal-B4j4eUOJ.js.map +1 -0
- package/dist/{SegmentedControl-DERBN-Xd.js → SegmentedControl-DyUPH3I5.js} +2 -2
- package/dist/{SegmentedControl-DERBN-Xd.js.map → SegmentedControl-DyUPH3I5.js.map} +1 -1
- package/dist/{SettingsModal-CSLE31yg.js → SettingsModal-D2MOOtuO.js} +28 -27
- package/dist/SettingsModal-D2MOOtuO.js.map +1 -0
- package/dist/__stories__/experiment-helpers.d.ts +10 -0
- package/dist/__tests__/components/AppTopBarOnboarding.test.d.ts +1 -0
- package/dist/__tests__/components/FloatingPanelsInModal.test.d.ts +1 -0
- package/dist/__tests__/composables/useFloatingPanel.test.d.ts +1 -0
- package/dist/__tests__/composables/useOnboarding.test.d.ts +1 -0
- package/dist/__tests__/styles/paletteTokens.test.d.ts +1 -0
- package/dist/__tests__/styles/v2AuditRules.test.d.ts +1 -0
- package/dist/components/AppSidebar.vue.d.ts +1 -1
- package/dist/components/AppTopBar.vue.d.ts +20 -5
- package/dist/components/ArtifactSaveDialog.vue.d.ts +9 -1
- package/dist/components/AuditTrail.vue.d.ts +1 -1
- package/dist/components/BaseButton.vue.d.ts +4 -2
- package/dist/components/BaseSelect.vue.d.ts +2 -2
- package/dist/components/ChartContainer.vue.d.ts +2 -0
- package/dist/components/DatePicker.vue.d.ts +1 -1
- package/dist/components/ExperimentPopover.vue.d.ts +18 -0
- package/dist/components/ExperimentSelectorModal.vue.d.ts +15 -5
- package/dist/components/FilePicker.vue.d.ts +1 -1
- package/dist/components/FileUploader.vue.d.ts +2 -2
- package/dist/components/JobsStatusTray.vue.d.ts +15 -4
- package/dist/components/PluginIcon.vue.d.ts +2 -2
- package/dist/components/PluginWorkspaceView.props.d.ts +3 -0
- package/dist/components/PluginWorkspaceView.vue.d.ts +3 -0
- package/dist/components/SampleSelector.vue.d.ts +6 -6
- package/dist/components/SequenceProgressBar.vue.d.ts +3 -0
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +3 -3
- package/dist/components/StepWizard.vue.d.ts +3 -0
- package/dist/components/internal/AppTopBarOnboardingInternal.vue.d.ts +7 -0
- package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -1
- package/dist/components/internal/AutoGroupModalInternal.vue.d.ts +3 -3
- package/dist/components/internal/DatePickerTimeInternal.vue.d.ts +1 -1
- package/dist/components/internal/OnboardingLayerInternal.vue.d.ts +7 -0
- package/dist/components/internal/ResourcePickerListInternal.vue.d.ts +2 -7
- package/dist/components/internal/SelectMultipleInternal.vue.d.ts +4 -0
- package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +8 -0
- package/dist/composables/createClient.d.ts +2 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/pluginIdentity.d.ts +24 -0
- package/dist/composables/pluginWorkspaceModel.d.ts +2 -2
- package/dist/composables/useAppExperiment.d.ts +8 -0
- package/dist/composables/useCalendarGrid.d.ts +1 -1
- package/dist/composables/useExperimentSelector.d.ts +6 -2
- package/dist/composables/useFileImport.d.ts +7 -0
- package/dist/composables/useFloatingPanel.d.ts +29 -0
- package/dist/composables/useJobCenter.d.ts +41 -3
- package/dist/composables/useOnboarding.d.ts +119 -0
- package/dist/composables/usePlatform.d.ts +1 -0
- package/dist/{controlSchemaModel-CP5-_nAj.js → controlSchemaModel-Bsg0t5Ab.js} +2 -2
- package/dist/{controlSchemaModel-CP5-_nAj.js.map → controlSchemaModel-Bsg0t5Ab.js.map} +1 -1
- package/dist/{data-C2PwI_dn.js → data-Boeh4uPI.js} +180 -125
- package/dist/data-Boeh4uPI.js.map +1 -0
- package/dist/data.d.ts +1 -1
- package/dist/data.js +9 -9
- package/dist/{experiment-43OJtH7R.js → experiment-DyO54FQB.js} +25 -61
- package/dist/experiment-DyO54FQB.js.map +1 -0
- package/dist/{experimentStatus-BZmU4czV.js → experimentStatus-DCn2QKG7.js} +3 -3
- package/dist/{experimentStatus-BZmU4czV.js.map → experimentStatus-DCn2QKG7.js.map} +1 -1
- package/dist/index.js +20 -20
- package/dist/{install-DAID6c2-.js → install-BEHp7Mo-.js} +15 -14
- package/dist/{install-DAID6c2-.js.map → install-BEHp7Mo-.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-DLwRl_Ql.js → lab-D61eaX6f.js} +136 -148
- package/dist/lab-D61eaX6f.js.map +1 -0
- package/dist/lab.js +4 -3
- package/dist/{records-BzTcxekG.js → records-D4aQXyZu.js} +647 -389
- package/dist/records-D4aQXyZu.js.map +1 -0
- package/dist/{runtime-D6gVat6r.js → runtime-q-R0CNLh.js} +417 -54
- package/dist/runtime-q-R0CNLh.js.map +1 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +7 -7
- package/dist/{shell-5ju97nOS.js → shell-6CGFvUUw.js} +10 -5
- package/dist/{shell-5ju97nOS.js.map → shell-6CGFvUUw.js.map} +1 -1
- package/dist/shell.d.ts +1 -0
- package/dist/shell.js +6 -6
- package/dist/stores/index.d.ts +1 -1
- package/dist/stores/index.js +2 -2
- package/dist/stores/settings.d.ts +4 -12
- package/dist/styles.css +2753 -2101
- package/dist/types/componentWorkflowTypes.d.ts +2 -0
- package/dist/types/components.d.ts +2 -0
- package/dist/types/generated/platformContext.d.ts +1 -0
- package/dist/types/generated-ui.d.ts +4 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/jobs.d.ts +9 -0
- package/dist/types/platform.d.ts +1 -1
- package/dist/{BaseModal-DfRIuVoj.js → useDropdownState-uFIz6Yv8.js} +75 -5
- package/dist/useDropdownState-uFIz6Yv8.js.map +1 -0
- package/dist/{useExperimentSamples-CxcnxZ8h.js → useExperimentSamples-QEFoq4tf.js} +48 -6
- package/dist/useExperimentSamples-QEFoq4tf.js.map +1 -0
- package/dist/{useListReorder-CcbEYxX-.js → useListReorder-DsuzwWGS.js} +219 -437
- package/dist/useListReorder-DsuzwWGS.js.map +1 -0
- package/dist/{useRackEditor-BD5NIo5s.js → useRackEditor-KpZ4iJ6o.js} +85 -81
- package/dist/useRackEditor-KpZ4iJ6o.js.map +1 -0
- package/dist/useRealtime-B58Lo-Tm.js +508 -0
- package/dist/useRealtime-B58Lo-Tm.js.map +1 -0
- package/dist/useRequest-BuDmW8Jc.js +215 -0
- package/dist/useRequest-BuDmW8Jc.js.map +1 -0
- package/dist/utils/time.d.ts +7 -0
- package/package.json +3 -2
- package/src/__stories__/experiment-helpers.ts +3 -3
- package/src/__tests__/components/ActionMenu.test.ts +2 -2
- package/src/__tests__/components/AppTopBar.test.ts +257 -3
- package/src/__tests__/components/AppTopBarOnboarding.test.ts +160 -0
- package/src/__tests__/components/AppTopBarPageSelector.test.ts +125 -9
- package/src/__tests__/components/AppTopBarPillNav.test.ts +35 -0
- package/src/__tests__/components/ArtifactSaveDialog.test.ts +78 -0
- package/src/__tests__/components/BaseButton.test.ts +19 -0
- package/src/__tests__/components/BaseModal.test.ts +13 -0
- package/src/__tests__/components/BaseSelect.multiple.test.ts +87 -32
- package/src/__tests__/components/BaseSelect.test.ts +2 -1
- package/src/__tests__/components/BatchProgressList.test.ts +16 -0
- package/src/__tests__/components/CalendarGridPanel.test.ts +2 -2
- package/src/__tests__/components/ChartContainer.test.ts +11 -0
- package/src/__tests__/components/ChemicalFormula.test.ts +6 -3
- package/src/__tests__/components/DatePicker.datetime.test.ts +6 -0
- package/src/__tests__/components/DatePicker.test.ts +21 -0
- package/src/__tests__/components/DatePicker.time.test.ts +2 -2
- package/src/__tests__/components/EmptyState.test.ts +1 -1
- package/src/__tests__/components/ExperimentPopover.test.ts +125 -16
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +153 -14
- package/src/__tests__/components/FloatingPanelsInModal.test.ts +94 -0
- package/src/__tests__/components/GeneratedPluginView.test.ts +23 -2
- package/src/__tests__/components/InstrumentStateBadge.test.ts +4 -4
- package/src/__tests__/components/JobsStatusTray.test.ts +373 -71
- package/src/__tests__/components/PluginWorkspaceView.test.ts +63 -1
- package/src/__tests__/components/ResourcePickerListInternal.test.ts +0 -20
- package/src/__tests__/components/SampleSelector.test.ts +33 -8
- package/src/__tests__/components/SequenceProgressBar.test.ts +12 -0
- package/src/__tests__/components/SettingsModal.test.ts +9 -2
- package/src/__tests__/components/StepWizard.test.ts +11 -0
- package/src/__tests__/components/WellPlate.decoration.test.ts +11 -4
- package/src/__tests__/components/deprecationTags.test.ts +1 -1
- package/src/__tests__/composables/useAppExperiment.test.ts +27 -1
- package/src/__tests__/composables/useCalendarGrid.test.ts +4 -2
- package/src/__tests__/composables/useExperimentSelector.test.ts +111 -0
- package/src/__tests__/composables/useFileImport.test.ts +36 -0
- package/src/__tests__/composables/useFloatingPanel.test.ts +68 -0
- package/src/__tests__/composables/useJobCenter.test.ts +262 -3
- package/src/__tests__/composables/useOnboarding.test.ts +317 -0
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +4 -3
- package/src/__tests__/fixtures/table-import/cells.xlsx +0 -0
- package/src/__tests__/fixtures/table-import/samples.xlsx +0 -0
- package/src/__tests__/stores/settings.test.ts +21 -21
- package/src/__tests__/styles/baseControlsRules.test.ts +6 -2
- package/src/__tests__/styles/controlRefreshRules.test.ts +0 -6
- package/src/__tests__/styles/cssTokens.test.ts +6 -3
- package/src/__tests__/styles/menusShellRefresh.test.ts +22 -12
- package/src/__tests__/styles/paletteTokens.test.ts +44 -0
- package/src/__tests__/styles/v2AuditRules.test.ts +104 -0
- package/src/__tests__/utils/plotlyTemplate.test.ts +4 -4
- package/src/__tests__/utils/time.test.ts +18 -1
- package/src/components/ActionMenu.vue +6 -37
- package/src/components/AdductText.story.vue +7 -0
- package/src/components/AdductText.vue +1 -1
- package/src/components/AlertBox.story.vue +2 -2
- package/src/components/AlertBox.vue +18 -16
- package/src/components/AppSidebar.story.vue +0 -28
- package/src/components/AppTopBar.onboarding.story.vue +69 -0
- package/src/components/AppTopBar.story.vue +105 -15
- package/src/components/AppTopBar.vue +248 -122
- package/src/components/ArtifactSaveDialog.story.vue +23 -0
- package/src/components/ArtifactSaveDialog.vue +102 -12
- package/src/components/AuditTrail.story.vue +2 -1
- package/src/components/AuditTrail.vue +2 -21
- package/src/components/Avatar.vue +10 -14
- package/src/components/BaseButton.vue +16 -2
- package/src/components/BaseModal.vue +2 -1
- package/src/components/BaseSelect.multiple.story.vue +13 -2
- package/src/components/BaseSelect.vue +4 -3
- package/src/components/BatchProgressList.vue +35 -25
- package/src/components/ChartContainer.story.vue +2 -1
- package/src/components/ChartContainer.vue +8 -6
- package/src/components/ConfirmDialog.story.vue +1 -1
- package/src/components/EmptyState.vue +1 -1
- package/src/components/ExperimentPopover.story.vue +165 -53
- package/src/components/ExperimentPopover.vue +169 -187
- package/src/components/ExperimentSelectorModal.story.vue +73 -31
- package/src/components/ExperimentSelectorModal.vue +144 -111
- package/src/components/FormField.story.vue +8 -48
- package/src/components/GeneratedPluginView.vue +11 -10
- package/src/components/GeneratedResultRenderer.vue +3 -4
- package/src/components/InstrumentStateBadge.vue +1 -1
- package/src/components/JobsStatusTray.story.vue +97 -43
- package/src/components/JobsStatusTray.vue +287 -290
- package/src/components/LayoutResizeHandle.story.vue +65 -4
- package/src/components/LoadingSpinner.vue +1 -1
- package/src/components/NumberInput.vue +3 -6
- package/src/components/PluginIcon.story.vue +2 -10
- package/src/components/PluginIcon.vue +2 -2
- package/src/components/PluginWorkspaceView.props.ts +3 -0
- package/src/components/PluginWorkspaceView.story.vue +1 -1
- package/src/components/PluginWorkspaceView.vue +17 -0
- package/src/components/RuntimeUpdateNotice.vue +1 -1
- package/src/components/SampleSelector.story.vue +22 -7
- package/src/components/SampleSelector.vue +34 -37
- package/src/components/SampleSelectorGroupNode.vue +1 -1
- package/src/components/SectionCard.story.vue +35 -2
- package/src/components/SequenceProgressBar.story.vue +9 -0
- package/src/components/SequenceProgressBar.vue +6 -1
- package/src/components/SettingsModal.schema.ts +1 -1
- package/src/components/SettingsModal.vue +9 -14
- package/src/components/SmartGroupFieldRecipe.story.vue +1 -1
- package/src/components/SmartGroupManual.story.vue +1 -1
- package/src/components/StepWizard.story.vue +33 -0
- package/src/components/StepWizard.vue +8 -1
- package/src/components/Tooltip.vue +9 -32
- package/src/components/WellPlate.story.vue +3 -1
- package/src/components/WellPlate.vue +7 -0
- package/src/components/internal/AppTopBarOnboardingInternal.vue +172 -0
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +140 -72
- package/src/components/internal/AppTopBarPillNavInternal.vue +33 -2
- package/src/components/internal/DatePickerDateInternal.vue +23 -85
- package/src/components/internal/DatePickerDateTimeInternal.vue +10 -14
- package/src/components/internal/DatePickerTimeInternal.vue +13 -9
- package/src/components/internal/NumberUnitInputInternal.vue +4 -18
- package/src/components/internal/OnboardingLayerInternal.vue +173 -0
- package/src/components/internal/ResourcePickerListInternal.vue +45 -85
- package/src/components/internal/SelectMultipleInternal.vue +187 -65
- package/src/components/internal/SelectSearchableInternal.vue +8 -4
- package/src/components/internal/WellEditPopupInternal.vue +11 -22
- package/src/components/internal/WorkspaceShellInternal.vue +12 -0
- package/src/composables/createClient.ts +2 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/pluginIdentity.ts +31 -0
- package/src/composables/pluginWorkspaceModel.ts +2 -2
- package/src/composables/useAppExperiment.ts +20 -0
- package/src/composables/useCalendarGrid.ts +4 -3
- package/src/composables/useExperimentSelector.ts +77 -31
- package/src/composables/useFileImport.ts +51 -1
- package/src/composables/useFloatingPanel.ts +107 -0
- package/src/composables/useJobCenter.ts +188 -6
- package/src/composables/useOnboarding.ts +465 -0
- package/src/composables/usePluginWorkspace.ts +1 -1
- package/src/data.ts +1 -0
- package/src/runtime.ts +2 -1
- package/src/shell.ts +17 -0
- package/src/stores/index.ts +0 -2
- package/src/stores/settings.ts +20 -50
- package/src/styles/components/action-menu.css +1 -1
- package/src/styles/components/adduct-text.css +13 -5
- package/src/styles/components/alert-box.css +39 -58
- package/src/styles/components/app-layout.css +10 -6
- package/src/styles/components/app-page-selector.css +121 -44
- package/src/styles/components/app-pill-nav.css +11 -12
- package/src/styles/components/app-sidebar.css +9 -18
- package/src/styles/components/app-top-bar.css +219 -55
- package/src/styles/components/artifact-save-dialog.css +153 -2
- package/src/styles/components/audit-trail.css +16 -19
- package/src/styles/components/avatar.css +6 -4
- package/src/styles/components/batch-progress-list.css +38 -36
- package/src/styles/components/button.css +36 -59
- package/src/styles/components/calendar.css +12 -12
- package/src/styles/components/chart-container.css +34 -23
- package/src/styles/components/checkbox.css +2 -12
- package/src/styles/components/collapsible-card.css +27 -29
- package/src/styles/components/confirm-dialog.css +13 -14
- package/src/styles/components/dataframe.css +13 -5
- package/src/styles/components/date-picker.css +84 -94
- package/src/styles/components/datetime-picker.css +35 -47
- package/src/styles/components/divider.css +20 -20
- package/src/styles/components/dose-calculator.css +3 -3
- package/src/styles/components/empty-state.css +20 -17
- package/src/styles/components/experiment-code-badge.css +2 -0
- package/src/styles/components/experiment-popover.css +174 -329
- package/src/styles/components/experiment-selector-modal.css +88 -56
- package/src/styles/components/file-picker.css +3 -3
- package/src/styles/components/file-uploader.css +38 -49
- package/src/styles/components/form-builder.css +3 -3
- package/src/styles/components/formula-input.css +1 -1
- package/src/styles/components/icon-button.css +18 -24
- package/src/styles/components/input.css +5 -4
- package/src/styles/components/instrument-monitor.css +13 -15
- package/src/styles/components/jobs-status-tray.css +140 -296
- package/src/styles/components/layout-resize-handle.css +59 -16
- package/src/styles/components/modal.css +5 -7
- package/src/styles/components/molecule-input.css +2 -2
- package/src/styles/components/multi-select.css +65 -117
- package/src/styles/components/number-input.css +26 -19
- package/src/styles/components/onboarding.css +368 -0
- package/src/styles/components/pill.css +3 -2
- package/src/styles/components/plate-editor.css +0 -1
- package/src/styles/components/plugin-icon.css +26 -23
- package/src/styles/components/popover.css +2 -2
- package/src/styles/components/presence-avatars.css +1 -1
- package/src/styles/components/progress-bar.css +3 -3
- package/src/styles/components/protocol-step-editor.css +1 -3
- package/src/styles/components/resource-card.css +2 -2
- package/src/styles/components/resource-picker-list.css +6 -57
- package/src/styles/components/sample-hierarchy-tree.css +4 -4
- package/src/styles/components/sample-selector.css +138 -79
- package/src/styles/components/schedule-calendar.css +8 -8
- package/src/styles/components/searchable-select.css +15 -19
- package/src/styles/components/section-card.css +54 -49
- package/src/styles/components/segmented-control.css +18 -15
- package/src/styles/components/select.css +3 -3
- package/src/styles/components/sequence-input.css +2 -2
- package/src/styles/components/sequence-progress-bar.css +17 -8
- package/src/styles/components/settings-modal.css +7 -2
- package/src/styles/components/smart-group.css +18 -18
- package/src/styles/components/step-wizard.css +52 -16
- package/src/styles/components/tabs.css +2 -2
- package/src/styles/components/textarea.css +3 -3
- package/src/styles/components/time-picker.css +37 -25
- package/src/styles/components/toast.css +13 -11
- package/src/styles/components/toggle.css +1 -11
- package/src/styles/components/tooltip.css +12 -11
- package/src/styles/components/unit-input.css +9 -8
- package/src/styles/components/well-edit-popup.css +2 -2
- package/src/styles/components/well-plate.css +9 -2
- package/src/styles/index.css +1 -0
- package/src/styles/variables.css +247 -78
- package/src/types/componentWorkflowTypes.ts +2 -0
- package/src/types/components.ts +2 -0
- package/src/types/generated/platformContext.ts +1 -0
- package/src/types/generated-ui.ts +4 -0
- package/src/types/index.ts +1 -0
- package/src/types/jobs.ts +10 -0
- package/src/types/platform.ts +1 -1
- package/src/utils/time.ts +21 -0
- package/dist/ActionMenu-CScINahU.js.map +0 -1
- package/dist/AppSidebar-BmiiPfz9.js.map +0 -1
- package/dist/BaseModal-DfRIuVoj.js.map +0 -1
- package/dist/ExperimentPopover-C4sGw-is.js +0 -359
- package/dist/ExperimentPopover-C4sGw-is.js.map +0 -1
- package/dist/ExperimentSelectorModal-D-9z7cNx.js.map +0 -1
- package/dist/SettingsModal-CSLE31yg.js.map +0 -1
- package/dist/data-C2PwI_dn.js.map +0 -1
- package/dist/experiment-43OJtH7R.js.map +0 -1
- package/dist/lab-DLwRl_Ql.js.map +0 -1
- package/dist/records-BzTcxekG.js.map +0 -1
- package/dist/runtime-D6gVat6r.js.map +0 -1
- package/dist/useExperimentSamples-CxcnxZ8h.js.map +0 -1
- package/dist/useListReorder-CcbEYxX-.js.map +0 -1
- package/dist/useRackEditor-BD5NIo5s.js.map +0 -1
- package/dist/useRealtime-PalKue84.js +0 -696
- package/dist/useRealtime-PalKue84.js.map +0 -1
- package/dist/useRequest-D5JDfsgZ.js +0 -76
- package/dist/useRequest-D5JDfsgZ.js.map +0 -1
|
@@ -21,7 +21,26 @@ const emit = defineEmits<{
|
|
|
21
21
|
const openItemId = ref<string | null>(null)
|
|
22
22
|
const itemRefs = ref<Map<string, HTMLElement>>(new Map())
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/** The bar shows this many items; the rest fold into a More dropdown. */
|
|
25
|
+
const MAX_VISIBLE_ITEMS = 4
|
|
26
|
+
const MORE_ITEM_ID = '__mint-pill-nav-more'
|
|
27
|
+
|
|
28
|
+
const allItems = computed<PillNavItem[]>(() => props.items.map(normalizePillNavItem))
|
|
29
|
+
|
|
30
|
+
const normalizedItems = computed<PillNavItem[]>(() => {
|
|
31
|
+
if (allItems.value.length <= MAX_VISIBLE_ITEMS) return allItems.value
|
|
32
|
+
const folded = allItems.value.slice(MAX_VISIBLE_ITEMS)
|
|
33
|
+
return [
|
|
34
|
+
...allItems.value.slice(0, MAX_VISIBLE_ITEMS),
|
|
35
|
+
{
|
|
36
|
+
id: MORE_ITEM_ID,
|
|
37
|
+
label: 'More',
|
|
38
|
+
dot: folded.some(item => item.dot),
|
|
39
|
+
// A folded group contributes its options; a plain item becomes one option.
|
|
40
|
+
children: folded.flatMap(item => item.children?.length ? item.children : [item]),
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
})
|
|
25
44
|
|
|
26
45
|
function handleClick(item: PillNavItem) {
|
|
27
46
|
if (item.disabled) return
|
|
@@ -35,8 +54,20 @@ function handleClick(item: PillNavItem) {
|
|
|
35
54
|
|
|
36
55
|
function handleOptionClick(option: PillNavOption, item: PillNavItem) {
|
|
37
56
|
if (option.disabled) return
|
|
38
|
-
if (!option.href && !option.to) emit('option-select', option, item)
|
|
39
57
|
openItemId.value = null
|
|
58
|
+
if (option.href || option.to) return
|
|
59
|
+
if (item.id !== MORE_ITEM_ID) {
|
|
60
|
+
emit('option-select', option, item)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
// A folded entry reports as it would from its own pill.
|
|
64
|
+
const folded = allItems.value.find(candidate => candidate.id === option.id && !candidate.children?.length)
|
|
65
|
+
if (folded) {
|
|
66
|
+
emit('select', folded)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const group = allItems.value.find(candidate => candidate.children?.some(child => child.id === option.id))
|
|
70
|
+
if (group) emit('option-select', option, group)
|
|
40
71
|
}
|
|
41
72
|
|
|
42
73
|
function normalizePillNavItem(item: PillNavItemInput): PillNavItem {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** DatePicker type="date": input that opens a calendar dropdown for selecting a single date, with clearable support. */
|
|
3
|
-
import {
|
|
4
|
-
import { ref, computed, nextTick, onBeforeUnmount, watch, useTemplateRef } from 'vue'
|
|
3
|
+
import { ref, computed, nextTick, watch, useTemplateRef } from 'vue'
|
|
5
4
|
import { useCalendarGrid, type CalendarGridDay } from '../../composables/useCalendarGrid'
|
|
6
5
|
import { useDropdownState } from '../../composables/useDropdownState'
|
|
6
|
+
import { useFloatingPanel } from '../../composables/useFloatingPanel'
|
|
7
7
|
import { useFocusTrap } from '../../composables/useFocusTrap'
|
|
8
8
|
import CalendarGridPanelInternal from './CalendarGridPanelInternal.vue'
|
|
9
9
|
|
|
@@ -30,11 +30,13 @@ const emit = defineEmits<{
|
|
|
30
30
|
}>()
|
|
31
31
|
|
|
32
32
|
const dropdownRef = ref<HTMLElement | null>(null)
|
|
33
|
-
const dropdownStyle = ref<Record<string, string>>({})
|
|
34
33
|
const { isOpen, triggerRef, open, close, toggle } = useDropdownState({
|
|
35
34
|
rootRef: useTemplateRef<HTMLElement>('container'),
|
|
36
35
|
insideRefs: [dropdownRef],
|
|
37
36
|
})
|
|
37
|
+
// floating-ui keeps the panel anchored (flip/shift) while the page scrolls,
|
|
38
|
+
// instead of the panel closing on every scroll or resize.
|
|
39
|
+
const { style: dropdownStyle } = useFloatingPanel(triggerRef, dropdownRef, isOpen)
|
|
38
40
|
// The panel is a non-modal dialog: Tab cycles inside it while it is open, and
|
|
39
41
|
// focus returns to the input when it closes.
|
|
40
42
|
useFocusTrap({ containerRef: dropdownRef, active: isOpen })
|
|
@@ -42,7 +44,6 @@ const {
|
|
|
42
44
|
weekDays,
|
|
43
45
|
monthYear,
|
|
44
46
|
calendarDays,
|
|
45
|
-
isDateDisabled,
|
|
46
47
|
formatDateValue,
|
|
47
48
|
setCurrentMonthFromDate,
|
|
48
49
|
prevMonth,
|
|
@@ -58,14 +59,7 @@ const selectedDate = computed(() => {
|
|
|
58
59
|
return isNaN(date.getTime()) ? null : date
|
|
59
60
|
})
|
|
60
61
|
|
|
61
|
-
const displayValue = computed(() =>
|
|
62
|
-
if (!selectedDate.value) return ''
|
|
63
|
-
return selectedDate.value.toLocaleDateString('en-US', {
|
|
64
|
-
year: 'numeric',
|
|
65
|
-
month: 'short',
|
|
66
|
-
day: 'numeric',
|
|
67
|
-
})
|
|
68
|
-
})
|
|
62
|
+
const displayValue = computed(() => (selectedDate.value ? formatDateValue(selectedDate.value) : ''))
|
|
69
63
|
|
|
70
64
|
function selectDate(day: CalendarGridDay) {
|
|
71
65
|
if (day.isDisabled || props.disabled) return
|
|
@@ -73,15 +67,6 @@ function selectDate(day: CalendarGridDay) {
|
|
|
73
67
|
close()
|
|
74
68
|
}
|
|
75
69
|
|
|
76
|
-
function goToToday() {
|
|
77
|
-
const today = new Date()
|
|
78
|
-
setCurrentMonthFromDate(today)
|
|
79
|
-
if (!isDateDisabled(today)) {
|
|
80
|
-
emit('update:modelValue', formatDateValue(today))
|
|
81
|
-
close()
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
70
|
function clear() {
|
|
86
71
|
emit('update:modelValue', undefined)
|
|
87
72
|
close()
|
|
@@ -100,41 +85,6 @@ function handleTriggerKeydown(event: KeyboardEvent) {
|
|
|
100
85
|
}
|
|
101
86
|
}
|
|
102
87
|
|
|
103
|
-
// floating-ui keeps the panel anchored (flip/shift) while the page scrolls,
|
|
104
|
-
// instead of the panel closing on every scroll or resize.
|
|
105
|
-
let stopAutoUpdate: (() => void) | null = null
|
|
106
|
-
|
|
107
|
-
async function updateDropdownPosition() {
|
|
108
|
-
const trigger = triggerRef.value
|
|
109
|
-
const panel = dropdownRef.value
|
|
110
|
-
if (!trigger || !panel) return
|
|
111
|
-
|
|
112
|
-
const { x, y } = await computePosition(trigger, panel, {
|
|
113
|
-
placement: 'bottom-start',
|
|
114
|
-
strategy: 'fixed',
|
|
115
|
-
middleware: [offset(4), flip(), shift({ padding: 8 })],
|
|
116
|
-
})
|
|
117
|
-
dropdownStyle.value = {
|
|
118
|
-
position: 'fixed',
|
|
119
|
-
top: `${y}px`,
|
|
120
|
-
left: `${x}px`,
|
|
121
|
-
zIndex: '9999',
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function stopPositioning() {
|
|
126
|
-
stopAutoUpdate?.()
|
|
127
|
-
stopAutoUpdate = null
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function startPositioning() {
|
|
131
|
-
const trigger = triggerRef.value
|
|
132
|
-
const panel = dropdownRef.value
|
|
133
|
-
if (!trigger || !panel) return
|
|
134
|
-
stopPositioning()
|
|
135
|
-
stopAutoUpdate = autoUpdate(trigger, panel, () => void updateDropdownPosition())
|
|
136
|
-
}
|
|
137
|
-
|
|
138
88
|
/** Moves focus into the calendar grid: selected day, else today, else the first day. */
|
|
139
89
|
function focusCalendar() {
|
|
140
90
|
const panel = dropdownRef.value
|
|
@@ -147,26 +97,25 @@ function focusCalendar() {
|
|
|
147
97
|
}
|
|
148
98
|
|
|
149
99
|
watch(isOpen, async (opened) => {
|
|
150
|
-
if (!opened)
|
|
151
|
-
stopPositioning()
|
|
152
|
-
return
|
|
153
|
-
}
|
|
100
|
+
if (!opened) return
|
|
154
101
|
if (selectedDate.value) {
|
|
155
102
|
setCurrentMonthFromDate(selectedDate.value)
|
|
156
103
|
}
|
|
157
104
|
await nextTick()
|
|
158
|
-
startPositioning()
|
|
159
105
|
// A second tick so this focus lands after the focus trap's initial focus.
|
|
160
106
|
await nextTick()
|
|
161
107
|
focusCalendar()
|
|
162
108
|
})
|
|
163
|
-
|
|
164
|
-
onBeforeUnmount(stopPositioning)
|
|
165
109
|
</script>
|
|
166
110
|
|
|
167
111
|
<template>
|
|
168
112
|
<div ref="container" class="mint-date-picker">
|
|
169
113
|
<div class="mint-date-picker__input-wrapper">
|
|
114
|
+
<div class="mint-date-picker__icon">
|
|
115
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
116
|
+
<path d="M8 2v4" /><path d="M16 2v4" /><rect width="18" height="18" x="3" y="4" rx="2" /><path d="M3 10h18" />
|
|
117
|
+
</svg>
|
|
118
|
+
</div>
|
|
170
119
|
<input
|
|
171
120
|
ref="triggerRef"
|
|
172
121
|
type="text"
|
|
@@ -185,11 +134,17 @@ onBeforeUnmount(stopPositioning)
|
|
|
185
134
|
@click="toggleCalendar"
|
|
186
135
|
@keydown="handleTriggerKeydown"
|
|
187
136
|
/>
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
137
|
+
<button
|
|
138
|
+
v-if="clearable && modelValue && !disabled"
|
|
139
|
+
type="button"
|
|
140
|
+
class="mint-date-picker__clear-btn"
|
|
141
|
+
aria-label="Clear date"
|
|
142
|
+
@click.stop="clear"
|
|
143
|
+
>
|
|
144
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
145
|
+
<path d="M18 6 6 18" /><path d="m6 6 12 12" />
|
|
191
146
|
</svg>
|
|
192
|
-
</
|
|
147
|
+
</button>
|
|
193
148
|
</div>
|
|
194
149
|
|
|
195
150
|
<Teleport to="body">
|
|
@@ -208,6 +163,7 @@ onBeforeUnmount(stopPositioning)
|
|
|
208
163
|
:style="dropdownStyle"
|
|
209
164
|
role="dialog"
|
|
210
165
|
aria-label="Date picker"
|
|
166
|
+
@keydown.escape.prevent="close(true)"
|
|
211
167
|
>
|
|
212
168
|
<CalendarGridPanelInternal
|
|
213
169
|
:week-days="weekDays"
|
|
@@ -218,24 +174,6 @@ onBeforeUnmount(stopPositioning)
|
|
|
218
174
|
@next-month="nextMonth"
|
|
219
175
|
@select="selectDate"
|
|
220
176
|
/>
|
|
221
|
-
|
|
222
|
-
<div class="mint-date-picker__footer">
|
|
223
|
-
<button
|
|
224
|
-
type="button"
|
|
225
|
-
class="mint-date-picker__footer-btn mint-date-picker__today-btn"
|
|
226
|
-
@click="goToToday"
|
|
227
|
-
>
|
|
228
|
-
Today
|
|
229
|
-
</button>
|
|
230
|
-
<button
|
|
231
|
-
v-if="clearable && modelValue"
|
|
232
|
-
type="button"
|
|
233
|
-
class="mint-date-picker__footer-btn mint-date-picker__clear-btn"
|
|
234
|
-
@click="clear"
|
|
235
|
-
>
|
|
236
|
-
Clear
|
|
237
|
-
</button>
|
|
238
|
-
</div>
|
|
239
177
|
</div>
|
|
240
178
|
</Transition>
|
|
241
179
|
</Teleport>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { computed, ref, watch, nextTick, useTemplateRef } from 'vue'
|
|
4
4
|
import { useCalendarGrid, type CalendarGridDay } from '../../composables/useCalendarGrid'
|
|
5
5
|
import { useDropdownState } from '../../composables/useDropdownState'
|
|
6
|
+
import { useFloatingPanel } from '../../composables/useFloatingPanel'
|
|
6
7
|
import { useFocusTrap } from '../../composables/useFocusTrap'
|
|
7
8
|
import { formatTime, formatTimeSlot, generateTimeSlots, toMinutes } from '../../utils/time'
|
|
8
9
|
import CalendarGridPanelInternal from './CalendarGridPanelInternal.vue'
|
|
@@ -37,10 +38,13 @@ const emit = defineEmits<{
|
|
|
37
38
|
}>()
|
|
38
39
|
|
|
39
40
|
const dropdownRef = ref<HTMLElement | null>(null)
|
|
41
|
+
const rootRef = useTemplateRef<HTMLElement>('root')
|
|
40
42
|
const { isOpen, open, close, toggle } = useDropdownState({
|
|
41
|
-
rootRef
|
|
43
|
+
rootRef,
|
|
42
44
|
triggerRef: useTemplateRef<HTMLElement>('trigger'),
|
|
43
45
|
})
|
|
46
|
+
// Fixed below the field, so a modal body's overflow does not clip it; it stays in place in the DOM.
|
|
47
|
+
const { style: dropdownStyle } = useFloatingPanel(rootRef, dropdownRef, isOpen)
|
|
44
48
|
// Non-modal dialog: Tab stays inside the panel, focus returns to the input on close.
|
|
45
49
|
useFocusTrap({ containerRef: dropdownRef, active: isOpen })
|
|
46
50
|
const {
|
|
@@ -76,17 +80,12 @@ const selectedTimeStr = computed(() => {
|
|
|
76
80
|
|
|
77
81
|
const displayValue = computed(() => {
|
|
78
82
|
if (!selectedDate.value) return ''
|
|
79
|
-
const dateStr = selectedDate.value.toLocaleDateString(props.locale, {
|
|
80
|
-
year: 'numeric',
|
|
81
|
-
month: 'short',
|
|
82
|
-
day: 'numeric',
|
|
83
|
-
})
|
|
84
83
|
const timeStr = formatTime(
|
|
85
84
|
selectedDate.value.getHours(),
|
|
86
85
|
selectedDate.value.getMinutes(),
|
|
87
86
|
props.timeFormat,
|
|
88
87
|
)
|
|
89
|
-
return `${
|
|
88
|
+
return `${formatDateValue(selectedDate.value)} ${timeStr}`
|
|
90
89
|
})
|
|
91
90
|
|
|
92
91
|
// Time slots
|
|
@@ -198,8 +197,8 @@ watch(isOpen, async (opened) => {
|
|
|
198
197
|
<div ref="root" class="mint-datetime-picker">
|
|
199
198
|
<div class="mint-datetime-picker__input-wrapper">
|
|
200
199
|
<div class="mint-datetime-picker__icon-calendar">
|
|
201
|
-
<svg fill="none" stroke="currentColor"
|
|
202
|
-
<path
|
|
200
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
201
|
+
<path d="M8 2v4" /><path d="M16 2v4" /><rect width="18" height="18" x="3" y="4" rx="2" /><path d="M3 10h18" />
|
|
203
202
|
</svg>
|
|
204
203
|
</div>
|
|
205
204
|
<input
|
|
@@ -221,11 +220,6 @@ watch(isOpen, async (opened) => {
|
|
|
221
220
|
@click="toggleDropdown"
|
|
222
221
|
@keydown="handleTriggerKeydown"
|
|
223
222
|
/>
|
|
224
|
-
<div class="mint-datetime-picker__icon-clock">
|
|
225
|
-
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
226
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
227
|
-
</svg>
|
|
228
|
-
</div>
|
|
229
223
|
</div>
|
|
230
224
|
|
|
231
225
|
<Transition
|
|
@@ -240,8 +234,10 @@ watch(isOpen, async (opened) => {
|
|
|
240
234
|
v-if="isOpen"
|
|
241
235
|
ref="dropdownRef"
|
|
242
236
|
class="mint-popover mint-datetime-picker__dropdown"
|
|
237
|
+
:style="dropdownStyle"
|
|
243
238
|
role="dialog"
|
|
244
239
|
aria-label="Date and time picker"
|
|
240
|
+
@keydown.escape.prevent="close(true)"
|
|
245
241
|
>
|
|
246
242
|
<!-- Calendar section -->
|
|
247
243
|
<div class="mint-datetime-picker__calendar-section">
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { ref, computed, watch, nextTick, useId, useTemplateRef } from 'vue'
|
|
4
4
|
import type { TimeSlotDisabledFn } from '../../types'
|
|
5
5
|
import { useDropdownState } from '../../composables/useDropdownState'
|
|
6
|
+
import { useFloatingPanel } from '../../composables/useFloatingPanel'
|
|
6
7
|
import {
|
|
7
8
|
compareTime,
|
|
8
9
|
findNearestTimeSlotIndex,
|
|
@@ -40,12 +41,16 @@ const emit = defineEmits<{
|
|
|
40
41
|
}>()
|
|
41
42
|
|
|
42
43
|
const listRef = ref<HTMLUListElement | null>(null)
|
|
44
|
+
const dropdownRef = ref<HTMLElement | null>(null)
|
|
43
45
|
const highlightedIndex = ref(-1)
|
|
44
46
|
const listboxId = `mint-time-picker-listbox-${useId().replace(/:/g, '')}`
|
|
47
|
+
const rootRef = useTemplateRef<HTMLElement>('root')
|
|
45
48
|
const { isOpen, open, close, toggle } = useDropdownState({
|
|
46
|
-
rootRef
|
|
49
|
+
rootRef,
|
|
47
50
|
triggerRef: useTemplateRef<HTMLElement>('trigger'),
|
|
48
51
|
})
|
|
52
|
+
// Fixed below the field and at least as wide, so a modal body's overflow does not clip it; it stays in place in the DOM.
|
|
53
|
+
const { style: dropdownStyle } = useFloatingPanel(rootRef, dropdownRef, isOpen, { matchWidth: true })
|
|
49
54
|
|
|
50
55
|
/** Id of the highlighted slot, published to the input as `aria-activedescendant`. */
|
|
51
56
|
const activeSlotId = computed(() =>
|
|
@@ -210,6 +215,11 @@ watch(isOpen, (open) => {
|
|
|
210
215
|
<template>
|
|
211
216
|
<div ref="root" class="mint-time-picker" @keydown="handleKeydown">
|
|
212
217
|
<div class="mint-time-picker__input-wrapper">
|
|
218
|
+
<div class="mint-time-picker__icon">
|
|
219
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
220
|
+
<circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" />
|
|
221
|
+
</svg>
|
|
222
|
+
</div>
|
|
213
223
|
<input
|
|
214
224
|
ref="trigger"
|
|
215
225
|
type="text"
|
|
@@ -241,11 +251,6 @@ watch(isOpen, (open) => {
|
|
|
241
251
|
<path d="M18 6 6 18" /><path d="m6 6 12 12" />
|
|
242
252
|
</svg>
|
|
243
253
|
</button>
|
|
244
|
-
<div class="mint-time-picker__icon">
|
|
245
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
246
|
-
<circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" />
|
|
247
|
-
</svg>
|
|
248
|
-
</div>
|
|
249
254
|
</div>
|
|
250
255
|
|
|
251
256
|
<Transition
|
|
@@ -258,7 +263,9 @@ watch(isOpen, (open) => {
|
|
|
258
263
|
>
|
|
259
264
|
<div
|
|
260
265
|
v-if="isOpen"
|
|
266
|
+
ref="dropdownRef"
|
|
261
267
|
class="mint-popover mint-time-picker__dropdown"
|
|
268
|
+
:style="dropdownStyle"
|
|
262
269
|
>
|
|
263
270
|
<ul
|
|
264
271
|
:id="listboxId"
|
|
@@ -284,9 +291,6 @@ watch(isOpen, (open) => {
|
|
|
284
291
|
>
|
|
285
292
|
<span class="mint-time-picker__slot-time">{{ formatTimeSlot(slot, format) }}</span>
|
|
286
293
|
<span v-if="slotReason(slot)" class="mint-time-picker__slot-reason">{{ slotReason(slot) }}</span>
|
|
287
|
-
<svg class="mint-menu-item__check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
288
|
-
<path d="M3.5 8.5l3 3 6-7" />
|
|
289
|
-
</svg>
|
|
290
294
|
</li>
|
|
291
295
|
</ul>
|
|
292
296
|
</div>
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* previews the value in each unit; units with a `factor` also get a hint in the neighbour unit
|
|
5
5
|
* nearest to 1–1000.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
|
|
7
|
+
import { computed, nextTick, ref, useTemplateRef } from 'vue'
|
|
9
8
|
import { useDropdownState } from '../../composables/useDropdownState'
|
|
9
|
+
import { useFloatingPanel } from '../../composables/useFloatingPanel'
|
|
10
10
|
import { useMenuKeyboard } from '../../composables/useMenuKeyboard'
|
|
11
11
|
import type { UnitOption } from '../../types'
|
|
12
12
|
|
|
@@ -148,27 +148,13 @@ function selectUnit(newUnitValue: string) {
|
|
|
148
148
|
|
|
149
149
|
// ---- Unit menu (disclosure + WAI-ARIA keyboard, shared with ActionMenu) ----
|
|
150
150
|
const menuRef = ref<HTMLElement | null>(null)
|
|
151
|
-
// Transparent (not visibility:hidden) until placed, so the open-time focus can land on an option.
|
|
152
|
-
const menuStyle = ref<Record<string, string>>({ opacity: '0' })
|
|
153
151
|
const { isOpen, rootRef, open, close } = useDropdownState({
|
|
154
152
|
triggerRef: useTemplateRef<HTMLElement>('trigger'),
|
|
155
|
-
closeOnScroll: true,
|
|
156
153
|
insideRefs: [menuRef],
|
|
157
|
-
onOpen: () => { menuStyle.value = { opacity: '0' } },
|
|
158
154
|
})
|
|
159
155
|
|
|
160
|
-
/** Teleported + fixed like ActionMenu, so overflow:hidden/auto ancestors never clip the menu. */
|
|
161
|
-
|
|
162
|
-
if (!opened) return
|
|
163
|
-
await nextTick()
|
|
164
|
-
if (!rootRef.value || !menuRef.value) return
|
|
165
|
-
const { x, y } = await computePosition(rootRef.value, menuRef.value, {
|
|
166
|
-
placement: 'bottom-end',
|
|
167
|
-
strategy: 'fixed',
|
|
168
|
-
middleware: [offset(4), flip(), shift({ padding: 8 })],
|
|
169
|
-
})
|
|
170
|
-
menuStyle.value = { left: `${x}px`, top: `${y}px` }
|
|
171
|
-
})
|
|
156
|
+
/** Teleported + fixed like ActionMenu, so overflow:hidden/auto ancestors never clip the menu; it follows the field on scroll. */
|
|
157
|
+
const { style: menuStyle } = useFloatingPanel(rootRef, menuRef, isOpen, { placement: 'bottom-end' })
|
|
172
158
|
|
|
173
159
|
function menuItems(): HTMLElement[] {
|
|
174
160
|
return menuRef.value ? Array.from(menuRef.value.querySelectorAll<HTMLElement>('[role="option"]')) : []
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The tour layer: a scrim with a hole over the anchor, the step card, and the
|
|
4
|
+
* what's-new dots. AppTopBar renders it when the app provides onboarding.
|
|
5
|
+
*/
|
|
6
|
+
import { computed, nextTick, onBeforeUnmount, ref, useId, watch } from 'vue'
|
|
7
|
+
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'
|
|
8
|
+
import { X } from 'lucide-vue-next'
|
|
9
|
+
import BaseButton from '../BaseButton.vue'
|
|
10
|
+
import { useEventListener } from '../../composables/useEventListener'
|
|
11
|
+
import type { OnboardingState } from '../../composables/useOnboarding'
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{ state: OnboardingState }>()
|
|
14
|
+
|
|
15
|
+
// Space between the anchor and the edge of the hole.
|
|
16
|
+
const HOLE_PAD = 4
|
|
17
|
+
|
|
18
|
+
const active = computed(() => props.state.active.value)
|
|
19
|
+
const card = ref<HTMLElement | null>(null)
|
|
20
|
+
const hole = ref<DOMRect | null>(null)
|
|
21
|
+
const cardPosition = ref({ x: 0, y: 0 })
|
|
22
|
+
const titleId = useId()
|
|
23
|
+
let stopAutoUpdate: (() => void) | undefined
|
|
24
|
+
|
|
25
|
+
async function position() {
|
|
26
|
+
const current = active.value
|
|
27
|
+
if (!current || !card.value) return
|
|
28
|
+
hole.value = current.element.getBoundingClientRect()
|
|
29
|
+
const { x, y } = await computePosition(current.element, card.value, {
|
|
30
|
+
strategy: 'fixed',
|
|
31
|
+
placement: current.step.placement ?? 'bottom',
|
|
32
|
+
middleware: [offset(12), flip(), shift({ padding: 12 })],
|
|
33
|
+
})
|
|
34
|
+
// The step may have changed while the position was computed.
|
|
35
|
+
if (active.value === current) cardPosition.value = { x, y }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Focus goes back where it was when the run ends.
|
|
39
|
+
let returnFocus: HTMLElement | null = null
|
|
40
|
+
watch(active, async (current, previous) => {
|
|
41
|
+
stopAutoUpdate?.()
|
|
42
|
+
stopAutoUpdate = undefined
|
|
43
|
+
hole.value = null
|
|
44
|
+
if (current && !previous) returnFocus = document.activeElement as HTMLElement | null
|
|
45
|
+
if (!current) {
|
|
46
|
+
if (returnFocus?.isConnected) returnFocus.focus()
|
|
47
|
+
returnFocus = null
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
await nextTick()
|
|
51
|
+
if (!card.value || active.value !== current) return
|
|
52
|
+
current.element.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
|
53
|
+
stopAutoUpdate = autoUpdate(current.element, card.value, () => void position())
|
|
54
|
+
// A click step: the keyboard user presses the control itself.
|
|
55
|
+
if (current.step.advanceOn === 'click') current.element.focus()
|
|
56
|
+
// Otherwise the card itself: no focus ring on load, and Tab reaches Back and Next.
|
|
57
|
+
else card.value.focus()
|
|
58
|
+
}, { immediate: true })
|
|
59
|
+
onBeforeUnmount(() => stopAutoUpdate?.())
|
|
60
|
+
|
|
61
|
+
// The layer is its own focus trap: Tab cycles over the card's buttons (and the
|
|
62
|
+
// anchor on a click step), never the page behind; from the card itself Tab goes
|
|
63
|
+
// to the first stop and Shift+Tab to the last.
|
|
64
|
+
useEventListener(document, 'keydown', (event) => {
|
|
65
|
+
const current = active.value
|
|
66
|
+
if (!current || !card.value) return
|
|
67
|
+
// Keys belong to the tour only while focus is on its card, its anchor, or nowhere.
|
|
68
|
+
const focused = document.activeElement
|
|
69
|
+
if (focused && focused !== document.body && !card.value.contains(focused) && focused !== current.element) return
|
|
70
|
+
const onCard = focused === card.value
|
|
71
|
+
if (event.key === 'Escape') {
|
|
72
|
+
event.stopPropagation()
|
|
73
|
+
props.state.dismiss()
|
|
74
|
+
} else if (event.key === 'Tab') {
|
|
75
|
+
const buttons = [...card.value.querySelectorAll<HTMLElement>('button')]
|
|
76
|
+
const stops = current.step.advanceOn === 'click' ? [current.element, ...buttons] : buttons
|
|
77
|
+
const at = stops.indexOf(document.activeElement as HTMLElement)
|
|
78
|
+
const target = at === -1 ? (event.shiftKey ? stops.length - 1 : 0) : (at + (event.shiftKey ? -1 : 1) + stops.length) % stops.length
|
|
79
|
+
event.preventDefault()
|
|
80
|
+
stops[target].focus()
|
|
81
|
+
} else if (event.key === 'Enter' && onCard && current.step.advanceOn !== 'click') {
|
|
82
|
+
event.preventDefault()
|
|
83
|
+
props.state.next()
|
|
84
|
+
}
|
|
85
|
+
}, { capture: true })
|
|
86
|
+
|
|
87
|
+
const holeStyle = computed(() => hole.value && {
|
|
88
|
+
left: `${hole.value.left - HOLE_PAD}px`,
|
|
89
|
+
top: `${hole.value.top - HOLE_PAD}px`,
|
|
90
|
+
width: `${hole.value.width + 2 * HOLE_PAD}px`,
|
|
91
|
+
height: `${hole.value.height + 2 * HOLE_PAD}px`,
|
|
92
|
+
})
|
|
93
|
+
// advanceOn 'click': cut the hole out of the click blocker, so the anchor takes the click.
|
|
94
|
+
const blockerStyle = computed(() => {
|
|
95
|
+
const rect = hole.value
|
|
96
|
+
if (!rect || active.value?.step.advanceOn !== 'click') return undefined
|
|
97
|
+
const [x, y, w, h] = [rect.left - HOLE_PAD, rect.top - HOLE_PAD, rect.width + 2 * HOLE_PAD, rect.height + 2 * HOLE_PAD]
|
|
98
|
+
return { clipPath: `path(evenodd, 'M0 0H${window.innerWidth}V${window.innerHeight}H0Z M${x} ${y}h${w}v${h}h${-w}Z')` }
|
|
99
|
+
})
|
|
100
|
+
const isLast = computed(() => !!active.value && active.value.index === active.value.count - 1)
|
|
101
|
+
const nextLabel = computed(() => active.value?.news ? 'Got it' : isLast.value ? 'Done' : 'Next')
|
|
102
|
+
|
|
103
|
+
// What's-new dots: one on each element with unseen news, hidden while a card shows.
|
|
104
|
+
const dots = ref<Array<{ id: string; title: string; x: number; y: number }>>([])
|
|
105
|
+
let frame = 0
|
|
106
|
+
function placeDots() {
|
|
107
|
+
cancelAnimationFrame(frame)
|
|
108
|
+
frame = requestAnimationFrame(() => {
|
|
109
|
+
dots.value = active.value ? [] : props.state.news.value.flatMap((item) => {
|
|
110
|
+
if (!item.unseen) return []
|
|
111
|
+
const element = document.querySelector(`[data-onboarding="${CSS.escape(item.anchor)}"]`)
|
|
112
|
+
if (!element) return []
|
|
113
|
+
const rect = element.getBoundingClientRect()
|
|
114
|
+
return rect.width ? [{ id: item.id, title: item.title, x: rect.right, y: rect.top }] : []
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
const observer = new MutationObserver(placeDots)
|
|
119
|
+
watch(() => props.state.unseenNews.value > 0, (watching) => {
|
|
120
|
+
observer.disconnect()
|
|
121
|
+
if (watching) observer.observe(document.body, { childList: true, subtree: true })
|
|
122
|
+
placeDots()
|
|
123
|
+
}, { immediate: true })
|
|
124
|
+
watch(active, placeDots)
|
|
125
|
+
useEventListener(window, 'resize', placeDots)
|
|
126
|
+
useEventListener(window, 'scroll', placeDots, { capture: true, passive: true })
|
|
127
|
+
onBeforeUnmount(() => {
|
|
128
|
+
observer.disconnect()
|
|
129
|
+
cancelAnimationFrame(frame)
|
|
130
|
+
})
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<template>
|
|
134
|
+
<Teleport to="body">
|
|
135
|
+
<button
|
|
136
|
+
v-for="dot in dots"
|
|
137
|
+
:key="dot.id"
|
|
138
|
+
type="button"
|
|
139
|
+
class="mint-onboarding__dot"
|
|
140
|
+
:style="{ left: `${dot.x}px`, top: `${dot.y}px` }"
|
|
141
|
+
:aria-label="`New: ${dot.title}`"
|
|
142
|
+
@click="state.showNews(dot.id)"
|
|
143
|
+
/>
|
|
144
|
+
<div v-if="active" class="mint-onboarding">
|
|
145
|
+
<div class="mint-onboarding__blocker" :style="blockerStyle" />
|
|
146
|
+
<div v-if="holeStyle" class="mint-onboarding__hole" :style="holeStyle" />
|
|
147
|
+
<div
|
|
148
|
+
ref="card"
|
|
149
|
+
class="mint-onboarding__card"
|
|
150
|
+
role="dialog"
|
|
151
|
+
aria-modal="true"
|
|
152
|
+
:aria-labelledby="titleId"
|
|
153
|
+
tabindex="-1"
|
|
154
|
+
:style="{ left: `${cardPosition.x}px`, top: `${cardPosition.y}px` }"
|
|
155
|
+
>
|
|
156
|
+
<button type="button" class="mint-onboarding__close" :aria-label="active.news ? 'Close' : 'Close tour'" @click="state.dismiss()">
|
|
157
|
+
<X :size="14" aria-hidden="true" />
|
|
158
|
+
</button>
|
|
159
|
+
<span v-if="active.count > 1" class="mint-onboarding__count">
|
|
160
|
+
<span aria-hidden="true">{{ active.index + 1 }} / {{ active.count }}</span>
|
|
161
|
+
<span class="mint-onboarding__sr">Step {{ active.index + 1 }} of {{ active.count }}</span>
|
|
162
|
+
</span>
|
|
163
|
+
<p :id="titleId" class="mint-onboarding__title">{{ active.step.title }}</p>
|
|
164
|
+
<p class="mint-onboarding__body">{{ active.step.body }}</p>
|
|
165
|
+
<div class="mint-onboarding__footer">
|
|
166
|
+
<span v-if="active.step.advanceOn === 'click'" class="mint-onboarding__hint">Click the highlighted control.</span>
|
|
167
|
+
<BaseButton v-if="active.index > 0" variant="ghost" size="sm" @click="state.back()">Back</BaseButton>
|
|
168
|
+
<BaseButton v-if="active.step.advanceOn !== 'click'" size="sm" @click="state.next()">{{ nextLabel }}</BaseButton>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</Teleport>
|
|
173
|
+
</template>
|