@morscherlab/mld-sdk 0.7.2 → 0.7.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 (55) hide show
  1. package/dist/components/BaseInput.vue.d.ts +1 -0
  2. package/dist/components/BaseInput.vue.js +5 -2
  3. package/dist/components/BaseInput.vue.js.map +1 -1
  4. package/dist/components/BaseModal.vue.d.ts +6 -2
  5. package/dist/components/BaseModal.vue.js +54 -10
  6. package/dist/components/BaseModal.vue.js.map +1 -1
  7. package/dist/components/BaseSelect.vue.d.ts +1 -0
  8. package/dist/components/BaseSelect.vue.js +5 -2
  9. package/dist/components/BaseSelect.vue.js.map +1 -1
  10. package/dist/components/BaseTextarea.vue.d.ts +1 -0
  11. package/dist/components/BaseTextarea.vue.js +5 -2
  12. package/dist/components/BaseTextarea.vue.js.map +1 -1
  13. package/dist/components/ExperimentCodeBadge.vue.d.ts +8 -0
  14. package/dist/components/ExperimentCodeBadge.vue.js +19 -0
  15. package/dist/components/ExperimentCodeBadge.vue.js.map +1 -0
  16. package/dist/components/ExperimentCodeBadge.vue3.js +6 -0
  17. package/dist/components/ExperimentCodeBadge.vue3.js.map +1 -0
  18. package/dist/components/ExperimentDataViewer.vue.d.ts +29 -0
  19. package/dist/components/ExperimentDataViewer.vue.js +258 -0
  20. package/dist/components/ExperimentDataViewer.vue.js.map +1 -0
  21. package/dist/components/ExperimentDataViewer.vue3.js +6 -0
  22. package/dist/components/ExperimentDataViewer.vue3.js.map +1 -0
  23. package/dist/components/FormField.vue.d.ts +4 -1
  24. package/dist/components/FormField.vue.js +24 -12
  25. package/dist/components/FormField.vue.js.map +1 -1
  26. package/dist/components/index.d.ts +2 -0
  27. package/dist/components/index.js +14 -8
  28. package/dist/components/index.js.map +1 -1
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +14 -8
  31. package/dist/index.js.map +1 -1
  32. package/dist/styles.css +298 -30
  33. package/dist/types/components.d.ts +23 -0
  34. package/dist/types/index.d.ts +1 -1
  35. package/package.json +1 -1
  36. package/src/components/BaseInput.vue +3 -0
  37. package/src/components/BaseModal.vue +59 -10
  38. package/src/components/BaseSelect.vue +3 -0
  39. package/src/components/BaseTextarea.vue +3 -0
  40. package/src/components/ExperimentCodeBadge.vue +20 -0
  41. package/src/components/ExperimentDataViewer.vue +250 -0
  42. package/src/components/FormField.vue +17 -4
  43. package/src/components/index.ts +4 -0
  44. package/src/index.ts +10 -0
  45. package/src/styles/components/button.css +4 -4
  46. package/src/styles/components/experiment-code-badge.css +13 -0
  47. package/src/styles/components/experiment-data-viewer.css +131 -0
  48. package/src/styles/components/modal.css +1 -1
  49. package/src/styles/components/select.css +1 -1
  50. package/src/styles/components/slider.css +4 -8
  51. package/src/styles/components/textarea.css +5 -1
  52. package/src/styles/index.css +2 -0
  53. package/src/styles/variables.css +7 -2
  54. package/src/types/components.ts +27 -0
  55. package/src/types/index.ts +4 -0
@@ -9,6 +9,7 @@ interface Props {
9
9
  rows?: number
10
10
  resize?: 'none' | 'vertical' | 'horizontal' | 'both'
11
11
  maxlength?: number
12
+ ariaDescribedby?: string
12
13
  }
13
14
 
14
15
  const props = withDefaults(defineProps<Props>(), {
@@ -40,6 +41,8 @@ function handleInput(event: Event) {
40
41
  :readonly="props.readonly"
41
42
  :rows="props.rows"
42
43
  :maxlength="props.maxlength"
44
+ :aria-invalid="props.error || undefined"
45
+ :aria-describedby="props.ariaDescribedby || undefined"
43
46
  :class="[
44
47
  'mld-textarea',
45
48
  `mld-textarea--${props.size}`,
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ interface Props {
3
+ code: string
4
+ size?: 'sm' | 'md' | 'lg'
5
+ }
6
+
7
+ withDefaults(defineProps<Props>(), {
8
+ size: 'md',
9
+ })
10
+ </script>
11
+
12
+ <template>
13
+ <span :class="['mld-exp-code', `mld-exp-code--${size}`]">
14
+ {{ code }}
15
+ </span>
16
+ </template>
17
+
18
+ <style>
19
+ @import '../styles/components/experiment-code-badge.css';
20
+ </style>
@@ -0,0 +1,250 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, watch } from 'vue'
3
+ import type { TreeNode, DataFrameColumn, SummaryData } from '../types'
4
+ import SampleHierarchyTree from './SampleHierarchyTree.vue'
5
+ import DataFrame from './DataFrame.vue'
6
+ import SegmentedControl from './SegmentedControl.vue'
7
+ import BaseButton from './BaseButton.vue'
8
+
9
+ interface Props {
10
+ treeData: TreeNode[]
11
+ tableData?: Record<string, unknown>[]
12
+ tableColumns?: DataFrameColumn[] | undefined
13
+ summaryData?: SummaryData | null
14
+ defaultView?: 'summary' | 'tree' | 'table'
15
+ title?: string
16
+ pluginName?: string
17
+ pluginRoutePrefix?: string
18
+ experimentId?: number
19
+ loading?: boolean
20
+ downloadJsonUrl?: string
21
+ downloadCsvUrl?: string
22
+ }
23
+
24
+ const props = withDefaults(defineProps<Props>(), {
25
+ title: 'Data',
26
+ defaultView: 'summary',
27
+ loading: false,
28
+ })
29
+
30
+ const emit = defineEmits<{
31
+ 'open-plugin': []
32
+ 'download-json': []
33
+ 'download-csv': []
34
+ }>()
35
+
36
+ const viewMode = ref<string | number>(props.defaultView)
37
+
38
+ // Reset view mode when defaultView prop changes
39
+ watch(() => props.defaultView, (val) => { viewMode.value = val })
40
+
41
+ const viewOptions = computed(() => {
42
+ const opts = []
43
+ if (props.summaryData) {
44
+ opts.push({ value: 'summary', label: 'Summary' })
45
+ }
46
+ opts.push({ value: 'tree', label: 'Tree' })
47
+ opts.push({ value: 'table', label: 'Table' })
48
+ return opts
49
+ })
50
+
51
+ // Fall back to tree if summary is selected but no summary data
52
+ watch(() => props.summaryData, (val) => {
53
+ if (!val && viewMode.value === 'summary') {
54
+ viewMode.value = 'tree'
55
+ }
56
+ }, { immediate: true })
57
+
58
+ const hasTableData = computed(() =>
59
+ props.tableData && props.tableData.length > 0
60
+ )
61
+
62
+ const metadataEntries = computed(() => {
63
+ if (!props.summaryData?.metadata) return []
64
+ return Object.entries(props.summaryData.metadata)
65
+ .filter(([, v]) => v !== null && v !== undefined && v !== '')
66
+ .map(([key, value]) => ({
67
+ key: key.replace(/_/g, ' '),
68
+ value: String(value),
69
+ }))
70
+ })
71
+
72
+ function humanizeColumn(col: string): string {
73
+ return col.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
74
+ }
75
+
76
+ function formatCellValue(value: unknown): string {
77
+ if (value === null || value === undefined) return ''
78
+ if (typeof value === 'object') return JSON.stringify(value)
79
+ return String(value)
80
+ }
81
+
82
+ function columnsForSection(columns: string[]): DataFrameColumn[] {
83
+ return columns.map((col) => ({
84
+ key: col,
85
+ label: humanizeColumn(col),
86
+ sortable: true,
87
+ formatter: (value: unknown) => formatCellValue(value),
88
+ }))
89
+ }
90
+
91
+ function handleDownloadJson() {
92
+ if (props.downloadJsonUrl) {
93
+ window.open(props.downloadJsonUrl, '_blank')
94
+ }
95
+ emit('download-json')
96
+ }
97
+
98
+ function handleDownloadCsv() {
99
+ if (props.downloadCsvUrl) {
100
+ window.open(props.downloadCsvUrl, '_blank')
101
+ }
102
+ emit('download-csv')
103
+ }
104
+ </script>
105
+
106
+ <template>
107
+ <div class="mld-data-viewer">
108
+ <div class="mld-data-viewer__header">
109
+ <div class="mld-data-viewer__controls">
110
+ <SegmentedControl
111
+ v-model="viewMode"
112
+ :options="viewOptions"
113
+ variant="card"
114
+ size="sm"
115
+ :full-width="false"
116
+ />
117
+ </div>
118
+ <div class="mld-data-viewer__actions">
119
+ <BaseButton
120
+ v-if="pluginRoutePrefix && experimentId"
121
+ variant="secondary"
122
+ size="sm"
123
+ @click="emit('open-plugin')"
124
+ >
125
+ Open in {{ pluginName || 'Plugin' }}
126
+ </BaseButton>
127
+ <BaseButton
128
+ variant="ghost"
129
+ size="sm"
130
+ @click="handleDownloadJson"
131
+ >
132
+ JSON
133
+ </BaseButton>
134
+ <BaseButton
135
+ variant="ghost"
136
+ size="sm"
137
+ @click="handleDownloadCsv"
138
+ >
139
+ CSV
140
+ </BaseButton>
141
+ </div>
142
+ </div>
143
+
144
+ <div class="mld-data-viewer__content">
145
+ <div v-if="loading" class="mld-data-viewer__loading">
146
+ Loading...
147
+ </div>
148
+ <template v-else>
149
+ <!-- Summary View -->
150
+ <div v-if="viewMode === 'summary' && summaryData" class="mld-summary">
151
+ <!-- Metadata pills -->
152
+ <div v-if="metadataEntries.length" class="mld-summary__metadata">
153
+ <span
154
+ v-for="entry in metadataEntries"
155
+ :key="entry.key"
156
+ class="mld-summary__pill"
157
+ >
158
+ <span class="mld-summary__pill-key">{{ entry.key }}</span>
159
+ <span class="mld-summary__pill-value">{{ entry.value }}</span>
160
+ </span>
161
+ </div>
162
+
163
+ <!-- Sections -->
164
+ <div
165
+ v-for="section in summaryData.sections"
166
+ :key="section.key"
167
+ class="mld-summary__section"
168
+ >
169
+ <!-- Group section: cards with embedded tables -->
170
+ <template v-if="section.type === 'group' && section.items">
171
+ <div
172
+ v-for="(item, idx) in section.items"
173
+ :key="idx"
174
+ class="mld-summary__group-card"
175
+ >
176
+ <div class="mld-summary__group-header">
177
+ <span class="mld-summary__group-label">{{ item.label }}</span>
178
+ <span class="mld-summary__group-count">
179
+ {{ item.item_count }} {{ item.item_key }}
180
+ </span>
181
+ </div>
182
+ <div v-if="item.metadata && Object.keys(item.metadata).length" class="mld-summary__group-meta">
183
+ <span
184
+ v-for="(val, key) in item.metadata"
185
+ :key="String(key)"
186
+ class="mld-summary__pill mld-summary__pill--sm"
187
+ >
188
+ <span class="mld-summary__pill-key">{{ String(key).replace(/_/g, ' ') }}</span>
189
+ <span class="mld-summary__pill-value">{{ val }}</span>
190
+ </span>
191
+ </div>
192
+ <DataFrame
193
+ v-if="item.rows.length"
194
+ :data="item.rows"
195
+ :columns="columnsForSection(item.columns)"
196
+ :searchable="item.rows.length > 10"
197
+ :sortable="true"
198
+ :striped="true"
199
+ size="sm"
200
+ />
201
+ </div>
202
+ </template>
203
+
204
+ <!-- Table section: flat table -->
205
+ <template v-else-if="section.type === 'table' && section.rows">
206
+ <div class="mld-summary__table-header">
207
+ <span class="mld-summary__section-label">{{ section.label }}</span>
208
+ <span class="mld-summary__section-count">{{ section.row_count }} rows</span>
209
+ </div>
210
+ <DataFrame
211
+ :data="section.rows"
212
+ :columns="columnsForSection(section.columns || [])"
213
+ :searchable="(section.rows?.length || 0) > 10"
214
+ :sortable="true"
215
+ :striped="true"
216
+ size="sm"
217
+ />
218
+ </template>
219
+ </div>
220
+ </div>
221
+
222
+ <!-- Tree View -->
223
+ <SampleHierarchyTree
224
+ v-else-if="viewMode === 'tree'"
225
+ :nodes="treeData"
226
+ :expand-all="false"
227
+ :show-icons="true"
228
+ :show-counts="true"
229
+ size="sm"
230
+ />
231
+
232
+ <!-- Table View -->
233
+ <DataFrame
234
+ v-else-if="viewMode === 'table' && hasTableData"
235
+ :data="tableData!"
236
+ :columns="tableColumns ?? []"
237
+ :searchable="true"
238
+ :sortable="true"
239
+ />
240
+ <div v-else-if="viewMode === 'table'" class="mld-data-viewer__empty">
241
+ No tabular data available. Use tree view.
242
+ </div>
243
+ </template>
244
+ </div>
245
+ </div>
246
+ </template>
247
+
248
+ <style>
249
+ @import '../styles/components/experiment-data-viewer.css';
250
+ </style>
@@ -1,13 +1,26 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
2
4
  interface Props {
3
5
  label?: string
4
6
  error?: string
5
7
  hint?: string
6
8
  required?: boolean
7
9
  htmlFor?: string
10
+ fieldId?: string
8
11
  }
9
12
 
10
- defineProps<Props>()
13
+ const props = defineProps<Props>()
14
+
15
+ const errorId = computed(() =>
16
+ props.error && props.fieldId ? `${props.fieldId}-error` : undefined
17
+ )
18
+
19
+ const hintId = computed(() =>
20
+ !props.error && props.hint && props.fieldId ? `${props.fieldId}-hint` : undefined
21
+ )
22
+
23
+ const describedBy = computed(() => errorId.value ?? hintId.value)
11
24
  </script>
12
25
 
13
26
  <template>
@@ -21,12 +34,12 @@ defineProps<Props>()
21
34
  <span v-if="required" class="mld-form-field__required">*</span>
22
35
  </label>
23
36
 
24
- <slot />
37
+ <slot :described-by="describedBy" />
25
38
 
26
- <p v-if="error" class="mld-form-field__error">
39
+ <p v-if="error" :id="errorId" class="mld-form-field__error" role="alert">
27
40
  {{ error }}
28
41
  </p>
29
- <p v-else-if="hint" class="mld-form-field__hint">
42
+ <p v-else-if="hint" :id="hintId" class="mld-form-field__hint">
30
43
  {{ hint }}
31
44
  </p>
32
45
  </div>
@@ -95,6 +95,10 @@ export { default as FormSection } from './FormSection.vue'
95
95
  export { default as FormActions } from './FormActions.vue'
96
96
  export { default as FormFieldRenderer } from './FormFieldRenderer.vue'
97
97
 
98
+ // Experiment data display components
99
+ export { default as ExperimentDataViewer } from './ExperimentDataViewer.vue'
100
+ export { default as ExperimentCodeBadge } from './ExperimentCodeBadge.vue'
101
+
98
102
  // Scheduling / booking components
99
103
  export { default as DateTimePicker } from './DateTimePicker.vue'
100
104
  export { default as TimeRangeInput } from './TimeRangeInput.vue'
package/src/index.ts CHANGED
@@ -82,6 +82,9 @@ export {
82
82
  StepWizard,
83
83
  AuditTrail,
84
84
  BatchProgressList,
85
+ // Experiment data display components
86
+ ExperimentDataViewer,
87
+ ExperimentCodeBadge,
85
88
  // Scheduling / booking components
86
89
  DateTimePicker,
87
90
  TimeRangeInput,
@@ -272,6 +275,13 @@ export type {
272
275
  RegisterRequest,
273
276
  UpdateProfileRequest,
274
277
  CredentialInfo,
278
+ // Summary types
279
+ SummaryData,
280
+ SummarySection,
281
+ SummarySectionItem,
282
+ // Tree types
283
+ TreeNode,
284
+ TreeNodeType,
275
285
  // Platform types
276
286
  PluginInfo,
277
287
  PluginNavItem,
@@ -15,12 +15,12 @@
15
15
  box-sizing: border-box;
16
16
  }
17
17
 
18
- .mld-button:focus {
18
+ .mld-button:focus-visible {
19
19
  outline: none;
20
20
  box-shadow: 0 0 0 2px white, 0 0 0 4px var(--color-primary);
21
21
  }
22
22
 
23
- html.dark .mld-button:focus {
23
+ html.dark .mld-button:focus-visible {
24
24
  box-shadow: 0 0 0 2px var(--bg-primary), 0 0 0 4px var(--color-primary);
25
25
  }
26
26
 
@@ -68,7 +68,7 @@ html.dark .mld-button:focus {
68
68
  }
69
69
 
70
70
  .mld-button--danger:hover:not(.mld-button--disabled) {
71
- background-color: #DC2626;
71
+ background-color: var(--mld-error-hover);
72
72
  }
73
73
 
74
74
  .mld-button--success {
@@ -77,7 +77,7 @@ html.dark .mld-button:focus {
77
77
  }
78
78
 
79
79
  .mld-button--success:hover:not(.mld-button--disabled) {
80
- background-color: #059669;
80
+ background-color: var(--mld-success-hover);
81
81
  }
82
82
 
83
83
  .mld-button--ghost {
@@ -0,0 +1,13 @@
1
+ .mld-exp-code {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ font-family: var(--mld-font-mono, ui-monospace, monospace);
5
+ font-weight: 600;
6
+ background: var(--mld-color-surface-2, #f0f0f0);
7
+ color: var(--mld-color-text-secondary, #666);
8
+ border-radius: var(--mld-radius-sm, 4px);
9
+ letter-spacing: 0.02em;
10
+ }
11
+ .mld-exp-code--sm { padding: 1px 6px; font-size: 11px; }
12
+ .mld-exp-code--md { padding: 2px 8px; font-size: 12px; }
13
+ .mld-exp-code--lg { padding: 3px 10px; font-size: 14px; }
@@ -0,0 +1,131 @@
1
+ .mld-data-viewer {
2
+ border: 1px solid var(--mld-color-border, #e0e0e0);
3
+ border-radius: var(--mld-radius-md, 8px);
4
+ overflow: hidden;
5
+ }
6
+ .mld-data-viewer__header {
7
+ display: flex;
8
+ align-items: center;
9
+ justify-content: space-between;
10
+ padding: 8px 12px;
11
+ border-bottom: 1px solid var(--mld-color-border, #e0e0e0);
12
+ background: var(--mld-color-surface-1, #fafafa);
13
+ gap: 8px;
14
+ }
15
+ .mld-data-viewer__controls { display: flex; gap: 8px; }
16
+ .mld-data-viewer__actions { display: flex; gap: 4px; align-items: center; }
17
+ .mld-data-viewer__content {
18
+ padding: 12px;
19
+ max-height: 600px;
20
+ overflow-y: auto;
21
+ }
22
+ .mld-data-viewer__loading,
23
+ .mld-data-viewer__empty {
24
+ text-align: center;
25
+ padding: 32px;
26
+ color: var(--mld-color-text-muted, #999);
27
+ }
28
+
29
+ /* Summary view */
30
+ .mld-summary {
31
+ display: flex;
32
+ flex-direction: column;
33
+ gap: 16px;
34
+ }
35
+
36
+ /* Metadata pills row */
37
+ .mld-summary__metadata {
38
+ display: flex;
39
+ flex-wrap: wrap;
40
+ gap: 6px;
41
+ }
42
+
43
+ .mld-summary__pill {
44
+ display: inline-flex;
45
+ align-items: center;
46
+ border-radius: 6px;
47
+ overflow: hidden;
48
+ font-size: 0.8125rem;
49
+ line-height: 1;
50
+ border: 1px solid var(--mld-color-border, #e0e0e0);
51
+ }
52
+
53
+ .mld-summary__pill--sm {
54
+ font-size: 0.75rem;
55
+ }
56
+
57
+ .mld-summary__pill-key {
58
+ padding: 4px 8px;
59
+ background: var(--mld-color-surface-1, #f5f5f5);
60
+ color: var(--mld-color-text-muted, #666);
61
+ text-transform: capitalize;
62
+ font-weight: 500;
63
+ }
64
+
65
+ .mld-summary__pill-value {
66
+ padding: 4px 8px;
67
+ color: var(--mld-color-text, #1a1a1a);
68
+ }
69
+
70
+ /* Section */
71
+ .mld-summary__section {
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: 12px;
75
+ }
76
+
77
+ .mld-summary__section-label {
78
+ font-weight: 600;
79
+ font-size: 0.875rem;
80
+ color: var(--mld-color-text, #1a1a1a);
81
+ }
82
+
83
+ .mld-summary__section-count {
84
+ font-size: 0.75rem;
85
+ color: var(--mld-color-text-muted, #999);
86
+ margin-left: 8px;
87
+ }
88
+
89
+ .mld-summary__table-header {
90
+ display: flex;
91
+ align-items: baseline;
92
+ gap: 4px;
93
+ }
94
+
95
+ /* Group cards */
96
+ .mld-summary__group-card {
97
+ border: 1px solid var(--mld-color-border, #e0e0e0);
98
+ border-radius: var(--mld-radius-md, 8px);
99
+ overflow: hidden;
100
+ }
101
+
102
+ .mld-summary__group-header {
103
+ display: flex;
104
+ align-items: center;
105
+ justify-content: space-between;
106
+ padding: 10px 14px;
107
+ background: var(--mld-color-surface-1, #fafafa);
108
+ border-bottom: 1px solid var(--mld-color-border, #e0e0e0);
109
+ }
110
+
111
+ .mld-summary__group-label {
112
+ font-weight: 600;
113
+ font-size: 0.875rem;
114
+ color: var(--mld-color-text, #1a1a1a);
115
+ }
116
+
117
+ .mld-summary__group-count {
118
+ font-size: 0.75rem;
119
+ color: var(--mld-color-text-muted, #999);
120
+ background: var(--mld-color-surface-2, #eee);
121
+ padding: 2px 8px;
122
+ border-radius: 10px;
123
+ }
124
+
125
+ .mld-summary__group-meta {
126
+ display: flex;
127
+ flex-wrap: wrap;
128
+ gap: 6px;
129
+ padding: 8px 14px;
130
+ border-bottom: 1px solid var(--mld-color-border, #e0e0e0);
131
+ }
@@ -67,7 +67,7 @@
67
67
  }
68
68
 
69
69
  .mld-modal__close {
70
- padding: 0.25rem;
70
+ padding: 0.5rem;
71
71
  border-radius: var(--mld-radius-sm);
72
72
  color: var(--text-muted);
73
73
  background: none;
@@ -33,7 +33,7 @@
33
33
  }
34
34
 
35
35
  .mld-select__control--disabled {
36
- opacity: 0.5;
36
+ opacity: var(--mld-disabled-opacity);
37
37
  cursor: not-allowed;
38
38
  background-color: var(--bg-tertiary);
39
39
  }
@@ -7,7 +7,7 @@
7
7
  }
8
8
 
9
9
  .mld-slider--disabled {
10
- opacity: 0.5;
10
+ opacity: var(--mld-disabled-opacity);
11
11
  cursor: not-allowed;
12
12
  }
13
13
 
@@ -24,18 +24,14 @@
24
24
  left: 0;
25
25
  right: 0;
26
26
  border-radius: 9999px;
27
- background-color: #E2E8F0;
28
- }
29
-
30
- html.dark .mld-slider__track {
31
- background-color: #334155;
27
+ background-color: var(--bg-tertiary);
32
28
  }
33
29
 
34
30
  .mld-slider__fill {
35
31
  position: absolute;
36
32
  left: 0;
37
33
  border-radius: 9999px;
38
- background-color: #3B82F6;
34
+ background-color: var(--color-primary);
39
35
  }
40
36
 
41
37
  .mld-slider__input {
@@ -59,7 +55,7 @@ html.dark .mld-slider__track {
59
55
  transform: translateX(-50%) translateY(-50%);
60
56
  border-radius: 9999px;
61
57
  background-color: white;
62
- border: 2px solid #3B82F6;
58
+ border: 2px solid var(--color-primary);
63
59
  box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
64
60
  pointer-events: none;
65
61
  transition: transform 150ms ease;
@@ -13,6 +13,10 @@
13
13
  color: var(--text-muted);
14
14
  }
15
15
 
16
+ .mld-textarea:hover:not(:focus):not(.mld-textarea--disabled):not(.mld-textarea--error) {
17
+ border-color: var(--text-muted);
18
+ }
19
+
16
20
  .mld-textarea:focus {
17
21
  outline: none;
18
22
  border-color: transparent;
@@ -28,7 +32,7 @@
28
32
  }
29
33
 
30
34
  .mld-textarea--disabled {
31
- opacity: 0.5;
35
+ opacity: var(--mld-disabled-opacity);
32
36
  cursor: not-allowed;
33
37
  background-color: var(--bg-tertiary);
34
38
  }
@@ -79,3 +79,5 @@
79
79
  @import './components/schedule-calendar.css';
80
80
  @import './components/resource-card.css';
81
81
  @import './components/form-builder.css';
82
+ @import './components/experiment-data-viewer.css';
83
+ @import './components/experiment-code-badge.css';
@@ -55,14 +55,19 @@
55
55
 
56
56
  /* Semantic colors */
57
57
  --mld-success: #10B981;
58
+ --mld-success-hover: #059669;
58
59
  --mld-success-bg: rgba(16, 185, 129, 0.1);
59
60
  --mld-error: #EF4444;
61
+ --mld-error-hover: #DC2626;
60
62
  --mld-error-bg: rgba(239, 68, 68, 0.1);
61
63
  --mld-warning: #F59E0B;
62
64
  --mld-warning-bg: rgba(245, 158, 11, 0.1);
63
65
  --mld-info: #3B82F6;
64
66
  --mld-info-bg: rgba(59, 130, 246, 0.1);
65
67
 
68
+ /* Shared component tokens */
69
+ --mld-disabled-opacity: 0.6;
70
+
66
71
  /* Legacy aliases (for backwards compatibility) */
67
72
  --mld-bg-primary: var(--bg-primary);
68
73
  --mld-bg-secondary: var(--bg-secondary);
@@ -631,11 +636,11 @@ code, pre {
631
636
  }
632
637
 
633
638
  .hover\:bg-mld-success-hover:hover {
634
- background-color: #059669;
639
+ background-color: var(--mld-success-hover);
635
640
  }
636
641
 
637
642
  .hover\:bg-mld-danger-hover:hover {
638
- background-color: #DC2626;
643
+ background-color: var(--mld-error-hover);
639
644
  }
640
645
 
641
646
  .bg-mld-primary\/10 {