@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.
- package/dist/components/AutoGroupModal.vue.d.ts +118 -0
- package/dist/components/AutoGroupModal.vue.js.map +1 -1
- package/dist/components/DataFrame.vue.js +1 -1
- package/dist/components/DataFrame.vue.js.map +1 -1
- package/dist/components/ExperimentPopover.vue.d.ts +1 -0
- package/dist/components/ExperimentPopover.vue.js +72 -46
- package/dist/components/ExperimentPopover.vue.js.map +1 -1
- package/dist/components/ExperimentSelectorModal.vue.d.ts +6 -0
- package/dist/components/ExperimentSelectorModal.vue.js +284 -35
- package/dist/components/ExperimentSelectorModal.vue.js.map +1 -1
- package/dist/components/FileUploader.vue.js +13 -10
- package/dist/components/FileUploader.vue.js.map +1 -1
- package/dist/components/FormBuilder.vue.d.ts +287 -0
- package/dist/components/ResourceCard.vue.d.ts +1 -1
- package/dist/components/StatusIndicator.vue.d.ts +1 -1
- package/dist/components/StepWizard.vue.d.ts +1 -1
- package/dist/components/StepWizard.vue.js.map +1 -1
- package/dist/composables/experiment-utils.d.ts +4 -1
- package/dist/composables/experiment-utils.js +22 -0
- package/dist/composables/experiment-utils.js.map +1 -1
- package/dist/composables/index.d.ts +1 -1
- package/dist/composables/index.js +4 -1
- package/dist/composables/useApi.js +26 -12
- package/dist/composables/useApi.js.map +1 -1
- package/dist/composables/useAuth.js +7 -6
- package/dist/composables/useAuth.js.map +1 -1
- package/dist/composables/useExperimentSelector.d.ts +6 -1
- package/dist/composables/useExperimentSelector.js +86 -8
- package/dist/composables/useExperimentSelector.js.map +1 -1
- package/dist/composables/useForm.js +2 -2
- package/dist/composables/useForm.js.map +1 -1
- package/dist/composables/usePlatformContext.js +11 -0
- package/dist/composables/usePlatformContext.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -1
- package/dist/stores/auth.js +21 -14
- package/dist/stores/auth.js.map +1 -1
- package/dist/styles.css +442 -104
- package/dist/types/components.d.ts +10 -0
- package/dist/types/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/AutoGroupModal.vue +1 -1
- package/src/components/DataFrame.vue +1 -1
- package/src/components/ExperimentPopover.vue +32 -19
- package/src/components/ExperimentSelectorModal.story.vue +170 -23
- package/src/components/ExperimentSelectorModal.vue +205 -14
- package/src/components/FileUploader.vue +14 -11
- package/src/components/StepWizard.vue +1 -1
- package/src/composables/experiment-utils.ts +23 -1
- package/src/composables/index.ts +3 -0
- package/src/composables/useApi.ts +33 -17
- package/src/composables/useAuth.ts +11 -8
- package/src/composables/useExperimentSelector.ts +126 -10
- package/src/composables/useForm.ts +3 -3
- package/src/composables/usePlatformContext.ts +11 -1
- package/src/index.ts +3 -0
- package/src/stores/auth.ts +24 -16
- package/src/styles/components/experiment-popover.css +85 -49
- package/src/styles/components/experiment-selector-modal.css +152 -3
- package/src/types/components.ts +11 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
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
|
-
//
|
|
159
|
-
const _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(
|
|
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
|
-
|
|
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,
|
package/src/stores/auth.ts
CHANGED
|
@@ -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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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.
|
|
13
|
-
border: 1px solid var(--border-color, var(--mld-border, #
|
|
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:
|
|
16
|
-
color: var(--text-primary, var(--mld-text-primary, #
|
|
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
|
-
|
|
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
|
-
|
|
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, #
|
|
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.
|
|
58
|
-
height: 0.
|
|
62
|
+
width: 0.75rem;
|
|
63
|
+
height: 0.75rem;
|
|
59
64
|
flex-shrink: 0;
|
|
60
|
-
color: var(--text-muted, var(--mld-text-muted, #
|
|
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, #
|
|
76
|
-
border-radius:
|
|
77
|
-
box-shadow:
|
|
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
|
-
|
|
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.
|
|
99
|
+
font-size: 0.8125rem;
|
|
89
100
|
font-weight: 600;
|
|
90
|
-
text-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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:
|
|
108
|
-
background:
|
|
109
|
-
border-radius:
|
|
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:
|
|
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.
|
|
126
|
-
padding: 0
|
|
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:
|
|
131
|
-
height:
|
|
149
|
+
width: 1.75rem;
|
|
150
|
+
height: 1.75rem;
|
|
132
151
|
flex-shrink: 0;
|
|
133
|
-
border-radius:
|
|
134
|
-
background: rgba(139, 92, 246, 0.
|
|
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:
|
|
142
|
-
height:
|
|
143
|
-
color: #
|
|
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, #
|
|
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, #
|
|
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.
|
|
186
|
+
padding: 0.25rem 0.375rem;
|
|
168
187
|
border: none;
|
|
169
188
|
background: transparent;
|
|
170
|
-
border-radius:
|
|
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:
|
|
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, #
|
|
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:
|
|
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.
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
251
|
-
height:
|
|
286
|
+
width: 1rem;
|
|
287
|
+
height: 1rem;
|
|
252
288
|
}
|