@morscherlab/mint-sdk 1.3.0 → 1.3.1
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/AutoGroupModal.anatomy.test.d.ts +1 -0
- package/dist/__tests__/components/AutoGroupModal.wiring.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/fieldEdits.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/golden.test.d.ts +1 -0
- package/dist/components/AutoGroupModal.adapter.d.ts +27 -4
- package/dist/components/SmartGroup.types.d.ts +22 -0
- package/dist/components/SmartGroupFieldRecipe.groups.d.ts +10 -2
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +3 -0
- package/dist/components/index.js +1 -1
- package/dist/{components-vHfxV2PB.js → components-bbanLd1u.js} +499 -263
- package/dist/components-bbanLd1u.js.map +1 -0
- package/dist/composables/autoGroup/classify.d.ts +7 -1
- package/dist/composables/autoGroup/columns.d.ts +16 -1
- package/dist/composables/autoGroup/index.d.ts +5 -7
- package/dist/composables/autoGroup/tokenize.d.ts +36 -1
- package/dist/composables/index.js +2 -2
- package/dist/composables/useAutoGroup.d.ts +13 -2
- package/dist/{composables-D6uqp0UF.js → composables-BbHozlA7.js} +2 -2
- package/dist/{composables-D6uqp0UF.js.map → composables-BbHozlA7.js.map} +1 -1
- package/dist/index.js +3 -3
- package/dist/install.js +1 -1
- package/dist/permissions.js +2 -2
- package/dist/permissions.js.map +1 -1
- package/dist/styles.css +71 -82
- package/dist/types/auto-group.d.ts +1 -1
- package/dist/{validation-Dmp9Q5W0.js → validation-CukfLjy-.js} +323 -187
- package/dist/validation-CukfLjy-.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/components/AutoGroupModal.anatomy.test.ts +86 -0
- package/src/__tests__/components/AutoGroupModal.wiring.test.ts +53 -0
- package/src/__tests__/components/SampleSelector.test.ts +14 -16
- package/src/__tests__/components/SmartGroupFieldRecipe.groups.test.ts +23 -0
- package/src/__tests__/components/SmartGroupFieldRecipe.test.ts +176 -0
- package/src/__tests__/composables/autoGroup/columns.test.ts +100 -11
- package/src/__tests__/composables/autoGroup/fieldEdits.test.ts +74 -0
- package/src/__tests__/composables/autoGroup/fingerprint.test.ts +9 -0
- package/src/__tests__/composables/autoGroup/golden.test.ts +187 -0
- package/src/__tests__/composables/autoGroup/groupByOrder.test.ts +7 -7
- package/src/__tests__/composables/autoGroup/tokenLength.test.ts +7 -0
- package/src/__tests__/composables/autoGroup/tokenize.test.ts +78 -1
- package/src/__tests__/composables/useAutoGroup.test.ts +2 -1
- package/src/__tests__/fixtures/auto-group/share-exp021.txt +103 -0
- package/src/__tests__/fixtures/auto-group/share-exp031.txt +39 -0
- package/src/__tests__/fixtures/auto-group/share-exp114.txt +123 -0
- package/src/__tests__/fixtures/auto-group/share-exp214.txt +63 -0
- package/src/__tests__/utils/permissions.test.ts +15 -0
- package/src/components/AutoGroupModal.adapter.ts +80 -9
- package/src/components/AutoGroupModal.vue +41 -8
- package/src/components/SmartGroup.types.ts +23 -0
- package/src/components/SmartGroupFieldRecipe.groups.ts +14 -2
- package/src/components/SmartGroupFieldRecipe.vue +289 -169
- package/src/composables/autoGroup/classify.ts +26 -7
- package/src/composables/autoGroup/columns.ts +144 -36
- package/src/composables/autoGroup/compose.ts +8 -10
- package/src/composables/autoGroup/fingerprint.ts +1 -1
- package/src/composables/autoGroup/index.ts +5 -12
- package/src/composables/autoGroup/tokenLength.ts +8 -0
- package/src/composables/autoGroup/tokenize.ts +148 -1
- package/src/composables/autoGroup/vocab.json +5 -11
- package/src/composables/useAutoGroup.ts +92 -79
- package/src/permissions.ts +3 -2
- package/src/styles/components/smart-group.css +71 -82
- package/src/types/auto-group.ts +1 -0
- package/dist/components-vHfxV2PB.js.map +0 -1
- package/dist/composables/autoGroup/replicatePreGroup.d.ts +0 -36
- package/dist/validation-Dmp9Q5W0.js.map +0 -1
- package/src/composables/autoGroup/replicatePreGroup.ts +0 -124
|
@@ -16,21 +16,21 @@ import type {
|
|
|
16
16
|
ValueOps,
|
|
17
17
|
} from '../types/auto-group'
|
|
18
18
|
import {
|
|
19
|
+
GROUPING_FIELD_ROLES,
|
|
19
20
|
buildClassSchema,
|
|
20
21
|
classKey,
|
|
21
22
|
composeGroups,
|
|
22
23
|
composeTemplate,
|
|
23
24
|
detectClass,
|
|
24
|
-
expandGroupsWithReplicates,
|
|
25
|
-
expandTreeWithReplicates,
|
|
26
25
|
findMerges,
|
|
27
|
-
|
|
28
|
-
preGroupReplicates,
|
|
26
|
+
isExperimentalKind,
|
|
29
27
|
restoreFingerprint,
|
|
30
28
|
serializeFingerprint,
|
|
31
29
|
splitByTokenLength,
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
splitColumns,
|
|
31
|
+
tokenizeBatch,
|
|
32
|
+
type Delimiter,
|
|
33
|
+
type ExperimentHeader,
|
|
34
34
|
type TemplateOptions,
|
|
35
35
|
} from './autoGroup'
|
|
36
36
|
import { unwrapExperimentDesignData } from './experimentDesignData'
|
|
@@ -82,12 +82,10 @@ export function useAutoGroup() {
|
|
|
82
82
|
})
|
|
83
83
|
|
|
84
84
|
const tokenized = ref<string[][]>([])
|
|
85
|
-
const delimiter = ref<
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
// null when the paste path hasn't run (CSV path keeps the old behaviour for now).
|
|
90
|
-
const preGrouping = ref<ReplicatePreGrouping | null>(null)
|
|
85
|
+
const delimiter = ref<Delimiter>('_')
|
|
86
|
+
// Experiment code / date / operator that all pasted names share. Empty for
|
|
87
|
+
// CSV and experiment input.
|
|
88
|
+
const experiment = ref<ExperimentHeader>({})
|
|
91
89
|
|
|
92
90
|
function parseInput() {
|
|
93
91
|
if ((inputMode.value === 'csv' || inputMode.value === 'experiment') && csvData.value) {
|
|
@@ -98,9 +96,7 @@ export function useAutoGroup() {
|
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
function parseInputFromCsv() {
|
|
101
|
-
|
|
102
|
-
// isn't needed (and would interfere with header-aware role inference).
|
|
103
|
-
preGrouping.value = null
|
|
99
|
+
experiment.value = {}
|
|
104
100
|
const csv = csvData.value!
|
|
105
101
|
const nonSampleCols = csv.columns.filter(c => c !== csv.sampleColumn && c !== 'sample_type')
|
|
106
102
|
if (nonSampleCols.length === 0) {
|
|
@@ -146,47 +142,54 @@ export function useAutoGroup() {
|
|
|
146
142
|
if (lines.length === 0) {
|
|
147
143
|
classes.value = []
|
|
148
144
|
schemas.value = {}
|
|
149
|
-
|
|
145
|
+
experiment.value = {}
|
|
150
146
|
return
|
|
151
147
|
}
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
tokenized.value = pre.baseNames.map(l => splitMulti(l, d))
|
|
165
|
-
// sampleTypeHints align to original samples, so pick the first sample's
|
|
166
|
-
// hint per base group as the representative hint.
|
|
167
|
-
const hints = pre.membersByBase.map(members => sampleTypeHints.value[members[0]])
|
|
168
|
-
// Classify by sample type, THEN split each class by token length so every
|
|
169
|
-
// member of a class shares the same field count. This keeps the per-class
|
|
170
|
-
// token matrix rectangular, so column index N means the same field for
|
|
171
|
-
// every sample and the trailing injection number lands in its own column.
|
|
148
|
+
// One token row for each sample. Replicate tokens, sample numbers and the
|
|
149
|
+
// injection number stay in the row and get a column role below
|
|
150
|
+
// ('replicate', 'id', 'run-order'), which keeps them out of the default
|
|
151
|
+
// group key. Nothing is removed from a name before the user sees it.
|
|
152
|
+
const batch = tokenizeBatch(lines)
|
|
153
|
+
delimiter.value = batch.delimiter
|
|
154
|
+
experiment.value = batch.experiment
|
|
155
|
+
const tokens = batch.rows
|
|
156
|
+
// Classify by sample type, THEN split each experimental class by token
|
|
157
|
+
// length so every member of a class shares the same field count. This
|
|
158
|
+
// keeps the per-class token matrix rectangular, so column index N means
|
|
159
|
+
// the same field for every sample.
|
|
172
160
|
const detected = splitByTokenLength(
|
|
173
|
-
detectClass(
|
|
174
|
-
|
|
161
|
+
detectClass(tokens, { sampleTypeHints: sampleTypeHints.value, sharedPrefix: batch.sharedPrefix }),
|
|
162
|
+
tokens,
|
|
175
163
|
)
|
|
176
|
-
classes.value = detected
|
|
177
164
|
const newSchemas: Record<string, ClassSchema> = {}
|
|
178
165
|
for (const cls of detected) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
)
|
|
187
|
-
|
|
166
|
+
let memberTokens = cls.members.map(i => tokens[i])
|
|
167
|
+
let tailPositions: number[] = []
|
|
168
|
+
const experimental = isExperimentalKind(cls.kind)
|
|
169
|
+
if (experimental) {
|
|
170
|
+
// Tokens that hold two fields (`2-1`, `C1`) become two columns.
|
|
171
|
+
const split = splitColumns(memberTokens)
|
|
172
|
+
cls.members.forEach((member, row) => { tokens[member] = split.rows[row] })
|
|
173
|
+
cls.classTagPositions = split.origin.flatMap((source, index) =>
|
|
174
|
+
cls.classTagPositions.includes(source) ? [index] : [],
|
|
175
|
+
)
|
|
176
|
+
memberTokens = split.rows
|
|
177
|
+
tailPositions = split.tail
|
|
188
178
|
}
|
|
179
|
+
const schema = buildClassSchema(
|
|
180
|
+
memberTokens,
|
|
181
|
+
cls.kind,
|
|
182
|
+
cls.subKind,
|
|
183
|
+
cls.classTagPositions,
|
|
184
|
+
tailPositions,
|
|
185
|
+
)
|
|
186
|
+
// A QC-kind class is one group named by the class label, also when the
|
|
187
|
+
// user routes it into the groups.
|
|
188
|
+
if (!experimental) schema.groupBy = []
|
|
189
|
+
newSchemas[classKey(cls)] = { ...schema, tokenLength: cls.tokenLength }
|
|
189
190
|
}
|
|
191
|
+
tokenized.value = tokens
|
|
192
|
+
classes.value = detected
|
|
190
193
|
schemas.value = newSchemas
|
|
191
194
|
if (detected.length > 0) {
|
|
192
195
|
activeClassKey.value = classKey(detected[0])
|
|
@@ -206,41 +209,20 @@ export function useAutoGroup() {
|
|
|
206
209
|
return findMerges(schema, memberTokens)
|
|
207
210
|
})
|
|
208
211
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const composed = composeGroups({
|
|
212
|
+
function composeWith(withSchemas: Record<string, ClassSchema>): AutoGroupResult {
|
|
213
|
+
return composeGroups({
|
|
212
214
|
tokenizedSamples: tokenized.value,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// pre-grouping hasn't run (e.g. the CSV path or empty input).
|
|
216
|
-
sampleNames: pre ? pre.baseNames : samples.value,
|
|
217
|
-
schemas: schemas.value,
|
|
215
|
+
sampleNames: samples.value,
|
|
216
|
+
schemas: withSchemas,
|
|
218
217
|
classes: classes.value,
|
|
219
218
|
prefixClassLabels: !(csvData.value && (inputMode.value === 'csv' || inputMode.value === 'experiment')),
|
|
220
219
|
missingGroupName: csvData.value && (inputMode.value === 'csv' || inputMode.value === 'experiment')
|
|
221
220
|
? STRUCTURED_MISSING_GROUP
|
|
222
221
|
: undefined,
|
|
223
222
|
})
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// bases, and so `metadata` reflects each input row exactly once.
|
|
228
|
-
return {
|
|
229
|
-
...composed,
|
|
230
|
-
groups: expandGroupsWithReplicates(composed.groups, pre),
|
|
231
|
-
experimentalGroups: composed.experimentalGroups
|
|
232
|
-
? expandGroupsWithReplicates(composed.experimentalGroups, pre)
|
|
233
|
-
: undefined,
|
|
234
|
-
qcGroups: composed.qcGroups
|
|
235
|
-
? expandGroupsWithReplicates(composed.qcGroups, pre)
|
|
236
|
-
: undefined,
|
|
237
|
-
// The tree is built over base names too; expand its leaves to the original
|
|
238
|
-
// samples so it agrees with the flat experimental-groups panel.
|
|
239
|
-
groupTree: composed.groupTree
|
|
240
|
-
? expandTreeWithReplicates(composed.groupTree, pre)
|
|
241
|
-
: composed.groupTree,
|
|
242
|
-
}
|
|
243
|
-
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const result = computed<AutoGroupResult>(() => composeWith(schemas.value))
|
|
244
226
|
|
|
245
227
|
const fingerprint = computed<SchemaFingerprint>(() =>
|
|
246
228
|
serializeFingerprint(result.value.schemas ?? []),
|
|
@@ -316,9 +298,11 @@ export function useAutoGroup() {
|
|
|
316
298
|
// the same field at a different position.
|
|
317
299
|
function schemasExposing(fieldKey: string): { key: string; index: number }[] {
|
|
318
300
|
return Object.entries(schemas.value).flatMap(([key, schema]) => {
|
|
301
|
+
// An overlay class is one group; its columns are not grouping fields.
|
|
302
|
+
if (!isExperimentalKind(schema.classKind)) return []
|
|
319
303
|
const col = schema.columns.find(c =>
|
|
320
304
|
(c.displayName ?? c.name) === fieldKey
|
|
321
|
-
&& (
|
|
305
|
+
&& (GROUPING_FIELD_ROLES.has(c.role) || schema.groupBy.includes(c.index)),
|
|
322
306
|
)
|
|
323
307
|
return col ? [{ key, index: col.index }] : []
|
|
324
308
|
})
|
|
@@ -345,6 +329,34 @@ export function useAutoGroup() {
|
|
|
345
329
|
}
|
|
346
330
|
}
|
|
347
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Rename a grouping field in every class exposing it. A blank name, or a
|
|
334
|
+
* name that a column of any experimental class has, changes nothing: the
|
|
335
|
+
* name is the field's identity across classes, and the cards merge by it.
|
|
336
|
+
*/
|
|
337
|
+
function renameGroupByField(fieldKey: string, name: string) {
|
|
338
|
+
const next = name.trim()
|
|
339
|
+
const targets = schemasExposing(fieldKey)
|
|
340
|
+
if (!next || next === fieldKey || targets.length === 0) return
|
|
341
|
+
const taken = Object.values(schemas.value).some(schema =>
|
|
342
|
+
isExperimentalKind(schema.classKind) && schema.columns.some(c => (c.displayName ?? c.name) === next),
|
|
343
|
+
)
|
|
344
|
+
if (taken) return
|
|
345
|
+
for (const { key, index } of targets) setColumnName(key, index, next)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Number of experimental groups that remain when the field is switched off in every class. */
|
|
349
|
+
function groupCountWithout(fieldKey: string): number {
|
|
350
|
+
const targets = new Map(schemasExposing(fieldKey).map(({ key, index }) => [key, index]))
|
|
351
|
+
const without = Object.fromEntries(
|
|
352
|
+
Object.entries(schemas.value).map(([key, schema]) => [
|
|
353
|
+
key,
|
|
354
|
+
targets.has(key) ? { ...schema, groupBy: schema.groupBy.filter(i => i !== targets.get(key)) } : schema,
|
|
355
|
+
]),
|
|
356
|
+
)
|
|
357
|
+
return composeWith(without).experimentalGroups?.length ?? 0
|
|
358
|
+
}
|
|
359
|
+
|
|
348
360
|
/**
|
|
349
361
|
* Move a grouping field in the active schema, then project that visible field
|
|
350
362
|
* order across every class. Class-only fields keep their relative order after
|
|
@@ -575,6 +587,7 @@ export function useAutoGroup() {
|
|
|
575
587
|
schemas.value = {}
|
|
576
588
|
activeClassKey.value = ''
|
|
577
589
|
tokenized.value = []
|
|
590
|
+
experiment.value = {}
|
|
578
591
|
outliers.value = []
|
|
579
592
|
sampleTypeHints.value = []
|
|
580
593
|
}
|
|
@@ -583,7 +596,7 @@ export function useAutoGroup() {
|
|
|
583
596
|
// State
|
|
584
597
|
inputMode, rawText, csvData, sampleTypeHints,
|
|
585
598
|
classes, schemas, activeClassKey,
|
|
586
|
-
delimiter, dominantFieldCount, minFieldCount,
|
|
599
|
+
delimiter, experiment, dominantFieldCount, minFieldCount,
|
|
587
600
|
// Computed
|
|
588
601
|
samples, tokenized, activeSchema, suggestions,
|
|
589
602
|
groups, qcGroups, metadata, excludedSamples, result, fingerprint,
|
|
@@ -592,7 +605,7 @@ export function useAutoGroup() {
|
|
|
592
605
|
canDownloadTemplate,
|
|
593
606
|
// Mutators
|
|
594
607
|
setClassDisposition, setClassDispositions, toggleGroupBy, moveGroupBy,
|
|
595
|
-
toggleGroupByField, moveGroupByField,
|
|
608
|
+
toggleGroupByField, moveGroupByField, renameGroupByField, groupCountWithout,
|
|
596
609
|
setRole, setColumnName, setValueOps, setBinning,
|
|
597
610
|
mergeColumns, loadFingerprint,
|
|
598
611
|
// Actions
|
package/src/permissions.ts
CHANGED
|
@@ -95,9 +95,10 @@ export function canAccessPlugin(
|
|
|
95
95
|
user: PermissionUser | null | undefined,
|
|
96
96
|
pluginName: string | null | undefined,
|
|
97
97
|
): boolean {
|
|
98
|
-
if (!pluginName) return true
|
|
98
|
+
if (!pluginName || isAdminUser(user)) return true
|
|
99
99
|
const access = getRoleInfo(user)?.plugin_access
|
|
100
|
-
|
|
100
|
+
// As on the platform: a value that is not null, 'all' or a list allows no plugin.
|
|
101
|
+
if (access == null || access === 'all') return true
|
|
101
102
|
return Array.isArray(access) && access.includes(pluginName)
|
|
102
103
|
}
|
|
103
104
|
|
|
@@ -160,22 +160,6 @@
|
|
|
160
160
|
}
|
|
161
161
|
.sg-field__toggle { grid-row: 1 / 3; grid-column: 2; align-self: center; }
|
|
162
162
|
|
|
163
|
-
/* toggle switch */
|
|
164
|
-
.sg-switch {
|
|
165
|
-
position: relative; width: 1.65rem; height: 0.95rem;
|
|
166
|
-
border-radius: 9999px; background: var(--bg-tertiary);
|
|
167
|
-
border: none; cursor: pointer; flex-shrink: 0;
|
|
168
|
-
transition: background 0.15s ease;
|
|
169
|
-
}
|
|
170
|
-
.sg-switch.on { background: var(--color-primary); }
|
|
171
|
-
.sg-switch::after {
|
|
172
|
-
content: ''; position: absolute; top: 0.125rem; left: 0.125rem;
|
|
173
|
-
width: 0.7rem; height: 0.7rem; border-radius: 9999px;
|
|
174
|
-
background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / 0.25);
|
|
175
|
-
transition: transform 0.15s ease;
|
|
176
|
-
}
|
|
177
|
-
.sg-switch.on::after { transform: translateX(0.7rem); }
|
|
178
|
-
|
|
179
163
|
/* ==========================================================================
|
|
180
164
|
Shared — live group preview (right rail)
|
|
181
165
|
========================================================================== */
|
|
@@ -229,18 +213,8 @@
|
|
|
229
213
|
font-size: 0.6875rem; font-weight: 600;
|
|
230
214
|
color: var(--text-secondary);
|
|
231
215
|
}
|
|
232
|
-
.
|
|
233
|
-
|
|
234
|
-
font: inherit; font-size: 0.75rem; font-weight: 500;
|
|
235
|
-
color: var(--text-secondary);
|
|
236
|
-
padding: 0.1875rem 1.5rem 0.1875rem 0.5rem;
|
|
237
|
-
background: var(--bg-secondary);
|
|
238
|
-
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%2394a3b8' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
|
239
|
-
background-position: right 0.25rem center; background-repeat: no-repeat;
|
|
240
|
-
background-size: 1.1em 1.1em;
|
|
241
|
-
border: 1px solid var(--border-color); border-radius: var(--radius-sm);
|
|
242
|
-
appearance: none; cursor: pointer;
|
|
243
|
-
}
|
|
216
|
+
.fr-qc__route { margin-left: auto; }
|
|
217
|
+
.fr-qc__chips { display: flex; flex-wrap: wrap; gap: 0.25rem; }
|
|
244
218
|
.sg-qc__chip {
|
|
245
219
|
display: inline-flex; align-items: center; gap: 0.375rem;
|
|
246
220
|
padding: 0.25rem 0.5rem; border-radius: var(--radius);
|
|
@@ -287,47 +261,85 @@
|
|
|
287
261
|
/* ==========================================================================
|
|
288
262
|
Field Recipe refinements (layered on the shared chrome above)
|
|
289
263
|
========================================================================== */
|
|
264
|
+
/* Section label above the anatomy and each field list. */
|
|
265
|
+
.fr-label { font-size: 0.75rem; font-weight: 600; color: var(--text-secondary); }
|
|
266
|
+
|
|
267
|
+
/* One-line field row: [grip] [layer] name [rename] values [hint] [toggle]. */
|
|
290
268
|
.fr .sg-field {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
row-gap: 0.25rem;
|
|
294
|
-
padding: 0.5rem 0.75rem;
|
|
269
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
270
|
+
padding: 0.375rem 0.625rem;
|
|
295
271
|
}
|
|
296
|
-
.fr-
|
|
297
|
-
.fr .sg-field__role { margin-left: 0; }
|
|
298
|
-
.fr .sg-field__vals {
|
|
299
|
-
.fr-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
.
|
|
303
|
-
|
|
304
|
-
|
|
272
|
+
.fr .sg-field__name { white-space: nowrap; }
|
|
273
|
+
.fr .sg-field__role { margin-left: 0; white-space: nowrap; }
|
|
274
|
+
.fr .sg-field__vals { flex: 1; min-width: 0; }
|
|
275
|
+
.fr-row__grip,
|
|
276
|
+
.fr-row__rename {
|
|
277
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
278
|
+
width: 1.25rem; height: 1.25rem; padding: 0; flex-shrink: 0;
|
|
279
|
+
border: none; border-radius: var(--radius-sm); background: none;
|
|
280
|
+
color: var(--text-muted); cursor: pointer;
|
|
281
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
305
282
|
}
|
|
306
|
-
.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
283
|
+
.fr-row__grip { cursor: grab; }
|
|
284
|
+
.fr-row__grip:hover,
|
|
285
|
+
.fr-row__rename:hover { color: var(--text-primary); background: var(--bg-hover); }
|
|
286
|
+
.fr-row__grip:focus-visible,
|
|
287
|
+
.fr-row__rename:focus-visible { outline: none; box-shadow: var(--focus-ring-soft); }
|
|
288
|
+
/* Which hierarchy layer this field forms (1 = the sample selector's major group). */
|
|
289
|
+
.fr-row__layer {
|
|
290
|
+
display: inline-grid; place-items: center; flex-shrink: 0;
|
|
291
|
+
width: 1.25rem; height: 1.25rem; border-radius: 9999px;
|
|
292
|
+
border: 1px solid var(--color-primary);
|
|
293
|
+
background: var(--bg-secondary); color: var(--_ink-primary);
|
|
294
|
+
font-size: 0.6875rem; font-weight: 700; font-variant-numeric: tabular-nums;
|
|
295
|
+
}
|
|
296
|
+
.fr-row__name-input {
|
|
297
|
+
width: 9rem; padding: 0.0625rem 0.375rem;
|
|
298
|
+
font: inherit; font-size: 0.875rem; font-weight: 600; color: var(--text-primary);
|
|
299
|
+
border: 1px solid var(--color-primary); border-radius: var(--radius-sm);
|
|
300
|
+
background: var(--bg-secondary);
|
|
312
301
|
}
|
|
313
|
-
.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
cursor: pointer; transition: background 0.15s ease, color 0.15s ease;
|
|
302
|
+
.fr-row__name-input:focus-visible { outline: none; box-shadow: var(--focus-ring-soft); }
|
|
303
|
+
/* Groups that remain when this field is switched off. */
|
|
304
|
+
.fr-row__hint {
|
|
305
|
+
font-size: 0.75rem; font-weight: 500; color: var(--text-secondary-strong);
|
|
306
|
+
white-space: nowrap; font-variant-numeric: tabular-nums;
|
|
319
307
|
}
|
|
320
|
-
|
|
321
|
-
.
|
|
308
|
+
/* The message is dark text (AA on the tinted row); the input border carries the error colour. */
|
|
309
|
+
.fr-row__error { font-size: 0.75rem; font-weight: 600; color: var(--text-primary); white-space: nowrap; }
|
|
310
|
+
.fr-row__name-input[aria-invalid='true'] { border-color: var(--_ink-error); box-shadow: var(--focus-ring-error); }
|
|
311
|
+
.fr-row--dragging { opacity: 0.5; }
|
|
312
|
+
.fr-row--over { box-shadow: inset 0 2px 0 var(--color-primary); }
|
|
322
313
|
|
|
323
|
-
/* Replicate-on: the field
|
|
314
|
+
/* Replicate-on: the field row itself warns instead of silently shattering. */
|
|
324
315
|
.fr-field--warn.sg-field--on {
|
|
325
316
|
border-color: var(--mint-warning-border);
|
|
326
317
|
background: var(--mint-warning-bg);
|
|
327
318
|
}
|
|
328
|
-
.fr-field--warn.sg-field--on .fr-
|
|
329
|
-
|
|
330
|
-
|
|
319
|
+
.fr-field--warn.sg-field--on .fr-row__layer { border-color: var(--mint-warning); color: var(--text-primary); }
|
|
320
|
+
|
|
321
|
+
/* Name anatomy: the tokens of one example sample and the role of each. */
|
|
322
|
+
.fr-anatomy { display: flex; flex-direction: column; gap: 0.375rem; }
|
|
323
|
+
.fr-anatomy__tokens { display: flex; flex-wrap: wrap; gap: 0.375rem 0.5rem; }
|
|
324
|
+
.fr-token { display: inline-flex; flex-direction: column; align-items: center; gap: 0.125rem; }
|
|
325
|
+
.fr-token__text {
|
|
326
|
+
font-family: var(--font-mono); font-size: 0.75rem; font-weight: 500;
|
|
327
|
+
padding: 0.125rem 0.375rem; border-radius: var(--radius-sm);
|
|
328
|
+
border: 1px solid var(--border-color); background: var(--bg-tertiary);
|
|
329
|
+
color: var(--text-secondary-strong);
|
|
330
|
+
}
|
|
331
|
+
.fr-token__caption { font-size: 0.6875rem; color: var(--text-secondary-strong); white-space: nowrap; }
|
|
332
|
+
.fr-token--on .fr-token__text {
|
|
333
|
+
border-color: var(--color-primary); background: var(--color-primary-soft);
|
|
334
|
+
color: var(--text-primary);
|
|
335
|
+
}
|
|
336
|
+
.fr-token--on .fr-token__caption { color: var(--_ink-primary); font-weight: 700; }
|
|
337
|
+
.fr-token--off .fr-token__text { border-style: dashed; background: transparent; }
|
|
338
|
+
|
|
339
|
+
/* No samples: say what to do. */
|
|
340
|
+
.fr-blank {
|
|
341
|
+
display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem;
|
|
342
|
+
padding: 1rem 0;
|
|
331
343
|
}
|
|
332
344
|
|
|
333
345
|
/* Neutral (no-selection) summary variant. */
|
|
@@ -652,29 +664,6 @@ html.dark .sg-summary code { background: rgba(255, 255, 255, 0.1); }
|
|
|
652
664
|
background: var(--bg-tertiary); color: var(--text-primary); border-color: var(--text-muted);
|
|
653
665
|
}
|
|
654
666
|
.fr-upload:focus-visible { outline: none; box-shadow: var(--focus-ring-soft); }
|
|
655
|
-
.fr-csv { position: relative; }
|
|
656
|
-
.fr-csv-menu {
|
|
657
|
-
position: absolute; top: calc(100% + 0.25rem); right: 0; z-index: 10;
|
|
658
|
-
min-width: 11rem; padding: 0.25rem;
|
|
659
|
-
display: flex; flex-direction: column; gap: 0.0625rem;
|
|
660
|
-
background: var(--bg-secondary);
|
|
661
|
-
border: 1px solid var(--border-color); border-radius: var(--radius-md);
|
|
662
|
-
box-shadow: var(--shadow-lg);
|
|
663
|
-
}
|
|
664
|
-
.fr-csv-item {
|
|
665
|
-
display: flex; align-items: center; gap: 0.5rem;
|
|
666
|
-
font: inherit; font-size: 0.8125rem; font-weight: 500; text-align: left;
|
|
667
|
-
color: var(--text-primary); cursor: pointer;
|
|
668
|
-
padding: 0.4375rem 0.5rem; border: none; border-radius: var(--radius);
|
|
669
|
-
background: none; transition: background 0.12s ease, color 0.12s ease;
|
|
670
|
-
}
|
|
671
|
-
.fr-csv-item:hover { background: var(--bg-hover); }
|
|
672
|
-
.fr-csv-item:disabled {
|
|
673
|
-
/* disabled cue: muted colour + struck label + not-allowed, not opacity alone */
|
|
674
|
-
color: var(--text-muted); cursor: not-allowed; opacity: 0.7;
|
|
675
|
-
text-decoration: line-through; text-decoration-color: var(--text-muted);
|
|
676
|
-
}
|
|
677
|
-
.fr-csv-item:disabled:hover { background: none; }
|
|
678
667
|
/* anchor the drop overlay to the main panel */
|
|
679
668
|
.sg-body__main { position: relative; }
|
|
680
669
|
.fr-dropzone {
|