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,209 +1,209 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!--
|
|
3
|
-
Shared modal shell — knows how a dialog is structured (teleport,
|
|
4
|
-
backdrop, dark-mode card, escape key, aria-modal). Concrete modals
|
|
5
|
-
should compose this rather than re-implementing the chrome.
|
|
6
|
-
|
|
7
|
-
Every modal shows an icon left of the title: the REQUIRED `icon`
|
|
8
|
-
prop (a component, e.g. a heroicon) renders inside a tinted
|
|
9
|
-
circular chip. Tint via `icon-bg-class` / `icon-class`; the `icon`
|
|
10
|
-
slot remains as a full-markup override for callers that need
|
|
11
|
-
something richer than the standard chip.
|
|
12
|
-
|
|
13
|
-
Slots:
|
|
14
|
-
• icon — override the standard icon chip next to the title
|
|
15
|
-
• default — body content
|
|
16
|
-
• footer — action buttons (cancel / confirm / etc.)
|
|
17
|
-
|
|
18
|
-
Backdrop click:
|
|
19
|
-
• Emits `backdrop`. By default this also fires `cancel` (the
|
|
20
|
-
common case). Parents that want to suppress this — e.g. a
|
|
21
|
-
modal with unsaved input — should pass `manual-close` and
|
|
22
|
-
listen on `backdrop` to decide what to do.
|
|
23
|
-
-->
|
|
24
|
-
<Teleport to="body">
|
|
25
|
-
<div
|
|
26
|
-
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
|
27
|
-
role="dialog"
|
|
28
|
-
aria-modal="true"
|
|
29
|
-
:aria-labelledby="titleId"
|
|
30
|
-
@click="onBackdrop"
|
|
31
|
-
>
|
|
32
|
-
<!--
|
|
33
|
-
Scrollable layout — for large content modals. A flex column
|
|
34
|
-
capped at 90vh: header and footer stay fixed (`shrink-0`) while
|
|
35
|
-
the body scrolls (`overflow-y-auto flex-1`). Supports a subtitle
|
|
36
|
-
and a `header-actions` slot (e.g. a close button) on the right.
|
|
37
|
-
-->
|
|
38
|
-
<div
|
|
39
|
-
v-if="scrollable"
|
|
40
|
-
class="relative w-full mx-4 rounded-lg shadow-xl max-h-[90vh] flex flex-col"
|
|
41
|
-
:class="[maxWidthClass, shellClass]"
|
|
42
|
-
@click.stop
|
|
43
|
-
>
|
|
44
|
-
<div class="flex items-start justify-between gap-4 p-6 border-b shrink-0" :class="t.border">
|
|
45
|
-
<div class="flex items-start gap-4 min-w-0">
|
|
46
|
-
<slot name="icon">
|
|
47
|
-
<div
|
|
48
|
-
class="flex-shrink-0 flex h-12 w-12 items-center justify-center rounded-full"
|
|
49
|
-
:class="iconChipBg"
|
|
50
|
-
>
|
|
51
|
-
<component :is="icon" class="h-6 w-6" :class="iconClass" aria-hidden="true" />
|
|
52
|
-
</div>
|
|
53
|
-
</slot>
|
|
54
|
-
<div class="flex-1 min-w-0">
|
|
55
|
-
<h3 :id="titleId" class="text-lg font-medium" :class="t.primaryText">
|
|
56
|
-
{{ title }}
|
|
57
|
-
</h3>
|
|
58
|
-
<p v-if="subtitle" class="text-sm mt-1 break-all" :class="t.mutedText">
|
|
59
|
-
{{ subtitle }}
|
|
60
|
-
</p>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
<slot name="header-actions" />
|
|
64
|
-
</div>
|
|
65
|
-
|
|
66
|
-
<div class="p-6 overflow-y-auto flex-1">
|
|
67
|
-
<slot />
|
|
68
|
-
</div>
|
|
69
|
-
|
|
70
|
-
<div v-if="$slots.footer" class="flex gap-3 justify-end p-6 border-t shrink-0" :class="t.border">
|
|
71
|
-
<slot name="footer" />
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<!-- Compact layout (default) — small centered dialog. -->
|
|
76
|
-
<div
|
|
77
|
-
v-else
|
|
78
|
-
class="relative w-full mx-4 rounded-lg shadow-xl"
|
|
79
|
-
:class="[maxWidthClass, shellClass]"
|
|
80
|
-
@click.stop
|
|
81
|
-
>
|
|
82
|
-
<div class="p-6">
|
|
83
|
-
<!-- Title row -->
|
|
84
|
-
<div class="flex items-start gap-4">
|
|
85
|
-
<slot name="icon">
|
|
86
|
-
<div
|
|
87
|
-
class="flex-shrink-0 flex h-12 w-12 items-center justify-center rounded-full"
|
|
88
|
-
:class="iconChipBg"
|
|
89
|
-
>
|
|
90
|
-
<component :is="icon" class="h-6 w-6" :class="iconClass" aria-hidden="true" />
|
|
91
|
-
</div>
|
|
92
|
-
</slot>
|
|
93
|
-
<div class="flex-1">
|
|
94
|
-
<h3
|
|
95
|
-
:id="titleId"
|
|
96
|
-
class="text-lg font-medium"
|
|
97
|
-
:class="t.primaryText"
|
|
98
|
-
>
|
|
99
|
-
{{ title }}
|
|
100
|
-
</h3>
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
103
|
-
|
|
104
|
-
<!-- Body -->
|
|
105
|
-
<div class="mt-4">
|
|
106
|
-
<slot />
|
|
107
|
-
</div>
|
|
108
|
-
|
|
109
|
-
<!-- Footer -->
|
|
110
|
-
<div class="mt-6 flex gap-3 justify-end">
|
|
111
|
-
<slot name="footer" />
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
</Teleport>
|
|
117
|
-
</template>
|
|
118
|
-
|
|
119
|
-
<script setup lang="ts">
|
|
120
|
-
import { computed } from 'vue'
|
|
121
|
-
import type { Component } from 'vue'
|
|
122
|
-
import { useTheme } from '../composables/useTheme'
|
|
123
|
-
import { useThemeClasses } from '../composables/useThemeClasses'
|
|
124
|
-
import { useEscapeKey } from '../composables/useEscapeKey'
|
|
125
|
-
|
|
126
|
-
interface Props {
|
|
127
|
-
title: string
|
|
128
|
-
/**
|
|
129
|
-
* Icon component (e.g. a heroicon) rendered in a tinted circular
|
|
130
|
-
* chip left of the title. REQUIRED — every modal must name itself
|
|
131
|
-
* with an icon. The `icon` slot can override the whole chip.
|
|
132
|
-
*/
|
|
133
|
-
icon: Component
|
|
134
|
-
/**
|
|
135
|
-
* Background classes of the icon chip. Defaults to the emerald
|
|
136
|
-
* tint (dark-mode aware).
|
|
137
|
-
*/
|
|
138
|
-
iconBgClass?: string
|
|
139
|
-
/** Classes applied to the icon itself. Defaults to emerald. */
|
|
140
|
-
iconClass?: string
|
|
141
|
-
/**
|
|
142
|
-
* Max-width of the dialog card. Defaults to `max-w-md`. Pass any
|
|
143
|
-
* Tailwind max-w utility, or empty string to opt out.
|
|
144
|
-
*/
|
|
145
|
-
maxWidthClass?: string
|
|
146
|
-
/**
|
|
147
|
-
* When true, the backdrop click and Escape key are NOT auto-bound
|
|
148
|
-
* to `cancel`. Use when the parent wants to gate dismissal on
|
|
149
|
-
* something (e.g. unsaved input). The parent should listen on
|
|
150
|
-
* `backdrop` / handle their own Escape.
|
|
151
|
-
*/
|
|
152
|
-
manualClose?: boolean
|
|
153
|
-
/**
|
|
154
|
-
* Switches to the scrollable layout — a flex column capped at 90vh
|
|
155
|
-
* with a fixed header/footer and a scrolling body. Use for large
|
|
156
|
-
* content modals (request detail, import preview, …) rather than the
|
|
157
|
-
* compact confirm-style default.
|
|
158
|
-
*/
|
|
159
|
-
scrollable?: boolean
|
|
160
|
-
/**
|
|
161
|
-
* Optional muted line under the title. Only rendered in the
|
|
162
|
-
* scrollable layout.
|
|
163
|
-
*/
|
|
164
|
-
subtitle?: string
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
168
|
-
maxWidthClass: 'max-w-md',
|
|
169
|
-
manualClose: false,
|
|
170
|
-
scrollable: false,
|
|
171
|
-
subtitle: '',
|
|
172
|
-
iconBgClass: '',
|
|
173
|
-
iconClass: 'text-emerald-600',
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
const emit = defineEmits<{
|
|
177
|
-
cancel: []
|
|
178
|
-
backdrop: []
|
|
179
|
-
}>()
|
|
180
|
-
|
|
181
|
-
const { isDark } = useTheme()
|
|
182
|
-
|
|
183
|
-
const t = useThemeClasses()
|
|
184
|
-
// Stable id derived per-instance so aria-labelledby points at this
|
|
185
|
-
// dialog's own heading. Suffix with a random short string to avoid
|
|
186
|
-
// collisions when two modals are momentarily stacked.
|
|
187
|
-
const titleId = `modal-title-${Math.random().toString(36).slice(2, 9)}`
|
|
188
|
-
|
|
189
|
-
const shellClass = computed(() =>
|
|
190
|
-
isDark.value
|
|
191
|
-
? 'bg-gray-900 border border-gray-700'
|
|
192
|
-
: 'bg-white border border-gray-200',
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
// The icon chip's background: an explicit `icon-bg-class` wins, else the
|
|
196
|
-
// standard emerald tint in the current theme.
|
|
197
|
-
const iconChipBg = computed(() =>
|
|
198
|
-
props.iconBgClass || (isDark.value ? 'bg-emerald-900/30' : 'bg-emerald-50'),
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
function onBackdrop() {
|
|
202
|
-
emit('backdrop')
|
|
203
|
-
if (!props.manualClose) emit('cancel')
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
useEscapeKey(() => {
|
|
207
|
-
if (!props.manualClose) emit('cancel')
|
|
208
|
-
})
|
|
209
|
-
</script>
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Shared modal shell — knows how a dialog is structured (teleport,
|
|
4
|
+
backdrop, dark-mode card, escape key, aria-modal). Concrete modals
|
|
5
|
+
should compose this rather than re-implementing the chrome.
|
|
6
|
+
|
|
7
|
+
Every modal shows an icon left of the title: the REQUIRED `icon`
|
|
8
|
+
prop (a component, e.g. a heroicon) renders inside a tinted
|
|
9
|
+
circular chip. Tint via `icon-bg-class` / `icon-class`; the `icon`
|
|
10
|
+
slot remains as a full-markup override for callers that need
|
|
11
|
+
something richer than the standard chip.
|
|
12
|
+
|
|
13
|
+
Slots:
|
|
14
|
+
• icon — override the standard icon chip next to the title
|
|
15
|
+
• default — body content
|
|
16
|
+
• footer — action buttons (cancel / confirm / etc.)
|
|
17
|
+
|
|
18
|
+
Backdrop click:
|
|
19
|
+
• Emits `backdrop`. By default this also fires `cancel` (the
|
|
20
|
+
common case). Parents that want to suppress this — e.g. a
|
|
21
|
+
modal with unsaved input — should pass `manual-close` and
|
|
22
|
+
listen on `backdrop` to decide what to do.
|
|
23
|
+
-->
|
|
24
|
+
<Teleport to="body">
|
|
25
|
+
<div
|
|
26
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
|
27
|
+
role="dialog"
|
|
28
|
+
aria-modal="true"
|
|
29
|
+
:aria-labelledby="titleId"
|
|
30
|
+
@click="onBackdrop"
|
|
31
|
+
>
|
|
32
|
+
<!--
|
|
33
|
+
Scrollable layout — for large content modals. A flex column
|
|
34
|
+
capped at 90vh: header and footer stay fixed (`shrink-0`) while
|
|
35
|
+
the body scrolls (`overflow-y-auto flex-1`). Supports a subtitle
|
|
36
|
+
and a `header-actions` slot (e.g. a close button) on the right.
|
|
37
|
+
-->
|
|
38
|
+
<div
|
|
39
|
+
v-if="scrollable"
|
|
40
|
+
class="relative w-full mx-4 rounded-lg shadow-xl max-h-[90vh] flex flex-col"
|
|
41
|
+
:class="[maxWidthClass, shellClass]"
|
|
42
|
+
@click.stop
|
|
43
|
+
>
|
|
44
|
+
<div class="flex items-start justify-between gap-4 p-6 border-b shrink-0" :class="t.border">
|
|
45
|
+
<div class="flex items-start gap-4 min-w-0">
|
|
46
|
+
<slot name="icon">
|
|
47
|
+
<div
|
|
48
|
+
class="flex-shrink-0 flex h-12 w-12 items-center justify-center rounded-full"
|
|
49
|
+
:class="iconChipBg"
|
|
50
|
+
>
|
|
51
|
+
<component :is="icon" class="h-6 w-6" :class="iconClass" aria-hidden="true" />
|
|
52
|
+
</div>
|
|
53
|
+
</slot>
|
|
54
|
+
<div class="flex-1 min-w-0">
|
|
55
|
+
<h3 :id="titleId" class="text-lg font-medium" :class="t.primaryText">
|
|
56
|
+
{{ title }}
|
|
57
|
+
</h3>
|
|
58
|
+
<p v-if="subtitle" class="text-sm mt-1 break-all" :class="t.mutedText">
|
|
59
|
+
{{ subtitle }}
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<slot name="header-actions" />
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="p-6 overflow-y-auto flex-1">
|
|
67
|
+
<slot />
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div v-if="$slots.footer" class="flex gap-3 justify-end p-6 border-t shrink-0" :class="t.border">
|
|
71
|
+
<slot name="footer" />
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<!-- Compact layout (default) — small centered dialog. -->
|
|
76
|
+
<div
|
|
77
|
+
v-else
|
|
78
|
+
class="relative w-full mx-4 rounded-lg shadow-xl"
|
|
79
|
+
:class="[maxWidthClass, shellClass]"
|
|
80
|
+
@click.stop
|
|
81
|
+
>
|
|
82
|
+
<div class="p-6">
|
|
83
|
+
<!-- Title row -->
|
|
84
|
+
<div class="flex items-start gap-4">
|
|
85
|
+
<slot name="icon">
|
|
86
|
+
<div
|
|
87
|
+
class="flex-shrink-0 flex h-12 w-12 items-center justify-center rounded-full"
|
|
88
|
+
:class="iconChipBg"
|
|
89
|
+
>
|
|
90
|
+
<component :is="icon" class="h-6 w-6" :class="iconClass" aria-hidden="true" />
|
|
91
|
+
</div>
|
|
92
|
+
</slot>
|
|
93
|
+
<div class="flex-1">
|
|
94
|
+
<h3
|
|
95
|
+
:id="titleId"
|
|
96
|
+
class="text-lg font-medium"
|
|
97
|
+
:class="t.primaryText"
|
|
98
|
+
>
|
|
99
|
+
{{ title }}
|
|
100
|
+
</h3>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- Body -->
|
|
105
|
+
<div class="mt-4">
|
|
106
|
+
<slot />
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<!-- Footer -->
|
|
110
|
+
<div class="mt-6 flex gap-3 justify-end">
|
|
111
|
+
<slot name="footer" />
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</Teleport>
|
|
117
|
+
</template>
|
|
118
|
+
|
|
119
|
+
<script setup lang="ts">
|
|
120
|
+
import { computed } from 'vue'
|
|
121
|
+
import type { Component } from 'vue'
|
|
122
|
+
import { useTheme } from '../composables/useTheme'
|
|
123
|
+
import { useThemeClasses } from '../composables/useThemeClasses'
|
|
124
|
+
import { useEscapeKey } from '../composables/useEscapeKey'
|
|
125
|
+
|
|
126
|
+
interface Props {
|
|
127
|
+
title: string
|
|
128
|
+
/**
|
|
129
|
+
* Icon component (e.g. a heroicon) rendered in a tinted circular
|
|
130
|
+
* chip left of the title. REQUIRED — every modal must name itself
|
|
131
|
+
* with an icon. The `icon` slot can override the whole chip.
|
|
132
|
+
*/
|
|
133
|
+
icon: Component
|
|
134
|
+
/**
|
|
135
|
+
* Background classes of the icon chip. Defaults to the emerald
|
|
136
|
+
* tint (dark-mode aware).
|
|
137
|
+
*/
|
|
138
|
+
iconBgClass?: string
|
|
139
|
+
/** Classes applied to the icon itself. Defaults to emerald. */
|
|
140
|
+
iconClass?: string
|
|
141
|
+
/**
|
|
142
|
+
* Max-width of the dialog card. Defaults to `max-w-md`. Pass any
|
|
143
|
+
* Tailwind max-w utility, or empty string to opt out.
|
|
144
|
+
*/
|
|
145
|
+
maxWidthClass?: string
|
|
146
|
+
/**
|
|
147
|
+
* When true, the backdrop click and Escape key are NOT auto-bound
|
|
148
|
+
* to `cancel`. Use when the parent wants to gate dismissal on
|
|
149
|
+
* something (e.g. unsaved input). The parent should listen on
|
|
150
|
+
* `backdrop` / handle their own Escape.
|
|
151
|
+
*/
|
|
152
|
+
manualClose?: boolean
|
|
153
|
+
/**
|
|
154
|
+
* Switches to the scrollable layout — a flex column capped at 90vh
|
|
155
|
+
* with a fixed header/footer and a scrolling body. Use for large
|
|
156
|
+
* content modals (request detail, import preview, …) rather than the
|
|
157
|
+
* compact confirm-style default.
|
|
158
|
+
*/
|
|
159
|
+
scrollable?: boolean
|
|
160
|
+
/**
|
|
161
|
+
* Optional muted line under the title. Only rendered in the
|
|
162
|
+
* scrollable layout.
|
|
163
|
+
*/
|
|
164
|
+
subtitle?: string
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
168
|
+
maxWidthClass: 'max-w-md',
|
|
169
|
+
manualClose: false,
|
|
170
|
+
scrollable: false,
|
|
171
|
+
subtitle: '',
|
|
172
|
+
iconBgClass: '',
|
|
173
|
+
iconClass: 'text-emerald-600',
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const emit = defineEmits<{
|
|
177
|
+
cancel: []
|
|
178
|
+
backdrop: []
|
|
179
|
+
}>()
|
|
180
|
+
|
|
181
|
+
const { isDark } = useTheme()
|
|
182
|
+
|
|
183
|
+
const t = useThemeClasses()
|
|
184
|
+
// Stable id derived per-instance so aria-labelledby points at this
|
|
185
|
+
// dialog's own heading. Suffix with a random short string to avoid
|
|
186
|
+
// collisions when two modals are momentarily stacked.
|
|
187
|
+
const titleId = `modal-title-${Math.random().toString(36).slice(2, 9)}`
|
|
188
|
+
|
|
189
|
+
const shellClass = computed(() =>
|
|
190
|
+
isDark.value
|
|
191
|
+
? 'bg-gray-900 border border-gray-700'
|
|
192
|
+
: 'bg-white border border-gray-200',
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
// The icon chip's background: an explicit `icon-bg-class` wins, else the
|
|
196
|
+
// standard emerald tint in the current theme.
|
|
197
|
+
const iconChipBg = computed(() =>
|
|
198
|
+
props.iconBgClass || (isDark.value ? 'bg-emerald-900/30' : 'bg-emerald-50'),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
function onBackdrop() {
|
|
202
|
+
emit('backdrop')
|
|
203
|
+
if (!props.manualClose) emit('cancel')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
useEscapeKey(() => {
|
|
207
|
+
if (!props.manualClose) emit('cancel')
|
|
208
|
+
})
|
|
209
|
+
</script>
|
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!--
|
|
3
|
-
Page-level header. Standardises the "icon badge + title/subtitle on
|
|
4
|
-
the left, action buttons on the right" pattern for top-level views.
|
|
5
|
-
|
|
6
|
-
Slots:
|
|
7
|
-
• icon — the page-specific Heroicon. Receives an `iconClass`
|
|
8
|
-
slot prop carrying the theme-aware text colour, so callers can
|
|
9
|
-
bind `:class="iconClass"` on their icon component. The badge
|
|
10
|
-
square only renders when this slot is filled, so icon-less apps
|
|
11
|
-
get a plain title/subtitle header.
|
|
12
|
-
• subtitle — rich subtitle content (links, emphasis); overrides
|
|
13
|
-
the `subtitle` prop.
|
|
14
|
-
• actions — refresh / destructive buttons rendered on the right.
|
|
15
|
-
|
|
16
|
-
Props:
|
|
17
|
-
• title — the H1 text.
|
|
18
|
-
• subtitle — optional muted line below the title.
|
|
19
|
-
• iconColor — badge background + icon text colour
|
|
20
|
-
('emerald' | 'sky' | 'red' | 'amber'). Defaults to 'emerald'.
|
|
21
|
-
• maxWidthClass — Tailwind max-w utility constraining the header
|
|
22
|
-
width so sibling views can share a footprint. Defaults to
|
|
23
|
-
'max-w-4xl'; pass '' to skip the width wrapper entirely (the
|
|
24
|
-
header then spans whatever container it sits in).
|
|
25
|
-
• align — vertical alignment of the title block vs the actions:
|
|
26
|
-
'center' (default) or 'end' (actions sit on the title baseline).
|
|
27
|
-
• marginClass — space under the header. Defaults to 'mb-8'; pass
|
|
28
|
-
'' when the parent already manages vertical rhythm (space-y-*).
|
|
29
|
-
-->
|
|
30
|
-
<header :class="maxWidthClass ? ['mx-auto px-4 sm:px-6', maxWidthClass] : undefined">
|
|
31
|
-
<!--
|
|
32
|
-
flex-wrap + gap so a long title and the action buttons reflow onto
|
|
33
|
-
separate rows on narrow (mobile) viewports instead of the actions
|
|
34
|
-
overflowing off the right edge of the screen. min-w-0 on the title
|
|
35
|
-
group lets a long title truncate rather than shoving the actions out.
|
|
36
|
-
-->
|
|
37
|
-
<div
|
|
38
|
-
class="flex flex-wrap justify-between gap-3"
|
|
39
|
-
:class="[align === 'end' ? 'items-end' : 'items-center', marginClass]"
|
|
40
|
-
>
|
|
41
|
-
<div class="flex items-center gap-3 min-w-0">
|
|
42
|
-
<div
|
|
43
|
-
v-if="$slots.icon"
|
|
44
|
-
class="w-10 h-10 rounded-lg flex items-center justify-center"
|
|
45
|
-
:class="badgeBgClass"
|
|
46
|
-
>
|
|
47
|
-
<slot name="icon" :icon-class="iconTextClass" />
|
|
48
|
-
</div>
|
|
49
|
-
<div>
|
|
50
|
-
<h1 class="text-2xl font-bold" :class="t.primaryTextSoft">
|
|
51
|
-
{{ title }}
|
|
52
|
-
</h1>
|
|
53
|
-
<p v-if="subtitle || $slots.subtitle" class="text-sm" :class="t.dimTextAlt">
|
|
54
|
-
<slot name="subtitle">{{ subtitle }}</slot>
|
|
55
|
-
</p>
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
<div class="flex items-center gap-2">
|
|
59
|
-
<slot name="actions" />
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
</header>
|
|
63
|
-
</template>
|
|
64
|
-
|
|
65
|
-
<script setup lang="ts">
|
|
66
|
-
import { computed } from 'vue'
|
|
67
|
-
import { useTheme } from '../composables/useTheme'
|
|
68
|
-
import { useThemeClasses } from '../composables/useThemeClasses'
|
|
69
|
-
|
|
70
|
-
type IconColor = 'emerald' | 'sky' | 'red' | 'amber'
|
|
71
|
-
|
|
72
|
-
const props = withDefaults(
|
|
73
|
-
defineProps<{
|
|
74
|
-
title: string
|
|
75
|
-
subtitle?: string
|
|
76
|
-
iconColor?: IconColor
|
|
77
|
-
maxWidthClass?: string
|
|
78
|
-
align?: 'center' | 'end'
|
|
79
|
-
marginClass?: string
|
|
80
|
-
}>(),
|
|
81
|
-
{
|
|
82
|
-
subtitle: '',
|
|
83
|
-
iconColor: 'emerald',
|
|
84
|
-
maxWidthClass: 'max-w-4xl',
|
|
85
|
-
align: 'center',
|
|
86
|
-
marginClass: 'mb-8',
|
|
87
|
-
},
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
const { isDark } = useTheme()
|
|
91
|
-
const t = useThemeClasses()
|
|
92
|
-
|
|
93
|
-
// Maps the named colour palette to the badge-background classes for
|
|
94
|
-
// dark/light themes. Callers just pass `iconColor="sky"` and the
|
|
95
|
-
// component resolves both modes.
|
|
96
|
-
const badgeBgClass = computed(() => {
|
|
97
|
-
const palette: Record<IconColor, { dark: string; light: string }> = {
|
|
98
|
-
emerald: { dark: 'bg-emerald-500/10', light: 'bg-emerald-50' },
|
|
99
|
-
sky: { dark: 'bg-sky-500/10', light: 'bg-sky-50' },
|
|
100
|
-
red: { dark: 'bg-red-500/10', light: 'bg-red-50' },
|
|
101
|
-
amber: { dark: 'bg-amber-500/10', light: 'bg-amber-50' },
|
|
102
|
-
}
|
|
103
|
-
const entry = palette[props.iconColor]
|
|
104
|
-
return isDark.value ? entry.dark : entry.light
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
// Icon text colour, routed through the same name → palette mapping so
|
|
108
|
-
// callers don't need to know the exact Tailwind shade per theme. Exposed
|
|
109
|
-
// to the `icon` slot via the `iconClass` slot prop.
|
|
110
|
-
const iconTextClass = computed(() => {
|
|
111
|
-
const palette: Record<IconColor, { dark: string; light: string }> = {
|
|
112
|
-
emerald: { dark: 'text-emerald-400', light: 'text-emerald-600' },
|
|
113
|
-
sky: { dark: 'text-sky-400', light: 'text-sky-600' },
|
|
114
|
-
red: { dark: 'text-red-400', light: 'text-red-500' },
|
|
115
|
-
amber: { dark: 'text-amber-400', light: 'text-amber-600' },
|
|
116
|
-
}
|
|
117
|
-
const entry = palette[props.iconColor]
|
|
118
|
-
return isDark.value ? entry.dark : entry.light
|
|
119
|
-
})
|
|
120
|
-
</script>
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Page-level header. Standardises the "icon badge + title/subtitle on
|
|
4
|
+
the left, action buttons on the right" pattern for top-level views.
|
|
5
|
+
|
|
6
|
+
Slots:
|
|
7
|
+
• icon — the page-specific Heroicon. Receives an `iconClass`
|
|
8
|
+
slot prop carrying the theme-aware text colour, so callers can
|
|
9
|
+
bind `:class="iconClass"` on their icon component. The badge
|
|
10
|
+
square only renders when this slot is filled, so icon-less apps
|
|
11
|
+
get a plain title/subtitle header.
|
|
12
|
+
• subtitle — rich subtitle content (links, emphasis); overrides
|
|
13
|
+
the `subtitle` prop.
|
|
14
|
+
• actions — refresh / destructive buttons rendered on the right.
|
|
15
|
+
|
|
16
|
+
Props:
|
|
17
|
+
• title — the H1 text.
|
|
18
|
+
• subtitle — optional muted line below the title.
|
|
19
|
+
• iconColor — badge background + icon text colour
|
|
20
|
+
('emerald' | 'sky' | 'red' | 'amber'). Defaults to 'emerald'.
|
|
21
|
+
• maxWidthClass — Tailwind max-w utility constraining the header
|
|
22
|
+
width so sibling views can share a footprint. Defaults to
|
|
23
|
+
'max-w-4xl'; pass '' to skip the width wrapper entirely (the
|
|
24
|
+
header then spans whatever container it sits in).
|
|
25
|
+
• align — vertical alignment of the title block vs the actions:
|
|
26
|
+
'center' (default) or 'end' (actions sit on the title baseline).
|
|
27
|
+
• marginClass — space under the header. Defaults to 'mb-8'; pass
|
|
28
|
+
'' when the parent already manages vertical rhythm (space-y-*).
|
|
29
|
+
-->
|
|
30
|
+
<header :class="maxWidthClass ? ['mx-auto px-4 sm:px-6', maxWidthClass] : undefined">
|
|
31
|
+
<!--
|
|
32
|
+
flex-wrap + gap so a long title and the action buttons reflow onto
|
|
33
|
+
separate rows on narrow (mobile) viewports instead of the actions
|
|
34
|
+
overflowing off the right edge of the screen. min-w-0 on the title
|
|
35
|
+
group lets a long title truncate rather than shoving the actions out.
|
|
36
|
+
-->
|
|
37
|
+
<div
|
|
38
|
+
class="flex flex-wrap justify-between gap-3"
|
|
39
|
+
:class="[align === 'end' ? 'items-end' : 'items-center', marginClass]"
|
|
40
|
+
>
|
|
41
|
+
<div class="flex items-center gap-3 min-w-0">
|
|
42
|
+
<div
|
|
43
|
+
v-if="$slots.icon"
|
|
44
|
+
class="w-10 h-10 rounded-lg flex items-center justify-center"
|
|
45
|
+
:class="badgeBgClass"
|
|
46
|
+
>
|
|
47
|
+
<slot name="icon" :icon-class="iconTextClass" />
|
|
48
|
+
</div>
|
|
49
|
+
<div>
|
|
50
|
+
<h1 class="text-2xl font-bold" :class="t.primaryTextSoft">
|
|
51
|
+
{{ title }}
|
|
52
|
+
</h1>
|
|
53
|
+
<p v-if="subtitle || $slots.subtitle" class="text-sm" :class="t.dimTextAlt">
|
|
54
|
+
<slot name="subtitle">{{ subtitle }}</slot>
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="flex items-center gap-2">
|
|
59
|
+
<slot name="actions" />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</header>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<script setup lang="ts">
|
|
66
|
+
import { computed } from 'vue'
|
|
67
|
+
import { useTheme } from '../composables/useTheme'
|
|
68
|
+
import { useThemeClasses } from '../composables/useThemeClasses'
|
|
69
|
+
|
|
70
|
+
type IconColor = 'emerald' | 'sky' | 'red' | 'amber'
|
|
71
|
+
|
|
72
|
+
const props = withDefaults(
|
|
73
|
+
defineProps<{
|
|
74
|
+
title: string
|
|
75
|
+
subtitle?: string
|
|
76
|
+
iconColor?: IconColor
|
|
77
|
+
maxWidthClass?: string
|
|
78
|
+
align?: 'center' | 'end'
|
|
79
|
+
marginClass?: string
|
|
80
|
+
}>(),
|
|
81
|
+
{
|
|
82
|
+
subtitle: '',
|
|
83
|
+
iconColor: 'emerald',
|
|
84
|
+
maxWidthClass: 'max-w-4xl',
|
|
85
|
+
align: 'center',
|
|
86
|
+
marginClass: 'mb-8',
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const { isDark } = useTheme()
|
|
91
|
+
const t = useThemeClasses()
|
|
92
|
+
|
|
93
|
+
// Maps the named colour palette to the badge-background classes for
|
|
94
|
+
// dark/light themes. Callers just pass `iconColor="sky"` and the
|
|
95
|
+
// component resolves both modes.
|
|
96
|
+
const badgeBgClass = computed(() => {
|
|
97
|
+
const palette: Record<IconColor, { dark: string; light: string }> = {
|
|
98
|
+
emerald: { dark: 'bg-emerald-500/10', light: 'bg-emerald-50' },
|
|
99
|
+
sky: { dark: 'bg-sky-500/10', light: 'bg-sky-50' },
|
|
100
|
+
red: { dark: 'bg-red-500/10', light: 'bg-red-50' },
|
|
101
|
+
amber: { dark: 'bg-amber-500/10', light: 'bg-amber-50' },
|
|
102
|
+
}
|
|
103
|
+
const entry = palette[props.iconColor]
|
|
104
|
+
return isDark.value ? entry.dark : entry.light
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Icon text colour, routed through the same name → palette mapping so
|
|
108
|
+
// callers don't need to know the exact Tailwind shade per theme. Exposed
|
|
109
|
+
// to the `icon` slot via the `iconClass` slot prop.
|
|
110
|
+
const iconTextClass = computed(() => {
|
|
111
|
+
const palette: Record<IconColor, { dark: string; light: string }> = {
|
|
112
|
+
emerald: { dark: 'text-emerald-400', light: 'text-emerald-600' },
|
|
113
|
+
sky: { dark: 'text-sky-400', light: 'text-sky-600' },
|
|
114
|
+
red: { dark: 'text-red-400', light: 'text-red-500' },
|
|
115
|
+
amber: { dark: 'text-amber-400', light: 'text-amber-600' },
|
|
116
|
+
}
|
|
117
|
+
const entry = palette[props.iconColor]
|
|
118
|
+
return isDark.value ? entry.dark : entry.light
|
|
119
|
+
})
|
|
120
|
+
</script>
|