@morscherlab/mint-sdk 1.1.0-beta.2 → 1.1.0-beta.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.
- package/dist/__tests__/components/SectionCard.test.d.ts +1 -0
- package/dist/components/SectionCard.vue.d.ts +47 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +2 -2
- package/dist/{components-UfkPN1bK.js → components-DFH5whuh.js} +699 -620
- package/dist/components-DFH5whuh.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/install.js +1 -1
- package/dist/styles.css +301 -0
- package/dist/tailwind.preset.d.ts +5 -0
- package/dist/tailwind.preset.js +7 -2
- package/dist/tailwind.preset.js.map +1 -1
- package/dist/types/components.d.ts +2 -0
- package/dist/types/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/SectionCard.test.ts +181 -0
- package/src/components/SectionCard.story.vue +163 -0
- package/src/components/SectionCard.vue +98 -0
- package/src/components/index.ts +1 -0
- package/src/styles/components/section-card.css +161 -0
- package/src/styles/index.css +1 -0
- package/src/styles/variables.css +25 -0
- package/src/tailwind.preset.ts +5 -0
- package/src/types/components.ts +12 -0
- package/src/types/index.ts +2 -0
- package/dist/components-UfkPN1bK.js.map +0 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import SectionCard from '../../components/SectionCard.vue'
|
|
4
|
+
|
|
5
|
+
describe('SectionCard', () => {
|
|
6
|
+
describe('basic rendering', () => {
|
|
7
|
+
it('should render container, header, and title when given the required title prop', () => {
|
|
8
|
+
const wrapper = mount(SectionCard, {
|
|
9
|
+
props: { title: 'Experiments' },
|
|
10
|
+
})
|
|
11
|
+
expect(wrapper.find('.mint-section-card').exists()).toBe(true)
|
|
12
|
+
expect(wrapper.find('.mint-section-card__header').exists()).toBe(true)
|
|
13
|
+
expect(wrapper.find('.mint-section-card__title').text()).toBe('Experiments')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should render default slot content in the body', () => {
|
|
17
|
+
const wrapper = mount(SectionCard, {
|
|
18
|
+
props: { title: 'Experiments' },
|
|
19
|
+
slots: { default: '<p class="body-probe">rows</p>' },
|
|
20
|
+
})
|
|
21
|
+
expect(wrapper.find('.mint-section-card__body .body-probe').exists()).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('count meta', () => {
|
|
26
|
+
it('should render the count in the header when provided', () => {
|
|
27
|
+
const wrapper = mount(SectionCard, {
|
|
28
|
+
props: { title: 'Plugins', count: '10 enabled' },
|
|
29
|
+
})
|
|
30
|
+
expect(wrapper.find('.mint-section-card__count').text()).toBe('10 enabled')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should not render a count element when count is omitted', () => {
|
|
34
|
+
const wrapper = mount(SectionCard, {
|
|
35
|
+
props: { title: 'Plugins' },
|
|
36
|
+
})
|
|
37
|
+
expect(wrapper.find('.mint-section-card__count').exists()).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should accept a numeric count', () => {
|
|
41
|
+
const wrapper = mount(SectionCard, {
|
|
42
|
+
props: { title: 'Artifacts', count: 4 },
|
|
43
|
+
})
|
|
44
|
+
expect(wrapper.find('.mint-section-card__count').text()).toBe('4')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('title style variants', () => {
|
|
49
|
+
it('should apply the uppercase modifier when uppercase is true', () => {
|
|
50
|
+
const wrapper = mount(SectionCard, {
|
|
51
|
+
props: { title: 'Experiments', uppercase: true },
|
|
52
|
+
})
|
|
53
|
+
expect(wrapper.find('.mint-section-card__title--uppercase').exists()).toBe(true)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should not apply the uppercase modifier by default', () => {
|
|
57
|
+
const wrapper = mount(SectionCard, {
|
|
58
|
+
props: { title: 'Design data' },
|
|
59
|
+
})
|
|
60
|
+
expect(wrapper.find('.mint-section-card__title--uppercase').exists()).toBe(false)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('icon chip', () => {
|
|
65
|
+
it('should render an icon chip with an SVG path when icon is provided', () => {
|
|
66
|
+
const wrapper = mount(SectionCard, {
|
|
67
|
+
props: { title: 'Experiments', icon: 'M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7' },
|
|
68
|
+
})
|
|
69
|
+
expect(wrapper.find('.mint-section-card__icon-chip').exists()).toBe(true)
|
|
70
|
+
expect(wrapper.find('.mint-section-card__icon-chip path').exists()).toBe(true)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should render one path per entry when icon is an array of paths', () => {
|
|
74
|
+
const wrapper = mount(SectionCard, {
|
|
75
|
+
props: { title: 'Experiments', icon: ['M4 7v10', 'M4 7c0 2.21'] },
|
|
76
|
+
})
|
|
77
|
+
expect(wrapper.findAll('.mint-section-card__icon-chip path')).toHaveLength(2)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should not render an icon chip when icon is omitted', () => {
|
|
81
|
+
const wrapper = mount(SectionCard, {
|
|
82
|
+
props: { title: 'Experiments' },
|
|
83
|
+
})
|
|
84
|
+
expect(wrapper.find('.mint-section-card__icon-chip').exists()).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should apply the accent modifier class matching the accent prop', () => {
|
|
88
|
+
const wrapper = mount(SectionCard, {
|
|
89
|
+
props: { title: 'Projects', icon: 'M3 7v10', accent: 'project' },
|
|
90
|
+
})
|
|
91
|
+
expect(wrapper.find('.mint-section-card__icon-chip--project').exists()).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should default to the primary accent when accent is omitted', () => {
|
|
95
|
+
const wrapper = mount(SectionCard, {
|
|
96
|
+
props: { title: 'Projects', icon: 'M3 7v10' },
|
|
97
|
+
})
|
|
98
|
+
expect(wrapper.find('.mint-section-card__icon-chip--primary').exists()).toBe(true)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('header divider', () => {
|
|
103
|
+
it('should use the strong divider by default', () => {
|
|
104
|
+
const wrapper = mount(SectionCard, {
|
|
105
|
+
props: { title: 'Experiments' },
|
|
106
|
+
})
|
|
107
|
+
expect(wrapper.find('.mint-section-card__header--divider-light').exists()).toBe(false)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should apply the light divider modifier when divider is light', () => {
|
|
111
|
+
const wrapper = mount(SectionCard, {
|
|
112
|
+
props: { title: 'Design data', divider: 'light' },
|
|
113
|
+
})
|
|
114
|
+
expect(wrapper.find('.mint-section-card__header--divider-light').exists()).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('fill mode', () => {
|
|
119
|
+
it('should not stretch by default', () => {
|
|
120
|
+
const wrapper = mount(SectionCard, {
|
|
121
|
+
props: { title: 'Experiments' },
|
|
122
|
+
})
|
|
123
|
+
expect(wrapper.find('.mint-section-card--fill').exists()).toBe(false)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should apply the fill modifier when fill is true', () => {
|
|
127
|
+
const wrapper = mount(SectionCard, {
|
|
128
|
+
props: { title: 'Experiments', fill: true },
|
|
129
|
+
})
|
|
130
|
+
expect(wrapper.find('.mint-section-card--fill').exists()).toBe(true)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should render the toolbar slot outside the scrolling body', () => {
|
|
134
|
+
const wrapper = mount(SectionCard, {
|
|
135
|
+
props: { title: 'Experiments', fill: true },
|
|
136
|
+
slots: { toolbar: '<div class="toolbar-probe">chips</div>', default: '<p>rows</p>' },
|
|
137
|
+
})
|
|
138
|
+
expect(wrapper.find('.mint-section-card__toolbar .toolbar-probe').exists()).toBe(true)
|
|
139
|
+
expect(wrapper.find('.mint-section-card__body .toolbar-probe').exists()).toBe(false)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should not render a toolbar region when the slot is unused', () => {
|
|
143
|
+
const wrapper = mount(SectionCard, {
|
|
144
|
+
props: { title: 'Experiments' },
|
|
145
|
+
})
|
|
146
|
+
expect(wrapper.find('.mint-section-card__toolbar').exists()).toBe(false)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should keep the header, body and footer structure when filling', () => {
|
|
150
|
+
const wrapper = mount(SectionCard, {
|
|
151
|
+
props: { title: 'Experiments', fill: true },
|
|
152
|
+
slots: { default: '<p>rows</p>', footer: '<span>1 / 3</span>' },
|
|
153
|
+
})
|
|
154
|
+
expect(wrapper.find('.mint-section-card__header').exists()).toBe(true)
|
|
155
|
+
expect(wrapper.find('.mint-section-card__body').exists()).toBe(true)
|
|
156
|
+
expect(wrapper.find('.mint-section-card__footer').exists()).toBe(true)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('slots', () => {
|
|
161
|
+
it('should render the actions slot at the end of the header', () => {
|
|
162
|
+
const wrapper = mount(SectionCard, {
|
|
163
|
+
props: { title: 'Experiments' },
|
|
164
|
+
slots: { actions: '<a class="actions-probe" href="#">All →</a>' },
|
|
165
|
+
})
|
|
166
|
+
expect(wrapper.find('.mint-section-card__header .actions-probe').exists()).toBe(true)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should render the footer strip only when the footer slot is provided', () => {
|
|
170
|
+
const withFooter = mount(SectionCard, {
|
|
171
|
+
props: { title: 'Experiments' },
|
|
172
|
+
slots: { footer: '<span class="footer-probe">Showing 1–18 of 42</span>' },
|
|
173
|
+
})
|
|
174
|
+
const withoutFooter = mount(SectionCard, {
|
|
175
|
+
props: { title: 'Experiments' },
|
|
176
|
+
})
|
|
177
|
+
expect(withFooter.find('.mint-section-card__footer .footer-probe').exists()).toBe(true)
|
|
178
|
+
expect(withoutFooter.find('.mint-section-card__footer').exists()).toBe(false)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
})
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import SectionCard from './SectionCard.vue'
|
|
3
|
+
import type { SectionCardAccent, SectionCardDivider } from '../types'
|
|
4
|
+
|
|
5
|
+
const flaskIcon =
|
|
6
|
+
'M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4'
|
|
7
|
+
const folderIcon = 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z'
|
|
8
|
+
|
|
9
|
+
const experiments = [
|
|
10
|
+
{ code: 'MM-2026-014', name: '13C glucose tracing — HeLa, 24h', updated: '2h ago' },
|
|
11
|
+
{ code: 'MM-2026-013', name: 'Hypoxia timecourse — passage 6', updated: '5h ago' },
|
|
12
|
+
{ code: 'MM-2026-011', name: 'Glutamine dropout, triplicate', updated: 'yesterday' },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
function initPlayground() {
|
|
16
|
+
return {
|
|
17
|
+
title: 'Experiments',
|
|
18
|
+
count: '14 ongoing · 42 total',
|
|
19
|
+
accent: 'experiment' as SectionCardAccent,
|
|
20
|
+
uppercase: true,
|
|
21
|
+
divider: 'strong' as SectionCardDivider,
|
|
22
|
+
showIcon: true,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<Story title="Layout/SectionCard">
|
|
29
|
+
<Variant title="Playground" :init-state="initPlayground">
|
|
30
|
+
<template #default="{ state }">
|
|
31
|
+
<div style="padding: 2rem; max-width: 600px;">
|
|
32
|
+
<SectionCard
|
|
33
|
+
:title="state.title"
|
|
34
|
+
:count="state.count || undefined"
|
|
35
|
+
:icon="state.showIcon ? flaskIcon : undefined"
|
|
36
|
+
:accent="state.accent"
|
|
37
|
+
:uppercase="state.uppercase"
|
|
38
|
+
:divider="state.divider"
|
|
39
|
+
>
|
|
40
|
+
<div class="mint-section-card__rows">
|
|
41
|
+
<a
|
|
42
|
+
v-for="e in experiments"
|
|
43
|
+
:key="e.code"
|
|
44
|
+
href="#"
|
|
45
|
+
class="mint-section-card__row"
|
|
46
|
+
>
|
|
47
|
+
<span style="flex: 1; min-width: 0">
|
|
48
|
+
<span style="display: block; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">{{ e.name }}</span>
|
|
49
|
+
<span style="display: block; font-family: 'Fira Code', monospace; font-size: 0.65625rem; color: var(--text-muted)">{{ e.code }}</span>
|
|
50
|
+
</span>
|
|
51
|
+
<span style="flex-shrink: 0; font-size: 0.71875rem; color: var(--text-muted)">{{ e.updated }}</span>
|
|
52
|
+
</a>
|
|
53
|
+
</div>
|
|
54
|
+
</SectionCard>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<template #controls="{ state }">
|
|
59
|
+
<HstText v-model="state.title" title="Title" />
|
|
60
|
+
<HstText v-model="state.count" title="Count" />
|
|
61
|
+
<HstCheckbox v-model="state.showIcon" title="Show icon chip" />
|
|
62
|
+
<HstCheckbox v-model="state.uppercase" title="Uppercase title" />
|
|
63
|
+
<HstSelect
|
|
64
|
+
v-model="state.accent"
|
|
65
|
+
title="Accent"
|
|
66
|
+
:options="{
|
|
67
|
+
primary: 'primary',
|
|
68
|
+
experiment: 'experiment',
|
|
69
|
+
project: 'project',
|
|
70
|
+
success: 'success',
|
|
71
|
+
error: 'error',
|
|
72
|
+
warning: 'warning',
|
|
73
|
+
info: 'info',
|
|
74
|
+
}"
|
|
75
|
+
/>
|
|
76
|
+
<HstSelect
|
|
77
|
+
v-model="state.divider"
|
|
78
|
+
title="Divider"
|
|
79
|
+
:options="{ strong: 'strong', light: 'light' }"
|
|
80
|
+
/>
|
|
81
|
+
</template>
|
|
82
|
+
</Variant>
|
|
83
|
+
|
|
84
|
+
<Variant title="Home category card">
|
|
85
|
+
<div style="padding: 2rem; max-width: 600px;">
|
|
86
|
+
<SectionCard
|
|
87
|
+
title="Projects"
|
|
88
|
+
count="6 active · 9 total"
|
|
89
|
+
:icon="folderIcon"
|
|
90
|
+
accent="project"
|
|
91
|
+
uppercase
|
|
92
|
+
>
|
|
93
|
+
<template #actions>
|
|
94
|
+
<a href="#" style="font-size: 0.75rem; font-weight: 500; color: var(--color-primary-hover)">All →</a>
|
|
95
|
+
</template>
|
|
96
|
+
<div class="mint-section-card__rows">
|
|
97
|
+
<a href="#" class="mint-section-card__row">
|
|
98
|
+
<span style="flex: 1; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">Hypoxia metabolic rewiring</span>
|
|
99
|
+
<span style="font-size: 0.71875rem; color: var(--text-muted)">12 experiments</span>
|
|
100
|
+
</a>
|
|
101
|
+
<a href="#" class="mint-section-card__row">
|
|
102
|
+
<span style="flex: 1; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">Serine one-carbon flux</span>
|
|
103
|
+
<span style="font-size: 0.71875rem; color: var(--text-muted)">7 experiments</span>
|
|
104
|
+
</a>
|
|
105
|
+
</div>
|
|
106
|
+
</SectionCard>
|
|
107
|
+
</div>
|
|
108
|
+
</Variant>
|
|
109
|
+
|
|
110
|
+
<Variant title="Detail card with footer">
|
|
111
|
+
<div style="padding: 2rem; max-width: 600px;">
|
|
112
|
+
<SectionCard title="Analysis artifacts" :count="4" divider="light">
|
|
113
|
+
<div class="mint-section-card__rows">
|
|
114
|
+
<div class="mint-section-card__row">
|
|
115
|
+
<span style="flex: 1; font-size: 0.8125rem; color: var(--text-primary)">isotopologue_matrix.csv</span>
|
|
116
|
+
<span style="font-size: 0.71875rem; color: var(--text-secondary)">DRP</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="mint-section-card__row">
|
|
119
|
+
<span style="flex: 1; font-size: 0.8125rem; color: var(--text-primary)">natural_abundance_corrected.parquet</span>
|
|
120
|
+
<span style="font-size: 0.71875rem; color: var(--text-secondary)">DRP</span>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<template #footer>
|
|
124
|
+
<span style="font-size: 0.71875rem; color: var(--text-muted)">Showing 1–2 of 4</span>
|
|
125
|
+
<span style="font-size: 0.71875rem; color: var(--text-secondary)">1 / 2</span>
|
|
126
|
+
</template>
|
|
127
|
+
</SectionCard>
|
|
128
|
+
</div>
|
|
129
|
+
</Variant>
|
|
130
|
+
|
|
131
|
+
<Variant title="Fill with pinned toolbar">
|
|
132
|
+
<div style="padding: 2rem; height: 420px; display: flex;">
|
|
133
|
+
<SectionCard title="All experiments" :count="42" divider="light" fill>
|
|
134
|
+
<template #toolbar>
|
|
135
|
+
<div style="display: flex; gap: 0.5rem; padding: 0.6875rem 1rem; border-bottom: 1px solid var(--border-color)">
|
|
136
|
+
<span style="font-size: 0.78125rem; color: var(--text-secondary)">Toolbar stays put while the rows scroll</span>
|
|
137
|
+
</div>
|
|
138
|
+
</template>
|
|
139
|
+
<div class="mint-section-card__rows">
|
|
140
|
+
<div v-for="n in 20" :key="n" class="mint-section-card__row">
|
|
141
|
+
<span style="flex: 1; font-size: 0.8125rem; color: var(--text-primary)">Row {{ n }}</span>
|
|
142
|
+
<span style="font-family: 'Fira Code', monospace; font-size: 0.71875rem; color: var(--text-muted)">MM-2026-{{ String(n).padStart(3, '0') }}</span>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<template #footer>
|
|
146
|
+
<span style="font-size: 0.71875rem; color: var(--text-muted)">Showing 1–20 of 42</span>
|
|
147
|
+
<span style="font-size: 0.71875rem; color: var(--text-secondary)">1 / 3</span>
|
|
148
|
+
</template>
|
|
149
|
+
</SectionCard>
|
|
150
|
+
</div>
|
|
151
|
+
</Variant>
|
|
152
|
+
|
|
153
|
+
<Variant title="No icon, plain title">
|
|
154
|
+
<div style="padding: 2rem; max-width: 600px;">
|
|
155
|
+
<SectionCard title="Design data" count="96 samples · 4 timepoints" divider="light">
|
|
156
|
+
<div style="padding: 1rem; font-size: 0.8125rem; color: var(--text-secondary)">
|
|
157
|
+
Card body content sits directly on the card surface — no nested card.
|
|
158
|
+
</div>
|
|
159
|
+
</SectionCard>
|
|
160
|
+
</div>
|
|
161
|
+
</Variant>
|
|
162
|
+
</Story>
|
|
163
|
+
</template>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** One-card page region from the MINT 1.1 card system: 12px radius, the
|
|
3
|
+
* single --card-shadow token, a header strip (icon chip, title, mono count,
|
|
4
|
+
* actions), and hairline-separated content — no card-in-card. */
|
|
5
|
+
import { computed } from 'vue'
|
|
6
|
+
import type { SectionCardAccent, SectionCardDivider } from '../types'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
title: string
|
|
10
|
+
/** Monospace meta shown next to the title, e.g. "14 ongoing · 42 total". */
|
|
11
|
+
count?: string | number
|
|
12
|
+
/** SVG path(s) for the 22px icon chip; omit to render no chip. */
|
|
13
|
+
icon?: string | string[]
|
|
14
|
+
/** Tint of the icon chip. */
|
|
15
|
+
accent?: SectionCardAccent
|
|
16
|
+
/** Uppercase category heading (home cards) vs plain heading (detail cards). */
|
|
17
|
+
uppercase?: boolean
|
|
18
|
+
/** Header hairline: 'strong' = --border-color, 'light' = --border-light. */
|
|
19
|
+
divider?: SectionCardDivider
|
|
20
|
+
/**
|
|
21
|
+
* Stretch to fill the height its parent offers, scrolling the body and
|
|
22
|
+
* pinning the header and footer. Requires a parent that gives the card a
|
|
23
|
+
* definite height (a flex or grid track).
|
|
24
|
+
*/
|
|
25
|
+
fill?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
29
|
+
count: undefined,
|
|
30
|
+
icon: undefined,
|
|
31
|
+
accent: 'primary',
|
|
32
|
+
uppercase: false,
|
|
33
|
+
divider: 'strong',
|
|
34
|
+
fill: false,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const iconPaths = computed(() =>
|
|
38
|
+
props.icon === undefined ? [] : Array.isArray(props.icon) ? props.icon : [props.icon],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const headerClasses = computed(() => [
|
|
42
|
+
'mint-section-card__header',
|
|
43
|
+
props.divider === 'light' ? 'mint-section-card__header--divider-light' : '',
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
const titleClasses = computed(() => [
|
|
47
|
+
'mint-section-card__title',
|
|
48
|
+
props.uppercase ? 'mint-section-card__title--uppercase' : '',
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
const cardClasses = computed(() => [
|
|
52
|
+
'mint-section-card',
|
|
53
|
+
props.fill ? 'mint-section-card--fill' : '',
|
|
54
|
+
])
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<section :class="cardClasses">
|
|
59
|
+
<div :class="headerClasses">
|
|
60
|
+
<span
|
|
61
|
+
v-if="iconPaths.length"
|
|
62
|
+
:class="['mint-section-card__icon-chip', `mint-section-card__icon-chip--${accent}`]"
|
|
63
|
+
>
|
|
64
|
+
<svg
|
|
65
|
+
class="mint-section-card__icon"
|
|
66
|
+
viewBox="0 0 24 24"
|
|
67
|
+
fill="none"
|
|
68
|
+
stroke="currentColor"
|
|
69
|
+
stroke-width="2"
|
|
70
|
+
stroke-linecap="round"
|
|
71
|
+
stroke-linejoin="round"
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
>
|
|
74
|
+
<path v-for="(d, i) in iconPaths" :key="i" :d="d" />
|
|
75
|
+
</svg>
|
|
76
|
+
</span>
|
|
77
|
+
<h2 :class="titleClasses">{{ title }}</h2>
|
|
78
|
+
<span v-if="count !== undefined" class="mint-section-card__count">{{ count }}</span>
|
|
79
|
+
<div class="mint-section-card__spacer"></div>
|
|
80
|
+
<slot name="actions" />
|
|
81
|
+
</div>
|
|
82
|
+
<!-- Controls belong to the card's chrome, so they sit outside the body and
|
|
83
|
+
stay put while the body scrolls. -->
|
|
84
|
+
<div v-if="$slots.toolbar" class="mint-section-card__toolbar">
|
|
85
|
+
<slot name="toolbar" />
|
|
86
|
+
</div>
|
|
87
|
+
<div class="mint-section-card__body">
|
|
88
|
+
<slot />
|
|
89
|
+
</div>
|
|
90
|
+
<div v-if="$slots.footer" class="mint-section-card__footer">
|
|
91
|
+
<slot name="footer" />
|
|
92
|
+
</div>
|
|
93
|
+
</section>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<style>
|
|
97
|
+
@import '../styles/components/section-card.css';
|
|
98
|
+
</style>
|
package/src/components/index.ts
CHANGED
|
@@ -126,6 +126,7 @@ export { default as DateTimePicker } from './DateTimePicker.vue'
|
|
|
126
126
|
export { default as TimeRangeInput } from './TimeRangeInput.vue'
|
|
127
127
|
export { default as ScheduleCalendar } from './ScheduleCalendar.vue'
|
|
128
128
|
export { default as ResourceCard } from './ResourceCard.vue'
|
|
129
|
+
export { default as SectionCard } from './SectionCard.vue'
|
|
129
130
|
|
|
130
131
|
// Experiment / analysis components
|
|
131
132
|
export { default as ExperimentSelectorModal } from './ExperimentSelectorModal.vue'
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/* SectionCard — the MINT 1.1 one-card system.
|
|
2
|
+
*
|
|
3
|
+
* One card per page region: 12px radius, one shadow token (--card-shadow),
|
|
4
|
+
* a single header strip, and hairline-separated regions inside — never a
|
|
5
|
+
* card nested in a card.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.mint-section-card {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
border-radius: 12px;
|
|
12
|
+
border: 1px solid var(--border-color);
|
|
13
|
+
background: var(--bg-secondary);
|
|
14
|
+
box-shadow: var(--card-shadow);
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
transition: background-color var(--mint-transition), border-color var(--mint-transition);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Fill the height the parent offers: the body takes the slack and scrolls,
|
|
20
|
+
* so the header and footer stay pinned to the card's edges. */
|
|
21
|
+
.mint-section-card--fill {
|
|
22
|
+
flex: 1;
|
|
23
|
+
min-height: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.mint-section-card--fill .mint-section-card__body {
|
|
27
|
+
flex: 1;
|
|
28
|
+
min-height: 0;
|
|
29
|
+
overflow: auto;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.mint-section-card__header {
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: 0.625rem;
|
|
36
|
+
padding: 0.6875rem 1rem;
|
|
37
|
+
border-bottom: 1px solid var(--border-color);
|
|
38
|
+
flex-shrink: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.mint-section-card__header--divider-light {
|
|
42
|
+
border-bottom-color: var(--border-light);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.mint-section-card__icon-chip {
|
|
46
|
+
display: inline-flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
flex-shrink: 0;
|
|
50
|
+
width: 1.375rem;
|
|
51
|
+
height: 1.375rem;
|
|
52
|
+
border-radius: 6px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.mint-section-card__icon {
|
|
56
|
+
width: 0.8125rem;
|
|
57
|
+
height: 0.8125rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.mint-section-card__icon-chip--primary {
|
|
61
|
+
background: var(--color-primary-soft);
|
|
62
|
+
color: var(--color-primary);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.mint-section-card__icon-chip--experiment {
|
|
66
|
+
background: var(--color-experiment-soft);
|
|
67
|
+
color: var(--color-experiment);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.mint-section-card__icon-chip--project {
|
|
71
|
+
background: var(--color-project-soft);
|
|
72
|
+
color: var(--color-project);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.mint-section-card__icon-chip--success {
|
|
76
|
+
background: var(--mint-success-bg);
|
|
77
|
+
color: var(--mint-success);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.mint-section-card__icon-chip--error {
|
|
81
|
+
background: var(--mint-error-bg);
|
|
82
|
+
color: var(--mint-error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.mint-section-card__icon-chip--warning {
|
|
86
|
+
background: var(--mint-warning-bg);
|
|
87
|
+
color: var(--mint-warning);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.mint-section-card__icon-chip--info {
|
|
91
|
+
background: var(--mint-info-bg);
|
|
92
|
+
color: var(--mint-info);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.mint-section-card__title {
|
|
96
|
+
margin: 0;
|
|
97
|
+
font-size: 0.8125rem;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
color: var(--text-primary);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.mint-section-card__title--uppercase {
|
|
103
|
+
font-size: 0.75rem;
|
|
104
|
+
letter-spacing: 0.08em;
|
|
105
|
+
text-transform: uppercase;
|
|
106
|
+
color: var(--text-secondary);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.mint-section-card__count {
|
|
110
|
+
font-family: 'Fira Code', monospace;
|
|
111
|
+
font-size: 0.6875rem;
|
|
112
|
+
color: var(--text-muted);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.mint-section-card__spacer {
|
|
116
|
+
flex: 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.mint-section-card__toolbar {
|
|
120
|
+
flex-shrink: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.mint-section-card__footer {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: space-between;
|
|
127
|
+
padding: 0.5rem 1rem;
|
|
128
|
+
border-top: 1px solid var(--border-color);
|
|
129
|
+
background: var(--bg-primary);
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Hairline row helpers — regions inside a card separate with 1px lines,
|
|
134
|
+
* rendered by a 1px gap over a hairline-colored track. */
|
|
135
|
+
.mint-section-card__rows {
|
|
136
|
+
display: flex;
|
|
137
|
+
flex-direction: column;
|
|
138
|
+
gap: 1px;
|
|
139
|
+
background: var(--border-light);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.mint-section-card__row {
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
gap: 0.625rem;
|
|
146
|
+
padding: 0.5625rem 1rem;
|
|
147
|
+
background: var(--bg-secondary);
|
|
148
|
+
transition: background-color var(--mint-transition);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
a.mint-section-card__row,
|
|
152
|
+
button.mint-section-card__row {
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
color: inherit;
|
|
155
|
+
text-decoration: none;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
a.mint-section-card__row:hover,
|
|
159
|
+
button.mint-section-card__row:hover {
|
|
160
|
+
background: var(--bg-primary);
|
|
161
|
+
}
|
package/src/styles/index.css
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
@import './components/plate-map-editor.css';
|
|
29
29
|
@import './components/radio-group.css';
|
|
30
30
|
@import './components/sample-legend.css';
|
|
31
|
+
@import './components/section-card.css';
|
|
31
32
|
@import './components/select.css';
|
|
32
33
|
@import './components/skeleton.css';
|
|
33
34
|
@import './components/slider.css';
|
package/src/styles/variables.css
CHANGED
|
@@ -36,6 +36,18 @@
|
|
|
36
36
|
--color-cta-hover: #EA580C;
|
|
37
37
|
--color-purple: #8B5CF6;
|
|
38
38
|
|
|
39
|
+
/* Entity accent colors — experiments (cyan) and projects (sky), from the
|
|
40
|
+
MINT 1.1 card system. -soft backs icon chips and pills, -border outlines
|
|
41
|
+
pills that sit on a card surface. */
|
|
42
|
+
--color-experiment: #06B6D4;
|
|
43
|
+
--color-experiment-hover: #0891B2;
|
|
44
|
+
--color-experiment-soft: rgba(6, 182, 212, 0.12);
|
|
45
|
+
--color-experiment-border: rgba(6, 182, 212, 0.3);
|
|
46
|
+
--color-project: #0EA5E9;
|
|
47
|
+
--color-project-hover: #0284C7;
|
|
48
|
+
--color-project-soft: rgba(14, 165, 233, 0.12);
|
|
49
|
+
--color-project-border: rgba(14, 165, 233, 0.32);
|
|
50
|
+
|
|
39
51
|
/* MINT brand mark — used for browser theme-color, marketing chrome, app icon background */
|
|
40
52
|
--mint-brand: #7BD0B5;
|
|
41
53
|
--mint-brand-hover: #5FB89A;
|
|
@@ -51,6 +63,8 @@
|
|
|
51
63
|
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
52
64
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
53
65
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
66
|
+
/* The MINT 1.1 card system uses exactly one shadow for every card. */
|
|
67
|
+
--card-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.04), 0 1px 3px 0 rgb(0 0 0 / 0.06);
|
|
54
68
|
|
|
55
69
|
/* Border radius */
|
|
56
70
|
--radius-sm: 0.25rem;
|
|
@@ -148,6 +162,17 @@ html.dark {
|
|
|
148
162
|
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.2);
|
|
149
163
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.2);
|
|
150
164
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.2);
|
|
165
|
+
--card-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.24), 0 1px 3px 0 rgb(0 0 0 / 0.32);
|
|
166
|
+
|
|
167
|
+
/* Entity accent colors - Dark mode */
|
|
168
|
+
--color-experiment: #22D3EE;
|
|
169
|
+
--color-experiment-hover: #67E8F9;
|
|
170
|
+
--color-experiment-soft: rgba(34, 211, 238, 0.16);
|
|
171
|
+
--color-experiment-border: rgba(34, 211, 238, 0.36);
|
|
172
|
+
--color-project: #38BDF8;
|
|
173
|
+
--color-project-hover: #7DD3FC;
|
|
174
|
+
--color-project-soft: rgba(56, 189, 248, 0.16);
|
|
175
|
+
--color-project-border: rgba(56, 189, 248, 0.38);
|
|
151
176
|
|
|
152
177
|
/* Semantic colors - Dark mode */
|
|
153
178
|
--mint-success-bg: rgba(16, 185, 129, 0.15);
|
package/src/tailwind.preset.ts
CHANGED
|
@@ -18,6 +18,10 @@ const mintPreset = {
|
|
|
18
18
|
'warning-hover': 'var(--mint-warning-hover)',
|
|
19
19
|
brand: 'var(--mint-brand)',
|
|
20
20
|
'brand-hover': 'var(--mint-brand-hover)',
|
|
21
|
+
experiment: 'var(--color-experiment)',
|
|
22
|
+
'experiment-hover': 'var(--color-experiment-hover)',
|
|
23
|
+
project: 'var(--color-project)',
|
|
24
|
+
'project-hover': 'var(--color-project-hover)',
|
|
21
25
|
},
|
|
22
26
|
bg: {
|
|
23
27
|
primary: 'var(--mint-bg-primary)',
|
|
@@ -50,6 +54,7 @@ const mintPreset = {
|
|
|
50
54
|
mint: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
|
51
55
|
'mint-md': '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
|
52
56
|
'mint-lg': '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
|
|
57
|
+
'mint-card': 'var(--card-shadow)',
|
|
53
58
|
},
|
|
54
59
|
transitionDuration: {
|
|
55
60
|
mint: '150ms',
|