@morscherlab/mint-sdk 1.4.0-beta.9 → 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/{AlertBox-B2Bgr3ei.js → AlertBox-DwgNTEqf.js} +3 -3
- package/dist/{AlertBox-B2Bgr3ei.js.map → AlertBox-DwgNTEqf.js.map} +1 -1
- package/dist/{AppSidebar-Dq9W1jE2.js → AppSidebar-CZZaxni0.js} +1103 -144
- package/dist/AppSidebar-CZZaxni0.js.map +1 -0
- package/dist/{ConfirmDialog-nbJj51is.js → ConfirmDialog-CjWoxAD1.js} +2 -2
- package/dist/{ConfirmDialog-nbJj51is.js.map → ConfirmDialog-CjWoxAD1.js.map} +1 -1
- package/dist/{ExperimentPopover-5T1fAq2L.js → ExperimentPopover-DujiuQIO.js} +5 -5
- package/dist/{ExperimentPopover-5T1fAq2L.js.map → ExperimentPopover-DujiuQIO.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-65peG4Up.js → ExperimentSelectorModal-B4j4eUOJ.js} +114 -98
- package/dist/ExperimentSelectorModal-B4j4eUOJ.js.map +1 -0
- package/dist/{SegmentedControl-BtnnjWzs.js → SegmentedControl-DyUPH3I5.js} +2 -2
- package/dist/{SegmentedControl-BtnnjWzs.js.map → SegmentedControl-DyUPH3I5.js.map} +1 -1
- package/dist/{SettingsModal-483nyI6b.js → SettingsModal-D2MOOtuO.js} +26 -25
- 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/AppTopBar.vue.d.ts +20 -5
- package/dist/components/BaseButton.vue.d.ts +4 -2
- package/dist/components/ExperimentSelectorModal.vue.d.ts +15 -1
- package/dist/components/JobsStatusTray.vue.d.ts +15 -4
- package/dist/components/PluginWorkspaceView.props.d.ts +3 -0
- package/dist/components/PluginWorkspaceView.vue.d.ts +3 -0
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +3 -3
- 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 +2 -2
- package/dist/components/internal/OnboardingLayerInternal.vue.d.ts +7 -0
- package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +8 -0
- package/dist/composables/pluginWorkspaceModel.d.ts +2 -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/{controlSchemaModel-B8CTXagP.js → controlSchemaModel-Bsg0t5Ab.js} +2 -2
- package/dist/{controlSchemaModel-B8CTXagP.js.map → controlSchemaModel-Bsg0t5Ab.js.map} +1 -1
- package/dist/{data-CgILxDLW.js → data-Boeh4uPI.js} +13 -12
- package/dist/{data-CgILxDLW.js.map → data-Boeh4uPI.js.map} +1 -1
- 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-Cqp6qNrR.js → experimentStatus-DCn2QKG7.js} +2 -2
- package/dist/{experimentStatus-Cqp6qNrR.js.map → experimentStatus-DCn2QKG7.js.map} +1 -1
- package/dist/index.js +20 -20
- package/dist/{install-BtLWTk6H.js → install-BEHp7Mo-.js} +15 -15
- package/dist/{install-BtLWTk6H.js.map → install-BEHp7Mo-.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-MHzp-_J8.js → lab-D61eaX6f.js} +68 -61
- package/dist/{lab-MHzp-_J8.js.map → lab-D61eaX6f.js.map} +1 -1
- package/dist/lab.js +4 -4
- package/dist/{records-B6gpM_3e.js → records-D4aQXyZu.js} +193 -133
- package/dist/records-D4aQXyZu.js.map +1 -0
- package/dist/{runtime-7vfl_bI3.js → runtime-q-R0CNLh.js} +400 -33
- 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-Cs9OJMnm.js → shell-6CGFvUUw.js} +10 -5
- package/dist/{shell-Cs9OJMnm.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 +1191 -479
- package/dist/types/components.d.ts +2 -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/{useDropdownState-8yWMu-7_.js → useDropdownState-uFIz6Yv8.js} +16 -4
- package/dist/useDropdownState-uFIz6Yv8.js.map +1 -0
- package/dist/{useExperimentSamples-DHnxKFAF.js → useExperimentSamples-QEFoq4tf.js} +48 -6
- package/dist/useExperimentSamples-QEFoq4tf.js.map +1 -0
- package/dist/{useListReorder-BitdUhxn.js → useListReorder-DsuzwWGS.js} +122 -172
- package/dist/useListReorder-DsuzwWGS.js.map +1 -0
- package/dist/{useRackEditor-DiwCt77M.js → useRackEditor-KpZ4iJ6o.js} +22 -32
- 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-D506eHi8.js → useRequest-BuDmW8Jc.js} +8 -37
- package/dist/useRequest-BuDmW8Jc.js.map +1 -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 +206 -1
- package/src/__tests__/components/AppTopBarOnboarding.test.ts +160 -0
- package/src/__tests__/components/AppTopBarPageSelector.test.ts +125 -9
- package/src/__tests__/components/BaseButton.test.ts +19 -0
- package/src/__tests__/components/BaseModal.test.ts +13 -0
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +34 -1
- package/src/__tests__/components/FloatingPanelsInModal.test.ts +94 -0
- package/src/__tests__/components/GeneratedPluginView.test.ts +1 -1
- package/src/__tests__/components/JobsStatusTray.test.ts +373 -71
- package/src/__tests__/components/PluginWorkspaceView.test.ts +63 -1
- package/src/__tests__/components/SampleSelector.test.ts +20 -0
- package/src/__tests__/components/SettingsModal.test.ts +9 -2
- 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/cssTokens.test.ts +4 -1
- package/src/__tests__/styles/menusShellRefresh.test.ts +8 -2
- package/src/__tests__/styles/paletteTokens.test.ts +44 -0
- package/src/__tests__/styles/v2AuditRules.test.ts +104 -0
- package/src/components/ActionMenu.vue +6 -37
- package/src/components/AppTopBar.onboarding.story.vue +69 -0
- package/src/components/AppTopBar.story.vue +59 -4
- package/src/components/AppTopBar.vue +132 -10
- package/src/components/BaseButton.vue +16 -2
- package/src/components/BaseModal.vue +2 -1
- package/src/components/ExperimentSelectorModal.story.vue +38 -0
- package/src/components/ExperimentSelectorModal.vue +20 -1
- package/src/components/GeneratedPluginView.vue +4 -9
- package/src/components/GeneratedResultRenderer.vue +3 -4
- 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/PluginWorkspaceView.props.ts +3 -0
- package/src/components/PluginWorkspaceView.vue +17 -0
- package/src/components/SampleSelector.story.vue +21 -6
- package/src/components/SampleSelector.vue +10 -3
- package/src/components/SectionCard.story.vue +33 -0
- package/src/components/SettingsModal.vue +7 -12
- package/src/components/Tooltip.vue +9 -32
- package/src/components/internal/AppTopBarOnboardingInternal.vue +172 -0
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +140 -72
- package/src/components/internal/DatePickerDateInternal.vue +7 -45
- package/src/components/internal/DatePickerDateTimeInternal.vue +7 -1
- package/src/components/internal/DatePickerTimeInternal.vue +8 -1
- package/src/components/internal/NumberUnitInputInternal.vue +4 -18
- package/src/components/internal/OnboardingLayerInternal.vue +173 -0
- package/src/components/internal/SelectMultipleInternal.vue +6 -1
- package/src/components/internal/SelectSearchableInternal.vue +6 -1
- package/src/components/internal/WellEditPopupInternal.vue +11 -22
- package/src/components/internal/WorkspaceShellInternal.vue +12 -0
- package/src/composables/pluginWorkspaceModel.ts +2 -2
- 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/app-layout.css +6 -3
- package/src/styles/components/app-page-selector.css +120 -45
- package/src/styles/components/app-top-bar.css +165 -3
- package/src/styles/components/button.css +8 -0
- package/src/styles/components/dataframe.css +1 -1
- package/src/styles/components/date-picker.css +4 -2
- package/src/styles/components/datetime-picker.css +4 -4
- package/src/styles/components/experiment-selector-modal.css +13 -0
- package/src/styles/components/jobs-status-tray.css +134 -296
- package/src/styles/components/layout-resize-handle.css +59 -16
- package/src/styles/components/modal.css +3 -3
- package/src/styles/components/number-input.css +1 -1
- package/src/styles/components/onboarding.css +368 -0
- package/src/styles/components/progress-bar.css +3 -3
- package/src/styles/components/sample-selector.css +43 -0
- package/src/styles/components/searchable-select.css +3 -7
- package/src/styles/components/section-card.css +30 -6
- package/src/styles/components/settings-modal.css +7 -2
- package/src/styles/components/time-picker.css +3 -6
- package/src/styles/components/toast.css +1 -1
- package/src/styles/components/tooltip.css +2 -2
- package/src/styles/components/unit-input.css +1 -1
- package/src/styles/components/well-edit-popup.css +2 -2
- package/src/styles/index.css +1 -0
- package/src/styles/variables.css +153 -15
- package/src/types/components.ts +2 -0
- package/src/types/index.ts +1 -0
- package/src/types/jobs.ts +10 -0
- package/src/types/platform.ts +1 -1
- package/dist/AppSidebar-Dq9W1jE2.js.map +0 -1
- package/dist/ExperimentSelectorModal-65peG4Up.js.map +0 -1
- package/dist/SettingsModal-483nyI6b.js.map +0 -1
- package/dist/experiment-43OJtH7R.js.map +0 -1
- package/dist/records-B6gpM_3e.js.map +0 -1
- package/dist/runtime-7vfl_bI3.js.map +0 -1
- package/dist/useDropdownState-8yWMu-7_.js.map +0 -1
- package/dist/useExperimentSamples-DHnxKFAF.js.map +0 -1
- package/dist/useListReorder-BitdUhxn.js.map +0 -1
- package/dist/useRackEditor-DiwCt77M.js.map +0 -1
- package/dist/useRealtime-C3iy9QkU.js +0 -699
- package/dist/useRealtime-C3iy9QkU.js.map +0 -1
- package/dist/useRequest-D506eHi8.js.map +0 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
import { computed, useTemplateRef } from 'vue'
|
|
2
|
+
/** "Plugin / page" switcher: the plugin name, then a trigger that opens the page menu (hints, number keys 1-9). */
|
|
3
|
+
import { computed, nextTick, useTemplateRef } from 'vue'
|
|
4
4
|
import { useDropdownState } from '../../composables/useDropdownState'
|
|
5
|
+
import { useMenuKeyboard } from '../../composables/useMenuKeyboard'
|
|
5
6
|
import type { PageSelectorItem, PageSelectorItemInput } from '../../types/components'
|
|
6
7
|
import { normalizeItemInput } from '../../utils/items'
|
|
7
8
|
import { isPluginIconFormat } from '../../utils/pluginIcon'
|
|
@@ -13,27 +14,74 @@ interface Props {
|
|
|
13
14
|
currentPageId?: string
|
|
14
15
|
placeholder?: string
|
|
15
16
|
minWidth?: string
|
|
17
|
+
/** Plugin name shown before the trigger and in the menu title. */
|
|
18
|
+
pluginName?: string
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
const props = withDefaults(defineProps<Props>(), {
|
|
19
22
|
placeholder: 'Go to…',
|
|
20
|
-
minWidth: '11.25rem',
|
|
21
23
|
})
|
|
22
24
|
|
|
23
25
|
const emit = defineEmits<{
|
|
24
26
|
select: [page: PageSelectorItem]
|
|
25
27
|
}>()
|
|
26
28
|
|
|
27
|
-
const
|
|
29
|
+
const triggerEl = useTemplateRef<HTMLElement>('trigger')
|
|
30
|
+
const menuEl = useTemplateRef<HTMLElement>('menu')
|
|
31
|
+
const { isOpen, open, close, toggle } = useDropdownState({
|
|
28
32
|
rootRef: useTemplateRef<HTMLElement>('root'),
|
|
29
|
-
triggerRef:
|
|
33
|
+
triggerRef: triggerEl,
|
|
30
34
|
})
|
|
31
35
|
const normalizedPages = computed<PageSelectorItem[]>(() => props.pages.map(normalizeItemInput))
|
|
36
|
+
const menuTitle = computed(() => (props.pluginName ? `${props.pluginName} pages` : 'Pages'))
|
|
32
37
|
|
|
33
38
|
const current = computed<PageSelectorItem | undefined>(() =>
|
|
34
39
|
normalizedPages.value.find((p) => p.id === props.currentPageId) ?? normalizedPages.value[0],
|
|
35
40
|
)
|
|
36
41
|
|
|
42
|
+
const rows = () => Array.from(menuEl.value?.querySelectorAll<HTMLElement>('[role="menuitem"]') ?? [])
|
|
43
|
+
const isEnabled = (row: HTMLElement) => !row.hasAttribute('disabled') && row.getAttribute('aria-disabled') !== 'true'
|
|
44
|
+
const enabledRows = () => rows().filter(isEnabled)
|
|
45
|
+
|
|
46
|
+
const { handlePanelKeydown } = useMenuKeyboard({
|
|
47
|
+
items: enabledRows,
|
|
48
|
+
isOpen: () => isOpen.value,
|
|
49
|
+
open,
|
|
50
|
+
close,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
function handleTriggerKeydown(event: KeyboardEvent) {
|
|
54
|
+
if (isOpen.value || !['ArrowDown', 'Enter', ' '].includes(event.key)) return
|
|
55
|
+
event.preventDefault()
|
|
56
|
+
open()
|
|
57
|
+
void nextTick(() => {
|
|
58
|
+
const items = enabledRows()
|
|
59
|
+
;(items.find((row) => row.getAttribute('aria-current') === 'page') ?? items[0])?.focus()
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A click routes through ActionItemInternal, so links, router links and disabled rows act as on a pointer click.
|
|
64
|
+
function activate(row: HTMLElement) {
|
|
65
|
+
row.click()
|
|
66
|
+
if (!isOpen.value) triggerEl.value?.focus()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleMenuKeydown(event: KeyboardEvent) {
|
|
70
|
+
if (/^[1-9]$/.test(event.key)) {
|
|
71
|
+
event.preventDefault()
|
|
72
|
+
const row = rows()[Number(event.key) - 1]
|
|
73
|
+
if (row && isEnabled(row)) activate(row)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
const focused = document.activeElement as HTMLElement | null
|
|
77
|
+
if ((event.key === 'Enter' || event.key === ' ') && focused && enabledRows().includes(focused)) {
|
|
78
|
+
event.preventDefault()
|
|
79
|
+
activate(focused)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
handlePanelKeydown(event)
|
|
83
|
+
}
|
|
84
|
+
|
|
37
85
|
function handleSelect(page: PageSelectorItem) {
|
|
38
86
|
if (page.disabled) return
|
|
39
87
|
if (page.href || page.to) {
|
|
@@ -46,82 +94,102 @@ function handleSelect(page: PageSelectorItem) {
|
|
|
46
94
|
</script>
|
|
47
95
|
|
|
48
96
|
<template>
|
|
49
|
-
<div ref="root" class="mint-page-
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<slot name="icon" :page="current">
|
|
65
|
-
<PluginIcon
|
|
66
|
-
v-if="isPluginIconFormat(current.icon)"
|
|
67
|
-
class="mint-page-selector__trigger-metadata-icon"
|
|
68
|
-
:icon="current.icon"
|
|
69
|
-
size="sm"
|
|
70
|
-
variant="solid"
|
|
71
|
-
/>
|
|
72
|
-
<span v-else class="mint-page-selector__icon-fallback">{{ current.label.charAt(0) }}</span>
|
|
73
|
-
</slot>
|
|
74
|
-
</span>
|
|
75
|
-
<span class="mint-page-selector__label">{{ current?.label ?? placeholder }}</span>
|
|
76
|
-
<span class="mint-page-selector__chevron-wrap" aria-hidden="true">
|
|
77
|
-
<svg
|
|
78
|
-
class="mint-page-selector__chevron"
|
|
79
|
-
:class="{ 'mint-page-selector__chevron--open': isOpen }"
|
|
80
|
-
viewBox="0 0 24 24"
|
|
81
|
-
fill="none"
|
|
82
|
-
stroke="currentColor"
|
|
83
|
-
stroke-width="2"
|
|
84
|
-
stroke-linecap="round"
|
|
85
|
-
stroke-linejoin="round"
|
|
86
|
-
>
|
|
87
|
-
<path d="m6 9 6 6 6-6" />
|
|
88
|
-
</svg>
|
|
89
|
-
</span>
|
|
90
|
-
</button>
|
|
91
|
-
|
|
92
|
-
<div v-if="isOpen" class="mint-page-selector__menu" role="menu">
|
|
93
|
-
<div class="mint-page-selector__menu-title">Go to</div>
|
|
94
|
-
<ActionItemInternal
|
|
95
|
-
v-for="page in normalizedPages"
|
|
96
|
-
:key="page.id"
|
|
97
|
-
:href="page.href"
|
|
98
|
-
:to="page.to"
|
|
99
|
-
:disabled="page.disabled"
|
|
100
|
-
:class="[
|
|
101
|
-
'mint-page-selector__item',
|
|
102
|
-
{ 'mint-page-selector__item--active': page.id === current?.id },
|
|
103
|
-
{ 'mint-page-selector__item--disabled': page.disabled },
|
|
104
|
-
]"
|
|
105
|
-
role="menuitem"
|
|
106
|
-
@click="handleSelect(page)"
|
|
97
|
+
<div ref="root" class="mint-page-selector__crumb">
|
|
98
|
+
<template v-if="pluginName">
|
|
99
|
+
<span class="mint-page-selector__plugin">{{ pluginName }}</span>
|
|
100
|
+
<span class="mint-page-selector__slash" aria-hidden="true">/</span>
|
|
101
|
+
</template>
|
|
102
|
+
<div class="mint-page-selector" :style="minWidth ? { minWidth } : undefined">
|
|
103
|
+
<button
|
|
104
|
+
ref="trigger"
|
|
105
|
+
type="button"
|
|
106
|
+
class="mint-page-selector__trigger"
|
|
107
|
+
:class="{ 'mint-page-selector__trigger--open': isOpen }"
|
|
108
|
+
:aria-expanded="isOpen"
|
|
109
|
+
aria-haspopup="menu"
|
|
110
|
+
@click.stop="toggle"
|
|
111
|
+
@keydown="handleTriggerKeydown"
|
|
107
112
|
>
|
|
108
113
|
<span
|
|
109
|
-
|
|
110
|
-
|
|
114
|
+
v-if="current"
|
|
115
|
+
class="mint-page-selector__icon"
|
|
116
|
+
:class="{ 'mint-page-selector__icon--metadata': isPluginIconFormat(current.icon) }"
|
|
111
117
|
>
|
|
112
|
-
<slot name="
|
|
118
|
+
<slot name="icon" :page="current">
|
|
113
119
|
<PluginIcon
|
|
114
|
-
v-if="isPluginIconFormat(
|
|
115
|
-
class="mint-page-
|
|
116
|
-
:icon="
|
|
120
|
+
v-if="isPluginIconFormat(current.icon)"
|
|
121
|
+
class="mint-page-selector__trigger-metadata-icon"
|
|
122
|
+
:icon="current.icon"
|
|
117
123
|
size="sm"
|
|
118
124
|
variant="solid"
|
|
119
125
|
/>
|
|
120
|
-
<span v-else class="mint-page-selector__icon-fallback">{{
|
|
126
|
+
<span v-else class="mint-page-selector__icon-fallback">{{ current.label.charAt(0) }}</span>
|
|
121
127
|
</slot>
|
|
122
128
|
</span>
|
|
123
|
-
<span class="mint-page-
|
|
124
|
-
|
|
129
|
+
<span class="mint-page-selector__label">{{ current?.label ?? placeholder }}</span>
|
|
130
|
+
<span class="mint-page-selector__chevron-wrap" aria-hidden="true">
|
|
131
|
+
<svg
|
|
132
|
+
class="mint-page-selector__chevron"
|
|
133
|
+
:class="{ 'mint-page-selector__chevron--open': isOpen }"
|
|
134
|
+
viewBox="0 0 24 24"
|
|
135
|
+
fill="none"
|
|
136
|
+
stroke="currentColor"
|
|
137
|
+
stroke-width="2"
|
|
138
|
+
stroke-linecap="round"
|
|
139
|
+
stroke-linejoin="round"
|
|
140
|
+
>
|
|
141
|
+
<path d="m6 9 6 6 6-6" />
|
|
142
|
+
</svg>
|
|
143
|
+
</span>
|
|
144
|
+
</button>
|
|
145
|
+
|
|
146
|
+
<div
|
|
147
|
+
v-if="isOpen"
|
|
148
|
+
ref="menu"
|
|
149
|
+
class="mint-page-selector__menu"
|
|
150
|
+
role="menu"
|
|
151
|
+
:aria-label="menuTitle"
|
|
152
|
+
@keydown="handleMenuKeydown"
|
|
153
|
+
>
|
|
154
|
+
<div class="mint-page-selector__menu-title" aria-hidden="true">{{ menuTitle }}</div>
|
|
155
|
+
<ActionItemInternal
|
|
156
|
+
v-for="(page, index) in normalizedPages"
|
|
157
|
+
:key="page.id"
|
|
158
|
+
:href="page.href"
|
|
159
|
+
:to="page.to"
|
|
160
|
+
:disabled="page.disabled"
|
|
161
|
+
:class="[
|
|
162
|
+
'mint-page-selector__item',
|
|
163
|
+
{ 'mint-page-selector__item--active': page.id === current?.id },
|
|
164
|
+
{ 'mint-page-selector__item--disabled': page.disabled },
|
|
165
|
+
]"
|
|
166
|
+
role="menuitem"
|
|
167
|
+
:aria-current="page.id === current?.id ? 'page' : undefined"
|
|
168
|
+
:aria-keyshortcuts="index < 9 ? String(index + 1) : undefined"
|
|
169
|
+
@click="handleSelect(page)"
|
|
170
|
+
>
|
|
171
|
+
<span
|
|
172
|
+
class="mint-page-selector__item-icon"
|
|
173
|
+
:class="{ 'mint-page-selector__item-icon--metadata': isPluginIconFormat(page.icon) }"
|
|
174
|
+
>
|
|
175
|
+
<slot name="item-icon" :page="page">
|
|
176
|
+
<PluginIcon
|
|
177
|
+
v-if="isPluginIconFormat(page.icon)"
|
|
178
|
+
class="mint-page-selector__metadata-icon"
|
|
179
|
+
:icon="page.icon"
|
|
180
|
+
size="sm"
|
|
181
|
+
variant="solid"
|
|
182
|
+
/>
|
|
183
|
+
<span v-else class="mint-page-selector__icon-fallback">{{ page.label.charAt(0) }}</span>
|
|
184
|
+
</slot>
|
|
185
|
+
</span>
|
|
186
|
+
<span class="mint-page-selector__item-text">
|
|
187
|
+
<span class="mint-page-selector__item-label">{{ page.label }}</span>
|
|
188
|
+
<span v-if="page.hint" class="mint-page-selector__item-hint">{{ page.hint }}</span>
|
|
189
|
+
</span>
|
|
190
|
+
<span v-if="index < 9" class="mint-page-selector__item-key" aria-hidden="true">{{ index + 1 }}</span>
|
|
191
|
+
</ActionItemInternal>
|
|
192
|
+
</div>
|
|
125
193
|
</div>
|
|
126
194
|
</div>
|
|
127
195
|
</template>
|
|
@@ -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 })
|
|
@@ -83,41 +85,6 @@ function handleTriggerKeydown(event: KeyboardEvent) {
|
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
// floating-ui keeps the panel anchored (flip/shift) while the page scrolls,
|
|
87
|
-
// instead of the panel closing on every scroll or resize.
|
|
88
|
-
let stopAutoUpdate: (() => void) | null = null
|
|
89
|
-
|
|
90
|
-
async function updateDropdownPosition() {
|
|
91
|
-
const trigger = triggerRef.value
|
|
92
|
-
const panel = dropdownRef.value
|
|
93
|
-
if (!trigger || !panel) return
|
|
94
|
-
|
|
95
|
-
const { x, y } = await computePosition(trigger, panel, {
|
|
96
|
-
placement: 'bottom-start',
|
|
97
|
-
strategy: 'fixed',
|
|
98
|
-
middleware: [offset(4), flip(), shift({ padding: 8 })],
|
|
99
|
-
})
|
|
100
|
-
dropdownStyle.value = {
|
|
101
|
-
position: 'fixed',
|
|
102
|
-
top: `${y}px`,
|
|
103
|
-
left: `${x}px`,
|
|
104
|
-
zIndex: '9999',
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function stopPositioning() {
|
|
109
|
-
stopAutoUpdate?.()
|
|
110
|
-
stopAutoUpdate = null
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function startPositioning() {
|
|
114
|
-
const trigger = triggerRef.value
|
|
115
|
-
const panel = dropdownRef.value
|
|
116
|
-
if (!trigger || !panel) return
|
|
117
|
-
stopPositioning()
|
|
118
|
-
stopAutoUpdate = autoUpdate(trigger, panel, () => void updateDropdownPosition())
|
|
119
|
-
}
|
|
120
|
-
|
|
121
88
|
/** Moves focus into the calendar grid: selected day, else today, else the first day. */
|
|
122
89
|
function focusCalendar() {
|
|
123
90
|
const panel = dropdownRef.value
|
|
@@ -130,21 +97,15 @@ function focusCalendar() {
|
|
|
130
97
|
}
|
|
131
98
|
|
|
132
99
|
watch(isOpen, async (opened) => {
|
|
133
|
-
if (!opened)
|
|
134
|
-
stopPositioning()
|
|
135
|
-
return
|
|
136
|
-
}
|
|
100
|
+
if (!opened) return
|
|
137
101
|
if (selectedDate.value) {
|
|
138
102
|
setCurrentMonthFromDate(selectedDate.value)
|
|
139
103
|
}
|
|
140
104
|
await nextTick()
|
|
141
|
-
startPositioning()
|
|
142
105
|
// A second tick so this focus lands after the focus trap's initial focus.
|
|
143
106
|
await nextTick()
|
|
144
107
|
focusCalendar()
|
|
145
108
|
})
|
|
146
|
-
|
|
147
|
-
onBeforeUnmount(stopPositioning)
|
|
148
109
|
</script>
|
|
149
110
|
|
|
150
111
|
<template>
|
|
@@ -202,6 +163,7 @@ onBeforeUnmount(stopPositioning)
|
|
|
202
163
|
:style="dropdownStyle"
|
|
203
164
|
role="dialog"
|
|
204
165
|
aria-label="Date picker"
|
|
166
|
+
@keydown.escape.prevent="close(true)"
|
|
205
167
|
>
|
|
206
168
|
<CalendarGridPanelInternal
|
|
207
169
|
:week-days="weekDays"
|
|
@@ -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 {
|
|
@@ -230,8 +234,10 @@ watch(isOpen, async (opened) => {
|
|
|
230
234
|
v-if="isOpen"
|
|
231
235
|
ref="dropdownRef"
|
|
232
236
|
class="mint-popover mint-datetime-picker__dropdown"
|
|
237
|
+
:style="dropdownStyle"
|
|
233
238
|
role="dialog"
|
|
234
239
|
aria-label="Date and time picker"
|
|
240
|
+
@keydown.escape.prevent="close(true)"
|
|
235
241
|
>
|
|
236
242
|
<!-- Calendar section -->
|
|
237
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(() =>
|
|
@@ -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"
|
|
@@ -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>
|