@morscherlab/mld-sdk 0.7.2 → 0.7.4

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 (56) 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 +311 -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/collapsible-card.css +7 -0
  47. package/src/styles/components/experiment-code-badge.css +13 -0
  48. package/src/styles/components/experiment-data-viewer.css +131 -0
  49. package/src/styles/components/modal.css +1 -1
  50. package/src/styles/components/select.css +1 -1
  51. package/src/styles/components/slider.css +4 -8
  52. package/src/styles/components/textarea.css +5 -1
  53. package/src/styles/index.css +2 -0
  54. package/src/styles/variables.css +7 -2
  55. package/src/types/components.ts +27 -0
  56. package/src/types/index.ts +4 -0
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { watch, onMounted, onUnmounted } from 'vue'
2
+ import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
3
3
  import type { ModalSize } from '../types'
4
4
 
5
5
  interface Props {
@@ -23,6 +23,49 @@ const emit = defineEmits<{
23
23
  close: []
24
24
  }>()
25
25
 
26
+ const containerRef = ref<HTMLElement | null>(null)
27
+ let previouslyFocused: HTMLElement | null = null
28
+
29
+ const FOCUSABLE_SELECTOR = [
30
+ 'a[href]',
31
+ 'button:not([disabled])',
32
+ 'input:not([disabled])',
33
+ 'select:not([disabled])',
34
+ 'textarea:not([disabled])',
35
+ '[tabindex]:not([tabindex="-1"])',
36
+ ].join(', ')
37
+
38
+ function getFocusableElements(): HTMLElement[] {
39
+ if (!containerRef.value) return []
40
+ return Array.from(containerRef.value.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))
41
+ }
42
+
43
+ function handleKeydown(event: KeyboardEvent) {
44
+ if (event.key === 'Escape' && props.closeOnEscape && props.modelValue) {
45
+ close()
46
+ return
47
+ }
48
+
49
+ if (event.key !== 'Tab' || !containerRef.value) return
50
+
51
+ const focusable = getFocusableElements()
52
+ if (focusable.length === 0) {
53
+ event.preventDefault()
54
+ return
55
+ }
56
+
57
+ const first = focusable[0]
58
+ const last = focusable[focusable.length - 1]
59
+
60
+ if (event.shiftKey && document.activeElement === first) {
61
+ event.preventDefault()
62
+ last.focus()
63
+ } else if (!event.shiftKey && document.activeElement === last) {
64
+ event.preventDefault()
65
+ first.focus()
66
+ }
67
+ }
68
+
26
69
  function close() {
27
70
  if (props.closable) {
28
71
  emit('update:modelValue', false)
@@ -36,26 +79,30 @@ function handleOverlayClick(event: MouseEvent) {
36
79
  }
37
80
  }
38
81
 
39
- function handleEscape(event: KeyboardEvent) {
40
- if (props.closeOnEscape && event.key === 'Escape' && props.modelValue) {
41
- close()
42
- }
43
- }
44
-
45
- watch(() => props.modelValue, (isOpen) => {
82
+ watch(() => props.modelValue, async (isOpen) => {
46
83
  if (isOpen) {
84
+ previouslyFocused = document.activeElement as HTMLElement | null
47
85
  document.body.style.overflow = 'hidden'
86
+ await nextTick()
87
+ const focusable = getFocusableElements()
88
+ if (focusable.length > 0) {
89
+ focusable[0].focus()
90
+ } else {
91
+ containerRef.value?.focus()
92
+ }
48
93
  } else {
49
94
  document.body.style.overflow = ''
95
+ previouslyFocused?.focus()
96
+ previouslyFocused = null
50
97
  }
51
98
  })
52
99
 
53
100
  onMounted(() => {
54
- document.addEventListener('keydown', handleEscape)
101
+ document.addEventListener('keydown', handleKeydown)
55
102
  })
56
103
 
57
104
  onUnmounted(() => {
58
- document.removeEventListener('keydown', handleEscape)
105
+ document.removeEventListener('keydown', handleKeydown)
59
106
  document.body.style.overflow = ''
60
107
  })
61
108
  </script>
@@ -73,12 +120,14 @@ onUnmounted(() => {
73
120
 
74
121
  <!-- Modal -->
75
122
  <div
123
+ ref="containerRef"
76
124
  :class="[
77
125
  'mld-modal__container',
78
126
  `mld-modal__container--${size}`,
79
127
  ]"
80
128
  role="dialog"
81
129
  aria-modal="true"
130
+ tabindex="-1"
82
131
  >
83
132
  <!-- Header -->
84
133
  <div v-if="title || closable" class="mld-modal__header">
@@ -8,6 +8,7 @@ interface Props {
8
8
  disabled?: boolean
9
9
  error?: boolean
10
10
  size?: 'sm' | 'md' | 'lg'
11
+ ariaDescribedby?: string
11
12
  }
12
13
 
13
14
  const props = withDefaults(defineProps<Props>(), {
@@ -34,6 +35,8 @@ function handleChange(event: Event) {
34
35
  <select
35
36
  :value="modelValue"
36
37
  :disabled="disabled"
38
+ :aria-invalid="error || undefined"
39
+ :aria-describedby="ariaDescribedby || undefined"
37
40
  :class="[
38
41
  'mld-select__control',
39
42
  `mld-select__control--${size}`,
@@ -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 {
@@ -7,6 +7,13 @@
7
7
  background-color: var(--bg-card);
8
8
  }
9
9
 
10
+ /* Allow floating dropdowns (DatePicker, TimePicker, etc.) to escape overflow */
11
+ .mld-collapsible-card:has(.mld-date-picker__dropdown),
12
+ .mld-collapsible-card:has(.mld-time-picker__dropdown),
13
+ .mld-collapsible-card:has(.mld-datetime-picker__dropdown) {
14
+ overflow: visible;
15
+ }
16
+
10
17
  .mld-collapsible-card__header {
11
18
  display: flex;
12
19
  align-items: center;
@@ -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;