betterstart-cli 0.0.19 → 0.0.21

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.
@@ -123,6 +123,6 @@
123
123
  "draft": true
124
124
  },
125
125
  "search": {
126
- "fields": ["title", "excerpt"]
126
+ "fields": ["title"]
127
127
  }
128
128
  }
@@ -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
@@ -3056,7 +3056,7 @@ async function resolvePasswordEnvValue(options) {
3056
3056
  options.overwriteEnvKeys.add(options.key);
3057
3057
  return result.trim();
3058
3058
  }
3059
- async function promptResendConfig(cwd) {
3059
+ async function collectResendConfig(cwd) {
3060
3060
  const overwriteEnvKeys = /* @__PURE__ */ new Set();
3061
3061
  const apiKey = await resolvePasswordEnvValue({
3062
3062
  cwd,
@@ -3083,9 +3083,8 @@ async function promptResendConfig(cwd) {
3083
3083
  }
3084
3084
  }
3085
3085
  });
3086
- const envResult = appendEnvVars(
3087
- cwd,
3088
- [
3086
+ return {
3087
+ sections: [
3089
3088
  {
3090
3089
  header: "Email (Resend)",
3091
3090
  vars: [
@@ -3094,15 +3093,19 @@ async function promptResendConfig(cwd) {
3094
3093
  ]
3095
3094
  }
3096
3095
  ],
3097
- overwriteEnvKeys
3098
- );
3096
+ overwriteKeys: overwriteEnvKeys
3097
+ };
3098
+ }
3099
+ async function promptResendConfig(cwd) {
3100
+ const collected = await collectResendConfig(cwd);
3101
+ const envResult = appendEnvVars(cwd, collected.sections, collected.overwriteKeys);
3099
3102
  return {
3100
3103
  configured: true,
3101
3104
  addedEnvKeys: envResult.added,
3102
3105
  updatedEnvKeys: envResult.updated
3103
3106
  };
3104
3107
  }
3105
- async function promptR2Config(cwd) {
3108
+ async function collectR2Config(cwd) {
3106
3109
  const overwriteEnvKeys = /* @__PURE__ */ new Set();
3107
3110
  const accountId = await resolveTextEnvValue({
3108
3111
  cwd,
@@ -3157,9 +3160,8 @@ async function promptR2Config(cwd) {
3157
3160
  }
3158
3161
  }
3159
3162
  });
3160
- const envResult = appendEnvVars(
3161
- cwd,
3162
- [
3163
+ return {
3164
+ sections: [
3163
3165
  {
3164
3166
  header: "Storage (Cloudflare R2)",
3165
3167
  vars: [
@@ -3171,14 +3173,34 @@ async function promptR2Config(cwd) {
3171
3173
  ]
3172
3174
  }
3173
3175
  ],
3174
- overwriteEnvKeys
3175
- );
3176
+ overwriteKeys: overwriteEnvKeys
3177
+ };
3178
+ }
3179
+ async function promptR2Config(cwd) {
3180
+ const collected = await collectR2Config(cwd);
3181
+ const envResult = appendEnvVars(cwd, collected.sections, collected.overwriteKeys);
3176
3182
  return {
3177
3183
  configured: true,
3178
3184
  addedEnvKeys: envResult.added,
3179
3185
  updatedEnvKeys: envResult.updated
3180
3186
  };
3181
3187
  }
3188
+ async function collectIntegrationConfig(cwd, integrationIds) {
3189
+ const sections = [];
3190
+ const overwriteKeys = /* @__PURE__ */ new Set();
3191
+ for (const integrationId of resolveIntegrationInstallOrder(integrationIds)) {
3192
+ const definition = getIntegrationDefinition(integrationId);
3193
+ const collected = definition.configure === "resend" ? await collectResendConfig(cwd) : definition.configure === "r2" ? await collectR2Config(cwd) : void 0;
3194
+ if (!collected) {
3195
+ continue;
3196
+ }
3197
+ sections.push(...collected.sections);
3198
+ for (const key of collected.overwriteKeys) {
3199
+ overwriteKeys.add(key);
3200
+ }
3201
+ }
3202
+ return { sections, overwriteKeys };
3203
+ }
3182
3204
  async function configureIntegration(definition, cwd, interactive) {
3183
3205
  if (definition.configure === "resend") {
3184
3206
  if (interactive) {
@@ -15366,7 +15388,7 @@ export type TabValue = (typeof TAB_VALUES)[number]
15366
15388
  const metadataStrip = showMetadataWidget ? ` {initialData?.id != null && (
15367
15389
  <div
15368
15390
  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',
15391
+ '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
15392
  {
15371
15393
  'shadow-modal/2': hasScrolled,
15372
15394
  'border-b border-border/80': !hasScrolled
@@ -15449,11 +15471,31 @@ ${relState}` : ""}${creatableSelectState ? `
15449
15471
  ${creatableSelectState}` : ""}${staticMultiSelectState ? `
15450
15472
  ${staticMultiSelectState}` : ""}${hasDraft ? "\n const [submitIntent, setSubmitIntent] = React.useState<'draft' | 'publish' | null>(null)" : ""}
15451
15473
 
15474
+ const defaultValues: FormValues = {
15475
+ ${defaultValues}
15476
+ }
15477
+
15478
+ const form = useForm<FormValues>({
15479
+ resolver: standardSchemaResolver(formSchema),
15480
+ mode: 'onBlur',
15481
+ reValidateMode: 'onChange',
15482
+ defaultValues
15483
+ })
15484
+ const defaultValuesRef = React.useRef(defaultValues)
15485
+ defaultValuesRef.current = defaultValues
15486
+ React.useEffect(
15487
+ () => () => {
15488
+ form.reset(defaultValuesRef.current)
15489
+ },
15490
+ [form]
15491
+ )
15492
+
15452
15493
  const createMutation = useMutation({
15453
15494
  mutationFn: (data: ${Singular}CreateInput) => create${Singular}(data),
15454
15495
  onSuccess: async () => {
15455
15496
  toast.success('${Singular} created successfully')
15456
15497
  await queryClient.invalidateQueries({ queryKey: ['${plural}'] })
15498
+ form.reset(defaultValues)
15457
15499
  router.push('${adminRoutePath}/${schema.name}')
15458
15500
  },
15459
15501
  onError: (error: Error) => {
@@ -15474,25 +15516,6 @@ ${staticMultiSelectState}` : ""}${hasDraft ? "\n const [submitIntent, setSubmit
15474
15516
  })
15475
15517
 
15476
15518
  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
15519
  const { isDirty } = form.formState
15497
15520
  ${hasDraft ? " const canPublishPristineDraft = Boolean(initialData && !initialData.published)\n const canSubmitDraftAction = Boolean(initialData?.published || isDirty)\n const canSubmitPublishAction = isDirty || canPublishPristineDraft\n" : ""}
15498
15521
  ${defaultValueFromSyncFunctions ? `
@@ -15607,9 +15630,7 @@ ${topLevelTabsField ? `import type { TabValue } from '${hookImportPath}'
15607
15630
  import { ${hookName} } from '${hookImportPath}'
15608
15631
  import { ${contentComponentName} } from './${schema.name}-form-content'
15609
15632
 
15610
- ${metadataOffsetConst}export type { FormValues } from '${hookImportPath}'
15611
-
15612
- interface ${Singular}FormProps {
15633
+ ${metadataOffsetConst}interface ${Singular}FormProps {
15613
15634
  initialData?: ${Singular}
15614
15635
  pageTitle: string
15615
15636
  }
@@ -21131,14 +21152,14 @@ ${styleText(
21131
21152
  value: "resend",
21132
21153
  label: "Resend"
21133
21154
  },
21134
- {
21135
- value: "skip",
21136
- label: "Skip"
21137
- },
21138
21155
  {
21139
21156
  value: "cloudflare",
21140
21157
  label: `Cloudflare ${styleText("dim", "(coming soon)")}`,
21141
21158
  disabled: true
21159
+ },
21160
+ {
21161
+ value: "skip",
21162
+ label: "Skip"
21142
21163
  }
21143
21164
  ],
21144
21165
  initialValue: "resend"
@@ -23620,6 +23641,7 @@ async function runInitCommand(name, options) {
23620
23641
  integrations: [],
23621
23642
  storage: "local"
23622
23643
  } : await promptPlugins();
23644
+ const collectedIntegrationConfig = !options.yes && pluginSelection.integrations.length > 0 ? await collectIntegrationConfig(cwd, pluginSelection.integrations) : { sections: [], overwriteKeys: /* @__PURE__ */ new Set() };
23623
23645
  let databaseUrl;
23624
23646
  const existingDbUrl = readExistingDbUrl(cwd);
23625
23647
  if (options.yes) {
@@ -23679,6 +23701,13 @@ async function runInitCommand(name, options) {
23679
23701
  });
23680
23702
  s.message("Environment variables");
23681
23703
  const envResult = scaffoldEnv(cwd, { databaseUrl, namespace });
23704
+ if (collectedIntegrationConfig.sections.length > 0) {
23705
+ appendEnvVars(
23706
+ cwd,
23707
+ collectedIntegrationConfig.sections,
23708
+ collectedIntegrationConfig.overwriteKeys
23709
+ );
23710
+ }
23682
23711
  const envCount = envResult.added.length + envResult.updated.length;
23683
23712
  results.push({
23684
23713
  label: "Environment variables",
@@ -23807,7 +23836,9 @@ async function runInitCommand(name, options) {
23807
23836
  config: resolvedPluginInstallResult.config,
23808
23837
  pm,
23809
23838
  integrationIds: pluginSelection.integrations,
23810
- interactive: !options.yes,
23839
+ // Secrets were already collected up front and written to .env.local,
23840
+ // so the install runs without prompting (rule: no mid-scaffold input).
23841
+ interactive: false,
23811
23842
  includeBiome: project2.linter.type === "none"
23812
23843
  });
23813
23844
  })() : Promise.resolve({