@falcondev-oss/nuxt-layers-base 0.21.0 → 0.23.0

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.
@@ -7,12 +7,14 @@ const overlay = useOverlay()
7
7
 
8
8
  const form = useForm({
9
9
  schema: z.object({
10
- duration: z.number(),
11
- dateIso: z.string(),
10
+ duration: z.number().meta({ title: 'Duration' }),
11
+ dateIso: z.string().meta({ title: 'Datum' }),
12
+ text: z.string().max(10).meta({ title: 'Text' }),
12
13
  }),
13
14
  sourceValues: () => ({
14
15
  dateIso: null,
15
16
  duration: null,
17
+ text: '',
16
18
  }),
17
19
  async submit({ values }) {
18
20
  await new Promise((resolve) => setTimeout(resolve, 2000))
@@ -67,86 +69,85 @@ const columns = useTableColumns<typeof data>(
67
69
  </script>
68
70
 
69
71
  <template>
70
- <LayoutDashboard
71
- :sidebar="{
72
- items: [
73
- {
74
- label: 'Home',
75
- icon: 'i-lucide-house',
76
- active: true,
77
- },
78
- {
79
- label: 'Inbox',
80
- icon: 'i-lucide-inbox',
81
- badge: '4',
82
- },
83
- {
84
- label: 'Contacts',
85
- icon: 'i-lucide-users',
86
- },
87
- {
88
- label: 'Settings',
89
- icon: 'i-lucide-settings',
90
- defaultOpen: true,
91
- children: [
92
- {
93
- label: 'General',
94
- },
95
- {
96
- label: 'Members',
97
- },
98
- {
99
- label: 'Notifications',
100
- },
101
- ],
102
- },
103
- ],
104
- bottomItems: [
105
- {
106
- label: 'Home',
107
- icon: 'i-lucide-house',
108
- active: true,
109
- },
110
- {
111
- label: 'Inbox',
112
- icon: 'i-lucide-inbox',
113
- badge: '4',
114
- },
115
- {
116
- label: 'Contacts',
117
- icon: 'i-lucide-users',
118
- },
119
- {
120
- label: 'Settings',
121
- icon: 'i-lucide-settings',
122
- defaultOpen: true,
123
- children: [
124
- {
125
- label: 'General',
126
- },
127
- {
128
- label: 'Members',
129
- },
130
- {
131
- label: 'Notifications',
132
- },
133
- ],
134
- },
135
- ],
136
- userMenu: {
137
- name: 'Benjamin Canac',
138
- avatarSrc: 'https://github.com/benjamincanac.png',
72
+ <LayoutSidebar
73
+ :items="[
74
+ {
75
+ label: 'Home',
76
+ icon: 'i-lucide-house',
77
+ active: true,
78
+ },
79
+ {
80
+ label: 'Inbox',
81
+ icon: 'i-lucide-inbox',
82
+ badge: '4',
83
+ },
84
+ {
85
+ label: 'Contacts',
86
+ icon: 'i-lucide-users',
87
+ },
88
+ {
89
+ label: 'Settings',
90
+ icon: 'i-lucide-settings',
91
+ defaultOpen: true,
92
+ children: [
93
+ {
94
+ label: 'General',
95
+ },
96
+ {
97
+ label: 'Members',
98
+ },
99
+ {
100
+ label: 'Notifications',
101
+ },
102
+ ],
103
+ },
104
+ ]"
105
+ :bottom-items="[
106
+ {
107
+ label: 'Home',
108
+ icon: 'i-lucide-house',
109
+ active: true,
110
+ },
111
+ {
112
+ label: 'Inbox',
113
+ icon: 'i-lucide-inbox',
114
+ badge: '4',
115
+ },
116
+ {
117
+ label: 'Contacts',
118
+ icon: 'i-lucide-users',
119
+ },
120
+ {
121
+ label: 'Settings',
122
+ icon: 'i-lucide-settings',
123
+ defaultOpen: true,
124
+ children: [
125
+ {
126
+ label: 'General',
127
+ },
128
+ {
129
+ label: 'Members',
130
+ },
131
+ {
132
+ label: 'Notifications',
133
+ },
134
+ ],
139
135
  },
136
+ ]"
137
+ :user-menu="{
138
+ name: 'Benjamin Canac',
139
+ avatarSrc: 'https://github.com/benjamincanac.png',
140
140
  }"
141
- :panel="{
142
- navbar: {
141
+ >
142
+ <LayoutNavbar
143
+ :navbar="{
143
144
  title: 'Dashboard',
144
145
  ui: {
145
146
  root: 'relative',
146
147
  title: 'flex-1 absolute inset-0 w-full',
147
148
  },
148
- },
149
- toolbar: {
149
+ }"
150
+ :toolbar="{
150
151
  items: [
151
152
  {
152
153
  label: 'General',
@@ -177,85 +178,90 @@ const columns = useTableColumns<typeof data>(
177
178
  icon: 'i-lucide-bell',
178
179
  },
179
180
  ],
180
- },
181
- }"
182
- >
183
- <template #navbar-title>
184
- <div class="w-full text-center">title</div>
185
- </template>
186
-
187
- <UTableCard>
188
- <UTable :data :columns @select="() => {}" />
189
- </UTableCard>
190
- <UCard
191
- :ui="{
192
- body: 'flex flex-col gap-4 items-start',
193
- }"
194
- >
195
- <UButton
196
- label="Confirm"
197
- variant="subtle"
198
- @click="
199
- () => {
200
- confirm.confirmDestructive({
201
- title: 'Are you sure?',
202
- description: 'This action cannot be undone.',
203
- submitLabel: 'Yes, delete it',
204
- })
205
- }
206
- "
207
- />
208
- <UButton
209
- label="Actions"
210
- variant="subtle"
211
- @click="
212
- () => {
213
- overlay.create(LazyOverlayModalActions, {
214
- defaultOpen: true,
215
- props: {
216
- title: 'Actions',
217
- description: 'Choose an action to perform',
218
- actions: [
219
- {
220
- label: 'Action 1',
221
- },
222
- {
223
- label: 'Action 2',
224
- },
225
- ],
226
- },
227
- })
228
- }
229
- "
230
- />
231
- </UCard>
232
- <UCard
233
- :ui="{
234
- body: 'flex flex-col gap-4 items-start ',
235
181
  }"
236
182
  >
237
- <UForm
238
- :form
239
- :success-toast="{
240
- title: 'test',
241
- description: 'wow',
183
+ <template #navbar-title>
184
+ <div class="w-full text-center">title</div>
185
+ </template>
186
+
187
+ <UTableCard>
188
+ <UTable :data :columns @select="() => {}" />
189
+ </UTableCard>
190
+ <UCard
191
+ :ui="{
192
+ body: 'flex flex-col gap-4 items-start',
242
193
  }"
243
194
  >
244
- {{ form.data }}
245
- <UField
246
- v-slot="props"
247
- :field="
248
- form.fields.dateIso.$use({
249
- translate: dateValueIsoTranslator(),
250
- })
195
+ <UButton
196
+ label="Confirm"
197
+ variant="subtle"
198
+ @click="
199
+ () => {
200
+ confirm.confirmDestructive({
201
+ title: 'Are you sure?',
202
+ description: 'This action cannot be undone.',
203
+ submitLabel: 'Yes, delete it',
204
+ })
205
+ }
251
206
  "
207
+ />
208
+ <UButton
209
+ label="Actions"
210
+ variant="subtle"
211
+ @click="
212
+ () => {
213
+ overlay.create(LazyOverlayModalActions, {
214
+ defaultOpen: true,
215
+ props: {
216
+ title: 'Actions',
217
+ description: 'Choose an action to perform',
218
+ actions: [
219
+ {
220
+ label: 'Action 1',
221
+ },
222
+ {
223
+ label: 'Action 2',
224
+ },
225
+ ],
226
+ },
227
+ })
228
+ }
229
+ "
230
+ />
231
+ </UCard>
232
+ <UCard
233
+ class="max-w-sm"
234
+ :ui="{
235
+ body: 'flex flex-col gap-4 items-start ',
236
+ }"
237
+ >
238
+ <UForm
239
+ :form
240
+ :success-toast="{
241
+ title: 'test',
242
+ description: 'wow',
243
+ }"
244
+ class="flex flex-col gap-4"
252
245
  >
253
- <UInputDatePicker v-bind="props" />
254
- </UField>
255
- <UField v-slot="props" :field="form.fields.duration.$use()">
256
- <UInputDurationMinutes v-bind="props" />
257
- </UField>
258
- </UForm>
259
- </UCard>
260
- </LayoutDashboard>
246
+ {{ form.data }}
247
+ <UField v-slot="{ props }" :field="form.fields.text.$use()">
248
+ <UInput class="w-full" v-bind="props" />
249
+ </UField>
250
+ <UField
251
+ v-slot="{ props }"
252
+ :field="
253
+ form.fields.dateIso.$use({
254
+ translate: dateValueIsoTranslator(),
255
+ })
256
+ "
257
+ >
258
+ <UInputDatePicker class="w-full" v-bind="props" />
259
+ </UField>
260
+ <UField v-slot="{ props }" :field="form.fields.duration.$use()">
261
+ <UInputDurationMinutes class="w-full" v-bind="props" />
262
+ </UField>
263
+ </UForm>
264
+ </UCard>
265
+ </LayoutNavbar>
266
+ </LayoutSidebar>
261
267
  </template>
@@ -0,0 +1,15 @@
1
+ <script setup lang="ts">
2
+ import { de } from '@nuxt/ui/locale'
3
+ import { Settings } from 'luxon'
4
+
5
+ Settings.throwOnInvalid = true
6
+ Settings.defaultLocale = 'de'
7
+ </script>
8
+
9
+ <template>
10
+ <UCustomApp
11
+ :app="{
12
+ locale: de,
13
+ }"
14
+ />
15
+ </template>
@@ -6,5 +6,15 @@ export default defineNuxtConfig({
6
6
  projectId: 'my-project',
7
7
  },
8
8
  },
9
+ typescript: {
10
+ tsConfig: {
11
+ vueCompilerOptions: {
12
+ strictTemplates: true,
13
+ strictVModel: false,
14
+ htmlAttributes: ['aria-*'],
15
+ dataAttributes: ['data-*'],
16
+ },
17
+ },
18
+ },
9
19
  css: ['~/assets/test.css'],
10
20
  })
@@ -1,66 +1,123 @@
1
- <script setup lang="ts" generic="T">
1
+ <script setup lang="ts" generic="T, const Nullable extends boolean = false">
2
2
  import type { FormField } from '@falcondev-oss/form-core'
3
3
  import type { FormFieldProps, FormFieldSlots } from '@nuxt/ui'
4
- import type { ModelModifiers } from '@nuxt/ui/runtime/types/input.js'
5
4
  import { useForwardProps } from 'reka-ui'
6
- import { omit } from 'remeda'
5
+ import * as R from 'remeda'
7
6
 
8
- type FieldSlotProps<T> = {
7
+ type InputSlotProps<T, Nullable extends boolean> = {
9
8
  'modelValue': T
10
9
  'onUpdate:modelValue': (value: T) => void
11
10
  'onBlur': () => void
12
11
  'disabled': boolean
13
12
  'loading': boolean
14
- 'modelModifiers'?: Pick<ModelModifiers, 'nullable'>
13
+ 'modelModifiers': true extends Nullable ? { nullable: true } : never
14
+ 'placeholder'?: string
15
15
  }
16
16
 
17
17
  const props = defineProps<
18
18
  FormFieldProps & {
19
19
  field: FormField<T>
20
- }
20
+ } & { nullable?: Nullable }
21
21
  >()
22
22
  const slots = defineSlots<
23
23
  {
24
- default: (props: Omit<FieldSlotProps<T>, 'modelModifiers'>) => any
24
+ default: (slot: { props: InputSlotProps<T, Nullable>; field: FormField<T> }) => any
25
25
  } & Omit<FormFieldSlots, 'default'>
26
26
  >()
27
27
 
28
28
  const forwardedProps = useForwardProps(props)
29
29
 
30
- const formFieldProps = computed(() => {
30
+ const isOverMaxLength = computed(() => {
31
+ const field = forwardedProps.value.field
32
+
33
+ return field.schema.maxLength === undefined || field.value == null
34
+ ? false
35
+ : (field.value as string | number)?.toString().length > field.schema.maxLength
36
+ })
37
+
38
+ const formFieldProps = computed<FormFieldProps>(() => {
31
39
  const { field, ...rest } = forwardedProps.value
32
- return rest
40
+
41
+ const hint =
42
+ field.schema.maxLength === undefined
43
+ ? undefined
44
+ : `${(field.value as string | number | null)?.toString().length ?? 0}/${field.schema.maxLength}`
45
+
46
+ return {
47
+ required: field.schema.required,
48
+ label: field.schema.title,
49
+ description: field.schema.description,
50
+ hint,
51
+ ...R.omitBy(rest, (v) => v === undefined),
52
+ }
33
53
  })
34
54
 
35
- const slotProps = computed(
36
- () =>
37
- ({
38
- 'modelValue': forwardedProps.value.field.value,
39
- 'onUpdate:modelValue': (value) => forwardedProps.value.field.handleChange(value),
40
- 'onBlur': () => forwardedProps.value.field.handleBlur(),
41
- 'disabled': forwardedProps.value.field.disabled,
42
- 'loading': forwardedProps.value.field.isPending,
43
- 'modelModifiers': {
44
- nullable: true,
45
- },
46
- }) satisfies FieldSlotProps<T>,
47
- )
55
+ const inputProps = computed(() => {
56
+ const field = forwardedProps.value.field
57
+
58
+ const placeholder = field.errors && field.errors.join('\n')
59
+
60
+ return {
61
+ 'modelValue': field.value,
62
+ 'onUpdate:modelValue': (value) => field.handleChange(value),
63
+ 'onBlur': () => field.handleBlur(),
64
+ 'disabled': field.disabled,
65
+ 'loading': field.isPending,
66
+ 'modelModifiers': (props.nullable === true
67
+ ? { nullable: true }
68
+ : undefined) as true extends Nullable ? { nullable: true } : never,
69
+ placeholder,
70
+ } satisfies InputSlotProps<T, Nullable>
71
+ })
48
72
  </script>
49
73
 
50
74
  <template>
51
75
  <UFormField
52
76
  v-bind="formFieldProps"
53
- :error="forwardedProps.field.errors && forwardedProps.field.errors.join('\n')"
77
+ :ui="{
78
+ hint: isOverMaxLength ? 'text-error' : '',
79
+ }"
80
+ :error="!!field.errors"
54
81
  >
55
- <slot v-bind="slotProps">
82
+ <template #hint="{ hint }">
83
+ <span class="flex items-center gap-1.5">
84
+ <UPopover
85
+ v-if="!!field.errors"
86
+ mode="hover"
87
+ :delay-duration="0"
88
+ :ui="{
89
+ content: 'bg-error-50 ring-error-200! rounded py-1 px-2',
90
+ }"
91
+ >
92
+ <UIcon name="lucide:circle-alert" class="text-error" />
93
+ <template #content>
94
+ <p class="text-(--ui-color-neutral-800) max-w-sm whitespace-normal text-xs">
95
+ {{ field.errors.join('\n') }}
96
+ </p>
97
+ </template>
98
+ </UPopover>
99
+
100
+ {{ hint }}
101
+ </span>
102
+ </template>
103
+
104
+ <slot v-bind="{ props: inputProps, field: forwardedProps.field }">
56
105
  <DevOnly>
57
106
  <p class="font-black text-red-500">UField missing slot</p>
58
107
  </DevOnly>
59
108
  </slot>
60
109
 
61
- <template v-for="(_, name) in omit(slots, ['default'])" #[name]="slotData">
110
+ <template v-for="(_, name) in R.omit(slots, ['default'])" #[name]="slotData">
62
111
  <!-- @vue-ignore -->
63
112
  <slot :name="name" v-bind="slotData || {}" />
64
113
  </template>
65
114
  </UFormField>
66
115
  </template>
116
+
117
+ <style scoped>
118
+ :deep([aria-invalid='true']) {
119
+ &::placeholder {
120
+ color: var(--ui-color-error-400);
121
+ }
122
+ }
123
+ </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@falcondev-oss/nuxt-layers-base",
3
3
  "type": "module",
4
- "version": "0.21.0",
4
+ "version": "0.23.0",
5
5
  "description": "Nuxt layer with lots of useful helpers and @nuxt/ui components",
6
6
  "license": "MIT",
7
7
  "repository": "github:falcondev-oss/nuxt-layers",
@@ -14,11 +14,11 @@
14
14
  "pnpm": "10"
15
15
  },
16
16
  "dependencies": {
17
- "@falcondev-oss/form-core": "^0.19.3",
18
- "@falcondev-oss/form-vue": "^0.19.3",
17
+ "@falcondev-oss/form-core": "^0.21.1",
18
+ "@falcondev-oss/form-vue": "^0.21.1",
19
19
  "@falcondev-oss/trpc-typed-form-data": "^0.4.1",
20
20
  "@falcondev-oss/trpc-vue-query": "^0.5.2",
21
- "@iconify-json/lucide": "^1.2.90",
21
+ "@iconify-json/lucide": "^1.2.94",
22
22
  "@internationalized/date": "^3.11.0",
23
23
  "@nuxt/icon": "^2.2.1",
24
24
  "@nuxt/ui": "4.4.0",
@@ -34,19 +34,19 @@
34
34
  "consola": "^3.4.2",
35
35
  "defu": "^6.1.4",
36
36
  "maska": "^3.2.0",
37
- "reka-ui": "^2.8.0",
37
+ "reka-ui": "^2.8.2",
38
38
  "remeda": "^2.33.6",
39
39
  "superjson": "^2.2.6",
40
- "tailwindcss": "^4.1.18",
40
+ "tailwindcss": "^4.2.1",
41
41
  "trpc-nuxt": "^2.0.1",
42
42
  "type-fest": "^5.4.4",
43
- "vue": "^3.5.28",
43
+ "vue": "^3.5.29",
44
44
  "vue-router": "^4.6.4"
45
45
  },
46
46
  "devDependencies": {
47
47
  "nuxt": "^4.3.1",
48
48
  "typescript": "^5.9.3",
49
- "vue-tsc": "^3.2.4",
49
+ "vue-tsc": "^3.2.5",
50
50
  "zod": "^4.3.6"
51
51
  },
52
52
  "scripts": {