@morscherlab/mint-sdk 1.0.15 → 1.0.17
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-CzdeV1xe.js} +1122 -535
- package/dist/components-CzdeV1xe.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 +4004 -2537
- 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 +45 -1
- package/src/components/SampleSelector.vue +32 -6
- 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 +758 -0
- package/src/styles/components/mobile-support-gate.css +119 -0
- package/src/styles/components/sample-selector.css +23 -9
- 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 = [
|
|
@@ -69,7 +92,7 @@ function handleSmartGroup(result: AutoGroupResult, state: { groups: SampleGroup[
|
|
|
69
92
|
</template>
|
|
70
93
|
<template #controls="{ state }">
|
|
71
94
|
<HstCheckbox v-model="state.enableGrouping" title="Enable Grouping" />
|
|
72
|
-
<HstCheckbox v-model="state.enableSmartGroup" title="Enable Smart
|
|
95
|
+
<HstCheckbox v-model="state.enableSmartGroup" title="Enable Smart" />
|
|
73
96
|
</template>
|
|
74
97
|
</Variant>
|
|
75
98
|
|
|
@@ -83,6 +106,27 @@ function handleSmartGroup(result: AutoGroupResult, state: { groups: SampleGroup[
|
|
|
83
106
|
</div>
|
|
84
107
|
</Variant>
|
|
85
108
|
|
|
109
|
+
<Variant title="Narrow Action Bar">
|
|
110
|
+
<div style="padding: 2rem; width: 320px;">
|
|
111
|
+
<SampleSelector
|
|
112
|
+
:model-value="['Control_Rep1']"
|
|
113
|
+
:samples="mockSamples"
|
|
114
|
+
:groups="mockGroups"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
</Variant>
|
|
118
|
+
|
|
119
|
+
<Variant title="With Manual Button">
|
|
120
|
+
<div style="padding: 2rem; max-width: 1040px;">
|
|
121
|
+
<SampleSelector
|
|
122
|
+
:model-value="[]"
|
|
123
|
+
:samples="manualSamples"
|
|
124
|
+
:groups="manualGroups"
|
|
125
|
+
:enable-smart-group="false"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
</Variant>
|
|
129
|
+
|
|
86
130
|
<Variant title="Hierarchical Color Family">
|
|
87
131
|
<div style="padding: 2rem; max-width: 460px;">
|
|
88
132
|
<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 = []
|
|
@@ -239,20 +245,38 @@ defineExpose({ handleSmartGroupApply })
|
|
|
239
245
|
|
|
240
246
|
<!-- Action Buttons Row -->
|
|
241
247
|
<div v-if="enableGrouping" class="mint-sample-selector__actions">
|
|
242
|
-
<div
|
|
243
|
-
|
|
248
|
+
<div
|
|
249
|
+
:class="[
|
|
250
|
+
'mint-sample-selector__actions-row',
|
|
251
|
+
!enableSmartGroup ? 'mint-sample-selector__actions-row--single-primary' : '',
|
|
252
|
+
]"
|
|
253
|
+
>
|
|
254
|
+
<!-- Smart Button -->
|
|
244
255
|
<BaseButton
|
|
245
256
|
v-if="enableSmartGroup"
|
|
246
257
|
:variant="groupingEnabled ? 'primary' : 'secondary'"
|
|
247
258
|
size="sm"
|
|
248
259
|
:disabled="resolvedSamples.length === 0"
|
|
249
260
|
class="mint-sample-selector__action-btn"
|
|
250
|
-
@click="
|
|
261
|
+
@click="openGroupingModal('auto')"
|
|
251
262
|
>
|
|
252
263
|
<svg class="mint-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
253
264
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
254
265
|
</svg>
|
|
255
|
-
<span>Smart
|
|
266
|
+
<span class="mint-sample-selector__action-label">Smart</span>
|
|
267
|
+
</BaseButton>
|
|
268
|
+
|
|
269
|
+
<BaseButton
|
|
270
|
+
variant="secondary"
|
|
271
|
+
size="sm"
|
|
272
|
+
:disabled="resolvedSamples.length === 0"
|
|
273
|
+
class="mint-sample-selector__action-btn"
|
|
274
|
+
@click="openGroupingModal('manual')"
|
|
275
|
+
>
|
|
276
|
+
<svg class="mint-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
277
|
+
<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" />
|
|
278
|
+
</svg>
|
|
279
|
+
<span class="mint-sample-selector__action-label">Manual</span>
|
|
256
280
|
</BaseButton>
|
|
257
281
|
|
|
258
282
|
<!-- Reset Button -->
|
|
@@ -580,7 +604,7 @@ defineExpose({ handleSmartGroupApply })
|
|
|
580
604
|
|
|
581
605
|
<!-- Empty state -->
|
|
582
606
|
<div v-if="internalGroups.length === 0" class="mint-sample-selector__empty">
|
|
583
|
-
Click Smart
|
|
607
|
+
Click Smart to auto-group samples
|
|
584
608
|
</div>
|
|
585
609
|
</div>
|
|
586
610
|
|
|
@@ -691,10 +715,12 @@ defineExpose({ handleSmartGroupApply })
|
|
|
691
715
|
</div>
|
|
692
716
|
</div>
|
|
693
717
|
|
|
694
|
-
<!-- Smart
|
|
718
|
+
<!-- Smart grouping modal -->
|
|
695
719
|
<AutoGroupModal
|
|
696
720
|
v-model="showSmartGroupModal"
|
|
697
721
|
:samples="resolvedSamples"
|
|
722
|
+
:groups="internalGroups"
|
|
723
|
+
:initial-mode="groupingModalMode"
|
|
698
724
|
:experiment-id="resolvedExperimentId"
|
|
699
725
|
:design-data="resolvedDesignData"
|
|
700
726
|
@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
|
+
}
|