@falcondev-oss/nuxt-layers-base 0.40.3 → 0.41.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 (62) hide show
  1. package/app/app.tsx +14 -0
  2. package/app/assets/css/base.css +6 -0
  3. package/app/components/layout/LayoutAuth.tsx +22 -0
  4. package/app/components/layout/LayoutNavbar.tsx +193 -0
  5. package/app/components/layout/LayoutPage.tsx +183 -0
  6. package/app/components/layout/LayoutSidebar.tsx +167 -0
  7. package/app/components/modals/ConfirmModal.tsx +80 -0
  8. package/app/components/modals/EntityCrudModal.tsx +124 -0
  9. package/app/components/overlay/modal/ModalActions.tsx +36 -0
  10. package/app/components/overlay/modal/ModalConfirm.tsx +46 -0
  11. package/app/components/overlay/modal/ModalRadioGroup.tsx +62 -0
  12. package/app/components/u/UActions.tsx +30 -0
  13. package/app/components/u/UAuthForm.tsx +30 -0
  14. package/app/components/u/UCustomApp.tsx +33 -0
  15. package/app/components/u/UField.tsx +211 -0
  16. package/app/components/u/UForm.tsx +133 -0
  17. package/app/components/u/UImageWithFallback.tsx +47 -0
  18. package/app/components/u/UInputDatePicker.tsx +94 -0
  19. package/app/components/u/UInputDateRangePicker.tsx +143 -0
  20. package/app/components/u/UInputDurationMinutes.tsx +106 -0
  21. package/app/components/u/UInputFile.tsx +132 -0
  22. package/app/components/u/URibbonCard.tsx +54 -0
  23. package/app/components/u/URibbonSection.tsx +36 -0
  24. package/app/components/u/USwitchCard.tsx +64 -0
  25. package/app/components/u/UToggleButton.tsx +46 -0
  26. package/app/components/util/Define.tsx +21 -0
  27. package/app/components/util/SlotRef.tsx +21 -0
  28. package/app/components/util/Sync.tsx +37 -0
  29. package/app/components/util/TemplateText.tsx +31 -0
  30. package/app/composables/confirm.ts +1 -1
  31. package/app/error.tsx +24 -0
  32. package/app/utils/define-setup-component.ts +39 -5
  33. package/nuxt.config.ts +8 -0
  34. package/package.json +4 -3
  35. package/app/app.vue +0 -3
  36. package/app/components/layout/LayoutAuth.vue +0 -13
  37. package/app/components/layout/LayoutNavbar.vue +0 -140
  38. package/app/components/layout/LayoutPage.vue +0 -174
  39. package/app/components/layout/LayoutSidebar.vue +0 -117
  40. package/app/components/modals/ConfirmModal.vue +0 -65
  41. package/app/components/modals/EntityCrudModal.vue +0 -109
  42. package/app/components/overlay/modal/ModalActions.vue +0 -25
  43. package/app/components/overlay/modal/ModalConfirm.vue +0 -25
  44. package/app/components/overlay/modal/ModalRadioGroup.vue +0 -37
  45. package/app/components/u/UActions.vue +0 -23
  46. package/app/components/u/UAuthForm.vue +0 -36
  47. package/app/components/u/UCustomApp.vue +0 -23
  48. package/app/components/u/UField.vue +0 -191
  49. package/app/components/u/UForm.vue +0 -96
  50. package/app/components/u/UImageWithFallback.vue +0 -35
  51. package/app/components/u/UInputDatePicker.vue +0 -79
  52. package/app/components/u/UInputDateRangePicker.vue +0 -100
  53. package/app/components/u/UInputDurationMinutes.vue +0 -90
  54. package/app/components/u/UInputFile.vue +0 -127
  55. package/app/components/u/URibbonCard.vue +0 -43
  56. package/app/components/u/URibbonSection.vue +0 -29
  57. package/app/components/u/USwitchCard.vue +0 -51
  58. package/app/components/util/Define.vue +0 -13
  59. package/app/components/util/SlotRef.vue +0 -17
  60. package/app/components/util/Sync.vue +0 -24
  61. package/app/components/util/TemplateText.vue +0 -20
  62. package/app/error.vue +0 -15
@@ -1,191 +0,0 @@
1
- <script setup lang="ts" generic="T">
2
- import type { FormField } from '@falcondev-oss/form-core'
3
- import type { FormFieldProps, FormFieldSlots } from '@nuxt/ui'
4
- import { createReusableTemplate } from '@vueuse/core'
5
- import { useForwardProps } from 'reka-ui'
6
- import * as R from 'remeda'
7
- import { mergeSlotClass } from '../../utils/ui'
8
-
9
- type InputProps<T> = {
10
- 'modelValue': T
11
- 'onUpdate:modelValue': (value: T) => void
12
- 'onBlur': () => void
13
- 'disabled': boolean
14
- 'loading': boolean
15
- 'modelModifiers': { nullable: true }
16
- 'placeholder'?: string
17
- }
18
-
19
- const props = defineProps<
20
- FormFieldProps & {
21
- field: FormField<T>
22
- errorInline?: boolean
23
- errorPreferPlaceholder?: boolean
24
- }
25
- >()
26
-
27
- const slots = defineSlots<
28
- {
29
- default: (slot: {
30
- bind: InputProps<T>
31
- model: WritableComputedRef<T>
32
- field: FormField<T>
33
- }) => any
34
- } & Omit<FormFieldSlots, 'default'>
35
- >()
36
-
37
- const forwardedProps = useForwardProps(props)
38
-
39
- const isOverMaxLength = computed(() => {
40
- const field = forwardedProps.value.field
41
-
42
- return field.schema.maxLength === undefined || field.value == null
43
- ? false
44
- : (field.value as string | number)?.toString().length > field.schema.maxLength
45
- })
46
-
47
- const formFieldProps = computed<FormFieldProps>(() => {
48
- const { field, ...rest } = forwardedProps.value
49
-
50
- const hint =
51
- field.schema.maxLength === undefined || field.schema.maxLength === field.schema.minLength
52
- ? undefined
53
- : `${(field.value as string | number | null)?.toString().length ?? 0}/${field.schema.maxLength}`
54
-
55
- return {
56
- required: field.schema.required,
57
- label: field.schema.title,
58
- description: field.schema.description,
59
- hint,
60
- ...R.omitBy(rest, (v) => v === undefined),
61
- }
62
- })
63
-
64
- const bind = computed(() => {
65
- const field = forwardedProps.value.field
66
-
67
- const placeholder = (field.errors && field.errors[0]) || field.schema.default?.toString()
68
-
69
- return {
70
- 'modelValue': field.value,
71
- 'onUpdate:modelValue': (value) => field.handleChange(value),
72
- 'onBlur': () => field.handleBlur(),
73
- 'disabled': field.disabled,
74
- 'loading': field.isPending,
75
- placeholder,
76
- 'modelModifiers': { nullable: true },
77
- } satisfies InputProps<T>
78
- })
79
-
80
- const model = computed({
81
- get() {
82
- return forwardedProps.value.field.value
83
- },
84
- set(value: T) {
85
- forwardedProps.value.field.handleChange(value)
86
- },
87
- })
88
-
89
- const model_ = { model }
90
-
91
- const [DefineErrorTemplate, ErrorTemplate] = createReusableTemplate({
92
- props: {
93
- errors: Array as PropType<string[]>,
94
- },
95
- })
96
- </script>
97
-
98
- <template>
99
- <UFormField
100
- v-bind="formFieldProps"
101
- :ui="{
102
- ...formFieldProps.ui,
103
- hint: mergeSlotClass(formFieldProps.ui?.hint, isOverMaxLength ? 'text-error' : ''),
104
- error: mergeSlotClass(formFieldProps.ui?.error, 'mt-1!'),
105
- }"
106
- :error="!!field.errors"
107
- :class="props.class"
108
- >
109
- <DefineErrorTemplate v-slot="{ errors }">
110
- <p v-if="errors.length === 1">
111
- {{ errors.join('\n') }}
112
- </p>
113
-
114
- <ul v-else>
115
- <li v-for="(error, index) in errors" :key="error + index" class="list-inside">
116
- {{ error }}
117
- </li>
118
- </ul>
119
- </DefineErrorTemplate>
120
-
121
- <template
122
- v-if="
123
- field.errors &&
124
- errorInline &&
125
- (errorPreferPlaceholder ? String(field.value).length > 0 : true)
126
- "
127
- #error
128
- >
129
- <ErrorTemplate :errors="field.errors" />
130
-
131
- <template v-if="typeof error === 'string'">
132
- {{ error }}
133
- </template>
134
- </template>
135
-
136
- <template v-else #hint="{ hint }">
137
- <span class="flex items-center gap-1.5">
138
- <UPopover
139
- v-if="!!field.errors && (errorPreferPlaceholder ? String(field.value).length > 0 : true)"
140
- mode="hover"
141
- :open-delay="0"
142
- :ui="{
143
- content: 'bg-error-50 ring-error-200! rounded py-1 px-2',
144
- }"
145
- >
146
- <UIcon name="lucide:circle-alert" class="text-error" />
147
- <template #content>
148
- <div class="max-w-sm text-xs text-(--ui-color-neutral-800)">
149
- <ErrorTemplate :errors="field.errors" />
150
- </div>
151
- </template>
152
- </UPopover>
153
-
154
- {{ hint }}
155
- </span>
156
- </template>
157
-
158
- <template v-if="field.schema.examples" #help>
159
- <template v-if="Array.isArray(field.schema.examples)">
160
- <ul>
161
- <li v-for="(example, index) in field.schema.examples" :key="index">
162
- {{ example }}
163
- </li>
164
- </ul>
165
- </template>
166
- <p v-else>
167
- {{ field.schema.examples }}
168
- </p>
169
- </template>
170
-
171
- <slot v-bind="{ bind, model: model_.model, field: forwardedProps.field }">
172
- <DevOnly>
173
- <p class="font-black text-red-500">UField missing slot</p>
174
- </DevOnly>
175
- </slot>
176
-
177
- <template v-for="(_, name) in R.omit(slots, ['default'])" #[name]="slotData">
178
- <!-- @vue-ignore -->
179
- <slot :name="name" v-bind="slotData || {}" />
180
- </template>
181
- </UFormField>
182
- </template>
183
-
184
- <style scoped>
185
- :deep([aria-invalid='true']) {
186
- &::placeholder {
187
- color: var(--ui-color-error-500);
188
- opacity: 0.5;
189
- }
190
- }
191
- </style>
@@ -1,96 +0,0 @@
1
- <script setup lang="ts">
2
- import type { FormHandle } from '@falcondev-oss/form-core'
3
- import type { ButtonProps } from '@nuxt/ui'
4
- import type { Toast } from '#ui/composables'
5
-
6
- const props = withDefaults(
7
- defineProps<{
8
- submitLabel?: string
9
- submitButtonProps?: ButtonProps
10
- actions?: ButtonProps[]
11
- actionsTeleportTo?: string
12
- form: FormHandle
13
- disableSubmitIfUnchanged?: boolean
14
- successToast?: Partial<Toast>
15
- preventPageLeave?: boolean
16
- }>(),
17
- {
18
- disableSubmitIfUnchanged: true,
19
- preventPageLeave: true,
20
- },
21
- )
22
-
23
- defineSlots<{
24
- default: any
25
- }>()
26
-
27
- usePreventPageLeave(() => props.preventPageLeave && props.form.isChanged && !props.form.isLoading)
28
-
29
- const toast = useToast()
30
- let unhook: (() => void) | null = null
31
- watch(
32
- () => props.form,
33
- (form) => {
34
- unhook?.()
35
- unhook = form.hooks.addHooks({
36
- afterSubmit(result) {
37
- if (!result.success || !props.successToast) return
38
-
39
- toast.add({
40
- preset: 'success',
41
- ...props.successToast,
42
- })
43
- },
44
- })
45
- },
46
- { immediate: true },
47
- )
48
-
49
- const actionsWithSubmit = computed(() => {
50
- const submit = {
51
- ...props.submitButtonProps,
52
- variant: 'solid',
53
- label: props.submitButtonProps?.label ?? props.submitLabel ?? 'Submit',
54
- disabled: props.disableSubmitIfUnchanged
55
- ? !props.form.isChanged || props.form.isLoading
56
- : props.form.isLoading,
57
- loading: props.form.isLoading,
58
- onClick: async () => {
59
- await props.form.submit()
60
- },
61
- } satisfies ButtonProps
62
- if (!props.actions) return [submit]
63
-
64
- return [...props.actions, submit]
65
- })
66
-
67
- const rootErrors = computed(() => props.form.errors?.filter((error) => error.path?.length === 0))
68
- </script>
69
-
70
- <template>
71
- <form class="w-full" @submit.prevent="() => form.submit()">
72
- <slot />
73
- <div
74
- v-show="rootErrors?.length || !actionsTeleportTo"
75
- class="col-span-full flex w-full flex-col gap-4"
76
- >
77
- <ul v-if="rootErrors?.length" class="text-error text-sm">
78
- <li v-for="error of rootErrors" :key="`${error.path}:${error.message}`">
79
- {{ error.path }}:{{ error.message }}
80
- </li>
81
- </ul>
82
-
83
- <hr class="h-px w-full text-(--ui-border-muted)" />
84
-
85
- <div v-show="!actionsTeleportTo" class="flex items-center justify-end gap-4">
86
- <Teleport defer :disabled="!actionsTeleportTo" :to="actionsTeleportTo">
87
- <UActions
88
- :defaults="{ variant: 'subtle' }"
89
- :actions="actionsWithSubmit"
90
- class="contents!"
91
- />
92
- </Teleport>
93
- </div>
94
- </div>
95
- </form>
96
- </template>
@@ -1,35 +0,0 @@
1
- <script setup lang="ts">
2
- import { useImage } from '@vueuse/core'
3
-
4
- const props = defineProps<{
5
- src?: string
6
- alt?: string
7
- fallbackSrc?: string
8
- }>()
9
-
10
- defineSlots<{
11
- default: () => any
12
- }>()
13
-
14
- const { error, isReady } = props.src
15
- ? useImage(() => ({
16
- src: props.src ?? '',
17
- }))
18
- : {
19
- isReady: false,
20
- error: true,
21
- }
22
- </script>
23
-
24
- <template>
25
- <img v-if="isReady" :src="src" :alt="alt" />
26
- <USkeleton v-else-if="!error" />
27
- <div v-else>
28
- <slot name="default">
29
- <!-- eslint-disable-next-line vue/no-duplicate-attr-inheritance -->
30
- <img v-if="fallbackSrc" :src="fallbackSrc" v-bind="$attrs" />
31
- <!-- eslint-disable-next-line vue/no-duplicate-attr-inheritance -->
32
- <USkeleton v-else v-bind="$attrs" />
33
- </slot>
34
- </div>
35
- </template>
@@ -1,79 +0,0 @@
1
- <script setup lang="ts">
2
- import type { DateValue } from '@internationalized/date'
3
- import type { CalendarProps, InputDateProps } from '@nuxt/ui'
4
-
5
- defineProps<{
6
- input?: InputDateProps
7
- calendar?: CalendarProps
8
- disabled?: boolean
9
- loading?: boolean
10
- placeholder?: string
11
- }>()
12
-
13
- const emit = defineEmits<{
14
- blur: []
15
- }>()
16
-
17
- const inputDate = useTemplateRef('inputDate')
18
-
19
- const model = defineModel<DateValue | null>({ required: true })
20
- const open = ref(false)
21
-
22
- watch(open, () => {
23
- if (!open.value) emit('blur')
24
- })
25
- </script>
26
-
27
- <template>
28
- <UInputDate
29
- ref="inputDate"
30
- v-bind="input"
31
- :disabled
32
- :loading
33
- :range="false"
34
- :model-value="model"
35
- @blur="() => emit('blur')"
36
- @update:model-value="
37
- (val) => {
38
- model = val ?? null
39
- }
40
- "
41
- >
42
- <template #trailing>
43
- <UPopover v-model:open="open" :reference="inputDate?.inputsRef[3]?.$el">
44
- <UButton color="neutral" variant="link" size="sm" icon="i-lucide-calendar" class="px-0" />
45
-
46
- <template #content>
47
- <div class="p-2">
48
- <UCalendar
49
- v-bind="calendar"
50
- :model-value="model ?? undefined"
51
- :multiple="false"
52
- :range="false"
53
- @update:model-value="
54
- (val) => {
55
- model = val ?? null
56
- open = false
57
- }
58
- "
59
- />
60
- <div v-if="model" class="flex justify-end">
61
- <UButton
62
- variant="ghost"
63
- icon="tabler:trash"
64
- color="error"
65
- label="Löschen"
66
- size="sm"
67
- @click="
68
- () => {
69
- model = null
70
- }
71
- "
72
- />
73
- </div>
74
- </div>
75
- </template>
76
- </UPopover>
77
- </template>
78
- </UInputDate>
79
- </template>
@@ -1,100 +0,0 @@
1
- <script setup lang="ts">
2
- import type { CalendarDate, DateValue } from '@internationalized/date'
3
- import { toCalendarDate } from '@internationalized/date'
4
-
5
- const inputDate = useTemplateRef('inputDate')
6
-
7
- const model = defineModel<{
8
- start: CalendarDate
9
- end: CalendarDate
10
- } | null>({
11
- required: true,
12
- })
13
-
14
- const localModel = shallowRef({
15
- // eslint-disable-next-line vue/no-ref-object-reactivity-loss
16
- start: model.value?.start,
17
- // eslint-disable-next-line vue/no-ref-object-reactivity-loss
18
- end: model.value?.end,
19
- } as
20
- | {
21
- start: DateValue | undefined
22
- end: DateValue | undefined
23
- }
24
- | undefined
25
- | null)
26
-
27
- const open = ref(false)
28
-
29
- let syncingModelValue = false
30
- watch(
31
- localModel,
32
- (newValue) => {
33
- // prevent infinite loop
34
- if (syncingModelValue) return
35
-
36
- if (newValue?.start && newValue.end) {
37
- model.value = {
38
- start: toCalendarDate(newValue.start),
39
- end: toCalendarDate(newValue.end),
40
- }
41
- open.value = false
42
- } else if (!newValue?.start && !newValue?.end) {
43
- model.value = null
44
- open.value = false
45
- }
46
- },
47
- {
48
- flush: 'sync',
49
- },
50
- )
51
-
52
- watch(model, (newValue) => {
53
- syncingModelValue = true
54
- if (newValue) {
55
- localModel.value = {
56
- start: newValue.start,
57
- end: newValue.end,
58
- }
59
- } else {
60
- localModel.value = {
61
- start: undefined,
62
- end: undefined,
63
- }
64
- }
65
- syncingModelValue = false
66
- })
67
- </script>
68
-
69
- <template>
70
- <UInputDate ref="inputDate" v-model="localModel" range>
71
- <template #trailing>
72
- <UPopover v-model:open="open" :reference="inputDate?.inputsRef[0]?.$el">
73
- <UButton color="neutral" variant="link" size="sm" icon="i-lucide-calendar" class="px-0" />
74
-
75
- <template #content>
76
- <div class="p-2">
77
- <UCalendar v-model="localModel" :number-of-months="2" range />
78
- <div v-if="model" class="flex justify-end">
79
- <UButton
80
- variant="ghost"
81
- icon="tabler:trash"
82
- color="error"
83
- label="Löschen"
84
- size="sm"
85
- @click="
86
- () => {
87
- localModel = {
88
- start: undefined,
89
- end: undefined,
90
- }
91
- }
92
- "
93
- />
94
- </div>
95
- </div>
96
- </template>
97
- </UPopover>
98
- </template>
99
- </UInputDate>
100
- </template>
@@ -1,90 +0,0 @@
1
- <script setup lang="ts">
2
- import type { InputEmits, InputProps } from '@nuxt/ui'
3
- import type { MaskOptions } from 'maska'
4
- import { regex } from 'arkregex'
5
- import { vMaska } from 'maska/vue'
6
- import { useForwardPropsEmits } from 'reka-ui'
7
-
8
- type Props = Omit<InputProps<string>, 'modelValue' | 'defaultValue' | 'modelModifiers'> & {
9
- showZeroMinutes?: boolean
10
- }
11
-
12
- const props = defineProps<Props>()
13
- const emit = defineEmits<Omit<InputEmits<string>, 'update:modelValue'>>()
14
- const forwardedProps = useForwardPropsEmits(props as Props, emit)
15
-
16
- const model = defineModel<number | null>({
17
- required: true,
18
- })
19
-
20
- const parser = regex('^-?(?<hours>\\d{1,}):?(?<minutes>\\d{1,2})?$')
21
- const duration = computed({
22
- get: () => {
23
- if (model.value === null) return null
24
-
25
- const absoluteMinutes = Math.abs(model.value)
26
-
27
- const hours = Math.floor(absoluteMinutes / 60)
28
- const minutes = absoluteMinutes % 60
29
- const sign = model.value < 0 ? '-' : ''
30
-
31
- // don't always add :00 if minutes is 0
32
- if (minutes > 0 || props.showZeroMinutes)
33
- return `${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`
34
-
35
- return `${sign}${hours.toString()}`
36
- },
37
- set: (val) => {
38
- if (!val) {
39
- model.value = null
40
- return
41
- }
42
-
43
- const parsed = parser.exec(val)
44
- if (!parsed) {
45
- model.value = null
46
- return
47
- }
48
-
49
- const isNegative = val.startsWith('-')
50
-
51
- const hours = Number.parseInt(parsed.groups.hours) * 60
52
- const minutes = parsed.groups.minutes ? Number.parseInt(parsed.groups.minutes) : 0
53
-
54
- model.value = isNegative ? (hours + minutes) * -1 : hours + minutes
55
- },
56
- })
57
-
58
- const maskOptions: MaskOptions = {
59
- // S: sign, H: hours (unlimited), 5: tens digit of minutes, D: digit of minutes
60
- mask: 'SH:5D',
61
- tokens: {
62
- 'S': { pattern: /[-+]/, optional: true },
63
- 'D': { pattern: /\d/ },
64
- 'H': { pattern: /\d/, multiple: true },
65
- '5': {
66
- pattern: /[0-5]/,
67
- transform(char) {
68
- // clamp char to max 5, otherwise maska would just block input if user tries to input 6-9
69
- const num = Number.parseInt(char)
70
- if (Number.isNaN(num)) return char
71
- return num > 5 ? '5' : char
72
- },
73
- },
74
- },
75
- }
76
- </script>
77
-
78
- <template>
79
- <UInput
80
- v-bind="forwardedProps"
81
- v-model="duration"
82
- v-maska="maskOptions"
83
- :model-modifiers="{
84
- nullable: true,
85
- lazy: true,
86
- }"
87
- placeholder="HH:mm"
88
- trailing-icon="lucide:timer"
89
- />
90
- </template>