@morscherlab/mld-sdk 0.7.5 → 0.7.7
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/README.md +1 -1
- package/dist/__tests__/composables/useAutoGroup.test.d.ts +1 -0
- package/dist/components/AutoGroupModal.vue.js +522 -0
- package/dist/components/AutoGroupModal.vue.js.map +1 -0
- package/dist/components/AutoGroupModal.vue3.js +6 -0
- package/dist/components/AutoGroupModal.vue3.js.map +1 -0
- package/dist/components/FormActions.vue.d.ts +1 -1
- package/dist/components/GroupingModal.vue.js.map +1 -1
- package/dist/components/NumberInput.vue.js +53 -50
- package/dist/components/NumberInput.vue.js.map +1 -1
- package/dist/components/SampleSelector.vue.d.ts +5 -9
- package/dist/components/SampleSelector.vue.js +127 -255
- package/dist/components/SampleSelector.vue.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +53 -50
- package/dist/components/index.js.map +1 -1
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +3 -0
- package/dist/composables/index.js.map +1 -1
- package/dist/composables/useAutoGroup.d.ts +81 -0
- package/dist/composables/useAutoGroup.js +417 -0
- package/dist/composables/useAutoGroup.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +48 -42
- package/dist/index.js.map +1 -1
- package/dist/styles.css +4722 -3830
- package/dist/types/auto-group.d.ts +31 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/__tests__/composables/useAutoGroup.test.ts +508 -0
- package/src/components/AutoGroupModal.story.vue +223 -0
- package/src/components/AutoGroupModal.vue +441 -0
- package/src/components/GroupingModal.vue +1 -0
- package/src/components/NumberInput.vue +33 -31
- package/src/components/SampleSelector.story.vue +8 -31
- package/src/components/SampleSelector.vue +22 -137
- package/src/components/index.ts +1 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/useAutoGroup.ts +524 -0
- package/src/index.ts +12 -0
- package/src/styles/components/app-sidebar.css +1 -1
- package/src/styles/components/auto-group-modal.css +502 -0
- package/src/styles/components/modal.css +6 -0
- package/src/styles/components/number-input.css +7 -5
- package/src/styles/components/step-wizard.css +5 -0
- package/src/styles/index.css +1 -0
- package/src/types/auto-group.ts +37 -0
- package/src/types/index.ts +11 -0
|
@@ -74,22 +74,6 @@ function increment() {
|
|
|
74
74
|
disabled ? 'mld-number-input--disabled' : '',
|
|
75
75
|
]"
|
|
76
76
|
>
|
|
77
|
-
<button
|
|
78
|
-
type="button"
|
|
79
|
-
aria-label="Decrease value"
|
|
80
|
-
:disabled="disabled || !canDecrement"
|
|
81
|
-
:class="[
|
|
82
|
-
'mld-number-input__button',
|
|
83
|
-
'mld-number-input__button--decrement',
|
|
84
|
-
`mld-number-input__button--${size}`,
|
|
85
|
-
]"
|
|
86
|
-
@click="decrement"
|
|
87
|
-
>
|
|
88
|
-
<svg class="mld-number-input__button-icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
|
89
|
-
<path d="M5 12h14" />
|
|
90
|
-
</svg>
|
|
91
|
-
</button>
|
|
92
|
-
|
|
93
77
|
<input
|
|
94
78
|
type="number"
|
|
95
79
|
:value="modelValue"
|
|
@@ -106,21 +90,39 @@ function increment() {
|
|
|
106
90
|
@input="handleInput"
|
|
107
91
|
/>
|
|
108
92
|
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
93
|
+
<div class="mld-number-input__buttons">
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
aria-label="Decrease value"
|
|
97
|
+
:disabled="disabled || !canDecrement"
|
|
98
|
+
:class="[
|
|
99
|
+
'mld-number-input__button',
|
|
100
|
+
'mld-number-input__button--decrement',
|
|
101
|
+
`mld-number-input__button--${size}`,
|
|
102
|
+
]"
|
|
103
|
+
@click="decrement"
|
|
104
|
+
>
|
|
105
|
+
<svg class="mld-number-input__button-icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
|
106
|
+
<path d="M5 12h14" />
|
|
107
|
+
</svg>
|
|
108
|
+
</button>
|
|
109
|
+
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
aria-label="Increase value"
|
|
113
|
+
:disabled="disabled || !canIncrement"
|
|
114
|
+
:class="[
|
|
115
|
+
'mld-number-input__button',
|
|
116
|
+
'mld-number-input__button--increment',
|
|
117
|
+
`mld-number-input__button--${size}`,
|
|
118
|
+
]"
|
|
119
|
+
@click="increment"
|
|
120
|
+
>
|
|
121
|
+
<svg class="mld-number-input__button-icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
|
122
|
+
<path d="M5 12h14" /><path d="M12 5v14" />
|
|
123
|
+
</svg>
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
124
126
|
</div>
|
|
125
127
|
</template>
|
|
126
128
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import SampleSelector from './SampleSelector.vue'
|
|
3
3
|
import type { SampleGroup } from '../types'
|
|
4
|
+
import type { AutoGroupResult } from '../types/auto-group'
|
|
4
5
|
|
|
5
6
|
const mockSamples = [
|
|
6
7
|
'Control_Rep1', 'Control_Rep2', 'Control_Rep3',
|
|
@@ -16,41 +17,19 @@ const mockGroups: SampleGroup[] = [
|
|
|
16
17
|
{ name: 'Vehicle', color: '#F59E0B', samples: ['Vehicle_Rep1', 'Vehicle_Rep2', 'Vehicle_Rep3'] },
|
|
17
18
|
]
|
|
18
19
|
|
|
19
|
-
const DEFAULT_COLORS = [
|
|
20
|
-
'#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6',
|
|
21
|
-
'#EC4899', '#06B6D4', '#84CC16', '#F97316', '#6366F1',
|
|
22
|
-
]
|
|
23
|
-
|
|
24
20
|
function initState() {
|
|
25
21
|
return {
|
|
26
22
|
samples: mockSamples,
|
|
27
23
|
selected: [] as string[],
|
|
28
24
|
groups: [] as SampleGroup[],
|
|
29
25
|
enableGrouping: true,
|
|
30
|
-
|
|
31
|
-
enableMetadataGroup: true,
|
|
26
|
+
enableSmartGroup: true,
|
|
32
27
|
}
|
|
33
28
|
}
|
|
34
29
|
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
for (const sample of state.samples) {
|
|
39
|
-
const parts = sample.split(/[_\-.]/)
|
|
40
|
-
const prefix = parts.slice(0, level).join('_')
|
|
41
|
-
if (!prefixGroups[prefix]) {
|
|
42
|
-
prefixGroups[prefix] = []
|
|
43
|
-
}
|
|
44
|
-
prefixGroups[prefix].push(sample)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
state.groups = Object.entries(prefixGroups)
|
|
48
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
49
|
-
.map(([name, samples], i) => ({
|
|
50
|
-
name,
|
|
51
|
-
color: DEFAULT_COLORS[i % DEFAULT_COLORS.length],
|
|
52
|
-
samples,
|
|
53
|
-
}))
|
|
30
|
+
function handleSmartGroup(result: AutoGroupResult, state: { groups: SampleGroup[] }) {
|
|
31
|
+
state.groups = result.groups
|
|
32
|
+
console.log('Smart group result:', result)
|
|
54
33
|
}
|
|
55
34
|
</script>
|
|
56
35
|
|
|
@@ -64,16 +43,14 @@ function handleAutoGroup(level: number, state: { samples: string[]; groups: Samp
|
|
|
64
43
|
v-model:groups="state.groups"
|
|
65
44
|
:samples="state.samples"
|
|
66
45
|
:enable-grouping="state.enableGrouping"
|
|
67
|
-
:enable-
|
|
68
|
-
|
|
69
|
-
@auto-group="(level: number) => handleAutoGroup(level, state)"
|
|
46
|
+
:enable-smart-group="state.enableSmartGroup"
|
|
47
|
+
@smart-group="(r: AutoGroupResult) => handleSmartGroup(r, state)"
|
|
70
48
|
/>
|
|
71
49
|
</div>
|
|
72
50
|
</template>
|
|
73
51
|
<template #controls="{ state }">
|
|
74
52
|
<HstCheckbox v-model="state.enableGrouping" title="Enable Grouping" />
|
|
75
|
-
<HstCheckbox v-model="state.
|
|
76
|
-
<HstCheckbox v-model="state.enableMetadataGroup" title="Enable Metadata Group" />
|
|
53
|
+
<HstCheckbox v-model="state.enableSmartGroup" title="Enable Smart Group" />
|
|
77
54
|
</template>
|
|
78
55
|
</Variant>
|
|
79
56
|
|
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, computed
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
3
|
import BaseButton from './BaseButton.vue'
|
|
4
4
|
import BaseInput from './BaseInput.vue'
|
|
5
|
-
import
|
|
5
|
+
import AutoGroupModal from './AutoGroupModal.vue'
|
|
6
6
|
import type { SampleGroup } from '../types'
|
|
7
|
+
import type { AutoGroupResult } from '../types/auto-group'
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
9
10
|
samples: string[]
|
|
10
11
|
modelValue: string[]
|
|
11
12
|
groups?: SampleGroup[]
|
|
12
13
|
enableGrouping?: boolean
|
|
13
|
-
|
|
14
|
-
enableMetadataGroup?: boolean
|
|
14
|
+
enableSmartGroup?: boolean
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const props = withDefaults(defineProps<Props>(), {
|
|
18
18
|
groups: () => [],
|
|
19
19
|
enableGrouping: true,
|
|
20
|
-
|
|
21
|
-
enableMetadataGroup: true,
|
|
20
|
+
enableSmartGroup: true,
|
|
22
21
|
})
|
|
23
22
|
|
|
24
23
|
const emit = defineEmits<{
|
|
25
24
|
'update:modelValue': [samples: string[]]
|
|
26
25
|
'update:groups': [groups: SampleGroup[]]
|
|
27
|
-
|
|
28
|
-
metadataGroup: [mapping: Record<string, string[]>]
|
|
26
|
+
smartGroup: [result: AutoGroupResult]
|
|
29
27
|
}>()
|
|
30
28
|
|
|
31
29
|
const DEFAULT_COLORS = [
|
|
@@ -34,9 +32,7 @@ const DEFAULT_COLORS = [
|
|
|
34
32
|
]
|
|
35
33
|
|
|
36
34
|
// UI State
|
|
37
|
-
const
|
|
38
|
-
const popoverRef = ref<HTMLDivElement | null>(null)
|
|
39
|
-
const showMetadataModal = ref(false)
|
|
35
|
+
const showSmartGroupModal = ref(false)
|
|
40
36
|
const newGroupName = ref('')
|
|
41
37
|
const editingGroupColor = ref<string | null>(null)
|
|
42
38
|
const colorPickerInput = ref<HTMLInputElement | null>(null)
|
|
@@ -127,48 +123,6 @@ const filteredSamples = computed(() => {
|
|
|
127
123
|
return props.samples.filter(s => s.toLowerCase().includes(query))
|
|
128
124
|
})
|
|
129
125
|
|
|
130
|
-
// Compute available grouping levels with preview
|
|
131
|
-
const groupingLevels = computed(() => {
|
|
132
|
-
if (props.samples.length === 0) return []
|
|
133
|
-
|
|
134
|
-
const maxLevel = detectGroupingLevels()
|
|
135
|
-
const levels: Array<{ level: number; groupCount: number; example: string; description: string }> = []
|
|
136
|
-
|
|
137
|
-
for (let level = 1; level <= maxLevel; level++) {
|
|
138
|
-
const prefixGroups: Record<string, string[]> = {}
|
|
139
|
-
|
|
140
|
-
for (const sample of props.samples) {
|
|
141
|
-
const parts = sample.split(/[_\-.]/)
|
|
142
|
-
const prefix = parts.slice(0, level).join('_')
|
|
143
|
-
if (prefix) {
|
|
144
|
-
if (!prefixGroups[prefix]) {
|
|
145
|
-
prefixGroups[prefix] = []
|
|
146
|
-
}
|
|
147
|
-
prefixGroups[prefix].push(sample)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const groupNames = Object.keys(prefixGroups).sort()
|
|
152
|
-
const example = groupNames[0] || ''
|
|
153
|
-
const descriptions = ['main groups', 'sub-groups']
|
|
154
|
-
const description = descriptions[level - 1] || `level ${level} groups`
|
|
155
|
-
|
|
156
|
-
levels.push({
|
|
157
|
-
level,
|
|
158
|
-
groupCount: groupNames.length,
|
|
159
|
-
example,
|
|
160
|
-
description
|
|
161
|
-
})
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return levels
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
function detectGroupingLevels(): number {
|
|
168
|
-
if (props.samples.length === 0) return 1
|
|
169
|
-
const maxParts = Math.max(...props.samples.map(s => s.split(/[_\-.]/).length))
|
|
170
|
-
return Math.min(maxParts, 3)
|
|
171
|
-
}
|
|
172
126
|
|
|
173
127
|
// Selection state
|
|
174
128
|
const isAllSelected = computed(() =>
|
|
@@ -264,14 +218,10 @@ function collapseAllGroups() {
|
|
|
264
218
|
expandedGroups.value = {}
|
|
265
219
|
}
|
|
266
220
|
|
|
267
|
-
//
|
|
268
|
-
function
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
function selectLevel(level: number) {
|
|
273
|
-
emit('autoGroup', level)
|
|
274
|
-
showLevelPopover.value = false
|
|
221
|
+
// Smart group
|
|
222
|
+
function handleSmartGroupApply(result: AutoGroupResult) {
|
|
223
|
+
emit('smartGroup', result)
|
|
224
|
+
emit('update:groups', result.groups)
|
|
275
225
|
}
|
|
276
226
|
|
|
277
227
|
// Group management
|
|
@@ -393,26 +343,6 @@ function addNewGroup() {
|
|
|
393
343
|
newGroupName.value = ''
|
|
394
344
|
}
|
|
395
345
|
|
|
396
|
-
// Metadata modal
|
|
397
|
-
function handleMetadataApply(mapping: Record<string, string[]>) {
|
|
398
|
-
emit('metadataGroup', mapping)
|
|
399
|
-
showMetadataModal.value = false
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Click outside handler
|
|
403
|
-
function handleClickOutside(event: MouseEvent) {
|
|
404
|
-
if (popoverRef.value && !popoverRef.value.contains(event.target as Node)) {
|
|
405
|
-
showLevelPopover.value = false
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
onMounted(() => {
|
|
410
|
-
document.addEventListener('click', handleClickOutside)
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
onUnmounted(() => {
|
|
414
|
-
document.removeEventListener('click', handleClickOutside)
|
|
415
|
-
})
|
|
416
346
|
</script>
|
|
417
347
|
|
|
418
348
|
<template>
|
|
@@ -430,36 +360,21 @@ onUnmounted(() => {
|
|
|
430
360
|
</label>
|
|
431
361
|
|
|
432
362
|
<!-- Action Buttons Row -->
|
|
433
|
-
<div v-if="enableGrouping" class="mld-sample-selector__actions"
|
|
363
|
+
<div v-if="enableGrouping" class="mld-sample-selector__actions">
|
|
434
364
|
<div class="mld-sample-selector__actions-row">
|
|
435
|
-
<!--
|
|
365
|
+
<!-- Smart Group Button -->
|
|
436
366
|
<BaseButton
|
|
437
|
-
v-if="
|
|
438
|
-
:variant="
|
|
367
|
+
v-if="enableSmartGroup"
|
|
368
|
+
:variant="groupingEnabled ? 'primary' : 'secondary'"
|
|
439
369
|
size="sm"
|
|
440
370
|
:disabled="samples.length === 0"
|
|
441
371
|
class="mld-sample-selector__action-btn"
|
|
442
|
-
@click
|
|
372
|
+
@click="showSmartGroupModal = true"
|
|
443
373
|
>
|
|
444
374
|
<svg class="mld-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
445
375
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
446
376
|
</svg>
|
|
447
|
-
<span>
|
|
448
|
-
</BaseButton>
|
|
449
|
-
|
|
450
|
-
<!-- Metadata Button -->
|
|
451
|
-
<BaseButton
|
|
452
|
-
v-if="enableMetadataGroup"
|
|
453
|
-
variant="secondary"
|
|
454
|
-
size="sm"
|
|
455
|
-
:disabled="samples.length === 0"
|
|
456
|
-
class="mld-sample-selector__action-btn"
|
|
457
|
-
@click="showMetadataModal = true"
|
|
458
|
-
>
|
|
459
|
-
<svg class="mld-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
460
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
461
|
-
</svg>
|
|
462
|
-
<span>Metadata</span>
|
|
377
|
+
<span>Smart Group</span>
|
|
463
378
|
</BaseButton>
|
|
464
379
|
|
|
465
380
|
<!-- Reset Button -->
|
|
@@ -476,35 +391,6 @@ onUnmounted(() => {
|
|
|
476
391
|
</svg>
|
|
477
392
|
</BaseButton>
|
|
478
393
|
</div>
|
|
479
|
-
|
|
480
|
-
<!-- Level Selection Popover -->
|
|
481
|
-
<Transition name="mld-popover">
|
|
482
|
-
<div v-if="showLevelPopover" class="mld-sample-selector__popover">
|
|
483
|
-
<div class="mld-sample-selector__popover-header">Auto-Group by Level</div>
|
|
484
|
-
<div class="mld-sample-selector__popover-body">
|
|
485
|
-
<button
|
|
486
|
-
v-for="levelInfo in groupingLevels"
|
|
487
|
-
:key="levelInfo.level"
|
|
488
|
-
type="button"
|
|
489
|
-
class="mld-sample-selector__level-btn"
|
|
490
|
-
@click="selectLevel(levelInfo.level)"
|
|
491
|
-
>
|
|
492
|
-
<div class="mld-sample-selector__level-info">
|
|
493
|
-
<div class="mld-sample-selector__level-title">
|
|
494
|
-
<span>Level {{ levelInfo.level }}</span>
|
|
495
|
-
<span class="mld-sample-selector__level-badge">{{ levelInfo.groupCount }} groups</span>
|
|
496
|
-
</div>
|
|
497
|
-
<div class="mld-sample-selector__level-desc">
|
|
498
|
-
{{ levelInfo.description }}
|
|
499
|
-
<span v-if="levelInfo.example" class="mld-sample-selector__level-example">
|
|
500
|
-
e.g. <code>{{ levelInfo.example }}</code>
|
|
501
|
-
</span>
|
|
502
|
-
</div>
|
|
503
|
-
</div>
|
|
504
|
-
</button>
|
|
505
|
-
</div>
|
|
506
|
-
</div>
|
|
507
|
-
</Transition>
|
|
508
394
|
</div>
|
|
509
395
|
|
|
510
396
|
<!-- Grouped View -->
|
|
@@ -820,7 +706,7 @@ onUnmounted(() => {
|
|
|
820
706
|
|
|
821
707
|
<!-- Empty state -->
|
|
822
708
|
<div v-if="internalGroups.length === 0" class="mld-sample-selector__empty">
|
|
823
|
-
Click
|
|
709
|
+
Click Smart Group to auto-group samples
|
|
824
710
|
</div>
|
|
825
711
|
</div>
|
|
826
712
|
|
|
@@ -940,12 +826,11 @@ onUnmounted(() => {
|
|
|
940
826
|
</div>
|
|
941
827
|
</div>
|
|
942
828
|
|
|
943
|
-
<!--
|
|
944
|
-
<
|
|
945
|
-
|
|
829
|
+
<!-- Smart Grouping Modal -->
|
|
830
|
+
<AutoGroupModal
|
|
831
|
+
v-model="showSmartGroupModal"
|
|
946
832
|
:samples="samples"
|
|
947
|
-
@
|
|
948
|
-
@apply="handleMetadataApply"
|
|
833
|
+
@apply="handleSmartGroupApply"
|
|
949
834
|
/>
|
|
950
835
|
</div>
|
|
951
836
|
</template>
|
package/src/components/index.ts
CHANGED
|
@@ -65,6 +65,7 @@ export { default as ExperimentTimeline } from './ExperimentTimeline.vue'
|
|
|
65
65
|
// Sample management components
|
|
66
66
|
export { default as SampleSelector } from './SampleSelector.vue'
|
|
67
67
|
export { default as GroupingModal } from './GroupingModal.vue'
|
|
68
|
+
export { default as AutoGroupModal } from './AutoGroupModal.vue'
|
|
68
69
|
export { default as GroupAssigner } from './GroupAssigner.vue'
|
|
69
70
|
|
|
70
71
|
// Lab/Experiment components
|
package/src/composables/index.ts
CHANGED
|
@@ -86,6 +86,7 @@ export {
|
|
|
86
86
|
} from './useTimeUtils'
|
|
87
87
|
export { useScheduleDrag } from './useScheduleDrag'
|
|
88
88
|
export { useFormBuilder, evaluateCondition } from './useFormBuilder'
|
|
89
|
+
export { useAutoGroup, DEFAULT_COLORS } from './useAutoGroup'
|
|
89
90
|
export { usePluginConfig, type UsePluginConfigReturn } from './usePluginConfig'
|
|
90
91
|
export {
|
|
91
92
|
getFieldRegistryEntry,
|