betterstart-cli 0.0.13 → 0.0.15

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 (76) hide show
  1. package/dist/assets/adapters/next/plugins/blog/schemas/menus.json +51 -11
  2. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar.tsx +7 -1
  3. package/dist/assets/adapters/next/templates/init/components/layouts/sidebar-branding.tsx +1 -1
  4. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-grid.tsx +107 -0
  5. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table.tsx +58 -45
  6. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/lifecycle-hooks-tab.tsx +3 -3
  7. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/snippets-tab.tsx +1 -1
  8. package/dist/assets/adapters/next/templates/init/components/shared/entity-filters-bar.tsx +1 -1
  9. package/dist/assets/adapters/next/templates/init/components/shared/media/edit-media-dialog-content.tsx +2 -2
  10. package/dist/assets/adapters/next/templates/init/components/shared/media/media-url-importer.tsx +1 -1
  11. package/dist/assets/adapters/next/templates/init/components/shared/sort-indicator.tsx +7 -3
  12. package/dist/assets/adapters/next/templates/init/data/navigation.ts +2 -0
  13. package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-table-button.ts +85 -0
  14. package/dist/assets/adapters/next/templates/init/lib/actions/audit/get-audit-logs.ts +41 -16
  15. package/dist/assets/adapters/next/templates/init/lib/actions/audit/index.ts +2 -0
  16. package/dist/assets/adapters/next/templates/init/lib/actions/audit/types.ts +5 -0
  17. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +2 -1
  18. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/types.ts +2 -1
  19. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-drawer.tsx +51 -43
  20. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-row.tsx +2 -4
  21. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-content.tsx +465 -179
  22. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-skeleton.tsx +3 -15
  23. package/dist/assets/adapters/next/templates/init/pages/dashboard-page.tsx +1 -1
  24. package/dist/assets/adapters/next/templates/init/pages/forgot-password-page-skeleton.tsx +1 -1
  25. package/dist/assets/adapters/next/templates/init/pages/login-page-skeleton.tsx +1 -1
  26. package/dist/assets/adapters/next/templates/init/pages/reset-password-page-skeleton.tsx +1 -1
  27. package/dist/assets/adapters/next/templates/init/pages/users/users-page-content.tsx +83 -2
  28. package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +126 -78
  29. package/dist/assets/adapters/next/templates/init/utils/editor/tiptap.ts +7 -8
  30. package/dist/assets/adapters/next/templates/init/utils/form/list-field.ts +17 -0
  31. package/dist/assets/adapters/next/templates/init/utils/user/user.ts +70 -0
  32. package/dist/assets/shared-assets/react-admin/custom/content-editor/math-bubble-menu.tsx +1 -1
  33. package/dist/assets/shared-assets/react-admin/custom/content-editor/selection-bubble-menu.tsx +1 -1
  34. package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +0 -2
  35. package/dist/assets/shared-assets/react-admin/custom/content-editor/table-button.tsx +17 -65
  36. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-control-popover.tsx +3 -8
  37. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-popover.tsx +1 -2
  38. package/dist/assets/shared-assets/react-admin/custom/date-range-picker.tsx +1 -1
  39. package/dist/assets/shared-assets/react-admin/custom/dynamic-list-field.tsx +20 -8
  40. package/dist/assets/shared-assets/react-admin/custom/gallery-field.tsx +2 -2
  41. package/dist/assets/shared-assets/react-admin/custom/media-gallery-field.tsx +2 -2
  42. package/dist/assets/shared-assets/react-admin/custom/nested-object-list-field.tsx +211 -0
  43. package/dist/assets/shared-assets/react-admin/custom/placeholder-card.tsx +1 -1
  44. package/dist/assets/shared-assets/react-admin/custom/placeholder.tsx +1 -1
  45. package/dist/assets/shared-assets/react-admin/custom/sortable-gallery-item.tsx +1 -1
  46. package/dist/assets/shared-assets/react-admin/schema.json +136 -0
  47. package/dist/assets/shared-assets/react-admin/ui/accordion.tsx +1 -1
  48. package/dist/assets/shared-assets/react-admin/ui/alert.tsx +1 -1
  49. package/dist/assets/shared-assets/react-admin/ui/card.tsx +2 -2
  50. package/dist/assets/shared-assets/react-admin/ui/chart.tsx +1 -1
  51. package/dist/assets/shared-assets/react-admin/ui/checkbox.tsx +1 -1
  52. package/dist/assets/shared-assets/react-admin/ui/combobox.tsx +2 -2
  53. package/dist/assets/shared-assets/react-admin/ui/command.tsx +1 -1
  54. package/dist/assets/shared-assets/react-admin/ui/context-menu.tsx +6 -6
  55. package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +2 -2
  56. package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +6 -6
  57. package/dist/assets/shared-assets/react-admin/ui/empty.tsx +2 -2
  58. package/dist/assets/shared-assets/react-admin/ui/field.tsx +1 -1
  59. package/dist/assets/shared-assets/react-admin/ui/form.tsx +1 -1
  60. package/dist/assets/shared-assets/react-admin/ui/hover-card.tsx +1 -1
  61. package/dist/assets/shared-assets/react-admin/ui/input-group.tsx +1 -1
  62. package/dist/assets/shared-assets/react-admin/ui/item.tsx +2 -2
  63. package/dist/assets/shared-assets/react-admin/ui/menubar.tsx +7 -7
  64. package/dist/assets/shared-assets/react-admin/ui/navigation-menu.tsx +4 -4
  65. package/dist/assets/shared-assets/react-admin/ui/popover.tsx +1 -1
  66. package/dist/assets/shared-assets/react-admin/ui/resizable.tsx +1 -1
  67. package/dist/assets/shared-assets/react-admin/ui/select.tsx +2 -2
  68. package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +3 -3
  69. package/dist/assets/shared-assets/react-admin/ui/skeleton.tsx +1 -1
  70. package/dist/assets/shared-assets/react-admin/ui/tabs.tsx +2 -2
  71. package/dist/assets/shared-assets/react-admin/ui/textarea.tsx +1 -1
  72. package/dist/assets/shared-assets/react-admin/ui/tooltip.tsx +1 -1
  73. package/dist/cli.js +2701 -1654
  74. package/dist/cli.js.map +1 -1
  75. package/package.json +10 -2
  76. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-filters.tsx +0 -152
@@ -4,16 +4,9 @@ import { Button } from '@admin/components/ui/button'
4
4
  import { Input } from '@admin/components/ui/input'
5
5
  import { Label } from '@admin/components/ui/label'
6
6
  import { Popover, PopoverContent, PopoverTrigger } from '@admin/components/ui/popover'
7
- import { normalizeCount } from '@admin/utils/editor/table-input'
8
- import { insertContentEditorTable } from '@admin/utils/editor/tiptap'
7
+ import { useTableButton } from '@admin/hooks/content-editor/use-table-button'
9
8
  import type { Editor } from '@tiptap/core'
10
9
  import { TableIcon } from 'lucide-react'
11
- import * as React from 'react'
12
-
13
- const DEFAULT_TABLE_ROWS = 3
14
- const DEFAULT_TABLE_COLS = 3
15
- const MIN_TABLE_ROWS = 2
16
- const MIN_TABLE_COLS = 1
17
10
 
18
11
  interface TableButtonProps {
19
12
  disabled?: boolean
@@ -22,95 +15,54 @@ interface TableButtonProps {
22
15
  }
23
16
 
24
17
  export function TableButton({ disabled, editor, onInsertTable }: TableButtonProps) {
25
- const [isOpen, setIsOpen] = React.useState(false)
26
- const [rows, setRows] = React.useState(String(DEFAULT_TABLE_ROWS))
27
- const [cols, setCols] = React.useState(String(DEFAULT_TABLE_COLS))
28
- const rowsId = React.useId()
29
- const colsId = React.useId()
30
- const canInsert = Boolean(onInsertTable || editor?.isEditable)
31
-
32
- const closePopover = React.useCallback(() => {
33
- setIsOpen(false)
34
- window.setTimeout(() => setIsOpen(false), 0)
35
- }, [])
36
-
37
- const insertTable = React.useCallback(() => {
38
- if (disabled || !canInsert) return
39
-
40
- const rowCount = normalizeCount(rows, DEFAULT_TABLE_ROWS, MIN_TABLE_ROWS)
41
- const colCount = normalizeCount(cols, DEFAULT_TABLE_COLS, MIN_TABLE_COLS)
42
-
43
- if (onInsertTable) {
44
- onInsertTable({ cols: colCount, rows: rowCount })
45
- closePopover()
46
- return
47
- }
48
-
49
- if (!editor) return
50
-
51
- insertContentEditorTable(editor, rowCount, colCount)
52
- closePopover()
53
- }, [canInsert, closePopover, cols, disabled, editor, onInsertTable, rows])
54
-
55
- const handleKeyDown = React.useCallback(
56
- (event: React.KeyboardEvent<HTMLDivElement>) => {
57
- if (event.key !== 'Enter') return
58
-
59
- event.preventDefault()
60
- event.stopPropagation()
61
- insertTable()
62
- },
63
- [insertTable]
64
- )
18
+ const tableButton = useTableButton({ disabled, editor, onInsertTable })
65
19
 
66
20
  return (
67
- <Popover open={isOpen} onOpenChange={setIsOpen}>
21
+ <Popover open={tableButton.isOpen} onOpenChange={tableButton.setIsOpen}>
68
22
  <PopoverTrigger asChild>
69
23
  <Button
70
24
  type="button"
71
25
  variant="ghost"
72
26
  size="sm"
73
- disabled={disabled || !canInsert}
27
+ disabled={tableButton.isDisabled}
74
28
  aria-label="Insert table"
75
29
  title="Table"
76
- onPointerDown={(event) => event.preventDefault()}
77
- onClick={() => setIsOpen((current) => !current)}
78
30
  >
79
31
  <TableIcon />
80
32
  <span className="tiptap-button-text">Table</span>
81
33
  </Button>
82
34
  </PopoverTrigger>
83
35
  <PopoverContent align="start" className="w-72 p-4">
84
- <div className="flex flex-col gap-3" onKeyDownCapture={handleKeyDown}>
36
+ <div className="flex flex-col gap-3" onKeyDownCapture={tableButton.handleKeyDown}>
85
37
  <div className="grid grid-cols-2 gap-2">
86
38
  <div className="flex flex-col gap-1.5">
87
- <Label htmlFor={rowsId}>Rows</Label>
39
+ <Label htmlFor={tableButton.rowsId}>Rows</Label>
88
40
  <Input
89
- id={rowsId}
41
+ id={tableButton.rowsId}
90
42
  inputMode="numeric"
91
- min={MIN_TABLE_ROWS}
92
- onChange={(event) => setRows(event.target.value)}
43
+ min={tableButton.minRows}
44
+ onChange={tableButton.handleRowsChange}
93
45
  type="number"
94
- value={rows}
46
+ value={tableButton.rows}
95
47
  />
96
48
  </div>
97
49
  <div className="flex flex-col gap-1.5">
98
- <Label htmlFor={colsId}>Cols</Label>
50
+ <Label htmlFor={tableButton.colsId}>Cols</Label>
99
51
  <Input
100
- id={colsId}
52
+ id={tableButton.colsId}
101
53
  inputMode="numeric"
102
- min={MIN_TABLE_COLS}
103
- onChange={(event) => setCols(event.target.value)}
54
+ min={tableButton.minCols}
55
+ onChange={tableButton.handleColsChange}
104
56
  type="number"
105
- value={cols}
57
+ value={tableButton.cols}
106
58
  />
107
59
  </div>
108
60
  </div>
109
61
  <Button
110
62
  type="button"
111
63
  variant="default"
112
- disabled={disabled || !canInsert}
113
- onClick={insertTable}
64
+ disabled={tableButton.isDisabled}
65
+ onClick={tableButton.insertTable}
114
66
  >
115
67
  Insert
116
68
  </Button>
@@ -9,6 +9,7 @@ import { LinkMain, type LinkMainProps } from './link-main'
9
9
 
10
10
  export interface LinkControlPopoverProps extends Omit<LinkButtonProps, 'type'>, LinkMainProps {
11
11
  canSet?: boolean
12
+ isActive?: boolean
12
13
  label?: string
13
14
  onOpenChange?: (isOpen: boolean) => void
14
15
  open?: boolean
@@ -17,7 +18,7 @@ export interface LinkControlPopoverProps extends Omit<LinkButtonProps, 'type'>,
17
18
  export function LinkControlPopover({
18
19
  canSet = true,
19
20
  children,
20
- isActive,
21
+ isActive = false,
21
22
  label = 'Link',
22
23
  onClick,
23
24
  onOpenChange,
@@ -75,13 +76,7 @@ export function LinkControlPopover({
75
76
  </PopoverTrigger>
76
77
 
77
78
  <PopoverContent className="w-auto max-w-[calc(100vw-2rem)]">
78
- <LinkMain
79
- url={url}
80
- setUrl={setUrl}
81
- setLink={handleSetLink}
82
- removeLink={removeLink}
83
- isActive={isActive}
84
- />
79
+ <LinkMain url={url} setUrl={setUrl} setLink={handleSetLink} removeLink={removeLink} />
85
80
  </PopoverContent>
86
81
  </Popover>
87
82
  )
@@ -57,7 +57,7 @@ export function LinkPopover({
57
57
  }: LinkPopoverProps) {
58
58
  const { editor } = useTiptapEditor(providedEditor)
59
59
 
60
- const { isVisible, canSet, isActive, url, setUrl, setLink, removeLink, openLink, label, Icon } =
60
+ const { isVisible, canSet, isActive, url, setUrl, setLink, removeLink, label, Icon } =
61
61
  useLinkPopover({
62
62
  editor,
63
63
  hideWhenUnavailable: shouldHideUnavailableControl(availability),
@@ -105,7 +105,6 @@ export function LinkPopover({
105
105
  onClick={onClick}
106
106
  onOpenChange={handleOnOpenChange}
107
107
  open={isOpen}
108
- openLink={openLink}
109
108
  removeLink={removeLink}
110
109
  setLink={setLink}
111
110
  setUrl={setUrl}
@@ -139,7 +139,7 @@ export function DateRangePicker({
139
139
  <ChevronDown />
140
140
  </Button>
141
141
  </PopoverTrigger>
142
- <PopoverContent className="w-[282px] gap-0 overflow-hidden rounded-xl p-0" align="start">
142
+ <PopoverContent className="w-[282px] gap-0 overflow-hidden rounded-lg p-0" align="start">
143
143
  <Calendar
144
144
  mode="range"
145
145
  month={calendarMonth}
@@ -1,9 +1,15 @@
1
1
  'use client'
2
2
 
3
3
  import { Button } from '@admin/components/ui/button'
4
- import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@admin/components/ui/form'
4
+ import {
5
+ FormControl,
6
+ FormDescription,
7
+ FormField,
8
+ FormItem,
9
+ FormLabel,
10
+ FormMessage
11
+ } from '@admin/components/ui/form'
5
12
  import { Input } from '@admin/components/ui/input'
6
- import { Label } from '@admin/components/ui/label'
7
13
  import { cn } from '@admin/utils/shared/cn'
8
14
  import { Plus, Trash2 } from 'lucide-react'
9
15
  import { useFieldArray, useFormContext } from 'react-hook-form'
@@ -46,12 +52,18 @@ export function DynamicListField({
46
52
  return (
47
53
  <div className={cn('space-y-4', className)}>
48
54
  <div className="flex items-start justify-between gap-3">
49
- <div className="flex flex-col gap-1">
50
- <Label className={cn({ 'sr-only': hideLabel })}>{label}</Label>
51
- {description ? (
52
- <p className="text-[0.8rem] text-muted-foreground">{description}</p>
53
- ) : null}
54
- </div>
55
+ <FormField
56
+ control={control}
57
+ name={name}
58
+ render={() => (
59
+ <FormItem className="flex flex-col gap-1">
60
+ <FormLabel className={cn({ 'sr-only': hideLabel })}>{label}</FormLabel>
61
+ {description ? (
62
+ <FormDescription className="text-muted-foreground">{description}</FormDescription>
63
+ ) : null}
64
+ </FormItem>
65
+ )}
66
+ />
55
67
  <Button
56
68
  type="button"
57
69
  variant="outline"
@@ -97,7 +97,7 @@ export function GalleryField({
97
97
  <button
98
98
  type="button"
99
99
  onClick={() => setOpen(true)}
100
- className="flex aspect-square items-center justify-center rounded-xl border border-dashed transition-colors hover:border-primary/50 hover:bg-muted/50"
100
+ className="flex aspect-square items-center justify-center rounded-lg border border-dashed transition-colors hover:border-primary/50 hover:bg-muted/50"
101
101
  >
102
102
  <Plus className="size-6 text-foreground" />
103
103
  </button>
@@ -111,7 +111,7 @@ export function GalleryField({
111
111
  type="button"
112
112
  onClick={() => !disabled && setOpen(true)}
113
113
  disabled={disabled}
114
- className="flex w-full flex-col items-center justify-center gap-2 rounded-xl border border-dashed p-8 transition-colors hover:border-primary/50 hover:bg-muted/50 disabled:cursor-not-allowed disabled:opacity-50"
114
+ className="flex w-full flex-col items-center justify-center gap-2 rounded-lg border border-dashed p-8 transition-colors hover:border-primary/50 hover:bg-muted/50 disabled:cursor-not-allowed disabled:opacity-50"
115
115
  >
116
116
  <ImageIcon className="size-8 text-foreground" />
117
117
  <span className="text-sm text-muted-foreground">Click to select media</span>
@@ -73,7 +73,7 @@ export function MediaGalleryField({
73
73
  <div className={cn('group relative', className)}>
74
74
  <Card className="p-0">
75
75
  <CardContent className="p-0! relative">
76
- <div className="relative aspect-video rounded-xl bg-background overflow-hidden">
76
+ <div className="relative aspect-video rounded-lg bg-background overflow-hidden">
77
77
  <MediaPreview media={selectedMedia} showMetadata={Boolean(media)} />
78
78
  </div>
79
79
 
@@ -81,7 +81,7 @@ export function MediaGalleryField({
81
81
  <>
82
82
  <div
83
83
  aria-hidden="true"
84
- className="pointer-events-none absolute inset-0 bg-black/10 opacity-0 backdrop-blur-xs transition-opacity duration-300 ease-out group-hover:opacity-100 rounded-xl"
84
+ className="pointer-events-none absolute inset-0 bg-black/10 opacity-0 backdrop-blur-xs transition-opacity duration-300 ease-out group-hover:opacity-100 rounded-lg"
85
85
  />
86
86
  <div className="absolute inset-0 flex items-center justify-center gap-1 opacity-0 transition-opacity duration-200 ease-out group-hover:opacity-100">
87
87
  <Button
@@ -0,0 +1,211 @@
1
+ 'use client'
2
+
3
+ import {
4
+ Accordion,
5
+ AccordionContent,
6
+ AccordionItem,
7
+ AccordionTrigger
8
+ } from '@admin/components/ui/accordion'
9
+ import { Button } from '@admin/components/ui/button'
10
+ import { Card, CardContent, CardHeader } from '@admin/components/ui/card'
11
+ import { FormDescription, FormField, FormItem, FormLabel } from '@admin/components/ui/form'
12
+ import { cloneDefaultValue, createListItemKey } from '@admin/utils/form/list-field'
13
+ import { cn } from '@admin/utils/shared/cn'
14
+ import { Plus, X } from 'lucide-react'
15
+ import * as React from 'react'
16
+ import { useFormContext, useWatch } from 'react-hook-form'
17
+
18
+ interface NestedObjectListFieldProps {
19
+ name: string
20
+ label: string
21
+ singularLabel: string
22
+ defaultValue: Record<string, unknown>
23
+ children: (index: number) => React.ReactNode
24
+ className?: string
25
+ description?: string
26
+ disabled?: boolean
27
+ hideLabel?: boolean
28
+ maxItems?: number
29
+ renderTitle?: (index: number) => React.ReactNode
30
+ validatePaths?: string[]
31
+ }
32
+
33
+ export function NestedObjectListField({
34
+ name,
35
+ label,
36
+ singularLabel,
37
+ defaultValue,
38
+ children,
39
+ className,
40
+ description,
41
+ disabled,
42
+ hideLabel,
43
+ maxItems,
44
+ renderTitle,
45
+ validatePaths
46
+ }: NestedObjectListFieldProps) {
47
+ const { control, setValue, trigger } = useFormContext()
48
+ const watchedItems = useWatch({
49
+ control,
50
+ name
51
+ })
52
+ const items = Array.isArray(watchedItems) ? watchedItems : []
53
+ const [expanded, setExpanded] = React.useState('')
54
+ const [itemKeys, setItemKeys] = React.useState<string[]>([])
55
+ const canAddMore = !maxItems || items.length < maxItems
56
+
57
+ React.useEffect(() => {
58
+ setItemKeys((currentKeys) => {
59
+ if (currentKeys.length === items.length) return currentKeys
60
+ if (currentKeys.length > items.length) {
61
+ return currentKeys.slice(0, items.length)
62
+ }
63
+
64
+ const missingKeyCount = items.length - currentKeys.length
65
+ const nextKeys = Array.from({ length: missingKeyCount }, createListItemKey)
66
+ return [...currentKeys, ...nextKeys]
67
+ })
68
+ }, [items.length])
69
+
70
+ async function handleAdd() {
71
+ if (!canAddMore) return
72
+ if (expanded) {
73
+ const isExpandedItemValid = await validateExpandedItem()
74
+ if (!isExpandedItemValid) return
75
+ }
76
+
77
+ const nextValue = cloneDefaultValue(defaultValue) as Record<string, unknown>
78
+ const nextItems = [...items, nextValue]
79
+ setValue(name, nextItems, {
80
+ shouldDirty: true,
81
+ shouldValidate: false
82
+ })
83
+ setItemKeys((currentKeys) => [...currentKeys, createListItemKey()])
84
+ setExpanded(`item-${nextItems.length}`)
85
+ }
86
+
87
+ const handleRemove = (index: number) => {
88
+ if (disabled) return
89
+
90
+ const nextItems = items.filter((_, itemIndex) => itemIndex !== index)
91
+ setValue(name, nextItems, {
92
+ shouldDirty: true,
93
+ shouldValidate: false
94
+ })
95
+ const keepOtherKeys = (_: string, itemIndex: number) => itemIndex !== index
96
+ setItemKeys((currentKeys) => currentKeys.filter(keepOtherKeys))
97
+ setExpanded('')
98
+ }
99
+
100
+ async function validateExpandedItem() {
101
+ if (!expanded) return true
102
+
103
+ const expandedIndex = Number(expanded.replace('item-', '')) - 1
104
+ if (expandedIndex < 0) return true
105
+
106
+ const itemPath = `${name}.${expandedIndex}`
107
+ const paths =
108
+ validatePaths && validatePaths.length > 0
109
+ ? validatePaths.map((path) => `${itemPath}.${path}`)
110
+ : [itemPath]
111
+
112
+ return trigger(paths)
113
+ }
114
+
115
+ async function handleExpandedChange(nextExpanded: string) {
116
+ if (expanded && nextExpanded && nextExpanded !== expanded) {
117
+ const isExpandedItemValid = await validateExpandedItem()
118
+ if (!isExpandedItemValid) return
119
+ }
120
+
121
+ setExpanded(nextExpanded)
122
+ }
123
+
124
+ const getTitle = (index: number) => {
125
+ if (renderTitle) return renderTitle(index)
126
+ return `${singularLabel} ${index + 1}`
127
+ }
128
+
129
+ return (
130
+ <FormItem className={cn('space-y-4', className)}>
131
+ {items.length === 0 ? (
132
+ <Card>
133
+ <CardContent className="flex flex-col items-center justify-center">
134
+ <p className="text-muted-foreground">No {label} added yet.</p>
135
+ <Button
136
+ type="button"
137
+ variant="outline"
138
+ onClick={handleAdd}
139
+ disabled={disabled || !canAddMore}
140
+ >
141
+ <Plus className="size-3" />
142
+ Add {singularLabel}
143
+ </Button>
144
+ </CardContent>
145
+ </Card>
146
+ ) : (
147
+ <Card className="space-y-4 border-t border-border pt-4">
148
+ <CardHeader className="flex items-center justify-between pl-5">
149
+ <FormField
150
+ control={control}
151
+ name={name}
152
+ render={() => (
153
+ <FormItem className="flex flex-col gap-1">
154
+ <FormLabel className={cn({ 'sr-only': hideLabel })}>{label}</FormLabel>
155
+ {description ? <FormDescription>{description}</FormDescription> : null}
156
+ </FormItem>
157
+ )}
158
+ />
159
+ <Button
160
+ type="button"
161
+ variant="outline"
162
+ onClick={handleAdd}
163
+ disabled={disabled || !canAddMore}
164
+ >
165
+ <Plus className="size-3" />
166
+ Add {singularLabel}
167
+ </Button>
168
+ </CardHeader>
169
+
170
+ <CardContent className="p-0!">
171
+ <Accordion
172
+ type="single"
173
+ collapsible
174
+ value={expanded}
175
+ onValueChange={handleExpandedChange}
176
+ className="border-none"
177
+ >
178
+ {items.map((_, index) => (
179
+ <AccordionItem
180
+ key={itemKeys[index] ?? `${name}-${index}`}
181
+ value={`item-${index + 1}`}
182
+ className="bg-transparent!"
183
+ >
184
+ <div className="relative py-1">
185
+ <AccordionTrigger className="flex min-h-12 w-full items-center gap-2 py-2">
186
+ <span className="w-full pr-16">{getTitle(index)}</span>
187
+ </AccordionTrigger>
188
+ <Button
189
+ type="button"
190
+ variant="ghost"
191
+ size="icon-sm"
192
+ className="absolute right-10 top-1/2 shrink-0 -translate-y-1/2"
193
+ aria-label={`Remove ${singularLabel}`}
194
+ onClick={() => handleRemove(index)}
195
+ disabled={disabled}
196
+ >
197
+ <X />
198
+ </Button>
199
+ </div>
200
+ <AccordionContent className="space-y-5 border rounded-lg border-border p-5 bg-background mb-4">
201
+ {children(index)}
202
+ </AccordionContent>
203
+ </AccordionItem>
204
+ ))}
205
+ </Accordion>
206
+ </CardContent>
207
+ </Card>
208
+ )}
209
+ </FormItem>
210
+ )
211
+ }
@@ -19,7 +19,7 @@ export function PlaceholderCard({
19
19
  return (
20
20
  <div
21
21
  className={cn(
22
- 'flex h-75 border border-border border-dashed bg-background flex-col items-center justify-center gap-5 rounded-xl',
22
+ 'flex h-75 border border-border border-dashed bg-background flex-col items-center justify-center gap-5 rounded-lg',
23
23
  className
24
24
  )}
25
25
  >
@@ -12,7 +12,7 @@ export function Placeholder({ label, description, action, className, ...props }:
12
12
  return (
13
13
  <div
14
14
  className={cn(
15
- 'border flex items-center flex-col gap-2 border-dashed border-border rounded-xl p-8 bg-background',
15
+ 'border flex items-center flex-col gap-2 border-dashed border-border rounded-lg p-8 bg-background',
16
16
  className
17
17
  )}
18
18
  {...props}
@@ -31,7 +31,7 @@ export function SortableGalleryItem({
31
31
  <div
32
32
  ref={setNodeRef}
33
33
  style={style}
34
- className="group relative aspect-square overflow-hidden rounded-xl border bg-muted"
34
+ className="group relative aspect-square overflow-hidden rounded-lg border bg-muted"
35
35
  >
36
36
  {!disabled && (
37
37
  <div
@@ -1441,6 +1441,111 @@
1441
1441
  "$ref": "#/$defs/fieldWithCreatableSelect"
1442
1442
  }
1443
1443
  },
1444
+ "fieldArrayWithTitle": {
1445
+ "type": "array",
1446
+ "contains": {
1447
+ "$ref": "#/$defs/fieldWithTitle"
1448
+ }
1449
+ },
1450
+ "fieldWithTitle": {
1451
+ "type": "object",
1452
+ "anyOf": [
1453
+ {
1454
+ "properties": {
1455
+ "name": {
1456
+ "const": "title"
1457
+ },
1458
+ "type": {
1459
+ "enum": ["string", "varchar", "text"]
1460
+ }
1461
+ },
1462
+ "required": ["name", "type"]
1463
+ },
1464
+ {
1465
+ "properties": {
1466
+ "type": {
1467
+ "enum": ["group", "section"]
1468
+ },
1469
+ "fields": {
1470
+ "$ref": "#/$defs/fieldArrayWithTitle"
1471
+ }
1472
+ },
1473
+ "required": ["type", "fields"]
1474
+ },
1475
+ {
1476
+ "properties": {
1477
+ "tabs": {
1478
+ "type": "array",
1479
+ "contains": {
1480
+ "$ref": "#/$defs/tabWithTitle"
1481
+ }
1482
+ }
1483
+ },
1484
+ "required": ["tabs"]
1485
+ }
1486
+ ]
1487
+ },
1488
+ "tabWithTitle": {
1489
+ "type": "object",
1490
+ "anyOf": [
1491
+ {
1492
+ "properties": {
1493
+ "fields": {
1494
+ "$ref": "#/$defs/fieldArrayWithTitle"
1495
+ }
1496
+ },
1497
+ "required": ["fields"]
1498
+ },
1499
+ {
1500
+ "properties": {
1501
+ "slot": {
1502
+ "$ref": "#/$defs/slotLayoutWithTitle"
1503
+ }
1504
+ },
1505
+ "required": ["slot"]
1506
+ }
1507
+ ]
1508
+ },
1509
+ "slotLayoutWithTitle": {
1510
+ "type": "object",
1511
+ "anyOf": [
1512
+ {
1513
+ "properties": {
1514
+ "main": {
1515
+ "properties": {
1516
+ "fields": {
1517
+ "$ref": "#/$defs/fieldArrayWithTitle"
1518
+ }
1519
+ },
1520
+ "required": ["fields"]
1521
+ }
1522
+ },
1523
+ "required": ["main"]
1524
+ },
1525
+ {
1526
+ "properties": {
1527
+ "sidebar": {
1528
+ "properties": {
1529
+ "fields": {
1530
+ "$ref": "#/$defs/fieldArrayWithTitle"
1531
+ }
1532
+ },
1533
+ "required": ["fields"]
1534
+ }
1535
+ },
1536
+ "required": ["sidebar"]
1537
+ }
1538
+ ]
1539
+ },
1540
+ "stepWithTitle": {
1541
+ "type": "object",
1542
+ "properties": {
1543
+ "fields": {
1544
+ "$ref": "#/$defs/fieldArrayWithTitle"
1545
+ }
1546
+ },
1547
+ "required": ["fields"]
1548
+ },
1444
1549
  "fieldWithCreatableSelect": {
1445
1550
  "type": "object",
1446
1551
  "anyOf": [
@@ -1611,6 +1716,37 @@
1611
1716
  }
1612
1717
  ],
1613
1718
  "allOf": [
1719
+ {
1720
+ "anyOf": [
1721
+ {
1722
+ "properties": {
1723
+ "fields": {
1724
+ "$ref": "#/$defs/fieldArrayWithTitle"
1725
+ }
1726
+ },
1727
+ "required": ["fields"]
1728
+ },
1729
+ {
1730
+ "properties": {
1731
+ "slot": {
1732
+ "$ref": "#/$defs/slotLayoutWithTitle"
1733
+ }
1734
+ },
1735
+ "required": ["slot"]
1736
+ },
1737
+ {
1738
+ "properties": {
1739
+ "steps": {
1740
+ "type": "array",
1741
+ "contains": {
1742
+ "$ref": "#/$defs/stepWithTitle"
1743
+ }
1744
+ }
1745
+ },
1746
+ "required": ["steps"]
1747
+ }
1748
+ ]
1749
+ },
1614
1750
  {
1615
1751
  "if": {
1616
1752
  "properties": {
@@ -10,7 +10,7 @@ function Accordion({ className, ...props }: React.ComponentProps<typeof Accordio
10
10
  <AccordionPrimitive.Root
11
11
  data-slot="accordion"
12
12
  className={cn(
13
- 'flex w-full flex-col rounded-xl overflow-hidden border border-border bg-card shadow-2xl/1',
13
+ 'flex w-full flex-col rounded-lg overflow-hidden border border-border bg-card shadow-2xl/1',
14
14
  className
15
15
  )}
16
16
  {...props}