@alfakit/vue 0.1.1 → 0.1.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/CHANGELOG.md +8 -0
- package/dist/components/actions/ActionButton.js.map +1 -1
- package/dist/components/actions/ActionButton.vue.d.ts.map +1 -1
- package/dist/components/actions/ActionButton.vue_vue_type_script_setup_true_lang.js +160 -159
- package/dist/components/actions/ActionButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/actions/ActionModal.js.map +1 -1
- package/dist/components/actions/ActionModal.vue.d.ts.map +1 -1
- package/dist/components/actions/ActionModal.vue_vue_type_script_setup_true_lang.js +214 -214
- package/dist/components/actions/ActionModal.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/fields/ColorPickerField.js.map +1 -1
- package/dist/components/fields/ColorPickerField.vue.d.ts.map +1 -1
- package/dist/components/fields/ColorPickerField.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/components/fields/ColorPickerField.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/fields/ComboboxSelect.js.map +1 -1
- package/dist/components/fields/ComboboxSelect.vue.d.ts.map +1 -1
- package/dist/components/fields/ComboboxSelect.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/fields/ComboboxSelect.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/fields/FileUploadField.js.map +1 -1
- package/dist/components/fields/FileUploadField.vue.d.ts.map +1 -1
- package/dist/components/fields/FileUploadField.vue_vue_type_script_setup_true_lang.js +183 -183
- package/dist/components/fields/FileUploadField.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/fields/OneTimeCodeInputField.js.map +1 -1
- package/dist/components/fields/OneTimeCodeInputField.vue_vue_type_script_setup_true_lang.js +1 -2
- package/dist/components/fields/OneTimeCodeInputField.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/fields/SelectField.js.map +1 -1
- package/dist/components/fields/SelectField.vue.d.ts.map +1 -1
- package/dist/components/fields/SelectField.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/fields/SelectField.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/fields/TableColumnHeaders.js.map +1 -1
- package/dist/components/fields/TableColumnHeaders.vue.d.ts.map +1 -1
- package/dist/components/fields/TableColumnHeaders.vue_vue_type_script_setup_true_lang.js +6 -3
- package/dist/components/fields/TableColumnHeaders.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/file-manager/FileManagerView.js.map +1 -1
- package/dist/components/file-manager/FileManagerView.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/file-manager/FileManagerView.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/form/FormBlock.js.map +1 -1
- package/dist/components/form/FormBlock.vue.d.ts.map +1 -1
- package/dist/components/form/FormBlock.vue_vue_type_script_setup_true_lang.js +81 -77
- package/dist/components/form/FormBlock.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/form/form-context.js +25 -25
- package/dist/components/form/form-context.js.map +1 -1
- package/dist/components/layout/FusedGroupComponent.js.map +1 -1
- package/dist/components/layout/FusedGroupComponent.vue.d.ts.map +1 -1
- package/dist/components/layout/FusedGroupComponent.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/components/layout/FusedGroupComponent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/layout/TabsComponent.js.map +1 -1
- package/dist/components/layout/TabsComponent.vue.d.ts.map +1 -1
- package/dist/components/layout/TabsComponent.vue_vue_type_script_setup_true_lang.js +76 -55
- package/dist/components/layout/TabsComponent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/notifications/NotificationsMenu.js.map +1 -1
- package/dist/components/notifications/NotificationsMenu.vue.d.ts.map +1 -1
- package/dist/components/notifications/NotificationsMenu.vue_vue_type_script_setup_true_lang.js +44 -42
- package/dist/components/notifications/NotificationsMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/panel/GlobalSearch.js.map +1 -1
- package/dist/components/panel/GlobalSearch.vue.d.ts.map +1 -1
- package/dist/components/panel/GlobalSearch.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/components/panel/GlobalSearch.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/panel/ShortcutKeys.js.map +1 -1
- package/dist/components/panel/ShortcutKeys.vue.d.ts.map +1 -1
- package/dist/components/panel/ShortcutKeys.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/components/panel/ShortcutKeys.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/panel/TenantMenu.js.map +1 -1
- package/dist/components/panel/TenantMenu.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/panel/TenantMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/table/GroupHeaderRow.js.map +1 -1
- package/dist/components/table/GroupHeaderRow.vue.d.ts.map +1 -1
- package/dist/components/table/GroupHeaderRow.vue_vue_type_script_setup_true_lang.js +55 -34
- package/dist/components/table/GroupHeaderRow.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/table/SelectCell.js.map +1 -1
- package/dist/components/table/SelectCell.vue.d.ts.map +1 -1
- package/dist/components/table/SelectCell.vue_vue_type_script_setup_true_lang.js +24 -19
- package/dist/components/table/SelectCell.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/table/TextInputCell.js.map +1 -1
- package/dist/components/table/TextInputCell.vue.d.ts.map +1 -1
- package/dist/components/table/TextInputCell.vue_vue_type_script_setup_true_lang.js +38 -33
- package/dist/components/table/TextInputCell.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components.css +347 -0
- package/dist/eject-manifest.json +2 -2
- package/dist/standalone/AlfakitToaster.js.map +1 -1
- package/dist/standalone/AlfakitToaster.vue.d.ts.map +1 -1
- package/dist/standalone/AlfakitToaster.vue_vue_type_script_setup_true_lang.js +23 -15
- package/dist/standalone/AlfakitToaster.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/standalone.css +47 -0
- package/dist/starter-kit/widgets/SearchTrigger.js.map +1 -1
- package/dist/starter-kit/widgets/SearchTrigger.vue.d.ts.map +1 -1
- package/dist/starter-kit/widgets/SearchTrigger.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/starter-kit/widgets/SearchTrigger.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/starter-kit/widgets/TenantChooser.js.map +1 -1
- package/dist/starter-kit/widgets/TenantChooser.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/starter-kit/widgets/TenantChooser.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/themes.css +2 -334
- package/package.json +3 -3
- package/src/components/actions/ActionButton.vue +14 -6
- package/src/components/actions/ActionModal.vue +3 -1
- package/src/components/fields/ColorPickerField.vue +1 -0
- package/src/components/fields/ComboboxSelect.vue +1 -1
- package/src/components/fields/FileUploadField.vue +14 -11
- package/src/components/fields/OneTimeCodeInputField.vue +1 -1
- package/src/components/fields/SelectField.vue +4 -2
- package/src/components/fields/TableColumnHeaders.vue +6 -1
- package/src/components/file-manager/FileManagerView.vue +1 -1
- package/src/components/form/FormBlock.vue +24 -0
- package/src/components/form/form-context.ts +2 -2
- package/src/components/layout/FusedGroupComponent.vue +1 -0
- package/src/components/layout/TabsComponent.vue +61 -5
- package/src/components/notifications/NotificationsMenu.vue +11 -0
- package/src/components/panel/GlobalSearch.vue +1 -0
- package/src/components/panel/ShortcutKeys.vue +1 -0
- package/src/components/panel/TenantMenu.vue +2 -2
- package/src/components/table/GroupHeaderRow.vue +27 -0
- package/src/components/table/SelectCell.vue +5 -2
- package/src/components/table/TextInputCell.vue +11 -5
- package/src/standalone/AlfakitToaster.vue +12 -2
- package/src/starter-kit/widgets/SearchTrigger.vue +1 -0
- package/src/starter-kit/widgets/TenantChooser.vue +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfakit/vue",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Vue 3 + shadcn-vue renderer for Alfakit, the Laravel admin panel builder by Alfa Six.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alfa Six (https://alfasixbh.com)",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"reka-ui": "^2.11.0",
|
|
97
97
|
"tailwind-merge": "^3.7.0",
|
|
98
98
|
"vue-sonner": "^2.0.9",
|
|
99
|
-
"@alfakit/core": "0.1.
|
|
99
|
+
"@alfakit/core": "0.1.2"
|
|
100
100
|
},
|
|
101
101
|
"peerDependencies": {
|
|
102
102
|
"@inertiajs/vue3": "^3.0.0",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"provenance": true
|
|
120
120
|
},
|
|
121
121
|
"scripts": {
|
|
122
|
-
"build": "vite build && vue-tsc --declaration --emitDeclarationOnly -p tsconfig.build.json && cp src/styles.css dist/styles.css && cp ../core/themes.css dist/themes.css && cp ../core/standalone.css dist/standalone.css && node ../../scripts/eject-manifest.mjs .",
|
|
122
|
+
"build": "vite build && vue-tsc --declaration --emitDeclarationOnly -p tsconfig.build.json && cp src/styles.css dist/styles.css && cp ../core/themes.css dist/themes.css && cp ../core/components.css dist/components.css && cp ../core/standalone.css dist/standalone.css && node ../../scripts/eject-manifest.mjs .",
|
|
123
123
|
"dev": "vite build --watch",
|
|
124
124
|
"typecheck": "vue-tsc --noEmit",
|
|
125
125
|
"test": "vitest run"
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
bindsKeyBindings,
|
|
5
5
|
formatKeyBinding,
|
|
6
6
|
iconButtonSize,
|
|
7
|
+
isActionLoading,
|
|
8
|
+
isActionProcessing,
|
|
7
9
|
registerKeyBindings,
|
|
8
10
|
resolveActionSize,
|
|
9
11
|
} from '@alfakit/core'
|
|
@@ -47,9 +49,12 @@ const props = withDefaults(defineProps<ActionButtonProps>(), {
|
|
|
47
49
|
})
|
|
48
50
|
|
|
49
51
|
const runner = useActionRunner()
|
|
50
|
-
|
|
52
|
+
// Only this run (this record's button), not the same action in every table row.
|
|
53
|
+
const loading = useStoreSelector(runner.store, (state) =>
|
|
54
|
+
isActionLoading(state, props.action, props.context),
|
|
55
|
+
)
|
|
51
56
|
const processing = useStoreSelector(runner.store, (state) =>
|
|
52
|
-
state
|
|
57
|
+
isActionProcessing(state, props.action, props.context),
|
|
53
58
|
)
|
|
54
59
|
|
|
55
60
|
const variant = computed(
|
|
@@ -87,7 +92,7 @@ function buttonClasses(action: ActionSchema, kind: ActionSchema['variant']): str
|
|
|
87
92
|
return colorClass(ghostButtonColorClasses, color, 'gray')
|
|
88
93
|
case 'link':
|
|
89
94
|
return cn(
|
|
90
|
-
'h-auto p-0 underline-offset-4 hover:underline',
|
|
95
|
+
'h-auto p-0 underline-offset-4 hover:underline has-[>svg]:px-0',
|
|
91
96
|
colorClass(textColorClasses, color, 'primary'),
|
|
92
97
|
)
|
|
93
98
|
case 'icon-button':
|
|
@@ -278,9 +283,12 @@ const disabledTooltip = computed(() => Boolean(props.action.disabled && tooltip.
|
|
|
278
283
|
</TooltipTrigger>
|
|
279
284
|
<TooltipContent>
|
|
280
285
|
{{ tooltip }}
|
|
281
|
-
<kbd
|
|
282
|
-
|
|
283
|
-
|
|
286
|
+
<kbd
|
|
287
|
+
v-if="shortcut"
|
|
288
|
+
dir="ltr"
|
|
289
|
+
class="ms-2 rounded border border-white/20 px-1 font-mono text-[10px]"
|
|
290
|
+
>{{ formatKeyBinding(shortcut) }}</kbd
|
|
291
|
+
>
|
|
284
292
|
</TooltipContent>
|
|
285
293
|
</Tooltip>
|
|
286
294
|
<template v-else>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { OpenModal } from '@alfakit/core'
|
|
3
3
|
import {
|
|
4
4
|
actionFormComponent,
|
|
5
|
+
formIdPrefix,
|
|
5
6
|
actionFormParams,
|
|
6
7
|
confirmModalClose,
|
|
7
8
|
FormStore,
|
|
@@ -103,6 +104,8 @@ const form = (() => {
|
|
|
103
104
|
return new FormStore(schema.value.form, {
|
|
104
105
|
token: context.token,
|
|
105
106
|
component: actionFormComponent(action, context),
|
|
107
|
+
// Own field ids: the modal opens over a form whose fields may share names.
|
|
108
|
+
idPrefix: formIdPrefix(actionFormComponent(action, context)),
|
|
106
109
|
record: context.record ?? null,
|
|
107
110
|
refreshUrl: panel.value.endpoints.schemaRefresh,
|
|
108
111
|
optionsUrl: panel.value.endpoints.schemaOptions,
|
|
@@ -513,7 +516,6 @@ const hasBody = computed(() =>
|
|
|
513
516
|
</Button>
|
|
514
517
|
</div>
|
|
515
518
|
</DialogFooter>
|
|
516
|
-
<span class="sr-only">{{ t('close', {}, 'Close') }}</span>
|
|
517
519
|
</DialogContent>
|
|
518
520
|
</Dialog>
|
|
519
521
|
</template>
|
|
@@ -210,7 +210,7 @@ const removeTag = (event: Event, key: string) => {
|
|
|
210
210
|
)
|
|
211
211
|
"
|
|
212
212
|
>
|
|
213
|
-
<span class="flex flex-1 flex-wrap items-center gap-1 text-start">
|
|
213
|
+
<span class="flex min-w-0 flex-1 flex-wrap items-center gap-1 text-start">
|
|
214
214
|
<span v-if="selected.length === 0" class="text-muted-foreground">{{
|
|
215
215
|
props.component.placeholder ?? t('select_option', {}, 'Select an option')
|
|
216
216
|
}}</span>
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
fileUploadAccept,
|
|
23
23
|
filesFromClipboard,
|
|
24
24
|
fileUploadPaths,
|
|
25
|
-
|
|
25
|
+
fileManagerSize,
|
|
26
26
|
insertUploadedFile,
|
|
27
27
|
moveRepeaterItem,
|
|
28
28
|
requestJson,
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
} from '@lucide/vue'
|
|
44
44
|
import { computed, ref, shallowRef, watch } from 'vue'
|
|
45
45
|
import { useStore } from '../../composables/use-store'
|
|
46
|
-
import { useChoice, usePanel, useT } from '../../context'
|
|
46
|
+
import { useChoice, useFormatter, usePanel, useT } from '../../context'
|
|
47
47
|
import { cn } from '../../lib/utils'
|
|
48
48
|
import { useField, useFormStore } from '../form/form-context'
|
|
49
49
|
import FieldWrapper from '../schema/FieldWrapper.vue'
|
|
@@ -59,6 +59,7 @@ interface LocalFile extends UploadedFile {
|
|
|
59
59
|
|
|
60
60
|
const props = defineProps<{ component: FileUploadSchema }>()
|
|
61
61
|
const t = useT()
|
|
62
|
+
const formatter = useFormatter()
|
|
62
63
|
const choice = useChoice()
|
|
63
64
|
const panel = usePanel()
|
|
64
65
|
const {
|
|
@@ -424,7 +425,9 @@ const onItemDrop = (event: DragEvent, index: number) => {
|
|
|
424
425
|
const hints = computed(() =>
|
|
425
426
|
[
|
|
426
427
|
props.component.acceptedFileTypes?.join(', '),
|
|
427
|
-
props.component.maxSize
|
|
428
|
+
props.component.maxSize
|
|
429
|
+
? `≤ ${fileManagerSize(props.component.maxSize * 1024, formatter.value)}`
|
|
430
|
+
: null,
|
|
428
431
|
props.component.minFiles
|
|
429
432
|
? choice(
|
|
430
433
|
'form.min_files',
|
|
@@ -491,7 +494,7 @@ function cssLength(value: number | string): string {
|
|
|
491
494
|
type="button"
|
|
492
495
|
variant="ghost"
|
|
493
496
|
size="icon-xs"
|
|
494
|
-
:aria-label="t('remove', {}, 'Remove')"
|
|
497
|
+
:aria-label="t('file_upload.remove', { name: file.name }, 'Remove :name')"
|
|
495
498
|
@click="removeEntry(file)"
|
|
496
499
|
><XIcon
|
|
497
500
|
/></Button>
|
|
@@ -525,7 +528,7 @@ function cssLength(value: number | string): string {
|
|
|
525
528
|
: file.progress !== undefined && !file.error
|
|
526
529
|
? (props.component.uploadingMessage ?? t('form.uploading', {}, 'Uploading…'))
|
|
527
530
|
: file.size
|
|
528
|
-
?
|
|
531
|
+
? fileManagerSize(file.size, formatter)
|
|
529
532
|
: file.temporary
|
|
530
533
|
? t('form.uploading', {}, 'Pending')
|
|
531
534
|
: '')
|
|
@@ -541,7 +544,7 @@ function cssLength(value: number | string): string {
|
|
|
541
544
|
type="button"
|
|
542
545
|
variant="ghost"
|
|
543
546
|
size="icon-xs"
|
|
544
|
-
:aria-label="t('
|
|
547
|
+
:aria-label="t('file_upload.move_up', { name: file.name }, 'Move :name up')"
|
|
545
548
|
:disabled="index === 0"
|
|
546
549
|
@click="move(index, index - 1)"
|
|
547
550
|
><ChevronUpIcon
|
|
@@ -550,7 +553,7 @@ function cssLength(value: number | string): string {
|
|
|
550
553
|
type="button"
|
|
551
554
|
variant="ghost"
|
|
552
555
|
size="icon-xs"
|
|
553
|
-
:aria-label="t('
|
|
556
|
+
:aria-label="t('file_upload.move_down', { name: file.name }, 'Move :name down')"
|
|
554
557
|
:disabled="index === files.length - 1"
|
|
555
558
|
@click="move(index, index + 1)"
|
|
556
559
|
><ChevronDownIcon
|
|
@@ -562,7 +565,7 @@ function cssLength(value: number | string): string {
|
|
|
562
565
|
:variant="circle ? 'secondary' : 'ghost'"
|
|
563
566
|
size="icon-xs"
|
|
564
567
|
:class="cn(circle && 'rounded-full shadow')"
|
|
565
|
-
:aria-label="t('
|
|
568
|
+
:aria-label="t('file_upload.edit', { name: file.name }, 'Edit :name')"
|
|
566
569
|
:title="t('image_editor.title', {}, 'Edit image')"
|
|
567
570
|
data-action="edit-image"
|
|
568
571
|
@click="void editExisting(file)"
|
|
@@ -579,7 +582,7 @@ function cssLength(value: number | string): string {
|
|
|
579
582
|
:href="uploadedFileLinks(file).open"
|
|
580
583
|
target="_blank"
|
|
581
584
|
rel="noopener noreferrer"
|
|
582
|
-
:aria-label="`${t('open', {}, 'Open')} ${t('opens_in_new_tab', {}, '(opens in a new tab)')}`"
|
|
585
|
+
:aria-label="`${t('file_upload.open', { name: file.name }, 'Open :name')} ${t('opens_in_new_tab', {}, '(opens in a new tab)')}`"
|
|
583
586
|
><ExternalLinkIcon
|
|
584
587
|
/></a>
|
|
585
588
|
</Button>
|
|
@@ -593,7 +596,7 @@ function cssLength(value: number | string): string {
|
|
|
593
596
|
<a
|
|
594
597
|
:href="uploadedFileLinks(file).download"
|
|
595
598
|
download
|
|
596
|
-
:aria-label="t('download', {}, 'Download')"
|
|
599
|
+
:aria-label="t('file_upload.download', { name: file.name }, 'Download :name')"
|
|
597
600
|
><DownloadIcon
|
|
598
601
|
/></a>
|
|
599
602
|
</Button>
|
|
@@ -603,7 +606,7 @@ function cssLength(value: number | string): string {
|
|
|
603
606
|
:variant="circle ? 'secondary' : 'ghost'"
|
|
604
607
|
size="icon-xs"
|
|
605
608
|
:class="cn(circle && 'rounded-full shadow')"
|
|
606
|
-
:aria-label="t('remove', {}, 'Remove')"
|
|
609
|
+
:aria-label="t('file_upload.remove', { name: file.name }, 'Remove :name')"
|
|
607
610
|
@click="removeEntry(file)"
|
|
608
611
|
><XIcon
|
|
609
612
|
/></Button>
|
|
@@ -43,7 +43,6 @@ const onInput = (event: Event) => {
|
|
|
43
43
|
inputmode="numeric"
|
|
44
44
|
autocomplete="one-time-code"
|
|
45
45
|
pattern="[0-9]*"
|
|
46
|
-
:maxlength="props.component.length"
|
|
47
46
|
:model-value="value ?? ''"
|
|
48
47
|
:placeholder="props.component.placeholder"
|
|
49
48
|
:disabled="disabled"
|
|
@@ -54,6 +53,7 @@ const onInput = (event: Event) => {
|
|
|
54
53
|
:aria-describedby="aria['aria-describedby']"
|
|
55
54
|
:aria-required="aria['aria-required']"
|
|
56
55
|
data-slot="one-time-code-input"
|
|
56
|
+
data-input=""
|
|
57
57
|
class="max-w-56 text-center font-mono text-lg tracking-[0.5em] tabular-nums"
|
|
58
58
|
v-bind="props.component.extraInputAttributes"
|
|
59
59
|
@input="onInput"
|
|
@@ -62,7 +62,7 @@ const plainLabel = (label: string): string =>
|
|
|
62
62
|
@blur="blur"
|
|
63
63
|
>
|
|
64
64
|
<option v-if="props.component.selectablePlaceholder" value="">
|
|
65
|
-
{{ props.component.placeholder ?? '' }}
|
|
65
|
+
{{ props.component.placeholder ?? t('select_option', {}, 'Select an option') }}
|
|
66
66
|
</option>
|
|
67
67
|
<template
|
|
68
68
|
v-for="option in props.component.options"
|
|
@@ -97,7 +97,9 @@ const plainLabel = (label: string): string =>
|
|
|
97
97
|
:aria-readonly="readOnly || undefined"
|
|
98
98
|
class="w-full"
|
|
99
99
|
>
|
|
100
|
-
<SelectValue
|
|
100
|
+
<SelectValue
|
|
101
|
+
:placeholder="props.component.placeholder ?? t('select_option', {}, 'Select an option')"
|
|
102
|
+
/>
|
|
101
103
|
</SelectTrigger>
|
|
102
104
|
<SelectContent>
|
|
103
105
|
<SelectItem
|
|
@@ -5,6 +5,11 @@ import { cn } from '../../lib/utils'
|
|
|
5
5
|
|
|
6
6
|
/** Header cells shared by the Repeater and RepeatableEntry tables. */
|
|
7
7
|
const props = defineProps<{ columns: TableColumnSchema[] }>()
|
|
8
|
+
|
|
9
|
+
// A fixed `width()` (`9rem`, `120px`) is also the column's minimum, so the table scrolls
|
|
10
|
+
// sideways instead of squeezing its fields; percentages stay flexible.
|
|
11
|
+
const widthStyle = (width?: string | null) =>
|
|
12
|
+
!width ? undefined : width.trim().endsWith('%') ? { width } : { width, minWidth: width }
|
|
8
13
|
</script>
|
|
9
14
|
|
|
10
15
|
<template>
|
|
@@ -12,7 +17,7 @@ const props = defineProps<{ columns: TableColumnSchema[] }>()
|
|
|
12
17
|
v-for="(column, index) in props.columns"
|
|
13
18
|
:key="`${column.name ?? 'column'}-${index}`"
|
|
14
19
|
scope="col"
|
|
15
|
-
:style="column.width
|
|
20
|
+
:style="widthStyle(column.width)"
|
|
16
21
|
:class="
|
|
17
22
|
cn(
|
|
18
23
|
'text-muted-foreground px-3 py-2 text-xs font-medium',
|
|
@@ -436,7 +436,7 @@ const onPreviewKeyDown = (event: KeyboardEvent) => {
|
|
|
436
436
|
</label>
|
|
437
437
|
<nav
|
|
438
438
|
:aria-label="tt('breadcrumb', {}, 'Folder path')"
|
|
439
|
-
class="min-w-0 flex-
|
|
439
|
+
class="min-w-0 flex-[1_1_12rem]"
|
|
440
440
|
data-slot="file-manager-breadcrumb"
|
|
441
441
|
>
|
|
442
442
|
<ol class="flex min-w-0 flex-wrap items-center gap-1 text-sm">
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
registerFormSubmitter,
|
|
15
15
|
registerPageForm,
|
|
16
16
|
resolvePartialProps,
|
|
17
|
+
shouldResyncForm,
|
|
17
18
|
wizardProgressStore,
|
|
18
19
|
} from '@alfakit/core'
|
|
19
20
|
import { router } from '@inertiajs/vue3'
|
|
@@ -102,6 +103,10 @@ watch(token, (value) => store.setToken(value))
|
|
|
102
103
|
// block may arrive before or after Inertia's `success` event, so handle both orders.
|
|
103
104
|
let submittedBlock: FormBlockSchema | null = null
|
|
104
105
|
let refillPending = false
|
|
106
|
+
// Another action succeeded and its new block has not arrived yet.
|
|
107
|
+
let resyncPending = false
|
|
108
|
+
// The block when the latest non-GET visit started, to tell whether its response arrived.
|
|
109
|
+
let visitBlock: FormBlockSchema | null = null
|
|
105
110
|
|
|
106
111
|
watch(
|
|
107
112
|
() => props.block,
|
|
@@ -110,6 +115,12 @@ watch(
|
|
|
110
115
|
refillPending = false
|
|
111
116
|
store.fill(block)
|
|
112
117
|
}
|
|
118
|
+
|
|
119
|
+
if (resyncPending) {
|
|
120
|
+
resyncPending = false
|
|
121
|
+
|
|
122
|
+
if (!store.isDirty) store.fill(block)
|
|
123
|
+
}
|
|
113
124
|
},
|
|
114
125
|
)
|
|
115
126
|
|
|
@@ -153,11 +164,24 @@ const offStart = router.on('start', (event) => {
|
|
|
153
164
|
if (submitting && !submitVisit && visit.method !== 'get') {
|
|
154
165
|
submitVisit = visit.id
|
|
155
166
|
}
|
|
167
|
+
|
|
168
|
+
if (visit.method !== 'get') visitBlock = props.block
|
|
156
169
|
})
|
|
157
170
|
|
|
158
171
|
const offSuccess = router.on('success', (event) => {
|
|
159
172
|
const id = submitVisit
|
|
160
173
|
|
|
174
|
+
// Another action changed the record: show the server's new form unless it holds edits.
|
|
175
|
+
if (!id && shouldResyncForm(pageFlash(event.detail.page), store)) {
|
|
176
|
+
if (props.block !== visitBlock) {
|
|
177
|
+
store.fill(props.block)
|
|
178
|
+
} else {
|
|
179
|
+
resyncPending = true
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
161
185
|
if (!id || (event.detail.visitId ?? id) !== id) return
|
|
162
186
|
|
|
163
187
|
// A halted action (rate limit, `halt()`) saved nothing: keep what the user typed.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BaseField, FieldAriaAttributes, FormState, FormStore } from '@alfakit/core'
|
|
2
|
-
import { fieldAria } from '@alfakit/core'
|
|
2
|
+
import { fieldAria, fieldDomId } from '@alfakit/core'
|
|
3
3
|
import {
|
|
4
4
|
computed,
|
|
5
5
|
inject,
|
|
@@ -75,7 +75,7 @@ export function useField<T = unknown>(field: MaybeRefOrGetter<BaseField>): Field
|
|
|
75
75
|
// is in flight, so locking inputs would only swallow keystrokes.
|
|
76
76
|
const disabled = computed(() => Boolean(toValue(field).disabled))
|
|
77
77
|
const readOnly = computed(() => Boolean(toValue(field).readOnly))
|
|
78
|
-
const id = toValue(field).id ??
|
|
78
|
+
const id = toValue(field).id ?? fieldDomId(store.idPrefix, path)
|
|
79
79
|
const aria = computed(() => fieldAria(id, toValue(field), error.value))
|
|
80
80
|
|
|
81
81
|
return {
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { TabsSchema } from '@alfakit/core'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
componentHasErrors,
|
|
5
|
+
readPersisted,
|
|
6
|
+
schemaGapClass,
|
|
7
|
+
tabFromQuery,
|
|
8
|
+
tabQueryValue,
|
|
9
|
+
tabToReveal,
|
|
10
|
+
writePersisted,
|
|
11
|
+
} from '@alfakit/core'
|
|
4
12
|
import { usePage } from '@inertiajs/vue3'
|
|
5
13
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
14
|
+
import { useStoreSelector } from '../../composables/use-store'
|
|
15
|
+
import { useT } from '../../context'
|
|
6
16
|
import { Icon } from '../../lib/icons'
|
|
7
17
|
import { cn } from '../../lib/utils'
|
|
18
|
+
import { useOptionalFormStore } from '../form/form-context'
|
|
8
19
|
import SchemaRenderer from '../schema/SchemaRenderer.vue'
|
|
9
20
|
import Tabs from '../ui/Tabs.vue'
|
|
10
21
|
import TabsContent from '../ui/TabsContent.vue'
|
|
@@ -17,11 +28,15 @@ const props = defineProps<{ component: TabsSchema }>()
|
|
|
17
28
|
const tabs = computed(() => props.component.schema ?? [])
|
|
18
29
|
// The page URL (not `window.location`) so the server renders the same tab as the browser.
|
|
19
30
|
const page = usePage()
|
|
31
|
+
// The tab's readable value (`?tab=billing-address`), or its key in links from older versions.
|
|
20
32
|
const fromQuery = props.component.queryStringKey
|
|
21
|
-
?
|
|
33
|
+
? tabFromQuery(
|
|
34
|
+
tabs.value,
|
|
35
|
+
new URL(page.url, 'http://localhost').searchParams.get(props.component.queryStringKey),
|
|
36
|
+
)
|
|
22
37
|
: null
|
|
23
38
|
const active = ref<string>(
|
|
24
|
-
|
|
39
|
+
fromQuery ??
|
|
25
40
|
tabs.value[Math.max(0, props.component.activeTab - 1)]?.key ??
|
|
26
41
|
tabs.value[0]?.key ??
|
|
27
42
|
'',
|
|
@@ -39,6 +54,36 @@ onMounted(() => {
|
|
|
39
54
|
if (stored && tabs.value.some((tab) => tab.key === stored)) active.value = stored
|
|
40
55
|
})
|
|
41
56
|
|
|
57
|
+
// Tabs holding a field with a validation error are marked, and when a submit brings new
|
|
58
|
+
// errors while the open tab has none, the first tab with an error opens.
|
|
59
|
+
const TAB_INVALID_CLASSES =
|
|
60
|
+
'text-destructive data-[state=active]:text-destructive dark:text-destructive dark:data-[state=active]:text-destructive'
|
|
61
|
+
const t = useT()
|
|
62
|
+
const store = useOptionalFormStore()
|
|
63
|
+
const errors = useStoreSelector(
|
|
64
|
+
store?.store ?? {
|
|
65
|
+
get: () => ({ errors: {} as Record<string, string[]> }),
|
|
66
|
+
subscribe: () => () => {},
|
|
67
|
+
},
|
|
68
|
+
(state) => state.errors,
|
|
69
|
+
)
|
|
70
|
+
const invalidTabs = computed(
|
|
71
|
+
() =>
|
|
72
|
+
new Set(
|
|
73
|
+
tabs.value.filter((tab) => componentHasErrors(tab, errors.value)).map((tab) => tab.key),
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
watch(
|
|
78
|
+
errors,
|
|
79
|
+
(next, previous) => {
|
|
80
|
+
const reveal = tabToReveal(tabs.value, active.value, previous ?? {}, next)
|
|
81
|
+
|
|
82
|
+
if (reveal !== null) active.value = reveal
|
|
83
|
+
},
|
|
84
|
+
{ immediate: true },
|
|
85
|
+
)
|
|
86
|
+
|
|
42
87
|
function changeTab(value: string) {
|
|
43
88
|
active.value = value
|
|
44
89
|
|
|
@@ -52,8 +97,9 @@ watch(
|
|
|
52
97
|
(value) => {
|
|
53
98
|
if (!props.component.queryStringKey || typeof window === 'undefined') return
|
|
54
99
|
|
|
100
|
+
const tab = tabs.value.find((item) => item.key === value)
|
|
55
101
|
const url = new URL(window.location.href)
|
|
56
|
-
url.searchParams.set(props.component.queryStringKey, value)
|
|
102
|
+
url.searchParams.set(props.component.queryStringKey, tab ? tabQueryValue(tab) : value)
|
|
57
103
|
window.history.replaceState(window.history.state, '', url)
|
|
58
104
|
},
|
|
59
105
|
{ immediate: true },
|
|
@@ -87,11 +133,21 @@ watch(
|
|
|
87
133
|
)
|
|
88
134
|
"
|
|
89
135
|
>
|
|
90
|
-
<TabsTrigger
|
|
136
|
+
<TabsTrigger
|
|
137
|
+
v-for="tab in tabs"
|
|
138
|
+
:key="tab.key"
|
|
139
|
+
:value="tab.key"
|
|
140
|
+
:data-invalid="invalidTabs.has(tab.key) || undefined"
|
|
141
|
+
:class="invalidTabs.has(tab.key) ? TAB_INVALID_CLASSES : undefined"
|
|
142
|
+
>
|
|
91
143
|
<Icon v-if="tab.icon && tab.iconPosition !== 'after'" :name="tab.icon" />
|
|
92
144
|
{{ tab.label }}
|
|
93
145
|
<Icon v-if="tab.icon && tab.iconPosition === 'after'" :name="tab.icon" />
|
|
94
146
|
<TabBadge :tab="tab" />
|
|
147
|
+
<template v-if="invalidTabs.has(tab.key)">
|
|
148
|
+
<span aria-hidden="true" class="bg-destructive size-1.5 shrink-0 rounded-full"></span>
|
|
149
|
+
<span class="sr-only">({{ t('translatable.has_errors') }})</span>
|
|
150
|
+
</template>
|
|
95
151
|
</TabsTrigger>
|
|
96
152
|
</TabsList>
|
|
97
153
|
<TabsContent
|
|
@@ -67,6 +67,17 @@ watch(open, (isOpen) => {
|
|
|
67
67
|
if (isOpen) void center?.load()
|
|
68
68
|
})
|
|
69
69
|
|
|
70
|
+
// Every visit brings the server's unread count (another user after impersonating, or
|
|
71
|
+
// notifications read in another tab): follow it instead of waiting for the next poll.
|
|
72
|
+
watch(
|
|
73
|
+
() => config.value?.unreadCount,
|
|
74
|
+
(count) => {
|
|
75
|
+
if (center && typeof count === 'number' && count !== center.store.get().unreadCount) {
|
|
76
|
+
center.store.update({ unreadCount: count })
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
70
81
|
// Realtime notifications announce stored ones, so the unread count updates right away.
|
|
71
82
|
let stopListening: (() => void) | null = null
|
|
72
83
|
onMounted(() => {
|
|
@@ -154,7 +154,7 @@ const tenants = computed(() =>
|
|
|
154
154
|
<DropdownMenuItem v-else-if="tenancy.profileUrl" as-child>
|
|
155
155
|
<Link :href="tenancy.profileUrl" class="w-full"
|
|
156
156
|
><Icon name="alfakit::panels.tenant-menu.profile-button" :fallback="SettingsIcon" />
|
|
157
|
-
{{ t('tenancy.profile', {}, 'Settings') }}</Link
|
|
157
|
+
{{ tenancy.profileLabel ?? t('tenancy.profile', {}, 'Settings') }}</Link
|
|
158
158
|
>
|
|
159
159
|
</DropdownMenuItem>
|
|
160
160
|
<MenuItemEntries v-if="tenancy.menu" :items="tenancy.menu" menu="tenantMenu" />
|
|
@@ -162,7 +162,7 @@ const tenants = computed(() =>
|
|
|
162
162
|
<DropdownMenuItem v-else-if="tenancy.registrationUrl" as-child>
|
|
163
163
|
<Link :href="tenancy.registrationUrl" class="w-full"
|
|
164
164
|
><Icon name="alfakit::panels.tenant-menu.registration-button" :fallback="PlusIcon" />
|
|
165
|
-
{{ t('tenancy.register', {}, 'New') }}</Link
|
|
165
|
+
{{ tenancy.registrationLabel ?? t('tenancy.register', {}, 'New') }}</Link
|
|
166
166
|
>
|
|
167
167
|
</DropdownMenuItem>
|
|
168
168
|
</DropdownMenuContent>
|
|
@@ -3,6 +3,7 @@ import type { TableRecordGroupSchema } from '@alfakit/core'
|
|
|
3
3
|
import { ChevronDownIcon, ChevronRightIcon } from '@lucide/vue'
|
|
4
4
|
import { computed } from 'vue'
|
|
5
5
|
import { useT } from '../../context'
|
|
6
|
+
import { colorClass, textColorClasses } from '../../lib/colors'
|
|
6
7
|
import { Icon } from '../../lib/icons'
|
|
7
8
|
import Checkbox from '../ui/Checkbox.vue'
|
|
8
9
|
import TableCell from '../ui/TableCell.vue'
|
|
@@ -20,6 +21,11 @@ const props = defineProps<{
|
|
|
20
21
|
const emit = defineEmits<{ toggle: [] }>()
|
|
21
22
|
const t = useT()
|
|
22
23
|
|
|
24
|
+
// An enum group shows its `HasIcon` icon, in its `HasColor` colour, before the title.
|
|
25
|
+
const groupIconClass = computed(
|
|
26
|
+
() => `size-4 shrink-0 ${colorClass(textColorClasses, props.group.color, 'gray')}`,
|
|
27
|
+
)
|
|
28
|
+
|
|
23
29
|
const toggleLabel = computed(
|
|
24
30
|
() =>
|
|
25
31
|
`${props.collapsed ? t('table_grouping.expand', {}, 'Expand group') : t('table_grouping.collapse', {}, 'Collapse group')}: ${props.group.title}`,
|
|
@@ -56,6 +62,12 @@ const toggleLabel = computed(
|
|
|
56
62
|
class="size-4 shrink-0"
|
|
57
63
|
aria-hidden="true"
|
|
58
64
|
/>
|
|
65
|
+
<Icon
|
|
66
|
+
v-if="props.group.icon"
|
|
67
|
+
:name="props.group.icon"
|
|
68
|
+
:class="groupIconClass"
|
|
69
|
+
aria-hidden="true"
|
|
70
|
+
/>
|
|
59
71
|
<span class="flex flex-col items-start">
|
|
60
72
|
<span class="font-medium">{{ props.group.title }}</span>
|
|
61
73
|
<span
|
|
@@ -65,6 +77,21 @@ const toggleLabel = computed(
|
|
|
65
77
|
>
|
|
66
78
|
</span>
|
|
67
79
|
</button>
|
|
80
|
+
<span
|
|
81
|
+
v-else-if="props.group.icon"
|
|
82
|
+
data-slot="table-group-title"
|
|
83
|
+
class="flex items-center gap-2"
|
|
84
|
+
>
|
|
85
|
+
<Icon :name="props.group.icon" :class="groupIconClass" aria-hidden="true" />
|
|
86
|
+
<span class="flex flex-col items-start">
|
|
87
|
+
<span class="font-medium">{{ props.group.title }}</span>
|
|
88
|
+
<span
|
|
89
|
+
v-if="props.group.description"
|
|
90
|
+
class="text-muted-foreground text-xs font-normal"
|
|
91
|
+
>{{ props.group.description }}</span
|
|
92
|
+
>
|
|
93
|
+
</span>
|
|
94
|
+
</span>
|
|
68
95
|
<span v-else class="flex flex-col items-start">
|
|
69
96
|
<span class="font-medium">{{ props.group.title }}</span>
|
|
70
97
|
<span v-if="props.group.description" class="text-muted-foreground text-xs font-normal">{{
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { SelectColumnSchema } from '@alfakit/core'
|
|
3
3
|
import { isOptionGroup } from '@alfakit/core'
|
|
4
|
-
import { computed, ref } from 'vue'
|
|
4
|
+
import { computed, ref, useId } from 'vue'
|
|
5
5
|
import { cn } from '../../lib/utils'
|
|
6
6
|
import { useCellUpdate, type CellProps } from './cell-update'
|
|
7
7
|
|
|
8
8
|
const props = defineProps<CellProps>()
|
|
9
9
|
const schema = computed(() => props.column as SelectColumnSchema)
|
|
10
10
|
const { update, saving, error } = useCellUpdate(props)
|
|
11
|
+
const errorId = useId()
|
|
11
12
|
const value = ref(
|
|
12
13
|
props.cell.state === null || props.cell.state === undefined ? '' : String(props.cell.state),
|
|
13
14
|
)
|
|
@@ -29,7 +30,9 @@ const onChange = (event: Event) => {
|
|
|
29
30
|
<select
|
|
30
31
|
:value="value"
|
|
31
32
|
:disabled="Boolean(props.cell.disabled) || saving"
|
|
33
|
+
:aria-label="props.column.label || props.column.name"
|
|
32
34
|
:aria-invalid="error ? true : undefined"
|
|
35
|
+
:aria-describedby="error ? errorId : undefined"
|
|
33
36
|
:class="
|
|
34
37
|
cn(
|
|
35
38
|
'border-input dark:bg-input/30 focus-visible:ring-ring/50 h-8 min-w-32 rounded-md border bg-transparent px-2 text-sm shadow-xs outline-none focus-visible:ring-[3px]',
|
|
@@ -48,6 +51,6 @@ const onChange = (event: Event) => {
|
|
|
48
51
|
{{ option.label }}
|
|
49
52
|
</option>
|
|
50
53
|
</select>
|
|
51
|
-
<span v-if="error" class="text-destructive block text-xs">{{ error }}</span>
|
|
54
|
+
<span v-if="error" :id="errorId" class="text-destructive block text-xs">{{ error }}</span>
|
|
52
55
|
</span>
|
|
53
56
|
</template>
|