betterstart-cli 0.0.117 → 0.0.118

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 (82) hide show
  1. package/dist/assets/adapters/next/templates/init/admin-globals.css +20 -0
  2. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-grid-row.tsx +33 -1
  3. package/dist/assets/adapters/next/templates/init/components/shared/media/media-gallery-dialog.tsx +3 -3
  4. package/dist/assets/adapters/next/templates/init/components/shared/media/media-preview.tsx +3 -1
  5. package/dist/assets/adapters/next/templates/init/hooks/use-field-error-visible.ts +25 -0
  6. package/dist/assets/adapters/next/templates/init/hooks/use-media.ts +1 -1
  7. package/dist/assets/adapters/next/templates/init/hooks/use-upload.ts +11 -5
  8. package/dist/assets/adapters/next/templates/init/lib/actions/audit/cleanup-audit-logs.ts +1 -0
  9. package/dist/assets/adapters/next/templates/init/lib/actions/audit/query-audit-logs.ts +2 -2
  10. package/dist/assets/adapters/next/templates/init/lib/actions/audit/record-audit-event.ts +1 -2
  11. package/dist/assets/adapters/next/templates/init/lib/actions/auth/client.ts +4 -1
  12. package/dist/assets/adapters/next/templates/init/lib/actions/entity-versions/get-entity-versions.ts +5 -1
  13. package/dist/assets/adapters/next/templates/init/lib/actions/forms/get-all-form-settings.ts +1 -1
  14. package/dist/assets/adapters/next/templates/init/lib/actions/media/create-media.ts +2 -1
  15. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media-bulk.ts +1 -0
  16. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media.ts +1 -0
  17. package/dist/assets/adapters/next/templates/init/lib/actions/media/update-media.ts +2 -4
  18. package/dist/assets/adapters/next/templates/init/lib/actions/profile/change-own-password.ts +1 -0
  19. package/dist/assets/adapters/next/templates/init/lib/actions/profile/update-email.ts +1 -0
  20. package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-files.ts +1 -0
  21. package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-media-from-url.ts +16 -6
  22. package/dist/assets/adapters/next/templates/init/lib/actions/users/change-user-password.ts +1 -0
  23. package/dist/assets/adapters/next/templates/init/lib/actions/users/create-user.ts +2 -1
  24. package/dist/assets/adapters/next/templates/init/lib/actions/users/delete-user.ts +1 -0
  25. package/dist/assets/adapters/next/templates/init/lib/actions/users/types.ts +1 -1
  26. package/dist/assets/adapters/next/templates/init/lib/actions/users/update-user-role.ts +1 -0
  27. package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +12 -4
  28. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/registry.ts +4 -4
  29. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +3 -7
  30. package/dist/assets/adapters/next/templates/init/pages/forgot-password-form.tsx +5 -8
  31. package/dist/assets/adapters/next/templates/init/pages/forgot-password-page-skeleton.tsx +2 -2
  32. package/dist/assets/adapters/next/templates/init/pages/login-form.tsx +7 -9
  33. package/dist/assets/adapters/next/templates/init/pages/login-page-skeleton.tsx +2 -2
  34. package/dist/assets/adapters/next/templates/init/pages/reset-password-form.tsx +8 -16
  35. package/dist/assets/adapters/next/templates/init/pages/reset-password-page-skeleton.tsx +2 -2
  36. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-detail-row.tsx +1 -1
  37. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-table.tsx +1 -6
  38. package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-table.tsx +2 -7
  39. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-table.tsx +1 -6
  40. package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +1 -1
  41. package/dist/assets/adapters/next/templates/init/public/placeholder.png +0 -0
  42. package/dist/assets/adapters/next/templates/init/types/index.ts +2 -0
  43. package/dist/assets/adapters/next/templates/init/utils/audit/normalize-comparable-value.ts +4 -2
  44. package/dist/assets/adapters/next/templates/init/utils/date/format-action-date.ts +12 -0
  45. package/dist/assets/adapters/next/templates/init/utils/dev-mode/dev-mode-json-schemas.ts +66 -0
  46. package/dist/assets/adapters/next/templates/init/utils/dev-mode/parse-dev-mode-lifecycle-hooks.ts +11 -0
  47. package/dist/assets/adapters/next/templates/init/utils/dev-mode/parse-dev-mode-snippets.ts +9 -0
  48. package/dist/assets/adapters/next/templates/init/utils/editor/set-task-item-checked.ts +13 -0
  49. package/dist/assets/adapters/next/templates/init/utils/editor/update-nodes-attr.ts +2 -1
  50. package/dist/assets/adapters/next/templates/init/utils/email/email-theme.ts +13 -0
  51. package/dist/assets/adapters/next/templates/init/utils/email/render-form-submission-rows.ts +3 -2
  52. package/dist/assets/adapters/next/templates/init/utils/email/render-notification-email.ts +7 -6
  53. package/dist/assets/adapters/next/templates/init/utils/email/render-thank-you-email.ts +5 -4
  54. package/dist/assets/adapters/next/templates/init/utils/error/read-string-property.ts +4 -2
  55. package/dist/assets/adapters/next/templates/init/utils/error/recognize.ts +2 -4
  56. package/dist/assets/adapters/next/templates/init/utils/error/to-action-error.ts +0 -1
  57. package/dist/assets/adapters/next/templates/init/utils/form/clone-default-value.ts +4 -2
  58. package/dist/assets/adapters/next/templates/init/utils/shared/is-record.ts +3 -0
  59. package/dist/assets/adapters/next/templates/init/utils/table/default-get-id.ts +3 -2
  60. package/dist/assets/shared-assets/react-admin/custom/content-editor/editor-toolbar.tsx +1 -1
  61. package/dist/assets/shared-assets/react-admin/custom/content-editor/slash-command-menu.tsx +5 -3
  62. package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +1 -1
  63. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-node/removable-image-node/removable-image-node.tsx +3 -1
  64. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-node/task-item-node/task-item-node-extension.tsx +6 -15
  65. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +1 -6
  66. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +1 -6
  67. package/dist/assets/shared-assets/react-admin/custom/date-range-picker.tsx +9 -2
  68. package/dist/assets/shared-assets/react-admin/custom/gallery-field.tsx +3 -3
  69. package/dist/assets/shared-assets/react-admin/custom/media-field.tsx +2 -2
  70. package/dist/assets/shared-assets/react-admin/custom/nested-object-list-field.tsx +3 -1
  71. package/dist/assets/shared-assets/react-admin/custom/progressive-blur.tsx +3 -1
  72. package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +20 -23
  73. package/dist/assets/shared-assets/react-admin/ui/form.tsx +6 -3
  74. package/dist/assets/shared-assets/react-admin/ui/navigation-menu.tsx +1 -1
  75. package/dist/{chunk-FGEVDA53.js → chunk-BNHFSHPE.js} +12 -1
  76. package/dist/chunk-BNHFSHPE.js.map +1 -0
  77. package/dist/cli.js +909 -598
  78. package/dist/cli.js.map +1 -1
  79. package/dist/{read-namespaced-template-KUENIIDC.js → read-namespaced-template-WYIGUH7S.js} +2 -2
  80. package/package.json +1 -1
  81. package/dist/chunk-FGEVDA53.js.map +0 -1
  82. /package/dist/{read-namespaced-template-KUENIIDC.js.map → read-namespaced-template-WYIGUH7S.js.map} +0 -0
@@ -9,6 +9,7 @@ import { Field, FieldGroup, FieldLabel } from '@admin/components/ui/field'
9
9
  import { Form } from '@admin/components/ui/form'
10
10
  import { Input } from '@admin/components/ui/input'
11
11
  import { Spinner } from '@admin/components/ui/spinner'
12
+ import { useFieldErrorVisible } from '@admin/hooks/use-field-error-visible'
12
13
  import { cn } from '@admin/utils/shared/cn'
13
14
  import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
14
15
  import Image from 'next/image'
@@ -49,19 +50,16 @@ export function ResetPasswordForm({ className, ...props }: React.ComponentProps<
49
50
  reValidateMode: 'onChange'
50
51
  })
51
52
 
53
+ const isNewPasswordErrorVisible = useFieldErrorVisible(form, 'newPassword')
54
+ const isConfirmPasswordErrorVisible = useFieldErrorVisible(form, 'confirmPassword')
55
+
52
56
  if (!token) {
53
57
  return (
54
58
  <div className={cn('flex flex-col gap-6', className)} {...props}>
55
59
  <Card className="overflow-hidden p-0">
56
60
  <CardContent className="grid grid-cols-[5fr_7fr] p-0">
57
61
  <div className="relative block bg-muted">
58
- <Image
59
- src="https://assets.betterstart.dev/assets/placeholder.png"
60
- alt=""
61
- fill
62
- sizes="42vw"
63
- className="object-cover"
64
- />
62
+ <Image src="/placeholder.png" alt="" fill sizes="42vw" className="object-cover" />
65
63
  </div>
66
64
  <div className="p-20">
67
65
  <FieldGroup className="gap-10 pb-12">
@@ -114,13 +112,7 @@ export function ResetPasswordForm({ className, ...props }: React.ComponentProps<
114
112
  <Card className="overflow-hidden p-0">
115
113
  <CardContent className="grid grid-cols-[5fr_7fr] p-0">
116
114
  <div className="relative block bg-muted">
117
- <Image
118
- src="https://assets.betterstart.dev/assets/placeholder.png"
119
- alt=""
120
- fill
121
- sizes="42vw"
122
- className="object-cover"
123
- />
115
+ <Image src="/placeholder.png" alt="" fill sizes="42vw" className="object-cover" />
124
116
  </div>
125
117
  <Form {...form}>
126
118
  <form
@@ -144,7 +136,7 @@ export function ResetPasswordForm({ className, ...props }: React.ComponentProps<
144
136
  type="password"
145
137
  autoComplete="off"
146
138
  placeholder="Enter new password"
147
- aria-invalid={Boolean(form.formState.errors.newPassword)}
139
+ aria-invalid={isNewPasswordErrorVisible}
148
140
  {...form.register('newPassword')}
149
141
  required
150
142
  disabled={isPending}
@@ -157,7 +149,7 @@ export function ResetPasswordForm({ className, ...props }: React.ComponentProps<
157
149
  type="password"
158
150
  autoComplete="off"
159
151
  placeholder="Confirm new password"
160
- aria-invalid={Boolean(form.formState.errors.confirmPassword)}
152
+ aria-invalid={isConfirmPasswordErrorVisible}
161
153
  {...form.register('confirmPassword')}
162
154
  required
163
155
  disabled={isPending}
@@ -2,8 +2,8 @@ export function ResetPasswordPageSkeleton() {
2
2
  return (
3
3
  <div className="overflow-hidden rounded-sm border bg-card">
4
4
  <div className="grid grid-cols-[5fr_7fr]">
5
- <div className="block min-h-[520px] bg-muted" />
6
- <div className="flex min-h-[520px] flex-col gap-10 p-20">
5
+ <div className="block min-h-auth-reset-panel bg-muted" />
6
+ <div className="flex min-h-auth-reset-panel flex-col gap-10 p-20">
7
7
  <div className="space-y-3">
8
8
  <div className="h-6 w-36 animate-pulse rounded-md bg-muted" />
9
9
  <div className="h-4 w-56 animate-pulse rounded-md bg-muted" />
@@ -7,7 +7,7 @@ interface AuditLogDetailRowProps {
7
7
 
8
8
  export function AuditLogDetailRow({ label, value }: AuditLogDetailRowProps) {
9
9
  return (
10
- <div className="grid grid-cols-[120px_1fr] gap-3 border-b border-border px-4 py-2 last:border-b-0">
10
+ <div className="grid grid-cols-audit-detail gap-3 border-b border-border px-4 py-2 last:border-b-0">
11
11
  <dt className="text-sm font-medium text-muted-foreground">{label}</dt>
12
12
  <dd className="min-w-0 text-sm break-words text-foreground">{value || 'Unknown'}</dd>
13
13
  </div>
@@ -248,12 +248,7 @@ export function AuditLogTable<TValue>({
248
248
  <DataGridRow
249
249
  key={row.id}
250
250
  className="cursor-pointer hover:bg-muted/50"
251
- onClick={(event) => {
252
- const target = event.target as HTMLElement
253
- if (!event.currentTarget.contains(target)) return
254
- if (target.closest('button, a, input, [role="checkbox"]')) return
255
- onRowClick(row.original)
256
- }}
251
+ onActivate={() => onRowClick(row.original)}
257
252
  >
258
253
  {row.getVisibleCells().map((cell) => (
259
254
  <DataGridCell
@@ -36,7 +36,7 @@ export function FormsSettingsTable({
36
36
  <div className={cn('flex h-full flex-col gap-6 pt-px pb-2', className)}>
37
37
  <DataGrid
38
38
  containerClassName="min-h-0 flex-1"
39
- gridTemplateColumns="minmax(200px, 1fr) minmax(240px, 2fr)"
39
+ gridTemplateColumns="minmax(var(--spacing-form-settings-name), 1fr) minmax(var(--spacing-form-settings-recipients), 2fr)"
40
40
  >
41
41
  <DataGridHeader>
42
42
  {table.getHeaderGroups().map((headerGroup) => (
@@ -57,12 +57,7 @@ export function FormsSettingsTable({
57
57
  <DataGridRow
58
58
  key={row.id}
59
59
  className="cursor-pointer hover:bg-muted/50"
60
- onClick={(event) => {
61
- const target = event.target as HTMLElement
62
- if (!event.currentTarget.contains(target)) return
63
- if (target.closest('button, a, input, [role="checkbox"]')) return
64
- onRowClick(row.original)
65
- }}
60
+ onActivate={() => onRowClick(row.original)}
66
61
  >
67
62
  {row.getVisibleCells().map((cell) => (
68
63
  <DataGridCell key={cell.id} data-column-id={cell.column.id}>
@@ -217,12 +217,7 @@ export function WebhooksTable<TValue>({
217
217
  <DataGridRow
218
218
  key={row.id}
219
219
  className="cursor-pointer hover:bg-muted/50"
220
- onClick={(e) => {
221
- const target = e.target as HTMLElement
222
- if (!e.currentTarget.contains(target)) return
223
- if (target.closest('button, a, input, [role="checkbox"], [role="switch"]')) return
224
- onRowClick(row.original)
225
- }}
220
+ onActivate={() => onRowClick(row.original)}
226
221
  >
227
222
  {row.getVisibleCells().map((cell) => (
228
223
  <DataGridCell
@@ -118,7 +118,7 @@ export function UsersTable<TValue>({
118
118
  email: session.user.email,
119
119
  name: session.user.name,
120
120
  image: session.user.image,
121
- role: (session.user as { role?: string }).role || 'member'
121
+ role: session.user.role || 'member'
122
122
  }
123
123
  : null
124
124
  },
@@ -23,6 +23,8 @@ export interface AdminMedia {
23
23
  updatedBy: string | null
24
24
  createdByUser?: AdminUserSummary | null
25
25
  updatedByUser?: AdminUserSummary | null
26
+ createdAtFormatted?: string
27
+ updatedAtFormatted?: string
26
28
  }
27
29
 
28
30
  export interface MediaListParams {
@@ -1,10 +1,12 @@
1
+ import { isRecord } from '@admin/utils/shared/is-record'
2
+
1
3
  export function normalizeComparableValue(value: unknown): string {
2
4
  if (value instanceof Date) return value.toISOString()
3
5
  if (Array.isArray(value)) {
4
6
  return JSON.stringify(value.map((item) => normalizeComparableValue(item)))
5
7
  }
6
- if (value && typeof value === 'object') {
7
- const entries = Object.entries(value as Record<string, unknown>)
8
+ if (isRecord(value)) {
9
+ const entries = Object.entries(value)
8
10
  .sort(([a], [b]) => a.localeCompare(b))
9
11
  .map(([key, item]) => [key, normalizeComparableValue(item)])
10
12
  return JSON.stringify(entries)
@@ -0,0 +1,12 @@
1
+ export function formatActionDate(value: unknown): string {
2
+ if (typeof value !== 'string') return ''
3
+ const date = new Date(value)
4
+ if (Number.isNaN(date.getTime())) return ''
5
+
6
+ return date.toLocaleDateString('en-US', {
7
+ month: 'short',
8
+ day: '2-digit',
9
+ year: 'numeric',
10
+ timeZone: 'UTC'
11
+ })
12
+ }
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod/v3'
2
+
3
+ export const devModeCodeLanguageSchema = z.enum(['tsx', 'ts', 'json'])
4
+
5
+ export const devModeSnippetIconSchema = z.enum([
6
+ 'eye',
7
+ 'file-code',
8
+ 'inbox',
9
+ 'list',
10
+ 'pencil',
11
+ 'plus',
12
+ 'save',
13
+ 'send',
14
+ 'trash'
15
+ ])
16
+
17
+ export const devModeSnippetExampleSchema = z.object({
18
+ label: z.string(),
19
+ language: devModeCodeLanguageSchema,
20
+ code: z.string()
21
+ })
22
+
23
+ export const devModeSnippetCodeTabSchema = z.object({
24
+ id: z.string(),
25
+ label: z.string(),
26
+ language: devModeCodeLanguageSchema,
27
+ code: z.string()
28
+ })
29
+
30
+ export const devModeSnippetSchema = z.object({
31
+ id: z.string(),
32
+ label: z.string(),
33
+ kind: z.enum(['jsx', 'query', 'mutation']),
34
+ icon: devModeSnippetIconSchema,
35
+ language: devModeCodeLanguageSchema,
36
+ code: z.string(),
37
+ codeTabs: z.array(devModeSnippetCodeTabSchema).optional(),
38
+ payloadExample: devModeSnippetExampleSchema.optional(),
39
+ responseExample: devModeSnippetExampleSchema.optional()
40
+ })
41
+
42
+ export const devModeLifecycleHookExampleSchema = z.object({
43
+ id: z.string(),
44
+ label: z.string(),
45
+ event: z.enum([
46
+ 'beforeCreate',
47
+ 'afterCreate',
48
+ 'beforeUpdate',
49
+ 'afterUpdate',
50
+ 'beforeDelete',
51
+ 'afterDelete',
52
+ 'beforePublish',
53
+ 'afterPublish',
54
+ 'beforeUnpublish',
55
+ 'afterUnpublish',
56
+ 'beforeRestore',
57
+ 'afterRestore',
58
+ 'beforeReorder',
59
+ 'afterReorder'
60
+ ]),
61
+ phase: z.enum(['before', 'after']),
62
+ icon: devModeSnippetIconSchema,
63
+ language: devModeCodeLanguageSchema,
64
+ code: z.string(),
65
+ contextExample: devModeSnippetExampleSchema
66
+ })
@@ -0,0 +1,11 @@
1
+ import type { DevModeLifecycleHookExample } from '@admin/components/shared/dev-mode/dev-mode-types'
2
+ import { devModeLifecycleHookExampleSchema } from '@admin/utils/dev-mode/dev-mode-json-schemas'
3
+ import { z } from 'zod/v3'
4
+
5
+ const lifecycleHooksSchema = z.array(devModeLifecycleHookExampleSchema)
6
+
7
+ export function parseDevModeLifecycleHooks<Raw>(
8
+ values: readonly Raw[]
9
+ ): DevModeLifecycleHookExample[] {
10
+ return lifecycleHooksSchema.parse(values)
11
+ }
@@ -0,0 +1,9 @@
1
+ import type { DevModeSnippet } from '@admin/components/shared/dev-mode/dev-mode-types'
2
+ import { devModeSnippetSchema } from '@admin/utils/dev-mode/dev-mode-json-schemas'
3
+ import { z } from 'zod/v3'
4
+
5
+ const snippetsSchema = z.array(devModeSnippetSchema)
6
+
7
+ export function parseDevModeSnippets<Raw>(values: readonly Raw[]): DevModeSnippet[] {
8
+ return snippetsSchema.parse(values)
9
+ }
@@ -0,0 +1,13 @@
1
+ import type { Transaction } from '@tiptap/pm/state'
2
+
3
+ export function setTaskItemChecked(
4
+ tr: Transaction,
5
+ position: number | undefined,
6
+ checked: boolean
7
+ ): boolean {
8
+ if (typeof position !== 'number') return false
9
+
10
+ const taskItem = tr.doc.nodeAt(position)
11
+ tr.setNodeMarkup(position, undefined, { ...taskItem?.attrs, checked })
12
+ return true
13
+ }
@@ -15,7 +15,8 @@ export function updateNodesAttr<A extends string = string, V = unknown>(
15
15
  const currentNode = tr.doc.nodeAt(pos)
16
16
  if (!currentNode) continue
17
17
 
18
- const prevValue = (currentNode.attrs as Record<string, unknown>)[attrName] as V | undefined
18
+ const attrs: Record<string, unknown> = currentNode.attrs
19
+ const prevValue = attrs[attrName] as V | undefined
19
20
  const resolvedNext =
20
21
  typeof next === 'function' ? (next as (p: V | undefined) => V | undefined)(prevValue) : next
21
22
 
@@ -0,0 +1,13 @@
1
+ export const emailTheme = {
2
+ pageBackground: '#f8fafc',
3
+ surface: '#ffffff',
4
+ border: '#e2e8f0',
5
+ headerBackground: '#0f172a',
6
+ text: '#0f172a',
7
+ bodyText: '#334155',
8
+ mutedText: '#475569',
9
+ inverseText: '#ffffff',
10
+ mutedInverseText: '#cbd5e1',
11
+ compactWidth: '560px',
12
+ contentWidth: '640px'
13
+ } as const
@@ -1,3 +1,4 @@
1
+ import { emailTheme } from '@admin/utils/email/email-theme'
1
2
  import { escapeHtml } from '@admin/utils/email/escape-html'
2
3
  import { formatEmailValue } from '@admin/utils/email/format-email-value'
3
4
 
@@ -8,8 +9,8 @@ export function renderFormSubmissionRows(submissionData: Record<string, unknown>
8
9
  const displayValue = escapeHtml(formatEmailValue(value))
9
10
 
10
11
  return `<tr>
11
- <th style="border-bottom:1px solid #e2e8f0;color:#475569;font-size:12px;font-weight:600;padding:12px;text-align:left;text-transform:uppercase;vertical-align:top;width:36%;">${label}</th>
12
- <td style="border-bottom:1px solid #e2e8f0;color:#0f172a;font-size:14px;line-height:20px;padding:12px;vertical-align:top;">${displayValue}</td>
12
+ <th style="border-bottom:1px solid ${emailTheme.border};color:${emailTheme.mutedText};font-size:12px;font-weight:600;padding:12px;text-align:left;text-transform:uppercase;vertical-align:top;width:36%;">${label}</th>
13
+ <td style="border-bottom:1px solid ${emailTheme.border};color:${emailTheme.text};font-size:14px;line-height:20px;padding:12px;vertical-align:top;">${displayValue}</td>
13
14
  </tr>`
14
15
  })
15
16
  .join('\n')
@@ -1,4 +1,5 @@
1
1
  import type { FormEmailDeliveryInput } from '@admin/utils/email/form-email-delivery-input'
2
+ import { emailTheme } from '@admin/utils/email/email-theme'
2
3
  import { escapeHtml } from '@admin/utils/email/escape-html'
3
4
  import { renderFormSubmissionRows } from '@admin/utils/email/render-form-submission-rows'
4
5
 
@@ -10,15 +11,15 @@ export function renderNotificationEmail(input: FormEmailDeliveryInput): string {
10
11
 
11
12
  return `<!doctype html>
12
13
  <html>
13
- <body style="margin:0;background:#f8fafc;font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#0f172a;">
14
- <div style="margin:0 auto;max-width:640px;padding:40px 20px;">
15
- <div style="border-radius:12px;background:#ffffff;border:1px solid #e2e8f0;overflow:hidden;">
16
- <div style="background:#0f172a;color:#ffffff;padding:24px 28px;">
17
- <p style="margin:0;color:#cbd5e1;font-size:13px;line-height:20px;">${formName}</p>
14
+ <body style="margin:0;background:${emailTheme.pageBackground};font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:${emailTheme.text};">
15
+ <div style="margin:0 auto;max-width:${emailTheme.contentWidth};padding:40px 20px;">
16
+ <div style="border-radius:12px;background:${emailTheme.surface};border:1px solid ${emailTheme.border};overflow:hidden;">
17
+ <div style="background:${emailTheme.headerBackground};color:${emailTheme.inverseText};padding:24px 28px;">
18
+ <p style="margin:0;color:${emailTheme.mutedInverseText};font-size:13px;line-height:20px;">${formName}</p>
18
19
  <h1 style="margin:4px 0 0;font-size:24px;line-height:32px;">New ${formLabel} submission</h1>
19
20
  </div>
20
21
  <div style="padding:24px 28px;">
21
- <p style="margin:0 0 20px;color:#475569;font-size:14px;line-height:20px;">Submitted at ${submittedAt}</p>
22
+ <p style="margin:0 0 20px;color:${emailTheme.mutedText};font-size:14px;line-height:20px;">Submitted at ${submittedAt}</p>
22
23
  <table cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;width:100%;">
23
24
  <tbody>
24
25
  ${rows}
@@ -1,4 +1,5 @@
1
1
  import type { FormEmailDeliveryInput } from '@admin/utils/email/form-email-delivery-input'
2
+ import { emailTheme } from '@admin/utils/email/email-theme'
2
3
  import { escapeHtml } from '@admin/utils/email/escape-html'
3
4
 
4
5
  export function renderThankYouEmail(input: FormEmailDeliveryInput): string {
@@ -6,11 +7,11 @@ export function renderThankYouEmail(input: FormEmailDeliveryInput): string {
6
7
 
7
8
  return `<!doctype html>
8
9
  <html>
9
- <body style="margin:0;background:#f8fafc;font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#0f172a;">
10
- <div style="margin:0 auto;max-width:560px;padding:40px 20px;">
11
- <div style="border-radius:12px;background:#ffffff;padding:32px;border:1px solid #e2e8f0;">
10
+ <body style="margin:0;background:${emailTheme.pageBackground};font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:${emailTheme.text};">
11
+ <div style="margin:0 auto;max-width:${emailTheme.compactWidth};padding:40px 20px;">
12
+ <div style="border-radius:12px;background:${emailTheme.surface};padding:32px;border:1px solid ${emailTheme.border};">
12
13
  <h1 style="margin:0 0 16px;font-size:24px;line-height:32px;">Thank you</h1>
13
- <p style="margin:0;color:#334155;font-size:16px;line-height:24px;">We received your ${formLabel} submission and will get back to you shortly.</p>
14
+ <p style="margin:0;color:${emailTheme.bodyText};font-size:16px;line-height:24px;">We received your ${formLabel} submission and will get back to you shortly.</p>
14
15
  </div>
15
16
  </div>
16
17
  </body>
@@ -1,5 +1,7 @@
1
+ import { isRecord } from '@admin/utils/shared/is-record'
2
+
1
3
  export function readStringProperty(value: unknown, key: string): string | undefined {
2
- if (typeof value !== 'object' || value === null) return undefined
3
- const property = (value as Record<string, unknown>)[key]
4
+ if (!isRecord(value)) return undefined
5
+ const property = value[key]
4
6
  return typeof property === 'string' ? property : undefined
5
7
  }
@@ -1,4 +1,5 @@
1
1
  import { readStringProperty } from '@admin/utils/error/read-string-property'
2
+ import { isRecord } from '@admin/utils/shared/is-record'
2
3
 
3
4
  const POSTGRES_MESSAGES: Record<string, string> = {
4
5
  '23502': 'A required value is missing.',
@@ -13,10 +14,7 @@ const AUTH_MESSAGES: Record<string, string> = {
13
14
  }
14
15
 
15
16
  export function recognize(error: unknown): string | undefined {
16
- const authCode = readStringProperty(
17
- (error as { body?: unknown } | null)?.body ?? undefined,
18
- 'code'
19
- )
17
+ const authCode = readStringProperty(isRecord(error) ? error.body : undefined, 'code')
20
18
  if (authCode && AUTH_MESSAGES[authCode]) return AUTH_MESSAGES[authCode]
21
19
 
22
20
  const sqlState = readStringProperty(error, 'code')
@@ -1,6 +1,5 @@
1
1
  import { recognize } from '@admin/utils/error/recognize'
2
2
 
3
3
  export function toActionError(error: unknown, fallback: string): string {
4
- console.error(error)
5
4
  return recognize(error) ?? fallback
6
5
  }
@@ -1,8 +1,10 @@
1
+ import { isRecord } from '@admin/utils/shared/is-record'
2
+
1
3
  export function cloneDefaultValue(value: unknown): unknown {
2
4
  if (Array.isArray(value)) return value.map(cloneDefaultValue)
3
5
 
4
- if (value && typeof value === 'object') {
5
- const clonedEntries = Object.entries(value as Record<string, unknown>).map(
6
+ if (isRecord(value)) {
7
+ const clonedEntries = Object.entries(value).map(
6
8
  ([key, childValue]) => [key, cloneDefaultValue(childValue)] as const
7
9
  )
8
10
  return Object.fromEntries(clonedEntries)
@@ -0,0 +1,3 @@
1
+ export function isRecord(value: unknown): value is Record<string, unknown> {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
3
+ }
@@ -1,6 +1,7 @@
1
1
  import type { RowData } from '@tanstack/react-table'
2
+ import { isRecord } from '@admin/utils/shared/is-record'
2
3
 
3
4
  export function defaultGetId<TData extends RowData>(row: TData): string | null | undefined {
4
- const id = (row as { id?: unknown }).id
5
- return typeof id === 'string' ? id : null
5
+ if (!isRecord(row)) return null
6
+ return typeof row.id === 'string' ? row.id : null
6
7
  }
@@ -15,7 +15,7 @@ export function EditorToolbar({
15
15
  role="toolbar"
16
16
  aria-label={ariaLabel}
17
17
  className={cn(
18
- 'sticky top-0 z-10 flex min-h-12 w-full items-center gap-1 overflow-x-auto overscroll-x-contain border-b border-border bg-card px-2 [scrollbar-width:none] max-[480px]:absolute max-[480px]:top-auto max-[480px]:h-[calc(2.75rem+env(safe-area-inset-bottom,0px))] max-[480px]:flex-nowrap max-[480px]:justify-start max-[480px]:border-t max-[480px]:border-b-0 max-[480px]:pb-[env(safe-area-inset-bottom,0px)] [&::-webkit-scrollbar]:hidden',
18
+ 'sticky top-0 z-10 flex min-h-12 w-full items-center gap-1 overflow-x-auto overscroll-x-contain border-b border-border bg-card px-2 [scrollbar-width:none] max-xs:absolute max-xs:top-auto max-xs:h-[calc(2.75rem+env(safe-area-inset-bottom,0px))] max-xs:flex-nowrap max-xs:justify-start max-xs:border-t max-xs:border-b-0 max-xs:pb-[env(safe-area-inset-bottom,0px)] [&::-webkit-scrollbar]:hidden',
19
19
  className
20
20
  )}
21
21
  >
@@ -33,6 +33,10 @@ export function SlashCommandMenu({
33
33
  searchQuery: string
34
34
  }) {
35
35
  const activeItemRef = React.useRef<HTMLDivElement | null>(null)
36
+
37
+ function assignActiveItem(node: HTMLDivElement | null) {
38
+ activeItemRef.current = node
39
+ }
36
40
  const searchInputRef = React.useRef<HTMLInputElement | null>(null)
37
41
  const activeCommandId = commands[activeIndex]?.id
38
42
 
@@ -159,9 +163,7 @@ export function SlashCommandMenu({
159
163
  index={index}
160
164
  key={command.id}
161
165
  data-active={isActive ? 'true' : undefined}
162
- ref={(node) => {
163
- if (isActive) activeItemRef.current = node
164
- }}
166
+ ref={isActive ? assignActiveItem : undefined}
165
167
  onMouseDown={(event) => event.preventDefault()}
166
168
  onMouseEnter={() => onActiveIndexChange(index)}
167
169
  value={command}
@@ -120,7 +120,7 @@ export function SourceMode({
120
120
  return (
121
121
  <div
122
122
  className={cn(
123
- 'content-editor-wrapper relative flex max-h-[90vh] min-h-[240px] resize-y flex-col overflow-hidden rounded-md border bg-background',
123
+ 'content-editor-wrapper relative flex max-h-[90vh] min-h-source-editor resize-y flex-col overflow-hidden rounded-md border bg-background',
124
124
  { 'opacity-50': disabled },
125
125
  className
126
126
  )}
@@ -14,6 +14,8 @@ import { cn } from '@admin/utils/shared/cn'
14
14
  import { NodeViewWrapper } from '@tiptap/react'
15
15
  import NextImage from 'next/image'
16
16
 
17
+ const EDITOR_IMAGE_SIZES = '768px'
18
+
17
19
  export const RemovableImageNode: React.FC<NodeViewProps> = (props) => {
18
20
  const { editor, node } = props
19
21
  const src = getStringAttr(node.attrs.src)
@@ -118,7 +120,7 @@ export const RemovableImageNode: React.FC<NodeViewProps> = (props) => {
118
120
  alt={alt}
119
121
  title={title || undefined}
120
122
  fill
121
- sizes="768px"
123
+ sizes={EDITOR_IMAGE_SIZES}
122
124
  unoptimized
123
125
  className="!m-0 !h-full !w-full !max-w-full object-contain"
124
126
  onLoad={handleImageLoad}
@@ -5,6 +5,7 @@ import { getTaskItemCheckboxLabel } from '@admin/utils/editor/get-task-item-chec
5
5
  import { hasOwnAttribute } from '@admin/utils/editor/has-own-attribute'
6
6
  import { isTaskItemChecked } from '@admin/utils/editor/is-task-item-checked'
7
7
  import { setElementAttribute } from '@admin/utils/editor/set-element-attribute'
8
+ import { setTaskItemChecked } from '@admin/utils/editor/set-task-item-checked'
8
9
  import { getRenderedAttributes } from '@tiptap/core'
9
10
  import { TaskItem } from '@tiptap/extension-list'
10
11
  import { createRoot } from 'react-dom/client'
@@ -20,7 +21,7 @@ export const ContentEditorTaskItem = TaskItem.extend({
20
21
  const checkboxMount = document.createElement('span')
21
22
  const content = document.createElement('div')
22
23
  const checkboxRoot = createRoot(checkboxMount)
23
- const staticAttributes = this.options.HTMLAttributes as Record<string, unknown>
24
+ const staticAttributes: Record<string, unknown> = this.options.HTMLAttributes
24
25
 
25
26
  checkboxContainer.contentEditable = 'false'
26
27
  checkboxContainer.className = 'content-editor-task-item-checkbox'
@@ -52,17 +53,7 @@ export const ContentEditorTaskItem = TaskItem.extend({
52
53
  editor
53
54
  .chain()
54
55
  .focus(undefined, { scrollIntoView: false })
55
- .command(({ tr }) => {
56
- const position = getPos()
57
- if (typeof position !== 'number') return false
58
-
59
- const taskItem = tr.doc.nodeAt(position)
60
- tr.setNodeMarkup(position, undefined, {
61
- ...taskItem?.attrs,
62
- checked
63
- })
64
- return true
65
- })
56
+ .command(({ tr }) => setTaskItemChecked(tr, getPos(), checked))
66
57
  .run()
67
58
  return
68
59
  }
@@ -89,7 +80,7 @@ export const ContentEditorTaskItem = TaskItem.extend({
89
80
  listItem.dataset.checked = String(renderedChecked)
90
81
  }
91
82
 
92
- applyAttributes(HTMLAttributes as Record<string, unknown>)
83
+ applyAttributes(HTMLAttributes)
93
84
  renderCheckbox()
94
85
 
95
86
  let prevRenderedAttributeKeys = new Set(Object.keys(HTMLAttributes))
@@ -104,10 +95,10 @@ export const ContentEditorTaskItem = TaskItem.extend({
104
95
  renderedChecked = isTaskItemChecked(updatedNode)
105
96
 
106
97
  const extensionAttributes = editor.extensionManager.attributes
107
- const newHTMLAttributes = getRenderedAttributes(
98
+ const newHTMLAttributes: Record<string, unknown> = getRenderedAttributes(
108
99
  updatedNode,
109
100
  extensionAttributes
110
- ) as Record<string, unknown>
101
+ )
111
102
  const newKeys = new Set(Object.keys(newHTMLAttributes))
112
103
 
113
104
  prevRenderedAttributeKeys.forEach((key) => {
@@ -93,12 +93,7 @@ export function HeadingDropdownMenu({
93
93
  }
94
94
  ></DropdownMenuTrigger>
95
95
 
96
- <DropdownMenuContent
97
- align="start"
98
- className="w-auto min-w-40"
99
- finalFocus={false}
100
- portalled={false}
101
- >
96
+ <DropdownMenuContent align="start" className="w-auto min-w-40" finalFocus={false}>
102
97
  <DropdownMenuGroup>
103
98
  {levels.map((level) => (
104
99
  <HeadingDropdownMenuItem key={`heading-${level}`} editor={editor} level={level} />
@@ -83,12 +83,7 @@ export function ListDropdownMenu({
83
83
  <ChevronDownIcon className="tiptap-button-dropdown-small" />
84
84
  </DropdownMenuTrigger>
85
85
 
86
- <DropdownMenuContent
87
- align="start"
88
- className="w-auto min-w-40"
89
- finalFocus={false}
90
- portalled={false}
91
- >
86
+ <DropdownMenuContent align="start" className="w-auto min-w-40" finalFocus={false}>
92
87
  <DropdownMenuGroup>
93
88
  {filteredLists.map((option) => (
94
89
  <ListDropdownMenuItem
@@ -132,14 +132,21 @@ export function DateRangePicker({
132
132
  >
133
133
  <PopoverTrigger
134
134
  render={
135
- <Button variant="outline" className={cn('w-[206px]', className)} disabled={disabled} />
135
+ <Button
136
+ variant="outline"
137
+ className={cn('w-date-range-trigger', className)}
138
+ disabled={disabled}
139
+ />
136
140
  }
137
141
  >
138
142
  <CalendarIcon />
139
143
  <span className="min-w-0 flex-1 truncate text-left tabular-nums">{triggerLabel}</span>
140
144
  <ChevronDown />
141
145
  </PopoverTrigger>
142
- <PopoverContent className="w-[282px] gap-0 overflow-hidden rounded-sm p-0" align="start">
146
+ <PopoverContent
147
+ className="w-date-range-popover gap-0 overflow-hidden rounded-sm p-0"
148
+ align="start"
149
+ >
143
150
  <Calendar
144
151
  mode="range"
145
152
  month={calendarMonth}