betterstart-cli 0.0.98 → 0.0.100

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 (66) hide show
  1. package/dist/assets/adapters/next/templates/init/api/upload-route.ts +2 -1
  2. package/dist/assets/adapters/next/templates/init/components/layouts/admin-settings-sidebar.tsx +2 -2
  3. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sign-out.tsx +27 -8
  4. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table.tsx +3 -1
  5. package/dist/assets/adapters/next/templates/init/components/shared/delete-dialog.tsx +3 -2
  6. package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-version-restore-dialog.tsx +4 -3
  7. package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-button.tsx +5 -2
  8. package/dist/assets/adapters/next/templates/init/components/shared/media/edit-media-dialog-content.tsx +11 -5
  9. package/dist/assets/adapters/next/templates/init/components/shared/media/media-delete-dialog.tsx +6 -4
  10. package/dist/assets/adapters/next/templates/init/components/shared/media/media-delete-drawer.tsx +12 -8
  11. package/dist/assets/adapters/next/templates/init/components/shared/media/media-empty-state.tsx +6 -5
  12. package/dist/assets/adapters/next/templates/init/components/shared/media/media-grid-pagination.tsx +1 -1
  13. package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +5 -3
  14. package/dist/assets/adapters/next/templates/init/components/shared/search-input.tsx +2 -0
  15. package/dist/assets/adapters/next/templates/init/components/shared/sort-order-dialog.tsx +12 -7
  16. package/dist/assets/adapters/next/templates/init/hooks/use-entity-versions.ts +2 -1
  17. package/dist/assets/adapters/next/templates/init/lib/actions/audit/cleanup-audit-logs.ts +2 -1
  18. package/dist/assets/adapters/next/templates/init/lib/actions/forms/upsert-form-settings.ts +2 -1
  19. package/dist/assets/adapters/next/templates/init/lib/actions/media/create-media.ts +2 -1
  20. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media-bulk.ts +2 -1
  21. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media.ts +2 -1
  22. package/dist/assets/adapters/next/templates/init/lib/actions/media/update-media.ts +2 -1
  23. package/dist/assets/adapters/next/templates/init/lib/actions/profile/change-own-password.ts +2 -1
  24. package/dist/assets/adapters/next/templates/init/lib/actions/profile/update-email.ts +2 -1
  25. package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-media-from-url.ts +2 -2
  26. package/dist/assets/adapters/next/templates/init/lib/actions/users/change-user-password.ts +2 -1
  27. package/dist/assets/adapters/next/templates/init/lib/actions/users/create-user.ts +2 -1
  28. package/dist/assets/adapters/next/templates/init/lib/actions/users/delete-user.ts +2 -1
  29. package/dist/assets/adapters/next/templates/init/lib/actions/users/update-user-role.ts +2 -1
  30. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/create-webhook.ts +2 -1
  31. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/delete-webhook.ts +2 -1
  32. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/get-webhook-secret.ts +2 -1
  33. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/regenerate-webhook-secret.ts +2 -1
  34. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/send-test-webhook.ts +2 -1
  35. package/dist/assets/adapters/next/templates/init/lib/actions/webhooks/update-webhook.ts +2 -1
  36. package/dist/assets/adapters/next/templates/init/pages/forgot-password-form.tsx +7 -7
  37. package/dist/assets/adapters/next/templates/init/pages/login-form.tsx +7 -7
  38. package/dist/assets/adapters/next/templates/init/pages/login-page.tsx +1 -1
  39. package/dist/assets/adapters/next/templates/init/pages/media/media-page-content.tsx +5 -4
  40. package/dist/assets/adapters/next/templates/init/pages/profile/profile-form.tsx +13 -10
  41. package/dist/assets/adapters/next/templates/init/pages/reset-password-form.tsx +5 -5
  42. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-table.tsx +3 -1
  43. package/dist/assets/adapters/next/templates/init/pages/settings/forms/form-notifications-drawer.tsx +10 -5
  44. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/delete-webhook-dialog.tsx +19 -10
  45. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-actions.tsx +9 -4
  46. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhook-endpoint-dialog.tsx +26 -20
  47. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-page-content.tsx +4 -4
  48. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-logs-table.tsx +3 -1
  49. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-table.tsx +3 -1
  50. package/dist/assets/adapters/next/templates/init/pages/users/change-password-dialog.tsx +6 -5
  51. package/dist/assets/adapters/next/templates/init/pages/users/create-user-dialog.tsx +5 -5
  52. package/dist/assets/adapters/next/templates/init/pages/users/delete-user-dialog.tsx +6 -4
  53. package/dist/assets/adapters/next/templates/init/pages/users/edit-role-dialog.tsx +9 -5
  54. package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +1 -1
  55. package/dist/assets/adapters/next/templates/init/utils/error/action-error.ts +35 -0
  56. package/dist/assets/shared-assets/react-admin/schema.json +8 -0
  57. package/dist/assets/shared-assets/react-admin/ui/sonner.tsx +3 -8
  58. package/dist/assets/shared-assets/react-admin/ui/spinner.tsx +1 -1
  59. package/dist/{chunk-AIQWGMU7.js → chunk-45Z6XDG5.js} +419 -83
  60. package/dist/chunk-45Z6XDG5.js.map +1 -0
  61. package/dist/cli.js +2027 -2391
  62. package/dist/cli.js.map +1 -1
  63. package/dist/{template-reader-F2VFSDKV.js → template-reader-J6G7N5V6.js} +2 -2
  64. package/package.json +1 -1
  65. package/dist/chunk-AIQWGMU7.js.map +0 -1
  66. /package/dist/{template-reader-F2VFSDKV.js.map → template-reader-J6G7N5V6.js.map} +0 -0
@@ -29,12 +29,12 @@ import {
29
29
  FormMessage
30
30
  } from '@admin/components/ui/form'
31
31
  import { Input } from '@admin/components/ui/input'
32
+ import { Spinner } from '@admin/components/ui/spinner'
32
33
  import { Switch } from '@admin/components/ui/switch'
33
34
  import { webhookEventSources } from '@admin/data/webhook-events'
34
35
  import { useWebhookSubscriptions } from '@admin/hooks/use-webhooks'
35
36
  import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
36
37
  import { useMutation, useQueryClient } from '@tanstack/react-query'
37
- import { LoaderCircle } from 'lucide-react'
38
38
  import { useForm } from 'react-hook-form'
39
39
  import { toast } from 'sonner'
40
40
  import { z } from 'zod/v3'
@@ -95,13 +95,13 @@ export function WebhookEndpointDialog({ webhook, open, onOpenChange }: WebhookEn
95
95
  ? await updateWebhook(webhook.id, { ...endpointValues, events })
96
96
  : await createWebhook({ ...endpointValues, events })
97
97
  if (!result.success) {
98
- throw new Error(result.error || 'Failed to save endpoint')
98
+ throw new Error(result.error || 'Failed to save this webhook')
99
99
  }
100
100
 
101
101
  return result
102
102
  },
103
103
  onSuccess: async () => {
104
- toast.success(isEdit ? 'Endpoint saved' : 'Endpoint created')
104
+ toast.success(isEdit ? 'Webhook saved' : 'Webhook created')
105
105
  await queryClient.invalidateQueries({ queryKey: ['webhooks'] })
106
106
  await queryClient.invalidateQueries({
107
107
  queryKey: ['webhook-subscriptions']
@@ -109,13 +109,13 @@ export function WebhookEndpointDialog({ webhook, open, onOpenChange }: WebhookEn
109
109
  onOpenChange(false)
110
110
  },
111
111
  onError: (error: Error) => {
112
- toast.error(error.message || 'An unexpected error occurred')
112
+ toast.error(error.message || 'Failed to save this webhook')
113
113
  }
114
114
  })
115
115
 
116
116
  const revealSecretMutation = useMutation({
117
117
  mutationFn: async () => {
118
- if (!webhook) throw new Error('Save the endpoint first')
118
+ if (!webhook) throw new Error('Save this webhook first.')
119
119
  const result = await getWebhookSecret(webhook.id)
120
120
  if (!result.success || !result.secret) {
121
121
  throw new Error(result.error || 'Failed to fetch signing secret')
@@ -131,7 +131,7 @@ export function WebhookEndpointDialog({ webhook, open, onOpenChange }: WebhookEn
131
131
 
132
132
  const regenerateSecretMutation = useMutation({
133
133
  mutationFn: async () => {
134
- if (!webhook) throw new Error('Save the endpoint first')
134
+ if (!webhook) throw new Error('Save this webhook first.')
135
135
  const result = await regenerateWebhookSecret(webhook.id)
136
136
  if (!result.success || !result.secret) {
137
137
  throw new Error(result.error || 'Failed to regenerate signing secret')
@@ -150,7 +150,7 @@ export function WebhookEndpointDialog({ webhook, open, onOpenChange }: WebhookEn
150
150
 
151
151
  const testMutation = useMutation({
152
152
  mutationFn: async () => {
153
- if (!webhook) throw new Error('Save the endpoint first')
153
+ if (!webhook) throw new Error('Save this webhook first.')
154
154
  const result = await sendTestWebhook(webhook.id)
155
155
  if (!result.success) {
156
156
  throw new Error(result.error || 'Test webhook failed')
@@ -183,9 +183,7 @@ export function WebhookEndpointDialog({ webhook, open, onOpenChange }: WebhookEn
183
183
  <Form {...form}>
184
184
  <form onSubmit={form.handleSubmit((values) => saveMutation.mutate(values))}>
185
185
  <DialogHeader>
186
- <DialogTitle>
187
- {isEdit ? `Edit endpoint — ${webhook.name}` : 'Add endpoint'}
188
- </DialogTitle>
186
+ <DialogTitle>{isEdit ? `Edit webhook — ${webhook.name}` : 'Add webhook'}</DialogTitle>
189
187
  <DialogDescription>
190
188
  Events are delivered as signed JSON POST requests
191
189
  </DialogDescription>
@@ -252,21 +250,22 @@ export function WebhookEndpointDialog({ webhook, open, onOpenChange }: WebhookEn
252
250
  variant="outline"
253
251
  size="sm"
254
252
  disabled={revealSecretMutation.isPending || secret !== null}
253
+ aria-busy={revealSecretMutation.isPending}
255
254
  onClick={() => revealSecretMutation.mutate()}
256
255
  >
257
- Reveal
256
+ {revealSecretMutation.isPending && <Spinner />}
257
+ {revealSecretMutation.isPending ? 'Revealing…' : 'Reveal'}
258
258
  </Button>
259
259
  <Button
260
260
  type="button"
261
261
  variant="outline"
262
262
  size="sm"
263
263
  disabled={regenerateSecretMutation.isPending}
264
+ aria-busy={regenerateSecretMutation.isPending}
264
265
  onClick={() => regenerateSecretMutation.mutate()}
265
266
  >
266
- {regenerateSecretMutation.isPending && (
267
- <LoaderCircle className="animate-spin" />
268
- )}
269
- Regenerate
267
+ {regenerateSecretMutation.isPending && <Spinner />}
268
+ {regenerateSecretMutation.isPending ? 'Regenerating…' : 'Regenerate'}
270
269
  </Button>
271
270
  </div>
272
271
  <p className="text-xs text-muted-foreground">
@@ -351,15 +350,22 @@ export function WebhookEndpointDialog({ webhook, open, onOpenChange }: WebhookEn
351
350
  variant="outline"
352
351
  size="lg"
353
352
  disabled={testMutation.isPending}
353
+ aria-busy={testMutation.isPending}
354
354
  onClick={() => testMutation.mutate()}
355
355
  >
356
- {testMutation.isPending && <LoaderCircle className="animate-spin" />}
357
- Send test
356
+ {testMutation.isPending && <Spinner />}
357
+ {testMutation.isPending ? 'Sending test…' : 'Send test'}
358
358
  </Button>
359
359
  )}
360
- <Button type="submit" disabled={isPending} size="lg">
361
- {isPending && <LoaderCircle className="animate-spin" />}
362
- {isEdit ? 'Save endpoint' : 'Create endpoint'}
360
+ <Button type="submit" disabled={isPending} aria-busy={isPending} size="lg">
361
+ {isPending && <Spinner />}
362
+ {isPending
363
+ ? isEdit
364
+ ? 'Saving webhook…'
365
+ : 'Creating webhook…'
366
+ : isEdit
367
+ ? 'Save webhook'
368
+ : 'Create webhook'}
363
369
  </Button>
364
370
  </DialogFooter>
365
371
  </form>
@@ -56,17 +56,17 @@ export function WebhooksLogsPageContent() {
56
56
  const filters = [
57
57
  {
58
58
  key: 'status',
59
- label: 'Results',
59
+ label: 'Status',
60
60
  type: 'select' as const,
61
61
  value: status,
62
62
  options: [
63
- { label: 'Success', value: 'success' },
63
+ { label: 'Delivered', value: 'success' },
64
64
  { label: 'Failed', value: 'failed' }
65
65
  ]
66
66
  },
67
67
  {
68
68
  key: 'endpoint',
69
- label: 'Endpoints',
69
+ label: 'Webhooks',
70
70
  type: 'select' as const,
71
71
  value: endpoint,
72
72
  options: (endpointOptions?.webhooks ?? []).map((webhook) => ({
@@ -85,7 +85,7 @@ export function WebhooksLogsPageContent() {
85
85
 
86
86
  return (
87
87
  <React.Fragment>
88
- <PageHeader title="Webhook Logs" />
88
+ <PageHeader title="Deliveries" />
89
89
 
90
90
  <EntityFiltersBar
91
91
  filters={filters}
@@ -205,7 +205,9 @@ export function WebhooksLogsTable<TValue>({
205
205
  className="flex h-24 items-center justify-center text-center"
206
206
  style={{ gridColumn: '1 / -1' }}
207
207
  >
208
- <div className="text-destructive">Error loading deliveries: {error.message}</div>
208
+ <div className="text-destructive">
209
+ Failed to load deliveries. Refresh to try again.
210
+ </div>
209
211
  </DataGridCell>
210
212
  </DataGridRow>
211
213
  ) : table.getRowModel().rows?.length ? (
@@ -207,7 +207,9 @@ export function WebhooksTable<TValue>({
207
207
  className="flex h-24 items-center justify-center text-center"
208
208
  style={{ gridColumn: '1 / -1' }}
209
209
  >
210
- <div className="text-destructive">Error loading Webhooks: {error.message}</div>
210
+ <div className="text-destructive">
211
+ Failed to load webhooks. Refresh to try again.
212
+ </div>
211
213
  </DataGridCell>
212
214
  </DataGridRow>
213
215
  ) : table.getRowModel().rows?.length ? (
@@ -25,9 +25,10 @@ import {
25
25
  FormMessage
26
26
  } from '@admin/components/ui/form'
27
27
  import { Input } from '@admin/components/ui/input'
28
+ import { Spinner } from '@admin/components/ui/spinner'
28
29
  import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
29
30
  import { useMutation, useQueryClient } from '@tanstack/react-query'
30
- import { KeySquare, LoaderCircle } from 'lucide-react'
31
+ import { KeySquare } from 'lucide-react'
31
32
  import { useForm } from 'react-hook-form'
32
33
  import { toast } from 'sonner'
33
34
  import { z } from 'zod/v3'
@@ -112,7 +113,7 @@ export function ChangePasswordDialog({
112
113
  setOpen(false)
113
114
  },
114
115
  onError: (error: Error) => {
115
- toast.error(error.message || 'An unexpected error occurred')
116
+ toast.error(error.message || 'Failed to change this password')
116
117
  }
117
118
  })
118
119
 
@@ -216,9 +217,9 @@ export function ChangePasswordDialog({
216
217
  </DialogBody>
217
218
 
218
219
  <DialogFooter showCloseButton={true}>
219
- <Button type="submit" disabled={isPending} size="lg">
220
- {isPending ? <LoaderCircle className="animate-spin" /> : null}
221
- {isPending ? 'Changing...' : 'Change Password'}
220
+ <Button type="submit" disabled={isPending} aria-busy={isPending} size="lg">
221
+ {isPending ? <Spinner /> : null}
222
+ {isPending ? 'Changing' : 'Change Password'}
222
223
  </Button>
223
224
  </DialogFooter>
224
225
  </form>
@@ -30,10 +30,10 @@ import {
30
30
  SelectTrigger,
31
31
  SelectValue
32
32
  } from '@admin/components/ui/select'
33
+ import { Spinner } from '@admin/components/ui/spinner'
33
34
  import { UserRole } from '@admin/types/auth'
34
35
  import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
35
36
  import { useMutation, useQueryClient } from '@tanstack/react-query'
36
- import { LoaderCircle } from 'lucide-react'
37
37
  import { useForm } from 'react-hook-form'
38
38
  import { toast } from 'sonner'
39
39
  import { z } from 'zod/v3'
@@ -87,7 +87,7 @@ export function CreateUserDialog() {
87
87
  setOpen(false)
88
88
  },
89
89
  onError: (error: Error) => {
90
- toast.error(error.message || 'An unexpected error occurred')
90
+ toast.error(error.message || 'Failed to create this user')
91
91
  }
92
92
  })
93
93
 
@@ -214,9 +214,9 @@ export function CreateUserDialog() {
214
214
  </DialogBody>
215
215
 
216
216
  <DialogFooter showCloseButton={true}>
217
- <Button type="submit" disabled={isPending} size="lg">
218
- {isPending && <LoaderCircle className="animate-spin" />}
219
- Create
217
+ <Button type="submit" disabled={isPending} aria-busy={isPending} size="lg">
218
+ {isPending && <Spinner />}
219
+ {isPending ? 'Creating…' : 'Create'}
220
220
  </Button>
221
221
  </DialogFooter>
222
222
  </form>
@@ -24,9 +24,10 @@ import {
24
24
  FormMessage
25
25
  } from '@admin/components/ui/form'
26
26
  import { Input } from '@admin/components/ui/input'
27
+ import { Spinner } from '@admin/components/ui/spinner'
27
28
  import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
28
29
  import { useMutation, useQueryClient } from '@tanstack/react-query'
29
- import { LoaderCircle, Trash } from 'lucide-react'
30
+ import { Trash } from 'lucide-react'
30
31
  import { useForm } from 'react-hook-form'
31
32
  import { toast } from 'sonner'
32
33
  import { z } from 'zod/v3'
@@ -80,7 +81,7 @@ export function DeleteUserDialog({ userId, userName, isCurrentUser }: DeleteUser
80
81
  setOpen(false)
81
82
  },
82
83
  onError: (error: Error) => {
83
- toast.error(error.message || 'An unexpected error occurred')
84
+ toast.error(error.message || 'Failed to delete this account')
84
85
  }
85
86
  })
86
87
 
@@ -149,11 +150,12 @@ export function DeleteUserDialog({ userId, userName, isCurrentUser }: DeleteUser
149
150
  <Button
150
151
  type="submit"
151
152
  disabled={isPending || !form.formState.isValid}
153
+ aria-busy={isPending}
152
154
  size="lg"
153
155
  variant="destructive"
154
156
  >
155
- {isPending ? <LoaderCircle className="animate-spin" /> : null}
156
- {isPending ? 'Deleting...' : 'Delete'}
157
+ {isPending ? <Spinner /> : null}
158
+ {isPending ? 'Deleting' : 'Delete account'}
157
159
  </Button>
158
160
  </AlertDialogFooter>
159
161
  </form>
@@ -19,9 +19,9 @@ import {
19
19
  SelectTrigger,
20
20
  SelectValue
21
21
  } from '@admin/components/ui/select'
22
+ import { Spinner } from '@admin/components/ui/spinner'
22
23
  import { UserRole } from '@admin/types/auth'
23
24
  import { useQueryClient } from '@tanstack/react-query'
24
- import { LoaderCircle } from 'lucide-react'
25
25
  import { toast } from 'sonner'
26
26
 
27
27
  interface EditRoleDialogProps {
@@ -49,7 +49,7 @@ export function EditRoleDialog({ userId, currentRole, userName, children }: Edit
49
49
  toast.error(result.error || 'Failed to update role')
50
50
  }
51
51
  } catch {
52
- toast.error('An unexpected error occurred')
52
+ toast.error('Failed to change this role. Try again.')
53
53
  }
54
54
  })
55
55
  }
@@ -80,9 +80,13 @@ export function EditRoleDialog({ userId, currentRole, userName, children }: Edit
80
80
  <Button variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
81
81
  Cancel
82
82
  </Button>
83
- <Button onClick={handleSave} disabled={isPending || role === currentRole}>
84
- {isPending && <LoaderCircle className="animate-spin" />}
85
- Save Role
83
+ <Button
84
+ onClick={handleSave}
85
+ disabled={isPending || role === currentRole}
86
+ aria-busy={isPending}
87
+ >
88
+ {isPending && <Spinner />}
89
+ {isPending ? 'Saving Role…' : 'Save Role'}
86
90
  </Button>
87
91
  </DialogFooter>
88
92
  </DialogContent>
@@ -215,7 +215,7 @@ export function UsersTable<TValue>({
215
215
  className="flex h-24 items-center justify-center px-4 text-center"
216
216
  style={{ gridColumn: '1 / -1' }}
217
217
  >
218
- <div className="text-destructive">Error: {error.message}</div>
218
+ <div className="text-destructive">Failed to load users. Refresh to try again.</div>
219
219
  </DataGridCell>
220
220
  </DataGridRow>
221
221
  ) : table.getRowModel().rows?.length ? (
@@ -0,0 +1,35 @@
1
+ const POSTGRES_MESSAGES: Record<string, string> = {
2
+ '23502': 'A required value is missing.',
3
+ '23503': 'This is still in use somewhere else.',
4
+ '23505': 'That value is already in use.'
5
+ }
6
+
7
+ const AUTH_MESSAGES: Record<string, string> = {
8
+ INVALID_EMAIL_OR_PASSWORD: 'That email or password is incorrect.',
9
+ PASSWORD_TOO_SHORT: 'That password is too short.',
10
+ USER_ALREADY_EXISTS: 'That email is already in use.'
11
+ }
12
+
13
+ function readStringProperty(value: unknown, key: string): string | undefined {
14
+ if (typeof value !== 'object' || value === null) return undefined
15
+ const property = (value as Record<string, unknown>)[key]
16
+ return typeof property === 'string' ? property : undefined
17
+ }
18
+
19
+ function recognize(error: unknown): string | undefined {
20
+ const authCode = readStringProperty(
21
+ (error as { body?: unknown } | null)?.body ?? undefined,
22
+ 'code'
23
+ )
24
+ if (authCode && AUTH_MESSAGES[authCode]) return AUTH_MESSAGES[authCode]
25
+
26
+ const sqlState = readStringProperty(error, 'code')
27
+ if (sqlState && POSTGRES_MESSAGES[sqlState]) return POSTGRES_MESSAGES[sqlState]
28
+
29
+ return undefined
30
+ }
31
+
32
+ export function toActionError(error: unknown, fallback: string): string {
33
+ console.error(error)
34
+ return recognize(error) ?? fallback
35
+ }
@@ -20,6 +20,14 @@
20
20
  "type": "string",
21
21
  "description": "Human-readable display name for the schema"
22
22
  },
23
+ "labelSingular": {
24
+ "type": "string",
25
+ "description": "Display noun for one record, used inside sentences (e.g., 'Job application' in 'Delete this job application?'). Derived from 'label' when omitted."
26
+ },
27
+ "labelPlural": {
28
+ "type": "string",
29
+ "description": "Display noun for many records, used inside sentences (e.g., 'job applications' in '3 job applications deleted'). Falls back to 'label' when omitted."
30
+ },
23
31
  "description": {
24
32
  "type": "string",
25
33
  "description": "Brief description of what this schema represents"
@@ -1,13 +1,8 @@
1
1
  'use client'
2
2
 
3
3
  import type { ToasterProps } from 'sonner'
4
- import {
5
- CircleCheckIcon,
6
- InfoIcon,
7
- LoaderCircle,
8
- OctagonXIcon,
9
- TriangleAlertIcon
10
- } from 'lucide-react'
4
+ import { Spinner } from '@admin/components/ui/spinner'
5
+ import { CircleCheckIcon, InfoIcon, OctagonXIcon, TriangleAlertIcon } from 'lucide-react'
11
6
  import { useTheme } from 'next-themes'
12
7
  import { Toaster as Sonner } from 'sonner'
13
8
 
@@ -23,7 +18,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
23
18
  info: <InfoIcon className="size-4" />,
24
19
  warning: <TriangleAlertIcon className="size-4" />,
25
20
  error: <OctagonXIcon className="size-4" />,
26
- loading: <LoaderCircle className="size-4 animate-spin" />
21
+ loading: <Spinner className="size-4" />
27
22
  }}
28
23
  style={
29
24
  {
@@ -6,7 +6,7 @@ function Spinner({ className, ...props }: React.ComponentProps<'svg'>) {
6
6
  <LoaderCircle
7
7
  role="status"
8
8
  aria-label="Loading"
9
- className={cn('size-4 animate-spin', className)}
9
+ className={cn('animate-spin', className)}
10
10
  {...props}
11
11
  />
12
12
  )