@morscherlab/mint-sdk 1.4.0-beta.9 → 1.4.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/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
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
inject,
|
|
4
|
+
onScopeDispose,
|
|
5
|
+
provide,
|
|
6
|
+
ref,
|
|
7
|
+
shallowRef,
|
|
8
|
+
toValue,
|
|
9
|
+
watch,
|
|
10
|
+
type ComputedRef,
|
|
11
|
+
type InjectionKey,
|
|
12
|
+
type MaybeRefOrGetter,
|
|
13
|
+
type ShallowRef,
|
|
14
|
+
} from 'vue'
|
|
15
|
+
import type { Placement } from '@floating-ui/dom'
|
|
16
|
+
import { usePlatform } from './usePlatform'
|
|
17
|
+
|
|
18
|
+
/** One card of a tour. It points at the element with `data-onboarding="<anchor>"`. */
|
|
19
|
+
export interface OnboardingStep {
|
|
20
|
+
/** Stable id. A returning user sees only steps whose id is new. */
|
|
21
|
+
id: string
|
|
22
|
+
/** `data-onboarding` value of the element the step points at. */
|
|
23
|
+
anchor: string
|
|
24
|
+
title: string
|
|
25
|
+
body: string
|
|
26
|
+
/** Side of the anchor for the card. Default `bottom`. */
|
|
27
|
+
placement?: Placement
|
|
28
|
+
/** `'click'`: the highlighted element takes clicks, and a click moves the tour to the next step. */
|
|
29
|
+
advanceOn?: 'click'
|
|
30
|
+
/** Leave the step out while this returns false, e.g. without the permission its control needs. */
|
|
31
|
+
available?: () => boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface OnboardingTour {
|
|
35
|
+
id: string
|
|
36
|
+
/** Name in the Help menu. */
|
|
37
|
+
title: string
|
|
38
|
+
/** Start only while the app's route is this value. Omit it for elements on every page, such as the top bar. */
|
|
39
|
+
route?: string
|
|
40
|
+
steps: OnboardingStep[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface OnboardingTask {
|
|
44
|
+
id: string
|
|
45
|
+
title: string
|
|
46
|
+
/** Leave the task out while this returns false, e.g. without the permission it needs. */
|
|
47
|
+
available?: () => boolean
|
|
48
|
+
/** The task is done when this returns true. The coordinator checks it at start, on a route change and when the Help menu opens. */
|
|
49
|
+
done?: () => boolean
|
|
50
|
+
/** The link at the end of the task row. */
|
|
51
|
+
action?: { label: string; run: () => void }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A new feature. A dot marks its element until the user opens it. */
|
|
55
|
+
export interface OnboardingNews {
|
|
56
|
+
id: string
|
|
57
|
+
anchor: string
|
|
58
|
+
title: string
|
|
59
|
+
body: string
|
|
60
|
+
placement?: Placement
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface OnboardingDefinition {
|
|
64
|
+
tours?: OnboardingTour[]
|
|
65
|
+
/** The "Getting started" checklist. */
|
|
66
|
+
checklist?: OnboardingTask[]
|
|
67
|
+
whatsNew?: OnboardingNews[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ProvideOnboardingOptions {
|
|
71
|
+
/** Storage namespace, e.g. `platform` or the plugin name. */
|
|
72
|
+
appId: string
|
|
73
|
+
/** The signed-in user. `null` turns onboarding off (no user yet). Default: the platform user; `local` standalone or with auth off. */
|
|
74
|
+
userId?: MaybeRefOrGetter<string | number | null | undefined>
|
|
75
|
+
/** The current route, matched against `OnboardingTour.route`. */
|
|
76
|
+
route?: MaybeRefOrGetter<string | null | undefined>
|
|
77
|
+
/** Start unseen tours on their own. Default `true`. */
|
|
78
|
+
autoStart?: boolean
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The step on screen and the element it points at. */
|
|
82
|
+
export interface OnboardingActiveStep {
|
|
83
|
+
step: OnboardingStep
|
|
84
|
+
element: HTMLElement
|
|
85
|
+
/** Position among the steps of this run, and their count. */
|
|
86
|
+
index: number
|
|
87
|
+
count: number
|
|
88
|
+
/** A what's-new card, not a tour step. */
|
|
89
|
+
news: boolean
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface OnboardingState {
|
|
93
|
+
definition: OnboardingDefinition
|
|
94
|
+
/** False until a user is known. */
|
|
95
|
+
enabled: ComputedRef<boolean>
|
|
96
|
+
active: ShallowRef<OnboardingActiveStep | null>
|
|
97
|
+
/** Tours that can run on the current route. */
|
|
98
|
+
tours: ComputedRef<OnboardingTour[]>
|
|
99
|
+
/** The available tasks. */
|
|
100
|
+
tasks: ComputedRef<Array<Omit<OnboardingTask, 'done'> & { done: boolean }>>
|
|
101
|
+
tasksDone: ComputedRef<number>
|
|
102
|
+
/** The checklist chip shows while a task is open and the user did not hide it. */
|
|
103
|
+
checklistVisible: ComputedRef<boolean>
|
|
104
|
+
checklistHidden: ComputedRef<boolean>
|
|
105
|
+
news: ComputedRef<Array<OnboardingNews & { unseen: boolean }>>
|
|
106
|
+
unseenNews: ComputedRef<number>
|
|
107
|
+
startTour: (id: string) => void
|
|
108
|
+
showNews: (id: string) => void
|
|
109
|
+
next: () => void
|
|
110
|
+
back: () => void
|
|
111
|
+
/** End the run (× or Esc). Every step of the tour counts as seen. */
|
|
112
|
+
dismiss: () => void
|
|
113
|
+
complete: (taskId: string) => void
|
|
114
|
+
setChecklistHidden: (hidden: boolean) => void
|
|
115
|
+
/** Run the task predicates again. */
|
|
116
|
+
refresh: () => void
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const ONBOARDING_KEY: InjectionKey<OnboardingState> = Symbol('mint-onboarding')
|
|
120
|
+
|
|
121
|
+
/** Wait this long for the missing anchor of a run's first step, or of the step after a click step; then skip it. */
|
|
122
|
+
export const ANCHOR_WAIT_MS = 2000
|
|
123
|
+
|
|
124
|
+
const HIDE_CHECKLIST = 'hide:checklist'
|
|
125
|
+
const stepKey = (tour: OnboardingTour, step: OnboardingStep) => `step:${tour.id}/${step.id}`
|
|
126
|
+
const anchorSelector = (anchor: string) => `[data-onboarding="${CSS.escape(anchor)}"]`
|
|
127
|
+
|
|
128
|
+
/** Types a definition, so it can live in its own file. */
|
|
129
|
+
export function defineOnboarding(definition: OnboardingDefinition): OnboardingDefinition {
|
|
130
|
+
return definition
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readSeen(key: string): string[] | null {
|
|
134
|
+
try {
|
|
135
|
+
const raw = globalThis.localStorage?.getItem(key)
|
|
136
|
+
const parsed: unknown = raw ? JSON.parse(raw) : null
|
|
137
|
+
return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === 'string') : null
|
|
138
|
+
} catch {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function writeSeen(key: string, seen: Set<string>) {
|
|
144
|
+
try {
|
|
145
|
+
globalThis.localStorage?.setItem(key, JSON.stringify([...seen]))
|
|
146
|
+
} catch {
|
|
147
|
+
// Private windows and full storage: the state lasts until reload.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Resolves with the element once it is in the document, or null after `timeout` or when `signal` aborts. */
|
|
152
|
+
function waitForElement(selector: string, timeout: number, signal: AbortSignal): Promise<HTMLElement | null> {
|
|
153
|
+
const found = document.querySelector<HTMLElement>(selector)
|
|
154
|
+
if (found) return Promise.resolve(found)
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
const observer = new MutationObserver(() => {
|
|
157
|
+
const element = document.querySelector<HTMLElement>(selector)
|
|
158
|
+
if (element) finish(element)
|
|
159
|
+
})
|
|
160
|
+
const timer = setTimeout(() => finish(null), timeout)
|
|
161
|
+
const onAbort = () => finish(null)
|
|
162
|
+
function finish(element: HTMLElement | null) {
|
|
163
|
+
observer.disconnect()
|
|
164
|
+
clearTimeout(timer)
|
|
165
|
+
signal.removeEventListener('abort', onAbort)
|
|
166
|
+
resolve(element)
|
|
167
|
+
}
|
|
168
|
+
signal.addEventListener('abort', onAbort)
|
|
169
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-onboarding'] })
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Any open modal except the tour card itself.
|
|
174
|
+
const MODAL_SELECTOR = '[aria-modal="true"]:not(.mint-onboarding__card)'
|
|
175
|
+
|
|
176
|
+
/** Resolves true once no modal is open, or false when `signal` aborts first. */
|
|
177
|
+
function waitForNoModal(signal: AbortSignal): Promise<boolean> {
|
|
178
|
+
if (!document.querySelector(MODAL_SELECTOR)) return Promise.resolve(true)
|
|
179
|
+
return new Promise((resolve) => {
|
|
180
|
+
const observer = new MutationObserver(() => {
|
|
181
|
+
if (!document.querySelector(MODAL_SELECTOR)) finish(true)
|
|
182
|
+
})
|
|
183
|
+
const onAbort = () => finish(false)
|
|
184
|
+
function finish(closed: boolean) {
|
|
185
|
+
observer.disconnect()
|
|
186
|
+
signal.removeEventListener('abort', onAbort)
|
|
187
|
+
resolve(closed)
|
|
188
|
+
}
|
|
189
|
+
signal.addEventListener('abort', onAbort)
|
|
190
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['aria-modal'] })
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Creates the app's onboarding coordinator and provides it to `AppTopBar`, which then
|
|
196
|
+
* shows the Help button, the checklist chip and the tour layer. Call it once, in the
|
|
197
|
+
* root component. It shows one item at a time and waits for open modals.
|
|
198
|
+
*
|
|
199
|
+
* ```ts
|
|
200
|
+
* provideOnboarding(onboarding, { appId: 'leaf' })
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export function provideOnboarding(definition: OnboardingDefinition, options: ProvideOnboardingOptions): OnboardingState {
|
|
204
|
+
const { user, isAdmin } = usePlatform()
|
|
205
|
+
const userId = computed(() => {
|
|
206
|
+
// With no user, isAdmin is true only standalone or with auth off.
|
|
207
|
+
const fallback = isAdmin.value ? 'local' : null
|
|
208
|
+
const value = options.userId === undefined ? (user.value?.id ?? fallback) : toValue(options.userId)
|
|
209
|
+
return value == null ? null : String(value)
|
|
210
|
+
})
|
|
211
|
+
const route = computed(() => toValue(options.route) ?? undefined)
|
|
212
|
+
const storageKey = computed(() => userId.value === null ? null : `mint.onboarding.${options.appId}.${userId.value}`)
|
|
213
|
+
const enabled = computed(() => storageKey.value !== null)
|
|
214
|
+
|
|
215
|
+
const seen = ref(new Set<string>())
|
|
216
|
+
// Bumped by refresh(): the predicates read app state that Vue may not track.
|
|
217
|
+
const tick = ref(0)
|
|
218
|
+
const active = shallowRef<OnboardingActiveStep | null>(null)
|
|
219
|
+
let run: { tour: OnboardingTour | null; steps: OnboardingStep[]; index: number; shown: Set<number> } | null = null
|
|
220
|
+
let runId = 0
|
|
221
|
+
let stepWait: AbortController | undefined
|
|
222
|
+
let detachClick: (() => void) | undefined
|
|
223
|
+
|
|
224
|
+
/** Applies `update` to the stored state merged with this tab's, so tabs do not drop each other's keys. */
|
|
225
|
+
function persist(update: (keys: Set<string>) => void) {
|
|
226
|
+
const key = storageKey.value
|
|
227
|
+
if (!key) return
|
|
228
|
+
const merged = new Set([...(readSeen(key) ?? []), ...seen.value])
|
|
229
|
+
update(merged)
|
|
230
|
+
seen.value = merged
|
|
231
|
+
writeSeen(key, merged)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function markSeen(...keys: string[]) {
|
|
235
|
+
if (keys.every((key) => seen.value.has(key))) return
|
|
236
|
+
persist((set) => keys.forEach((key) => set.add(key)))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Another tab changed the state.
|
|
240
|
+
function onStorage(event: StorageEvent) {
|
|
241
|
+
if (event.key && event.key === storageKey.value) seen.value = new Set(readSeen(event.key) ?? [])
|
|
242
|
+
}
|
|
243
|
+
window.addEventListener('storage', onStorage)
|
|
244
|
+
|
|
245
|
+
watch(storageKey, (key) => {
|
|
246
|
+
stop()
|
|
247
|
+
if (!key) {
|
|
248
|
+
seen.value = new Set()
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
const stored = readSeen(key)
|
|
252
|
+
seen.value = new Set(stored ?? [])
|
|
253
|
+
// First run: the tours introduce today's features, so today's news counts as seen.
|
|
254
|
+
if (stored === null) {
|
|
255
|
+
for (const item of definition.whatsNew ?? []) seen.value.add(`news:${item.id}`)
|
|
256
|
+
writeSeen(key, seen.value)
|
|
257
|
+
}
|
|
258
|
+
}, { immediate: true })
|
|
259
|
+
|
|
260
|
+
const tours = computed(() => (definition.tours ?? []).filter((tour) => !tour.route || tour.route === route.value))
|
|
261
|
+
|
|
262
|
+
const tasks = computed(() => {
|
|
263
|
+
void tick.value
|
|
264
|
+
return (definition.checklist ?? []).filter((task) => task.available?.() ?? true).map((task) => ({
|
|
265
|
+
...task,
|
|
266
|
+
done: seen.value.has(`task:${task.id}`) || !!task.done?.(),
|
|
267
|
+
}))
|
|
268
|
+
})
|
|
269
|
+
// Done stays done, also when a predicate later returns false.
|
|
270
|
+
watch(tasks, (list) => markSeen(...list.filter((task) => task.done).map((task) => `task:${task.id}`)), { immediate: true })
|
|
271
|
+
const tasksDone = computed(() => tasks.value.filter((task) => task.done).length)
|
|
272
|
+
const checklistHidden = computed(() => seen.value.has(HIDE_CHECKLIST))
|
|
273
|
+
const checklistVisible = computed(() =>
|
|
274
|
+
enabled.value && tasks.value.length > 0 && tasksDone.value < tasks.value.length && !checklistHidden.value)
|
|
275
|
+
|
|
276
|
+
const news = computed(() => (definition.whatsNew ?? []).map((item) => ({ ...item, unseen: !seen.value.has(`news:${item.id}`) })))
|
|
277
|
+
const unseenNews = computed(() => enabled.value ? news.value.filter((item) => item.unseen).length : 0)
|
|
278
|
+
|
|
279
|
+
function stop() {
|
|
280
|
+
runId += 1
|
|
281
|
+
stepWait?.abort()
|
|
282
|
+
run = null
|
|
283
|
+
detachClick?.()
|
|
284
|
+
detachClick = undefined
|
|
285
|
+
active.value = null
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Shows the first step at or after `from` whose anchor is there. Only the first step of
|
|
290
|
+
* the run and the step after a click step wait for theirs (the page or a menu can still
|
|
291
|
+
* be opening); after Next a missing anchor is skipped at once. Skipped steps stay unseen.
|
|
292
|
+
*/
|
|
293
|
+
/**
|
|
294
|
+
* The later steps the page can still show: the anchor is on the page now, or
|
|
295
|
+
* a click step that can show opens it. The count grows if an anchor renders later.
|
|
296
|
+
*/
|
|
297
|
+
function laterSteps(steps: OnboardingStep[], from: number) {
|
|
298
|
+
let count = 0
|
|
299
|
+
let previous = true
|
|
300
|
+
for (let at = from + 1; at < steps.length; at += 1) {
|
|
301
|
+
previous = (previous && steps[at - 1].advanceOn === 'click') || !!document.querySelector(anchorSelector(steps[at].anchor))
|
|
302
|
+
if (previous) count += 1
|
|
303
|
+
}
|
|
304
|
+
return count
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function showFrom(from: number) {
|
|
308
|
+
if (!run) return
|
|
309
|
+
const id = ++runId
|
|
310
|
+
stepWait?.abort()
|
|
311
|
+
const wait = new AbortController()
|
|
312
|
+
stepWait = wait
|
|
313
|
+
detachClick?.()
|
|
314
|
+
detachClick = undefined
|
|
315
|
+
const waitFirst = from === 0 || run.steps[from - 1].advanceOn === 'click'
|
|
316
|
+
for (let index = from; index < run.steps.length; index += 1) {
|
|
317
|
+
const step = run.steps[index]
|
|
318
|
+
const timeout = index === from && waitFirst ? ANCHOR_WAIT_MS : 0
|
|
319
|
+
const element = await waitForElement(anchorSelector(step.anchor), timeout, wait.signal)
|
|
320
|
+
if (id !== runId) return
|
|
321
|
+
if (!element) {
|
|
322
|
+
// Debug level: a missing anchor is normal, e.g. an admin-only button for a member.
|
|
323
|
+
console.debug(`[mint-onboarding] No element with data-onboarding="${step.anchor}"; step "${step.id}" skipped.`)
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
run.index = index
|
|
327
|
+
markSeen(run.tour ? stepKey(run.tour, step) : `news:${step.id}`)
|
|
328
|
+
if (step.advanceOn === 'click') {
|
|
329
|
+
const onClick = () => next()
|
|
330
|
+
element.addEventListener('click', onClick)
|
|
331
|
+
detachClick = () => element.removeEventListener('click', onClick)
|
|
332
|
+
}
|
|
333
|
+
run.shown.add(index)
|
|
334
|
+
// Earlier steps count as the later ones do: shown, and still on the page.
|
|
335
|
+
const steps = run.steps
|
|
336
|
+
const position = [...run.shown].filter((at) => at < index && !!document.querySelector(anchorSelector(steps[at].anchor))).length
|
|
337
|
+
active.value = { step, element, index: position, count: position + 1 + laterSteps(run.steps, index), news: !run.tour }
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
// A finished tour hands over to the next one; × does not (dismiss stops the run first).
|
|
341
|
+
const finished = !!run.tour
|
|
342
|
+
stop()
|
|
343
|
+
if (finished && options.autoStart !== false) void autoStart()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function begin(tour: OnboardingTour | null, steps: OnboardingStep[]) {
|
|
347
|
+
if (!enabled.value || steps.length === 0) return
|
|
348
|
+
stop()
|
|
349
|
+
run = { tour, steps, index: 0, shown: new Set() }
|
|
350
|
+
void showFrom(0)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function startTour(id: string) {
|
|
354
|
+
const tour = tours.value.find((item) => item.id === id)
|
|
355
|
+
if (!tour) return
|
|
356
|
+
autoStartAbort?.abort()
|
|
357
|
+
begin(tour, availableSteps(tour))
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function showNews(id: string) {
|
|
361
|
+
const item = definition.whatsNew?.find((entry) => entry.id === id)
|
|
362
|
+
if (!item) return
|
|
363
|
+
autoStartAbort?.abort()
|
|
364
|
+
begin(null, [item])
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function next() {
|
|
368
|
+
if (run) void showFrom(run.index + 1)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function back() {
|
|
372
|
+
if (!run) return
|
|
373
|
+
// Back goes only to steps whose element is still on the page; it does not wait.
|
|
374
|
+
for (let index = run.index - 1; index >= 0; index -= 1) {
|
|
375
|
+
if (document.querySelector(anchorSelector(run.steps[index].anchor))) {
|
|
376
|
+
void showFrom(index)
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function dismiss() {
|
|
383
|
+
autoStartAbort?.abort()
|
|
384
|
+
const tour = run?.tour
|
|
385
|
+
if (tour) markSeen(...tour.steps.map((step) => stepKey(tour, step)))
|
|
386
|
+
stop()
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function complete(taskId: string) {
|
|
390
|
+
markSeen(`task:${taskId}`)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function setChecklistHidden(hidden: boolean) {
|
|
394
|
+
if (!storageKey.value) return
|
|
395
|
+
if (hidden) markSeen(HIDE_CHECKLIST)
|
|
396
|
+
else persist((set) => set.delete(HIDE_CHECKLIST))
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function refresh() {
|
|
400
|
+
tick.value += 1
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const availableSteps = (tour: OnboardingTour) => tour.steps.filter((step) => step.available?.() ?? true)
|
|
404
|
+
|
|
405
|
+
// Tours started on this page; a tour with skipped steps does not start again until the page changes.
|
|
406
|
+
const tried = new Set<string>()
|
|
407
|
+
let autoStartAbort: AbortController | undefined
|
|
408
|
+
async function autoStart() {
|
|
409
|
+
autoStartAbort?.abort()
|
|
410
|
+
const controller = new AbortController()
|
|
411
|
+
autoStartAbort = controller
|
|
412
|
+
if (!(await waitForNoModal(controller.signal)) || run || !enabled.value) return
|
|
413
|
+
for (const tour of tours.value) {
|
|
414
|
+
if (tried.has(tour.id)) continue
|
|
415
|
+
const unseen = availableSteps(tour).filter((step) => !seen.value.has(stepKey(tour, step)))
|
|
416
|
+
if (unseen.length) {
|
|
417
|
+
tried.add(tour.id)
|
|
418
|
+
begin(tour, unseen)
|
|
419
|
+
return
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// A new page ends the run: its elements are gone. Its unseen steps start again.
|
|
425
|
+
watch([route, storageKey], () => {
|
|
426
|
+
stop()
|
|
427
|
+
tried.clear()
|
|
428
|
+
refresh()
|
|
429
|
+
if (options.autoStart !== false) void autoStart()
|
|
430
|
+
}, { immediate: true })
|
|
431
|
+
|
|
432
|
+
onScopeDispose(() => {
|
|
433
|
+
autoStartAbort?.abort()
|
|
434
|
+
window.removeEventListener('storage', onStorage)
|
|
435
|
+
stop()
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
const state: OnboardingState = {
|
|
439
|
+
definition,
|
|
440
|
+
enabled,
|
|
441
|
+
active,
|
|
442
|
+
tours,
|
|
443
|
+
tasks,
|
|
444
|
+
tasksDone,
|
|
445
|
+
checklistVisible,
|
|
446
|
+
checklistHidden,
|
|
447
|
+
news,
|
|
448
|
+
unseenNews,
|
|
449
|
+
startTour,
|
|
450
|
+
showNews,
|
|
451
|
+
next,
|
|
452
|
+
back,
|
|
453
|
+
dismiss,
|
|
454
|
+
complete,
|
|
455
|
+
setChecklistHidden,
|
|
456
|
+
refresh,
|
|
457
|
+
}
|
|
458
|
+
provide(ONBOARDING_KEY, state)
|
|
459
|
+
return state
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** The app's onboarding coordinator, e.g. to call `complete(taskId)`. Null when the app provides none. */
|
|
463
|
+
export function useOnboarding(): OnboardingState | null {
|
|
464
|
+
return inject(ONBOARDING_KEY, null)
|
|
465
|
+
}
|
|
@@ -120,7 +120,7 @@ export function usePluginWorkspace(options: UsePluginWorkspaceOptions): UsePlugi
|
|
|
120
120
|
sidebarToggleLabel: sidebar.toggleLabel ?? 'Open workspace sidebar',
|
|
121
121
|
sidebarCloseLabel: sidebar.closeLabel ?? 'Close workspace sidebar',
|
|
122
122
|
hasSidebarSurface: hasSidebarSurface.value,
|
|
123
|
-
title: props.spec?.title
|
|
123
|
+
title: props.spec?.title,
|
|
124
124
|
subtitle: props.spec?.subtitle,
|
|
125
125
|
topBar: topBarSpec.value === false
|
|
126
126
|
? false
|
package/src/data.ts
CHANGED
package/src/runtime.ts
CHANGED
package/src/shell.ts
CHANGED
|
@@ -101,3 +101,20 @@ export {
|
|
|
101
101
|
type UsePluginWorkspaceOptions,
|
|
102
102
|
type UsePluginWorkspaceReturn,
|
|
103
103
|
} from './composables/usePluginWorkspace'
|
|
104
|
+
|
|
105
|
+
// Onboarding: tours, the Getting started checklist and what's-new hints
|
|
106
|
+
export {
|
|
107
|
+
ANCHOR_WAIT_MS,
|
|
108
|
+
defineOnboarding,
|
|
109
|
+
provideOnboarding,
|
|
110
|
+
useOnboarding,
|
|
111
|
+
ONBOARDING_KEY,
|
|
112
|
+
type OnboardingActiveStep,
|
|
113
|
+
type OnboardingDefinition,
|
|
114
|
+
type OnboardingNews,
|
|
115
|
+
type OnboardingState,
|
|
116
|
+
type OnboardingStep,
|
|
117
|
+
type OnboardingTask,
|
|
118
|
+
type OnboardingTour,
|
|
119
|
+
type ProvideOnboardingOptions,
|
|
120
|
+
} from './composables/useOnboarding'
|
package/src/stores/index.ts
CHANGED
package/src/stores/settings.ts
CHANGED
|
@@ -55,39 +55,24 @@ const defaultSettings: SettingsState = {
|
|
|
55
55
|
tableDensity: 'normal',
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
export const colorPalettes: Record<ColorPalette, { name: string; hues: [number, number] }> = {
|
|
59
|
-
default: { name: 'Default (Cyan-Pink)', hues: [180, 320] },
|
|
60
|
-
colorblind: { name: 'Colorblind-friendly', hues: [45, 260] },
|
|
61
|
-
viridis: { name: 'Viridis', hues: [280, 80] },
|
|
62
|
-
pastel: { name: 'Pastel', hues: [200, 340] },
|
|
63
|
-
}
|
|
64
|
-
|
|
65
58
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* The saturation/lightness pairs are lifted from the original indigo/orange
|
|
69
|
-
* tokens in `styles/variables.css` so only the hue moves; the `default`
|
|
70
|
-
* palette writes nothing at all and the stylesheet values stand.
|
|
59
|
+
* Accent palettes. `styles/variables.css` holds each palette's light and dark
|
|
60
|
+
* tokens under `[data-palette]`; the store only writes the attribute.
|
|
71
61
|
*/
|
|
72
|
-
export const
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
'--color-primary-hover': `hsl(${primaryHue} 75% 59%)`,
|
|
87
|
-
'--color-primary-soft': `hsl(${primaryHue} 84% 67% / 0.12)`,
|
|
88
|
-
'--color-cta': `hsl(${accentHue} 95% 53%)`,
|
|
89
|
-
'--color-cta-hover': `hsl(${accentHue} 90% 48%)`,
|
|
90
|
-
}
|
|
62
|
+
export const colorPalettes: Record<ColorPalette, { name: string }> = {
|
|
63
|
+
default: { name: 'Teal' },
|
|
64
|
+
colorblind: { name: 'Colour-blind safe' },
|
|
65
|
+
purple: { name: 'Purple' },
|
|
66
|
+
ocean: { name: 'Ocean' },
|
|
67
|
+
forest: { name: 'Forest' },
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Pre-v2 palette ids and the palette that replaces each one. */
|
|
71
|
+
const RETIRED_PALETTES: Record<string, ColorPalette> = { viridis: 'purple', pastel: 'default' }
|
|
72
|
+
|
|
73
|
+
function knownPalette(value: unknown): ColorPalette {
|
|
74
|
+
if (typeof value === 'string' && value in colorPalettes) return value as ColorPalette
|
|
75
|
+
return (typeof value === 'string' && RETIRED_PALETTES[value]) || 'default'
|
|
91
76
|
}
|
|
92
77
|
|
|
93
78
|
/** `DataFrame`-style size that each table density maps onto. */
|
|
@@ -107,13 +92,13 @@ function loadSettings(): SettingsState {
|
|
|
107
92
|
const stored = localStorage.getItem(STORAGE_KEY)
|
|
108
93
|
if (stored) {
|
|
109
94
|
const parsed = JSON.parse(stored)
|
|
110
|
-
return { ...defaultSettings, ...parsed }
|
|
95
|
+
return { ...defaultSettings, ...parsed, colorPalette: knownPalette(parsed.colorPalette) }
|
|
111
96
|
}
|
|
112
97
|
|
|
113
98
|
const legacyStored = localStorage.getItem(LEGACY_STORAGE_KEY)
|
|
114
99
|
if (legacyStored) {
|
|
115
100
|
const parsed = JSON.parse(legacyStored)
|
|
116
|
-
const migrated = { ...defaultSettings, ...parsed }
|
|
101
|
+
const migrated = { ...defaultSettings, ...parsed, colorPalette: knownPalette(parsed.colorPalette) }
|
|
117
102
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(migrated))
|
|
118
103
|
localStorage.removeItem(LEGACY_STORAGE_KEY)
|
|
119
104
|
return migrated
|
|
@@ -202,20 +187,10 @@ export const useSettingsStore = defineStore('mint-settings', () => {
|
|
|
202
187
|
document.documentElement.classList.toggle('dark', dark)
|
|
203
188
|
}
|
|
204
189
|
|
|
205
|
-
/**
|
|
206
|
-
* Write the selected palette's hues onto `<html>` as custom-property
|
|
207
|
-
* overrides. The `default` palette clears them so `styles/variables.css`
|
|
208
|
-
* keeps ownership of the brand colors.
|
|
209
|
-
*/
|
|
190
|
+
/** Mirror the palette onto `<html data-palette>`; `styles/variables.css` maps it to tokens. */
|
|
210
191
|
function applyPalette() {
|
|
211
192
|
if (typeof document === 'undefined') return
|
|
212
|
-
|
|
213
|
-
const overrides = paletteCssVariables(colorPalette.value)
|
|
214
|
-
for (const name of PALETTE_CSS_VARIABLES) {
|
|
215
|
-
const value = overrides[name]
|
|
216
|
-
if (value) style.setProperty(name, value)
|
|
217
|
-
else style.removeProperty(name)
|
|
218
|
-
}
|
|
193
|
+
document.documentElement.dataset.palette = colorPalette.value
|
|
219
194
|
}
|
|
220
195
|
|
|
221
196
|
/** Mirror the table density onto `<html data-density>` for CSS consumers. */
|
|
@@ -269,10 +244,6 @@ export const useSettingsStore = defineStore('mint-settings', () => {
|
|
|
269
244
|
tableDensity.value = defaultSettings.tableDensity
|
|
270
245
|
}
|
|
271
246
|
|
|
272
|
-
function getPaletteHues(): [number, number] {
|
|
273
|
-
return colorPalettes[colorPalette.value].hues
|
|
274
|
-
}
|
|
275
|
-
|
|
276
247
|
/** The `size` the SDK table components default to for the current density. */
|
|
277
248
|
function getTableSize(): 'sm' | 'md' | 'lg' {
|
|
278
249
|
return tableDensityToSize(tableDensity.value)
|
|
@@ -298,7 +269,6 @@ export const useSettingsStore = defineStore('mint-settings', () => {
|
|
|
298
269
|
applyDensity,
|
|
299
270
|
persistSettings,
|
|
300
271
|
resetToDefaults,
|
|
301
|
-
getPaletteHues,
|
|
302
272
|
getTableSize,
|
|
303
273
|
isDark,
|
|
304
274
|
getApiBaseUrl,
|
|
@@ -60,10 +60,11 @@
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/* Floating variant: all sections appear as separate cards on a background,
|
|
63
|
-
spaced by the density gutter (8px, 12px
|
|
63
|
+
16px from the window edge and spaced by the density gutter (8px, 12px
|
|
64
|
+
comfortable). */
|
|
64
65
|
.mint-layout--floating {
|
|
65
66
|
background-color: var(--bg-primary);
|
|
66
|
-
padding: var(--mint-
|
|
67
|
+
padding: var(--mint-page-inset);
|
|
67
68
|
gap: var(--mint-gutter);
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -123,8 +124,10 @@
|
|
|
123
124
|
transform: translateX(calc(100% + 1rem));
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
/* `none`, not translateX(0): any transform makes the sidebar the containing
|
|
128
|
+
block of its fixed dropdown panels, and its overflow would clip them. */
|
|
126
129
|
.mint-layout--responsive-sidebar.mint-layout--sidebar-open .mint-layout__sidebar {
|
|
127
|
-
transform:
|
|
130
|
+
transform: none;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
.mint-layout--responsive-sidebar .mint-layout__sidebar-toggle {
|