@betterstart/cli 0.1.27 → 0.1.29

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 (65) hide show
  1. package/dist/{chunk-SAPJG4NO.js → chunk-6JCWMKSY.js} +7 -4
  2. package/dist/{chunk-SAPJG4NO.js.map → chunk-6JCWMKSY.js.map} +1 -1
  3. package/dist/cli.js +987 -1035
  4. package/dist/cli.js.map +1 -1
  5. package/dist/drizzle-config-EDKOEZ6G.js +7 -0
  6. package/package.json +1 -1
  7. package/templates/ui/accordion.tsx +73 -42
  8. package/templates/ui/alert-dialog.tsx +155 -90
  9. package/templates/ui/alert.tsx +46 -26
  10. package/templates/ui/aspect-ratio.tsx +4 -2
  11. package/templates/ui/avatar.tsx +92 -43
  12. package/templates/ui/badge.tsx +27 -12
  13. package/templates/ui/breadcrumb.tsx +63 -60
  14. package/templates/ui/button-group.tsx +8 -8
  15. package/templates/ui/button.tsx +44 -26
  16. package/templates/ui/calendar.tsx +43 -34
  17. package/templates/ui/card.tsx +71 -34
  18. package/templates/ui/carousel.tsx +111 -115
  19. package/templates/ui/chart.tsx +197 -207
  20. package/templates/ui/checkbox.tsx +21 -20
  21. package/templates/ui/collapsible.tsx +14 -4
  22. package/templates/ui/combobox.tsx +272 -0
  23. package/templates/ui/command.tsx +139 -101
  24. package/templates/ui/context-menu.tsx +214 -156
  25. package/templates/ui/dialog.tsx +118 -77
  26. package/templates/ui/direction.tsx +20 -0
  27. package/templates/ui/drawer.tsx +89 -69
  28. package/templates/ui/dropdown-menu.tsx +228 -164
  29. package/templates/ui/empty.tsx +8 -5
  30. package/templates/ui/field.tsx +25 -32
  31. package/templates/ui/hover-card.tsx +29 -20
  32. package/templates/ui/input-group.tsx +20 -37
  33. package/templates/ui/input-otp.tsx +57 -42
  34. package/templates/ui/input.tsx +14 -17
  35. package/templates/ui/item.tsx +27 -17
  36. package/templates/ui/kbd.tsx +1 -3
  37. package/templates/ui/label.tsx +14 -14
  38. package/templates/ui/markdown-editor.tsx +1 -1
  39. package/templates/ui/menubar.tsx +220 -188
  40. package/templates/ui/native-select.tsx +42 -0
  41. package/templates/ui/navigation-menu.tsx +130 -90
  42. package/templates/ui/pagination.tsx +88 -73
  43. package/templates/ui/popover.tsx +67 -26
  44. package/templates/ui/progress.tsx +24 -18
  45. package/templates/ui/radio-group.tsx +26 -20
  46. package/templates/ui/resizable.tsx +29 -29
  47. package/templates/ui/scroll-area.tsx +47 -38
  48. package/templates/ui/select.tsx +158 -125
  49. package/templates/ui/separator.tsx +21 -19
  50. package/templates/ui/sheet.tsx +104 -95
  51. package/templates/ui/sidebar.tsx +77 -183
  52. package/templates/ui/skeleton.tsx +8 -2
  53. package/templates/ui/slider.tsx +46 -17
  54. package/templates/ui/sonner.tsx +19 -9
  55. package/templates/ui/spinner.tsx +2 -2
  56. package/templates/ui/switch.tsx +24 -20
  57. package/templates/ui/table.tsx +68 -73
  58. package/templates/ui/tabs.tsx +71 -46
  59. package/templates/ui/textarea.tsx +13 -16
  60. package/templates/ui/toggle-group.tsx +57 -28
  61. package/templates/ui/toggle.tsx +21 -20
  62. package/templates/ui/tooltip.tsx +44 -23
  63. package/dist/drizzle-config-KISB26BA.js +0 -7
  64. package/templates/ui/use-mobile.tsx +0 -19
  65. /package/dist/{drizzle-config-KISB26BA.js.map → drizzle-config-EDKOEZ6G.js.map} +0 -0
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  drizzleConfigTemplate
4
- } from "./chunk-SAPJG4NO.js";
4
+ } from "./chunk-6JCWMKSY.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { Command as Command8 } from "commander";
@@ -1404,6 +1404,7 @@ function generatePageContent(pascal, kebab, camel, label) {
1404
1404
 
1405
1405
  import type { ${pascal}SubmissionData } from '@cms/actions/${kebab}-form'
1406
1406
  import { deleteBulk${pascal}Submissions } from '@cms/actions/${kebab}-form'
1407
+ import { PageHeader } from '@cms/components/shared/page-header'
1407
1408
  import { Button } from '@cms/components/ui/button'
1408
1409
  import { Input } from '@cms/components/ui/input'
1409
1410
  import {
@@ -1417,11 +1418,11 @@ import {
1417
1418
  AlertDialogTitle,
1418
1419
  AlertDialogTrigger,
1419
1420
  } from '@cms/components/ui/alert-dialog'
1420
- import { PageHeader } from '@cms/components/shared/page-header'
1421
1421
  import { useQueryClient } from '@tanstack/react-query'
1422
1422
  import type { ColumnDef } from '@tanstack/react-table'
1423
- import { Search, Settings, Trash2 } from 'lucide-react'
1423
+ import { ChevronLeft, Search, Settings, Trash2 } from 'lucide-react'
1424
1424
  import Link from 'next/link'
1425
+ import { useRouter } from 'next/navigation'
1425
1426
  import { parseAsString, useQueryState } from 'nuqs'
1426
1427
  import { startTransition, useCallback, useState, useTransition } from 'react'
1427
1428
  import { toast } from 'sonner'
@@ -1434,6 +1435,7 @@ interface ${pascal}SubmissionsPageContentProps<TValue> {
1434
1435
  export function ${pascal}SubmissionsPageContent<TValue>({
1435
1436
  columns,
1436
1437
  }: ${pascal}SubmissionsPageContentProps<TValue>) {
1438
+ const router = useRouter()
1437
1439
  const queryClient = useQueryClient()
1438
1440
  const [search, setSearch] = useQueryState('q', parseAsString.withDefault(''))
1439
1441
  const [selectedIds, setSelectedIds] = useState<number[]>([])
@@ -1470,33 +1472,21 @@ export function ${pascal}SubmissionsPageContent<TValue>({
1470
1472
 
1471
1473
  return (
1472
1474
  <>
1473
- <div className="flex items-center justify-between border-b px-6 py-4">
1474
- <PageHeader title="${label}" description="View and manage form submissions" />
1475
- <div className="flex items-center gap-2">
1476
- <form action={searchAction} className="flex items-center gap-2">
1477
- <div className="relative">
1478
- <Search className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" />
1479
- <Input
1480
- key={search}
1481
- name="search"
1482
- placeholder="Search submissions..."
1483
- defaultValue={search}
1484
- className="w-64 pl-9"
1485
- />
1486
- </div>
1487
- <Button type="submit" variant="outline">Search</Button>
1488
- </form>
1489
- <Button variant="outline" asChild>
1490
- <Link href="/cms/forms/${kebab}/settings">
1491
- <Settings className="size-3.5" />
1492
- Settings
1493
- </Link>
1494
- </Button>
1475
+ <PageHeader title="${label}" back={<Button variant="ghost" size="icon" onClick={() => router.back()}><ChevronLeft /></Button>} search={<form action={searchAction} className="flex items-center gap-2 relative">
1476
+ <Search className="text-muted-foreground/70 pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" />
1477
+ <Input
1478
+ key={search}
1479
+ name="search"
1480
+ placeholder="Search submissions..."
1481
+ defaultValue={search}
1482
+ className="w-64 pl-9 bg-white rounded-full"
1483
+ />
1484
+ </form>} actions={<div className="flex items-center gap-2">
1495
1485
  {selectedIds.length > 0 && (
1496
1486
  <AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
1497
1487
  <AlertDialogTrigger asChild>
1498
- <Button variant="destructive">
1499
- <Trash2 className="size-3.5" />
1488
+ <Button variant="destructive" size="default">
1489
+ <Trash2 className="size-3.5 -ml-0.5" strokeWidth={2} />
1500
1490
  Delete {selectedIds.length}
1501
1491
  </Button>
1502
1492
  </AlertDialogTrigger>
@@ -1521,9 +1511,13 @@ export function ${pascal}SubmissionsPageContent<TValue>({
1521
1511
  </AlertDialogContent>
1522
1512
  </AlertDialog>
1523
1513
  )}
1524
- </div>
1525
- </div>
1526
-
1514
+ <Button variant="outline" asChild>
1515
+ <Link href="/cms/forms/${kebab}/settings">
1516
+ <Settings className="size-3.5" />
1517
+ Settings
1518
+ </Link>
1519
+ </Button>
1520
+ </div>} />
1527
1521
  <main className="space-y-4 p-6">
1528
1522
  <${pascal}SubmissionsTable
1529
1523
  columns={columns}
@@ -1597,8 +1591,9 @@ function generateViewPage(pascal, kebab, fields, label, includeDynamic) {
1597
1591
  </div>
1598
1592
  )}` : "";
1599
1593
  return `import { get${pascal}Submission } from '@cms/actions/${kebab}-form'
1594
+ import { PageHeader } from '@cms/components/shared/page-header'
1600
1595
  import { Button } from '@cms/components/ui/button'
1601
- import { ArrowLeft } from 'lucide-react'
1596
+ import { ChevronLeft } from 'lucide-react'
1602
1597
  import Link from 'next/link'
1603
1598
  import { notFound } from 'next/navigation'
1604
1599
 
@@ -1615,21 +1610,9 @@ export default async function Page({ params }: PageProps) {
1615
1610
  }
1616
1611
 
1617
1612
  return (
1618
- <div className="space-y-6 p-6">
1619
- <div className="flex items-center justify-between">
1620
- <div>
1621
- <h1 className="text-2xl font-bold">${label} Submission</h1>
1622
- <p className="text-muted-foreground">Viewing submission #{id}</p>
1623
- </div>
1624
- <Button variant="outline" asChild>
1625
- <Link href="/cms/forms/${kebab}">
1626
- <ArrowLeft className="size-4" />
1627
- Back to ${label}
1628
- </Link>
1629
- </Button>
1630
- </div>
1631
-
1632
- <div className="rounded-lg border p-6 space-y-4">
1613
+ <>
1614
+ <PageHeader title="${label} Submission" back={<Button variant="ghost" size="icon" asChild><Link href="/cms/forms/${kebab}"><ChevronLeft /></Link></Button>} />
1615
+ <div className="rounded-lg border p-6 mx-6 mt-6 space-y-4">
1633
1616
  ${fieldItems}${customFieldsSection}
1634
1617
  <div className="space-y-1 border-t pt-4">
1635
1618
  <p className="text-sm font-medium text-muted-foreground">Submitted At</p>
@@ -1641,7 +1624,7 @@ ${fieldItems}${customFieldsSection}
1641
1624
  </p>
1642
1625
  </div>
1643
1626
  </div>
1644
- </div>
1627
+ </>
1645
1628
  )
1646
1629
  }
1647
1630
  `;
@@ -1654,17 +1637,19 @@ import {
1654
1637
  testFormWebhook,
1655
1638
  upsertFormSettings,
1656
1639
  } from '@cms/actions/form-settings'
1640
+ import { PageHeader } from '@cms/components/shared/page-header'
1657
1641
  import { Button } from '@cms/components/ui/button'
1658
1642
  import { Input } from '@cms/components/ui/input'
1659
1643
  import { Label } from '@cms/components/ui/label'
1660
1644
  import { Switch } from '@cms/components/ui/switch'
1661
1645
  import { Textarea } from '@cms/components/ui/textarea'
1662
- import { ArrowLeft, Loader2 } from 'lucide-react'
1663
- import Link from 'next/link'
1646
+ import { ChevronLeft, Loader2 } from 'lucide-react'
1647
+ import { useRouter } from 'next/navigation'
1664
1648
  import { useEffect, useState, useTransition } from 'react'
1665
1649
  import { toast } from 'sonner'
1666
1650
 
1667
1651
  export default function ${pascal}SettingsPage() {
1652
+ const router = useRouter()
1668
1653
  const [notificationEmails, setNotificationEmails] = useState('')
1669
1654
  const [webhookUrl, setWebhookUrl] = useState('')
1670
1655
  const [webhookEnabled, setWebhookEnabled] = useState(false)
@@ -1718,23 +1703,9 @@ export default function ${pascal}SettingsPage() {
1718
1703
  }
1719
1704
 
1720
1705
  return (
1721
- <div className="space-y-6 p-6">
1722
- <div className="flex items-center justify-between">
1723
- <div>
1724
- <h1 className="text-2xl font-bold">${label} Settings</h1>
1725
- <p className="text-muted-foreground">
1726
- Configure notifications and webhooks for this form
1727
- </p>
1728
- </div>
1729
- <Button variant="outline" asChild>
1730
- <Link href="/cms/forms/${kebab}">
1731
- <ArrowLeft className="size-4" />
1732
- Back to ${label}
1733
- </Link>
1734
- </Button>
1735
- </div>
1736
-
1737
- <div className="space-y-6 rounded-lg border p-6">
1706
+ <>
1707
+ <PageHeader title="${label} Settings" back={<Button variant="ghost" size="icon" onClick={() => router.back()}><ChevronLeft /></Button>} />
1708
+ <div className="space-y-6 rounded-lg border p-6 mx-6 mt-6">
1738
1709
  <div className="space-y-2">
1739
1710
  <Label htmlFor="notificationEmails">Notification Emails</Label>
1740
1711
  <Textarea
@@ -1804,7 +1775,7 @@ export default function ${pascal}SettingsPage() {
1804
1775
  </Button>
1805
1776
  </div>
1806
1777
  </div>
1807
- </div>
1778
+ </>
1808
1779
  )
1809
1780
  }
1810
1781
  `;
@@ -1866,55 +1837,47 @@ function parseSingleItem(str) {
1866
1837
  const labelMatch = str.match(/label:\s*['"]([^'"]+)['"]/);
1867
1838
  const hrefMatch = str.match(/href:\s*['"]([^'"]+)['"]/);
1868
1839
  const iconMatch = str.match(/icon:\s*(\w+)/);
1840
+ const groupMatch = str.match(/group:\s*['"]([^'"]+)['"]/);
1869
1841
  if (!labelMatch || !hrefMatch) return null;
1870
1842
  const item = { label: labelMatch[1], href: hrefMatch[1] };
1871
1843
  if (iconMatch) item.icon = iconMatch[1];
1872
- const childrenMatch = str.match(/children:\s*\[([\s\S]*?)\]/);
1873
- if (childrenMatch) {
1874
- item.children = parseItemsBlock(childrenMatch[1]);
1875
- }
1844
+ if (groupMatch) item.group = groupMatch[1];
1876
1845
  return item;
1877
1846
  }
1878
1847
  function generateNavigationCode(items, iconImports) {
1879
1848
  const lines = [];
1880
- lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
1881
1849
  lines.push("import type { LucideIcon } from 'lucide-react'");
1850
+ lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
1882
1851
  lines.push("");
1883
1852
  lines.push("export interface CmsNavigationItem {");
1884
1853
  lines.push(" label: string");
1885
1854
  lines.push(" href: string");
1886
1855
  lines.push(" icon?: LucideIcon");
1887
- lines.push(" children?: CmsNavigationItem[]");
1856
+ lines.push(" group?: string");
1888
1857
  lines.push("}");
1889
1858
  lines.push("");
1890
1859
  lines.push("export const cmsNavigation: CmsNavigationItem[] = [");
1891
1860
  for (let i = 0; i < items.length; i++) {
1892
- appendItem(lines, items[i], 2, i === items.length - 1);
1861
+ appendItem(lines, items[i], i === items.length - 1);
1893
1862
  }
1894
1863
  lines.push("]");
1895
1864
  lines.push("");
1896
1865
  return lines.join("\n");
1897
1866
  }
1898
- function appendItem(lines, item, indent, isLast) {
1899
- const pad = " ".repeat(indent);
1900
- if (item.children && item.children.length > 0) {
1901
- lines.push(`${pad}{`);
1902
- lines.push(`${pad} label: '${item.label}',`);
1903
- lines.push(`${pad} href: '${item.href}',`);
1904
- if (item.icon) lines.push(`${pad} icon: ${item.icon},`);
1905
- lines.push(`${pad} children: [`);
1906
- for (let j = 0; j < item.children.length; j++) {
1907
- appendItem(lines, item.children[j], indent + 4, j === item.children.length - 1);
1908
- }
1909
- lines.push(`${pad} ]`);
1910
- lines.push(`${pad}}${isLast ? "" : ","}`);
1911
- } else {
1912
- lines.push(`${pad}{`);
1913
- lines.push(`${pad} label: '${item.label}',`);
1914
- lines.push(`${pad} href: '${item.href}'${item.icon ? "," : ""}`);
1915
- if (item.icon) lines.push(`${pad} icon: ${item.icon}`);
1916
- lines.push(`${pad}}${isLast ? "" : ","}`);
1917
- }
1867
+ function appendItem(lines, item, isLast) {
1868
+ lines.push(" {");
1869
+ lines.push(` label: '${item.label}',`);
1870
+ const hasMore = item.icon != null || item.group != null;
1871
+ lines.push(` href: '${item.href}'${hasMore ? "," : ""}`);
1872
+ if (item.icon && item.group) {
1873
+ lines.push(` icon: ${item.icon},`);
1874
+ lines.push(` group: '${item.group}'`);
1875
+ } else if (item.icon) {
1876
+ lines.push(` icon: ${item.icon}`);
1877
+ } else if (item.group) {
1878
+ lines.push(` group: '${item.group}'`);
1879
+ }
1880
+ lines.push(` }${isLast ? "" : ","}`);
1918
1881
  }
1919
1882
  function toKebabCase2(str) {
1920
1883
  return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
@@ -1929,45 +1892,35 @@ function updateFormNavigation(schema, cwd, cmsDir, options = {}) {
1929
1892
  items = parsed.items;
1930
1893
  iconImports = parsed.iconImports;
1931
1894
  }
1932
- let formsGroup = items.find((item) => item.label === "Forms");
1933
- if (!formsGroup) {
1934
- formsGroup = {
1935
- label: "Forms",
1936
- href: "#",
1937
- icon: "FileInput",
1938
- children: []
1939
- };
1940
- items.push(formsGroup);
1941
- }
1942
- if (!formsGroup.children) {
1943
- formsGroup.children = [];
1944
- }
1945
1895
  const kebab = toKebabCase2(schema.name);
1946
1896
  const formHref = `/cms/forms/${kebab}`;
1947
- const existingIndex = formsGroup.children.findIndex((c) => c.href === formHref);
1948
- const newChild = {
1897
+ const existingIndex = items.findIndex((item) => item.href === formHref);
1898
+ const newItem = {
1949
1899
  label: schema.label,
1950
1900
  href: formHref,
1951
- icon: "Inbox"
1901
+ icon: "Inbox",
1902
+ group: "Forms"
1952
1903
  };
1953
1904
  if (existingIndex >= 0) {
1954
1905
  if (options.force) {
1955
- formsGroup.children[existingIndex] = newChild;
1906
+ items[existingIndex] = newItem;
1956
1907
  } else {
1957
1908
  return { files: [] };
1958
1909
  }
1959
1910
  } else {
1960
- formsGroup.children.push(newChild);
1961
- formsGroup.children.sort((a, b) => a.label.localeCompare(b.label));
1911
+ items.push(newItem);
1962
1912
  }
1963
1913
  const dashboard = items.find((item) => item.href === "/cms");
1964
1914
  const others = items.filter((item) => item.href !== "/cms");
1965
- others.sort((a, b) => a.label.localeCompare(b.label));
1915
+ others.sort((a, b) => {
1916
+ if (!a.group && b.group) return -1;
1917
+ if (a.group && !b.group) return 1;
1918
+ if (a.group && b.group && a.group !== b.group) return a.group.localeCompare(b.group);
1919
+ return a.label.localeCompare(b.label);
1920
+ });
1966
1921
  items = [...dashboard ? [dashboard] : [], ...others];
1967
- for (const icon of ["FileInput", "Inbox"]) {
1968
- if (!iconImports.includes(icon)) {
1969
- iconImports.push(icon);
1970
- }
1922
+ if (!iconImports.includes("Inbox")) {
1923
+ iconImports.push("Inbox");
1971
1924
  }
1972
1925
  iconImports.sort();
1973
1926
  const dir = path5.dirname(navFilePath);
@@ -2423,7 +2376,7 @@ function generateFormComponent(schema, cwd, cmsDir, options) {
2423
2376
  import { zodResolver } from '@hookform/resolvers/zod'
2424
2377
  import { useState } from 'react'
2425
2378
  import { useForm } from 'react-hook-form'
2426
- import { z } from 'zod'
2379
+ import { z } from 'zod/v3'
2427
2380
  import { create${pascal}Submission } from '@cms/actions/${formName}-form'
2428
2381
  import { Button } from '@cms/components/ui/button'
2429
2382
  import {
@@ -4517,7 +4470,7 @@ function generateCreatePage(schema, cwd, pagesDir, options = {}) {
4517
4470
  const Singular = toPascalCase8(singular);
4518
4471
  const singLabel = singularizeLabel(schema.label);
4519
4472
  const kebabName = toKebabCase4(schema.name);
4520
- const content = `import { ArrowLeft } from 'lucide-react'
4473
+ const content = `import { ChevronLeft } from 'lucide-react'
4521
4474
  import Link from 'next/link'
4522
4475
  import { connection } from 'next/server'
4523
4476
  import { PageHeader } from '@cms/components/shared/page-header'
@@ -4528,23 +4481,12 @@ export default async function Create${Singular}Page() {
4528
4481
  await connection()
4529
4482
 
4530
4483
  return (
4531
- <div className="flex flex-col w-full pb-20">
4532
- <div className="flex items-center justify-between bg-card px-6 py-4 border-b">
4533
- <PageHeader
4534
- title="Create ${singLabel}"
4535
- description="Add a new ${singLabel.toLowerCase()} to the system"
4536
- />
4537
- <Button variant="outline" asChild>
4538
- <Link href="/cms/${schema.name}">
4539
- <ArrowLeft className="size-4" />
4540
- Back to ${schema.label}
4541
- </Link>
4542
- </Button>
4543
- </div>
4544
- <main className="container mx-auto max-w-5xl p-6">
4484
+ <>
4485
+ <PageHeader title="Create ${singLabel}" back={<Button variant="ghost" size="icon" asChild><Link href="/cms/${schema.name}"><ChevronLeft /></Link></Button>} />
4486
+ <main className="container mx-auto max-w-5xl p-6 pb-20">
4545
4487
  <${Singular}Form key={Date.now()} />
4546
4488
  </main>
4547
- </div>
4489
+ </>
4548
4490
  )
4549
4491
  }
4550
4492
  `;
@@ -4838,7 +4780,7 @@ function generateEditPage(schema, cwd, pagesDir, options = {}) {
4838
4780
  const Singular = toPascalCase10(singular);
4839
4781
  const camelSingular = toCamelCase6(singular);
4840
4782
  const kebabName = toKebabCase5(schema.name);
4841
- const content = `import { ArrowLeft } from 'lucide-react'
4783
+ const content = `import { ChevronLeft } from 'lucide-react'
4842
4784
  import Link from 'next/link'
4843
4785
  import { notFound } from 'next/navigation'
4844
4786
  import { PageHeader } from '@cms/components/shared/page-header'
@@ -4861,23 +4803,12 @@ export default async function Edit${Singular}Page({ params }: PageProps) {
4861
4803
  }
4862
4804
 
4863
4805
  return (
4864
- <div className="flex flex-col w-full pb-20">
4865
- <div className="flex items-center justify-between bg-card px-6 py-4 border-b">
4866
- <PageHeader
4867
- title="Edit ${Singular}"
4868
- description="Update ${singular} information"
4869
- />
4870
- <Button variant="outline" asChild>
4871
- <Link href="/cms/${schema.name}">
4872
- <ArrowLeft className="size-4" />
4873
- Back to ${schema.label}
4874
- </Link>
4875
- </Button>
4876
- </div>
4877
- <main className="container mx-auto max-w-5xl p-6">
4806
+ <>
4807
+ <PageHeader title="Edit ${Singular}" back={<Button variant="ghost" size="icon" asChild><Link href="/cms/${schema.name}"><ChevronLeft /></Link></Button>} />
4808
+ <main className="container mx-auto max-w-5xl p-6 pb-20">
4878
4809
  <${Singular}Form key={${camelSingular}.id} initialData={${camelSingular}} />
4879
4810
  </main>
4880
- </div>
4811
+ </>
4881
4812
  )
4882
4813
  }
4883
4814
  `;
@@ -5868,7 +5799,7 @@ import { ${lucideIcons.join(", ")} } from 'lucide-react'` : ""}
5868
5799
  import { useRouter } from 'next/navigation'
5869
5800
  import {${hasNestedList ? " useFieldArray," : ""} useForm } from 'react-hook-form'
5870
5801
  import { toast } from 'sonner'
5871
- import { z } from 'zod'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
5802
+ import { z } from 'zod/v3'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
5872
5803
  ${relHookImports ? `${relHookImports}
5873
5804
  ` : ""}${uiImports.join("\n")}
5874
5805
  import type {
@@ -6195,7 +6126,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'${lucideIcons
6195
6126
  import { ${lucideIcons.join(", ")} } from 'lucide-react'` : ""}
6196
6127
  import {${hasNestedList ? " useFieldArray," : ""} useForm } from 'react-hook-form'
6197
6128
  import { toast } from 'sonner'
6198
- import { z } from 'zod'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
6129
+ import { z } from 'zod/v3'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
6199
6130
  ${relHookImports ? `${relHookImports}
6200
6131
  ` : ""}${uiImports.join("\n")}
6201
6132
  import type {
@@ -6486,60 +6417,52 @@ function parseSingleItem2(str) {
6486
6417
  const labelMatch = str.match(/label:\s*['"]([^'"]+)['"]/);
6487
6418
  const hrefMatch = str.match(/href:\s*['"]([^'"]+)['"]/);
6488
6419
  const iconMatch = str.match(/icon:\s*(\w+)/);
6420
+ const groupMatch = str.match(/group:\s*['"]([^'"]+)['"]/);
6489
6421
  if (!labelMatch || !hrefMatch) return null;
6490
6422
  const item = {
6491
6423
  label: labelMatch[1],
6492
6424
  href: hrefMatch[1]
6493
6425
  };
6494
6426
  if (iconMatch) item.icon = iconMatch[1];
6495
- const childrenMatch = str.match(/children:\s*\[([\s\S]*?)\]/);
6496
- if (childrenMatch) {
6497
- item.children = parseItemsBlock2(childrenMatch[1]);
6498
- }
6427
+ if (groupMatch) item.group = groupMatch[1];
6499
6428
  return item;
6500
6429
  }
6501
6430
  function generateNavigationCode2(items, iconImports) {
6502
6431
  const lines = [];
6503
- lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
6504
6432
  lines.push("import type { LucideIcon } from 'lucide-react'");
6433
+ lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
6505
6434
  lines.push("");
6506
6435
  lines.push("export interface CmsNavigationItem {");
6507
6436
  lines.push(" label: string");
6508
6437
  lines.push(" href: string");
6509
6438
  lines.push(" icon?: LucideIcon");
6510
- lines.push(" children?: CmsNavigationItem[]");
6439
+ lines.push(" group?: string");
6511
6440
  lines.push("}");
6512
6441
  lines.push("");
6513
6442
  lines.push("export const cmsNavigation: CmsNavigationItem[] = [");
6514
6443
  for (let i = 0; i < items.length; i++) {
6515
6444
  const item = items[i];
6516
6445
  const isLast = i === items.length - 1;
6517
- appendItem2(lines, item, 2, isLast);
6446
+ appendItem2(lines, item, isLast);
6518
6447
  }
6519
6448
  lines.push("]");
6520
6449
  lines.push("");
6521
6450
  return lines.join("\n");
6522
6451
  }
6523
- function appendItem2(lines, item, indent, isLast) {
6524
- const pad = " ".repeat(indent);
6525
- if (item.children && item.children.length > 0) {
6526
- lines.push(`${pad}{`);
6527
- lines.push(`${pad} label: '${item.label}',`);
6528
- lines.push(`${pad} href: '${item.href}',`);
6529
- if (item.icon) lines.push(`${pad} icon: ${item.icon},`);
6530
- lines.push(`${pad} children: [`);
6531
- for (let j = 0; j < item.children.length; j++) {
6532
- appendItem2(lines, item.children[j], indent + 4, j === item.children.length - 1);
6533
- }
6534
- lines.push(`${pad} ]`);
6535
- lines.push(`${pad}}${isLast ? "" : ","}`);
6536
- } else {
6537
- lines.push(`${pad}{`);
6538
- lines.push(`${pad} label: '${item.label}',`);
6539
- lines.push(`${pad} href: '${item.href}'${item.icon ? "," : ""}`);
6540
- if (item.icon) lines.push(`${pad} icon: ${item.icon}`);
6541
- lines.push(`${pad}}${isLast ? "" : ","}`);
6542
- }
6452
+ function appendItem2(lines, item, isLast) {
6453
+ lines.push(" {");
6454
+ lines.push(` label: '${item.label}',`);
6455
+ const hasMore = item.icon != null || item.group != null;
6456
+ lines.push(` href: '${item.href}'${hasMore ? "," : ""}`);
6457
+ if (item.icon && item.group) {
6458
+ lines.push(` icon: ${item.icon},`);
6459
+ lines.push(` group: '${item.group}'`);
6460
+ } else if (item.icon) {
6461
+ lines.push(` icon: ${item.icon}`);
6462
+ } else if (item.group) {
6463
+ lines.push(` group: '${item.group}'`);
6464
+ }
6465
+ lines.push(` }${isLast ? "" : ","}`);
6543
6466
  }
6544
6467
  function updateNavigation(schema, cwd, cmsDir, options = {}) {
6545
6468
  const navFilePath = path15.join(cwd, cmsDir, "data", "navigation.ts");
@@ -6561,48 +6484,26 @@ function updateNavigation(schema, cwd, cmsDir, options = {}) {
6561
6484
  icon: schema.icon
6562
6485
  };
6563
6486
  if (schema.navGroup) {
6564
- let group = items.find((item) => item.label === schema.navGroup?.label);
6565
- if (!group) {
6566
- group = {
6567
- label: schema.navGroup.label,
6568
- href: "#",
6569
- icon: schema.navGroup.icon,
6570
- children: []
6571
- };
6572
- items.push(group);
6573
- }
6574
- if (!group.children) {
6575
- group.children = [];
6576
- }
6577
- const existingChild = group.children.findIndex((c) => c.href === entityHref);
6578
- if (existingChild >= 0) {
6579
- if (options.force) {
6580
- group.children[existingChild] = newItem;
6581
- } else {
6582
- return { files: [] };
6583
- }
6487
+ newItem.group = schema.navGroup.label;
6488
+ }
6489
+ const existingIndex = items.findIndex((item) => item.href === entityHref);
6490
+ if (existingIndex >= 0) {
6491
+ if (options.force) {
6492
+ items[existingIndex] = newItem;
6584
6493
  } else {
6585
- group.children.push(newItem);
6586
- group.children.sort((a, b) => a.label.localeCompare(b.label));
6587
- }
6588
- if (schema.navGroup.icon && !iconImports.includes(schema.navGroup.icon)) {
6589
- iconImports.push(schema.navGroup.icon);
6494
+ return { files: [] };
6590
6495
  }
6591
6496
  } else {
6592
- const existingIndex = items.findIndex((item) => item.href === entityHref);
6593
- if (existingIndex >= 0) {
6594
- if (options.force) {
6595
- items[existingIndex] = newItem;
6596
- } else {
6597
- return { files: [] };
6598
- }
6599
- } else {
6600
- items.push(newItem);
6601
- }
6497
+ items.push(newItem);
6602
6498
  }
6603
6499
  const dashboard = items.find((item) => item.href === "/cms");
6604
6500
  const others = items.filter((item) => item.href !== "/cms");
6605
- others.sort((a, b) => a.label.localeCompare(b.label));
6501
+ others.sort((a, b) => {
6502
+ if (!a.group && b.group) return -1;
6503
+ if (a.group && !b.group) return 1;
6504
+ if (a.group && b.group && a.group !== b.group) return a.group.localeCompare(b.group);
6505
+ return a.label.localeCompare(b.label);
6506
+ });
6606
6507
  items = [...dashboard ? [dashboard] : [], ...others];
6607
6508
  if (schema.icon && !iconImports.includes(schema.icon)) {
6608
6509
  iconImports.push(schema.icon);
@@ -6721,8 +6622,8 @@ function generatePageContent2(schema, cwd, pagesDir, options = {}) {
6721
6622
  const hasCreate = schema.actions?.create ?? false;
6722
6623
  const hasDelete = schema.actions?.delete ?? false;
6723
6624
  const hasFilters = schema.filters && schema.filters.length > 0;
6724
- const lucideIcons = ["Search"];
6725
- if (hasCreate) lucideIcons.push("FilePlus");
6625
+ const lucideIcons = ["ChevronLeft", "Search", "CornerDownLeft"];
6626
+ if (hasCreate) lucideIcons.push("Plus");
6726
6627
  if (hasDelete) lucideIcons.push("Trash2");
6727
6628
  if (hasFilters) lucideIcons.push("Check", "ChevronsUpDown");
6728
6629
  let imports = `'use client'
@@ -6730,7 +6631,8 @@ function generatePageContent2(schema, cwd, pagesDir, options = {}) {
6730
6631
  import { useQueryClient } from '@tanstack/react-query'
6731
6632
  import type { ColumnDef } from '@tanstack/react-table'
6732
6633
  import { ${lucideIcons.join(", ")} } from 'lucide-react'
6733
- ${hasCreate ? "import Link from 'next/link'\n" : ""}import { parseAsString${hasDelete ? ", parseAsArrayOf, parseAsInteger" : ""}, useQueryState } from 'nuqs'
6634
+ ${hasCreate ? "import Link from 'next/link'\n" : ""}import { useRouter } from 'next/navigation'
6635
+ import { parseAsString${hasDelete ? ", parseAsArrayOf, parseAsInteger" : ""}, useQueryState } from 'nuqs'
6734
6636
  import * as React from 'react'
6735
6637
  import { useFormStatus } from 'react-dom'
6736
6638
  ${hasDelete ? "import { toast } from 'sonner'\n" : ""}import { PageHeader } from '@cms/components/shared/page-header'
@@ -6830,7 +6732,7 @@ ${filterLogic}` : ""}`;
6830
6732
  })
6831
6733
  }
6832
6734
  ` : "";
6833
- const filterDropdowns = hasFilters ? schema.filters.map(
6735
+ const _filterDropdowns = hasFilters ? schema.filters.map(
6834
6736
  (f) => ` <Popover open={${f.field}ComboboxOpen} onOpenChange={set${toPascalCase14(f.field)}ComboboxOpen}>
6835
6737
  <PopoverTrigger asChild>
6836
6738
  <Button
@@ -6893,19 +6795,17 @@ ${filterLogic}` : ""}`;
6893
6795
  </PopoverContent>
6894
6796
  </Popover>`
6895
6797
  ).join("\n") : "";
6896
- const searchInput = ` <form action={searchAction} className="flex items-center gap-2">
6897
- <div className="relative">
6898
- <Search className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" />
6899
- <Input
6900
- key={search}
6901
- name="search"
6902
- placeholder="Search ${schema.label.toLowerCase()}..."
6903
- defaultValue={search}
6904
- className="w-64 pl-9"
6905
- />
6906
- </div>
6907
- <SearchButton />
6908
- </form>`;
6798
+ const searchInput = `<form action={searchAction} className="flex items-center gap-2 relative">
6799
+ <Search className="text-muted-foreground/70 pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" />
6800
+ <Input
6801
+ key={search}
6802
+ name="search"
6803
+ placeholder="Search ${schema.label.toLowerCase()}..."
6804
+ defaultValue={search}
6805
+ className="w-64 pl-9 bg-white rounded-lg"
6806
+ />
6807
+ <CornerDownLeft className="text-muted-foreground/70 pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2" />
6808
+ </form>`;
6909
6809
  const deleteButton = hasDelete ? ` {selectedIds.length > 0 && (
6910
6810
  <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
6911
6811
  <AlertDialogTrigger asChild>
@@ -6941,7 +6841,7 @@ ${filterLogic}` : ""}`;
6941
6841
  )}` : "";
6942
6842
  const createButton = hasCreate ? ` <Button asChild>
6943
6843
  <Link href="/cms/${schema.name}/new">
6944
- <FilePlus className="size-3.5 -ml-0.5" strokeWidth={2} />
6844
+ <Plus className="size-3.5 -ml-0.5" strokeWidth={2} />
6945
6845
  Create ${singularizeLabel2(schema.label)}
6946
6846
  </Link>
6947
6847
  </Button>` : "";
@@ -6956,20 +6856,12 @@ ${searchButton}interface ${Plural}PageContentProps<TValue> {
6956
6856
  export function ${Plural}PageContent<TValue>({
6957
6857
  columns
6958
6858
  }: ${Plural}PageContentProps<TValue>) {
6859
+ const router = useRouter()
6959
6860
  const queryClient = useQueryClient()
6960
6861
  ${searchLogic}${deleteLogic}
6961
6862
  return (
6962
6863
  <>
6963
- <div className="flex items-center justify-between bg-card px-6 py-4 border-b">
6964
- <PageHeader title="${schema.label}" description="${schema.description}" />
6965
- <div className="flex items-center gap-2">
6966
- ${filterDropdowns ? `${filterDropdowns}
6967
- ` : ""}${searchInput}
6968
- ${deleteButton}
6969
- ${createButton}
6970
- </div>
6971
- </div>
6972
-
6864
+ <PageHeader title="${schema.label}" back={<Button variant="ghost" size="icon" onClick={() => router.back()}><ChevronLeft /></Button>} search={${searchInput}} actions={<div className="flex items-center gap-2">${deleteButton} ${createButton}</div>} />
6973
6865
  <main className="space-y-6 p-6">
6974
6866
  <${Plural}Table ${tableProps} />
6975
6867
  </main>
@@ -7018,14 +6910,12 @@ export default async function ${PageName}Page() {
7018
6910
  const data = await get${Singular}()
7019
6911
 
7020
6912
  return (
7021
- <div className="flex flex-col w-full pb-20">
7022
- <div className="flex items-center justify-between bg-card px-6 py-4 border-b">
7023
- <PageHeader title="${schema.label}" description="Manage ${schema.label.toLowerCase()}" />
7024
- </div>
7025
- <main className="container mx-auto max-w-5xl p-6">
6913
+ <>
6914
+ <PageHeader title="${schema.label}" />
6915
+ <main className="container mx-auto max-w-5xl p-6 pb-20">
7026
6916
  <${Singular}Form initialData={data} />
7027
6917
  </main>
7028
- </div>
6918
+ </>
7029
6919
  )
7030
6920
  }
7031
6921
  `;
@@ -7972,14 +7862,6 @@ function openBrowser(url) {
7972
7862
  // src/init/prompts/features.ts
7973
7863
  import * as p2 from "@clack/prompts";
7974
7864
  async function promptFeatures(presetOverride) {
7975
- const includeEmail = await p2.confirm({
7976
- message: "Include email system? (Resend + React Email)",
7977
- initialValue: true
7978
- });
7979
- if (p2.isCancel(includeEmail)) {
7980
- p2.cancel("Setup cancelled.");
7981
- process.exit(0);
7982
- }
7983
7865
  let preset;
7984
7866
  if (presetOverride && isValidPreset(presetOverride)) {
7985
7867
  preset = presetOverride;
@@ -7999,7 +7881,7 @@ async function promptFeatures(presetOverride) {
7999
7881
  }
8000
7882
  preset = selected;
8001
7883
  }
8002
- return { includeEmail, preset };
7884
+ return { includeEmail: true, preset };
8003
7885
  }
8004
7886
  function isValidPreset(value) {
8005
7887
  return value === "blank" || value === "blog" || value === "full";
@@ -8067,8 +7949,8 @@ export const { GET, POST } = toNextJsHandler(auth)
8067
7949
  function uploadRouteTemplate() {
8068
7950
  return `import { PutObjectCommand } from '@aws-sdk/client-s3'
8069
7951
  import { BUCKET_NAME, generateFilePath, getPublicUrl, getR2Client } from '@cms/lib/r2'
8070
- import { validateFiles } from '@cms/utils/validation'
8071
7952
  import type { UploadedFile } from '@cms/types'
7953
+ import { validateFiles } from '@cms/utils/validation'
8072
7954
  import { type NextRequest, NextResponse } from 'next/server'
8073
7955
 
8074
7956
  export async function POST(request: NextRequest) {
@@ -8088,27 +7970,22 @@ export async function POST(request: NextRequest) {
8088
7970
  }
8089
7971
 
8090
7972
  if (files.length === 0) {
8091
- return NextResponse.json(
8092
- { success: false, error: 'No files provided' },
8093
- { status: 400 }
8094
- )
7973
+ return NextResponse.json({ success: false, error: 'No files provided' }, { status: 400 })
8095
7974
  }
8096
7975
 
8097
7976
  const validation = validateFiles(files, {
8098
7977
  maxSizeInBytes,
8099
7978
  allowedTypes,
8100
- maxFiles: 10
7979
+ maxFiles: 10,
8101
7980
  })
8102
7981
 
8103
7982
  if (!validation.valid) {
8104
7983
  return NextResponse.json(
8105
7984
  {
8106
7985
  success: false,
8107
- error: validation.errors
8108
- .map((e) => \`\${e.filename}: \${e.error}\`)
8109
- .join('; ')
7986
+ error: validation.errors.map((e) => \`\${e.filename}: \${e.error}\`).join('; '),
8110
7987
  },
8111
- { status: 400 }
7988
+ { status: 400 },
8112
7989
  )
8113
7990
  }
8114
7991
 
@@ -8124,7 +8001,7 @@ export async function POST(request: NextRequest) {
8124
8001
  Key: key,
8125
8002
  Body: buffer,
8126
8003
  ContentType: file.type,
8127
- ContentLength: file.size
8004
+ ContentLength: file.size,
8128
8005
  })
8129
8006
 
8130
8007
  await getR2Client().send(command)
@@ -8134,18 +8011,14 @@ export async function POST(request: NextRequest) {
8134
8011
  url: getPublicUrl(key),
8135
8012
  filename: file.name,
8136
8013
  size: file.size,
8137
- contentType: file.type
8014
+ contentType: file.type,
8138
8015
  })
8139
8016
  }
8140
8017
 
8141
8018
  return NextResponse.json({ success: true, files: uploadedFiles })
8142
8019
  } catch (error) {
8143
- const message =
8144
- error instanceof Error ? error.message : 'Failed to upload files'
8145
- return NextResponse.json(
8146
- { success: false, error: message },
8147
- { status: 500 }
8148
- )
8020
+ const message = error instanceof Error ? error.message : 'Failed to upload files'
8021
+ return NextResponse.json({ success: false, error: message }, { status: 500 })
8149
8022
  }
8150
8023
  }
8151
8024
  `;
@@ -8225,7 +8098,9 @@ function authClientTemplate() {
8225
8098
  import { createAuthClient } from 'better-auth/react'
8226
8099
 
8227
8100
  export const authClient = createAuthClient({
8228
- baseURL: process.env.NEXT_PUBLIC_BETTERSTART_AUTH_URL || (typeof window !== 'undefined' ? window.location.origin : ''),
8101
+ baseURL:
8102
+ process.env.NEXT_PUBLIC_BETTERSTART_AUTH_URL ||
8103
+ (typeof window !== 'undefined' ? window.location.origin : ''),
8229
8104
  basePath: '/api/cms/auth',
8230
8105
  })
8231
8106
 
@@ -8246,10 +8121,7 @@ export enum UserRole {
8246
8121
  }
8247
8122
 
8248
8123
  export function isUserRole(value: unknown): value is UserRole {
8249
- return (
8250
- typeof value === 'string' &&
8251
- Object.values(UserRole).includes(value as UserRole)
8252
- )
8124
+ return typeof value === 'string' && Object.values(UserRole).includes(value as UserRole)
8253
8125
  }
8254
8126
 
8255
8127
  /**
@@ -8704,184 +8576,121 @@ function hasEnvBetterstartVars(cwd) {
8704
8576
  // src/init/templates/components/cms-globals.ts
8705
8577
  function cmsGlobalsCssTemplate() {
8706
8578
  return `@import "tailwindcss";
8579
+ @import "tw-animate-css";
8580
+ @import "shadcn/tailwind.css";
8707
8581
 
8708
8582
  @custom-variant dark (&:is(.dark *));
8709
8583
 
8710
- :root {
8711
- --background: oklch(0.985 0 0);
8712
- --foreground: oklch(0 0 0);
8713
- --card: oklch(1 0 0);
8714
- --card-foreground: oklch(0 0 0);
8715
- --popover: oklch(0.99 0 0);
8716
- --popover-foreground: oklch(0 0 0);
8717
- --primary: oklch(0 0 0);
8718
- --primary-foreground: oklch(1 0 0);
8719
- --secondary: oklch(0.97 0 0);
8720
- --secondary-foreground: oklch(0 0 0);
8721
- --muted: oklch(0.97 0 0);
8722
- --muted-foreground: oklch(0.44 0 0);
8723
- --accent: oklch(0.97 0 0);
8724
- --accent-foreground: oklch(0 0 0);
8725
- --destructive: oklch(0.63 0.19 23.03);
8726
- --destructive-foreground: oklch(1 0 0);
8727
- --border: oklch(0.92 0 0);
8728
- --input: oklch(0.99 0 0);
8729
- --ring: oklch(0 0 0);
8730
- --chart-1: oklch(0.81 0.17 75.35);
8731
- --chart-2: oklch(0.55 0.22 264.53);
8732
- --chart-3: oklch(0.72 0 0);
8733
- --chart-4: oklch(0.92 0 0);
8734
- --chart-5: oklch(0.56 0 0);
8735
- --sidebar: oklch(1 0 0);
8736
- --sidebar-foreground: oklch(0 0 0);
8737
- --sidebar-primary: oklch(0 0 0);
8738
- --sidebar-primary-foreground: oklch(1 0 0);
8739
- --sidebar-accent: oklch(0.94 0 0);
8740
- --sidebar-accent-foreground: oklch(0 0 0);
8741
- --sidebar-border: oklch(0.94 0 0);
8742
- --sidebar-ring: oklch(0 0 0);
8743
- --font-sans: Geist, sans-serif;
8744
- --font-serif: Georgia, serif;
8745
- --font-mono: Geist Mono, monospace;
8746
- --radius: 0.4rem;
8747
- --shadow-x: 0px;
8748
- --shadow-y: 1px;
8749
- --shadow-blur: 3px;
8750
- --shadow-spread: 0px;
8751
- --shadow-opacity: 0.02;
8752
- --shadow-color: hsl(0 0% 0%);
8753
- --shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
8754
- --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
8755
- --shadow-sm:
8756
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
8757
- --shadow:
8758
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
8759
- --shadow-md:
8760
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 2px 4px -1px hsl(0 0% 0% / 0.02);
8761
- --shadow-lg:
8762
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 4px 6px -1px hsl(0 0% 0% / 0.02);
8763
- --shadow-xl:
8764
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 8px 10px -1px hsl(0 0% 0% / 0.02);
8765
- --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
8766
- --tracking-normal: 0em;
8767
- --spacing: 0.25rem;
8768
- }
8769
-
8770
- .dark {
8771
- --background: oklch(0 0 0);
8772
- --foreground: oklch(1 0 0);
8773
- --card: oklch(0.14 0 0);
8774
- --card-foreground: oklch(1 0 0);
8775
- --popover: oklch(0.18 0 0);
8776
- --popover-foreground: oklch(1 0 0);
8777
- --primary: oklch(1 0 0);
8778
- --primary-foreground: oklch(0 0 0);
8779
- --secondary: oklch(0.25 0 0);
8780
- --secondary-foreground: oklch(1 0 0);
8781
- --muted: oklch(0.23 0 0);
8782
- --muted-foreground: oklch(0.72 0 0);
8783
- --accent: oklch(0.32 0 0);
8784
- --accent-foreground: oklch(1 0 0);
8785
- --destructive: oklch(0.69 0.2 23.91);
8786
- --destructive-foreground: oklch(0 0 0);
8787
- --border: oklch(0.26 0 0);
8788
- --input: oklch(0.32 0 0);
8789
- --ring: oklch(0.72 0 0);
8790
- --chart-1: oklch(0.81 0.17 75.35);
8791
- --chart-2: oklch(0.58 0.21 260.84);
8792
- --chart-3: oklch(0.56 0 0);
8793
- --chart-4: oklch(0.44 0 0);
8794
- --chart-5: oklch(0.92 0 0);
8795
- --sidebar: oklch(0.18 0 0);
8796
- --sidebar-foreground: oklch(1 0 0);
8797
- --sidebar-primary: oklch(1 0 0);
8798
- --sidebar-primary-foreground: oklch(0 0 0);
8799
- --sidebar-accent: oklch(0.32 0 0);
8800
- --sidebar-accent-foreground: oklch(1 0 0);
8801
- --sidebar-border: oklch(0.32 0 0);
8802
- --sidebar-ring: oklch(0.72 0 0);
8803
- --font-sans: Geist, sans-serif;
8804
- --font-serif: Georgia, serif;
8805
- --font-mono: Geist Mono, monospace;
8806
- --radius: 0.4rem;
8807
- --shadow-x: 0px;
8808
- --shadow-y: 1px;
8809
- --shadow-blur: 3px;
8810
- --shadow-spread: 0px;
8811
- --shadow-opacity: 0.02;
8812
- --shadow-color: hsl(0 0% 0%);
8813
- --shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
8814
- --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
8815
- --shadow-sm:
8816
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
8817
- --shadow:
8818
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
8819
- --shadow-md:
8820
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 2px 4px -1px hsl(0 0% 0% / 0.02);
8821
- --shadow-lg:
8822
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 4px 6px -1px hsl(0 0% 0% / 0.02);
8823
- --shadow-xl:
8824
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 8px 10px -1px hsl(0 0% 0% / 0.02);
8825
- --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
8826
- }
8827
-
8828
- .cms-root {
8829
- --font-sans: var(--font-geist-sans, sans-serif);
8830
- --font-mono: var(--font-geist-mono, monospace);
8831
- font-family: var(--font-sans);
8832
- }
8833
-
8834
8584
  @theme inline {
8835
8585
  --color-background: var(--background);
8836
8586
  --color-foreground: var(--foreground);
8837
- --color-card: var(--card);
8838
- --color-card-foreground: var(--card-foreground);
8839
- --color-popover: var(--popover);
8840
- --color-popover-foreground: var(--popover-foreground);
8841
- --color-primary: var(--primary);
8842
- --color-primary-foreground: var(--primary-foreground);
8843
- --color-secondary: var(--secondary);
8844
- --color-secondary-foreground: var(--secondary-foreground);
8845
- --color-muted: var(--muted);
8846
- --color-muted-foreground: var(--muted-foreground);
8847
- --color-accent: var(--accent);
8848
- --color-accent-foreground: var(--accent-foreground);
8849
- --color-destructive: var(--destructive);
8850
- --color-destructive-foreground: var(--destructive-foreground);
8851
- --color-border: var(--border);
8852
- --color-input: var(--input);
8853
- --color-ring: var(--ring);
8854
- --color-chart-1: var(--chart-1);
8855
- --color-chart-2: var(--chart-2);
8856
- --color-chart-3: var(--chart-3);
8857
- --color-chart-4: var(--chart-4);
8858
- --color-chart-5: var(--chart-5);
8859
- --color-sidebar: var(--sidebar);
8860
- --color-sidebar-foreground: var(--sidebar-foreground);
8861
- --color-sidebar-primary: var(--sidebar-primary);
8862
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
8863
- --color-sidebar-accent: var(--sidebar-accent);
8864
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
8865
- --color-sidebar-border: var(--sidebar-border);
8866
- --color-sidebar-ring: var(--sidebar-ring);
8867
-
8868
8587
  --font-sans: var(--font-sans);
8869
- --font-mono: var(--font-mono);
8870
- --font-serif: var(--font-serif);
8871
-
8588
+ --font-mono: var(--font-geist-mono);
8589
+ --color-sidebar-ring: var(--sidebar-ring);
8590
+ --color-sidebar-border: var(--sidebar-border);
8591
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
8592
+ --color-sidebar-accent: var(--sidebar-accent);
8593
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
8594
+ --color-sidebar-primary: var(--sidebar-primary);
8595
+ --color-sidebar-foreground: var(--sidebar-foreground);
8596
+ --color-sidebar: var(--sidebar);
8597
+ --color-chart-5: var(--chart-5);
8598
+ --color-chart-4: var(--chart-4);
8599
+ --color-chart-3: var(--chart-3);
8600
+ --color-chart-2: var(--chart-2);
8601
+ --color-chart-1: var(--chart-1);
8602
+ --color-ring: var(--ring);
8603
+ --color-input: var(--input);
8604
+ --color-border: var(--border);
8605
+ --color-destructive: var(--destructive);
8606
+ --color-accent-foreground: var(--accent-foreground);
8607
+ --color-accent: var(--accent);
8608
+ --color-muted-foreground: var(--muted-foreground);
8609
+ --color-muted: var(--muted);
8610
+ --color-secondary-foreground: var(--secondary-foreground);
8611
+ --color-secondary: var(--secondary);
8612
+ --color-primary-foreground: var(--primary-foreground);
8613
+ --color-primary: var(--primary);
8614
+ --color-popover-foreground: var(--popover-foreground);
8615
+ --color-popover: var(--popover);
8616
+ --color-card-foreground: var(--card-foreground);
8617
+ --color-card: var(--card);
8872
8618
  --radius-sm: calc(var(--radius) - 4px);
8873
8619
  --radius-md: calc(var(--radius) - 2px);
8874
8620
  --radius-lg: var(--radius);
8875
8621
  --radius-xl: calc(var(--radius) + 4px);
8622
+ --radius-2xl: calc(var(--radius) + 8px);
8623
+ --radius-3xl: calc(var(--radius) + 12px);
8624
+ --radius-4xl: calc(var(--radius) + 16px);
8625
+ }
8626
+
8627
+ :root {
8628
+ --background: oklch(1 0 0);
8629
+ --foreground: oklch(0.141 0.005 285.823);
8630
+ --card: oklch(1 0 0);
8631
+ --card-foreground: oklch(0.141 0.005 285.823);
8632
+ --popover: oklch(1 0 0);
8633
+ --popover-foreground: oklch(0.141 0.005 285.823);
8634
+ --primary: oklch(0.21 0.006 285.885);
8635
+ --primary-foreground: oklch(0.985 0 0);
8636
+ --secondary: oklch(0.967 0.001 286.375);
8637
+ --secondary-foreground: oklch(0.21 0.006 285.885);
8638
+ --muted: oklch(0.967 0.001 286.375);
8639
+ --muted-foreground: oklch(0.552 0.016 285.938);
8640
+ --accent: oklch(0.967 0.001 286.375);
8641
+ --accent-foreground: oklch(0.21 0.006 285.885);
8642
+ --destructive: oklch(0.577 0.245 27.325);
8643
+ --border: oklch(0.92 0.004 286.32);
8644
+ --input: oklch(0.92 0.004 286.32);
8645
+ --ring: oklch(0.705 0.015 286.067);
8646
+ --chart-1: oklch(0.646 0.222 41.116);
8647
+ --chart-2: oklch(0.6 0.118 184.704);
8648
+ --chart-3: oklch(0.398 0.07 227.392);
8649
+ --chart-4: oklch(0.828 0.189 84.429);
8650
+ --chart-5: oklch(0.769 0.188 70.08);
8651
+ --radius: 0.625rem;
8652
+ --sidebar: oklch(0.985 0 0);
8653
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
8654
+ --sidebar-primary: oklch(0.21 0.006 285.885);
8655
+ --sidebar-primary-foreground: oklch(0.985 0 0);
8656
+ --sidebar-accent: oklch(0.967 0.001 286.375);
8657
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
8658
+ --sidebar-border: oklch(0.92 0.004 286.32);
8659
+ --sidebar-ring: oklch(0.705 0.015 286.067);
8660
+ }
8876
8661
 
8877
- --shadow-2xs: var(--shadow-2xs);
8878
- --shadow-xs: var(--shadow-xs);
8879
- --shadow-sm: var(--shadow-sm);
8880
- --shadow: var(--shadow);
8881
- --shadow-md: var(--shadow-md);
8882
- --shadow-lg: var(--shadow-lg);
8883
- --shadow-xl: var(--shadow-xl);
8884
- --shadow-2xl: var(--shadow-2xl);
8662
+ .dark {
8663
+ --background: oklch(0.141 0.005 285.823);
8664
+ --foreground: oklch(0.985 0 0);
8665
+ --card: oklch(0.21 0.006 285.885);
8666
+ --card-foreground: oklch(0.985 0 0);
8667
+ --popover: oklch(0.21 0.006 285.885);
8668
+ --popover-foreground: oklch(0.985 0 0);
8669
+ --primary: oklch(0.92 0.004 286.32);
8670
+ --primary-foreground: oklch(0.21 0.006 285.885);
8671
+ --secondary: oklch(0.274 0.006 286.033);
8672
+ --secondary-foreground: oklch(0.985 0 0);
8673
+ --muted: oklch(0.274 0.006 286.033);
8674
+ --muted-foreground: oklch(0.705 0.015 286.067);
8675
+ --accent: oklch(0.274 0.006 286.033);
8676
+ --accent-foreground: oklch(0.985 0 0);
8677
+ --destructive: oklch(0.704 0.191 22.216);
8678
+ --border: oklch(1 0 0 / 10%);
8679
+ --input: oklch(1 0 0 / 15%);
8680
+ --ring: oklch(0.552 0.016 285.938);
8681
+ --chart-1: oklch(0.488 0.243 264.376);
8682
+ --chart-2: oklch(0.696 0.17 162.48);
8683
+ --chart-3: oklch(0.769 0.188 70.08);
8684
+ --chart-4: oklch(0.627 0.265 303.9);
8685
+ --chart-5: oklch(0.645 0.246 16.439);
8686
+ --sidebar: oklch(0.21 0.006 285.885);
8687
+ --sidebar-foreground: oklch(0.985 0 0);
8688
+ --sidebar-primary: oklch(0.488 0.243 264.376);
8689
+ --sidebar-primary-foreground: oklch(0.985 0 0);
8690
+ --sidebar-accent: oklch(0.274 0.006 286.033);
8691
+ --sidebar-accent-foreground: oklch(0.985 0 0);
8692
+ --sidebar-border: oklch(1 0 0 / 10%);
8693
+ --sidebar-ring: oklch(0.552 0.016 285.938);
8885
8694
  }
8886
8695
 
8887
8696
  @layer base {
@@ -8889,7 +8698,7 @@ function cmsGlobalsCssTemplate() {
8889
8698
  @apply border-border outline-ring/50;
8890
8699
  }
8891
8700
  body {
8892
- @apply bg-background text-foreground antialiased;
8701
+ @apply bg-background text-foreground;
8893
8702
  }
8894
8703
  }
8895
8704
 
@@ -8922,6 +8731,14 @@ function cmsGlobalsCssTemplate() {
8922
8731
  function dataTableTemplate() {
8923
8732
  return `'use client'
8924
8733
 
8734
+ import {
8735
+ Table,
8736
+ TableBody,
8737
+ TableCell,
8738
+ TableHead,
8739
+ TableHeader,
8740
+ TableRow,
8741
+ } from '@cms/components/ui/table'
8925
8742
  import {
8926
8743
  type ColumnDef,
8927
8744
  type ColumnFiltersState,
@@ -8931,20 +8748,11 @@ import {
8931
8748
  getPaginationRowModel,
8932
8749
  getSortedRowModel,
8933
8750
  type SortingState,
8934
- type Table as TanstackTable,
8935
8751
  useReactTable,
8936
- type VisibilityState
8752
+ type VisibilityState,
8937
8753
  } from '@tanstack/react-table'
8938
8754
  import { parseAsInteger, useQueryState } from 'nuqs'
8939
8755
  import * as React from 'react'
8940
- import {
8941
- Table,
8942
- TableBody,
8943
- TableCell,
8944
- TableHead,
8945
- TableHeader,
8946
- TableRow
8947
- } from '@cms/components/ui/table'
8948
8756
  import { DataTablePagination } from './data-table-pagination'
8949
8757
 
8950
8758
  interface DataTableProps<TData, TValue> {
@@ -8970,7 +8778,7 @@ export function DataTable<TData, TValue>({
8970
8778
  selectedIds,
8971
8779
  onSelectedIdsChange,
8972
8780
  meta,
8973
- getId = (row) => (row as { id: number }).id
8781
+ getId = (row) => (row as { id: number }).id,
8974
8782
  }: DataTableProps<TData, TValue>) {
8975
8783
  const [sorting, setSorting] = React.useState<SortingState>([])
8976
8784
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
@@ -8993,27 +8801,38 @@ export function DataTable<TData, TValue>({
8993
8801
  }, [selectedIds, data, getId])
8994
8802
 
8995
8803
  const handleRowSelectionChange = React.useCallback(
8996
- (updater: Record<string, boolean> | ((old: Record<string, boolean>) => Record<string, boolean>)) => {
8804
+ (
8805
+ updater:
8806
+ | Record<string, boolean>
8807
+ | ((old: Record<string, boolean>) => Record<string, boolean>),
8808
+ ) => {
8997
8809
  if (!onSelectedIdsChange) return
8998
8810
  const newSelection = typeof updater === 'function' ? updater(rowSelection) : updater
8999
8811
  const newIds = Object.keys(newSelection)
9000
8812
  .filter((key) => newSelection[key])
9001
- .map((key) => getId(data[Number.parseInt(key)]))
8813
+ .map((key) => getId(data[Number.parseInt(key, 10)]))
9002
8814
  .filter(Boolean)
9003
8815
  onSelectedIdsChange(newIds)
9004
8816
  },
9005
- [data, rowSelection, onSelectedIdsChange, getId]
8817
+ [data, rowSelection, onSelectedIdsChange, getId],
9006
8818
  )
9007
8819
 
9008
8820
  const handlePaginationChange = React.useCallback(
9009
- (updater: { pageIndex: number; pageSize: number } | ((old: { pageIndex: number; pageSize: number }) => { pageIndex: number; pageSize: number })) => {
8821
+ (
8822
+ updater:
8823
+ | { pageIndex: number; pageSize: number }
8824
+ | ((old: { pageIndex: number; pageSize: number }) => {
8825
+ pageIndex: number
8826
+ pageSize: number
8827
+ }),
8828
+ ) => {
9010
8829
  const current = { pageIndex, pageSize: effectivePageSize }
9011
8830
  const next = typeof updater === 'function' ? updater(current) : updater
9012
8831
  React.startTransition(() => {
9013
8832
  setPageIndex(next.pageIndex)
9014
8833
  })
9015
8834
  },
9016
- [pageIndex, effectivePageSize, setPageIndex]
8835
+ [pageIndex, effectivePageSize, setPageIndex],
9017
8836
  )
9018
8837
 
9019
8838
  const table = useReactTable({
@@ -9036,9 +8855,9 @@ export function DataTable<TData, TValue>({
9036
8855
  rowSelection,
9037
8856
  pagination: {
9038
8857
  pageIndex,
9039
- pageSize: effectivePageSize
9040
- }
9041
- }
8858
+ pageSize: effectivePageSize,
8859
+ },
8860
+ },
9042
8861
  })
9043
8862
 
9044
8863
  return (
@@ -9105,23 +8924,23 @@ export type { DataTableProps }
9105
8924
  function dataTablePaginationTemplate() {
9106
8925
  return `'use client'
9107
8926
 
9108
- import type { Table } from '@tanstack/react-table'
9109
- import * as React from 'react'
9110
8927
  import { Button } from '@cms/components/ui/button'
9111
8928
  import {
9112
8929
  Select,
9113
8930
  SelectContent,
9114
8931
  SelectItem,
9115
8932
  SelectTrigger,
9116
- SelectValue
8933
+ SelectValue,
9117
8934
  } from '@cms/components/ui/select'
8935
+ import type { Table } from '@tanstack/react-table'
8936
+ import * as React from 'react'
9118
8937
 
9119
8938
  const PAGE_SIZE_OPTIONS = [
9120
8939
  { value: '10', label: '10' },
9121
8940
  { value: '20', label: '20' },
9122
8941
  { value: '50', label: '50' },
9123
8942
  { value: '100', label: '100' },
9124
- { value: 'all', label: 'All' }
8943
+ { value: 'all', label: 'All' },
9125
8944
  ]
9126
8945
 
9127
8946
  interface DataTablePaginationProps<TData> {
@@ -9133,7 +8952,7 @@ interface DataTablePaginationProps<TData> {
9133
8952
  export function DataTablePagination<TData>({
9134
8953
  table,
9135
8954
  pageSize,
9136
- setPageSize
8955
+ setPageSize,
9137
8956
  }: DataTablePaginationProps<TData>) {
9138
8957
  const handlePageSizeChange = React.useCallback(
9139
8958
  (value: string) => {
@@ -9146,7 +8965,7 @@ export function DataTablePagination<TData>({
9146
8965
  table.setPageIndex(0)
9147
8966
  })
9148
8967
  },
9149
- [setPageSize, table]
8968
+ [setPageSize, table],
9150
8969
  )
9151
8970
 
9152
8971
  return (
@@ -9200,11 +9019,11 @@ export function DataTablePagination<TData>({
9200
9019
  function dataTableToolbarTemplate() {
9201
9020
  return `'use client'
9202
9021
 
9203
- import { ArrowUpDown, Save, Search } from 'lucide-react'
9204
- import * as React from 'react'
9205
- import { useFormStatus } from 'react-dom'
9206
9022
  import { Button } from '@cms/components/ui/button'
9207
9023
  import { Input } from '@cms/components/ui/input'
9024
+ import { ArrowUpDown, Save, Search } from 'lucide-react'
9025
+ import type * as React from 'react'
9026
+ import { useFormStatus } from 'react-dom'
9208
9027
 
9209
9028
  function SearchButton() {
9210
9029
  const { pending } = useFormStatus()
@@ -9226,7 +9045,7 @@ export function DataTableToolbar({
9226
9045
  search,
9227
9046
  onSearch,
9228
9047
  searchPlaceholder = 'Search...',
9229
- children
9048
+ children,
9230
9049
  }: DataTableToolbarProps) {
9231
9050
  return (
9232
9051
  <div className="flex items-center gap-2">
@@ -9263,7 +9082,7 @@ export function ReorderControls({
9263
9082
  onSave,
9264
9083
  onCancel,
9265
9084
  hasChanges,
9266
- isSaving
9085
+ isSaving,
9267
9086
  }: ReorderControlsProps) {
9268
9087
  return (
9269
9088
  <div className="flex items-center gap-2">
@@ -9278,26 +9097,14 @@ export function ReorderControls({
9278
9097
  </Button>
9279
9098
  {reorderMode && (
9280
9099
  <>
9281
- <Button
9282
- variant="default"
9283
- size="sm"
9284
- onClick={onSave}
9285
- disabled={!hasChanges || isSaving}
9286
- >
9100
+ <Button variant="default" size="sm" onClick={onSave} disabled={!hasChanges || isSaving}>
9287
9101
  <Save className="size-4 mr-1" />
9288
9102
  {isSaving ? 'Saving...' : 'Save'}
9289
9103
  </Button>
9290
- <Button
9291
- variant="outline"
9292
- size="sm"
9293
- onClick={onCancel}
9294
- disabled={isSaving}
9295
- >
9104
+ <Button variant="outline" size="sm" onClick={onCancel} disabled={isSaving}>
9296
9105
  Cancel
9297
9106
  </Button>
9298
- {hasChanges && (
9299
- <span className="text-sm text-muted-foreground">Unsaved changes</span>
9300
- )}
9107
+ {hasChanges && <span className="text-sm text-muted-foreground">Unsaved changes</span>}
9301
9108
  </>
9302
9109
  )}
9303
9110
  </div>
@@ -9310,7 +9117,6 @@ export function ReorderControls({
9310
9117
  function cmsHeaderTemplate() {
9311
9118
  return `'use client'
9312
9119
 
9313
- import { CmsSearch } from '@cms/components/layout/cms-search'
9314
9120
  import { Button } from '@cms/components/ui/button'
9315
9121
  import { SidebarTrigger, useSidebar } from '@cms/components/ui/sidebar'
9316
9122
  import { useTheme } from '@cms/hooks/use-cms-theme'
@@ -9321,11 +9127,10 @@ export function CmsHeader() {
9321
9127
  const { state } = useSidebar()
9322
9128
 
9323
9129
  return (
9324
- <header className="flex h-14 shrink-0 items-center gap-2 border-b border-border w-full sticky top-0 z-50 bg-sidebar">
9130
+ <header className="flex h-14 shrink-0 items-center gap-2 border-b border-border w-full sticky top-0 z-50">
9325
9131
  <div className="flex items-center px-5 gap-1 flex-1 w-full justify-between">
9326
9132
  <div className="flex items-center gap-2 w-full">
9327
9133
  {state === 'collapsed' && <SidebarTrigger />}
9328
- <CmsSearch />
9329
9134
  </div>
9330
9135
  <div className="flex items-center gap-2 ml-auto">
9331
9136
  <Button
@@ -9345,17 +9150,47 @@ export function CmsHeader() {
9345
9150
  `;
9346
9151
  }
9347
9152
 
9348
- // src/init/templates/components/layout/cms-providers.ts
9349
- function cmsProvidersTemplate() {
9153
+ // src/init/templates/components/layout/cms-nav-link.ts
9154
+ function cmsNavLinkTemplate() {
9350
9155
  return `'use client'
9351
9156
 
9352
- import { useState } from 'react'
9353
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
9354
- import { CmsThemeProvider } from '@cms/hooks/use-cms-theme'
9355
- import { Toaster } from '@cms/components/ui/sonner'
9356
- import { NuqsAdapter } from 'nuqs/adapters/next/app'
9157
+ import { SidebarMenuButton } from '@cms/components/ui/sidebar'
9158
+ import Link from 'next/link'
9159
+ import { usePathname } from 'next/navigation'
9357
9160
 
9358
- export function CmsProviders({ children }: { children: React.ReactNode }) {
9161
+ export function CmsNavLink({
9162
+ href,
9163
+ children,
9164
+ }: {
9165
+ href: string
9166
+ children: React.ReactNode
9167
+ }) {
9168
+ const pathname = usePathname()
9169
+ const isActive =
9170
+ href === '/cms'
9171
+ ? pathname === '/cms'
9172
+ : pathname === href || pathname.startsWith(href + '/')
9173
+
9174
+ return (
9175
+ <SidebarMenuButton asChild isActive={isActive}>
9176
+ <Link href={href}>{children}</Link>
9177
+ </SidebarMenuButton>
9178
+ )
9179
+ }
9180
+ `;
9181
+ }
9182
+
9183
+ // src/init/templates/components/layout/cms-providers.ts
9184
+ function cmsProvidersTemplate() {
9185
+ return `'use client'
9186
+
9187
+ import { Toaster } from '@cms/components/ui/sonner'
9188
+ import { CmsThemeProvider } from '@cms/hooks/use-cms-theme'
9189
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
9190
+ import { NuqsAdapter } from 'nuqs/adapters/next/app'
9191
+ import { useState } from 'react'
9192
+
9193
+ export function CmsProviders({ children }: { children: React.ReactNode }) {
9359
9194
  const [queryClient] = useState(
9360
9195
  () =>
9361
9196
  new QueryClient({
@@ -9393,15 +9228,15 @@ export const CmsSearch = () => {
9393
9228
  <div className="flex items-center gap-2 relative w-full max-w-[240px]">
9394
9229
  <Button
9395
9230
  variant="outline"
9396
- className="w-full text-left items-center pr-1! rounded-full"
9397
- size="sm"
9231
+ className="w-full text-left items-center pr-1.5! py-0 rounded-lg bg-white"
9232
+ size="lg"
9398
9233
  >
9399
- <Search className="shrink-0 size-3.5 -ml-0.5 text-muted-foreground" strokeWidth={2} />
9400
- <span className="w-full font-medium text-xs pt-px text-muted-foreground leading-0">
9401
- Find...
9234
+ <Search className="shrink-0 size-3.5 -ml-0.5 text-muted-foreground/70" />
9235
+ <span className="w-full font-normal text-sm text-muted-foreground/70 [text-box-trim:trim-both]">
9236
+ Quick search...
9402
9237
  </span>
9403
9238
  <div className="flex items-center gap-1 py-0.5 border rounded-full corner-squircle px-2 border-border bg-background">
9404
- <Command className="size-3! text-muted-foreground" strokeWidth={1.5} />
9239
+ <Command className="size-3! text-muted-foreground" />
9405
9240
  <span className="font-mono text-xs font-medium">K</span>
9406
9241
  </div>
9407
9242
  </Button>
@@ -9415,123 +9250,110 @@ CmsSearch.displayName = 'CmsSearch'
9415
9250
 
9416
9251
  // src/init/templates/components/layout/cms-sidebar.ts
9417
9252
  function cmsSidebarTemplate() {
9418
- return `import { getSetting } from '@/cms/lib/actions/settings'
9419
- import { getSession } from '@cms/auth/middleware'
9253
+ return `import { getSession } from '@cms/auth/middleware'
9420
9254
  import { Avatar, AvatarFallback, AvatarImage } from '@cms/components/ui/avatar'
9421
- import {
9422
- Collapsible,
9423
- CollapsibleContent,
9424
- CollapsibleTrigger,
9425
- } from '@cms/components/ui/collapsible'
9426
9255
  import {
9427
9256
  Sidebar,
9428
9257
  SidebarContent,
9429
9258
  SidebarFooter,
9259
+ SidebarGroup,
9260
+ SidebarGroupLabel,
9430
9261
  SidebarHeader,
9431
9262
  SidebarMenu,
9432
- SidebarMenuButton,
9433
9263
  SidebarMenuItem,
9434
- SidebarMenuSub,
9435
- SidebarMenuSubButton,
9436
- SidebarMenuSubItem,
9437
- SidebarRail,
9438
- SidebarTrigger,
9439
9264
  } from '@cms/components/ui/sidebar'
9440
9265
  import { cms } from '@cms/data/cms'
9441
9266
  import { type CmsNavigationItem, cmsNavigation } from '@cms/data/navigation'
9442
- import { ChevronRight, Settings, Users } from 'lucide-react'
9267
+ import { Settings, Users } from 'lucide-react'
9443
9268
  import Link from 'next/link'
9269
+ import { Fragment } from 'react'
9270
+ import { getSetting } from '@/cms/lib/actions/settings'
9271
+ import { CmsNavLink } from './cms-nav-link'
9272
+ import { CmsSearch } from './cms-search'
9444
9273
 
9445
- function NavItem({ item }: { item: CmsNavigationItem }) {
9446
- if (item.children && item.children.length > 0) {
9447
- return (
9448
- <Collapsible asChild defaultOpen className="group/collapsible border-y border-border py-2 px-2">
9449
- <SidebarMenuItem>
9450
- <CollapsibleTrigger asChild>
9451
- <SidebarMenuButton>
9452
- {item.icon && <item.icon className="size-3.5!" />}
9453
- <span>{item.label}</span>
9454
- <ChevronRight className="ml-auto size-3.5! transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
9455
- </SidebarMenuButton>
9456
- </CollapsibleTrigger>
9457
- <CollapsibleContent>
9458
- <SidebarMenuSub>
9459
- {item.children.map((child) => (
9460
- <SidebarMenuSubItem key={child.href}>
9461
- <SidebarMenuSubButton asChild>
9462
- <Link href={child.href}>
9463
- {child.icon && <child.icon className="size-3.5!" />}
9464
- <span>{child.label}</span>
9465
- </Link>
9466
- </SidebarMenuSubButton>
9467
- </SidebarMenuSubItem>
9468
- ))}
9469
- </SidebarMenuSub>
9470
- </CollapsibleContent>
9471
- </SidebarMenuItem>
9472
- </Collapsible>
9473
- )
9274
+ function groupNavItems(items: CmsNavigationItem[]) {
9275
+ const groups: { label: string | null; items: CmsNavigationItem[] }[] = []
9276
+ const groupMap = new Map<string | null, CmsNavigationItem[]>()
9277
+
9278
+ for (const item of items) {
9279
+ const key = item.group ?? null
9280
+ if (!groupMap.has(key)) {
9281
+ const arr: CmsNavigationItem[] = []
9282
+ groupMap.set(key, arr)
9283
+ groups.push({ label: key, items: arr })
9284
+ }
9285
+ groupMap.get(key)!.push(item)
9474
9286
  }
9475
9287
 
9476
- return (
9477
- <SidebarMenuItem className="px-2">
9478
- <SidebarMenuButton asChild>
9479
- <Link href={item.href}>
9480
- {item.icon && <item.icon className="size-3.5!" />}
9481
- <span>{item.label}</span>
9482
- </Link>
9483
- </SidebarMenuButton>
9484
- </SidebarMenuItem>
9485
- )
9288
+ return groups
9486
9289
  }
9487
9290
 
9488
9291
  export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
9489
9292
  const session = await getSession()
9490
9293
  const settings = await getSetting()
9491
9294
  const user = session?.user ?? null
9295
+ const groups = groupNavItems(cmsNavigation)
9492
9296
 
9493
9297
  return (
9494
9298
  <Sidebar collapsible="icon" {...props}>
9495
9299
  <SidebarHeader className="border-b border-border h-14 items-center flex w-full">
9496
9300
  <div className="flex items-center gap-2 w-full relative h-full">
9497
9301
  <Link href="/cms" className="flex items-center gap-2 w-full">
9498
- <Avatar className="size-6.5">
9302
+ <Avatar className="size-8">
9499
9303
  <AvatarImage src={'/favicon.ico'} />
9500
- <AvatarFallback className="text-sm font-semibold">
9304
+ <AvatarFallback className="text-sm font-semibold text-foreground">
9501
9305
  {settings?.siteName?.charAt(0) ?? cms.name?.charAt(0)}
9502
9306
  </AvatarFallback>
9503
9307
  </Avatar>
9504
- <div className="flex items-center gap-1 w-full group-data-[collapsible=icon]:hidden">
9505
- <span className="text-sm font-semibold line-clamp-1">{settings?.siteName ?? cms.name}</span>
9308
+ <div className="flex text-foreground items-center gap-1 w-full group-data-[collapsible=icon]:hidden">
9309
+ <span className="text-sm font-medium line-clamp-1">
9310
+ {settings?.siteName ?? cms.name}
9311
+ </span>
9506
9312
  </div>
9507
9313
  </Link>
9508
- <SidebarTrigger className="hidden md:flex" />
9509
9314
  </div>
9510
9315
  </SidebarHeader>
9511
- <SidebarContent className="gap-2">
9512
- <SidebarMenu className="py-2 gap-2">
9513
- {cmsNavigation.map((item) => (
9514
- <NavItem key={item.href + item.label} item={item} />
9515
- ))}
9516
- </SidebarMenu>
9517
- <SidebarMenu className="py-2 mt-auto border-t border-border px-2">
9518
- <SidebarMenuItem>
9519
- <SidebarMenuButton asChild>
9520
- <Link href="/cms/users">
9521
- <Users className="size-3.5!" />
9316
+ <SidebarContent>
9317
+ <SidebarGroup className="pb-1">
9318
+ <CmsSearch />
9319
+ </SidebarGroup>
9320
+
9321
+ {groups.map((group) => (
9322
+ <Fragment key={group.label ?? '_ungrouped'}>
9323
+ <SidebarGroup>
9324
+ {group.label && <SidebarGroupLabel>{group.label}</SidebarGroupLabel>}
9325
+ <SidebarMenu>
9326
+ {group.items.map((item) => (
9327
+ <SidebarMenuItem key={item.href}>
9328
+ <CmsNavLink href={item.href}>
9329
+ {item.icon && (
9330
+ <item.icon className="text-muted-foreground" absoluteStrokeWidth />
9331
+ )}
9332
+ <span>{item.label}</span>
9333
+ </CmsNavLink>
9334
+ </SidebarMenuItem>
9335
+ ))}
9336
+ </SidebarMenu>
9337
+ </SidebarGroup>
9338
+ </Fragment>
9339
+ ))}
9340
+
9341
+ <SidebarGroup className="mt-auto">
9342
+ <SidebarMenu>
9343
+ <SidebarMenuItem>
9344
+ <CmsNavLink href="/cms/users">
9345
+ <Users />
9522
9346
  <span>Users</span>
9523
- </Link>
9524
- </SidebarMenuButton>
9525
- </SidebarMenuItem>
9526
- <SidebarMenuItem>
9527
- <SidebarMenuButton asChild>
9528
- <Link href="/cms/settings">
9529
- <Settings className="size-3.5!" />
9347
+ </CmsNavLink>
9348
+ </SidebarMenuItem>
9349
+ <SidebarMenuItem>
9350
+ <CmsNavLink href="/cms/settings">
9351
+ <Settings />
9530
9352
  <span>Settings</span>
9531
- </Link>
9532
- </SidebarMenuButton>
9533
- </SidebarMenuItem>
9534
- </SidebarMenu>
9353
+ </CmsNavLink>
9354
+ </SidebarMenuItem>
9355
+ </SidebarMenu>
9356
+ </SidebarGroup>
9535
9357
  </SidebarContent>
9536
9358
  <SidebarFooter>
9537
9359
  {user && (
@@ -9543,7 +9365,6 @@ export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
9543
9365
  </div>
9544
9366
  )}
9545
9367
  </SidebarFooter>
9546
- <SidebarRail />
9547
9368
  </Sidebar>
9548
9369
  )
9549
9370
  }
@@ -9563,7 +9384,7 @@ import {
9563
9384
  AlertDialogFooter,
9564
9385
  AlertDialogHeader,
9565
9386
  AlertDialogTitle,
9566
- AlertDialogTrigger
9387
+ AlertDialogTrigger,
9567
9388
  } from '@cms/components/ui/alert-dialog'
9568
9389
  import { Button } from '@cms/components/ui/button'
9569
9390
  import { Trash2 } from 'lucide-react'
@@ -9585,7 +9406,7 @@ export function DeleteDialog({
9585
9406
  isPending = false,
9586
9407
  title = 'Are you sure?',
9587
9408
  description = 'This action cannot be undone. This will permanently delete this item.',
9588
- trigger
9409
+ trigger,
9589
9410
  }: DeleteDialogProps) {
9590
9411
  return (
9591
9412
  <AlertDialog open={open} onOpenChange={onOpenChange}>
@@ -9634,18 +9455,24 @@ export function DeleteButton({ onClick, label = 'item', count }: DeleteButtonPro
9634
9455
  function pageHeaderTemplate() {
9635
9456
  return `interface PageHeaderProps {
9636
9457
  title: string
9637
- description: string
9638
9458
  children?: React.ReactNode
9459
+ search?: React.ReactNode
9460
+ actions?: React.ReactNode
9461
+ back?: React.ReactNode
9639
9462
  }
9640
9463
 
9641
- export function PageHeader({ title, description, children }: PageHeaderProps) {
9464
+ export function PageHeader({ title, children, search, actions, back }: PageHeaderProps) {
9642
9465
  return (
9643
- <div className="flex items-center justify-between w-full">
9644
- <div className="flex flex-col">
9645
- <h2 className="text-base font-semibold tracking-tight">{title}</h2>
9646
- <p className="text-muted-foreground text-sm">{description}</p>
9466
+ <div className="grid grid-cols-3 items-center justify-between w-full h-14 px-4 border-b border-border">
9467
+ <div className="flex items-center justify-start gap-2 w-full">{back && back}</div>
9468
+ <div className="flex items-center justify-center gap-2">
9469
+ <h2 className="text-sm font-medium tracking-tight">{title}</h2>
9470
+ </div>
9471
+ <div className="flex items-center justify-end gap-2">
9472
+ {children && children}
9473
+ {search && search}
9474
+ {actions && actions}
9647
9475
  </div>
9648
- {children && <div className="flex items-center gap-2">{children}</div>}
9649
9476
  </div>
9650
9477
  )
9651
9478
  }
@@ -9664,7 +9491,7 @@ const statusStyles: Record<StatusVariant, string> = {
9664
9491
  success: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300',
9665
9492
  warning: 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300',
9666
9493
  error: 'bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300',
9667
- info: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300'
9494
+ info: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300',
9668
9495
  }
9669
9496
 
9670
9497
  interface StatusBadgeProps extends Omit<BadgeProps, 'variant'> {
@@ -9682,7 +9509,11 @@ export function StatusBadge({ status, className, ...props }: StatusBadgeProps) {
9682
9509
  }
9683
9510
 
9684
9511
  /** Map boolean values to status badges */
9685
- export function BooleanBadge({ value, trueLabel = 'Yes', falseLabel = 'No' }: {
9512
+ export function BooleanBadge({
9513
+ value,
9514
+ trueLabel = 'Yes',
9515
+ falseLabel = 'No',
9516
+ }: {
9686
9517
  value: boolean
9687
9518
  trueLabel?: string
9688
9519
  falseLabel?: string
@@ -9706,22 +9537,44 @@ function cmsDataTemplate(projectName) {
9706
9537
 
9707
9538
  // src/init/templates/data/navigation.ts
9708
9539
  function navigationDataTemplate() {
9709
- return `import { House } from 'lucide-react'
9710
- import type { LucideIcon } from 'lucide-react'
9540
+ return `import type { LucideIcon } from 'lucide-react'
9541
+ import { ChartSpline, FileText, House, ImagePlay, Tag } from 'lucide-react'
9711
9542
 
9712
9543
  export interface CmsNavigationItem {
9713
9544
  label: string
9714
9545
  href: string
9715
9546
  icon?: LucideIcon
9716
- children?: CmsNavigationItem[]
9547
+ group?: string
9717
9548
  }
9718
9549
 
9719
9550
  export const cmsNavigation: CmsNavigationItem[] = [
9720
9551
  {
9721
- label: 'Dashboard',
9552
+ label: 'Overview',
9722
9553
  href: '/cms',
9723
- icon: House
9724
- }
9554
+ icon: House,
9555
+ },
9556
+ {
9557
+ label: 'Analytics',
9558
+ href: '/cms/analytics',
9559
+ icon: ChartSpline,
9560
+ },
9561
+ {
9562
+ label: 'Media',
9563
+ href: '/cms/media',
9564
+ icon: ImagePlay,
9565
+ },
9566
+ {
9567
+ label: 'Categories',
9568
+ href: '/cms/categories',
9569
+ icon: Tag,
9570
+ group: 'Blog',
9571
+ },
9572
+ {
9573
+ label: 'Posts',
9574
+ href: '/cms/posts',
9575
+ icon: FileText,
9576
+ group: 'Blog',
9577
+ },
9725
9578
  ]
9726
9579
  `;
9727
9580
  }
@@ -9795,14 +9648,10 @@ export function CmsThemeProvider({ children }: { children: React.ReactNode }) {
9795
9648
 
9796
9649
  const value = React.useMemo(
9797
9650
  () => ({ theme, setTheme, resolvedTheme: resolved }),
9798
- [theme, setTheme, resolved]
9651
+ [theme, setTheme, resolved],
9799
9652
  )
9800
9653
 
9801
- return (
9802
- <CmsThemeContext.Provider value={value}>
9803
- {children}
9804
- </CmsThemeContext.Provider>
9805
- )
9654
+ return <CmsThemeContext.Provider value={value}>{children}</CmsThemeContext.Provider>
9806
9655
  }
9807
9656
 
9808
9657
  export function useTheme(): ThemeContext {
@@ -9842,11 +9691,11 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
9842
9691
  if (result.success && result.files) {
9843
9692
  const images: EditorImageUploadResult[] = result.files.map((f) => ({
9844
9693
  url: f.url,
9845
- filename: f.filename
9694
+ filename: f.filename,
9846
9695
  }))
9847
9696
  onImagesUploadedRef.current(images)
9848
9697
  }
9849
- }
9698
+ },
9850
9699
  })
9851
9700
 
9852
9701
  const isUploading = mutation.isPending
@@ -9857,21 +9706,19 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
9857
9706
  if (imageFiles.length === 0) return
9858
9707
  upload(imageFiles, 'images')
9859
9708
  },
9860
- [upload]
9709
+ [upload],
9861
9710
  )
9862
9711
 
9863
9712
  const handleDrop = React.useCallback(
9864
9713
  (e: React.DragEvent) => {
9865
9714
  e.preventDefault()
9866
9715
  e.stopPropagation()
9867
- const files = Array.from(e.dataTransfer.files).filter((f) =>
9868
- f.type.startsWith('image/')
9869
- )
9716
+ const files = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith('image/'))
9870
9717
  if (files.length > 0) {
9871
9718
  uploadImages(files)
9872
9719
  }
9873
9720
  },
9874
- [uploadImages]
9721
+ [uploadImages],
9875
9722
  )
9876
9723
 
9877
9724
  const openFilePicker = React.useCallback(() => {
@@ -9886,7 +9733,7 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
9886
9733
  }
9887
9734
  e.target.value = ''
9888
9735
  },
9889
- [uploadImages]
9736
+ [uploadImages],
9890
9737
  )
9891
9738
 
9892
9739
  return {
@@ -9896,7 +9743,7 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
9896
9743
  handleDrop,
9897
9744
  openFilePicker,
9898
9745
  fileInputRef,
9899
- handleFileInputChange
9746
+ handleFileInputChange,
9900
9747
  }
9901
9748
  }
9902
9749
  `;
@@ -9920,7 +9767,7 @@ export function useLocalStorage<T>(key: string) {
9920
9767
  // Silent failure for localStorage access errors
9921
9768
  }
9922
9769
  },
9923
- [prefixedKey]
9770
+ [prefixedKey],
9924
9771
  )
9925
9772
 
9926
9773
  const getItem = React.useCallback((): T | null => {
@@ -9953,18 +9800,36 @@ export function useLocalStorage<T>(key: string) {
9953
9800
  `;
9954
9801
  }
9955
9802
 
9803
+ // src/init/templates/hooks/use-mobile.ts
9804
+ function useMobileHookTemplate() {
9805
+ return `import * as React from 'react'
9806
+
9807
+ const MOBILE_BREAKPOINT = 768
9808
+
9809
+ export function useIsMobile() {
9810
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
9811
+
9812
+ React.useEffect(() => {
9813
+ const mql = window.matchMedia(\`(max-width: \${MOBILE_BREAKPOINT - 1}px)\`)
9814
+ const onChange = () => {
9815
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
9816
+ }
9817
+ mql.addEventListener('change', onChange)
9818
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
9819
+ return () => mql.removeEventListener('change', onChange)
9820
+ }, [])
9821
+
9822
+ return !!isMobile
9823
+ }
9824
+ `;
9825
+ }
9826
+
9956
9827
  // src/init/templates/hooks/use-upload.ts
9957
9828
  function useUploadHookTemplate() {
9958
9829
  return `'use client'
9959
9830
 
9960
- import type {
9961
- UploadFileResult,
9962
- UploadProgress
9963
- } from '@cms/types'
9964
- import {
9965
- type FileValidationConfig,
9966
- validateFiles
9967
- } from '@cms/utils/validation'
9831
+ import type { UploadFileResult, UploadProgress } from '@cms/types'
9832
+ import { type FileValidationConfig, validateFiles } from '@cms/utils/validation'
9968
9833
  import { type UseMutationResult, useMutation } from '@tanstack/react-query'
9969
9834
  import * as React from 'react'
9970
9835
 
@@ -10012,7 +9877,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10012
9877
  onProgress,
10013
9878
  onSuccess,
10014
9879
  onError,
10015
- prefix: defaultPrefix
9880
+ prefix: defaultPrefix,
10016
9881
  } = options
10017
9882
 
10018
9883
  const validationConfig = React.useMemo<FileValidationConfig>(() => {
@@ -10022,7 +9887,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10022
9887
  const parsedTypes = parseAcceptTypes(accept)
10023
9888
  if (config.allowedTypes && config.allowedTypes.length > 0) {
10024
9889
  config.allowedTypes = [...config.allowedTypes, ...parsedTypes].filter(
10025
- (v, i, a) => a.indexOf(v) === i
9890
+ (v, i, a) => a.indexOf(v) === i,
10026
9891
  )
10027
9892
  } else {
10028
9893
  config.allowedTypes = parsedTypes
@@ -10045,7 +9910,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10045
9910
  filename: file.name,
10046
9911
  progress: 0,
10047
9912
  loaded: 0,
10048
- total: file.size
9913
+ total: file.size,
10049
9914
  }))
10050
9915
  setProgress(initialProgress)
10051
9916
  onProgress?.(initialProgress)
@@ -10067,9 +9932,9 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10067
9932
  return {
10068
9933
  ...p,
10069
9934
  progress: newProgress,
10070
- loaded: Math.floor((p.total * newProgress) / 100)
9935
+ loaded: Math.floor((p.total * newProgress) / 100),
10071
9936
  }
10072
- })
9937
+ }),
10073
9938
  )
10074
9939
  }, 200)
10075
9940
 
@@ -10083,7 +9948,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10083
9948
 
10084
9949
  const response = await fetch('/api/cms/upload', {
10085
9950
  method: 'POST',
10086
- body: formData
9951
+ body: formData,
10087
9952
  })
10088
9953
 
10089
9954
  const result = (await response.json()) as UploadFileResult
@@ -10093,7 +9958,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10093
9958
  filename: file.name,
10094
9959
  progress: 100,
10095
9960
  loaded: file.size,
10096
- total: file.size
9961
+ total: file.size,
10097
9962
  }))
10098
9963
  setProgress(completeProgress)
10099
9964
  onProgress?.(completeProgress)
@@ -10115,14 +9980,14 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10115
9980
  onError: (error) => {
10116
9981
  onError?.(error)
10117
9982
  setProgress([])
10118
- }
9983
+ },
10119
9984
  })
10120
9985
 
10121
9986
  const upload = React.useCallback(
10122
9987
  (files: File[], prefix?: string) => {
10123
9988
  mutation.mutate({ files, prefix })
10124
9989
  },
10125
- [mutation]
9990
+ [mutation],
10126
9991
  )
10127
9992
 
10128
9993
  const validate = React.useCallback(
@@ -10130,10 +9995,10 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10130
9995
  const result = validateFiles(files, validationConfig)
10131
9996
  return {
10132
9997
  valid: result.valid,
10133
- errors: result.errors.map((e) => \`\${e.filename}: \${e.error}\`)
9998
+ errors: result.errors.map((e) => \`\${e.filename}: \${e.error}\`),
10134
9999
  }
10135
10000
  },
10136
- [validationConfig]
10001
+ [validationConfig],
10137
10002
  )
10138
10003
 
10139
10004
  return { mutation, progress, upload, validate }
@@ -10153,7 +10018,7 @@ export function useUsers() {
10153
10018
  return useQuery<UsersResponse>({
10154
10019
  queryKey: ['users'],
10155
10020
  queryFn: () => getUsers(),
10156
- staleTime: 0
10021
+ staleTime: 0,
10157
10022
  })
10158
10023
  }
10159
10024
  `;
@@ -10163,9 +10028,9 @@ export function useUsers() {
10163
10028
  function formSettingsActionTemplate() {
10164
10029
  return `'use server'
10165
10030
 
10166
- import { eq } from 'drizzle-orm'
10167
10031
  import db from '@cms/db'
10168
10032
  import { formSettings } from '@cms/db/schema'
10033
+ import { eq } from 'drizzle-orm'
10169
10034
 
10170
10035
  export interface FormSettingsData {
10171
10036
  id: number
@@ -10189,9 +10054,7 @@ export interface FormSettingsResult {
10189
10054
  settings?: FormSettingsData
10190
10055
  }
10191
10056
 
10192
- export async function getFormSettings(
10193
- formName: string
10194
- ): Promise<FormSettingsData | null> {
10057
+ export async function getFormSettings(formName: string): Promise<FormSettingsData | null> {
10195
10058
  try {
10196
10059
  const [settings] = await db
10197
10060
  .select()
@@ -10207,7 +10070,7 @@ export async function getFormSettings(
10207
10070
 
10208
10071
  export async function upsertFormSettings(
10209
10072
  formName: string,
10210
- data: UpsertFormSettingsInput
10073
+ data: UpsertFormSettingsInput,
10211
10074
  ): Promise<FormSettingsResult> {
10212
10075
  try {
10213
10076
  const existing = await getFormSettings(formName)
@@ -10238,8 +10101,7 @@ export async function upsertFormSettings(
10238
10101
  console.error(\`Error upserting form settings for \${formName}:\`, error)
10239
10102
  return {
10240
10103
  success: false,
10241
- error:
10242
- error instanceof Error ? error.message : 'Failed to save form settings',
10104
+ error: error instanceof Error ? error.message : 'Failed to save form settings',
10243
10105
  }
10244
10106
  }
10245
10107
  }
@@ -10255,7 +10117,7 @@ export async function getAllFormSettings(): Promise<FormSettingsData[]> {
10255
10117
  }
10256
10118
 
10257
10119
  export async function testFormWebhook(
10258
- formName: string
10120
+ formName: string,
10259
10121
  ): Promise<{ success: boolean; error?: string }> {
10260
10122
  try {
10261
10123
  const settings = await getFormSettings(formName)
@@ -10286,8 +10148,7 @@ export async function testFormWebhook(
10286
10148
  } catch (error) {
10287
10149
  return {
10288
10150
  success: false,
10289
- error:
10290
- error instanceof Error ? error.message : 'Failed to send test webhook',
10151
+ error: error instanceof Error ? error.message : 'Failed to send test webhook',
10291
10152
  }
10292
10153
  }
10293
10154
  }
@@ -10456,10 +10317,10 @@ export async function uploadImageFromUrl(
10456
10317
  function usersActionTemplate() {
10457
10318
  return `'use server'
10458
10319
 
10320
+ import { auth } from '@cms/auth'
10459
10321
  import db from '@cms/db'
10460
- import { user, account } from '@cms/db/schema'
10322
+ import { user } from '@cms/db/schema'
10461
10323
  import { eq } from 'drizzle-orm'
10462
- import { auth } from '@cms/auth'
10463
10324
 
10464
10325
  export interface UserData {
10465
10326
  id: string
@@ -10572,15 +10433,9 @@ export async function getUsers(): Promise<UsersResponse> {
10572
10433
  /**
10573
10434
  * Update a user's role
10574
10435
  */
10575
- export async function updateUserRole(
10576
- userId: string,
10577
- role: string,
10578
- ): Promise<UpdateUserRoleResult> {
10436
+ export async function updateUserRole(userId: string, role: string): Promise<UpdateUserRoleResult> {
10579
10437
  try {
10580
- await db
10581
- .update(user)
10582
- .set({ role, updatedAt: new Date() })
10583
- .where(eq(user.id, userId))
10438
+ await db.update(user).set({ role, updatedAt: new Date() }).where(eq(user.id, userId))
10584
10439
 
10585
10440
  return { success: true }
10586
10441
  } catch (error) {
@@ -10730,10 +10585,21 @@ function trimMathBlock(content: string): string {
10730
10585
  const shiki = createHighlighterCoreSync({
10731
10586
  themes: [githubDark, githubLight],
10732
10587
  langs: [
10733
- javascript, typescript, jsx, tsx, python, rust, go,
10734
- json, yaml, css, sql, shellscript, markdown
10588
+ javascript,
10589
+ typescript,
10590
+ jsx,
10591
+ tsx,
10592
+ python,
10593
+ rust,
10594
+ go,
10595
+ json,
10596
+ yaml,
10597
+ css,
10598
+ sql,
10599
+ shellscript,
10600
+ markdown,
10735
10601
  ],
10736
- engine: createJavaScriptRegexEngine()
10602
+ engine: createJavaScriptRegexEngine(),
10737
10603
  })
10738
10604
 
10739
10605
  const loadedLangs = shiki.getLoadedLanguages()
@@ -10775,11 +10641,11 @@ const md = MarkdownIt({
10775
10641
  lang: language,
10776
10642
  themes: { light: 'github-light', dark: 'github-dark' },
10777
10643
  defaultColor: false,
10778
- transformers: [transformerNotationHighlight(), transformerNotationDiff()]
10644
+ transformers: [transformerNotationHighlight(), transformerNotationDiff()],
10779
10645
  })
10780
10646
  const escapedCode = code.replace(/</g, '&lt;').replace(/>/g, '&gt;')
10781
10647
  return \`<div class="code-block-wrapper not-prose" data-lang="\${language}"><button class="copy-button" data-code="\${escapedCode.replace(/"/g, '&quot;')}" aria-label="Copy code">Copy</button>\${highlighted}</div>\`
10782
- }
10648
+ },
10783
10649
  })
10784
10650
  .use(headingAnchorPlugin)
10785
10651
  .use(dollarmath, {
@@ -10792,7 +10658,7 @@ const md = MarkdownIt({
10792
10658
  return renderToString(content, {
10793
10659
  displayMode,
10794
10660
  throwOnError: false,
10795
- strict: 'ignore'
10661
+ strict: 'ignore',
10796
10662
  })
10797
10663
  },
10798
10664
  labelNormalizer(label: string) {
@@ -10800,7 +10666,7 @@ const md = MarkdownIt({
10800
10666
  },
10801
10667
  labelRenderer(label: string) {
10802
10668
  return \`<a href="#\${label}" class="mathlabel" title="Permalink to this equation">\xB6</a>\`
10803
- }
10669
+ },
10804
10670
  })
10805
10671
 
10806
10672
  export function renderMarkdownSync(src: string): string {
@@ -10895,7 +10761,7 @@ export interface AuthSession {
10895
10761
  export enum UserRole {
10896
10762
  ADMIN = 'admin',
10897
10763
  EDITOR = 'editor',
10898
- MEMBER = 'member'
10764
+ MEMBER = 'member',
10899
10765
  }
10900
10766
 
10901
10767
  export interface UserWithRole extends AuthUser {
@@ -10904,17 +10770,11 @@ export interface UserWithRole extends AuthUser {
10904
10770
 
10905
10771
  /** Type guard to check if a value is a valid UserRole */
10906
10772
  export function isUserRole(value: unknown): value is UserRole {
10907
- return (
10908
- typeof value === 'string' &&
10909
- Object.values(UserRole).includes(value as UserRole)
10910
- )
10773
+ return typeof value === 'string' && Object.values(UserRole).includes(value as UserRole)
10911
10774
  }
10912
10775
 
10913
10776
  /** Check if user has one of the allowed roles */
10914
- export function hasRequiredRole(
10915
- userRole: UserRole,
10916
- allowedRoles: UserRole[]
10917
- ): boolean {
10777
+ export function hasRequiredRole(userRole: UserRole, allowedRoles: UserRole[]): boolean {
10918
10778
  return allowedRoles.includes(userRole)
10919
10779
  }
10920
10780
 
@@ -11100,7 +10960,7 @@ export function createMetadata({
11100
10960
  description,
11101
10961
  path,
11102
10962
  ogImage,
11103
- noIndex = false
10963
+ noIndex = false,
11104
10964
  }: CreateMetadataOptions): Metadata {
11105
10965
  const metadata: Metadata = {
11106
10966
  title,
@@ -11109,14 +10969,14 @@ export function createMetadata({
11109
10969
  title,
11110
10970
  description,
11111
10971
  type: 'website',
11112
- ...(ogImage && { images: [{ url: ogImage }] })
10972
+ ...(ogImage && { images: [{ url: ogImage }] }),
11113
10973
  },
11114
10974
  twitter: {
11115
10975
  card: ogImage ? 'summary_large_image' : 'summary',
11116
10976
  title,
11117
10977
  description,
11118
- ...(ogImage && { images: [ogImage] })
11119
- }
10978
+ ...(ogImage && { images: [ogImage] }),
10979
+ },
11120
10980
  }
11121
10981
 
11122
10982
  if (path) {
@@ -11140,7 +11000,7 @@ export function generateArticleSchema({
11140
11000
  imageUrl,
11141
11001
  datePublished,
11142
11002
  dateModified,
11143
- authorName
11003
+ authorName,
11144
11004
  }: {
11145
11005
  title: string
11146
11006
  description: string
@@ -11160,8 +11020,8 @@ export function generateArticleSchema({
11160
11020
  datePublished,
11161
11021
  ...(dateModified && { dateModified }),
11162
11022
  ...(authorName && {
11163
- author: { '@type': 'Person', name: authorName }
11164
- })
11023
+ author: { '@type': 'Person', name: authorName },
11024
+ }),
11165
11025
  }
11166
11026
  }
11167
11027
 
@@ -11214,20 +11074,16 @@ function isFileTypeAllowed(file: File, allowedTypes: string[]): boolean {
11214
11074
  */
11215
11075
  export function validateFiles(
11216
11076
  files: File[],
11217
- config: FileValidationConfig = {}
11077
+ config: FileValidationConfig = {},
11218
11078
  ): FileValidationResult {
11219
- const {
11220
- maxSizeInBytes = DEFAULT_MAX_SIZE,
11221
- allowedTypes,
11222
- maxFiles = DEFAULT_MAX_FILES
11223
- } = config
11079
+ const { maxSizeInBytes = DEFAULT_MAX_SIZE, allowedTypes, maxFiles = DEFAULT_MAX_FILES } = config
11224
11080
 
11225
11081
  const errors: FileValidationError[] = []
11226
11082
 
11227
11083
  if (files.length > maxFiles) {
11228
11084
  errors.push({
11229
11085
  filename: '',
11230
- error: \`Too many files. Maximum is \${maxFiles}.\`
11086
+ error: \`Too many files. Maximum is \${maxFiles}.\`,
11231
11087
  })
11232
11088
  }
11233
11089
 
@@ -11236,14 +11092,14 @@ export function validateFiles(
11236
11092
  const maxMB = Math.round(maxSizeInBytes / (1024 * 1024))
11237
11093
  errors.push({
11238
11094
  filename: file.name,
11239
- error: \`File exceeds maximum size of \${maxMB}MB.\`
11095
+ error: \`File exceeds maximum size of \${maxMB}MB.\`,
11240
11096
  })
11241
11097
  }
11242
11098
 
11243
11099
  if (allowedTypes && allowedTypes.length > 0 && !isFileTypeAllowed(file, allowedTypes)) {
11244
11100
  errors.push({
11245
11101
  filename: file.name,
11246
- error: \`File type "\${file.type || 'unknown'}" is not allowed.\`
11102
+ error: \`File type "\${file.type || 'unknown'}" is not allowed.\`,
11247
11103
  })
11248
11104
  }
11249
11105
  }
@@ -11297,17 +11153,15 @@ function webhookUtilTemplate() {
11297
11153
  */
11298
11154
  export function sendWebhook(
11299
11155
  webhookUrl: string | null | undefined,
11300
- payload: Record<string, unknown>
11156
+ payload: Record<string, unknown>,
11301
11157
  ): void {
11302
- if (!webhookUrl) return
11303
- // Fire-and-forget: runs in background, doesn't block
11158
+ if (!webhookUrl) return // Fire-and-forget: runs in background, doesn't block
11304
11159
  ;(async () => {
11305
11160
  try {
11306
11161
  const formData = new URLSearchParams()
11307
11162
  for (const [key, value] of Object.entries(payload)) {
11308
11163
  if (value === null || value === undefined) continue
11309
- const stringValue =
11310
- typeof value === 'object' ? JSON.stringify(value) : String(value)
11164
+ const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value)
11311
11165
  formData.append(key, stringValue)
11312
11166
  }
11313
11167
  await fetch(webhookUrl, {
@@ -11335,6 +11189,7 @@ function scaffoldComponents({ cwd, config }) {
11335
11189
  }
11336
11190
  write("cms-globals.css", cmsGlobalsCssTemplate());
11337
11191
  write("components/layout/cms-providers.tsx", cmsProvidersTemplate());
11192
+ write("components/layout/cms-nav-link.tsx", cmsNavLinkTemplate());
11338
11193
  write("components/layout/cms-sidebar.tsx", cmsSidebarTemplate());
11339
11194
  write("components/layout/cms-header.tsx", cmsHeaderTemplate());
11340
11195
  write("components/layout/cms-search.tsx", cmsSearchTemplate());
@@ -11360,6 +11215,7 @@ function scaffoldComponents({ cwd, config }) {
11360
11215
  write("hooks/use-local-storage.ts", useLocalStorageHookTemplate());
11361
11216
  write("hooks/use-cms-theme.tsx", useCmsThemeTemplate());
11362
11217
  write("hooks/use-users.ts", useUsersHookTemplate());
11218
+ write("hooks/use-mobile.ts", useMobileHookTemplate());
11363
11219
  const projectName = detectProjectName(cwd);
11364
11220
  write("data/cms.ts", cmsDataTemplate(projectName));
11365
11221
  write("data/navigation.ts", navigationDataTemplate());
@@ -11596,6 +11452,7 @@ var CORE_DEPS = [
11596
11452
  "nuqs",
11597
11453
  "sonner",
11598
11454
  // Styling utilities
11455
+ "geist",
11599
11456
  "class-variance-authority",
11600
11457
  "clsx",
11601
11458
  "tailwind-merge",
@@ -11841,24 +11698,18 @@ import path32 from "path";
11841
11698
 
11842
11699
  // src/init/templates/pages/authenticated-layout.ts
11843
11700
  function authenticatedLayoutTemplate() {
11844
- return `import { CmsHeader } from '@cms/components/layout/cms-header'
11701
+ return `import { requireRole } from '@cms/auth/middleware'
11845
11702
  import { CmsSidebar } from '@cms/components/layout/cms-sidebar'
11846
- import { requireRole } from '@cms/auth/middleware'
11847
- import { UserRole } from '@cms/types/auth'
11848
11703
  import { SidebarInset, SidebarProvider } from '@cms/components/ui/sidebar'
11704
+ import { UserRole } from '@cms/types/auth'
11849
11705
 
11850
- export default async function CmsAuthLayout({
11851
- children
11852
- }: {
11853
- children: React.ReactNode
11854
- }) {
11706
+ export default async function CmsAuthLayout({ children }: { children: React.ReactNode }) {
11855
11707
  await requireRole([UserRole.ADMIN, UserRole.EDITOR])
11856
11708
 
11857
11709
  return (
11858
11710
  <SidebarProvider>
11859
11711
  <CmsSidebar />
11860
11712
  <SidebarInset>
11861
- <CmsHeader />
11862
11713
  <main>{children}</main>
11863
11714
  </SidebarInset>
11864
11715
  </SidebarProvider>
@@ -11871,11 +11722,17 @@ export default async function CmsAuthLayout({
11871
11722
  function cmsLayoutTemplate() {
11872
11723
  return `import '@cms/cms-globals.css'
11873
11724
  import { CmsProviders } from '@cms/components/layout/cms-providers'
11725
+ import { GeistMono } from 'geist/font/mono'
11726
+ import { GeistSans } from 'geist/font/sans'
11874
11727
 
11875
11728
  export default function CmsLayout({ children }: { children: React.ReactNode }) {
11876
11729
  return (
11877
11730
  <CmsProviders>
11878
- <div className="cms-root min-h-screen">{children}</div>
11731
+ <div
11732
+ className={\`cms-root min-h-screen antialiased \${GeistSans.variable} \${GeistMono.variable}\`}
11733
+ >
11734
+ {children}
11735
+ </div>
11879
11736
  </CmsProviders>
11880
11737
  )
11881
11738
  }
@@ -11885,8 +11742,8 @@ export default function CmsLayout({ children }: { children: React.ReactNode }) {
11885
11742
  // src/init/templates/pages/dashboard-page.ts
11886
11743
  function dashboardPageTemplate() {
11887
11744
  return `import { PageHeader } from '@cms/components/shared/page-header'
11888
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cms/components/ui/card'
11889
11745
  import { Badge } from '@cms/components/ui/badge'
11746
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cms/components/ui/card'
11890
11747
  import { FileText, Settings, Users } from 'lucide-react'
11891
11748
  import Link from 'next/link'
11892
11749
 
@@ -11895,32 +11752,27 @@ const quickLinks = [
11895
11752
  title: 'Users',
11896
11753
  description: 'Manage admin users and roles',
11897
11754
  href: '/cms/users',
11898
- icon: Users
11755
+ icon: Users,
11899
11756
  },
11900
11757
  {
11901
11758
  title: 'Settings',
11902
11759
  description: 'Configure CMS settings',
11903
11760
  href: '/cms/settings',
11904
- icon: Settings
11761
+ icon: Settings,
11905
11762
  },
11906
11763
  {
11907
11764
  title: 'Generate',
11908
11765
  description: 'Add a new resource from a schema',
11909
11766
  href: '#',
11910
11767
  icon: FileText,
11911
- hint: 'npx betterstart generate <schema>'
11912
- }
11768
+ hint: 'npx betterstart generate <schema>',
11769
+ },
11913
11770
  ]
11914
11771
 
11915
11772
  export default function DashboardPage() {
11916
11773
  return (
11917
11774
  <div className="flex flex-col">
11918
- <div className="flex items-center justify-between bg-card px-6 py-4 border-b">
11919
- <PageHeader
11920
- title="Dashboard"
11921
- description="Welcome to your CMS admin panel"
11922
- />
11923
- </div>
11775
+ <PageHeader title="Dashboard" />
11924
11776
  <div className="p-6 space-y-6">
11925
11777
  <div className="grid gap-4 md:grid-cols-3">
11926
11778
  {quickLinks.map((link) => (
@@ -11990,7 +11842,7 @@ import { LoginForm } from './login-form'
11990
11842
 
11991
11843
  export const metadata: Metadata = {
11992
11844
  title: 'CMS Login',
11993
- robots: { index: false, follow: false }
11845
+ robots: { index: false, follow: false },
11994
11846
  }
11995
11847
 
11996
11848
  export default function LoginPage() {
@@ -11999,9 +11851,7 @@ export default function LoginPage() {
11999
11851
  <div className="w-full max-w-sm">
12000
11852
  <div className="mb-8 text-center">
12001
11853
  <h1 className="text-2xl font-semibold tracking-tight">CMS</h1>
12002
- <p className="text-muted-foreground text-sm mt-1">
12003
- Sign in to access the admin panel
12004
- </p>
11854
+ <p className="text-muted-foreground text-sm mt-1">Sign in to access the admin panel</p>
12005
11855
  </div>
12006
11856
  <LoginForm />
12007
11857
  </div>
@@ -12036,7 +11886,7 @@ export function LoginForm() {
12036
11886
  try {
12037
11887
  const result = await authClient.signIn.email({
12038
11888
  email,
12039
- password
11889
+ password,
12040
11890
  })
12041
11891
 
12042
11892
  if (result.error) {
@@ -12056,9 +11906,7 @@ export function LoginForm() {
12056
11906
  return (
12057
11907
  <form onSubmit={handleSubmit} className="space-y-5">
12058
11908
  {error && (
12059
- <div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">
12060
- {error}
12061
- </div>
11909
+ <div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">{error}</div>
12062
11910
  )}
12063
11911
 
12064
11912
  <div className="space-y-2">
@@ -12103,6 +11951,7 @@ export function LoginForm() {
12103
11951
  function createUserDialogTemplate() {
12104
11952
  return `'use client'
12105
11953
 
11954
+ import { createUser } from '@cms/actions/users'
12106
11955
  import { Button } from '@cms/components/ui/button'
12107
11956
  import {
12108
11957
  Dialog,
@@ -12110,11 +11959,10 @@ import {
12110
11959
  DialogDescription,
12111
11960
  DialogHeader,
12112
11961
  DialogTitle,
12113
- DialogTrigger
11962
+ DialogTrigger,
12114
11963
  } from '@cms/components/ui/dialog'
12115
11964
  import { Input } from '@cms/components/ui/input'
12116
11965
  import { Label } from '@cms/components/ui/label'
12117
- import { createUser } from '@cms/actions/users'
12118
11966
  import { useQueryClient } from '@tanstack/react-query'
12119
11967
  import { Loader2, UserPlus } from 'lucide-react'
12120
11968
  import * as React from 'react'
@@ -12224,6 +12072,7 @@ export function CreateUserDialog() {
12224
12072
  function editRoleDialogTemplate() {
12225
12073
  return `'use client'
12226
12074
 
12075
+ import { updateUserRole } from '@cms/actions/users'
12227
12076
  import { Button } from '@cms/components/ui/button'
12228
12077
  import {
12229
12078
  Dialog,
@@ -12231,7 +12080,7 @@ import {
12231
12080
  DialogDescription,
12232
12081
  DialogHeader,
12233
12082
  DialogTitle,
12234
- DialogTrigger
12083
+ DialogTrigger,
12235
12084
  } from '@cms/components/ui/dialog'
12236
12085
  import { Label } from '@cms/components/ui/label'
12237
12086
  import {
@@ -12239,9 +12088,8 @@ import {
12239
12088
  SelectContent,
12240
12089
  SelectItem,
12241
12090
  SelectTrigger,
12242
- SelectValue
12091
+ SelectValue,
12243
12092
  } from '@cms/components/ui/select'
12244
- import { updateUserRole } from '@cms/actions/users'
12245
12093
  import { UserRole } from '@cms/types/auth'
12246
12094
  import { useQueryClient } from '@tanstack/react-query'
12247
12095
  import { Loader2 } from 'lucide-react'
@@ -12255,12 +12103,7 @@ interface EditRoleDialogProps {
12255
12103
  children: React.ReactNode
12256
12104
  }
12257
12105
 
12258
- export function EditRoleDialog({
12259
- userId,
12260
- currentRole,
12261
- userName,
12262
- children
12263
- }: EditRoleDialogProps) {
12106
+ export function EditRoleDialog({ userId, currentRole, userName, children }: EditRoleDialogProps) {
12264
12107
  const [open, setOpen] = React.useState(false)
12265
12108
  const [role, setRole] = React.useState(currentRole)
12266
12109
  const [isPending, startTransition] = React.useTransition()
@@ -12289,9 +12132,7 @@ export function EditRoleDialog({
12289
12132
  <DialogContent className="sm:max-w-[350px]">
12290
12133
  <DialogHeader>
12291
12134
  <DialogTitle>Edit Role</DialogTitle>
12292
- <DialogDescription>
12293
- Change the role for {userName}
12294
- </DialogDescription>
12135
+ <DialogDescription>Change the role for {userName}</DialogDescription>
12295
12136
  </DialogHeader>
12296
12137
  <div className="space-y-4">
12297
12138
  <div className="space-y-2">
@@ -12308,17 +12149,10 @@ export function EditRoleDialog({
12308
12149
  </Select>
12309
12150
  </div>
12310
12151
  <div className="flex justify-end gap-2">
12311
- <Button
12312
- variant="outline"
12313
- onClick={() => setOpen(false)}
12314
- disabled={isPending}
12315
- >
12152
+ <Button variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
12316
12153
  Cancel
12317
12154
  </Button>
12318
- <Button
12319
- onClick={handleSave}
12320
- disabled={isPending || role === currentRole}
12321
- >
12155
+ <Button onClick={handleSave} disabled={isPending || role === currentRole}>
12322
12156
  {isPending && <Loader2 className="size-4 mr-1 animate-spin" />}
12323
12157
  Save
12324
12158
  </Button>
@@ -12335,11 +12169,7 @@ export function EditRoleDialog({
12335
12169
  function usersColumnsTemplate() {
12336
12170
  return `'use client'
12337
12171
 
12338
- import React from 'react'
12339
- import {
12340
- Avatar,
12341
- AvatarFallback
12342
- } from '@cms/components/ui/avatar'
12172
+ import { deleteUser } from '@cms/actions/users'
12343
12173
  import {
12344
12174
  AlertDialog,
12345
12175
  AlertDialogAction,
@@ -12351,6 +12181,7 @@ import {
12351
12181
  AlertDialogTitle,
12352
12182
  AlertDialogTrigger,
12353
12183
  } from '@cms/components/ui/alert-dialog'
12184
+ import { Avatar, AvatarFallback } from '@cms/components/ui/avatar'
12354
12185
  import { Badge } from '@cms/components/ui/badge'
12355
12186
  import { Button } from '@cms/components/ui/button'
12356
12187
  import {
@@ -12359,14 +12190,14 @@ import {
12359
12190
  DropdownMenuItem,
12360
12191
  DropdownMenuLabel,
12361
12192
  DropdownMenuSeparator,
12362
- DropdownMenuTrigger
12193
+ DropdownMenuTrigger,
12363
12194
  } from '@cms/components/ui/dropdown-menu'
12364
12195
  import type { UserData } from '@cms/types/auth'
12365
- import type { ColumnDef } from '@tanstack/react-table'
12366
12196
  import { useQueryClient } from '@tanstack/react-query'
12197
+ import type { ColumnDef } from '@tanstack/react-table'
12367
12198
  import { ArrowUpDown, Edit, MoreHorizontal, Trash } from 'lucide-react'
12199
+ import React from 'react'
12368
12200
  import { toast } from 'sonner'
12369
- import { deleteUser } from '@cms/actions/users'
12370
12201
  import { EditRoleDialog } from './edit-role-dialog'
12371
12202
 
12372
12203
  function getInitials(nameOrEmail: string): string {
@@ -12417,10 +12248,7 @@ function DeleteUserAction({
12417
12248
  return (
12418
12249
  <AlertDialog open={open} onOpenChange={setOpen}>
12419
12250
  <AlertDialogTrigger asChild>
12420
- <DropdownMenuItem
12421
- className="text-destructive"
12422
- onSelect={(e) => e.preventDefault()}
12423
- >
12251
+ <DropdownMenuItem className="text-destructive" onSelect={(e) => e.preventDefault()}>
12424
12252
  <Trash className="size-4 mr-2" />
12425
12253
  Delete user
12426
12254
  </DropdownMenuItem>
@@ -12429,8 +12257,8 @@ function DeleteUserAction({
12429
12257
  <AlertDialogHeader>
12430
12258
  <AlertDialogTitle>Are you sure?</AlertDialogTitle>
12431
12259
  <AlertDialogDescription>
12432
- This action cannot be undone. This will permanently delete{' '}
12433
- <strong>{userName}</strong> and all of their data.
12260
+ This action cannot be undone. This will permanently delete <strong>{userName}</strong>{' '}
12261
+ and all of their data.
12434
12262
  </AlertDialogDescription>
12435
12263
  </AlertDialogHeader>
12436
12264
  <AlertDialogFooter>
@@ -12478,7 +12306,7 @@ export const columns: ColumnDef<UserData>[] = [
12478
12306
  </div>
12479
12307
  </div>
12480
12308
  )
12481
- }
12309
+ },
12482
12310
  },
12483
12311
  {
12484
12312
  accessorKey: 'emailVerified',
@@ -12490,7 +12318,7 @@ export const columns: ColumnDef<UserData>[] = [
12490
12318
  {verified ? 'Verified' : 'Unverified'}
12491
12319
  </Badge>
12492
12320
  )
12493
- }
12321
+ },
12494
12322
  },
12495
12323
  {
12496
12324
  accessorKey: 'role',
@@ -12520,7 +12348,7 @@ export const columns: ColumnDef<UserData>[] = [
12520
12348
  </Badge>
12521
12349
  </EditRoleDialog>
12522
12350
  )
12523
- }
12351
+ },
12524
12352
  },
12525
12353
  {
12526
12354
  accessorKey: 'createdAt',
@@ -12541,11 +12369,11 @@ export const columns: ColumnDef<UserData>[] = [
12541
12369
  {date.toLocaleDateString('en-US', {
12542
12370
  month: 'short',
12543
12371
  day: 'numeric',
12544
- year: 'numeric'
12372
+ year: 'numeric',
12545
12373
  })}
12546
12374
  </div>
12547
12375
  )
12548
- }
12376
+ },
12549
12377
  },
12550
12378
  {
12551
12379
  id: 'actions',
@@ -12564,9 +12392,7 @@ export const columns: ColumnDef<UserData>[] = [
12564
12392
  </DropdownMenuTrigger>
12565
12393
  <DropdownMenuContent align="end">
12566
12394
  <DropdownMenuLabel>Actions</DropdownMenuLabel>
12567
- <DropdownMenuItem
12568
- onClick={() => navigator.clipboard.writeText(row.original.id)}
12569
- >
12395
+ <DropdownMenuItem onClick={() => navigator.clipboard.writeText(row.original.id)}>
12570
12396
  Copy user ID
12571
12397
  </DropdownMenuItem>
12572
12398
  <DropdownMenuSeparator />
@@ -12579,8 +12405,8 @@ export const columns: ColumnDef<UserData>[] = [
12579
12405
  </DropdownMenu>
12580
12406
  </div>
12581
12407
  )
12582
- }
12583
- }
12408
+ },
12409
+ },
12584
12410
  ]
12585
12411
  `;
12586
12412
  }
@@ -12596,10 +12422,7 @@ export default function UsersPage() {
12596
12422
  return (
12597
12423
  <div className="flex flex-col">
12598
12424
  <div className="flex items-center justify-between bg-card px-6 py-4 border-b">
12599
- <PageHeader
12600
- title="Users"
12601
- description="Manage all CMS users"
12602
- >
12425
+ <PageHeader title="Users">
12603
12426
  <CreateUserDialog />
12604
12427
  </PageHeader>
12605
12428
  </div>
@@ -12616,6 +12439,18 @@ export default function UsersPage() {
12616
12439
  function usersTableTemplate() {
12617
12440
  return `'use client'
12618
12441
 
12442
+ import { authClient } from '@cms/auth/client'
12443
+ import { Button } from '@cms/components/ui/button'
12444
+ import {
12445
+ Table,
12446
+ TableBody,
12447
+ TableCell,
12448
+ TableHead,
12449
+ TableHeader,
12450
+ TableRow,
12451
+ } from '@cms/components/ui/table'
12452
+ import { useUsers } from '@cms/hooks/use-users'
12453
+ import type { UserData } from '@cms/types/auth'
12619
12454
  import {
12620
12455
  type ColumnDef,
12621
12456
  type ColumnFiltersState,
@@ -12626,21 +12461,9 @@ import {
12626
12461
  getSortedRowModel,
12627
12462
  type SortingState,
12628
12463
  useReactTable,
12629
- type VisibilityState
12464
+ type VisibilityState,
12630
12465
  } from '@tanstack/react-table'
12631
12466
  import * as React from 'react'
12632
- import { Button } from '@cms/components/ui/button'
12633
- import {
12634
- Table,
12635
- TableBody,
12636
- TableCell,
12637
- TableHead,
12638
- TableHeader,
12639
- TableRow
12640
- } from '@cms/components/ui/table'
12641
- import { useUsers } from '@cms/hooks/use-users'
12642
- import { authClient } from '@cms/auth/client'
12643
- import type { UserData } from '@cms/types/auth'
12644
12467
 
12645
12468
  interface UsersTableProps<TValue> {
12646
12469
  columns: ColumnDef<UserData, TValue>[]
@@ -12670,11 +12493,11 @@ export function UsersTable<TValue>({ columns }: UsersTableProps<TValue>) {
12670
12493
  email: session.user.email,
12671
12494
  name: session.user.name,
12672
12495
  image: session.user.image,
12673
- role: (session.user as { role?: string }).role || 'member'
12496
+ role: (session.user as { role?: string }).role || 'member',
12674
12497
  }
12675
- : null
12498
+ : null,
12676
12499
  },
12677
- state: { sorting, columnFilters, columnVisibility }
12500
+ state: { sorting, columnFilters, columnVisibility },
12678
12501
  })
12679
12502
 
12680
12503
  return (
@@ -13614,32 +13437,37 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
13614
13437
  const { execFile } = await import("child_process");
13615
13438
  const tsxBin = path36.join(cwd, "node_modules", ".bin", "tsx");
13616
13439
  const runSeed2 = (overwrite) => new Promise((resolve, reject) => {
13617
- execFile(tsxBin, [seedPath], {
13618
- cwd,
13619
- env: {
13620
- ...process.env,
13621
- SEED_EMAIL: email,
13622
- SEED_PASSWORD: password3,
13623
- SEED_NAME: name || "Admin",
13624
- ...overwrite ? { SEED_OVERWRITE: "true" } : {}
13625
- }
13626
- }, (err, stdout, stderr) => {
13627
- if (err && "code" in err && err.code === 2) {
13628
- resolve({ code: 2, stdout });
13629
- } else if (err) {
13630
- reject(new Error(stderr || err.message));
13631
- } else {
13632
- resolve({ code: 0, stdout });
13440
+ execFile(
13441
+ tsxBin,
13442
+ [seedPath],
13443
+ {
13444
+ cwd,
13445
+ env: {
13446
+ ...process.env,
13447
+ SEED_EMAIL: email,
13448
+ SEED_PASSWORD: password3,
13449
+ SEED_NAME: name || "Admin",
13450
+ ...overwrite ? { SEED_OVERWRITE: "true" } : {}
13451
+ }
13452
+ },
13453
+ (err, stdout, stderr) => {
13454
+ if (err && "code" in err && err.code === 2) {
13455
+ resolve({ code: 2, stdout });
13456
+ } else if (err) {
13457
+ reject(new Error(stderr || err.message));
13458
+ } else {
13459
+ resolve({ code: 0, stdout });
13460
+ }
13633
13461
  }
13634
- });
13462
+ );
13635
13463
  });
13636
- const spinner4 = clack.spinner();
13637
- spinner4.start("Creating admin user...");
13464
+ const spinner5 = clack.spinner();
13465
+ spinner5.start("Creating admin user...");
13638
13466
  try {
13639
13467
  const result = await runSeed2(false);
13640
13468
  if (result.code === 2) {
13641
13469
  const existingName = result.stdout.split("\n").find((l) => l.startsWith("EXISTING_USER:"))?.replace("EXISTING_USER:", "")?.trim() || "unknown";
13642
- spinner4.stop(`Account already exists for ${email}`);
13470
+ spinner5.stop(`Account already exists for ${email}`);
13643
13471
  const overwrite = await clack.confirm({
13644
13472
  message: `An admin account (${existingName}) already exists with this email. Replace it?`
13645
13473
  });
@@ -13651,14 +13479,14 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
13651
13479
  }
13652
13480
  process.exit(0);
13653
13481
  }
13654
- spinner4.start("Replacing admin user...");
13482
+ spinner5.start("Replacing admin user...");
13655
13483
  await runSeed2(true);
13656
- spinner4.stop("Admin user replaced");
13484
+ spinner5.stop("Admin user replaced");
13657
13485
  } else {
13658
- spinner4.stop("Admin user created");
13486
+ spinner5.stop("Admin user created");
13659
13487
  }
13660
13488
  } catch (err) {
13661
- spinner4.stop("Failed to create admin user");
13489
+ spinner5.stop("Failed to create admin user");
13662
13490
  const errMsg = err instanceof Error ? err.message : String(err);
13663
13491
  clack.log.error(errMsg);
13664
13492
  clack.log.info("You can run the seed script manually:");
@@ -13682,7 +13510,7 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
13682
13510
  var initCommand = new Command3("init").description("Scaffold CMS into a new or existing Next.js project").argument("[name]", "Project name (creates new directory if fresh project)").option("--preset <preset>", "Starter preset: blank, blog, or full", "blog").option("-y, --yes", "Skip all prompts (accept defaults)").option(
13683
13511
  "--database-url <url>",
13684
13512
  "PostgreSQL database connection string (postgres:// or postgresql://)"
13685
- ).action(
13513
+ ).option("--force", "Overwrite all existing CMS files (nuclear option)").action(
13686
13514
  async (name, options) => {
13687
13515
  p4.intro(pc2.bgCyan(pc2.black(" BetterStart CMS ")));
13688
13516
  let cwd = process.cwd();
@@ -13691,21 +13519,45 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13691
13519
  let isFreshProject = false;
13692
13520
  let srcDir;
13693
13521
  if (project.isExisting) {
13694
- p4.log.info(`Existing Next.js project detected`);
13695
- p4.log.info(`Package manager: ${pc2.cyan(pm)}`);
13522
+ p4.log.info(`Next.js project detected ${pc2.dim("\xB7")} ${pc2.cyan(pm)}`);
13696
13523
  srcDir = project.hasSrcDir;
13697
13524
  if (!project.hasTypeScript) {
13698
13525
  p4.log.error("TypeScript is required. Please add a tsconfig.json first.");
13699
13526
  process.exit(1);
13700
13527
  }
13701
- if (project.conflicts.length > 0) {
13702
- p4.log.error("Conflicts detected:");
13703
- for (const conflict of project.conflicts) {
13704
- p4.log.warning(` - ${conflict}`);
13528
+ if (options.force) {
13529
+ const nukeDirs = ["cms", "app/(cms)"];
13530
+ const nukeFiles = ["cms.config.ts", "CMS.md", "drizzle.config.ts"];
13531
+ let nuked = 0;
13532
+ for (const dir of nukeDirs) {
13533
+ const fullPath = path37.resolve(cwd, dir);
13534
+ if (fs32.existsSync(fullPath)) {
13535
+ fs32.rmSync(fullPath, { recursive: true, force: true });
13536
+ nuked++;
13537
+ }
13538
+ }
13539
+ for (const file of nukeFiles) {
13540
+ const fullPath = path37.resolve(cwd, file);
13541
+ if (fs32.existsSync(fullPath)) {
13542
+ fs32.unlinkSync(fullPath);
13543
+ nuked++;
13544
+ }
13705
13545
  }
13546
+ if (nuked > 0) {
13547
+ p4.log.warn(`${pc2.yellow("Force mode:")} removed ${nuked} existing CMS paths`);
13548
+ }
13549
+ project = detectProject(cwd);
13550
+ } else if (project.conflicts.length > 0) {
13551
+ const conflictLines = project.conflicts.map((c) => `${pc2.yellow("\u25B2")} ${c}`);
13552
+ conflictLines.push(
13553
+ "",
13554
+ pc2.dim("Existing files will not be overwritten."),
13555
+ pc2.dim(`Use ${pc2.bold("--force")} to remove existing CMS files before scaffolding.`)
13556
+ );
13557
+ p4.note(conflictLines.join("\n"), pc2.yellow("Conflicts"));
13706
13558
  if (!options.yes) {
13707
13559
  const proceed = await p4.confirm({
13708
- message: "Continue anyway? (existing files will NOT be overwritten)",
13560
+ message: "Continue anyway?",
13709
13561
  initialValue: true
13710
13562
  });
13711
13563
  if (p4.isCancel(proceed) || !proceed) {
@@ -13814,80 +13666,91 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13814
13666
  ...getDefaultConfig(srcDir),
13815
13667
  features: { email: features.includeEmail }
13816
13668
  };
13669
+ const results = [];
13817
13670
  const s = p4.spinner();
13818
- s.start("Creating CMS directory structure...");
13671
+ s.start("Directory structure");
13819
13672
  const baseFiles = scaffoldBase({ cwd, config });
13820
- s.stop(`Created ${baseFiles.length} files`);
13821
- s.start("Configuring TypeScript path aliases...");
13673
+ results.push({ label: "Directory structure", result: `${baseFiles.length} files` });
13674
+ s.message("TypeScript aliases");
13822
13675
  const tsResult = scaffoldTsconfig(cwd);
13823
- s.stop(`Added ${tsResult.added.length} path aliases`);
13824
- s.start("Configuring Tailwind CSS...");
13676
+ results.push({
13677
+ label: "TypeScript aliases",
13678
+ result: tsResult.added.length > 0 ? `${tsResult.added.length} paths` : "already set"
13679
+ });
13680
+ s.message("Tailwind CSS");
13825
13681
  const twResult = scaffoldTailwind(cwd, srcDir);
13826
- if (twResult.appended) {
13827
- s.stop(`Updated ${twResult.file}`);
13828
- } else if (twResult.file) {
13829
- s.stop("Tailwind already configured for CMS");
13830
- } else {
13831
- s.stop("No CSS file found (will configure later)");
13832
- }
13833
- s.start("Setting up environment variables...");
13682
+ results.push({
13683
+ label: "Tailwind CSS",
13684
+ result: twResult.appended ? "updated" : twResult.file ? "already set" : "no CSS file"
13685
+ });
13686
+ s.message("Environment variables");
13834
13687
  const envResult = scaffoldEnv(cwd, { includeEmail: features.includeEmail, databaseUrl });
13835
- const envParts = [`Added ${envResult.added.length}`];
13836
- if (envResult.updated.length > 0) envParts.push(`updated ${envResult.updated.length}`);
13837
- s.stop(`${envParts.join(", ")} env vars in .env.local`);
13838
- s.start("Setting up database...");
13688
+ const envCount = envResult.added.length + envResult.updated.length;
13689
+ results.push({
13690
+ label: "Environment variables",
13691
+ result: envCount > 0 ? `${envCount} vars` : "already set"
13692
+ });
13693
+ s.message("Database");
13839
13694
  const dbFiles = scaffoldDatabase({ cwd, config });
13840
- s.stop(`Created ${dbFiles.length} database files`);
13695
+ results.push({ label: "Database", result: `${dbFiles.length} files` });
13696
+ s.message("Authentication");
13697
+ const authFiles = scaffoldAuth({ cwd, config });
13698
+ results.push({ label: "Authentication", result: `${authFiles.length} files` });
13699
+ s.message("Components");
13700
+ const compFiles = scaffoldComponents({ cwd, config });
13701
+ results.push({ label: "Components", result: `${compFiles.length} files` });
13702
+ s.message("Pages & layouts");
13703
+ const layoutFiles = scaffoldLayout({ cwd, config });
13704
+ results.push({ label: "Pages & layouts", result: `${layoutFiles.length} files` });
13705
+ s.message("API routes");
13706
+ const apiFiles = scaffoldApiRoutes({ cwd, config });
13707
+ results.push({ label: "API routes", result: `${apiFiles.length} routes` });
13708
+ s.message("Linter");
13709
+ let linterResult;
13710
+ if (project.linter.type === "none") {
13711
+ const biomeResult = scaffoldBiome(cwd, project.linter);
13712
+ linterResult = biomeResult.installed ? "biome (new)" : "none";
13713
+ } else {
13714
+ linterResult = project.linter.type;
13715
+ }
13716
+ results.push({ label: "Linter", result: linterResult });
13717
+ const maxLabel = Math.max(...results.map((r) => r.label.length));
13718
+ const noteLines = results.map((r) => {
13719
+ const padded = r.label.padEnd(maxLabel + 3);
13720
+ return `${pc2.green("\u2713")} ${padded}${pc2.dim(r.result)}`;
13721
+ });
13722
+ s.stop("");
13723
+ process.stdout.write("\x1B[2A\x1B[J");
13724
+ p4.note(noteLines.join("\n"), "Scaffolded CMS");
13841
13725
  const drizzleConfigPath = path37.join(cwd, "drizzle.config.ts");
13842
13726
  if (!dbFiles.includes("drizzle.config.ts") && fs32.existsSync(drizzleConfigPath)) {
13843
- if (!options.yes) {
13727
+ if (options.force) {
13728
+ const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-EDKOEZ6G.js");
13729
+ fs32.writeFileSync(drizzleConfigPath, drizzleConfigTemplate2(), "utf-8");
13730
+ p4.log.success("Updated drizzle.config.ts");
13731
+ } else if (!options.yes) {
13844
13732
  const overwrite = await p4.confirm({
13845
13733
  message: "drizzle.config.ts already exists. Overwrite with latest version?",
13846
13734
  initialValue: true
13847
13735
  });
13848
13736
  if (!p4.isCancel(overwrite) && overwrite) {
13849
- const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-KISB26BA.js");
13737
+ const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-EDKOEZ6G.js");
13850
13738
  fs32.writeFileSync(drizzleConfigPath, drizzleConfigTemplate2(), "utf-8");
13851
13739
  p4.log.success("Updated drizzle.config.ts");
13852
13740
  }
13853
13741
  }
13854
13742
  }
13855
- s.start("Setting up authentication...");
13856
- const authFiles = scaffoldAuth({ cwd, config });
13857
- s.stop(`Created ${authFiles.length} auth files`);
13858
- s.start("Copying CMS components...");
13859
- const compFiles = scaffoldComponents({ cwd, config });
13860
- s.stop(`Created ${compFiles.length} component files`);
13861
- s.start("Creating CMS pages and layouts...");
13862
- const layoutFiles = scaffoldLayout({ cwd, config });
13863
- s.stop(`Created ${layoutFiles.length} page files`);
13864
- s.start("Creating API routes...");
13865
- const apiFiles = scaffoldApiRoutes({ cwd, config });
13866
- s.stop(`Created ${apiFiles.length} API routes`);
13867
- s.start("Checking for linter...");
13868
- if (project.linter.type === "none") {
13869
- s.stop("No linter found");
13870
- s.start("Setting up Biome linter...");
13871
- const biomeResult = scaffoldBiome(cwd, project.linter);
13872
- if (biomeResult.installed) {
13873
- s.stop("Created biome.json");
13874
- } else {
13875
- s.stop(`Biome skipped: ${biomeResult.skippedReason}`);
13876
- }
13877
- } else {
13878
- s.stop(`Linter: ${pc2.cyan(project.linter.type)} (${project.linter.configFile})`);
13879
- }
13880
- s.start("Installing dependencies (this may take a minute)...");
13743
+ s.start("Installing dependencies (this may take a minute)");
13881
13744
  const depsResult = await installDependenciesAsync({
13882
13745
  cwd,
13883
13746
  pm,
13884
13747
  includeEmail: features.includeEmail,
13885
13748
  includeBiome: project.linter.type === "none"
13886
13749
  });
13750
+ let depsInstalled = false;
13887
13751
  if (depsResult.success) {
13888
- s.stop(
13889
- `Installed ${depsResult.coreDeps.length} deps + ${depsResult.devDeps.length} dev deps`
13890
- );
13752
+ s.stop("");
13753
+ depsInstalled = true;
13891
13754
  } else {
13892
13755
  s.stop("Failed to install dependencies");
13893
13756
  p4.log.warning(depsResult.error ?? "Unknown error");
@@ -13897,24 +13760,59 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13897
13760
  ${pc2.cyan(`${pm} add -D ${depsResult.devDeps.join(" ")}`)}`
13898
13761
  );
13899
13762
  }
13900
- s.start(`Applying ${features.preset} preset...`);
13763
+ if (depsInstalled) {
13764
+ process.stdout.write("\x1B[2A\x1B[J");
13765
+ }
13766
+ s.start(`Applying ${features.preset} preset`);
13901
13767
  const presetResult = scaffoldPreset({ cwd, config, preset: features.preset });
13768
+ {
13769
+ const entityNames = [];
13770
+ const formNames = [];
13771
+ const schemasDir = path37.join(cwd, config.paths.schemas);
13772
+ const formsDir = path37.join(schemasDir, "forms");
13773
+ if (fs32.existsSync(schemasDir)) {
13774
+ for (const f of fs32.readdirSync(schemasDir)) {
13775
+ if (f.endsWith(".json")) entityNames.push(f.replace(".json", ""));
13776
+ }
13777
+ }
13778
+ if (fs32.existsSync(formsDir)) {
13779
+ for (const f of fs32.readdirSync(formsDir)) {
13780
+ if (f.endsWith(".json")) formNames.push(f.replace(".json", ""));
13781
+ }
13782
+ }
13783
+ regenerateCmsDoc(cwd, config, {
13784
+ preset: features.preset,
13785
+ schemas: entityNames,
13786
+ forms: formNames
13787
+ });
13788
+ }
13789
+ s.stop("");
13790
+ process.stdout.write("\x1B[2A\x1B[J");
13791
+ const installLines = [];
13792
+ if (depsInstalled) {
13793
+ installLines.push(
13794
+ `${pc2.green("\u2713")} Dependencies ${pc2.dim(`${depsResult.coreDeps.length} deps + ${depsResult.devDeps.length} dev deps`)}`
13795
+ );
13796
+ }
13902
13797
  if (presetResult.errors.length > 0) {
13903
- s.stop(`Preset applied with ${presetResult.errors.length} warning(s)`);
13798
+ installLines.push(
13799
+ `${pc2.yellow("\u25B2")} Preset ${pc2.dim(`${features.preset} \u2014 ${presetResult.errors.length} warning(s)`)}`
13800
+ );
13904
13801
  for (const err of presetResult.errors) {
13905
- p4.log.warning(` ${err}`);
13802
+ installLines.push(` ${pc2.dim(err)}`);
13906
13803
  }
13907
13804
  } else {
13908
- s.stop(
13909
- `Created ${presetResult.schemas.length} schemas, generated ${presetResult.generatedFiles.length} files`
13805
+ installLines.push(
13806
+ `${pc2.green("\u2713")} Preset ${pc2.dim(`${features.preset} \u2014 ${presetResult.schemas.length} schemas, ${presetResult.generatedFiles.length} files`)}`
13910
13807
  );
13911
13808
  }
13809
+ p4.note(installLines.join("\n"), "Installed");
13912
13810
  let dbPushed = false;
13913
13811
  if (depsResult.success && hasDbUrl(cwd)) {
13914
- s.start("Pushing database schema (drizzle-kit push)...");
13812
+ s.start("Pushing database schema (drizzle-kit push)");
13915
13813
  const pushResult = await runDrizzlePush(cwd);
13916
13814
  if (pushResult.success) {
13917
- s.stop("Database schema pushed");
13815
+ s.stop(`${pc2.green("\u2713")} Database schema pushed`);
13918
13816
  dbPushed = true;
13919
13817
  } else {
13920
13818
  s.stop("Database push failed");
@@ -13926,66 +13824,73 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13926
13824
  let seedPassword;
13927
13825
  let seedSuccess = false;
13928
13826
  if (dbPushed && !options.yes) {
13929
- p4.log.step("Create your admin account");
13930
- const email = await p4.text({
13931
- message: "Admin email",
13932
- placeholder: "admin@example.com",
13933
- validate: (v) => {
13934
- if (!v || !v.includes("@")) return "Please enter a valid email";
13827
+ p4.note(pc2.dim("Create your first admin user to access the CMS."), "Admin account");
13828
+ const credentials = await p4.group(
13829
+ {
13830
+ email: () => p4.text({
13831
+ message: "Admin email",
13832
+ placeholder: "admin@example.com",
13833
+ validate: (v) => {
13834
+ if (!v || !v.includes("@")) return "Please enter a valid email";
13835
+ }
13836
+ }),
13837
+ password: () => p4.password({
13838
+ message: "Admin password",
13839
+ validate: (v) => {
13840
+ if (!v || v.length < 8) return "Password must be at least 8 characters";
13841
+ }
13842
+ })
13843
+ },
13844
+ {
13845
+ onCancel: () => {
13846
+ p4.cancel("Setup cancelled.");
13847
+ process.exit(0);
13848
+ }
13935
13849
  }
13936
- });
13937
- if (p4.isCancel(email)) {
13938
- p4.cancel("Setup cancelled.");
13939
- process.exit(0);
13940
- }
13941
- const password3 = await p4.password({
13942
- message: "Admin password",
13943
- validate: (v) => {
13944
- if (!v || v.length < 8) return "Password must be at least 8 characters";
13850
+ );
13851
+ seedEmail = credentials.email;
13852
+ seedPassword = credentials.password;
13853
+ s.start("Creating admin user");
13854
+ let seedResult = await runSeed(
13855
+ cwd,
13856
+ config.paths?.cms ?? "./cms",
13857
+ credentials.email,
13858
+ credentials.password
13859
+ );
13860
+ if (seedResult.existingUser) {
13861
+ s.stop(`${pc2.yellow("\u25B2")} Admin user already exists (${seedResult.existingUser})`);
13862
+ const replace = await p4.confirm({
13863
+ message: "Replace existing admin user?",
13864
+ initialValue: false
13865
+ });
13866
+ if (!p4.isCancel(replace) && replace) {
13867
+ s.start("Replacing admin user");
13868
+ seedResult = await runSeed(
13869
+ cwd,
13870
+ config.paths?.cms ?? "./cms",
13871
+ credentials.email,
13872
+ credentials.password,
13873
+ true
13874
+ );
13875
+ } else {
13876
+ seedSuccess = true;
13945
13877
  }
13946
- });
13947
- if (p4.isCancel(password3)) {
13948
- p4.cancel("Setup cancelled.");
13949
- process.exit(0);
13950
13878
  }
13951
- seedEmail = email;
13952
- seedPassword = password3;
13953
- s.start("Creating admin user...");
13954
- const seedResult = await runSeed(cwd, config.paths?.cms ?? "./cms", email, password3);
13955
13879
  if (seedResult.success) {
13956
- s.stop("Admin user created");
13880
+ s.stop(`${pc2.green("\u2713")} Admin user created`);
13957
13881
  seedSuccess = true;
13958
- } else {
13959
- s.stop("Failed to create admin user");
13960
- p4.log.warning(seedResult.error ?? "Unknown error");
13961
- p4.log.info(`You can run it manually: ${pc2.cyan("npx betterstart seed")}`);
13962
- }
13963
- }
13964
- s.start("Generating documentation...");
13965
- {
13966
- const entityNames = [];
13967
- const formNames = [];
13968
- const schemasDir = path37.join(cwd, config.paths.schemas);
13969
- const formsDir = path37.join(schemasDir, "forms");
13970
- if (fs32.existsSync(schemasDir)) {
13971
- for (const f of fs32.readdirSync(schemasDir)) {
13972
- if (f.endsWith(".json")) entityNames.push(f.replace(".json", ""));
13973
- }
13974
- }
13975
- if (fs32.existsSync(formsDir)) {
13976
- for (const f of fs32.readdirSync(formsDir)) {
13977
- if (f.endsWith(".json")) formNames.push(f.replace(".json", ""));
13978
- }
13882
+ } else if (!seedSuccess && seedResult.error) {
13883
+ s.stop(`${pc2.red("\u2717")} Failed to create admin user`);
13884
+ p4.note(
13885
+ `${pc2.red(seedResult.error)}
13886
+
13887
+ Run manually: ${pc2.cyan("npx betterstart seed")}`,
13888
+ pc2.red("Seed failed")
13889
+ );
13979
13890
  }
13980
- regenerateCmsDoc(cwd, config, {
13981
- preset: features.preset,
13982
- schemas: entityNames,
13983
- forms: formNames
13984
- });
13985
13891
  }
13986
- s.stop("Generated CMS.md");
13987
13892
  if (isFreshProject) {
13988
- s.start("Creating initial git commit...");
13893
+ s.start("Creating initial git commit");
13989
13894
  try {
13990
13895
  execFileSync4("git", ["init"], { cwd, stdio: "pipe" });
13991
13896
  execFileSync4("git", ["add", "."], { cwd, stdio: "pipe" });
@@ -14001,7 +13906,6 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
14001
13906
  const totalFiles = baseFiles.length + dbFiles.length + authFiles.length + compFiles.length + layoutFiles.length + apiFiles.length;
14002
13907
  const summaryLines = [
14003
13908
  `Preset: ${pc2.cyan(features.preset)}`,
14004
- `Email: ${features.includeEmail ? pc2.green("yes") : pc2.dim("no")}`,
14005
13909
  `Files created: ${pc2.cyan(String(totalFiles))}`,
14006
13910
  `Env vars: ${envResult.added.length} added, ${envResult.skipped.length} skipped`
14007
13911
  ];
@@ -14091,26 +13995,14 @@ function hasDbUrl(cwd) {
14091
13995
  }
14092
13996
  return false;
14093
13997
  }
14094
- async function runSeed(cwd, cmsDir, email, password3) {
13998
+ function runSeed(cwd, cmsDir, email, password3, overwrite = false) {
14095
13999
  const scriptsDir = path37.join(cwd, cmsDir, "scripts");
14096
14000
  const seedPath = path37.join(scriptsDir, "seed.ts");
14097
14001
  if (!fs32.existsSync(scriptsDir)) {
14098
14002
  fs32.mkdirSync(scriptsDir, { recursive: true });
14099
14003
  }
14100
14004
  fs32.writeFileSync(seedPath, buildSeedScript(), "utf-8");
14101
- try {
14102
- const tsxBin = path37.join(cwd, "node_modules", ".bin", "tsx");
14103
- execFileSync4(tsxBin, [seedPath], {
14104
- cwd,
14105
- stdio: "pipe",
14106
- timeout: 3e4,
14107
- env: { ...process.env, SEED_EMAIL: email, SEED_PASSWORD: password3, SEED_NAME: "Admin" }
14108
- });
14109
- return { success: true, error: null };
14110
- } catch (err) {
14111
- const msg = err instanceof Error ? err.message : String(err);
14112
- return { success: false, error: msg };
14113
- } finally {
14005
+ const cleanup = () => {
14114
14006
  try {
14115
14007
  fs32.unlinkSync(seedPath);
14116
14008
  if (fs32.existsSync(scriptsDir) && fs32.readdirSync(scriptsDir).length === 0) {
@@ -14118,7 +14010,70 @@ async function runSeed(cwd, cmsDir, email, password3) {
14118
14010
  }
14119
14011
  } catch {
14120
14012
  }
14121
- }
14013
+ };
14014
+ return new Promise((resolve) => {
14015
+ const tsxBin = path37.join(cwd, "node_modules", ".bin", "tsx");
14016
+ const child = spawn2(tsxBin, [seedPath], {
14017
+ cwd,
14018
+ stdio: "pipe",
14019
+ env: {
14020
+ ...process.env,
14021
+ SEED_EMAIL: email,
14022
+ SEED_PASSWORD: password3,
14023
+ SEED_NAME: "Admin",
14024
+ ...overwrite ? { SEED_OVERWRITE: "true" } : {}
14025
+ }
14026
+ });
14027
+ let stdout = "";
14028
+ let stderr = "";
14029
+ child.stdout?.on("data", (chunk) => {
14030
+ stdout += chunk.toString();
14031
+ });
14032
+ child.stderr?.on("data", (chunk) => {
14033
+ stderr += chunk.toString();
14034
+ });
14035
+ const timeout = setTimeout(() => {
14036
+ child.kill();
14037
+ cleanup();
14038
+ resolve({ success: false, error: "Seed timed out after 30 seconds" });
14039
+ }, 3e4);
14040
+ child.on("close", (code) => {
14041
+ clearTimeout(timeout);
14042
+ cleanup();
14043
+ if (code === 0) {
14044
+ resolve({ success: true, error: null });
14045
+ } else if (code === 2) {
14046
+ const name = stdout.match(/EXISTING_USER:(.+)/)?.[1]?.trim();
14047
+ resolve({ success: false, error: null, existingUser: name ?? email });
14048
+ } else {
14049
+ resolve({ success: false, error: parseSeedError(stdout, stderr) });
14050
+ }
14051
+ });
14052
+ child.on("error", (err) => {
14053
+ clearTimeout(timeout);
14054
+ cleanup();
14055
+ resolve({ success: false, error: parseSeedError("", err.message) });
14056
+ });
14057
+ });
14058
+ }
14059
+ function parseSeedError(stdout, stderr) {
14060
+ const combined = `${stdout}
14061
+ ${stderr}`;
14062
+ const seedFailed = combined.match(/Seed failed:\s*(.+)/)?.[1]?.trim();
14063
+ if (seedFailed) return seedFailed;
14064
+ if (combined.includes("Failed to create user")) return "Auth API failed to create user";
14065
+ if (combined.includes("ECONNREFUSED") || combined.includes("connection refused"))
14066
+ return "Could not connect to database";
14067
+ if (combined.includes("BETTERSTART_DATABASE_URL")) return "Database URL is missing or invalid";
14068
+ if (combined.includes("password authentication failed"))
14069
+ return "Database authentication failed \u2014 check your connection string";
14070
+ if (combined.includes("does not exist") && combined.includes("relation"))
14071
+ return "Database tables not found \u2014 run npx drizzle-kit push first";
14072
+ if (combined.includes("MODULE_NOT_FOUND") || combined.includes("Cannot find module"))
14073
+ return "Missing dependencies \u2014 run your package manager install first";
14074
+ const firstLine = stderr.split("\n").map((l) => l.trim()).find((l) => l.length > 0 && !l.startsWith("at ") && !l.startsWith("node:"));
14075
+ if (firstLine) return firstLine;
14076
+ return "Unknown error \u2014 run npx betterstart seed for details";
14122
14077
  }
14123
14078
  function runDrizzlePush(cwd) {
14124
14079
  return new Promise((resolve) => {
@@ -14364,41 +14319,12 @@ var removeCommand = new Command4("remove").alias("rm").description("Remove all g
14364
14319
  console.log("");
14365
14320
  });
14366
14321
 
14367
- // src/commands/update-deps.ts
14368
- import path39 from "path";
14369
- import * as clack2 from "@clack/prompts";
14370
- import { Command as Command5 } from "commander";
14371
- var updateDepsCommand = new Command5("update-deps").description("Install or update all CMS dependencies").option("--cwd <path>", "Project root path").action(async (options) => {
14372
- const cwd = options.cwd ? path39.resolve(options.cwd) : process.cwd();
14373
- clack2.intro("BetterStart Update Dependencies");
14374
- const pm = detectPackageManager(cwd);
14375
- clack2.log.info(`Package manager: ${pm}`);
14376
- const config = await resolveConfig(cwd);
14377
- const includeEmail = config.features?.email ?? true;
14378
- const s = clack2.spinner();
14379
- s.start("Installing dependencies...");
14380
- const result = await installDependenciesAsync({
14381
- cwd,
14382
- pm,
14383
- includeEmail,
14384
- includeBiome: false
14385
- });
14386
- if (result.success) {
14387
- s.stop(`Installed ${result.coreDeps.length} deps + ${result.devDeps.length} dev deps`);
14388
- } else {
14389
- s.stop("Dependency install failed");
14390
- clack2.log.error(result.error ?? "Unknown error");
14391
- process.exit(1);
14392
- }
14393
- clack2.outro("Dependencies updated");
14394
- });
14395
-
14396
14322
  // src/commands/uninstall.ts
14397
14323
  import fs35 from "fs";
14398
- import path40 from "path";
14324
+ import path39 from "path";
14399
14325
  import * as p5 from "@clack/prompts";
14326
+ import { Command as Command5 } from "commander";
14400
14327
  import pc3 from "picocolors";
14401
- import { Command as Command6 } from "commander";
14402
14328
 
14403
14329
  // src/commands/uninstall-cleaners.ts
14404
14330
  import fs34 from "fs";
@@ -14455,7 +14381,7 @@ function cleanTsconfig(tsconfigPath) {
14455
14381
  }
14456
14382
  if (removed.length === 0) return [];
14457
14383
  if (Object.keys(paths).length === 0) {
14458
- delete compilerOptions.paths;
14384
+ compilerOptions.paths = void 0;
14459
14385
  } else {
14460
14386
  compilerOptions.paths = paths;
14461
14387
  }
@@ -14509,14 +14435,14 @@ function cleanEnvFile(envPath) {
14509
14435
  }
14510
14436
  if (trimmed.startsWith("#") && !headerPattern.test(trimmed)) {
14511
14437
  const nextNonEmpty = findNextNonEmptyLine(lines, i + 1);
14512
- if (nextNonEmpty !== null && nextNonEmpty.match(/^BETTERSTART_\w+=/)) {
14438
+ if (nextNonEmpty?.match(/^BETTERSTART_\w+=/)) {
14513
14439
  continue;
14514
14440
  }
14515
14441
  }
14516
14442
  kept.push(line);
14517
14443
  }
14518
14444
  if (removed.length === 0) return [];
14519
- let result = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
14445
+ const result = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
14520
14446
  if (result === "") {
14521
14447
  fs34.unlinkSync(envPath);
14522
14448
  } else {
@@ -14546,7 +14472,7 @@ function findMainCss2(cwd) {
14546
14472
  "globals.css"
14547
14473
  ];
14548
14474
  for (const candidate of candidates) {
14549
- const filePath = path40.join(cwd, candidate);
14475
+ const filePath = path39.join(cwd, candidate);
14550
14476
  if (fs35.existsSync(filePath)) return filePath;
14551
14477
  }
14552
14478
  return void 0;
@@ -14562,17 +14488,19 @@ function isCLICreatedBiome(biomePath) {
14562
14488
  }
14563
14489
  function buildUninstallPlan(cwd) {
14564
14490
  const steps = [];
14565
- const hasSrc = fs35.existsSync(path40.join(cwd, "src"));
14491
+ const hasSrc = fs35.existsSync(path39.join(cwd, "src"));
14566
14492
  const appBase = hasSrc ? "src/app" : "app";
14567
14493
  const dirs = [];
14568
- const cmsDir = path40.join(cwd, "cms");
14569
- const cmsRouteGroup = path40.join(cwd, appBase, "(cms)");
14494
+ const cmsDir = path39.join(cwd, "cms");
14495
+ const cmsRouteGroup = path39.join(cwd, appBase, "(cms)");
14570
14496
  if (fs35.existsSync(cmsDir)) dirs.push("cms/");
14571
14497
  if (fs35.existsSync(cmsRouteGroup)) dirs.push(`${appBase}/(cms)/`);
14572
14498
  if (dirs.length > 0) {
14573
14499
  steps.push({
14574
14500
  label: "CMS directories",
14575
14501
  items: dirs,
14502
+ count: dirs.length,
14503
+ unit: dirs.length === 1 ? "directory" : "directories",
14576
14504
  execute() {
14577
14505
  if (fs35.existsSync(cmsDir)) fs35.rmSync(cmsDir, { recursive: true, force: true });
14578
14506
  if (fs35.existsSync(cmsRouteGroup)) fs35.rmSync(cmsRouteGroup, { recursive: true, force: true });
@@ -14582,9 +14510,9 @@ function buildUninstallPlan(cwd) {
14582
14510
  const configFiles = [];
14583
14511
  const configPaths = [];
14584
14512
  const candidates = [
14585
- ["cms.config.ts", path40.join(cwd, "cms.config.ts")],
14586
- ["drizzle.config.ts", path40.join(cwd, "drizzle.config.ts")],
14587
- ["CMS.md", path40.join(cwd, "CMS.md")]
14513
+ ["cms.config.ts", path39.join(cwd, "cms.config.ts")],
14514
+ ["drizzle.config.ts", path39.join(cwd, "drizzle.config.ts")],
14515
+ ["CMS.md", path39.join(cwd, "CMS.md")]
14588
14516
  ];
14589
14517
  for (const [label, fullPath] of candidates) {
14590
14518
  if (fs35.existsSync(fullPath)) {
@@ -14592,7 +14520,7 @@ function buildUninstallPlan(cwd) {
14592
14520
  configPaths.push(fullPath);
14593
14521
  }
14594
14522
  }
14595
- const biomePath = path40.join(cwd, "biome.json");
14523
+ const biomePath = path39.join(cwd, "biome.json");
14596
14524
  if (isCLICreatedBiome(biomePath)) {
14597
14525
  configFiles.push("biome.json (CLI-created)");
14598
14526
  configPaths.push(biomePath);
@@ -14601,6 +14529,8 @@ function buildUninstallPlan(cwd) {
14601
14529
  steps.push({
14602
14530
  label: "Config files",
14603
14531
  items: configFiles,
14532
+ count: configFiles.length,
14533
+ unit: configFiles.length === 1 ? "file" : "files",
14604
14534
  execute() {
14605
14535
  for (const p6 of configPaths) {
14606
14536
  if (fs35.existsSync(p6)) fs35.unlinkSync(p6);
@@ -14608,13 +14538,17 @@ function buildUninstallPlan(cwd) {
14608
14538
  }
14609
14539
  });
14610
14540
  }
14611
- const tsconfigPath = path40.join(cwd, "tsconfig.json");
14541
+ const tsconfigPath = path39.join(cwd, "tsconfig.json");
14612
14542
  if (fs35.existsSync(tsconfigPath)) {
14613
14543
  const content = fs35.readFileSync(tsconfigPath, "utf-8");
14614
- if (content.includes("@cms/")) {
14544
+ const aliasMatches = content.match(/"@cms\//g);
14545
+ if (aliasMatches && aliasMatches.length > 0) {
14546
+ const aliasCount = aliasMatches.length;
14615
14547
  steps.push({
14616
14548
  label: "tsconfig.json path aliases",
14617
- items: ["Remove all @cms/* paths from compilerOptions.paths"],
14549
+ items: [`@cms/* aliases in tsconfig.json`],
14550
+ count: aliasCount,
14551
+ unit: aliasCount === 1 ? "alias" : "aliases",
14618
14552
  execute() {
14619
14553
  cleanTsconfig(tsconfigPath);
14620
14554
  }
@@ -14624,26 +14558,30 @@ function buildUninstallPlan(cwd) {
14624
14558
  const cssFile = findMainCss2(cwd);
14625
14559
  if (cssFile) {
14626
14560
  const cssContent = fs35.readFileSync(cssFile, "utf-8");
14627
- const sourcePattern = /^@source\s+"[^"]*cms[^"]*";\s*$/m;
14628
- if (sourcePattern.test(cssContent)) {
14629
- const relCss = path40.relative(cwd, cssFile);
14561
+ const sourceLines = cssContent.split("\n").filter((l) => /^@source\s+"[^"]*cms[^"]*";\s*$/.test(l));
14562
+ if (sourceLines.length > 0) {
14563
+ const relCss = path39.relative(cwd, cssFile);
14630
14564
  steps.push({
14631
14565
  label: `CSS @source lines (${relCss})`,
14632
- items: ["Remove @source lines referencing cms/"],
14566
+ items: [`@source lines in ${relCss}`],
14567
+ count: sourceLines.length,
14568
+ unit: sourceLines.length === 1 ? "line" : "lines",
14633
14569
  execute() {
14634
14570
  cleanCss(cssFile);
14635
14571
  }
14636
14572
  });
14637
14573
  }
14638
14574
  }
14639
- const envPath = path40.join(cwd, ".env.local");
14575
+ const envPath = path39.join(cwd, ".env.local");
14640
14576
  if (fs35.existsSync(envPath)) {
14641
14577
  const envContent = fs35.readFileSync(envPath, "utf-8");
14642
14578
  const bsVars = envContent.split("\n").filter((l) => l.trim().match(/^BETTERSTART_\w+=/)).map((l) => l.split("=")[0]);
14643
14579
  if (bsVars.length > 0) {
14644
14580
  steps.push({
14645
14581
  label: ".env.local variables",
14646
- items: bsVars,
14582
+ items: ["BETTERSTART_* vars in .env.local"],
14583
+ count: bsVars.length,
14584
+ unit: bsVars.length === 1 ? "variable" : "variables",
14647
14585
  execute() {
14648
14586
  cleanEnvFile(envPath);
14649
14587
  }
@@ -14652,26 +14590,24 @@ function buildUninstallPlan(cwd) {
14652
14590
  }
14653
14591
  return steps;
14654
14592
  }
14655
- var uninstallCommand = new Command6("uninstall").description("Remove all CMS files and undo modifications made by betterstart init").option("-f, --force", "Skip all confirmation prompts", false).option("--cwd <path>", "Project root path").action(async (options) => {
14656
- const cwd = options.cwd ? path40.resolve(options.cwd) : process.cwd();
14593
+ var uninstallCommand = new Command5("uninstall").description("Remove all CMS files and undo modifications made by betterstart init").option("-f, --force", "Skip all confirmation prompts", false).option("--cwd <path>", "Project root path").action(async (options) => {
14594
+ const cwd = options.cwd ? path39.resolve(options.cwd) : process.cwd();
14657
14595
  p5.intro(pc3.bgRed(pc3.white(" BetterStart Uninstall ")));
14658
14596
  const steps = buildUninstallPlan(cwd);
14659
14597
  if (steps.length === 0) {
14660
- p5.log.info("Nothing to remove \u2014 project is already clean.");
14598
+ p5.log.success(`${pc3.green("\u2713")} Nothing to remove \u2014 project is already clean.`);
14661
14599
  p5.outro("Done");
14662
14600
  return;
14663
14601
  }
14664
- p5.log.warn("The following will be removed/modified:\n");
14665
- for (const step of steps) {
14666
- p5.log.message(` ${pc3.bold(step.label)}`);
14667
- for (const item of step.items) {
14668
- p5.log.message(` ${pc3.red("\xD7")} ${pc3.dim(item)}`);
14669
- }
14670
- p5.log.message("");
14671
- }
14602
+ const planLines = steps.map((step) => {
14603
+ const names = step.items.join(" ");
14604
+ const countLabel = pc3.dim(`${step.count} ${step.unit}`);
14605
+ return `${pc3.red("\xD7")} ${names} ${countLabel}`;
14606
+ });
14607
+ p5.note(planLines.join("\n"), "Uninstall plan");
14672
14608
  if (!options.force) {
14673
14609
  const confirmed = await p5.confirm({
14674
- message: `Proceed with uninstall? (${steps.length} ${steps.length === 1 ? "area" : "areas"})`,
14610
+ message: "Proceed with uninstall?",
14675
14611
  initialValue: false
14676
14612
  });
14677
14613
  if (p5.isCancel(confirmed) || !confirmed) {
@@ -14679,29 +14615,45 @@ var uninstallCommand = new Command6("uninstall").description("Remove all CMS fil
14679
14615
  process.exit(0);
14680
14616
  }
14681
14617
  }
14682
- let completedCount = 0;
14618
+ const s = p5.spinner();
14619
+ s.start(steps[0].label);
14683
14620
  for (const step of steps) {
14621
+ s.message(step.label);
14684
14622
  step.execute();
14685
- completedCount++;
14686
- p5.log.success(`Removed: ${step.label}`);
14687
14623
  }
14688
- p5.log.message("");
14689
- if (completedCount === 0) {
14690
- p5.log.info("No changes were made.");
14624
+ const parts = steps.map((step) => `${step.count} ${step.unit}`);
14625
+ s.stop(`Removed ${parts.join(", ")}`);
14626
+ p5.note(pc3.dim("Database tables were NOT dropped \u2014 drop them manually if needed."), "Next steps");
14627
+ p5.outro("Uninstall complete");
14628
+ });
14629
+
14630
+ // src/commands/update-deps.ts
14631
+ import path40 from "path";
14632
+ import * as clack2 from "@clack/prompts";
14633
+ import { Command as Command6 } from "commander";
14634
+ var updateDepsCommand = new Command6("update-deps").description("Install or update all CMS dependencies").option("--cwd <path>", "Project root path").action(async (options) => {
14635
+ const cwd = options.cwd ? path40.resolve(options.cwd) : process.cwd();
14636
+ clack2.intro("BetterStart Update Dependencies");
14637
+ const pm = detectPackageManager(cwd);
14638
+ clack2.log.info(`Package manager: ${pm}`);
14639
+ const config = await resolveConfig(cwd);
14640
+ const includeEmail = config.features?.email ?? true;
14641
+ const s = clack2.spinner();
14642
+ s.start("Installing dependencies...");
14643
+ const result = await installDependenciesAsync({
14644
+ cwd,
14645
+ pm,
14646
+ includeEmail,
14647
+ includeBiome: false
14648
+ });
14649
+ if (result.success) {
14650
+ s.stop(`Installed ${result.coreDeps.length} deps + ${result.devDeps.length} dev deps`);
14691
14651
  } else {
14692
- p5.note(
14693
- pc3.dim("Database tables were NOT dropped \u2014 drop them manually if needed."),
14694
- "Next steps"
14695
- );
14696
- }
14697
- if (findMainCss2(cwd)) {
14698
- p5.log.info(
14699
- pc3.dim(
14700
- "Note: @theme tokens were left in your CSS \u2014 they're harmless and may be shared with your own styles."
14701
- )
14702
- );
14652
+ s.stop("Dependency install failed");
14653
+ clack2.log.error(result.error ?? "Unknown error");
14654
+ process.exit(1);
14703
14655
  }
14704
- p5.outro(completedCount > 0 ? "Uninstall complete" : "Done");
14656
+ clack2.outro("Dependencies updated");
14705
14657
  });
14706
14658
 
14707
14659
  // src/commands/update-styles.ts