betterstart-cli 0.0.18 → 0.0.20

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "posts",
3
- "type": "entity",
4
3
  "label": "Posts",
4
+ "type": "entity",
5
5
  "description": "Manage blog posts",
6
6
  "icon": "FileText",
7
7
  "navGroup": {
@@ -13,142 +13,110 @@
13
13
  "main": {
14
14
  "fields": [
15
15
  {
16
- "name": "postTabs",
17
- "type": "tabs",
18
- "tabs": [
16
+ "name": "title",
17
+ "type": "string",
18
+ "label": "Title",
19
+ "required": true
20
+ },
21
+ {
22
+ "name": "slug",
23
+ "type": "string",
24
+ "label": "Slug",
25
+ "hint": "URL-friendly identifier (auto-generated from title)"
26
+ },
27
+ {
28
+ "name": "excerpt",
29
+ "type": "text",
30
+ "label": "Excerpt",
31
+ "hint": "Short summary for the content"
32
+ },
33
+ {
34
+ "name": "content",
35
+ "type": "richtext",
36
+ "output": "html",
37
+ "height": "fill"
38
+ }
39
+ ]
40
+ },
41
+ "sidebar": {
42
+ "fields": [
43
+ {
44
+ "name": "generalSidebar",
45
+ "type": "section",
46
+ "heading": {
47
+ "icon": "bolt",
48
+ "title": "General"
49
+ },
50
+ "fields": [
19
51
  {
20
- "name": "content",
21
- "label": "Content",
22
- "icon": "file-text",
23
- "slot": {
24
- "main": {
25
- "fields": [
26
- {
27
- "name": "title",
28
- "type": "string",
29
- "label": "Title",
30
- "required": true
31
- },
32
- {
33
- "name": "slug",
34
- "type": "string",
35
- "label": "Slug",
36
- "hint": "URL-friendly identifier (auto-generated from title)"
37
- },
38
- {
39
- "name": "excerpt",
40
- "type": "text",
41
- "label": "Excerpt",
42
- "hint": "Short summary for the content"
43
- },
44
- {
45
- "name": "content",
46
- "type": "richtext",
47
- "output": "html",
48
- "height": "fill"
49
- }
50
- ]
51
- },
52
- "sidebar": {
53
- "fields": [
54
- {
55
- "name": "generalSidebar",
56
- "type": "section",
57
- "heading": {
58
- "icon": "bolt",
59
- "title": "General"
60
- },
61
- "fields": [
62
- {
63
- "name": "coverImage",
64
- "type": "image",
65
- "label": "Cover Image"
66
- },
67
- {
68
- "name": "categories",
69
- "type": "select",
70
- "label": "Categories",
71
- "placeholder": "Select categories",
72
- "hint": "Assign one or more categories",
73
- "default": "category-1",
74
- "creatable": true,
75
- "multiple": true,
76
- "previewLimit": 1,
77
- "options": [
78
- {
79
- "label": "Category 1",
80
- "value": "category-1"
81
- },
82
- {
83
- "label": "Category 2",
84
- "value": "category-2"
85
- }
86
- ]
87
- }
88
- ]
89
- }
90
- ]
91
- }
92
- }
52
+ "name": "coverImage",
53
+ "type": "image",
54
+ "label": "Cover Image"
93
55
  },
94
56
  {
95
- "name": "seo",
96
- "label": "SEO",
97
- "icon": "globe",
98
- "slot": {
99
- "main": {
100
- "fields": [
101
- {
102
- "name": "seoTitle",
103
- "type": "string",
104
- "label": "SEO Title",
105
- "defaultValueFrom": "title",
106
- "hint": "Overrides the post title in search results",
107
- "length": 70
108
- },
109
- {
110
- "name": "seoDescription",
111
- "type": "text",
112
- "label": "SEO Description",
113
- "defaultValueFrom": "excerpt",
114
- "hint": "Overrides the excerpt in search result snippets"
115
- },
116
- {
117
- "name": "noIndex",
118
- "type": "boolean",
119
- "label": "No Index",
120
- "hint": "Ask search engines not to index this post",
121
- "default": false
122
- },
123
- {
124
- "name": "noFollow",
125
- "type": "boolean",
126
- "label": "No Follow",
127
- "hint": "Ask search engines not to follow links on this post",
128
- "default": false
129
- }
130
- ]
57
+ "name": "categories",
58
+ "type": "select",
59
+ "label": "Categories",
60
+ "placeholder": "Select categories",
61
+ "hint": "Assign one or more categories",
62
+ "default": "category-1",
63
+ "creatable": true,
64
+ "multiple": true,
65
+ "previewLimit": 1,
66
+ "options": [
67
+ {
68
+ "label": "Category 1",
69
+ "value": "category-1"
131
70
  },
132
- "sidebar": {
133
- "fields": [
134
- {
135
- "name": "seoSidebar",
136
- "type": "section",
137
- "heading": {
138
- "icon": "globe",
139
- "title": "SEO Settings"
140
- },
141
- "fields": [
142
- {
143
- "name": "openGraphImage",
144
- "type": "image",
145
- "label": "OpenGraph Image"
146
- }
147
- ]
148
- }
149
- ]
71
+ {
72
+ "label": "Category 2",
73
+ "value": "category-2"
150
74
  }
151
- }
75
+ ]
76
+ }
77
+ ]
78
+ },
79
+ {
80
+ "name": "seoSidebar",
81
+ "type": "section",
82
+ "heading": {
83
+ "icon": "globe",
84
+ "title": "SEO Settings"
85
+ },
86
+ "fields": [
87
+ {
88
+ "name": "openGraphImage",
89
+ "type": "image",
90
+ "label": "OpenGraph Image"
91
+ },
92
+ {
93
+ "name": "seoTitle",
94
+ "type": "string",
95
+ "label": "SEO Title",
96
+ "defaultValueFrom": "title",
97
+ "hint": "Overrides the post title in search results",
98
+ "length": 70
99
+ },
100
+ {
101
+ "name": "seoDescription",
102
+ "type": "text",
103
+ "label": "SEO Description",
104
+ "defaultValueFrom": "excerpt",
105
+ "hint": "Overrides the excerpt in search result snippets"
106
+ },
107
+ {
108
+ "name": "noIndex",
109
+ "type": "boolean",
110
+ "label": "No Index",
111
+ "hint": "Ask search engines not to index this post",
112
+ "default": false
113
+ },
114
+ {
115
+ "name": "noFollow",
116
+ "type": "boolean",
117
+ "label": "No Follow",
118
+ "hint": "Ask search engines not to follow links on this post",
119
+ "default": false
152
120
  }
153
121
  ]
154
122
  }
@@ -123,6 +123,6 @@
123
123
  "draft": true
124
124
  },
125
125
  "search": {
126
- "fields": ["title", "excerpt"]
126
+ "fields": ["title"]
127
127
  }
128
128
  }
@@ -27,6 +27,7 @@ export function MediaPreview({ media, isFilled = false, showMetadata = false }:
27
27
  })}
28
28
  fill
29
29
  sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 200px"
30
+ unoptimized
30
31
  loading="lazy"
31
32
  decoding="async"
32
33
  draggable={false}
@@ -1,5 +1,6 @@
1
1
  import type { AdminNavigationItem } from '@admin/types/navigation'
2
2
  import { House, ImagePlay } from 'lucide-react'
3
+ import { contactNav } from './navigation/contact'
3
4
  import { menusNav } from './navigation/menus'
4
5
  import { portfolioNav } from './navigation/portfolio'
5
6
  import { postsNav } from './navigation/posts'
@@ -30,7 +31,7 @@ function comparePositions(a?: number, b?: number): number {
30
31
  return 0
31
32
  }
32
33
 
33
- const combinedNavigation = [...coreNav, ...menusNav, ...portfolioNav, ...postsNav]
34
+ const combinedNavigation = [...coreNav, ...contactNav, ...menusNav, ...portfolioNav, ...postsNav]
34
35
 
35
36
  const dashboard = combinedNavigation.find((item) => item.href === '/admin')
36
37
  const others = combinedNavigation.filter((item) => item.href !== '/admin')
@@ -195,7 +195,7 @@ export function GalleryField({
195
195
  <CarouselContent className="-ml-3">
196
196
  {canAdd && !disabled ? (
197
197
  <CarouselItem className="basis-[168px] pl-3 sm:basis-[184px]">
198
- <Card className="h-full">
198
+ <Card className="h-full" onClick={() => setOpen(true)}>
199
199
  <CardContent className="p-2 flex flex-col cursor-pointer">
200
200
  <div className="flex size-full bg-background border border-border rounded-sm aspect-square overflow-hidden text-muted-foreground hover:text-foreground transition-colors items-center flex-col justify-center gap-1">
201
201
  <Plus className="size-6" strokeWidth={1.5} />
@@ -6,39 +6,27 @@ import { Card, CardContent } from '@admin/components/ui/card'
6
6
  import type { AdminMedia } from '@admin/types'
7
7
  import { cn } from '@admin/utils/shared/cn'
8
8
  import { ImageIcon } from 'lucide-react'
9
- import type * as React from 'react'
10
9
 
11
10
  interface MediaFieldEmptyStateProps {
12
11
  accept?: string
13
- addButtonLabel?: string
14
12
  className?: string
15
13
  disabled?: boolean
16
- isRemovable?: boolean
17
14
  mode?: 'single' | 'multi'
18
15
  open: boolean
19
16
  onOpenChange: (open: boolean) => void
20
- onRemove?: React.MouseEventHandler<HTMLButtonElement>
21
17
  onSelect: (selected: AdminMedia | AdminMedia[]) => void
22
- showAddButton?: boolean
23
18
  }
24
19
 
25
20
  export function MediaFieldEmptyState({
26
21
  accept,
27
- addButtonLabel = 'Add Image',
28
22
  className,
29
23
  disabled,
30
- isRemovable = false,
31
24
  mode = 'single',
32
25
  open,
33
26
  onOpenChange,
34
- onRemove,
35
- onSelect,
36
- showAddButton = false
27
+ onSelect
37
28
  }: MediaFieldEmptyStateProps) {
38
- const shouldShowAddButton = showAddButton || isRemovable
39
- const shouldShowActions = !disabled && (shouldShowAddButton || onRemove)
40
-
41
- function handleAddImage(e: React.MouseEvent) {
29
+ function handleAddImage(e: React.MouseEvent<HTMLButtonElement>) {
42
30
  e.preventDefault()
43
31
  e.stopPropagation()
44
32
  onOpenChange(true)
@@ -54,36 +42,20 @@ export function MediaFieldEmptyState({
54
42
  >
55
43
  <CardContent className="relative p-0 justify-center items-center flex flex-col h-40 hover:bg-muted/50 transition-colors duration-200">
56
44
  <ImageIcon
57
- className={cn('text-muted-foreground transition-opacity', {
58
- 'group-hover:opacity-0 group-focus-within:opacity-0': shouldShowActions
59
- })}
45
+ className="text-muted-foreground transition-opacity group-hover:opacity-0 group-focus-within:opacity-0"
60
46
  strokeWidth={1}
61
47
  />
62
- {shouldShowActions && (
63
- <div className="absolute inset-0 flex items-center justify-center gap-1 opacity-0 transition-opacity duration-200 ease-out group-hover:opacity-100 group-focus-within:opacity-100">
64
- {shouldShowAddButton ? (
65
- <Button
66
- type="button"
67
- data-media-field-action="true"
68
- variant="outline"
69
- onClick={handleAddImage}
70
- className="border-primary/10"
71
- >
72
- {addButtonLabel}
73
- </Button>
74
- ) : null}
75
- {onRemove ? (
76
- <Button
77
- type="button"
78
- data-media-field-action="true"
79
- variant="default"
80
- onClick={onRemove}
81
- >
82
- Remove
83
- </Button>
84
- ) : null}
85
- </div>
86
- )}
48
+ <div className="absolute inset-0 flex items-center justify-center gap-1 opacity-0 transition-opacity duration-200 ease-out group-hover:opacity-100 group-focus-within:opacity-100">
49
+ <Button
50
+ type="button"
51
+ data-media-field-action="true"
52
+ variant="outline"
53
+ onClick={handleAddImage}
54
+ className="border-primary/10"
55
+ >
56
+ Add Image
57
+ </Button>
58
+ </div>
87
59
  </CardContent>
88
60
  </Card>
89
61
 
@@ -174,7 +174,6 @@ export function MediaField({
174
174
  isRemovable={isRemovable}
175
175
  open={open}
176
176
  onOpenChange={setOpen}
177
- onRemove={handleRemove}
178
177
  onSelect={handleSelect}
179
178
  />
180
179
  )
@@ -4,7 +4,7 @@
4
4
  "title": "BetterStart Schema Definition",
5
5
  "description": "JSON Schema for validating custom schema files (courses.json, testimonials.json, instructors.json, events.json, etc.)",
6
6
  "type": "object",
7
- "required": ["type", "name", "label"],
7
+ "required": ["name", "label"],
8
8
  "additionalProperties": false,
9
9
  "properties": {
10
10
  "$schema": {
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "type": {
32
32
  "type": "string",
33
- "description": "Schema type: 'entity' for CRUD list, 'single' for singleton record (site settings, about page), or 'form' for public submission forms",
33
+ "description": "Schema type: 'entity' for CRUD list (default), 'single' for singleton record (site settings, about page), or 'form' for public submission forms",
34
34
  "enum": ["entity", "single", "form"]
35
35
  },
36
36
  "fields": {
@@ -157,9 +157,6 @@
157
157
  {
158
158
  "$ref": "#/$defs/stringField"
159
159
  },
160
- {
161
- "$ref": "#/$defs/emailField"
162
- },
163
160
  {
164
161
  "$ref": "#/$defs/textField"
165
162
  },
@@ -426,61 +423,6 @@
426
423
  }
427
424
  }
428
425
  },
429
- "emailField": {
430
- "type": "object",
431
- "description": "Email input field",
432
- "required": ["name", "type"],
433
- "additionalProperties": false,
434
- "properties": {
435
- "name": {
436
- "type": "string"
437
- },
438
- "type": {
439
- "const": "email"
440
- },
441
- "label": {
442
- "type": "string"
443
- },
444
- "required": {
445
- "type": "boolean"
446
- },
447
- "length": {
448
- "type": "integer",
449
- "description": "Maximum character length",
450
- "minimum": 1,
451
- "maximum": 65535
452
- },
453
- "hint": {
454
- "type": "string"
455
- },
456
- "placeholder": {
457
- "type": "string",
458
- "description": "Placeholder text displayed in the email input"
459
- },
460
- "default": {
461
- "type": "string"
462
- },
463
- "defaultValueFrom": {
464
- "$ref": "#/$defs/defaultValueFrom"
465
- },
466
- "hasIcon": {
467
- "type": "boolean",
468
- "description": "Whether this field has an associated icon picker"
469
- },
470
- "hidden": {
471
- "type": "boolean",
472
- "description": "Whether this field is hidden from the form UI but still exists in the database"
473
- },
474
- "showWhen": {
475
- "$ref": "#/$defs/showWhenCondition"
476
- },
477
- "height": {
478
- "enum": ["auto", "fill"],
479
- "default": "auto",
480
- "description": "Generated field height behavior: auto size or fill remaining flexible space."
481
- }
482
- }
483
- },
484
426
  "markdownField": {
485
427
  "type": "object",
486
428
  "description": "Rich text editor with markdown support",
@@ -1279,10 +1221,6 @@
1279
1221
  "type": "string",
1280
1222
  "description": "Tab display label"
1281
1223
  },
1282
- "icon": {
1283
- "type": "string",
1284
- "description": "Lucide icon name rendered before the tab label"
1285
- },
1286
1224
  "fields": {
1287
1225
  "type": "array",
1288
1226
  "description": "Fields within this tab",
@@ -1,14 +1,30 @@
1
1
  'use client'
2
2
 
3
3
  import { cn } from '@admin/utils/shared/cn'
4
- import type * as React from 'react'
4
+ import * as React from 'react'
5
5
  import { Drawer as DrawerPrimitive } from 'vaul'
6
6
 
7
7
  function Drawer({
8
+ children,
9
+ container,
8
10
  handleOnly = true,
9
11
  ...props
10
12
  }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
11
- return <DrawerPrimitive.Root data-slot="drawer" handleOnly={handleOnly} {...props} />
13
+ const portalContainerRef = React.useRef<HTMLDivElement>(null)
14
+
15
+ return (
16
+ <>
17
+ <div ref={portalContainerRef} data-slot="drawer-container" className="contents" />
18
+ <DrawerPrimitive.Root
19
+ data-slot="drawer"
20
+ container={container ?? portalContainerRef.current}
21
+ handleOnly={handleOnly}
22
+ {...props}
23
+ >
24
+ {children}
25
+ </DrawerPrimitive.Root>
26
+ </>
27
+ )
12
28
  }
13
29
 
14
30
  function DrawerTrigger({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
package/dist/cli.js CHANGED
@@ -15366,7 +15366,7 @@ export type TabValue = (typeof TAB_VALUES)[number]
15366
15366
  const metadataStrip = showMetadataWidget ? ` {initialData?.id != null && (
15367
15367
  <div
15368
15368
  className={cn(
15369
- 'sticky z-10 flex flex-col gap-1 px-4 bg-background h-14 xl:flex-row xl:items-center xl:justify-between top-14 transition-shadow overflow-x-clip',
15369
+ 'sticky z-100 flex flex-col gap-1 px-4 bg-background h-14 xl:flex-row xl:items-center xl:justify-between top-14 transition-shadow overflow-x-clip',
15370
15370
  {
15371
15371
  'shadow-modal/2': hasScrolled,
15372
15372
  'border-b border-border/80': !hasScrolled
@@ -15449,11 +15449,31 @@ ${relState}` : ""}${creatableSelectState ? `
15449
15449
  ${creatableSelectState}` : ""}${staticMultiSelectState ? `
15450
15450
  ${staticMultiSelectState}` : ""}${hasDraft ? "\n const [submitIntent, setSubmitIntent] = React.useState<'draft' | 'publish' | null>(null)" : ""}
15451
15451
 
15452
+ const defaultValues: FormValues = {
15453
+ ${defaultValues}
15454
+ }
15455
+
15456
+ const form = useForm<FormValues>({
15457
+ resolver: standardSchemaResolver(formSchema),
15458
+ mode: 'onBlur',
15459
+ reValidateMode: 'onChange',
15460
+ defaultValues
15461
+ })
15462
+ const defaultValuesRef = React.useRef(defaultValues)
15463
+ defaultValuesRef.current = defaultValues
15464
+ React.useEffect(
15465
+ () => () => {
15466
+ form.reset(defaultValuesRef.current)
15467
+ },
15468
+ [form]
15469
+ )
15470
+
15452
15471
  const createMutation = useMutation({
15453
15472
  mutationFn: (data: ${Singular}CreateInput) => create${Singular}(data),
15454
15473
  onSuccess: async () => {
15455
15474
  toast.success('${Singular} created successfully')
15456
15475
  await queryClient.invalidateQueries({ queryKey: ['${plural}'] })
15476
+ form.reset(defaultValues)
15457
15477
  router.push('${adminRoutePath}/${schema.name}')
15458
15478
  },
15459
15479
  onError: (error: Error) => {
@@ -15474,25 +15494,6 @@ ${staticMultiSelectState}` : ""}${hasDraft ? "\n const [submitIntent, setSubmit
15474
15494
  })
15475
15495
 
15476
15496
  const isPending = createMutation.isPending || updateMutation.isPending
15477
-
15478
- const defaultValues: FormValues = {
15479
- ${defaultValues}
15480
- }
15481
-
15482
- const form = useForm<FormValues>({
15483
- resolver: standardSchemaResolver(formSchema),
15484
- mode: 'onBlur',
15485
- reValidateMode: 'onChange',
15486
- defaultValues
15487
- })
15488
- const defaultValuesRef = React.useRef(defaultValues)
15489
- defaultValuesRef.current = defaultValues
15490
- React.useEffect(
15491
- () => () => {
15492
- form.reset(defaultValuesRef.current)
15493
- },
15494
- [form]
15495
- )
15496
15497
  const { isDirty } = form.formState
15497
15498
  ${hasDraft ? " const canPublishPristineDraft = Boolean(initialData && !initialData.published)\n const canSubmitDraftAction = Boolean(initialData?.published || isDirty)\n const canSubmitPublishAction = isDirty || canPublishPristineDraft\n" : ""}
15498
15499
  ${defaultValueFromSyncFunctions ? `
@@ -15607,9 +15608,7 @@ ${topLevelTabsField ? `import type { TabValue } from '${hookImportPath}'
15607
15608
  import { ${hookName} } from '${hookImportPath}'
15608
15609
  import { ${contentComponentName} } from './${schema.name}-form-content'
15609
15610
 
15610
- ${metadataOffsetConst}export type { FormValues } from '${hookImportPath}'
15611
-
15612
- interface ${Singular}FormProps {
15611
+ ${metadataOffsetConst}interface ${Singular}FormProps {
15613
15612
  initialData?: ${Singular}
15614
15613
  pageTitle: string
15615
15614
  }
@@ -21131,6 +21130,10 @@ ${styleText(
21131
21130
  value: "resend",
21132
21131
  label: "Resend"
21133
21132
  },
21133
+ {
21134
+ value: "skip",
21135
+ label: "Skip"
21136
+ },
21134
21137
  {
21135
21138
  value: "cloudflare",
21136
21139
  label: `Cloudflare ${styleText("dim", "(coming soon)")}`,