@konoma-development/vue-components 0.0.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.
Files changed (55) hide show
  1. package/.nuxtrc +1 -0
  2. package/.playground/app.vue +64 -0
  3. package/.playground/eslint.config.ts +3 -0
  4. package/.playground/nuxt.config.ts +9 -0
  5. package/.tool-versions +1 -0
  6. package/.vscode/extensions.json +9 -0
  7. package/.vscode/settings.json +12 -0
  8. package/README.md +70 -0
  9. package/app.config.ts +13 -0
  10. package/components/KonomaTheme.vue +92 -0
  11. package/components/defaults/button.ts +19 -0
  12. package/components/defaults/checkbox.ts +13 -0
  13. package/components/defaults/checkboxList.ts +9 -0
  14. package/components/defaults/columnChooser.ts +8 -0
  15. package/components/defaults/input.ts +16 -0
  16. package/components/defaults/pagination.ts +10 -0
  17. package/components/defaults/radioButtonGroup.ts +13 -0
  18. package/components/defaults/select.ts +20 -0
  19. package/components/defaults/table.ts +16 -0
  20. package/components/defaults/tabs.ts +10 -0
  21. package/components/defaults/tag.ts +7 -0
  22. package/components/defaults/tagList.ts +14 -0
  23. package/components/defaults/textarea.ts +16 -0
  24. package/components/form/KonomaCheckbox.vue +68 -0
  25. package/components/form/KonomaCheckboxList.vue +42 -0
  26. package/components/form/KonomaForm.vue +71 -0
  27. package/components/form/KonomaFormField.vue +36 -0
  28. package/components/form/KonomaInput.vue +92 -0
  29. package/components/form/KonomaPhoneInput.vue +9 -0
  30. package/components/form/KonomaRadioButtonGroup.vue +41 -0
  31. package/components/form/KonomaSelect.vue +9 -0
  32. package/components/form/KonomaTagList.vue +55 -0
  33. package/components/form/KonomaTextarea.vue +81 -0
  34. package/components/form/injectionKeys.ts +8 -0
  35. package/components/table/KonomaColumnChooser.vue +64 -0
  36. package/components/table/KonomaColumnChooserEntry.vue +18 -0
  37. package/components/table/KonomaPagination.vue +81 -0
  38. package/components/table/KonomaTable.vue +355 -0
  39. package/components/table/KonomaTableActionEntry.vue +27 -0
  40. package/components/table/KonomaTableActions.vue +32 -0
  41. package/components/ui/KonomaButton.vue +109 -0
  42. package/components/ui/KonomaIcon.vue +13 -0
  43. package/components/ui/KonomaLoadingIndicator.vue +14 -0
  44. package/components/ui/KonomaModal.vue +120 -0
  45. package/components/ui/KonomaTabs.vue +70 -0
  46. package/components/ui/KonomaTag.vue +49 -0
  47. package/composables/useKonomaTheme.ts +5 -0
  48. package/eslint.config.ts +43 -0
  49. package/index.d.ts +33 -0
  50. package/nuxt.config.ts +20 -0
  51. package/package.json +60 -0
  52. package/tsconfig.json +11 -0
  53. package/types/form.ts +149 -0
  54. package/types/table.ts +33 -0
  55. package/unocss.config.ts +83 -0
@@ -0,0 +1,355 @@
1
+ <template>
2
+ <div :class="wrapperClasses">
3
+ <div :style="`gridTemplateRows: repeat(${data.length + 1}, auto)`" :class="[tableClasses, 'grid auto-rows-auto grid-flow-row'].join(' ')" @scroll="emit('onScroll', $event as UIEvent)">
4
+ <!-- Header -->
5
+ <div
6
+ :key="locale" class="sticky top-0 z-1 flex flex-row items-center justify-between rounded-t-kvc-table bg-kvc-table-header"
7
+ >
8
+ <div v-if="currentColumnsLeft.length" class="sticky left-0 z-1 flex flex-row">
9
+ <div
10
+ v-for="column in currentColumnsLeft"
11
+ :key="column.id"
12
+ :style="`minWidth: ${column.initialWidth}; maxWidth: ${!column.grow ? column.initialWidth : undefined};`"
13
+ :class="[
14
+ headerClasses,
15
+ hasFilters ? 'h-24' : 'h-12',
16
+ column.sortKey ? 'cursor-pointer' : '',
17
+ column.grow ? 'grow' : '',
18
+ ].join(' ')"
19
+ @click="() => {
20
+ if (column.sortKey) {
21
+ emit('onSort', Object.assign({}, column, {
22
+ sorting: column.sorting ? ({ '+': '-', '-': undefined }[column.sorting] as '+' | '-' | undefined) : '+',
23
+ }))
24
+ }
25
+ }"
26
+ >
27
+ <div class="w-full flex flex-row items-center justify-between gap-2">
28
+ <span>{{ column.title }}</span>
29
+ <div v-if="column.sortKey">
30
+ <KonomaIcon
31
+ class-name="h-4 w-4"
32
+ :name="column.sorting
33
+ ? {
34
+ '+': 'heroicons:chevron-down-16-solid',
35
+ '-': 'heroicons:chevron-up-16-solid',
36
+ }[column.sorting]
37
+ : 'heroicons:chevron-up-down-16-solid'"
38
+ />
39
+ </div>
40
+ </div>
41
+ <div v-if="hasFilters" :key="Object.keys(filters).join('-')" class="min-h-10 w-full bg-kvc-table-header text-xs text-secondary-500 font-medium" @click.stop>
42
+ <template v-if="column.filterable && column.filterKey">
43
+ <component :is="column.filterComponent?.(filters, updateFilters) || filterComponents?.[column.id]?.(filters, updateFilters)" v-if="column.filterComponent?.(filters, updateFilters) || filterComponents?.[column.id]?.(filters, updateFilters)" />
44
+ <KonomaInput
45
+ v-else
46
+ :key="filters[column.filterKey]?.join(', ')"
47
+ :value="filters[column.filterKey]?.join(', ')"
48
+ is-clearable
49
+ class="h-10"
50
+ icon-right-name="filters[column.filterKey] ? 'heroicons:x-mark' : ''"
51
+ @key-down="async ($event) => {
52
+ if ($event.key === 'Enter' && column.filterKey) {
53
+ const key = column.filterKey;
54
+ const value = ($event.currentTarget as HTMLInputElement).value;
55
+ if ($event) {
56
+ await updateFilters({ ...filters, [key]: [value] });
57
+ }
58
+ else {
59
+ const newFilters = { ...filters };
60
+ delete newFilters[key];
61
+ await updateFilters(newFilters);
62
+ }
63
+ }
64
+ }"
65
+ @click.stop
66
+ @icon-right-click.stop="async ($event) => {
67
+ $event.stopPropagation();
68
+ const key = column.filterKey;
69
+ if (key) {
70
+ const newFilters = { ...filters };
71
+ delete newFilters[key];
72
+ await updateFilters(newFilters);
73
+ }
74
+ }"
75
+ />
76
+ </template>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ <div
81
+ v-for="column in currentColumnsCenter" :key="column.id"
82
+ :style="`minWidth: ${column.initialWidth}; maxWidth: ${!column.grow ? column.initialWidth : undefined};`"
83
+ :class="[
84
+ headerClasses,
85
+ hasFilters ? 'h-24' : 'h-12',
86
+ column.sortKey ? 'cursor-pointer' : '',
87
+ column.grow ? 'grow' : '',
88
+ ].join(' ')"
89
+ @click="() => {
90
+ if (column.sortKey) {
91
+ emit('onSort', Object.assign({}, column, {
92
+ sorting: column.sorting ? ({ '+': '-', '-': undefined }[column.sorting] as '+' | '-' | undefined) : '+',
93
+ }))
94
+ }
95
+ }"
96
+ >
97
+ <div class="w-full flex flex-row items-center justify-between gap-2">
98
+ <span>{{ column.title }}</span>
99
+ <div v-if="column.sortKey">
100
+ <KonomaIcon
101
+ class-name="h-4 w-4"
102
+ :name="column.sorting
103
+ ? {
104
+ '+': 'heroicons:chevron-down-16-solid',
105
+ '-': 'heroicons:chevron-up-16-solid',
106
+ }[column.sorting]
107
+ : 'heroicons:chevron-up-down-16-solid'"
108
+ />
109
+ </div>
110
+ </div>
111
+ <div v-if="hasFilters" :key="Object.keys(filters).join('-')" class="min-h-10 w-full bg-kvc-table-header text-xs text-secondary-500 font-medium" @click.stop>
112
+ <template v-if="column.filterable && column.filterKey">
113
+ <component :is="column.filterComponent?.(filters, updateFilters) || filterComponents?.[column.id]?.(filters, updateFilters)" v-if="column.filterComponent?.(filters, updateFilters) || filterComponents?.[column.id]?.(filters, updateFilters)" />
114
+ <KonomaInput
115
+ v-else
116
+ :key="filters[column.filterKey]?.join(', ')"
117
+ :value="filters[column.filterKey]?.join(', ')"
118
+ is-clearable
119
+ class="h-10"
120
+ icon-right-name="filters[column.filterKey] ? 'heroicons:x-mark' : ''"
121
+ @key-down="async ($event) => {
122
+ if ($event.key === 'Enter' && column.filterKey) {
123
+ const key = column.filterKey;
124
+ const value = ($event.currentTarget as HTMLInputElement).value;
125
+ if ($event) {
126
+ await updateFilters({ ...filters, [key]: [value] });
127
+ }
128
+ else {
129
+ const newFilters = { ...filters };
130
+ delete newFilters[key];
131
+ await updateFilters(newFilters);
132
+ }
133
+ }
134
+ }"
135
+ @click.stop
136
+ @icon-right-click.stop="async ($event) => {
137
+ $event.stopPropagation();
138
+ const key = column.filterKey;
139
+ if (key) {
140
+ const newFilters = { ...filters };
141
+ delete newFilters[key];
142
+ await updateFilters(newFilters);
143
+ }
144
+ }"
145
+ />
146
+ </template>
147
+ </div>
148
+ </div>
149
+ <div v-if="currentColumnsRight.length" class="sticky right-0 h-full flex flex-row items-center">
150
+ <div
151
+ v-for="column in currentColumnsRight"
152
+ :key="column.id" :style="`minWidth: ${column.initialWidth}; maxWidth: ${!column.grow ? column.initialWidth : undefined};`" class="h-full flex flex-row items-start justify-end truncate bg-kvc-table-header px-4 py-3 text-xs font-medium last:rounded-tr-kvc-table"
153
+ >
154
+ {{ column.title }}
155
+ </div>
156
+ </div>
157
+ </div>
158
+ <!-- Body -->
159
+ <!-- TODO: Reorderable items -->
160
+ <div v-if="allowReorder" />
161
+ <template v-else>
162
+ <div v-for="entry, i in data" :key="i" :class="rowClasses" @click="emit('onRowClick', entry)" @dblclick="emit('onRowDoubleClick', entry)">
163
+ <div v-if="currentColumnsLeft.length" class="sticky left-0 flex flex-row border-r">
164
+ <div
165
+ v-for="column in currentColumnsRight"
166
+ :key="column.id"
167
+ :style="`minWidth: ${column.initialWidth}; maxWidth: ${!column.grow ? column.initialWidth : undefined};`"
168
+ :class="[rowLeftWrapperClasses, column.grow ? 'grow' : ''].join(' ')"
169
+ :title="(entry[column.id] as string) || ''"
170
+ >
171
+ <component
172
+ :is="cellRenderer?.[column.id]?.(entry)"
173
+ v-if="cellRenderer?.[column.id]?.(entry)"
174
+ />
175
+ <div v-else class="h-14 truncate p-4 text-sm">
176
+ {{ (entry[column.id] as string) || '-' }}
177
+ </div>
178
+ </div>
179
+ </div>
180
+ <div
181
+ v-for="column in currentColumnsCenter"
182
+ :key="column.id"
183
+ :style="`minWidth: ${column.initialWidth}; maxWidth: ${!column.grow ? column.initialWidth : undefined};`"
184
+ :class="[rowCenterWrapperClasses, column.grow ? 'grow' : ''].join(' ')"
185
+ :title="(entry[column.id] as string) || ''"
186
+ >
187
+ <component
188
+ :is="cellRenderer?.[column.id]?.(entry)"
189
+ v-if="cellRenderer?.[column.id]?.(entry)"
190
+ />
191
+ <div v-else class="h-14 truncate p-4 text-sm">
192
+ {{ (entry[column.id] as string) || '-' }}
193
+ </div>
194
+ </div>
195
+ <div v-if="currentColumnsRight.length" class="sticky right-0 flex flex-row border-l">
196
+ <div
197
+ v-for="column in currentColumnsRight"
198
+ :key="column.id"
199
+ :style="`minWidth: ${column.initialWidth}; maxWidth: ${!column.grow ? column.initialWidth : undefined};`"
200
+ :class="[rowRightWrapperClasses, column.grow ? 'grow' : ''].join(' ')"
201
+ :title="(entry[column.id] as string) || ''"
202
+ >
203
+ <component
204
+ :is="cellRenderer?.[column.id]?.(entry)"
205
+ v-if="cellRenderer?.[column.id]?.(entry)"
206
+ />
207
+ <div v-else class="h-14 truncate p-4 text-sm">
208
+ {{ (entry[column.id] as string) || '-' }}
209
+ </div>
210
+ </div>
211
+ </div>
212
+ </div>
213
+ </template>
214
+ <div v-if="data.length === 0" :class="noDataClasses">
215
+ {{ noEntryLabel }}
216
+ </div>
217
+ </div>
218
+ <KonomaPagination
219
+ v-if="pagination && xToY && data.length > 0"
220
+ :show-buttons="!isInfinite" :x-to-y="xToY"
221
+ :current-total="totalRows"
222
+ :current-loaded="data.length"
223
+ :current-start="currentStart"
224
+ :current-end="currentEnd"
225
+ :current-page="currentPage"
226
+ :total-pages="totalPages"
227
+ v-bind="paginationClasses"
228
+ @on-first-page="() => emit('onFirstPage')"
229
+ @on-previous-page="() => emit('onPreviousPage')"
230
+ @on-next-page="() => emit('onNextPage')"
231
+ @on-last-page="() => emit('onLastPage')"
232
+ @to-page="(page) => emit('toPage', page)"
233
+ />
234
+ </div>
235
+ </template>
236
+
237
+ <script lang="ts" setup generic="DataType extends { index?: number }">
238
+ import type { PaginationClasses, TableColumn } from '../../types/table';
239
+ import { baseClasses } from '../defaults/table';
240
+ import KonomaInput from '../form/KonomaInput.vue';
241
+ import KonomaIcon from '../ui/KonomaIcon.vue';
242
+ import KonomaPagination from './KonomaPagination.vue';
243
+
244
+ const props = withDefaults(defineProps<{
245
+ wrapperClasses?: string
246
+ tableClasses?: string
247
+ rowClasses?: string
248
+ rowLeftWrapperClasses?: string
249
+ rowCenterWrapperClasses?: string
250
+ rowRightWrapperClasses?: string
251
+ columnsWrapperClasses?: string
252
+ columnsLeftClasses?: string
253
+ columnsCenterClasses?: string
254
+ columnsRightClasses?: string
255
+ headerClasses?: string
256
+ noDataClasses?: string
257
+ currentPage?: number
258
+ totalPagesProp?: number
259
+ paginationClasses?: PaginationClasses
260
+ columnsCenter: TableColumn<DataType>[]
261
+ columnsRight?: TableColumn<DataType>[]
262
+ columnsLeft?: TableColumn<DataType>[]
263
+ // TODO: Custom cell renderers
264
+ cellRenderer?: { [key in keyof DataType]?: (data: DataType & { dragRef?: Ref }) => string }
265
+ // TODO: Custom filter components
266
+ filterComponents?: {
267
+ [key in keyof DataType]?: (
268
+ filters: Record<string, string[]>,
269
+ setFilters: (filters: Record<string, string[]>) => Promise<void>
270
+ ) => string;
271
+ }
272
+ filters?: Record<string, string[]>
273
+ showFilters?: boolean
274
+ data: DataType[]
275
+ pagination?: boolean
276
+ totalRows: number
277
+ noEntryLabel?: string
278
+ allowReorder?: boolean
279
+ xToY?: string
280
+ isInfinite?: boolean
281
+ pagesize?: number
282
+ }>(), {
283
+ noDataClasses: baseClasses.noDataClasses,
284
+ wrapperClasses: baseClasses.wrapperClasses,
285
+ tableClasses: baseClasses.tableClasses,
286
+ rowClasses: baseClasses.rowClasses,
287
+ rowLeftWrapperClasses: baseClasses.rowLeftWrapperClasses,
288
+ rowCenterWrapperClasses: baseClasses.rowCenterWrapperClasses,
289
+ rowRightWrapperClasses: baseClasses.rowRightWrapperClasses,
290
+ headerClasses: baseClasses.headerClasses,
291
+ filters: () => ({} as Record<string, string[]>),
292
+ currentPage: 0,
293
+ pagesize: 10,
294
+ isInfinite: true,
295
+ })
296
+
297
+ const emit = defineEmits<{
298
+ (e: 'onDragRow', dragIndex: number, hoverIndex: number): void
299
+ (e: 'onDropRow', dragIndex: number, hoverIndex: number): void
300
+ (e: 'onScroll', event: UIEvent): void
301
+ (e: 'onFirstPage'): void
302
+ (e: 'onPreviousPage'): void
303
+ (e: 'onNextPage'): void
304
+ (e: 'onLastPage'): void
305
+ (e: 'toPage', page: number): void
306
+ (e: 'onRowClick', data: DataType): void
307
+ (e: 'onRowDoubleClick', data: DataType): void
308
+ (e: 'onSort', column: TableColumn<DataType>): void
309
+ (e: 'onUpdateFilters', filters: Record<string, string[]>): Promise<void>
310
+ (e: 'onUpdateColumnsLeft', columns: TableColumn<DataType>[], updateMeta?: boolean): void
311
+ (e: 'onUpdateColumnsCenter', columns: TableColumn<DataType>[], updateMeta?: boolean): void
312
+ (e: 'onUpdateColumnsRight', columns: TableColumn<DataType>[], updateMeta?: boolean): void
313
+ }>()
314
+
315
+ const locale = inject<string>('locale')
316
+
317
+ const hasFilters = computed(() => {
318
+ return !!(
319
+ props.showFilters
320
+ && (props.columnsCenter.some(column => column.filterable)
321
+ || props.columnsLeft?.some(column => column.filterable)
322
+ || props.columnsRight?.some(column => column.filterable))
323
+ )
324
+ });
325
+
326
+ const totalPages = computed(() => {
327
+ return props.totalPagesProp || Math.ceil(props.totalRows / props.pagesize);
328
+ });
329
+
330
+ const currentStart = computed(() => {
331
+ return (props.currentPage - 1) * props.pagesize;
332
+ });
333
+
334
+ const currentEnd = computed(() => {
335
+ return props.currentPage * props.pagesize;
336
+ });
337
+
338
+ // const headerRef = useTemplateRef('header');
339
+
340
+ const currentColumnsLeft = computed(() => {
341
+ return props.columnsLeft?.filter(column => !column?.hidden) || []
342
+ });
343
+
344
+ const currentColumnsCenter = computed(() => {
345
+ return props.columnsCenter.filter(column => !column?.hidden)
346
+ });
347
+
348
+ const currentColumnsRight = computed(() => {
349
+ return props.columnsRight?.filter(column => !column?.hidden) || []
350
+ });
351
+
352
+ async function updateFilters(newFilters: Record<string, string[]>) {
353
+ await emit('onUpdateFilters', newFilters);
354
+ }
355
+ </script>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <span v-if="variant === 'error'" :class="errorClasses">
3
+ {{ text }}
4
+ </span>
5
+ <span v-else :class="defaultClasses">
6
+ {{ text }}
7
+ </span>
8
+ </template>
9
+
10
+ <script lang="ts" setup>
11
+ type TableActionVariant = 'error' | 'success' | 'warning' | 'default';
12
+
13
+ withDefaults(defineProps<{
14
+ text: string
15
+ errorClasses?: string
16
+ defaultClasses?: string
17
+ variant?: TableActionVariant
18
+ }>(), {
19
+ errorClasses: 'cursor-pointer px-4 py-2 text-error-900 hover:bg-error-100',
20
+ defaultClasses: 'cursor-pointer px-4 py-2 text-secondary-900 hover:bg-primary-100',
21
+ variant: 'default',
22
+ })
23
+
24
+ defineEmits<{
25
+ (e: 'click'): void
26
+ }>()
27
+ </script>
@@ -0,0 +1,32 @@
1
+ <template>
2
+ <div v-bind="$props">
3
+ <Dropdown>
4
+ <div class="cursor-pointer">
5
+ <div>
6
+ <KonomaIcon
7
+ name="heroicons:ellipsis-vertical-16-solid"
8
+ class="h-5 w-5"
9
+ />
10
+ </div>
11
+ </div>
12
+ <template #popper>
13
+ <div class="w-48 flex flex-col border border-secondary-200 rounded-md bg-white py-2 text-sm font-medium shadow-sm">
14
+ <slot />
15
+ </div>
16
+ </template>
17
+ </Dropdown>
18
+ </div>
19
+ </template>
20
+
21
+ <script lang="ts" setup>
22
+ import { Dropdown } from 'floating-vue';
23
+ import KonomaIcon from '../ui/KonomaIcon.vue';
24
+
25
+ withDefaults(defineProps<{
26
+ class?: string
27
+ }>(), {
28
+ class: 'h-14 p-4',
29
+ })
30
+
31
+ // const actionsVisible = ref(false)
32
+ </script>
@@ -0,0 +1,109 @@
1
+ <template>
2
+ <button
3
+ :disabled="disabled" :type="type" :class="combinedClasses.join(' ')"
4
+ @click="async ($event) => await emit('click', $event)"
5
+ >
6
+ <KonomaIcon v-if="iconLeftPath || iconLeftName" :class="iconLeftClasses" :name="iconLeftName" :path="iconLeftPath" />
7
+ <span v-if="label">{{ label }}</span>
8
+ <KonomaIcon v-if="iconRightPath || iconRightName" :class="iconRightClasses" :name="iconRightName" :path="iconRightPath" />
9
+ <KonomaLoadingIndicator v-if="loading" :class="combinedLoadingClasses.join(' ')" />
10
+ </button>
11
+ </template>
12
+
13
+ <script lang="ts" setup>
14
+ import { baseClasses } from '../defaults/button'
15
+ import KonomaIcon from './KonomaIcon.vue';
16
+ import KonomaLoadingIndicator from './KonomaLoadingIndicator.vue';
17
+
18
+ const props = withDefaults(defineProps<{
19
+ classesBase?: string
20
+ classesPrimary?: string
21
+ classesSecondary?: string
22
+ classesActiveSecondary?: string
23
+ classesError?: string
24
+ classesAlert?: string
25
+ loadingClassesBase?: string
26
+ loadingClassesPrimary?: string
27
+ loadingClassesSecondary?: string
28
+ loadingClassesActiveSecondary?: string
29
+ loadingClassesError?: string
30
+ loadingClassesAlert?: string
31
+ iconLeftClasses?: string
32
+ iconRightClasses?: string
33
+ disabled?: boolean
34
+ class?: string
35
+ type?: 'button' | 'submit' | 'reset'
36
+ variant: 'primary' | 'secondary' | 'error' | 'alert' | 'active-secondary'
37
+ label: string
38
+ loading?: boolean
39
+ iconLeftPath?: string
40
+ iconLeftName?: string
41
+ iconRightPath?: string
42
+ iconRightName?: string
43
+ }>(), {
44
+ class: '',
45
+ classesBase: baseClasses.classesBase,
46
+ classesPrimary: baseClasses.classesPrimary,
47
+ classesSecondary: baseClasses.classesSecondary,
48
+ classesActiveSecondary: baseClasses.classesActiveSecondary,
49
+ classesError: baseClasses.classesError,
50
+ classesAlert: baseClasses.classesAlert,
51
+ loadingClassesBase: baseClasses.loadingClassesBase,
52
+ loadingClassesPrimary: baseClasses.loadingClassesPrimary,
53
+ loadingClassesSecondary: baseClasses.loadingClassesSecondary,
54
+ loadingClassesActiveSecondary: baseClasses.loadingClassesActiveSecondary,
55
+ loadingClassesError: baseClasses.loadingClassesError,
56
+ loadingClassesAlert: baseClasses.loadingClassesAlert,
57
+ iconLeftClasses: baseClasses.iconLeftClasses,
58
+ iconRightClasses: baseClasses.iconRightClasses,
59
+ type: 'button',
60
+ })
61
+
62
+ const emit = defineEmits<{
63
+ (e: 'click', e2: MouseEvent): void | Promise<void>
64
+ }>()
65
+
66
+ const combinedLoadingClasses = computed(() => {
67
+ const classes = [props.loadingClassesBase];
68
+ switch (props.variant) {
69
+ case 'primary':
70
+ classes.push(props.loadingClassesPrimary);
71
+ break;
72
+ case 'secondary':
73
+ classes.push(props.loadingClassesSecondary);
74
+ break;
75
+ case 'active-secondary':
76
+ classes.push(props.loadingClassesActiveSecondary);
77
+ break;
78
+ case 'error':
79
+ classes.push(props.loadingClassesError);
80
+ break;
81
+ case 'alert':
82
+ classes.push(props.loadingClassesAlert);
83
+ break;
84
+ }
85
+ return classes
86
+ })
87
+
88
+ const combinedClasses = computed(() => {
89
+ const classes = [props.classesBase, props.class];
90
+ switch (props.variant) {
91
+ case 'primary':
92
+ classes.push(props.classesPrimary);
93
+ break;
94
+ case 'secondary':
95
+ classes.push(props.classesSecondary);
96
+ break;
97
+ case 'active-secondary':
98
+ classes.push(props.classesActiveSecondary);
99
+ break;
100
+ case 'error':
101
+ classes.push(props.classesError);
102
+ break;
103
+ case 'alert':
104
+ classes.push(props.classesAlert);
105
+ break;
106
+ }
107
+ return classes
108
+ })
109
+ </script>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <svg v-if="path">
3
+ <use :href="path" />
4
+ </svg>
5
+ <Icon v-else-if="name" :name="name" />
6
+ </template>
7
+
8
+ <script lang="ts" setup>
9
+ defineProps<{
10
+ name?: string
11
+ path?: string
12
+ }>()
13
+ </script>
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <svg aria-hidden="true" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+ <path
4
+ d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
5
+ fill="currentColor"
6
+ />
7
+ <path
8
+ d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
9
+ fill="currentFill"
10
+ />
11
+ </svg>
12
+ </template>
13
+
14
+ <script lang="ts" setup></script>
@@ -0,0 +1,120 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <div
4
+ :class="backdropClasses"
5
+ role="presentation"
6
+ @mousedown="handleBackdropClick"
7
+ >
8
+ <div
9
+ :class="contentClasses"
10
+ role="presentation"
11
+ @mousedown.stop
12
+ >
13
+ <!-- TODO: Handle close and header simultaneously -->
14
+ <div v-if="hasCloseIcon" :class="closeWrapperClasses">
15
+ <button @click="emit('close')">
16
+ <KonomaIcon name="heroicons:x-mark-16-solid" :class="iconClasses" />
17
+ </button>
18
+ </div>
19
+
20
+ <!-- Header content -->
21
+ <div v-if="headerContent || title" :class="headerWrapperClasses">
22
+ <slot v-if="headerContent" name="header" />
23
+ <span v-else-if="title" :class="titleClasses">{{ title }}</span>
24
+ </div>
25
+
26
+ <!-- Main content -->
27
+ <div class="grow">
28
+ <slot />
29
+ </div>
30
+
31
+ <!-- Footer content -->
32
+ <div v-if="footerContent || footerActions?.length" :class="footerWrapperClasses">
33
+ <slot v-if="footerContent" name="footer" />
34
+ <template v-else-if="footerActions?.length">
35
+ <div :class="footerLeftClasses">
36
+ <div
37
+ v-for="(action, i) in leftActions"
38
+ :key="i"
39
+ >
40
+ <KonomaButton
41
+ :variant="action.variant || 'primary'"
42
+ :label="action.label"
43
+ @click="action.onClick"
44
+ />
45
+ </div>
46
+ </div>
47
+ <div :class="footerRightClasses">
48
+ <div
49
+ v-for="(action, i) in rightActions"
50
+ :key="i"
51
+ >
52
+ <KonomaButton
53
+ :variant="action.variant || 'secondary'"
54
+ :label="action.label"
55
+ @click="action.onClick"
56
+ />
57
+ </div>
58
+ </div>
59
+ </template>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </Teleport>
64
+ </template>
65
+
66
+ <script lang="ts" setup>
67
+ import KonomaButton from './KonomaButton.vue'
68
+ import KonomaIcon from './KonomaIcon.vue'
69
+
70
+ interface FooterAction {
71
+ label: string
72
+ variant?: 'primary' | 'secondary' | 'error' | 'alert' | 'active-secondary'
73
+ position: 'left' | 'right'
74
+ onClick: () => void
75
+ }
76
+
77
+ const props = withDefaults(defineProps<{
78
+ backdropClasses?: string
79
+ contentClasses?: string
80
+ headerWrapperClasses?: string
81
+ footerWrapperClasses?: string
82
+ footerLeftClasses?: string
83
+ footerRightClasses?: string
84
+ titleClasses?: string
85
+ closeWrapperClasses?: string
86
+ iconClasses?: string
87
+ title?: string
88
+ headerContent?: any
89
+ footerContent?: any
90
+ footerActions?: FooterAction[]
91
+ hasCloseIcon?: boolean
92
+ }>(), {
93
+ backdropClasses: 'fixed inset-0 z-10 flex items-center justify-center bg-black/50',
94
+ contentClasses: 'rounded-kvc-modal bg-white shadow-lg flex flex-col',
95
+ headerWrapperClasses: 'w-full h-16 px-6 bg-primary-900 rounded-t justify-start items-center flex',
96
+ footerWrapperClasses: 'w-full pb-4 px-6 bg-white rounded-b justify-between items-center flex flex-row',
97
+ footerLeftClasses: 'flex flex-row gap-4 justify-start',
98
+ footerRightClasses: 'grow flex flex-row gap-4 justify-end',
99
+ titleClasses: 'text-white text-base font-semibold',
100
+ closeWrapperClasses: 'flex justify-between border-b border-secondary-200 p-4',
101
+ iconClasses: 'h-5 w-5 cursor-pointer',
102
+ hasCloseIcon: false,
103
+ })
104
+
105
+ const emit = defineEmits<{
106
+ (e: 'close'): void
107
+ }>()
108
+
109
+ const leftActions = computed(() =>
110
+ props.footerActions?.filter(action => action.position === 'left') || [],
111
+ )
112
+
113
+ const rightActions = computed(() =>
114
+ props.footerActions?.filter(action => action.position === 'right') || [],
115
+ )
116
+
117
+ function handleBackdropClick() {
118
+ emit('close')
119
+ }
120
+ </script>