@morscherlab/mint-sdk 1.0.15 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BaseSelect-DksaKYq_.js → BaseSelect-ekgr9fDo.js} +4 -1
- package/dist/BaseSelect-ekgr9fDo.js.map +1 -0
- package/dist/{ExperimentSelectorModal-DIFyL5ta.js → ExperimentSelectorModal-BOzDs8TU.js} +2 -2
- package/dist/{ExperimentSelectorModal-CHsU-LIh.js → ExperimentSelectorModal-CX0oBzpV.js} +2 -2
- package/dist/{ExperimentSelectorModal-CHsU-LIh.js.map → ExperimentSelectorModal-CX0oBzpV.js.map} +1 -1
- package/dist/{SettingsModal-LEKI6Ebl.js → SettingsModal-BTyXD0uP.js} +3 -3
- package/dist/{SettingsModal-LEKI6Ebl.js.map → SettingsModal-BTyXD0uP.js.map} +1 -1
- package/dist/SettingsModal-DXcSKk9D.js +5 -0
- package/dist/__tests__/components/MobileSupportGate.test.d.ts +1 -0
- package/dist/__tests__/composables/useMobileSupportGate.test.d.ts +1 -0
- package/dist/components/AutoGroupModal.vue.d.ts +6 -0
- package/dist/components/BaseInput.vue.d.ts +1 -0
- package/dist/components/MobileSupportGate.vue.d.ts +40 -0
- package/dist/components/SampleSelector.colors.d.ts +2 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +6 -6
- package/dist/{components-Cyk8QEyL.js → components-D0CzE0XK.js} +1061 -473
- package/dist/components-D0CzE0XK.js.map +1 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +7 -7
- package/dist/composables/useMobileSupportGate.d.ts +14 -0
- package/dist/{composables-D9mexHSW.js → composables-Da-4XOe2.js} +3 -3
- package/dist/{composables-D9mexHSW.js.map → composables-Da-4XOe2.js.map} +1 -1
- package/dist/index.js +10 -10
- package/dist/install.js +5 -5
- package/dist/styles.css +3705 -2286
- package/dist/templates/index.js +3 -3
- package/dist/{templates-Do43ZIMb.js → templates-Dnf8UNxg.js} +2 -2
- package/dist/{templates-Do43ZIMb.js.map → templates-Dnf8UNxg.js.map} +1 -1
- package/dist/{useControlSchema-0n8Bcftq.js → useControlSchema-Dkm-W_lg.js} +2 -2
- package/dist/{useControlSchema-0n8Bcftq.js.map → useControlSchema-Dkm-W_lg.js.map} +1 -1
- package/dist/{useFormBuilder-COfYWDuC.js → useFormBuilder-BOJ52N4M.js} +2 -2
- package/dist/{useFormBuilder-COfYWDuC.js.map → useFormBuilder-BOJ52N4M.js.map} +1 -1
- package/dist/{useProtocolTemplates-DODHlhxr.js → useProtocolTemplates-r2GOnnH1.js} +55 -5
- package/dist/useProtocolTemplates-r2GOnnH1.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/MobileSupportGate.test.ts +120 -0
- package/src/__tests__/components/SampleSelector.test.ts +119 -0
- package/src/__tests__/composables/useMobileSupportGate.test.ts +74 -0
- package/src/components/AutoGroupModal.story.vue +46 -0
- package/src/components/AutoGroupModal.vue +578 -2
- package/src/components/BaseInput.vue +2 -0
- package/src/components/MobileSupportGate.story.vue +52 -0
- package/src/components/MobileSupportGate.vue +115 -0
- package/src/components/SampleSelector.colors.ts +7 -2
- package/src/components/SampleSelector.story.vue +34 -0
- package/src/components/SampleSelector.vue +22 -1
- package/src/components/index.ts +1 -0
- package/src/composables/index.ts +8 -0
- package/src/composables/useMobileSupportGate.ts +80 -0
- package/src/styles/components/auto-group-modal.css +744 -0
- package/src/styles/components/mobile-support-gate.css +119 -0
- package/dist/BaseSelect-DksaKYq_.js.map +0 -1
- package/dist/SettingsModal-L7Ejny45.js +0 -5
- package/dist/components-Cyk8QEyL.js.map +0 -1
- package/dist/useProtocolTemplates-DODHlhxr.js.map +0 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import MobileSupportGate from './MobileSupportGate.vue'
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<Story title="Layout/MobileSupportGate">
|
|
7
|
+
<Variant title="Unsupported State">
|
|
8
|
+
<div style="min-height: 560px;">
|
|
9
|
+
<MobileSupportGate
|
|
10
|
+
:supported="false"
|
|
11
|
+
media-query="(min-width: 0px)"
|
|
12
|
+
app-name="MINT platform"
|
|
13
|
+
title="Desktop workspace recommended"
|
|
14
|
+
message="This page is not supported on mobile. Experiments, Projects, and Admin remain available on smaller screens."
|
|
15
|
+
desktop-label="Switch to a desktop browser for the full platform workspace."
|
|
16
|
+
>
|
|
17
|
+
<div>Workspace content</div>
|
|
18
|
+
</MobileSupportGate>
|
|
19
|
+
</div>
|
|
20
|
+
</Variant>
|
|
21
|
+
|
|
22
|
+
<Variant title="Supported Content">
|
|
23
|
+
<div style="padding: 2rem; min-height: 360px;">
|
|
24
|
+
<MobileSupportGate :supported="true" media-query="(min-width: 0px)">
|
|
25
|
+
<div style="max-width: 640px; border: 1px solid var(--border-color, #e2e8f0); border-radius: 0.5rem; padding: 1.5rem; background: var(--bg-secondary, #fff);">
|
|
26
|
+
<h3 style="margin: 0 0 0.5rem; font-size: 1rem; line-height: 1.2;">Route content</h3>
|
|
27
|
+
<p style="margin: 0; color: var(--text-secondary, #475569);">
|
|
28
|
+
The gate renders its default slot whenever the route is supported or the viewport is wider than the mobile query.
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
</MobileSupportGate>
|
|
32
|
+
</div>
|
|
33
|
+
</Variant>
|
|
34
|
+
|
|
35
|
+
<Variant title="Custom Unsupported Slot">
|
|
36
|
+
<div style="min-height: 420px;">
|
|
37
|
+
<MobileSupportGate :supported="false" media-query="(min-width: 0px)">
|
|
38
|
+
<template #unsupported>
|
|
39
|
+
<div style="min-height: 420px; display: grid; place-items: center; padding: 2rem; background: var(--bg-primary, #f8fafc);">
|
|
40
|
+
<div style="max-width: 420px; border: 1px solid var(--border-color, #e2e8f0); border-radius: 0.5rem; padding: 1.5rem; background: var(--bg-secondary, #fff);">
|
|
41
|
+
<h3 style="margin: 0 0 0.5rem; font-size: 1.125rem;">Custom fallback</h3>
|
|
42
|
+
<p style="margin: 0; color: var(--text-secondary, #475569); line-height: 1.5;">
|
|
43
|
+
Consumers can replace the default message while keeping the SDK viewport detection behavior.
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
</MobileSupportGate>
|
|
49
|
+
</div>
|
|
50
|
+
</Variant>
|
|
51
|
+
</Story>
|
|
52
|
+
</template>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let nextMobileSupportGateTitleId = 0
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
import { computed } from 'vue'
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_MOBILE_VIEWPORT_QUERY,
|
|
9
|
+
useMobileSupportGate,
|
|
10
|
+
} from '../composables/useMobileSupportGate'
|
|
11
|
+
|
|
12
|
+
export interface MobileSupportGateProps {
|
|
13
|
+
supported?: boolean
|
|
14
|
+
mediaQuery?: string
|
|
15
|
+
appName?: string
|
|
16
|
+
title?: string
|
|
17
|
+
message?: string
|
|
18
|
+
desktopLabel?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(defineProps<MobileSupportGateProps>(), {
|
|
22
|
+
supported: true,
|
|
23
|
+
mediaQuery: DEFAULT_MOBILE_VIEWPORT_QUERY,
|
|
24
|
+
appName: 'MINT',
|
|
25
|
+
title: 'Desktop workspace recommended',
|
|
26
|
+
message: 'This workspace is optimized for a wider desktop screen and is not supported on mobile.',
|
|
27
|
+
desktopLabel: 'Open this page from a desktop browser to use the full workflow.',
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
defineSlots<{
|
|
31
|
+
default(props: { isMobileViewport: boolean; isSupported: boolean }): unknown
|
|
32
|
+
unsupported?(props: { isMobileViewport: boolean; isSupported: boolean }): unknown
|
|
33
|
+
}>()
|
|
34
|
+
|
|
35
|
+
const titleId = `mint-mobile-support-title-${++nextMobileSupportGateTitleId}`
|
|
36
|
+
|
|
37
|
+
const supported = computed(() => props.supported)
|
|
38
|
+
const mediaQuery = computed(() => props.mediaQuery)
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
isMobileViewport,
|
|
42
|
+
isSupported,
|
|
43
|
+
shouldShowUnsupported,
|
|
44
|
+
} = useMobileSupportGate({
|
|
45
|
+
supported,
|
|
46
|
+
mediaQuery,
|
|
47
|
+
})
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<div
|
|
52
|
+
v-if="shouldShowUnsupported"
|
|
53
|
+
class="mint-mobile-support-gate"
|
|
54
|
+
role="main"
|
|
55
|
+
:aria-labelledby="titleId"
|
|
56
|
+
data-testid="mobile-unsupported"
|
|
57
|
+
>
|
|
58
|
+
<slot
|
|
59
|
+
name="unsupported"
|
|
60
|
+
:is-mobile-viewport="isMobileViewport"
|
|
61
|
+
:is-supported="isSupported"
|
|
62
|
+
>
|
|
63
|
+
<section class="mint-mobile-support-gate__panel">
|
|
64
|
+
<div class="mint-mobile-support-gate__mark" aria-hidden="true">
|
|
65
|
+
<svg
|
|
66
|
+
class="mint-mobile-support-gate__icon"
|
|
67
|
+
viewBox="0 0 24 24"
|
|
68
|
+
fill="none"
|
|
69
|
+
stroke="currentColor"
|
|
70
|
+
stroke-width="2"
|
|
71
|
+
stroke-linecap="round"
|
|
72
|
+
stroke-linejoin="round"
|
|
73
|
+
>
|
|
74
|
+
<rect x="3" y="4" width="18" height="12" rx="2" />
|
|
75
|
+
<path d="M8 20h8" />
|
|
76
|
+
<path d="M12 16v4" />
|
|
77
|
+
</svg>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="mint-mobile-support-gate__copy">
|
|
81
|
+
<p class="mint-mobile-support-gate__eyebrow">{{ appName }}</p>
|
|
82
|
+
<h1 :id="titleId" class="mint-mobile-support-gate__title">{{ title }}</h1>
|
|
83
|
+
<p class="mint-mobile-support-gate__message">{{ message }}</p>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="mint-mobile-support-gate__hint">
|
|
87
|
+
<svg
|
|
88
|
+
class="mint-mobile-support-gate__hint-icon"
|
|
89
|
+
viewBox="0 0 24 24"
|
|
90
|
+
fill="none"
|
|
91
|
+
stroke="currentColor"
|
|
92
|
+
stroke-width="2"
|
|
93
|
+
stroke-linecap="round"
|
|
94
|
+
stroke-linejoin="round"
|
|
95
|
+
aria-hidden="true"
|
|
96
|
+
>
|
|
97
|
+
<path d="M12 20h9" />
|
|
98
|
+
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
|
|
99
|
+
</svg>
|
|
100
|
+
<span>{{ desktopLabel }}</span>
|
|
101
|
+
</div>
|
|
102
|
+
</section>
|
|
103
|
+
</slot>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<slot
|
|
107
|
+
v-else
|
|
108
|
+
:is-mobile-viewport="isMobileViewport"
|
|
109
|
+
:is-supported="isSupported"
|
|
110
|
+
/>
|
|
111
|
+
</template>
|
|
112
|
+
|
|
113
|
+
<style>
|
|
114
|
+
@import '../styles/components/mobile-support-gate.css';
|
|
115
|
+
</style>
|
|
@@ -6,6 +6,7 @@ export type ColorEdit =
|
|
|
6
6
|
| { kind: 'family'; names: string[] }
|
|
7
7
|
|
|
8
8
|
export const DEFAULT_COLOR_PICKER_SEED = '#3B82F6'
|
|
9
|
+
export const SAMPLE_GROUP_COLOR_OPTIONS = DEFAULT_COLORS.slice(0, 5)
|
|
9
10
|
|
|
10
11
|
export function applySampleGroupColorEdit(
|
|
11
12
|
groups: SampleGroup[],
|
|
@@ -31,13 +32,17 @@ export function pickUnusedSampleGroupColor(groups: SampleGroup[]): string {
|
|
|
31
32
|
return DEFAULT_COLORS.find(color => !usedColors.has(color)) || DEFAULT_COLORS[0]
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
export function createSampleGroup(
|
|
35
|
+
export function createSampleGroup(
|
|
36
|
+
name: string,
|
|
37
|
+
existingGroups: SampleGroup[],
|
|
38
|
+
color?: string,
|
|
39
|
+
): SampleGroup | null {
|
|
35
40
|
const trimmedName = name.trim()
|
|
36
41
|
if (!trimmedName) return null
|
|
37
42
|
|
|
38
43
|
return {
|
|
39
44
|
name: trimmedName,
|
|
40
|
-
color: pickUnusedSampleGroupColor(existingGroups),
|
|
45
|
+
color: color || pickUnusedSampleGroupColor(existingGroups),
|
|
41
46
|
samples: [],
|
|
42
47
|
}
|
|
43
48
|
}
|
|
@@ -17,6 +17,29 @@ const mockGroups: SampleGroup[] = [
|
|
|
17
17
|
{ name: 'Vehicle', color: '#F59E0B', samples: ['Vehicle_Rep1', 'Vehicle_Rep2', 'Vehicle_Rep3'] },
|
|
18
18
|
]
|
|
19
19
|
|
|
20
|
+
const manualSamples = [
|
|
21
|
+
'Pt001_TumorA_d7_rep1',
|
|
22
|
+
'Pt001_TumorA_d7_rep1_reseq2024',
|
|
23
|
+
'Pt001_TumorA_d14_rep1',
|
|
24
|
+
'Pt002_TumorB_d7_rep1',
|
|
25
|
+
'Pt002_tumorb_d7_rep2',
|
|
26
|
+
'Pt002_TumorB_d14_rep1',
|
|
27
|
+
'Pt003_NoID_d7_rep1',
|
|
28
|
+
'Pt003_TumorC_d14_rep1_redo',
|
|
29
|
+
'Ctrl_pooled_old_naming',
|
|
30
|
+
'control_2_(redo)',
|
|
31
|
+
'Pt004_TumorA_d7_2024batch2',
|
|
32
|
+
'Pt004_TumorA_d14',
|
|
33
|
+
'QC_pool_01',
|
|
34
|
+
'blank_run3',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const manualGroups: SampleGroup[] = [
|
|
38
|
+
{ name: 'Responder/Day 7', color: '#0EA5A4', samples: ['Pt001_TumorA_d7_rep1'] },
|
|
39
|
+
{ name: 'Responder/Day 14', color: '#0EA5A4', samples: ['Pt001_TumorA_d14_rep1'] },
|
|
40
|
+
{ name: 'Non-responder/Day 7', color: '#F43F5E', samples: ['Pt002_TumorB_d7_rep1'] },
|
|
41
|
+
]
|
|
42
|
+
|
|
20
43
|
// Demonstrates same-hue gradient for sub-groups inside a major group:
|
|
21
44
|
// each Treatment_* gets a shade of the major group's color.
|
|
22
45
|
const hierarchicalSamples = [
|
|
@@ -83,6 +106,17 @@ function handleSmartGroup(result: AutoGroupResult, state: { groups: SampleGroup[
|
|
|
83
106
|
</div>
|
|
84
107
|
</Variant>
|
|
85
108
|
|
|
109
|
+
<Variant title="With Manual Group Button">
|
|
110
|
+
<div style="padding: 2rem; max-width: 1040px;">
|
|
111
|
+
<SampleSelector
|
|
112
|
+
:model-value="[]"
|
|
113
|
+
:samples="manualSamples"
|
|
114
|
+
:groups="manualGroups"
|
|
115
|
+
:enable-smart-group="false"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
</Variant>
|
|
119
|
+
|
|
86
120
|
<Variant title="Hierarchical Color Family">
|
|
87
121
|
<div style="padding: 2rem; max-width: 460px;">
|
|
88
122
|
<SampleSelector
|
|
@@ -54,6 +54,7 @@ const emit = defineEmits<{
|
|
|
54
54
|
|
|
55
55
|
// UI State
|
|
56
56
|
const showSmartGroupModal = ref(false)
|
|
57
|
+
const groupingModalMode = ref<'auto' | 'manual'>('auto')
|
|
57
58
|
const newGroupName = ref('')
|
|
58
59
|
const editingColor = ref<ColorEdit | null>(null)
|
|
59
60
|
const colorPickerInput = ref<HTMLInputElement | null>(null)
|
|
@@ -165,6 +166,11 @@ function handleSmartGroupApply(result: AutoGroupResult) {
|
|
|
165
166
|
emit('update:groups', result.groups)
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
function openGroupingModal(mode: 'auto' | 'manual') {
|
|
170
|
+
groupingModalMode.value = mode
|
|
171
|
+
showSmartGroupModal.value = true
|
|
172
|
+
}
|
|
173
|
+
|
|
168
174
|
// Group management
|
|
169
175
|
function clearGroups() {
|
|
170
176
|
internalGroups.value = []
|
|
@@ -247,7 +253,7 @@ defineExpose({ handleSmartGroupApply })
|
|
|
247
253
|
size="sm"
|
|
248
254
|
:disabled="resolvedSamples.length === 0"
|
|
249
255
|
class="mint-sample-selector__action-btn"
|
|
250
|
-
@click="
|
|
256
|
+
@click="openGroupingModal('auto')"
|
|
251
257
|
>
|
|
252
258
|
<svg class="mint-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
253
259
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
@@ -255,6 +261,19 @@ defineExpose({ handleSmartGroupApply })
|
|
|
255
261
|
<span>Smart Group</span>
|
|
256
262
|
</BaseButton>
|
|
257
263
|
|
|
264
|
+
<BaseButton
|
|
265
|
+
variant="secondary"
|
|
266
|
+
size="sm"
|
|
267
|
+
:disabled="resolvedSamples.length === 0"
|
|
268
|
+
class="mint-sample-selector__action-btn"
|
|
269
|
+
@click="openGroupingModal('manual')"
|
|
270
|
+
>
|
|
271
|
+
<svg class="mint-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
272
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3 3 7.5l9 4.5 9-4.5L12 3ZM3 12l9 4.5 9-4.5M3 16.5l9 4.5 9-4.5" />
|
|
273
|
+
</svg>
|
|
274
|
+
<span>Manual Group</span>
|
|
275
|
+
</BaseButton>
|
|
276
|
+
|
|
258
277
|
<!-- Reset Button -->
|
|
259
278
|
<BaseButton
|
|
260
279
|
variant="ghost"
|
|
@@ -695,6 +714,8 @@ defineExpose({ handleSmartGroupApply })
|
|
|
695
714
|
<AutoGroupModal
|
|
696
715
|
v-model="showSmartGroupModal"
|
|
697
716
|
:samples="resolvedSamples"
|
|
717
|
+
:groups="internalGroups"
|
|
718
|
+
:initial-mode="groupingModalMode"
|
|
698
719
|
:experiment-id="resolvedExperimentId"
|
|
699
720
|
:design-data="resolvedDesignData"
|
|
700
721
|
@apply="handleSmartGroupApply"
|
package/src/components/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ export { default as AppAvatarMenu } from './AppAvatarMenu.vue'
|
|
|
40
40
|
export { default as AppPluginSwitcher } from './AppPluginSwitcher.vue'
|
|
41
41
|
export { default as AppSidebar } from './AppSidebar.vue'
|
|
42
42
|
export { default as AppLayout } from './AppLayout.vue'
|
|
43
|
+
export { default as MobileSupportGate } from './MobileSupportGate.vue'
|
|
43
44
|
export { default as PluginWorkspaceView } from './PluginWorkspaceView.vue'
|
|
44
45
|
export { default as ComponentBindingRenderer } from './ComponentBindingRenderer.vue'
|
|
45
46
|
export { default as ControlWorkspaceView } from './ControlWorkspaceView.vue'
|
package/src/composables/index.ts
CHANGED
|
@@ -102,6 +102,14 @@ export {
|
|
|
102
102
|
type UseEventListenerObjectOptions,
|
|
103
103
|
type UseEventListenerOptions,
|
|
104
104
|
} from './useEventListener'
|
|
105
|
+
export {
|
|
106
|
+
DEFAULT_MOBILE_VIEWPORT_QUERY,
|
|
107
|
+
useMobileSupportGate,
|
|
108
|
+
type MobileSupportSource,
|
|
109
|
+
type MobileViewportQuerySource,
|
|
110
|
+
type UseMobileSupportGateOptions,
|
|
111
|
+
type UseMobileSupportGateReturn,
|
|
112
|
+
} from './useMobileSupportGate'
|
|
105
113
|
export {
|
|
106
114
|
useDebouncedWatch,
|
|
107
115
|
type DebouncedWatchCallback,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { computed, onMounted, onUnmounted, ref, unref, watch, type ComputedRef, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_MOBILE_VIEWPORT_QUERY = '(max-width: 767px)'
|
|
4
|
+
|
|
5
|
+
export type MobileSupportSource = boolean | Ref<boolean> | ComputedRef<boolean>
|
|
6
|
+
export type MobileViewportQuerySource = string | Ref<string> | ComputedRef<string>
|
|
7
|
+
|
|
8
|
+
export interface UseMobileSupportGateOptions {
|
|
9
|
+
supported?: MobileSupportSource
|
|
10
|
+
mediaQuery?: MobileViewportQuerySource
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseMobileSupportGateReturn {
|
|
14
|
+
isMobileViewport: Ref<boolean>
|
|
15
|
+
isSupported: ComputedRef<boolean>
|
|
16
|
+
shouldShowUnsupported: ComputedRef<boolean>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useMobileSupportGate(options: UseMobileSupportGateOptions = {}): UseMobileSupportGateReturn {
|
|
20
|
+
const isMobileViewport = ref(false)
|
|
21
|
+
|
|
22
|
+
const mediaQuery = computed(() => {
|
|
23
|
+
const value = unref(options.mediaQuery)
|
|
24
|
+
return value?.trim() || DEFAULT_MOBILE_VIEWPORT_QUERY
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const isSupported = computed(() => {
|
|
28
|
+
if (options.supported === undefined) return true
|
|
29
|
+
return Boolean(unref(options.supported))
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const shouldShowUnsupported = computed(() => isMobileViewport.value && !isSupported.value)
|
|
33
|
+
|
|
34
|
+
let mediaQueryList: MediaQueryList | null = null
|
|
35
|
+
let removeMediaListener: (() => void) | undefined
|
|
36
|
+
|
|
37
|
+
const syncMobileState = (event?: MediaQueryListEvent | MediaQueryList) => {
|
|
38
|
+
isMobileViewport.value = Boolean(event?.matches ?? mediaQueryList?.matches)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const unbindMediaQuery = () => {
|
|
42
|
+
removeMediaListener?.()
|
|
43
|
+
removeMediaListener = undefined
|
|
44
|
+
mediaQueryList = null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const bindMediaQuery = () => {
|
|
48
|
+
unbindMediaQuery()
|
|
49
|
+
|
|
50
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
51
|
+
isMobileViewport.value = false
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const queryList = window.matchMedia(mediaQuery.value)
|
|
56
|
+
mediaQueryList = queryList
|
|
57
|
+
syncMobileState(queryList)
|
|
58
|
+
|
|
59
|
+
const handleChange = (event: MediaQueryListEvent) => syncMobileState(event)
|
|
60
|
+
|
|
61
|
+
if (typeof queryList.addEventListener === 'function') {
|
|
62
|
+
queryList.addEventListener('change', handleChange)
|
|
63
|
+
removeMediaListener = () => queryList.removeEventListener('change', handleChange)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
queryList.addListener(handleChange)
|
|
68
|
+
removeMediaListener = () => queryList.removeListener(handleChange)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
onMounted(bindMediaQuery)
|
|
72
|
+
watch(mediaQuery, bindMediaQuery, { flush: 'post' })
|
|
73
|
+
onUnmounted(unbindMediaQuery)
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
isMobileViewport,
|
|
77
|
+
isSupported,
|
|
78
|
+
shouldShowUnsupported,
|
|
79
|
+
}
|
|
80
|
+
}
|