@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.
Files changed (38) hide show
  1. package/dist/__tests__/components/SectionCard.test.d.ts +1 -0
  2. package/dist/components/SectionCard.vue.d.ts +47 -0
  3. package/dist/components/index.d.ts +1 -0
  4. package/dist/components/index.js +2 -2
  5. package/dist/{components-UfkPN1bK.js → components-DFH5whuh.js} +699 -620
  6. package/dist/components-DFH5whuh.js.map +1 -0
  7. package/dist/composables/index.js +1 -1
  8. package/dist/composables/usePlatformContext.d.ts +3 -0
  9. package/dist/{composables-F4JFK9K1.js → composables-B512bWJP.js} +133 -26
  10. package/dist/composables-B512bWJP.js.map +1 -0
  11. package/dist/index.js +3 -3
  12. package/dist/install.js +1 -1
  13. package/dist/styles.css +301 -0
  14. package/dist/tailwind.preset.d.ts +5 -0
  15. package/dist/tailwind.preset.js +7 -2
  16. package/dist/tailwind.preset.js.map +1 -1
  17. package/dist/types/components.d.ts +2 -0
  18. package/dist/types/index.d.ts +1 -1
  19. package/dist/types/platform.d.ts +2 -0
  20. package/package.json +1 -1
  21. package/src/__tests__/components/SectionCard.test.ts +181 -0
  22. package/src/__tests__/composables/usePlatformContext.test.ts +28 -0
  23. package/src/__tests__/composables/usePluginClient.test.ts +329 -20
  24. package/src/__tests__/composables/usePluginConfig.test.ts +323 -7
  25. package/src/components/SectionCard.story.vue +163 -0
  26. package/src/components/SectionCard.vue +98 -0
  27. package/src/components/index.ts +1 -0
  28. package/src/composables/usePluginClient.ts +118 -15
  29. package/src/composables/usePluginConfig.ts +156 -20
  30. package/src/styles/components/section-card.css +161 -0
  31. package/src/styles/index.css +1 -0
  32. package/src/styles/variables.css +25 -0
  33. package/src/tailwind.preset.ts +5 -0
  34. package/src/types/components.ts +12 -0
  35. package/src/types/index.ts +2 -0
  36. package/src/types/platform.ts +2 -0
  37. package/dist/components-UfkPN1bK.js.map +0 -1
  38. 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 { ...value }
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 = shallowRef<PluginSettingsValues<TSettings>>({} as PluginSettingsValues<TSettings>)
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((isIntegrated.value || injectedContext?.isIntegrated) && resolvedPluginName.value),
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>): void {
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(responseSettingsPayload(response))
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
- const response = usesPlatformConfig.value
640
- ? await request.run(
641
- () => platformApi.patch<{ plugin_name: string; config: Record<string, unknown> }>(
642
- `/plugins/${encodeURIComponent(resolvedPluginName.value)}/config`,
643
- { config: payload },
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
- : await request.run(
648
- () => pluginApi.put<{ settings: Record<string, unknown>; mode?: string }>('/settings', payload),
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
- applyLoaded(responseSettingsPayload(response))
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 api = useApi()
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(() => pluginName ?? plugin.value?.name ?? '')
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 = await request.run(
56
- () => api.get<{ plugin_name: string; config: Record<string, unknown> }>(
57
- `/plugins/${encodeURIComponent(name)}/config`,
58
- ),
59
- { success: 'load', errorMessage: 'Failed to load plugin config' },
60
- )
61
- config.value = { ...response.config }
62
- savedConfig.value = { ...response.config }
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 response = await request.run(
77
- () => api.patch<{ plugin_name: string; config: Record<string, unknown> }>(
78
- `/plugins/${encodeURIComponent(name)}/config`,
79
- { config: config.value },
80
- ),
81
- { success: 'save', errorMessage: 'Failed to save plugin config' },
82
- )
83
- config.value = { ...response.config }
84
- savedConfig.value = { ...response.config }
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 = { ...savedConfig.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
+ }
@@ -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';
@@ -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);
@@ -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',
@@ -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'
@@ -16,6 +16,8 @@ export type {
16
16
  RadioOption,
17
17
  RadioOptionInput,
18
18
  FormFieldProps,
19
+ SectionCardAccent,
20
+ SectionCardDivider,
19
21
  SidebarBadgeTone,
20
22
  SidebarToolSection,
21
23
  SidebarToolSectionAction,
@@ -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
  }