@morscherlab/mld-sdk 0.9.4 → 0.9.6

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 (61) hide show
  1. package/dist/components/AutoGroupModal.vue.d.ts +118 -0
  2. package/dist/components/AutoGroupModal.vue.js.map +1 -1
  3. package/dist/components/DataFrame.vue.js +1 -1
  4. package/dist/components/DataFrame.vue.js.map +1 -1
  5. package/dist/components/ExperimentPopover.vue.d.ts +1 -0
  6. package/dist/components/ExperimentPopover.vue.js +72 -46
  7. package/dist/components/ExperimentPopover.vue.js.map +1 -1
  8. package/dist/components/ExperimentSelectorModal.vue.d.ts +6 -0
  9. package/dist/components/ExperimentSelectorModal.vue.js +284 -35
  10. package/dist/components/ExperimentSelectorModal.vue.js.map +1 -1
  11. package/dist/components/FileUploader.vue.js +13 -10
  12. package/dist/components/FileUploader.vue.js.map +1 -1
  13. package/dist/components/FormBuilder.vue.d.ts +287 -0
  14. package/dist/components/ResourceCard.vue.d.ts +1 -1
  15. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  16. package/dist/components/StepWizard.vue.d.ts +1 -1
  17. package/dist/components/StepWizard.vue.js.map +1 -1
  18. package/dist/composables/experiment-utils.d.ts +4 -1
  19. package/dist/composables/experiment-utils.js +22 -0
  20. package/dist/composables/experiment-utils.js.map +1 -1
  21. package/dist/composables/index.d.ts +1 -1
  22. package/dist/composables/index.js +4 -1
  23. package/dist/composables/useApi.js +26 -12
  24. package/dist/composables/useApi.js.map +1 -1
  25. package/dist/composables/useAuth.js +7 -6
  26. package/dist/composables/useAuth.js.map +1 -1
  27. package/dist/composables/useExperimentSelector.d.ts +6 -1
  28. package/dist/composables/useExperimentSelector.js +86 -8
  29. package/dist/composables/useExperimentSelector.js.map +1 -1
  30. package/dist/composables/useForm.js +2 -2
  31. package/dist/composables/useForm.js.map +1 -1
  32. package/dist/composables/usePlatformContext.js +11 -0
  33. package/dist/composables/usePlatformContext.js.map +1 -1
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +4 -1
  36. package/dist/stores/auth.js +21 -14
  37. package/dist/stores/auth.js.map +1 -1
  38. package/dist/styles.css +442 -104
  39. package/dist/types/components.d.ts +10 -0
  40. package/dist/types/index.d.ts +1 -1
  41. package/package.json +1 -1
  42. package/src/components/AutoGroupModal.vue +1 -1
  43. package/src/components/DataFrame.vue +1 -1
  44. package/src/components/ExperimentPopover.vue +32 -19
  45. package/src/components/ExperimentSelectorModal.story.vue +170 -23
  46. package/src/components/ExperimentSelectorModal.vue +205 -14
  47. package/src/components/FileUploader.vue +14 -11
  48. package/src/components/StepWizard.vue +1 -1
  49. package/src/composables/experiment-utils.ts +23 -1
  50. package/src/composables/index.ts +3 -0
  51. package/src/composables/useApi.ts +33 -17
  52. package/src/composables/useAuth.ts +11 -8
  53. package/src/composables/useExperimentSelector.ts +126 -10
  54. package/src/composables/useForm.ts +3 -3
  55. package/src/composables/usePlatformContext.ts +11 -1
  56. package/src/index.ts +3 -0
  57. package/src/stores/auth.ts +24 -16
  58. package/src/styles/components/experiment-popover.css +85 -49
  59. package/src/styles/components/experiment-selector-modal.css +152 -3
  60. package/src/types/components.ts +11 -0
  61. package/src/types/index.ts +3 -0
@@ -1,6 +1,15 @@
1
1
  import { ref, reactive, computed, watch, onScopeDispose, type Ref, type ComputedRef } from 'vue'
2
2
  import { useApi } from './useApi'
3
- import type { ExperimentSummary, ExperimentListResponse, ExperimentFilters, PlatformContext } from '../types'
3
+ import { datePresetToISO } from './experiment-utils'
4
+ import type {
5
+ ExperimentSummary,
6
+ ExperimentListResponse,
7
+ ExperimentFilters,
8
+ ExperimentTypeOption,
9
+ ExperimentSortField,
10
+ PlatformContext,
11
+ SelectOption,
12
+ } from '../types'
4
13
 
5
14
  function getPlatformContext(): PlatformContext | undefined {
6
15
  if (typeof window === 'undefined') return undefined
@@ -27,11 +36,20 @@ export interface UseExperimentSelectorReturn {
27
36
  error: Ref<string | null>
28
37
  page: Ref<number>
29
38
  hasMore: ComputedRef<boolean>
39
+ // Sort
40
+ sortKey: Ref<string>
41
+ // Filter options
42
+ experimentTypes: Ref<ExperimentTypeOption[]>
43
+ projects: Ref<SelectOption<string>[]>
44
+ // Grouped view
45
+ groupedByProject: ComputedRef<[string, ExperimentSummary[]][]>
46
+ // Methods
30
47
  fetch: () => Promise<void>
31
48
  loadMore: () => Promise<void>
32
49
  reset: () => void
33
50
  select: (experiment: ExperimentSummary) => void
34
51
  clear: () => void
52
+ fetchFilterOptions: () => Promise<void>
35
53
  }
36
54
 
37
55
  export function useExperimentSelector(
@@ -48,26 +66,62 @@ export function useExperimentSelector(
48
66
  const error = ref<string | null>(null)
49
67
  const page = ref(0)
50
68
 
69
+ // Sort: combined key like "created_at:desc"
70
+ const sortKey = ref<string>('created_at:desc')
71
+
72
+ // Filter option data (fetched once, cached)
73
+ const experimentTypes = ref<ExperimentTypeOption[]>([])
74
+ const projects = ref<SelectOption<string>[]>([])
75
+ let filterOptionsFetched = false
76
+
51
77
  const hasMore = computed(() => experiments.value.length < total.value)
52
78
 
79
+ const filters: ExperimentFilters = reactive({
80
+ search: undefined,
81
+ status: undefined,
82
+ project: undefined,
83
+ experimentType: undefined,
84
+ datePreset: undefined,
85
+ })
86
+
87
+ function parseSortKey(): { sortBy: ExperimentSortField; sortOrder: 'asc' | 'desc' } {
88
+ const [field, order] = sortKey.value.split(':')
89
+ return {
90
+ sortBy: (field || 'created_at') as ExperimentSortField,
91
+ sortOrder: (order || 'desc') as 'asc' | 'desc',
92
+ }
93
+ }
94
+
53
95
  async function fetchExperiments(): Promise<void> {
54
96
  isLoading.value = true
55
97
  error.value = null
56
98
  try {
57
99
  const params = new URLSearchParams()
58
- // Priority: explicit option > platform context (single type) > no filter
100
+ // Priority: explicit option > platform context (single type) > filter dropdown > no filter
59
101
  const allowedTypes = getPlatformContext()?.allowedExperimentTypes
60
102
  const effectiveType = experimentType
61
103
  ?? (allowedTypes?.length === 1 ? allowedTypes[0] : undefined)
104
+ ?? filters.experimentType
105
+ ?? undefined
62
106
  if (effectiveType) params.set('experiment_type', effectiveType)
63
107
  if (filters.status) params.set('status', filters.status)
64
108
  if (filters.search) params.set('search', filters.search)
65
109
  if (filters.project) params.set('project', filters.project)
110
+
111
+ // Sort params
112
+ const { sortBy, sortOrder } = parseSortKey()
113
+ params.set('sort_by', sortBy)
114
+ params.set('sort_order', sortOrder)
115
+
116
+ // Date preset → created_after
117
+ if (filters.datePreset) {
118
+ params.set('created_after', datePresetToISO(filters.datePreset))
119
+ }
120
+
66
121
  params.set('limit', String(limit))
67
122
  params.set('skip', String(page.value * limit))
68
123
 
69
124
  const query = params.toString()
70
- // Use absolute platform URL in integrated mode to bypass plugin's baseURL
71
125
  const base = platformBase ?? '/api'
72
126
  const url = `${base}/experiments${query ? `?${query}` : ''}`
73
127
  const data = await api.get<ExperimentListResponse>(url)
@@ -84,7 +138,19 @@ export function useExperimentSelector(
84
138
  } else {
85
139
  experiments.value = [...experiments.value, ...filtered]
86
140
  }
87
- total.value = effectiveType || !allowedTypes?.length ? data.total : filtered.length
141
+ // When client-side filtering is active (multiple allowedTypes), we can't
142
+ // use data.total since it counts all types. Check if server has more pages.
143
+ if (!effectiveType && allowedTypes && allowedTypes.length > 1) {
144
+ if (data.experiments.length < limit) {
145
+ // Server returned less than a full page — no more data
146
+ total.value = experiments.value.length
147
+ } else {
148
+ // Might be more pages on the server
149
+ total.value = experiments.value.length + 1
150
+ }
151
+ } else {
152
+ total.value = data.total
153
+ }
88
154
  } catch (e) {
89
155
  error.value = e instanceof Error ? e.message : 'Failed to fetch experiments'
90
156
  if (page.value === 0) {
@@ -96,6 +162,32 @@ export function useExperimentSelector(
96
162
  }
97
163
  }
98
164
 
165
+ async function fetchFilterOptions(): Promise<void> {
166
+ if (filterOptionsFetched) return
167
+ filterOptionsFetched = true
168
+
169
+ const base = platformBase ?? '/api'
170
+ const [typesRes, projectsRes] = await Promise.allSettled([
171
+ api.get<Array<{ value: string; label: string; color?: string }>>(`${base}/experiments/experiment-types`),
172
+ api.get<{ projects: Array<{ id: number; name: string }>; total: number }>(`${base}/projects`),
173
+ ])
174
+
175
+ if (typesRes.status === 'fulfilled' && Array.isArray(typesRes.value)) {
176
+ experimentTypes.value = typesRes.value.map(t => ({
177
+ value: t.value,
178
+ label: t.label,
179
+ color: t.color,
180
+ }))
181
+ }
182
+
183
+ if (projectsRes.status === 'fulfilled' && projectsRes.value?.projects && Array.isArray(projectsRes.value.projects)) {
184
+ projects.value = projectsRes.value.projects.map(p => ({
185
+ value: p.name,
186
+ label: p.name,
187
+ }))
188
+ }
189
+ }
190
+
99
191
  async function loadMore(): Promise<void> {
100
192
  if (!hasMore.value || isLoading.value) return
101
193
  page.value++
@@ -118,13 +210,30 @@ export function useExperimentSelector(
118
210
  filters.search = undefined
119
211
  filters.status = undefined
120
212
  filters.project = undefined
213
+ filters.experimentType = undefined
214
+ filters.datePreset = undefined
215
+ sortKey.value = 'created_at:desc'
121
216
  page.value = 0
122
217
  }
123
218
 
124
- const filters: ExperimentFilters = reactive({
125
- search: undefined,
126
- status: undefined,
127
- project: undefined,
219
+ // Group experiments by project (client-side)
220
+ const groupedByProject = computed<[string, ExperimentSummary[]][]>(() => {
221
+ const groups = new Map<string, ExperimentSummary[]>()
222
+ for (const exp of experiments.value) {
223
+ const key = exp.project_name ?? exp.project ?? 'No project'
224
+ const list = groups.get(key)
225
+ if (list) {
226
+ list.push(exp)
227
+ } else {
228
+ groups.set(key, [exp])
229
+ }
230
+ }
231
+ // Sort alphabetically, "No project" last
232
+ return [...groups.entries()].sort(([a], [b]) => {
233
+ if (a === 'No project') return 1
234
+ if (b === 'No project') return -1
235
+ return a.localeCompare(b)
236
+ })
128
237
  })
129
238
 
130
239
  // Debounced watch on search filter
@@ -140,10 +249,12 @@ export function useExperimentSelector(
140
249
  },
141
250
  )
142
251
 
143
- // Immediate watch on status/project filters (no debounce needed)
252
+ // Immediate watch on discrete filters and sort (cancel any pending search debounce)
144
253
  watch(
145
- () => [filters.status, filters.project],
254
+ () => [filters.status, filters.project, filters.experimentType, filters.datePreset, sortKey.value],
146
255
  () => {
256
+ if (debounceTimer) clearTimeout(debounceTimer)
257
+ debounceTimer = null
147
258
  page.value = 0
148
259
  fetchExperiments()
149
260
  },
@@ -166,10 +277,15 @@ export function useExperimentSelector(
166
277
  error,
167
278
  page,
168
279
  hasMore,
280
+ sortKey,
281
+ experimentTypes,
282
+ projects,
283
+ groupedByProject,
169
284
  fetch: fetchExperiments,
170
285
  loadMore,
171
286
  reset,
172
287
  select,
173
288
  clear,
289
+ fetchFilterOptions,
174
290
  }
175
291
  }
@@ -155,11 +155,11 @@ export function useForm<T extends Record<string, unknown>>(
155
155
  initialValues: T,
156
156
  rules: Partial<Record<keyof T, FieldRules>> = {}
157
157
  ): UseFormReturn<T> {
158
- // Store initial values for reset
159
- const _initialValues = { ...initialValues }
158
+ // Deep copy initial values so nested objects are not shared
159
+ const _initialValues = structuredClone(initialValues)
160
160
 
161
161
  // Reactive form data
162
- const data = reactive({ ...initialValues }) as T
162
+ const data = reactive(structuredClone(initialValues)) as T
163
163
 
164
164
  // Field state - use simple Record types for better TS compatibility
165
165
  const errors = reactive<Record<string, string | null>>(
@@ -10,6 +10,8 @@ const platformContext = ref<PlatformContext>({
10
10
  let allowedOrigins: Set<string> = new Set()
11
11
  let allowAnyOrigin = false
12
12
  let initialized = false
13
+ let listenerCount = 0
14
+ let currentHandler: ((event: MessageEvent) => void) | null = null
13
15
 
14
16
  /**
15
17
  * Derive origin from URL (protocol + host)
@@ -203,13 +205,21 @@ export function usePlatformContext(options: PlatformContextOptions = {}) {
203
205
  onMounted(() => {
204
206
  if (!initialized) {
205
207
  detectPlatform()
208
+ currentHandler = handlePlatformMessage
206
209
  window.addEventListener('message', handlePlatformMessage)
207
210
  initialized = true
208
211
  }
212
+ listenerCount++
209
213
  })
210
214
 
211
215
  onUnmounted(() => {
212
- // Don't remove listener as other components may still need it
216
+ listenerCount--
217
+ if (listenerCount <= 0 && currentHandler) {
218
+ window.removeEventListener('message', currentHandler)
219
+ currentHandler = null
220
+ initialized = false
221
+ listenerCount = 0
222
+ }
213
223
  })
214
224
 
215
225
  const isIntegrated = computed(() => platformContext.value.isIntegrated)
package/src/index.ts CHANGED
@@ -153,9 +153,12 @@ export {
153
153
  type UseExperimentSelectorReturn,
154
154
  // Experiment utilities
155
155
  formatExperimentDate,
156
+ datePresetToISO,
156
157
  EXPERIMENT_STATUS_OPTIONS,
157
158
  EXPERIMENT_STATUS_VARIANT_MAP,
158
159
  EXPERIMENT_STATUS_LABELS,
160
+ DATE_PRESET_OPTIONS,
161
+ SORT_OPTIONS,
159
162
  // Experiment data
160
163
  useExperimentData,
161
164
  type UseExperimentDataOptions,
@@ -5,6 +5,8 @@ import type { AuthConfig, UserInfo } from '../types'
5
5
  const AUTH_TOKEN_KEY = 'mld-auth-token'
6
6
  const AUTH_EXPIRES_KEY = 'mld-auth-expires'
7
7
 
8
+ const hasLocalStorage = typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function'
9
+
8
10
  export const useAuthStore = defineStore('mld-auth', () => {
9
11
  // State
10
12
  const token = ref<string | null>(null)
@@ -50,18 +52,20 @@ export const useAuthStore = defineStore('mld-auth', () => {
50
52
 
51
53
  // Actions
52
54
  function initialize() {
53
- const storedToken = localStorage.getItem(AUTH_TOKEN_KEY)
54
- const storedExpires = localStorage.getItem(AUTH_EXPIRES_KEY)
55
-
56
- if (storedToken) {
57
- token.value = storedToken
58
-
59
- if (storedExpires) {
60
- const expires = new Date(storedExpires)
61
- if (expires > new Date()) {
62
- tokenExpires.value = expires
63
- } else {
64
- clearToken()
55
+ if (hasLocalStorage) {
56
+ const storedToken = localStorage.getItem(AUTH_TOKEN_KEY)
57
+ const storedExpires = localStorage.getItem(AUTH_EXPIRES_KEY)
58
+
59
+ if (storedToken) {
60
+ token.value = storedToken
61
+
62
+ if (storedExpires) {
63
+ const expires = new Date(storedExpires)
64
+ if (expires > new Date()) {
65
+ tokenExpires.value = expires
66
+ } else {
67
+ clearToken()
68
+ }
65
69
  }
66
70
  }
67
71
  }
@@ -74,8 +78,10 @@ export const useAuthStore = defineStore('mld-auth', () => {
74
78
  const expires = new Date(Date.now() + expiresIn * 1000)
75
79
  tokenExpires.value = expires
76
80
 
77
- localStorage.setItem(AUTH_TOKEN_KEY, accessToken)
78
- localStorage.setItem(AUTH_EXPIRES_KEY, expires.toISOString())
81
+ if (hasLocalStorage) {
82
+ localStorage.setItem(AUTH_TOKEN_KEY, accessToken)
83
+ localStorage.setItem(AUTH_EXPIRES_KEY, expires.toISOString())
84
+ }
79
85
  }
80
86
 
81
87
  function clearToken() {
@@ -84,8 +90,10 @@ export const useAuthStore = defineStore('mld-auth', () => {
84
90
  username.value = null
85
91
  userInfo.value = null
86
92
 
87
- localStorage.removeItem(AUTH_TOKEN_KEY)
88
- localStorage.removeItem(AUTH_EXPIRES_KEY)
93
+ if (hasLocalStorage) {
94
+ localStorage.removeItem(AUTH_TOKEN_KEY)
95
+ localStorage.removeItem(AUTH_EXPIRES_KEY)
96
+ }
89
97
  }
90
98
 
91
99
  function setUserInfo(info: UserInfo) {
@@ -9,17 +9,18 @@
9
9
  display: inline-flex;
10
10
  align-items: center;
11
11
  gap: 0.375rem;
12
- padding: 0.3125rem 0.625rem;
13
- border: 1px solid var(--border-color, var(--mld-border, #e5e7eb));
12
+ padding: 0.0625rem 0.5625rem;
13
+ border: 1px solid var(--border-color, var(--mld-border, #e2e8f0));
14
14
  background: var(--bg-secondary, var(--mld-bg-card, #ffffff));
15
- border-radius: var(--mld-radius, 0.5rem);
16
- color: var(--text-primary, var(--mld-text-primary, #111827));
15
+ border-radius: 0.375rem;
16
+ color: var(--text-primary, var(--mld-text-primary, #1e293b));
17
17
  font-size: 0.8125rem;
18
18
  font-weight: 500;
19
19
  cursor: pointer;
20
- transition: border-color 0.15s ease, box-shadow 0.15s ease;
20
+ transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
21
21
  white-space: nowrap;
22
22
  max-width: 220px;
23
+ height: 1.75rem;
23
24
  }
24
25
 
25
26
  .mld-experiment-popover__trigger:hover {
@@ -28,12 +29,12 @@
28
29
 
29
30
  .mld-experiment-popover__trigger--active {
30
31
  border-color: var(--color-primary, #6366F1);
31
- box-shadow: 0 0 0 1px var(--color-primary, #6366F1);
32
+ background: rgba(99, 102, 241, 0.12);
33
+ color: var(--color-primary, #6366F1);
32
34
  }
33
35
 
34
36
  .mld-experiment-popover__trigger--empty {
35
- border-style: dashed;
36
- color: var(--text-secondary, var(--mld-text-secondary, #6b7280));
37
+ color: var(--text-muted, var(--mld-text-muted, #94a3b8));
37
38
  }
38
39
 
39
40
  .mld-experiment-popover__trigger-icon {
@@ -44,7 +45,11 @@
44
45
  }
45
46
 
46
47
  .mld-experiment-popover__trigger--empty .mld-experiment-popover__trigger-icon {
47
- color: var(--text-muted, var(--mld-text-muted, #9ca3af));
48
+ color: var(--text-muted, var(--mld-text-muted, #94a3b8));
49
+ }
50
+
51
+ .mld-experiment-popover__trigger--active .mld-experiment-popover__trigger-icon {
52
+ color: var(--color-primary, #6366F1);
48
53
  }
49
54
 
50
55
  .mld-experiment-popover__trigger-text {
@@ -54,15 +59,16 @@
54
59
  }
55
60
 
56
61
  .mld-experiment-popover__trigger-chevron {
57
- width: 0.875rem;
58
- height: 0.875rem;
62
+ width: 0.75rem;
63
+ height: 0.75rem;
59
64
  flex-shrink: 0;
60
- color: var(--text-muted, var(--mld-text-muted, #9ca3af));
65
+ color: var(--text-muted, var(--mld-text-muted, #94a3b8));
61
66
  transition: transform 0.15s ease;
62
67
  }
63
68
 
64
69
  .mld-experiment-popover__trigger--active .mld-experiment-popover__trigger-chevron {
65
70
  transform: rotate(180deg);
71
+ color: var(--color-primary, #6366F1);
66
72
  }
67
73
 
68
74
  /* Popover panel */
@@ -72,31 +78,41 @@
72
78
  right: 0;
73
79
  width: 280px;
74
80
  background: var(--bg-secondary, var(--mld-bg-card, #ffffff));
75
- border: 1px solid var(--border-color, var(--mld-border, #e5e7eb));
76
- border-radius: var(--mld-radius, 0.5rem);
77
- box-shadow: var(--mld-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05));
81
+ border: 1px solid var(--border-color, var(--mld-border, #e2e8f0));
82
+ border-radius: 0.75rem;
83
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
78
84
  z-index: 50;
79
85
  overflow: hidden;
80
86
  }
81
87
 
82
88
  /* Header */
83
89
  .mld-experiment-popover__header {
84
- padding: 0.75rem 1rem 0.5rem;
90
+ display: flex;
91
+ flex-direction: column;
92
+ gap: 0.125rem;
93
+ padding: 0.75rem 1rem 0;
94
+ border-bottom: 1px solid var(--border-color, var(--mld-border, #e2e8f0));
95
+ padding-bottom: 0.5rem;
85
96
  }
86
97
 
87
98
  .mld-experiment-popover__title {
88
- font-size: 0.75rem;
99
+ font-size: 0.8125rem;
89
100
  font-weight: 600;
90
- text-transform: uppercase;
91
- letter-spacing: 0.05em;
92
- color: var(--text-muted, var(--mld-text-muted, #9ca3af));
101
+ color: var(--text-primary, var(--mld-text-primary, #1e293b));
93
102
  }
94
103
 
95
- /* Empty state */
96
- .mld-experiment-popover__empty {
97
- padding: 0 1rem 0.75rem;
104
+ .mld-experiment-popover__subtitle {
105
+ font-size: 0.6875rem;
106
+ font-weight: 400;
107
+ color: var(--text-muted, var(--mld-text-muted, #94a3b8));
98
108
  }
99
109
 
110
+ /* Body (content area below header) */
111
+ .mld-experiment-popover__body {
112
+ padding: 0.75rem 1rem;
113
+ }
114
+
115
+ /* Select experiment button (no experiment state) */
100
116
  .mld-experiment-popover__select-btn {
101
117
  display: flex;
102
118
  align-items: center;
@@ -104,43 +120,46 @@
104
120
  gap: 0.375rem;
105
121
  width: 100%;
106
122
  padding: 0.5rem;
107
- border: 1.5px dashed var(--color-primary, #6366F1);
108
- background: var(--color-primary-soft, rgba(99, 102, 241, 0.06));
109
- border-radius: var(--mld-radius-sm, 0.375rem);
123
+ border: 1px solid var(--color-primary, #6366F1);
124
+ background: rgba(99, 102, 241, 0.12);
125
+ border-radius: 0.375rem;
110
126
  color: var(--color-primary, #6366F1);
111
127
  font-size: 0.8125rem;
112
128
  font-weight: 500;
113
129
  cursor: pointer;
114
130
  transition: background-color 0.15s ease;
131
+ height: 2.125rem;
115
132
  }
116
133
 
117
134
  .mld-experiment-popover__select-btn:hover {
118
- background: var(--color-primary-soft, rgba(99, 102, 241, 0.12));
135
+ background: rgba(99, 102, 241, 0.18);
119
136
  }
120
137
 
121
138
  /* Experiment card */
122
139
  .mld-experiment-popover__card {
123
140
  display: flex;
124
141
  align-items: center;
125
- gap: 0.625rem;
126
- padding: 0 1rem 0.75rem;
142
+ gap: 0.5rem;
143
+ padding: 0.625rem;
144
+ background: var(--bg-tertiary, #f1f5f9);
145
+ border-radius: 0.375rem;
127
146
  }
128
147
 
129
148
  .mld-experiment-popover__card-icon {
130
- width: 2rem;
131
- height: 2rem;
149
+ width: 1.75rem;
150
+ height: 1.75rem;
132
151
  flex-shrink: 0;
133
- border-radius: var(--mld-radius-sm, 0.375rem);
134
- background: rgba(139, 92, 246, 0.12);
152
+ border-radius: 0.375rem;
153
+ background: rgba(139, 92, 246, 0.15);
135
154
  display: flex;
136
155
  align-items: center;
137
156
  justify-content: center;
138
157
  }
139
158
 
140
159
  .mld-experiment-popover__card-icon svg {
141
- width: 1.125rem;
142
- height: 1.125rem;
143
- color: #8B5CF6;
160
+ width: 1rem;
161
+ height: 1rem;
162
+ color: #6366F1;
144
163
  }
145
164
 
146
165
  .mld-experiment-popover__card-info {
@@ -151,7 +170,7 @@
151
170
  .mld-experiment-popover__card-name {
152
171
  font-size: 0.8125rem;
153
172
  font-weight: 500;
154
- color: var(--text-primary, var(--mld-text-primary, #111827));
173
+ color: var(--text-primary, var(--mld-text-primary, #1e293b));
155
174
  overflow: hidden;
156
175
  text-overflow: ellipsis;
157
176
  white-space: nowrap;
@@ -159,15 +178,15 @@
159
178
 
160
179
  .mld-experiment-popover__card-status {
161
180
  font-size: 0.6875rem;
162
- color: var(--text-muted, var(--mld-text-muted, #9ca3af));
181
+ color: var(--text-muted, var(--mld-text-muted, #94a3b8));
163
182
  }
164
183
 
165
184
  .mld-experiment-popover__change-btn {
166
185
  flex-shrink: 0;
167
- padding: 0.25rem 0.5rem;
186
+ padding: 0.25rem 0.375rem;
168
187
  border: none;
169
188
  background: transparent;
170
- border-radius: var(--mld-radius-sm, 0.25rem);
189
+ border-radius: 0.25rem;
171
190
  color: var(--color-primary, #6366F1);
172
191
  font-size: 0.75rem;
173
192
  font-weight: 500;
@@ -176,13 +195,13 @@
176
195
  }
177
196
 
178
197
  .mld-experiment-popover__change-btn:hover {
179
- background: var(--color-primary-soft, rgba(99, 102, 241, 0.08));
198
+ background: rgba(99, 102, 241, 0.08);
180
199
  }
181
200
 
182
201
  /* Divider */
183
202
  .mld-experiment-popover__divider {
184
203
  height: 1px;
185
- background: var(--border-color, var(--mld-border, #e5e7eb));
204
+ background: var(--border-color, var(--mld-border, #e2e8f0));
186
205
  margin: 0;
187
206
  }
188
207
 
@@ -200,12 +219,13 @@
200
219
  padding: 0.4375rem 0.75rem;
201
220
  border: none;
202
221
  background: var(--color-primary, #6366F1);
203
- border-radius: var(--mld-radius-sm, 0.375rem);
222
+ border-radius: 0.375rem;
204
223
  color: white;
205
224
  font-size: 0.8125rem;
206
225
  font-weight: 500;
207
226
  cursor: pointer;
208
227
  transition: background-color 0.15s ease, opacity 0.15s ease;
228
+ height: 2rem;
209
229
  }
210
230
 
211
231
  .mld-experiment-popover__save-btn:hover:not(:disabled) {
@@ -213,7 +233,7 @@
213
233
  }
214
234
 
215
235
  .mld-experiment-popover__save-btn:disabled {
216
- opacity: 0.5;
236
+ opacity: 0.45;
217
237
  cursor: not-allowed;
218
238
  }
219
239
 
@@ -222,13 +242,29 @@
222
242
  pointer-events: none;
223
243
  }
224
244
 
225
- /* Save success state */
245
+ /* Save icon */
246
+ .mld-experiment-popover__save-icon {
247
+ width: 1rem;
248
+ height: 1rem;
249
+ }
250
+
251
+ /* Save success state - outlined green */
226
252
  .mld-experiment-popover__save-btn--success {
227
- background: var(--mld-success-bg, #059669);
253
+ background: rgba(16, 185, 129, 0.12);
254
+ border: 1px solid rgba(16, 185, 129, 0.3);
255
+ color: #10b981;
228
256
  }
229
257
 
230
258
  .mld-experiment-popover__save-btn--success:hover {
231
- background: var(--mld-success-bg, #059669);
259
+ background: rgba(16, 185, 129, 0.18);
260
+ }
261
+
262
+ /* Save hint text */
263
+ .mld-experiment-popover__save-hint {
264
+ font-size: 0.6875rem;
265
+ color: var(--text-muted, var(--mld-text-muted, #94a3b8));
266
+ text-align: center;
267
+ margin-top: 0.5rem;
232
268
  }
233
269
 
234
270
  /* Spinner */
@@ -247,6 +283,6 @@
247
283
 
248
284
  /* Check icon for success */
249
285
  .mld-experiment-popover__check-icon {
250
- width: 0.875rem;
251
- height: 0.875rem;
286
+ width: 1rem;
287
+ height: 1rem;
252
288
  }