@morscherlab/mint-sdk 1.1.0-beta.1 → 1.1.0-beta.3
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/__tests__/components/SectionCard.test.d.ts +1 -0
- package/dist/components/SectionCard.vue.d.ts +47 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +2 -2
- package/dist/{components-UfkPN1bK.js → components-DFH5whuh.js} +699 -620
- package/dist/components-DFH5whuh.js.map +1 -0
- package/dist/composables/index.js +1 -1
- package/dist/composables/usePlatformContext.d.ts +3 -0
- package/dist/{composables-F4JFK9K1.js → composables-B512bWJP.js} +133 -26
- package/dist/composables-B512bWJP.js.map +1 -0
- package/dist/index.js +3 -3
- package/dist/install.js +1 -1
- package/dist/styles.css +301 -0
- package/dist/tailwind.preset.d.ts +5 -0
- package/dist/tailwind.preset.js +7 -2
- package/dist/tailwind.preset.js.map +1 -1
- package/dist/types/components.d.ts +2 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/platform.d.ts +2 -0
- package/package.json +1 -1
- package/src/__tests__/components/SectionCard.test.ts +181 -0
- package/src/__tests__/composables/usePlatformContext.test.ts +28 -0
- package/src/__tests__/composables/usePluginClient.test.ts +329 -20
- package/src/__tests__/composables/usePluginConfig.test.ts +323 -7
- package/src/components/SectionCard.story.vue +163 -0
- package/src/components/SectionCard.vue +98 -0
- package/src/components/index.ts +1 -0
- package/src/composables/usePluginClient.ts +118 -15
- package/src/composables/usePluginConfig.ts +156 -20
- package/src/styles/components/section-card.css +161 -0
- package/src/styles/index.css +1 -0
- package/src/styles/variables.css +25 -0
- package/src/tailwind.preset.ts +5 -0
- package/src/types/components.ts +12 -0
- package/src/types/index.ts +2 -0
- package/src/types/platform.ts +2 -0
- package/dist/components-UfkPN1bK.js.map +0 -1
- package/dist/composables-F4JFK9K1.js.map +0 -1
|
@@ -546,8 +546,14 @@ function normalizePluginSettingsOptions(options: UsePluginSettingsOptions | stri
|
|
|
546
546
|
return typeof options === 'string' ? { pluginName: options } : options
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
+
const PLATFORM_OWNED_SETTINGS_KEY = 'allowed_experiment_types'
|
|
550
|
+
|
|
549
551
|
function cloneRecord(value: Record<string, unknown>): Record<string, unknown> {
|
|
550
|
-
return
|
|
552
|
+
return JSON.parse(JSON.stringify(value)) as Record<string, unknown>
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function hasOwnSetting(settings: Record<string, unknown>, key: string): boolean {
|
|
556
|
+
return Object.prototype.hasOwnProperty.call(settings, key)
|
|
551
557
|
}
|
|
552
558
|
|
|
553
559
|
function responseSettingsPayload(response: unknown): Record<string, unknown> {
|
|
@@ -565,6 +571,43 @@ function responseSettingsPayload(response: unknown): Record<string, unknown> {
|
|
|
565
571
|
return {}
|
|
566
572
|
}
|
|
567
573
|
|
|
574
|
+
function responseSettingsRevision(response: unknown): string | null {
|
|
575
|
+
if (!response || typeof response !== 'object') return null
|
|
576
|
+
const revision = (response as { revision?: unknown }).revision
|
|
577
|
+
return typeof revision === 'string' && revision ? revision : null
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function topLevelSettingsPatch(
|
|
581
|
+
current: Record<string, unknown>,
|
|
582
|
+
keys: Iterable<string>,
|
|
583
|
+
): Record<string, unknown> {
|
|
584
|
+
return Object.fromEntries(
|
|
585
|
+
[...keys].map(key => [
|
|
586
|
+
key,
|
|
587
|
+
hasOwnSetting(current, key) ? current[key] : null,
|
|
588
|
+
]),
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function changedTopLevelSettingKeys(
|
|
593
|
+
current: Record<string, unknown>,
|
|
594
|
+
saved: Record<string, unknown>,
|
|
595
|
+
): Set<string> {
|
|
596
|
+
return new Set(
|
|
597
|
+
[...new Set([...Object.keys(current), ...Object.keys(saved)])].filter(
|
|
598
|
+
key => JSON.stringify(current[key]) !== JSON.stringify(saved[key]),
|
|
599
|
+
),
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function platformOwnedSettingsPatch(settings: Record<string, unknown>): Record<string, unknown> {
|
|
604
|
+
return {
|
|
605
|
+
[PLATFORM_OWNED_SETTINGS_KEY]: hasOwnSetting(settings, PLATFORM_OWNED_SETTINGS_KEY)
|
|
606
|
+
? settings[PLATFORM_OWNED_SETTINGS_KEY]
|
|
607
|
+
: null,
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
568
611
|
/** Load, edit, and persist plugin settings from platform config or the plugin-local settings router. */
|
|
569
612
|
export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
570
613
|
options: UsePluginSettingsOptions | string = {},
|
|
@@ -575,8 +618,9 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
575
618
|
const platformApi = useApi({ baseUrl: resolvedOptions.platformApiBaseUrl ?? injectedContext?.platformApiUrl })
|
|
576
619
|
const pluginApi = useApi({ baseUrl: resolvedOptions.apiBaseUrl ?? injectedContext?.plugin?.api_prefix })
|
|
577
620
|
|
|
578
|
-
const values =
|
|
621
|
+
const values = ref({}) as Ref<PluginSettingsValues<TSettings>>
|
|
579
622
|
const savedValues = shallowRef<Record<string, unknown>>({})
|
|
623
|
+
const revision = ref<string | null>(null)
|
|
580
624
|
const request = useRequestSyncState('Plugin settings request failed.')
|
|
581
625
|
const isLoading = ref(false)
|
|
582
626
|
const isSaving = ref(false)
|
|
@@ -584,8 +628,17 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
584
628
|
const resolvedPluginName = computed(() =>
|
|
585
629
|
resolvedOptions.pluginName ?? plugin.value?.name ?? injectedContext?.plugin?.name ?? '',
|
|
586
630
|
)
|
|
631
|
+
const usesPluginSettingsApi = computed(() => {
|
|
632
|
+
const currentPlugin = injectedContext?.plugin ?? plugin.value
|
|
633
|
+
return currentPlugin?.settings_api === 'plugin'
|
|
634
|
+
&& resolvedPluginName.value === currentPlugin.name
|
|
635
|
+
})
|
|
587
636
|
const usesPlatformConfig = computed(() =>
|
|
588
|
-
Boolean(
|
|
637
|
+
Boolean(
|
|
638
|
+
(isIntegrated.value || injectedContext?.isIntegrated)
|
|
639
|
+
&& resolvedPluginName.value
|
|
640
|
+
&& !usesPluginSettingsApi.value,
|
|
641
|
+
),
|
|
589
642
|
)
|
|
590
643
|
const settings = computed(() => values.value)
|
|
591
644
|
const isDirty = computed(() => JSON.stringify(values.value) !== JSON.stringify(savedValues.value))
|
|
@@ -600,10 +653,11 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
600
653
|
values.value = cloneRecord(nextValues) as PluginSettingsValues<TSettings>
|
|
601
654
|
}
|
|
602
655
|
|
|
603
|
-
function applyLoaded(nextValues: Record<string, unknown
|
|
656
|
+
function applyLoaded(nextValues: Record<string, unknown>, nextRevision: string | null): void {
|
|
604
657
|
const cloned = cloneRecord(nextValues)
|
|
605
658
|
values.value = cloned as PluginSettingsValues<TSettings>
|
|
606
659
|
savedValues.value = cloneRecord(cloned)
|
|
660
|
+
revision.value = nextRevision
|
|
607
661
|
}
|
|
608
662
|
|
|
609
663
|
async function load(): Promise<void> {
|
|
@@ -620,7 +674,10 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
620
674
|
() => pluginApi.get<{ settings: Record<string, unknown>; mode?: string }>('/settings'),
|
|
621
675
|
{ success: 'load', errorMessage: 'Failed to load plugin settings' },
|
|
622
676
|
)
|
|
623
|
-
applyLoaded(
|
|
677
|
+
applyLoaded(
|
|
678
|
+
responseSettingsPayload(response),
|
|
679
|
+
responseSettingsRevision(response),
|
|
680
|
+
)
|
|
624
681
|
} catch {
|
|
625
682
|
// Error state is handled by request.run().
|
|
626
683
|
} finally {
|
|
@@ -636,19 +693,65 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
636
693
|
isSaving.value = true
|
|
637
694
|
try {
|
|
638
695
|
const payload = cloneRecord(values.value)
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
{ success: 'save', errorMessage: 'Failed to save plugin settings' },
|
|
696
|
+
let response: unknown
|
|
697
|
+
if (usesPlatformConfig.value) {
|
|
698
|
+
const dirtyKeys = changedTopLevelSettingKeys(payload, savedValues.value)
|
|
699
|
+
const platformOwnedChanged = dirtyKeys.has(PLATFORM_OWNED_SETTINGS_KEY)
|
|
700
|
+
const typedKeys = [...dirtyKeys].filter(
|
|
701
|
+
key => key !== PLATFORM_OWNED_SETTINGS_KEY,
|
|
646
702
|
)
|
|
647
|
-
|
|
648
|
-
|
|
703
|
+
let nextRevision = revision.value
|
|
704
|
+
|
|
705
|
+
if (typedKeys.length) {
|
|
706
|
+
response = await request.run(
|
|
707
|
+
() => platformApi.patch<{
|
|
708
|
+
plugin_name: string
|
|
709
|
+
config: Record<string, unknown>
|
|
710
|
+
revision?: string
|
|
711
|
+
}>(
|
|
712
|
+
`/plugins/${encodeURIComponent(resolvedPluginName.value)}/config`,
|
|
713
|
+
{ config: topLevelSettingsPatch(payload, typedKeys) },
|
|
714
|
+
),
|
|
715
|
+
{ success: 'save', errorMessage: 'Failed to save plugin settings' },
|
|
716
|
+
)
|
|
717
|
+
nextRevision = responseSettingsRevision(response) ?? nextRevision
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (platformOwnedChanged) {
|
|
721
|
+
response = await request.run(
|
|
722
|
+
() => platformApi.patch<{
|
|
723
|
+
plugin_name: string
|
|
724
|
+
config: Record<string, unknown>
|
|
725
|
+
revision?: string
|
|
726
|
+
}>(
|
|
727
|
+
`/plugins/${encodeURIComponent(resolvedPluginName.value)}/config`,
|
|
728
|
+
{ config: platformOwnedSettingsPatch(payload) },
|
|
729
|
+
),
|
|
730
|
+
{ success: 'save', errorMessage: 'Failed to save plugin settings' },
|
|
731
|
+
)
|
|
732
|
+
nextRevision = responseSettingsRevision(response) ?? nextRevision
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (!response) return true
|
|
736
|
+
applyLoaded(responseSettingsPayload(response), nextRevision)
|
|
737
|
+
return true
|
|
738
|
+
} else {
|
|
739
|
+
const ifMatch = revision.value
|
|
740
|
+
? { headers: { 'If-Match': `"${revision.value}"` } }
|
|
741
|
+
: undefined
|
|
742
|
+
response = await request.run(
|
|
743
|
+
() => pluginApi.put<{
|
|
744
|
+
settings: Record<string, unknown>
|
|
745
|
+
mode?: string
|
|
746
|
+
revision?: string
|
|
747
|
+
}>('/settings', payload, ifMatch),
|
|
649
748
|
{ success: 'save', errorMessage: 'Failed to save plugin settings' },
|
|
650
749
|
)
|
|
651
|
-
|
|
750
|
+
}
|
|
751
|
+
applyLoaded(
|
|
752
|
+
responseSettingsPayload(response),
|
|
753
|
+
responseSettingsRevision(response) ?? revision.value,
|
|
754
|
+
)
|
|
652
755
|
return true
|
|
653
756
|
} catch {
|
|
654
757
|
return false
|
|
@@ -1,8 +1,57 @@
|
|
|
1
1
|
import { ref, computed, onMounted, type Ref, type ComputedRef } from 'vue'
|
|
2
2
|
import { useApi } from './useApi'
|
|
3
3
|
import { usePlatformContext } from './usePlatformContext'
|
|
4
|
+
import { getInjectedPlatformContext } from './platformContextHelpers'
|
|
4
5
|
import { useRequestSyncState } from './useRequestSyncState'
|
|
5
6
|
|
|
7
|
+
const PLATFORM_OWNED_SETTINGS_KEY = 'allowed_experiment_types'
|
|
8
|
+
|
|
9
|
+
function cloneConfig(config: Record<string, unknown>): Record<string, unknown> {
|
|
10
|
+
return JSON.parse(JSON.stringify(config)) as Record<string, unknown>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hasOwnConfig(config: Record<string, unknown>, key: string): boolean {
|
|
14
|
+
return Object.prototype.hasOwnProperty.call(config, key)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function changedTopLevelKeys(
|
|
18
|
+
current: Record<string, unknown>,
|
|
19
|
+
saved: Record<string, unknown>,
|
|
20
|
+
): Set<string> {
|
|
21
|
+
return new Set(
|
|
22
|
+
[...new Set([...Object.keys(current), ...Object.keys(saved)])].filter(
|
|
23
|
+
key => JSON.stringify(current[key]) !== JSON.stringify(saved[key]),
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function topLevelConfigPatch(
|
|
29
|
+
current: Record<string, unknown>,
|
|
30
|
+
keys: Iterable<string>,
|
|
31
|
+
): Record<string, unknown> {
|
|
32
|
+
return Object.fromEntries(
|
|
33
|
+
[...keys].map(key => [
|
|
34
|
+
key,
|
|
35
|
+
hasOwnConfig(current, key) ? current[key] : null,
|
|
36
|
+
]),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function platformOwnedConfigPatch(config: Record<string, unknown>): Record<string, unknown> {
|
|
41
|
+
return {
|
|
42
|
+
[PLATFORM_OWNED_SETTINGS_KEY]: hasOwnConfig(config, PLATFORM_OWNED_SETTINGS_KEY)
|
|
43
|
+
? config[PLATFORM_OWNED_SETTINGS_KEY]
|
|
44
|
+
: null,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function responseConfigPayload(response: {
|
|
49
|
+
config?: Record<string, unknown>
|
|
50
|
+
settings?: Record<string, unknown>
|
|
51
|
+
}): Record<string, unknown> {
|
|
52
|
+
return response.config ?? response.settings ?? {}
|
|
53
|
+
}
|
|
54
|
+
|
|
6
55
|
export interface UsePluginConfigReturn {
|
|
7
56
|
/** Editable plugin configuration values. */
|
|
8
57
|
config: Ref<Record<string, unknown>>
|
|
@@ -28,13 +77,25 @@ export interface UsePluginConfigReturn {
|
|
|
28
77
|
|
|
29
78
|
/** Loads, saves, and tracks dirty state for a plugin's persistent configuration via the platform API. */
|
|
30
79
|
export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
|
|
31
|
-
const
|
|
80
|
+
const injectedContext = getInjectedPlatformContext()
|
|
32
81
|
const { plugin } = usePlatformContext()
|
|
82
|
+
const platformApi = useApi()
|
|
83
|
+
const pluginApi = useApi({
|
|
84
|
+
baseUrl: injectedContext?.plugin?.api_prefix ?? plugin.value?.api_prefix,
|
|
85
|
+
})
|
|
33
86
|
|
|
34
|
-
const resolvedName = computed(
|
|
87
|
+
const resolvedName = computed(
|
|
88
|
+
() => pluginName ?? plugin.value?.name ?? injectedContext?.plugin?.name ?? '',
|
|
89
|
+
)
|
|
90
|
+
const usesPluginSettingsApi = computed(() => {
|
|
91
|
+
const currentPlugin = injectedContext?.plugin ?? plugin.value
|
|
92
|
+
return currentPlugin?.settings_api === 'plugin'
|
|
93
|
+
&& resolvedName.value === currentPlugin.name
|
|
94
|
+
})
|
|
35
95
|
|
|
36
96
|
const config = ref<Record<string, unknown>>({})
|
|
37
97
|
const savedConfig = ref<Record<string, unknown>>({})
|
|
98
|
+
const revision = ref<string | null>(null)
|
|
38
99
|
const request = useRequestSyncState('Plugin config request failed.')
|
|
39
100
|
const isLoading = ref(false)
|
|
40
101
|
const isSaving = ref(false)
|
|
@@ -52,14 +113,29 @@ export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
|
|
|
52
113
|
|
|
53
114
|
isLoading.value = true
|
|
54
115
|
try {
|
|
55
|
-
const response =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
116
|
+
const response = usesPluginSettingsApi.value
|
|
117
|
+
? await request.run(
|
|
118
|
+
() => pluginApi.get<{
|
|
119
|
+
settings: Record<string, unknown>
|
|
120
|
+
mode?: string
|
|
121
|
+
revision?: string
|
|
122
|
+
}>('/settings'),
|
|
123
|
+
{ success: 'load', errorMessage: 'Failed to load plugin config' },
|
|
124
|
+
)
|
|
125
|
+
: await request.run(
|
|
126
|
+
() => platformApi.get<{
|
|
127
|
+
plugin_name: string
|
|
128
|
+
config: Record<string, unknown>
|
|
129
|
+
revision?: string
|
|
130
|
+
}>(
|
|
131
|
+
`/plugins/${encodeURIComponent(name)}/config`,
|
|
132
|
+
),
|
|
133
|
+
{ success: 'load', errorMessage: 'Failed to load plugin config' },
|
|
134
|
+
)
|
|
135
|
+
const responseConfig = responseConfigPayload(response)
|
|
136
|
+
config.value = cloneConfig(responseConfig)
|
|
137
|
+
savedConfig.value = cloneConfig(responseConfig)
|
|
138
|
+
revision.value = response.revision ?? null
|
|
63
139
|
} catch {
|
|
64
140
|
// Error state is handled by request.run().
|
|
65
141
|
} finally {
|
|
@@ -73,15 +149,75 @@ export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
|
|
|
73
149
|
|
|
74
150
|
isSaving.value = true
|
|
75
151
|
try {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
152
|
+
const payload = cloneConfig(config.value)
|
|
153
|
+
let response: {
|
|
154
|
+
plugin_name?: string
|
|
155
|
+
config?: Record<string, unknown>
|
|
156
|
+
settings?: Record<string, unknown>
|
|
157
|
+
mode?: string
|
|
158
|
+
revision?: string
|
|
159
|
+
} | undefined
|
|
160
|
+
let nextRevision = revision.value
|
|
161
|
+
|
|
162
|
+
if (usesPluginSettingsApi.value) {
|
|
163
|
+
response = await request.run(
|
|
164
|
+
() => pluginApi.put<{
|
|
165
|
+
settings: Record<string, unknown>
|
|
166
|
+
mode?: string
|
|
167
|
+
revision?: string
|
|
168
|
+
}>(
|
|
169
|
+
'/settings',
|
|
170
|
+
payload,
|
|
171
|
+
revision.value
|
|
172
|
+
? { headers: { 'If-Match': `"${revision.value}"` } }
|
|
173
|
+
: undefined,
|
|
174
|
+
),
|
|
175
|
+
{ success: 'save', errorMessage: 'Failed to save plugin config' },
|
|
176
|
+
)
|
|
177
|
+
nextRevision = response.revision ?? nextRevision
|
|
178
|
+
} else {
|
|
179
|
+
const dirtyKeys = changedTopLevelKeys(payload, savedConfig.value)
|
|
180
|
+
const platformOwnedChanged = dirtyKeys.has(PLATFORM_OWNED_SETTINGS_KEY)
|
|
181
|
+
const typedKeys = [...dirtyKeys].filter(
|
|
182
|
+
key => key !== PLATFORM_OWNED_SETTINGS_KEY,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if (typedKeys.length) {
|
|
186
|
+
response = await request.run(
|
|
187
|
+
() => platformApi.patch<{
|
|
188
|
+
plugin_name: string
|
|
189
|
+
config: Record<string, unknown>
|
|
190
|
+
revision?: string
|
|
191
|
+
}>(
|
|
192
|
+
`/plugins/${encodeURIComponent(name)}/config`,
|
|
193
|
+
{ config: topLevelConfigPatch(payload, typedKeys) },
|
|
194
|
+
),
|
|
195
|
+
{ success: 'save', errorMessage: 'Failed to save plugin config' },
|
|
196
|
+
)
|
|
197
|
+
nextRevision = response.revision ?? nextRevision
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (platformOwnedChanged) {
|
|
201
|
+
response = await request.run(
|
|
202
|
+
() => platformApi.patch<{
|
|
203
|
+
plugin_name: string
|
|
204
|
+
config: Record<string, unknown>
|
|
205
|
+
revision?: string
|
|
206
|
+
}>(
|
|
207
|
+
`/plugins/${encodeURIComponent(name)}/config`,
|
|
208
|
+
{ config: platformOwnedConfigPatch(payload) },
|
|
209
|
+
),
|
|
210
|
+
{ success: 'save', errorMessage: 'Failed to save plugin config' },
|
|
211
|
+
)
|
|
212
|
+
nextRevision = response.revision ?? nextRevision
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!response) return true
|
|
217
|
+
const responseConfig = responseConfigPayload(response)
|
|
218
|
+
config.value = cloneConfig(responseConfig)
|
|
219
|
+
savedConfig.value = cloneConfig(responseConfig)
|
|
220
|
+
revision.value = response.revision ?? nextRevision
|
|
85
221
|
return true
|
|
86
222
|
} catch {
|
|
87
223
|
return false
|
|
@@ -91,7 +227,7 @@ export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
|
|
|
91
227
|
}
|
|
92
228
|
|
|
93
229
|
function reset(): void {
|
|
94
|
-
config.value =
|
|
230
|
+
config.value = cloneConfig(savedConfig.value)
|
|
95
231
|
request.clearError()
|
|
96
232
|
}
|
|
97
233
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/* SectionCard — the MINT 1.1 one-card system.
|
|
2
|
+
*
|
|
3
|
+
* One card per page region: 12px radius, one shadow token (--card-shadow),
|
|
4
|
+
* a single header strip, and hairline-separated regions inside — never a
|
|
5
|
+
* card nested in a card.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.mint-section-card {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
border-radius: 12px;
|
|
12
|
+
border: 1px solid var(--border-color);
|
|
13
|
+
background: var(--bg-secondary);
|
|
14
|
+
box-shadow: var(--card-shadow);
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
transition: background-color var(--mint-transition), border-color var(--mint-transition);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Fill the height the parent offers: the body takes the slack and scrolls,
|
|
20
|
+
* so the header and footer stay pinned to the card's edges. */
|
|
21
|
+
.mint-section-card--fill {
|
|
22
|
+
flex: 1;
|
|
23
|
+
min-height: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.mint-section-card--fill .mint-section-card__body {
|
|
27
|
+
flex: 1;
|
|
28
|
+
min-height: 0;
|
|
29
|
+
overflow: auto;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.mint-section-card__header {
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: 0.625rem;
|
|
36
|
+
padding: 0.6875rem 1rem;
|
|
37
|
+
border-bottom: 1px solid var(--border-color);
|
|
38
|
+
flex-shrink: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.mint-section-card__header--divider-light {
|
|
42
|
+
border-bottom-color: var(--border-light);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.mint-section-card__icon-chip {
|
|
46
|
+
display: inline-flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
flex-shrink: 0;
|
|
50
|
+
width: 1.375rem;
|
|
51
|
+
height: 1.375rem;
|
|
52
|
+
border-radius: 6px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.mint-section-card__icon {
|
|
56
|
+
width: 0.8125rem;
|
|
57
|
+
height: 0.8125rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.mint-section-card__icon-chip--primary {
|
|
61
|
+
background: var(--color-primary-soft);
|
|
62
|
+
color: var(--color-primary);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.mint-section-card__icon-chip--experiment {
|
|
66
|
+
background: var(--color-experiment-soft);
|
|
67
|
+
color: var(--color-experiment);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.mint-section-card__icon-chip--project {
|
|
71
|
+
background: var(--color-project-soft);
|
|
72
|
+
color: var(--color-project);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.mint-section-card__icon-chip--success {
|
|
76
|
+
background: var(--mint-success-bg);
|
|
77
|
+
color: var(--mint-success);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.mint-section-card__icon-chip--error {
|
|
81
|
+
background: var(--mint-error-bg);
|
|
82
|
+
color: var(--mint-error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.mint-section-card__icon-chip--warning {
|
|
86
|
+
background: var(--mint-warning-bg);
|
|
87
|
+
color: var(--mint-warning);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.mint-section-card__icon-chip--info {
|
|
91
|
+
background: var(--mint-info-bg);
|
|
92
|
+
color: var(--mint-info);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.mint-section-card__title {
|
|
96
|
+
margin: 0;
|
|
97
|
+
font-size: 0.8125rem;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
color: var(--text-primary);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.mint-section-card__title--uppercase {
|
|
103
|
+
font-size: 0.75rem;
|
|
104
|
+
letter-spacing: 0.08em;
|
|
105
|
+
text-transform: uppercase;
|
|
106
|
+
color: var(--text-secondary);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.mint-section-card__count {
|
|
110
|
+
font-family: 'Fira Code', monospace;
|
|
111
|
+
font-size: 0.6875rem;
|
|
112
|
+
color: var(--text-muted);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.mint-section-card__spacer {
|
|
116
|
+
flex: 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.mint-section-card__toolbar {
|
|
120
|
+
flex-shrink: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.mint-section-card__footer {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: space-between;
|
|
127
|
+
padding: 0.5rem 1rem;
|
|
128
|
+
border-top: 1px solid var(--border-color);
|
|
129
|
+
background: var(--bg-primary);
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Hairline row helpers — regions inside a card separate with 1px lines,
|
|
134
|
+
* rendered by a 1px gap over a hairline-colored track. */
|
|
135
|
+
.mint-section-card__rows {
|
|
136
|
+
display: flex;
|
|
137
|
+
flex-direction: column;
|
|
138
|
+
gap: 1px;
|
|
139
|
+
background: var(--border-light);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.mint-section-card__row {
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
gap: 0.625rem;
|
|
146
|
+
padding: 0.5625rem 1rem;
|
|
147
|
+
background: var(--bg-secondary);
|
|
148
|
+
transition: background-color var(--mint-transition);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
a.mint-section-card__row,
|
|
152
|
+
button.mint-section-card__row {
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
color: inherit;
|
|
155
|
+
text-decoration: none;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
a.mint-section-card__row:hover,
|
|
159
|
+
button.mint-section-card__row:hover {
|
|
160
|
+
background: var(--bg-primary);
|
|
161
|
+
}
|
package/src/styles/index.css
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
@import './components/plate-map-editor.css';
|
|
29
29
|
@import './components/radio-group.css';
|
|
30
30
|
@import './components/sample-legend.css';
|
|
31
|
+
@import './components/section-card.css';
|
|
31
32
|
@import './components/select.css';
|
|
32
33
|
@import './components/skeleton.css';
|
|
33
34
|
@import './components/slider.css';
|
package/src/styles/variables.css
CHANGED
|
@@ -36,6 +36,18 @@
|
|
|
36
36
|
--color-cta-hover: #EA580C;
|
|
37
37
|
--color-purple: #8B5CF6;
|
|
38
38
|
|
|
39
|
+
/* Entity accent colors — experiments (cyan) and projects (sky), from the
|
|
40
|
+
MINT 1.1 card system. -soft backs icon chips and pills, -border outlines
|
|
41
|
+
pills that sit on a card surface. */
|
|
42
|
+
--color-experiment: #06B6D4;
|
|
43
|
+
--color-experiment-hover: #0891B2;
|
|
44
|
+
--color-experiment-soft: rgba(6, 182, 212, 0.12);
|
|
45
|
+
--color-experiment-border: rgba(6, 182, 212, 0.3);
|
|
46
|
+
--color-project: #0EA5E9;
|
|
47
|
+
--color-project-hover: #0284C7;
|
|
48
|
+
--color-project-soft: rgba(14, 165, 233, 0.12);
|
|
49
|
+
--color-project-border: rgba(14, 165, 233, 0.32);
|
|
50
|
+
|
|
39
51
|
/* MINT brand mark — used for browser theme-color, marketing chrome, app icon background */
|
|
40
52
|
--mint-brand: #7BD0B5;
|
|
41
53
|
--mint-brand-hover: #5FB89A;
|
|
@@ -51,6 +63,8 @@
|
|
|
51
63
|
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
52
64
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
53
65
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
66
|
+
/* The MINT 1.1 card system uses exactly one shadow for every card. */
|
|
67
|
+
--card-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.04), 0 1px 3px 0 rgb(0 0 0 / 0.06);
|
|
54
68
|
|
|
55
69
|
/* Border radius */
|
|
56
70
|
--radius-sm: 0.25rem;
|
|
@@ -148,6 +162,17 @@ html.dark {
|
|
|
148
162
|
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.2);
|
|
149
163
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.2);
|
|
150
164
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.2);
|
|
165
|
+
--card-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.24), 0 1px 3px 0 rgb(0 0 0 / 0.32);
|
|
166
|
+
|
|
167
|
+
/* Entity accent colors - Dark mode */
|
|
168
|
+
--color-experiment: #22D3EE;
|
|
169
|
+
--color-experiment-hover: #67E8F9;
|
|
170
|
+
--color-experiment-soft: rgba(34, 211, 238, 0.16);
|
|
171
|
+
--color-experiment-border: rgba(34, 211, 238, 0.36);
|
|
172
|
+
--color-project: #38BDF8;
|
|
173
|
+
--color-project-hover: #7DD3FC;
|
|
174
|
+
--color-project-soft: rgba(56, 189, 248, 0.16);
|
|
175
|
+
--color-project-border: rgba(56, 189, 248, 0.38);
|
|
151
176
|
|
|
152
177
|
/* Semantic colors - Dark mode */
|
|
153
178
|
--mint-success-bg: rgba(16, 185, 129, 0.15);
|
package/src/tailwind.preset.ts
CHANGED
|
@@ -18,6 +18,10 @@ const mintPreset = {
|
|
|
18
18
|
'warning-hover': 'var(--mint-warning-hover)',
|
|
19
19
|
brand: 'var(--mint-brand)',
|
|
20
20
|
'brand-hover': 'var(--mint-brand-hover)',
|
|
21
|
+
experiment: 'var(--color-experiment)',
|
|
22
|
+
'experiment-hover': 'var(--color-experiment-hover)',
|
|
23
|
+
project: 'var(--color-project)',
|
|
24
|
+
'project-hover': 'var(--color-project-hover)',
|
|
21
25
|
},
|
|
22
26
|
bg: {
|
|
23
27
|
primary: 'var(--mint-bg-primary)',
|
|
@@ -50,6 +54,7 @@ const mintPreset = {
|
|
|
50
54
|
mint: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
|
51
55
|
'mint-md': '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
|
52
56
|
'mint-lg': '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
|
|
57
|
+
'mint-card': 'var(--card-shadow)',
|
|
53
58
|
},
|
|
54
59
|
transitionDuration: {
|
|
55
60
|
mint: '150ms',
|
package/src/types/components.ts
CHANGED
|
@@ -66,6 +66,18 @@ export interface FormFieldProps {
|
|
|
66
66
|
required?: boolean
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// Section card types
|
|
70
|
+
export type SectionCardAccent =
|
|
71
|
+
| 'primary'
|
|
72
|
+
| 'experiment'
|
|
73
|
+
| 'project'
|
|
74
|
+
| 'success'
|
|
75
|
+
| 'error'
|
|
76
|
+
| 'warning'
|
|
77
|
+
| 'info'
|
|
78
|
+
|
|
79
|
+
export type SectionCardDivider = 'strong' | 'light'
|
|
80
|
+
|
|
69
81
|
// Sidebar types
|
|
70
82
|
export type SidebarBadgeTone =
|
|
71
83
|
| 'primary'
|
package/src/types/index.ts
CHANGED
package/src/types/platform.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface PluginInfo {
|
|
|
10
10
|
color?: string
|
|
11
11
|
route_prefix: string
|
|
12
12
|
api_prefix: string
|
|
13
|
+
/** Settings route selected by the platform for this plugin runtime. */
|
|
14
|
+
settings_api?: 'plugin' | 'platform'
|
|
13
15
|
nav_items?: PluginNavItem[]
|
|
14
16
|
settings?: PluginSettings
|
|
15
17
|
}
|