mgv-backoffice 1.36.0 → 1.36.2
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 +1794 -1719
- package/dist/components/BaseBadge.vue.d.ts +1 -11
- package/dist/components/BaseDropdown.vue.d.ts +1 -1
- package/dist/ui-lib.js +942 -925
- package/dist/ui-lib.umd.cjs +1 -1
- package/dist/utils/specForm.d.ts +1 -1
- package/package.json +54 -54
- package/src/components/BaseAlert.vue +104 -104
- package/src/components/BaseBadge.vue +12 -2
- package/src/components/BaseBarDistribution.test.ts +51 -51
- package/src/components/BaseBarDistribution.vue +73 -73
- package/src/components/BaseChipButton.vue +42 -42
- package/src/components/BaseCodeBlock.test.ts +70 -70
- package/src/components/BaseCodeBlock.vue +74 -74
- package/src/components/BaseCollapsibleSection.vue +113 -113
- package/src/components/BaseConfirmModal.vue +114 -114
- package/src/components/BaseCredentialsForm.test.ts +108 -108
- package/src/components/BaseCredentialsForm.vue +241 -241
- package/src/components/BaseDropdown.test.ts +87 -87
- package/src/components/BaseDropdown.vue +172 -172
- package/src/components/BaseEntityPickerModal.vue +330 -330
- package/src/components/BaseFileDropzone.vue +94 -94
- package/src/components/BaseFilterChip.test.ts +44 -44
- package/src/components/BaseFilterChip.vue +67 -67
- package/src/components/BaseModalShell.vue +209 -209
- package/src/components/BasePageHeader.vue +120 -120
- package/src/components/BasePillPickerModal.test.ts +95 -95
- package/src/components/BaseRemoveButton.vue +22 -22
- package/src/components/BaseSpecFields.test.ts +39 -0
- package/src/components/BaseSpecFields.vue +27 -3
- package/src/components/BaseStatBreakdown.test.ts +56 -56
- package/src/components/BaseStatBreakdown.vue +49 -49
- package/src/components/BaseStatusPill.vue +54 -54
- package/src/components/BaseTextInputModal.vue +140 -140
- package/src/components/BaseToolbarButton.test.ts +48 -48
- package/src/components/BaseToolbarButton.vue +94 -94
- package/src/composables/useFieldClasses.test.ts +55 -55
- package/src/composables/useFieldClasses.ts +56 -56
- package/src/composables/usePolling.test.ts +112 -112
- package/src/composables/usePolling.ts +102 -102
- package/src/composables/useQueryParamSync.test.ts +56 -56
- package/src/composables/useQueryParamSync.ts +42 -42
- package/src/composables/useThemeClasses.test.ts +34 -34
- package/src/composables/useThemeClasses.ts +137 -137
- package/src/index.ts +138 -138
- package/src/style.css +32 -32
- package/src/types/distribution.ts +7 -7
- package/src/types/pillPicker.ts +14 -14
- package/src/types/specField.ts +30 -30
- package/src/types/statBreakdown.ts +6 -6
- package/src/utils/format.test.ts +264 -264
- package/src/utils/format.ts +216 -216
- package/src/utils/httpColors.test.ts +100 -100
- package/src/utils/httpColors.ts +99 -99
- package/src/utils/kvRows.test.ts +47 -47
- package/src/utils/kvRows.ts +34 -34
- package/src/utils/specForm.test.ts +64 -53
- package/src/utils/specForm.ts +49 -48
- package/src/utils/validate.test.ts +82 -82
- package/src/utils/validate.ts +76 -76
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!--
|
|
3
|
-
Small tinted "chip" action button — the compact emerald "+ Add" pill
|
|
4
|
-
used above repeatable form rows (extracted from WireMate's CreateMock,
|
|
5
|
-
where the identical class string appeared seven times).
|
|
6
|
-
|
|
7
|
-
Label comes from the default slot so callers can mix text and icons.
|
|
8
|
-
-->
|
|
9
|
-
<button
|
|
10
|
-
type="button"
|
|
11
|
-
:disabled="disabled"
|
|
12
|
-
class="inline-flex items-center gap-1 font-medium rounded-md border transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
|
|
13
|
-
:class="[
|
|
14
|
-
size === 'xs' ? 'text-xs px-2 py-0.5' : 'text-xs px-2.5 py-1',
|
|
15
|
-
isDark
|
|
16
|
-
? 'text-emerald-300 border-emerald-700 bg-emerald-900/30 hover:bg-emerald-800/50'
|
|
17
|
-
: 'text-emerald-700 border-emerald-200 bg-emerald-50 hover:bg-emerald-100',
|
|
18
|
-
]"
|
|
19
|
-
@click="emit('click')"
|
|
20
|
-
>
|
|
21
|
-
<slot />
|
|
22
|
-
</button>
|
|
23
|
-
</template>
|
|
24
|
-
|
|
25
|
-
<script lang="ts" setup>
|
|
26
|
-
import { useTheme } from '../composables/useTheme'
|
|
27
|
-
|
|
28
|
-
interface Props {
|
|
29
|
-
/** 'sm' = px-2.5 py-1 (default); 'xs' = px-2 py-0.5 for tight corners. */
|
|
30
|
-
size?: 'xs' | 'sm'
|
|
31
|
-
disabled?: boolean
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
withDefaults(defineProps<Props>(), {
|
|
35
|
-
size: 'sm',
|
|
36
|
-
disabled: false,
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
const emit = defineEmits<{ click: [] }>()
|
|
40
|
-
|
|
41
|
-
const { isDark } = useTheme()
|
|
42
|
-
</script>
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Small tinted "chip" action button — the compact emerald "+ Add" pill
|
|
4
|
+
used above repeatable form rows (extracted from WireMate's CreateMock,
|
|
5
|
+
where the identical class string appeared seven times).
|
|
6
|
+
|
|
7
|
+
Label comes from the default slot so callers can mix text and icons.
|
|
8
|
+
-->
|
|
9
|
+
<button
|
|
10
|
+
type="button"
|
|
11
|
+
:disabled="disabled"
|
|
12
|
+
class="inline-flex items-center gap-1 font-medium rounded-md border transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
|
|
13
|
+
:class="[
|
|
14
|
+
size === 'xs' ? 'text-xs px-2 py-0.5' : 'text-xs px-2.5 py-1',
|
|
15
|
+
isDark
|
|
16
|
+
? 'text-emerald-300 border-emerald-700 bg-emerald-900/30 hover:bg-emerald-800/50'
|
|
17
|
+
: 'text-emerald-700 border-emerald-200 bg-emerald-50 hover:bg-emerald-100',
|
|
18
|
+
]"
|
|
19
|
+
@click="emit('click')"
|
|
20
|
+
>
|
|
21
|
+
<slot />
|
|
22
|
+
</button>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script lang="ts" setup>
|
|
26
|
+
import { useTheme } from '../composables/useTheme'
|
|
27
|
+
|
|
28
|
+
interface Props {
|
|
29
|
+
/** 'sm' = px-2.5 py-1 (default); 'xs' = px-2 py-0.5 for tight corners. */
|
|
30
|
+
size?: 'xs' | 'sm'
|
|
31
|
+
disabled?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
withDefaults(defineProps<Props>(), {
|
|
35
|
+
size: 'sm',
|
|
36
|
+
disabled: false,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const emit = defineEmits<{ click: [] }>()
|
|
40
|
+
|
|
41
|
+
const { isDark } = useTheme()
|
|
42
|
+
</script>
|
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
-
import { mount } from '@vue/test-utils'
|
|
3
|
-
import BaseCodeBlock from './BaseCodeBlock.vue'
|
|
4
|
-
import { useTheme } from '../composables/useTheme'
|
|
5
|
-
|
|
6
|
-
function ensureLight() {
|
|
7
|
-
const { isDark, toggleTheme } = useTheme()
|
|
8
|
-
if (isDark.value) toggleTheme()
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe('BaseCodeBlock', () => {
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
ensureLight()
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('renders the code verbatim inside a <pre>', () => {
|
|
17
|
-
const code = '{\n "a": 1\n}'
|
|
18
|
-
const wrapper = mount(BaseCodeBlock, { props: { code } })
|
|
19
|
-
const pre = wrapper.find('pre')
|
|
20
|
-
expect(pre.exists()).toBe(true)
|
|
21
|
-
expect(pre.text()).toBe(code)
|
|
22
|
-
expect(pre.classes()).toContain('font-mono')
|
|
23
|
-
expect(pre.classes()).toContain('overflow-auto')
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('defaults to the soft variant at size sm', () => {
|
|
27
|
-
const wrapper = mount(BaseCodeBlock, { props: { code: 'x' } })
|
|
28
|
-
const classes = wrapper.find('pre').classes()
|
|
29
|
-
expect(classes).toContain('bg-gray-100')
|
|
30
|
-
expect(classes).toContain('text-sm')
|
|
31
|
-
expect(classes).toContain('px-5')
|
|
32
|
-
expect(classes).not.toContain('border')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('renders the bordered variant with a border and card fill', () => {
|
|
36
|
-
const wrapper = mount(BaseCodeBlock, {
|
|
37
|
-
props: { code: 'x', variant: 'bordered' },
|
|
38
|
-
})
|
|
39
|
-
const classes = wrapper.find('pre').classes()
|
|
40
|
-
expect(classes).toContain('border')
|
|
41
|
-
expect(classes).toContain('bg-white')
|
|
42
|
-
expect(classes).toContain('border-gray-200')
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('renders dense padding at size xs', () => {
|
|
46
|
-
const wrapper = mount(BaseCodeBlock, { props: { code: 'x', size: 'xs' } })
|
|
47
|
-
const classes = wrapper.find('pre').classes()
|
|
48
|
-
expect(classes).toContain('text-xs')
|
|
49
|
-
expect(classes).toContain('p-3')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('applies maxHeightClass and merges fallthrough classes', () => {
|
|
53
|
-
const wrapper = mount(BaseCodeBlock, {
|
|
54
|
-
props: { code: 'x', maxHeightClass: 'max-h-96' },
|
|
55
|
-
attrs: { class: 'mx-4' },
|
|
56
|
-
})
|
|
57
|
-
const classes = wrapper.find('pre').classes()
|
|
58
|
-
expect(classes).toContain('max-h-96')
|
|
59
|
-
expect(classes).toContain('mx-4')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('switches palette when the theme flips', async () => {
|
|
63
|
-
const wrapper = mount(BaseCodeBlock, { props: { code: 'x' } })
|
|
64
|
-
const { toggleTheme } = useTheme()
|
|
65
|
-
toggleTheme()
|
|
66
|
-
await wrapper.vm.$nextTick()
|
|
67
|
-
expect(wrapper.find('pre').classes()).toContain('bg-gray-800/70')
|
|
68
|
-
toggleTheme()
|
|
69
|
-
})
|
|
70
|
-
})
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import BaseCodeBlock from './BaseCodeBlock.vue'
|
|
4
|
+
import { useTheme } from '../composables/useTheme'
|
|
5
|
+
|
|
6
|
+
function ensureLight() {
|
|
7
|
+
const { isDark, toggleTheme } = useTheme()
|
|
8
|
+
if (isDark.value) toggleTheme()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('BaseCodeBlock', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
ensureLight()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('renders the code verbatim inside a <pre>', () => {
|
|
17
|
+
const code = '{\n "a": 1\n}'
|
|
18
|
+
const wrapper = mount(BaseCodeBlock, { props: { code } })
|
|
19
|
+
const pre = wrapper.find('pre')
|
|
20
|
+
expect(pre.exists()).toBe(true)
|
|
21
|
+
expect(pre.text()).toBe(code)
|
|
22
|
+
expect(pre.classes()).toContain('font-mono')
|
|
23
|
+
expect(pre.classes()).toContain('overflow-auto')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('defaults to the soft variant at size sm', () => {
|
|
27
|
+
const wrapper = mount(BaseCodeBlock, { props: { code: 'x' } })
|
|
28
|
+
const classes = wrapper.find('pre').classes()
|
|
29
|
+
expect(classes).toContain('bg-gray-100')
|
|
30
|
+
expect(classes).toContain('text-sm')
|
|
31
|
+
expect(classes).toContain('px-5')
|
|
32
|
+
expect(classes).not.toContain('border')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('renders the bordered variant with a border and card fill', () => {
|
|
36
|
+
const wrapper = mount(BaseCodeBlock, {
|
|
37
|
+
props: { code: 'x', variant: 'bordered' },
|
|
38
|
+
})
|
|
39
|
+
const classes = wrapper.find('pre').classes()
|
|
40
|
+
expect(classes).toContain('border')
|
|
41
|
+
expect(classes).toContain('bg-white')
|
|
42
|
+
expect(classes).toContain('border-gray-200')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('renders dense padding at size xs', () => {
|
|
46
|
+
const wrapper = mount(BaseCodeBlock, { props: { code: 'x', size: 'xs' } })
|
|
47
|
+
const classes = wrapper.find('pre').classes()
|
|
48
|
+
expect(classes).toContain('text-xs')
|
|
49
|
+
expect(classes).toContain('p-3')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('applies maxHeightClass and merges fallthrough classes', () => {
|
|
53
|
+
const wrapper = mount(BaseCodeBlock, {
|
|
54
|
+
props: { code: 'x', maxHeightClass: 'max-h-96' },
|
|
55
|
+
attrs: { class: 'mx-4' },
|
|
56
|
+
})
|
|
57
|
+
const classes = wrapper.find('pre').classes()
|
|
58
|
+
expect(classes).toContain('max-h-96')
|
|
59
|
+
expect(classes).toContain('mx-4')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('switches palette when the theme flips', async () => {
|
|
63
|
+
const wrapper = mount(BaseCodeBlock, { props: { code: 'x' } })
|
|
64
|
+
const { toggleTheme } = useTheme()
|
|
65
|
+
toggleTheme()
|
|
66
|
+
await wrapper.vm.$nextTick()
|
|
67
|
+
expect(wrapper.find('pre').classes()).toContain('bg-gray-800/70')
|
|
68
|
+
toggleTheme()
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!--
|
|
3
|
-
Themed monospace <pre> for JSON payloads, request dumps and code
|
|
4
|
-
snippets — extracted from WireMate, where the same class soup was
|
|
5
|
-
hand-rolled on every stub/request detail view.
|
|
6
|
-
|
|
7
|
-
Variants:
|
|
8
|
-
• soft — tinted fill, no border (bg-gray-800/70 / bg-gray-100).
|
|
9
|
-
The in-card look used under a section heading.
|
|
10
|
-
• bordered — bordered card fill (bg-gray-900 / bg-white). The
|
|
11
|
-
standalone look used when the block sits directly on the page.
|
|
12
|
-
|
|
13
|
-
Sizing:
|
|
14
|
-
• sm — text-sm with roomy padding, for primary content blocks.
|
|
15
|
-
• xs — text-xs with tight padding, for dense/secondary dumps.
|
|
16
|
-
|
|
17
|
-
`max-height-class` caps tall payloads (e.g. 'max-h-96'); the block
|
|
18
|
-
scrolls both axes. Extra classes (margins etc.) fall through via the
|
|
19
|
-
normal class attr merge.
|
|
20
|
-
-->
|
|
21
|
-
<pre
|
|
22
|
-
class="font-mono overflow-auto"
|
|
23
|
-
:class="[sizeClass, paletteClass, maxHeightClass]"
|
|
24
|
-
>{{ code }}</pre>
|
|
25
|
-
</template>
|
|
26
|
-
|
|
27
|
-
<script setup lang="ts">
|
|
28
|
-
import { computed } from 'vue'
|
|
29
|
-
import { useTheme } from '../composables/useTheme'
|
|
30
|
-
|
|
31
|
-
const props = withDefaults(
|
|
32
|
-
defineProps<{
|
|
33
|
-
/** The raw text to render. Whitespace is preserved verbatim. */
|
|
34
|
-
code: string
|
|
35
|
-
/** Visual style: tinted fill ('soft') or bordered card ('bordered'). */
|
|
36
|
-
variant?: 'soft' | 'bordered'
|
|
37
|
-
/** Text size + padding: 'sm' (roomy) or 'xs' (dense). */
|
|
38
|
-
size?: 'sm' | 'xs'
|
|
39
|
-
/** Optional Tailwind max-height utility, e.g. 'max-h-96'. */
|
|
40
|
-
maxHeightClass?: string
|
|
41
|
-
}>(),
|
|
42
|
-
{
|
|
43
|
-
variant: 'soft',
|
|
44
|
-
size: 'sm',
|
|
45
|
-
maxHeightClass: '',
|
|
46
|
-
},
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
const { isDark } = useTheme()
|
|
50
|
-
|
|
51
|
-
// Full literal class strings so Tailwind detects them at build time.
|
|
52
|
-
const PALETTES: Record<'soft' | 'bordered', { dark: string; light: string }> = {
|
|
53
|
-
soft: {
|
|
54
|
-
dark: 'bg-gray-800/70 text-gray-300',
|
|
55
|
-
light: 'bg-gray-100 text-gray-700',
|
|
56
|
-
},
|
|
57
|
-
bordered: {
|
|
58
|
-
dark: 'border bg-gray-900 border-gray-700 text-gray-300',
|
|
59
|
-
light: 'border bg-white border-gray-200 text-gray-700',
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const SIZES: Record<'sm' | 'xs', string> = {
|
|
64
|
-
sm: 'text-sm px-5 py-4 rounded-lg',
|
|
65
|
-
xs: 'text-xs p-3 rounded-lg',
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const paletteClass = computed(() => {
|
|
69
|
-
const entry = PALETTES[props.variant]
|
|
70
|
-
return isDark.value ? entry.dark : entry.light
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
const sizeClass = computed(() => SIZES[props.size])
|
|
74
|
-
</script>
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Themed monospace <pre> for JSON payloads, request dumps and code
|
|
4
|
+
snippets — extracted from WireMate, where the same class soup was
|
|
5
|
+
hand-rolled on every stub/request detail view.
|
|
6
|
+
|
|
7
|
+
Variants:
|
|
8
|
+
• soft — tinted fill, no border (bg-gray-800/70 / bg-gray-100).
|
|
9
|
+
The in-card look used under a section heading.
|
|
10
|
+
• bordered — bordered card fill (bg-gray-900 / bg-white). The
|
|
11
|
+
standalone look used when the block sits directly on the page.
|
|
12
|
+
|
|
13
|
+
Sizing:
|
|
14
|
+
• sm — text-sm with roomy padding, for primary content blocks.
|
|
15
|
+
• xs — text-xs with tight padding, for dense/secondary dumps.
|
|
16
|
+
|
|
17
|
+
`max-height-class` caps tall payloads (e.g. 'max-h-96'); the block
|
|
18
|
+
scrolls both axes. Extra classes (margins etc.) fall through via the
|
|
19
|
+
normal class attr merge.
|
|
20
|
+
-->
|
|
21
|
+
<pre
|
|
22
|
+
class="font-mono overflow-auto"
|
|
23
|
+
:class="[sizeClass, paletteClass, maxHeightClass]"
|
|
24
|
+
>{{ code }}</pre>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
import { computed } from 'vue'
|
|
29
|
+
import { useTheme } from '../composables/useTheme'
|
|
30
|
+
|
|
31
|
+
const props = withDefaults(
|
|
32
|
+
defineProps<{
|
|
33
|
+
/** The raw text to render. Whitespace is preserved verbatim. */
|
|
34
|
+
code: string
|
|
35
|
+
/** Visual style: tinted fill ('soft') or bordered card ('bordered'). */
|
|
36
|
+
variant?: 'soft' | 'bordered'
|
|
37
|
+
/** Text size + padding: 'sm' (roomy) or 'xs' (dense). */
|
|
38
|
+
size?: 'sm' | 'xs'
|
|
39
|
+
/** Optional Tailwind max-height utility, e.g. 'max-h-96'. */
|
|
40
|
+
maxHeightClass?: string
|
|
41
|
+
}>(),
|
|
42
|
+
{
|
|
43
|
+
variant: 'soft',
|
|
44
|
+
size: 'sm',
|
|
45
|
+
maxHeightClass: '',
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const { isDark } = useTheme()
|
|
50
|
+
|
|
51
|
+
// Full literal class strings so Tailwind detects them at build time.
|
|
52
|
+
const PALETTES: Record<'soft' | 'bordered', { dark: string; light: string }> = {
|
|
53
|
+
soft: {
|
|
54
|
+
dark: 'bg-gray-800/70 text-gray-300',
|
|
55
|
+
light: 'bg-gray-100 text-gray-700',
|
|
56
|
+
},
|
|
57
|
+
bordered: {
|
|
58
|
+
dark: 'border bg-gray-900 border-gray-700 text-gray-300',
|
|
59
|
+
light: 'border bg-white border-gray-200 text-gray-700',
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const SIZES: Record<'sm' | 'xs', string> = {
|
|
64
|
+
sm: 'text-sm px-5 py-4 rounded-lg',
|
|
65
|
+
xs: 'text-xs p-3 rounded-lg',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const paletteClass = computed(() => {
|
|
69
|
+
const entry = PALETTES[props.variant]
|
|
70
|
+
return isDark.value ? entry.dark : entry.light
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const sizeClass = computed(() => SIZES[props.size])
|
|
74
|
+
</script>
|
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!--
|
|
3
|
-
Collapsible section wrapper. Parent owns the `collapsed` state so
|
|
4
|
-
callers can persist it across edits or batch-collapse many sections
|
|
5
|
-
at once.
|
|
6
|
-
|
|
7
|
-
Slots:
|
|
8
|
-
• default — the section body (only rendered when expanded)
|
|
9
|
-
|
|
10
|
-
Props:
|
|
11
|
-
• title — heading text
|
|
12
|
-
• collapsed — current collapsed state
|
|
13
|
-
• badge — optional pill next to the heading (label or count)
|
|
14
|
-
• bodyClass — override classes for the body container. Defaults
|
|
15
|
-
to a theme-aware white/gray-800 surface.
|
|
16
|
-
|
|
17
|
-
Emits:
|
|
18
|
-
• toggle — fired when the user clicks the header. The parent
|
|
19
|
-
flips its own collapsed state in response.
|
|
20
|
-
-->
|
|
21
|
-
<!--
|
|
22
|
-
NOTE: no `overflow-hidden` on the section — it would clip floating
|
|
23
|
-
menus (BaseDropdown) rendered near the bottom of the body. The child
|
|
24
|
-
corners are rounded explicitly instead so the card silhouette is
|
|
25
|
-
unchanged.
|
|
26
|
-
-->
|
|
27
|
-
<section class="rounded-xl border transition-colors" :class="t.cardAlt">
|
|
28
|
-
<button
|
|
29
|
-
type="button"
|
|
30
|
-
class="w-full flex items-center justify-between p-4 text-left transition-colors cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-emerald-500"
|
|
31
|
-
:class="[headerClasses, collapsed ? 'rounded-xl' : 'rounded-t-xl']"
|
|
32
|
-
:aria-expanded="!collapsed"
|
|
33
|
-
@click="emit('toggle')"
|
|
34
|
-
>
|
|
35
|
-
<div class="flex items-center gap-3">
|
|
36
|
-
<h2 class="text-base font-semibold" :class="headingClasses">{{ title }}</h2>
|
|
37
|
-
<span
|
|
38
|
-
v-if="badge"
|
|
39
|
-
class="text-xs px-2 py-0.5 rounded-full"
|
|
40
|
-
:class="badgeClasses"
|
|
41
|
-
>
|
|
42
|
-
{{ badge }}
|
|
43
|
-
</span>
|
|
44
|
-
</div>
|
|
45
|
-
<svg
|
|
46
|
-
class="w-5 h-5 transition-transform"
|
|
47
|
-
:class="[collapsed ? '' : 'rotate-180', t.dimTextAlt]"
|
|
48
|
-
fill="none"
|
|
49
|
-
viewBox="0 0 24 24"
|
|
50
|
-
stroke="currentColor"
|
|
51
|
-
stroke-width="2"
|
|
52
|
-
aria-hidden="true"
|
|
53
|
-
>
|
|
54
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
|
55
|
-
</svg>
|
|
56
|
-
</button>
|
|
57
|
-
<div
|
|
58
|
-
v-show="!collapsed"
|
|
59
|
-
class="p-6 border-t rounded-b-xl"
|
|
60
|
-
:class="[t.border, resolvedBodyClass]"
|
|
61
|
-
>
|
|
62
|
-
<slot />
|
|
63
|
-
</div>
|
|
64
|
-
</section>
|
|
65
|
-
</template>
|
|
66
|
-
|
|
67
|
-
<script setup lang="ts">
|
|
68
|
-
import { computed } from 'vue'
|
|
69
|
-
import { useTheme } from '../composables/useTheme'
|
|
70
|
-
import { useThemeClasses } from '../composables/useThemeClasses'
|
|
71
|
-
|
|
72
|
-
interface Props {
|
|
73
|
-
title: string
|
|
74
|
-
collapsed: boolean
|
|
75
|
-
/**
|
|
76
|
-
* Optional pill next to the title. String so callers can pass either
|
|
77
|
-
* a label ('Active') or a count ('3'). Null/undefined hides the pill.
|
|
78
|
-
*/
|
|
79
|
-
badge?: string | null
|
|
80
|
-
/**
|
|
81
|
-
* Override the body container's background classes. Defaults to a
|
|
82
|
-
* theme-aware surface: bg-white in light, bg-gray-800 in dark.
|
|
83
|
-
*/
|
|
84
|
-
bodyClass?: string
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const props = defineProps<Props>()
|
|
88
|
-
const emit = defineEmits<{ toggle: [] }>()
|
|
89
|
-
|
|
90
|
-
const { isDark } = useTheme()
|
|
91
|
-
const t = useThemeClasses()
|
|
92
|
-
|
|
93
|
-
const headingClasses = computed(() =>
|
|
94
|
-
isDark.value ? 'text-gray-100' : 'text-gray-700',
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
const headerClasses = computed(() =>
|
|
98
|
-
isDark.value
|
|
99
|
-
? 'bg-gray-800/50 hover:bg-gray-800 border-gray-700'
|
|
100
|
-
: 'bg-gray-50 hover:bg-gray-100 border-gray-200',
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
const badgeClasses = computed(() =>
|
|
104
|
-
isDark.value
|
|
105
|
-
? 'bg-emerald-500/15 text-emerald-400'
|
|
106
|
-
: 'bg-emerald-50 text-emerald-700',
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
const resolvedBodyClass = computed(() => {
|
|
110
|
-
if (props.bodyClass !== undefined) return props.bodyClass
|
|
111
|
-
return isDark.value ? 'bg-gray-800' : 'bg-white'
|
|
112
|
-
})
|
|
113
|
-
</script>
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Collapsible section wrapper. Parent owns the `collapsed` state so
|
|
4
|
+
callers can persist it across edits or batch-collapse many sections
|
|
5
|
+
at once.
|
|
6
|
+
|
|
7
|
+
Slots:
|
|
8
|
+
• default — the section body (only rendered when expanded)
|
|
9
|
+
|
|
10
|
+
Props:
|
|
11
|
+
• title — heading text
|
|
12
|
+
• collapsed — current collapsed state
|
|
13
|
+
• badge — optional pill next to the heading (label or count)
|
|
14
|
+
• bodyClass — override classes for the body container. Defaults
|
|
15
|
+
to a theme-aware white/gray-800 surface.
|
|
16
|
+
|
|
17
|
+
Emits:
|
|
18
|
+
• toggle — fired when the user clicks the header. The parent
|
|
19
|
+
flips its own collapsed state in response.
|
|
20
|
+
-->
|
|
21
|
+
<!--
|
|
22
|
+
NOTE: no `overflow-hidden` on the section — it would clip floating
|
|
23
|
+
menus (BaseDropdown) rendered near the bottom of the body. The child
|
|
24
|
+
corners are rounded explicitly instead so the card silhouette is
|
|
25
|
+
unchanged.
|
|
26
|
+
-->
|
|
27
|
+
<section class="rounded-xl border transition-colors" :class="t.cardAlt">
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
class="w-full flex items-center justify-between p-4 text-left transition-colors cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-emerald-500"
|
|
31
|
+
:class="[headerClasses, collapsed ? 'rounded-xl' : 'rounded-t-xl']"
|
|
32
|
+
:aria-expanded="!collapsed"
|
|
33
|
+
@click="emit('toggle')"
|
|
34
|
+
>
|
|
35
|
+
<div class="flex items-center gap-3">
|
|
36
|
+
<h2 class="text-base font-semibold" :class="headingClasses">{{ title }}</h2>
|
|
37
|
+
<span
|
|
38
|
+
v-if="badge"
|
|
39
|
+
class="text-xs px-2 py-0.5 rounded-full"
|
|
40
|
+
:class="badgeClasses"
|
|
41
|
+
>
|
|
42
|
+
{{ badge }}
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
<svg
|
|
46
|
+
class="w-5 h-5 transition-transform"
|
|
47
|
+
:class="[collapsed ? '' : 'rotate-180', t.dimTextAlt]"
|
|
48
|
+
fill="none"
|
|
49
|
+
viewBox="0 0 24 24"
|
|
50
|
+
stroke="currentColor"
|
|
51
|
+
stroke-width="2"
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
>
|
|
54
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
|
55
|
+
</svg>
|
|
56
|
+
</button>
|
|
57
|
+
<div
|
|
58
|
+
v-show="!collapsed"
|
|
59
|
+
class="p-6 border-t rounded-b-xl"
|
|
60
|
+
:class="[t.border, resolvedBodyClass]"
|
|
61
|
+
>
|
|
62
|
+
<slot />
|
|
63
|
+
</div>
|
|
64
|
+
</section>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<script setup lang="ts">
|
|
68
|
+
import { computed } from 'vue'
|
|
69
|
+
import { useTheme } from '../composables/useTheme'
|
|
70
|
+
import { useThemeClasses } from '../composables/useThemeClasses'
|
|
71
|
+
|
|
72
|
+
interface Props {
|
|
73
|
+
title: string
|
|
74
|
+
collapsed: boolean
|
|
75
|
+
/**
|
|
76
|
+
* Optional pill next to the title. String so callers can pass either
|
|
77
|
+
* a label ('Active') or a count ('3'). Null/undefined hides the pill.
|
|
78
|
+
*/
|
|
79
|
+
badge?: string | null
|
|
80
|
+
/**
|
|
81
|
+
* Override the body container's background classes. Defaults to a
|
|
82
|
+
* theme-aware surface: bg-white in light, bg-gray-800 in dark.
|
|
83
|
+
*/
|
|
84
|
+
bodyClass?: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const props = defineProps<Props>()
|
|
88
|
+
const emit = defineEmits<{ toggle: [] }>()
|
|
89
|
+
|
|
90
|
+
const { isDark } = useTheme()
|
|
91
|
+
const t = useThemeClasses()
|
|
92
|
+
|
|
93
|
+
const headingClasses = computed(() =>
|
|
94
|
+
isDark.value ? 'text-gray-100' : 'text-gray-700',
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const headerClasses = computed(() =>
|
|
98
|
+
isDark.value
|
|
99
|
+
? 'bg-gray-800/50 hover:bg-gray-800 border-gray-700'
|
|
100
|
+
: 'bg-gray-50 hover:bg-gray-100 border-gray-200',
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const badgeClasses = computed(() =>
|
|
104
|
+
isDark.value
|
|
105
|
+
? 'bg-emerald-500/15 text-emerald-400'
|
|
106
|
+
: 'bg-emerald-50 text-emerald-700',
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const resolvedBodyClass = computed(() => {
|
|
110
|
+
if (props.bodyClass !== undefined) return props.bodyClass
|
|
111
|
+
return isDark.value ? 'bg-gray-800' : 'bg-white'
|
|
112
|
+
})
|
|
113
|
+
</script>
|