@betterstart/cli 0.1.28 → 0.1.30

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 +749 -866
  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
  `;
@@ -5720,9 +5651,14 @@ function generateForm(schema, cwd, pagesDir, options = {}) {
5720
5651
  function collectListFields(fields) {
5721
5652
  for (const f of fields) {
5722
5653
  if (f.type === "list" && f.fields && f.fields.length > 0 && !f.hidden) {
5723
- if (!tabFieldNames.has(f.name)) listFieldsWithNested.push(f);
5654
+ listFieldsWithNested.push(f);
5724
5655
  }
5725
5656
  if (f.type === "group" && f.fields) collectListFields(f.fields);
5657
+ if (f.type === "tabs" && f.tabs) {
5658
+ for (const tab of f.tabs) {
5659
+ if (tab.fields) collectListFields(tab.fields);
5660
+ }
5661
+ }
5726
5662
  }
5727
5663
  }
5728
5664
  collectListFields(allFormFields);
@@ -5868,7 +5804,7 @@ import { ${lucideIcons.join(", ")} } from 'lucide-react'` : ""}
5868
5804
  import { useRouter } from 'next/navigation'
5869
5805
  import {${hasNestedList ? " useFieldArray," : ""} useForm } from 'react-hook-form'
5870
5806
  import { toast } from 'sonner'
5871
- import { z } from 'zod'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
5807
+ import { z } from 'zod/v3'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
5872
5808
  ${relHookImports ? `${relHookImports}
5873
5809
  ` : ""}${uiImports.join("\n")}
5874
5810
  import type {
@@ -6048,9 +5984,14 @@ function generateSingleForm(schema, cwd, pagesDir, options = {}) {
6048
5984
  function collectListFieldsSingle(fields) {
6049
5985
  for (const f of fields) {
6050
5986
  if (f.type === "list" && f.fields && f.fields.length > 0 && !f.hidden) {
6051
- if (!tabFieldNames.has(f.name)) listFieldsWithNested.push(f);
5987
+ listFieldsWithNested.push(f);
6052
5988
  }
6053
5989
  if (f.type === "group" && f.fields) collectListFieldsSingle(f.fields);
5990
+ if (f.type === "tabs" && f.tabs) {
5991
+ for (const tab of f.tabs) {
5992
+ if (tab.fields) collectListFieldsSingle(tab.fields);
5993
+ }
5994
+ }
6054
5995
  }
6055
5996
  }
6056
5997
  collectListFieldsSingle(allFormFields);
@@ -6195,7 +6136,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'${lucideIcons
6195
6136
  import { ${lucideIcons.join(", ")} } from 'lucide-react'` : ""}
6196
6137
  import {${hasNestedList ? " useFieldArray," : ""} useForm } from 'react-hook-form'
6197
6138
  import { toast } from 'sonner'
6198
- import { z } from 'zod'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
6139
+ import { z } from 'zod/v3'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
6199
6140
  ${relHookImports ? `${relHookImports}
6200
6141
  ` : ""}${uiImports.join("\n")}
6201
6142
  import type {
@@ -6486,60 +6427,52 @@ function parseSingleItem2(str) {
6486
6427
  const labelMatch = str.match(/label:\s*['"]([^'"]+)['"]/);
6487
6428
  const hrefMatch = str.match(/href:\s*['"]([^'"]+)['"]/);
6488
6429
  const iconMatch = str.match(/icon:\s*(\w+)/);
6430
+ const groupMatch = str.match(/group:\s*['"]([^'"]+)['"]/);
6489
6431
  if (!labelMatch || !hrefMatch) return null;
6490
6432
  const item = {
6491
6433
  label: labelMatch[1],
6492
6434
  href: hrefMatch[1]
6493
6435
  };
6494
6436
  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
- }
6437
+ if (groupMatch) item.group = groupMatch[1];
6499
6438
  return item;
6500
6439
  }
6501
6440
  function generateNavigationCode2(items, iconImports) {
6502
6441
  const lines = [];
6503
- lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
6504
6442
  lines.push("import type { LucideIcon } from 'lucide-react'");
6443
+ lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
6505
6444
  lines.push("");
6506
6445
  lines.push("export interface CmsNavigationItem {");
6507
6446
  lines.push(" label: string");
6508
6447
  lines.push(" href: string");
6509
6448
  lines.push(" icon?: LucideIcon");
6510
- lines.push(" children?: CmsNavigationItem[]");
6449
+ lines.push(" group?: string");
6511
6450
  lines.push("}");
6512
6451
  lines.push("");
6513
6452
  lines.push("export const cmsNavigation: CmsNavigationItem[] = [");
6514
6453
  for (let i = 0; i < items.length; i++) {
6515
6454
  const item = items[i];
6516
6455
  const isLast = i === items.length - 1;
6517
- appendItem2(lines, item, 2, isLast);
6456
+ appendItem2(lines, item, isLast);
6518
6457
  }
6519
6458
  lines.push("]");
6520
6459
  lines.push("");
6521
6460
  return lines.join("\n");
6522
6461
  }
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
- }
6462
+ function appendItem2(lines, item, isLast) {
6463
+ lines.push(" {");
6464
+ lines.push(` label: '${item.label}',`);
6465
+ const hasMore = item.icon != null || item.group != null;
6466
+ lines.push(` href: '${item.href}'${hasMore ? "," : ""}`);
6467
+ if (item.icon && item.group) {
6468
+ lines.push(` icon: ${item.icon},`);
6469
+ lines.push(` group: '${item.group}'`);
6470
+ } else if (item.icon) {
6471
+ lines.push(` icon: ${item.icon}`);
6472
+ } else if (item.group) {
6473
+ lines.push(` group: '${item.group}'`);
6474
+ }
6475
+ lines.push(` }${isLast ? "" : ","}`);
6543
6476
  }
6544
6477
  function updateNavigation(schema, cwd, cmsDir, options = {}) {
6545
6478
  const navFilePath = path15.join(cwd, cmsDir, "data", "navigation.ts");
@@ -6561,48 +6494,26 @@ function updateNavigation(schema, cwd, cmsDir, options = {}) {
6561
6494
  icon: schema.icon
6562
6495
  };
6563
6496
  if (schema.navGroup) {
6564
- let group2 = items.find((item) => item.label === schema.navGroup?.label);
6565
- if (!group2) {
6566
- group2 = {
6567
- label: schema.navGroup.label,
6568
- href: "#",
6569
- icon: schema.navGroup.icon,
6570
- children: []
6571
- };
6572
- items.push(group2);
6573
- }
6574
- if (!group2.children) {
6575
- group2.children = [];
6576
- }
6577
- const existingChild = group2.children.findIndex((c) => c.href === entityHref);
6578
- if (existingChild >= 0) {
6579
- if (options.force) {
6580
- group2.children[existingChild] = newItem;
6581
- } else {
6582
- return { files: [] };
6583
- }
6497
+ newItem.group = schema.navGroup.label;
6498
+ }
6499
+ const existingIndex = items.findIndex((item) => item.href === entityHref);
6500
+ if (existingIndex >= 0) {
6501
+ if (options.force) {
6502
+ items[existingIndex] = newItem;
6584
6503
  } else {
6585
- group2.children.push(newItem);
6586
- group2.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);
6504
+ return { files: [] };
6590
6505
  }
6591
6506
  } 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
- }
6507
+ items.push(newItem);
6602
6508
  }
6603
6509
  const dashboard = items.find((item) => item.href === "/cms");
6604
6510
  const others = items.filter((item) => item.href !== "/cms");
6605
- others.sort((a, b) => a.label.localeCompare(b.label));
6511
+ others.sort((a, b) => {
6512
+ if (!a.group && b.group) return -1;
6513
+ if (a.group && !b.group) return 1;
6514
+ if (a.group && b.group && a.group !== b.group) return a.group.localeCompare(b.group);
6515
+ return a.label.localeCompare(b.label);
6516
+ });
6606
6517
  items = [...dashboard ? [dashboard] : [], ...others];
6607
6518
  if (schema.icon && !iconImports.includes(schema.icon)) {
6608
6519
  iconImports.push(schema.icon);
@@ -6721,8 +6632,8 @@ function generatePageContent2(schema, cwd, pagesDir, options = {}) {
6721
6632
  const hasCreate = schema.actions?.create ?? false;
6722
6633
  const hasDelete = schema.actions?.delete ?? false;
6723
6634
  const hasFilters = schema.filters && schema.filters.length > 0;
6724
- const lucideIcons = ["Search"];
6725
- if (hasCreate) lucideIcons.push("FilePlus");
6635
+ const lucideIcons = ["ChevronLeft", "Search", "CornerDownLeft"];
6636
+ if (hasCreate) lucideIcons.push("Plus");
6726
6637
  if (hasDelete) lucideIcons.push("Trash2");
6727
6638
  if (hasFilters) lucideIcons.push("Check", "ChevronsUpDown");
6728
6639
  let imports = `'use client'
@@ -6730,7 +6641,8 @@ function generatePageContent2(schema, cwd, pagesDir, options = {}) {
6730
6641
  import { useQueryClient } from '@tanstack/react-query'
6731
6642
  import type { ColumnDef } from '@tanstack/react-table'
6732
6643
  import { ${lucideIcons.join(", ")} } from 'lucide-react'
6733
- ${hasCreate ? "import Link from 'next/link'\n" : ""}import { parseAsString${hasDelete ? ", parseAsArrayOf, parseAsInteger" : ""}, useQueryState } from 'nuqs'
6644
+ ${hasCreate ? "import Link from 'next/link'\n" : ""}import { useRouter } from 'next/navigation'
6645
+ import { parseAsString${hasDelete ? ", parseAsArrayOf, parseAsInteger" : ""}, useQueryState } from 'nuqs'
6734
6646
  import * as React from 'react'
6735
6647
  import { useFormStatus } from 'react-dom'
6736
6648
  ${hasDelete ? "import { toast } from 'sonner'\n" : ""}import { PageHeader } from '@cms/components/shared/page-header'
@@ -6830,7 +6742,7 @@ ${filterLogic}` : ""}`;
6830
6742
  })
6831
6743
  }
6832
6744
  ` : "";
6833
- const filterDropdowns = hasFilters ? schema.filters.map(
6745
+ const _filterDropdowns = hasFilters ? schema.filters.map(
6834
6746
  (f) => ` <Popover open={${f.field}ComboboxOpen} onOpenChange={set${toPascalCase14(f.field)}ComboboxOpen}>
6835
6747
  <PopoverTrigger asChild>
6836
6748
  <Button
@@ -6893,19 +6805,17 @@ ${filterLogic}` : ""}`;
6893
6805
  </PopoverContent>
6894
6806
  </Popover>`
6895
6807
  ).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>`;
6808
+ const searchInput = `<form action={searchAction} className="flex items-center gap-2 relative">
6809
+ <Search className="text-muted-foreground/70 pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" />
6810
+ <Input
6811
+ key={search}
6812
+ name="search"
6813
+ placeholder="Search ${schema.label.toLowerCase()}..."
6814
+ defaultValue={search}
6815
+ className="w-64 pl-9 bg-white rounded-lg"
6816
+ />
6817
+ <CornerDownLeft className="text-muted-foreground/70 pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2" />
6818
+ </form>`;
6909
6819
  const deleteButton = hasDelete ? ` {selectedIds.length > 0 && (
6910
6820
  <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
6911
6821
  <AlertDialogTrigger asChild>
@@ -6941,7 +6851,7 @@ ${filterLogic}` : ""}`;
6941
6851
  )}` : "";
6942
6852
  const createButton = hasCreate ? ` <Button asChild>
6943
6853
  <Link href="/cms/${schema.name}/new">
6944
- <FilePlus className="size-3.5 -ml-0.5" strokeWidth={2} />
6854
+ <Plus className="size-3.5 -ml-0.5" strokeWidth={2} />
6945
6855
  Create ${singularizeLabel2(schema.label)}
6946
6856
  </Link>
6947
6857
  </Button>` : "";
@@ -6956,20 +6866,12 @@ ${searchButton}interface ${Plural}PageContentProps<TValue> {
6956
6866
  export function ${Plural}PageContent<TValue>({
6957
6867
  columns
6958
6868
  }: ${Plural}PageContentProps<TValue>) {
6869
+ const router = useRouter()
6959
6870
  const queryClient = useQueryClient()
6960
6871
  ${searchLogic}${deleteLogic}
6961
6872
  return (
6962
6873
  <>
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
-
6874
+ <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
6875
  <main className="space-y-6 p-6">
6974
6876
  <${Plural}Table ${tableProps} />
6975
6877
  </main>
@@ -7018,14 +6920,12 @@ export default async function ${PageName}Page() {
7018
6920
  const data = await get${Singular}()
7019
6921
 
7020
6922
  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">
6923
+ <>
6924
+ <PageHeader title="${schema.label}" />
6925
+ <main className="container mx-auto max-w-5xl p-6 pb-20">
7026
6926
  <${Singular}Form initialData={data} />
7027
6927
  </main>
7028
- </div>
6928
+ </>
7029
6929
  )
7030
6930
  }
7031
6931
  `;
@@ -8059,8 +7959,8 @@ export const { GET, POST } = toNextJsHandler(auth)
8059
7959
  function uploadRouteTemplate() {
8060
7960
  return `import { PutObjectCommand } from '@aws-sdk/client-s3'
8061
7961
  import { BUCKET_NAME, generateFilePath, getPublicUrl, getR2Client } from '@cms/lib/r2'
8062
- import { validateFiles } from '@cms/utils/validation'
8063
7962
  import type { UploadedFile } from '@cms/types'
7963
+ import { validateFiles } from '@cms/utils/validation'
8064
7964
  import { type NextRequest, NextResponse } from 'next/server'
8065
7965
 
8066
7966
  export async function POST(request: NextRequest) {
@@ -8080,27 +7980,22 @@ export async function POST(request: NextRequest) {
8080
7980
  }
8081
7981
 
8082
7982
  if (files.length === 0) {
8083
- return NextResponse.json(
8084
- { success: false, error: 'No files provided' },
8085
- { status: 400 }
8086
- )
7983
+ return NextResponse.json({ success: false, error: 'No files provided' }, { status: 400 })
8087
7984
  }
8088
7985
 
8089
7986
  const validation = validateFiles(files, {
8090
7987
  maxSizeInBytes,
8091
7988
  allowedTypes,
8092
- maxFiles: 10
7989
+ maxFiles: 10,
8093
7990
  })
8094
7991
 
8095
7992
  if (!validation.valid) {
8096
7993
  return NextResponse.json(
8097
7994
  {
8098
7995
  success: false,
8099
- error: validation.errors
8100
- .map((e) => \`\${e.filename}: \${e.error}\`)
8101
- .join('; ')
7996
+ error: validation.errors.map((e) => \`\${e.filename}: \${e.error}\`).join('; '),
8102
7997
  },
8103
- { status: 400 }
7998
+ { status: 400 },
8104
7999
  )
8105
8000
  }
8106
8001
 
@@ -8116,7 +8011,7 @@ export async function POST(request: NextRequest) {
8116
8011
  Key: key,
8117
8012
  Body: buffer,
8118
8013
  ContentType: file.type,
8119
- ContentLength: file.size
8014
+ ContentLength: file.size,
8120
8015
  })
8121
8016
 
8122
8017
  await getR2Client().send(command)
@@ -8126,18 +8021,14 @@ export async function POST(request: NextRequest) {
8126
8021
  url: getPublicUrl(key),
8127
8022
  filename: file.name,
8128
8023
  size: file.size,
8129
- contentType: file.type
8024
+ contentType: file.type,
8130
8025
  })
8131
8026
  }
8132
8027
 
8133
8028
  return NextResponse.json({ success: true, files: uploadedFiles })
8134
8029
  } catch (error) {
8135
- const message =
8136
- error instanceof Error ? error.message : 'Failed to upload files'
8137
- return NextResponse.json(
8138
- { success: false, error: message },
8139
- { status: 500 }
8140
- )
8030
+ const message = error instanceof Error ? error.message : 'Failed to upload files'
8031
+ return NextResponse.json({ success: false, error: message }, { status: 500 })
8141
8032
  }
8142
8033
  }
8143
8034
  `;
@@ -8217,7 +8108,9 @@ function authClientTemplate() {
8217
8108
  import { createAuthClient } from 'better-auth/react'
8218
8109
 
8219
8110
  export const authClient = createAuthClient({
8220
- baseURL: process.env.NEXT_PUBLIC_BETTERSTART_AUTH_URL || (typeof window !== 'undefined' ? window.location.origin : ''),
8111
+ baseURL:
8112
+ process.env.NEXT_PUBLIC_BETTERSTART_AUTH_URL ||
8113
+ (typeof window !== 'undefined' ? window.location.origin : ''),
8221
8114
  basePath: '/api/cms/auth',
8222
8115
  })
8223
8116
 
@@ -8238,10 +8131,7 @@ export enum UserRole {
8238
8131
  }
8239
8132
 
8240
8133
  export function isUserRole(value: unknown): value is UserRole {
8241
- return (
8242
- typeof value === 'string' &&
8243
- Object.values(UserRole).includes(value as UserRole)
8244
- )
8134
+ return typeof value === 'string' && Object.values(UserRole).includes(value as UserRole)
8245
8135
  }
8246
8136
 
8247
8137
  /**
@@ -8696,184 +8586,121 @@ function hasEnvBetterstartVars(cwd) {
8696
8586
  // src/init/templates/components/cms-globals.ts
8697
8587
  function cmsGlobalsCssTemplate() {
8698
8588
  return `@import "tailwindcss";
8589
+ @import "tw-animate-css";
8590
+ @import "shadcn/tailwind.css";
8699
8591
 
8700
8592
  @custom-variant dark (&:is(.dark *));
8701
8593
 
8702
- :root {
8703
- --background: oklch(0.985 0 0);
8704
- --foreground: oklch(0 0 0);
8705
- --card: oklch(1 0 0);
8706
- --card-foreground: oklch(0 0 0);
8707
- --popover: oklch(0.99 0 0);
8708
- --popover-foreground: oklch(0 0 0);
8709
- --primary: oklch(0 0 0);
8710
- --primary-foreground: oklch(1 0 0);
8711
- --secondary: oklch(0.97 0 0);
8712
- --secondary-foreground: oklch(0 0 0);
8713
- --muted: oklch(0.97 0 0);
8714
- --muted-foreground: oklch(0.44 0 0);
8715
- --accent: oklch(0.97 0 0);
8716
- --accent-foreground: oklch(0 0 0);
8717
- --destructive: oklch(0.63 0.19 23.03);
8718
- --destructive-foreground: oklch(1 0 0);
8719
- --border: oklch(0.92 0 0);
8720
- --input: oklch(0.99 0 0);
8721
- --ring: oklch(0 0 0);
8722
- --chart-1: oklch(0.81 0.17 75.35);
8723
- --chart-2: oklch(0.55 0.22 264.53);
8724
- --chart-3: oklch(0.72 0 0);
8725
- --chart-4: oklch(0.92 0 0);
8726
- --chart-5: oklch(0.56 0 0);
8727
- --sidebar: oklch(1 0 0);
8728
- --sidebar-foreground: oklch(0 0 0);
8729
- --sidebar-primary: oklch(0 0 0);
8730
- --sidebar-primary-foreground: oklch(1 0 0);
8731
- --sidebar-accent: oklch(0.94 0 0);
8732
- --sidebar-accent-foreground: oklch(0 0 0);
8733
- --sidebar-border: oklch(0.94 0 0);
8734
- --sidebar-ring: oklch(0 0 0);
8735
- --font-sans: Geist, sans-serif;
8736
- --font-serif: Georgia, serif;
8737
- --font-mono: Geist Mono, monospace;
8738
- --radius: 0.4rem;
8739
- --shadow-x: 0px;
8740
- --shadow-y: 1px;
8741
- --shadow-blur: 3px;
8742
- --shadow-spread: 0px;
8743
- --shadow-opacity: 0.02;
8744
- --shadow-color: hsl(0 0% 0%);
8745
- --shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
8746
- --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
8747
- --shadow-sm:
8748
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
8749
- --shadow:
8750
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
8751
- --shadow-md:
8752
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 2px 4px -1px hsl(0 0% 0% / 0.02);
8753
- --shadow-lg:
8754
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 4px 6px -1px hsl(0 0% 0% / 0.02);
8755
- --shadow-xl:
8756
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 8px 10px -1px hsl(0 0% 0% / 0.02);
8757
- --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
8758
- --tracking-normal: 0em;
8759
- --spacing: 0.25rem;
8760
- }
8761
-
8762
- .dark {
8763
- --background: oklch(0 0 0);
8764
- --foreground: oklch(1 0 0);
8765
- --card: oklch(0.14 0 0);
8766
- --card-foreground: oklch(1 0 0);
8767
- --popover: oklch(0.18 0 0);
8768
- --popover-foreground: oklch(1 0 0);
8769
- --primary: oklch(1 0 0);
8770
- --primary-foreground: oklch(0 0 0);
8771
- --secondary: oklch(0.25 0 0);
8772
- --secondary-foreground: oklch(1 0 0);
8773
- --muted: oklch(0.23 0 0);
8774
- --muted-foreground: oklch(0.72 0 0);
8775
- --accent: oklch(0.32 0 0);
8776
- --accent-foreground: oklch(1 0 0);
8777
- --destructive: oklch(0.69 0.2 23.91);
8778
- --destructive-foreground: oklch(0 0 0);
8779
- --border: oklch(0.26 0 0);
8780
- --input: oklch(0.32 0 0);
8781
- --ring: oklch(0.72 0 0);
8782
- --chart-1: oklch(0.81 0.17 75.35);
8783
- --chart-2: oklch(0.58 0.21 260.84);
8784
- --chart-3: oklch(0.56 0 0);
8785
- --chart-4: oklch(0.44 0 0);
8786
- --chart-5: oklch(0.92 0 0);
8787
- --sidebar: oklch(0.18 0 0);
8788
- --sidebar-foreground: oklch(1 0 0);
8789
- --sidebar-primary: oklch(1 0 0);
8790
- --sidebar-primary-foreground: oklch(0 0 0);
8791
- --sidebar-accent: oklch(0.32 0 0);
8792
- --sidebar-accent-foreground: oklch(1 0 0);
8793
- --sidebar-border: oklch(0.32 0 0);
8794
- --sidebar-ring: oklch(0.72 0 0);
8795
- --font-sans: Geist, sans-serif;
8796
- --font-serif: Georgia, serif;
8797
- --font-mono: Geist Mono, monospace;
8798
- --radius: 0.4rem;
8799
- --shadow-x: 0px;
8800
- --shadow-y: 1px;
8801
- --shadow-blur: 3px;
8802
- --shadow-spread: 0px;
8803
- --shadow-opacity: 0.02;
8804
- --shadow-color: hsl(0 0% 0%);
8805
- --shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
8806
- --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.01);
8807
- --shadow-sm:
8808
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
8809
- --shadow:
8810
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 1px 2px -1px hsl(0 0% 0% / 0.02);
8811
- --shadow-md:
8812
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 2px 4px -1px hsl(0 0% 0% / 0.02);
8813
- --shadow-lg:
8814
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 4px 6px -1px hsl(0 0% 0% / 0.02);
8815
- --shadow-xl:
8816
- 0px 1px 3px 0px hsl(0 0% 0% / 0.02), 0px 8px 10px -1px hsl(0 0% 0% / 0.02);
8817
- --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.05);
8818
- }
8819
-
8820
- .cms-root {
8821
- --font-sans: var(--font-geist-sans, sans-serif);
8822
- --font-mono: var(--font-geist-mono, monospace);
8823
- font-family: var(--font-sans);
8824
- }
8825
-
8826
8594
  @theme inline {
8827
8595
  --color-background: var(--background);
8828
8596
  --color-foreground: var(--foreground);
8829
- --color-card: var(--card);
8830
- --color-card-foreground: var(--card-foreground);
8831
- --color-popover: var(--popover);
8832
- --color-popover-foreground: var(--popover-foreground);
8833
- --color-primary: var(--primary);
8834
- --color-primary-foreground: var(--primary-foreground);
8835
- --color-secondary: var(--secondary);
8836
- --color-secondary-foreground: var(--secondary-foreground);
8837
- --color-muted: var(--muted);
8838
- --color-muted-foreground: var(--muted-foreground);
8839
- --color-accent: var(--accent);
8840
- --color-accent-foreground: var(--accent-foreground);
8841
- --color-destructive: var(--destructive);
8842
- --color-destructive-foreground: var(--destructive-foreground);
8843
- --color-border: var(--border);
8844
- --color-input: var(--input);
8845
- --color-ring: var(--ring);
8846
- --color-chart-1: var(--chart-1);
8847
- --color-chart-2: var(--chart-2);
8848
- --color-chart-3: var(--chart-3);
8849
- --color-chart-4: var(--chart-4);
8850
- --color-chart-5: var(--chart-5);
8851
- --color-sidebar: var(--sidebar);
8852
- --color-sidebar-foreground: var(--sidebar-foreground);
8853
- --color-sidebar-primary: var(--sidebar-primary);
8854
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
8855
- --color-sidebar-accent: var(--sidebar-accent);
8856
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
8857
- --color-sidebar-border: var(--sidebar-border);
8858
- --color-sidebar-ring: var(--sidebar-ring);
8859
-
8860
8597
  --font-sans: var(--font-sans);
8861
- --font-mono: var(--font-mono);
8862
- --font-serif: var(--font-serif);
8863
-
8598
+ --font-mono: var(--font-geist-mono);
8599
+ --color-sidebar-ring: var(--sidebar-ring);
8600
+ --color-sidebar-border: var(--sidebar-border);
8601
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
8602
+ --color-sidebar-accent: var(--sidebar-accent);
8603
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
8604
+ --color-sidebar-primary: var(--sidebar-primary);
8605
+ --color-sidebar-foreground: var(--sidebar-foreground);
8606
+ --color-sidebar: var(--sidebar);
8607
+ --color-chart-5: var(--chart-5);
8608
+ --color-chart-4: var(--chart-4);
8609
+ --color-chart-3: var(--chart-3);
8610
+ --color-chart-2: var(--chart-2);
8611
+ --color-chart-1: var(--chart-1);
8612
+ --color-ring: var(--ring);
8613
+ --color-input: var(--input);
8614
+ --color-border: var(--border);
8615
+ --color-destructive: var(--destructive);
8616
+ --color-accent-foreground: var(--accent-foreground);
8617
+ --color-accent: var(--accent);
8618
+ --color-muted-foreground: var(--muted-foreground);
8619
+ --color-muted: var(--muted);
8620
+ --color-secondary-foreground: var(--secondary-foreground);
8621
+ --color-secondary: var(--secondary);
8622
+ --color-primary-foreground: var(--primary-foreground);
8623
+ --color-primary: var(--primary);
8624
+ --color-popover-foreground: var(--popover-foreground);
8625
+ --color-popover: var(--popover);
8626
+ --color-card-foreground: var(--card-foreground);
8627
+ --color-card: var(--card);
8864
8628
  --radius-sm: calc(var(--radius) - 4px);
8865
8629
  --radius-md: calc(var(--radius) - 2px);
8866
8630
  --radius-lg: var(--radius);
8867
8631
  --radius-xl: calc(var(--radius) + 4px);
8632
+ --radius-2xl: calc(var(--radius) + 8px);
8633
+ --radius-3xl: calc(var(--radius) + 12px);
8634
+ --radius-4xl: calc(var(--radius) + 16px);
8635
+ }
8636
+
8637
+ :root {
8638
+ --background: oklch(1 0 0);
8639
+ --foreground: oklch(0.141 0.005 285.823);
8640
+ --card: oklch(1 0 0);
8641
+ --card-foreground: oklch(0.141 0.005 285.823);
8642
+ --popover: oklch(1 0 0);
8643
+ --popover-foreground: oklch(0.141 0.005 285.823);
8644
+ --primary: oklch(0.21 0.006 285.885);
8645
+ --primary-foreground: oklch(0.985 0 0);
8646
+ --secondary: oklch(0.967 0.001 286.375);
8647
+ --secondary-foreground: oklch(0.21 0.006 285.885);
8648
+ --muted: oklch(0.967 0.001 286.375);
8649
+ --muted-foreground: oklch(0.552 0.016 285.938);
8650
+ --accent: oklch(0.967 0.001 286.375);
8651
+ --accent-foreground: oklch(0.21 0.006 285.885);
8652
+ --destructive: oklch(0.577 0.245 27.325);
8653
+ --border: oklch(0.92 0.004 286.32);
8654
+ --input: oklch(0.92 0.004 286.32);
8655
+ --ring: oklch(0.705 0.015 286.067);
8656
+ --chart-1: oklch(0.646 0.222 41.116);
8657
+ --chart-2: oklch(0.6 0.118 184.704);
8658
+ --chart-3: oklch(0.398 0.07 227.392);
8659
+ --chart-4: oklch(0.828 0.189 84.429);
8660
+ --chart-5: oklch(0.769 0.188 70.08);
8661
+ --radius: 0.625rem;
8662
+ --sidebar: oklch(0.985 0 0);
8663
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
8664
+ --sidebar-primary: oklch(0.21 0.006 285.885);
8665
+ --sidebar-primary-foreground: oklch(0.985 0 0);
8666
+ --sidebar-accent: oklch(0.967 0.001 286.375);
8667
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
8668
+ --sidebar-border: oklch(0.92 0.004 286.32);
8669
+ --sidebar-ring: oklch(0.705 0.015 286.067);
8670
+ }
8868
8671
 
8869
- --shadow-2xs: var(--shadow-2xs);
8870
- --shadow-xs: var(--shadow-xs);
8871
- --shadow-sm: var(--shadow-sm);
8872
- --shadow: var(--shadow);
8873
- --shadow-md: var(--shadow-md);
8874
- --shadow-lg: var(--shadow-lg);
8875
- --shadow-xl: var(--shadow-xl);
8876
- --shadow-2xl: var(--shadow-2xl);
8672
+ .dark {
8673
+ --background: oklch(0.141 0.005 285.823);
8674
+ --foreground: oklch(0.985 0 0);
8675
+ --card: oklch(0.21 0.006 285.885);
8676
+ --card-foreground: oklch(0.985 0 0);
8677
+ --popover: oklch(0.21 0.006 285.885);
8678
+ --popover-foreground: oklch(0.985 0 0);
8679
+ --primary: oklch(0.92 0.004 286.32);
8680
+ --primary-foreground: oklch(0.21 0.006 285.885);
8681
+ --secondary: oklch(0.274 0.006 286.033);
8682
+ --secondary-foreground: oklch(0.985 0 0);
8683
+ --muted: oklch(0.274 0.006 286.033);
8684
+ --muted-foreground: oklch(0.705 0.015 286.067);
8685
+ --accent: oklch(0.274 0.006 286.033);
8686
+ --accent-foreground: oklch(0.985 0 0);
8687
+ --destructive: oklch(0.704 0.191 22.216);
8688
+ --border: oklch(1 0 0 / 10%);
8689
+ --input: oklch(1 0 0 / 15%);
8690
+ --ring: oklch(0.552 0.016 285.938);
8691
+ --chart-1: oklch(0.488 0.243 264.376);
8692
+ --chart-2: oklch(0.696 0.17 162.48);
8693
+ --chart-3: oklch(0.769 0.188 70.08);
8694
+ --chart-4: oklch(0.627 0.265 303.9);
8695
+ --chart-5: oklch(0.645 0.246 16.439);
8696
+ --sidebar: oklch(0.21 0.006 285.885);
8697
+ --sidebar-foreground: oklch(0.985 0 0);
8698
+ --sidebar-primary: oklch(0.488 0.243 264.376);
8699
+ --sidebar-primary-foreground: oklch(0.985 0 0);
8700
+ --sidebar-accent: oklch(0.274 0.006 286.033);
8701
+ --sidebar-accent-foreground: oklch(0.985 0 0);
8702
+ --sidebar-border: oklch(1 0 0 / 10%);
8703
+ --sidebar-ring: oklch(0.552 0.016 285.938);
8877
8704
  }
8878
8705
 
8879
8706
  @layer base {
@@ -8881,7 +8708,7 @@ function cmsGlobalsCssTemplate() {
8881
8708
  @apply border-border outline-ring/50;
8882
8709
  }
8883
8710
  body {
8884
- @apply bg-background text-foreground antialiased;
8711
+ @apply bg-background text-foreground;
8885
8712
  }
8886
8713
  }
8887
8714
 
@@ -8914,6 +8741,14 @@ function cmsGlobalsCssTemplate() {
8914
8741
  function dataTableTemplate() {
8915
8742
  return `'use client'
8916
8743
 
8744
+ import {
8745
+ Table,
8746
+ TableBody,
8747
+ TableCell,
8748
+ TableHead,
8749
+ TableHeader,
8750
+ TableRow,
8751
+ } from '@cms/components/ui/table'
8917
8752
  import {
8918
8753
  type ColumnDef,
8919
8754
  type ColumnFiltersState,
@@ -8923,20 +8758,11 @@ import {
8923
8758
  getPaginationRowModel,
8924
8759
  getSortedRowModel,
8925
8760
  type SortingState,
8926
- type Table as TanstackTable,
8927
8761
  useReactTable,
8928
- type VisibilityState
8762
+ type VisibilityState,
8929
8763
  } from '@tanstack/react-table'
8930
8764
  import { parseAsInteger, useQueryState } from 'nuqs'
8931
8765
  import * as React from 'react'
8932
- import {
8933
- Table,
8934
- TableBody,
8935
- TableCell,
8936
- TableHead,
8937
- TableHeader,
8938
- TableRow
8939
- } from '@cms/components/ui/table'
8940
8766
  import { DataTablePagination } from './data-table-pagination'
8941
8767
 
8942
8768
  interface DataTableProps<TData, TValue> {
@@ -8962,7 +8788,7 @@ export function DataTable<TData, TValue>({
8962
8788
  selectedIds,
8963
8789
  onSelectedIdsChange,
8964
8790
  meta,
8965
- getId = (row) => (row as { id: number }).id
8791
+ getId = (row) => (row as { id: number }).id,
8966
8792
  }: DataTableProps<TData, TValue>) {
8967
8793
  const [sorting, setSorting] = React.useState<SortingState>([])
8968
8794
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
@@ -8985,27 +8811,38 @@ export function DataTable<TData, TValue>({
8985
8811
  }, [selectedIds, data, getId])
8986
8812
 
8987
8813
  const handleRowSelectionChange = React.useCallback(
8988
- (updater: Record<string, boolean> | ((old: Record<string, boolean>) => Record<string, boolean>)) => {
8814
+ (
8815
+ updater:
8816
+ | Record<string, boolean>
8817
+ | ((old: Record<string, boolean>) => Record<string, boolean>),
8818
+ ) => {
8989
8819
  if (!onSelectedIdsChange) return
8990
8820
  const newSelection = typeof updater === 'function' ? updater(rowSelection) : updater
8991
8821
  const newIds = Object.keys(newSelection)
8992
8822
  .filter((key) => newSelection[key])
8993
- .map((key) => getId(data[Number.parseInt(key)]))
8823
+ .map((key) => getId(data[Number.parseInt(key, 10)]))
8994
8824
  .filter(Boolean)
8995
8825
  onSelectedIdsChange(newIds)
8996
8826
  },
8997
- [data, rowSelection, onSelectedIdsChange, getId]
8827
+ [data, rowSelection, onSelectedIdsChange, getId],
8998
8828
  )
8999
8829
 
9000
8830
  const handlePaginationChange = React.useCallback(
9001
- (updater: { pageIndex: number; pageSize: number } | ((old: { pageIndex: number; pageSize: number }) => { pageIndex: number; pageSize: number })) => {
8831
+ (
8832
+ updater:
8833
+ | { pageIndex: number; pageSize: number }
8834
+ | ((old: { pageIndex: number; pageSize: number }) => {
8835
+ pageIndex: number
8836
+ pageSize: number
8837
+ }),
8838
+ ) => {
9002
8839
  const current = { pageIndex, pageSize: effectivePageSize }
9003
8840
  const next = typeof updater === 'function' ? updater(current) : updater
9004
8841
  React.startTransition(() => {
9005
8842
  setPageIndex(next.pageIndex)
9006
8843
  })
9007
8844
  },
9008
- [pageIndex, effectivePageSize, setPageIndex]
8845
+ [pageIndex, effectivePageSize, setPageIndex],
9009
8846
  )
9010
8847
 
9011
8848
  const table = useReactTable({
@@ -9028,9 +8865,9 @@ export function DataTable<TData, TValue>({
9028
8865
  rowSelection,
9029
8866
  pagination: {
9030
8867
  pageIndex,
9031
- pageSize: effectivePageSize
9032
- }
9033
- }
8868
+ pageSize: effectivePageSize,
8869
+ },
8870
+ },
9034
8871
  })
9035
8872
 
9036
8873
  return (
@@ -9097,23 +8934,23 @@ export type { DataTableProps }
9097
8934
  function dataTablePaginationTemplate() {
9098
8935
  return `'use client'
9099
8936
 
9100
- import type { Table } from '@tanstack/react-table'
9101
- import * as React from 'react'
9102
8937
  import { Button } from '@cms/components/ui/button'
9103
8938
  import {
9104
8939
  Select,
9105
8940
  SelectContent,
9106
8941
  SelectItem,
9107
8942
  SelectTrigger,
9108
- SelectValue
8943
+ SelectValue,
9109
8944
  } from '@cms/components/ui/select'
8945
+ import type { Table } from '@tanstack/react-table'
8946
+ import * as React from 'react'
9110
8947
 
9111
8948
  const PAGE_SIZE_OPTIONS = [
9112
8949
  { value: '10', label: '10' },
9113
8950
  { value: '20', label: '20' },
9114
8951
  { value: '50', label: '50' },
9115
8952
  { value: '100', label: '100' },
9116
- { value: 'all', label: 'All' }
8953
+ { value: 'all', label: 'All' },
9117
8954
  ]
9118
8955
 
9119
8956
  interface DataTablePaginationProps<TData> {
@@ -9125,7 +8962,7 @@ interface DataTablePaginationProps<TData> {
9125
8962
  export function DataTablePagination<TData>({
9126
8963
  table,
9127
8964
  pageSize,
9128
- setPageSize
8965
+ setPageSize,
9129
8966
  }: DataTablePaginationProps<TData>) {
9130
8967
  const handlePageSizeChange = React.useCallback(
9131
8968
  (value: string) => {
@@ -9138,7 +8975,7 @@ export function DataTablePagination<TData>({
9138
8975
  table.setPageIndex(0)
9139
8976
  })
9140
8977
  },
9141
- [setPageSize, table]
8978
+ [setPageSize, table],
9142
8979
  )
9143
8980
 
9144
8981
  return (
@@ -9192,11 +9029,11 @@ export function DataTablePagination<TData>({
9192
9029
  function dataTableToolbarTemplate() {
9193
9030
  return `'use client'
9194
9031
 
9195
- import { ArrowUpDown, Save, Search } from 'lucide-react'
9196
- import * as React from 'react'
9197
- import { useFormStatus } from 'react-dom'
9198
9032
  import { Button } from '@cms/components/ui/button'
9199
9033
  import { Input } from '@cms/components/ui/input'
9034
+ import { ArrowUpDown, Save, Search } from 'lucide-react'
9035
+ import type * as React from 'react'
9036
+ import { useFormStatus } from 'react-dom'
9200
9037
 
9201
9038
  function SearchButton() {
9202
9039
  const { pending } = useFormStatus()
@@ -9218,7 +9055,7 @@ export function DataTableToolbar({
9218
9055
  search,
9219
9056
  onSearch,
9220
9057
  searchPlaceholder = 'Search...',
9221
- children
9058
+ children,
9222
9059
  }: DataTableToolbarProps) {
9223
9060
  return (
9224
9061
  <div className="flex items-center gap-2">
@@ -9255,7 +9092,7 @@ export function ReorderControls({
9255
9092
  onSave,
9256
9093
  onCancel,
9257
9094
  hasChanges,
9258
- isSaving
9095
+ isSaving,
9259
9096
  }: ReorderControlsProps) {
9260
9097
  return (
9261
9098
  <div className="flex items-center gap-2">
@@ -9270,26 +9107,14 @@ export function ReorderControls({
9270
9107
  </Button>
9271
9108
  {reorderMode && (
9272
9109
  <>
9273
- <Button
9274
- variant="default"
9275
- size="sm"
9276
- onClick={onSave}
9277
- disabled={!hasChanges || isSaving}
9278
- >
9110
+ <Button variant="default" size="sm" onClick={onSave} disabled={!hasChanges || isSaving}>
9279
9111
  <Save className="size-4 mr-1" />
9280
9112
  {isSaving ? 'Saving...' : 'Save'}
9281
9113
  </Button>
9282
- <Button
9283
- variant="outline"
9284
- size="sm"
9285
- onClick={onCancel}
9286
- disabled={isSaving}
9287
- >
9114
+ <Button variant="outline" size="sm" onClick={onCancel} disabled={isSaving}>
9288
9115
  Cancel
9289
9116
  </Button>
9290
- {hasChanges && (
9291
- <span className="text-sm text-muted-foreground">Unsaved changes</span>
9292
- )}
9117
+ {hasChanges && <span className="text-sm text-muted-foreground">Unsaved changes</span>}
9293
9118
  </>
9294
9119
  )}
9295
9120
  </div>
@@ -9302,7 +9127,6 @@ export function ReorderControls({
9302
9127
  function cmsHeaderTemplate() {
9303
9128
  return `'use client'
9304
9129
 
9305
- import { CmsSearch } from '@cms/components/layout/cms-search'
9306
9130
  import { Button } from '@cms/components/ui/button'
9307
9131
  import { SidebarTrigger, useSidebar } from '@cms/components/ui/sidebar'
9308
9132
  import { useTheme } from '@cms/hooks/use-cms-theme'
@@ -9313,11 +9137,10 @@ export function CmsHeader() {
9313
9137
  const { state } = useSidebar()
9314
9138
 
9315
9139
  return (
9316
- <header className="flex h-14 shrink-0 items-center gap-2 border-b border-border w-full sticky top-0 z-50 bg-sidebar">
9140
+ <header className="flex h-14 shrink-0 items-center gap-2 border-b border-border w-full sticky top-0 z-50">
9317
9141
  <div className="flex items-center px-5 gap-1 flex-1 w-full justify-between">
9318
9142
  <div className="flex items-center gap-2 w-full">
9319
9143
  {state === 'collapsed' && <SidebarTrigger />}
9320
- <CmsSearch />
9321
9144
  </div>
9322
9145
  <div className="flex items-center gap-2 ml-auto">
9323
9146
  <Button
@@ -9337,15 +9160,45 @@ export function CmsHeader() {
9337
9160
  `;
9338
9161
  }
9339
9162
 
9340
- // src/init/templates/components/layout/cms-providers.ts
9341
- function cmsProvidersTemplate() {
9163
+ // src/init/templates/components/layout/cms-nav-link.ts
9164
+ function cmsNavLinkTemplate() {
9165
+ return `'use client'
9166
+
9167
+ import { SidebarMenuButton } from '@cms/components/ui/sidebar'
9168
+ import Link from 'next/link'
9169
+ import { usePathname } from 'next/navigation'
9170
+
9171
+ export function CmsNavLink({
9172
+ href,
9173
+ children,
9174
+ }: {
9175
+ href: string
9176
+ children: React.ReactNode
9177
+ }) {
9178
+ const pathname = usePathname()
9179
+ const isActive =
9180
+ href === '/cms'
9181
+ ? pathname === '/cms'
9182
+ : pathname === href || pathname.startsWith(href + '/')
9183
+
9184
+ return (
9185
+ <SidebarMenuButton asChild isActive={isActive}>
9186
+ <Link href={href}>{children}</Link>
9187
+ </SidebarMenuButton>
9188
+ )
9189
+ }
9190
+ `;
9191
+ }
9192
+
9193
+ // src/init/templates/components/layout/cms-providers.ts
9194
+ function cmsProvidersTemplate() {
9342
9195
  return `'use client'
9343
9196
 
9344
- import { useState } from 'react'
9345
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
9346
- import { CmsThemeProvider } from '@cms/hooks/use-cms-theme'
9347
9197
  import { Toaster } from '@cms/components/ui/sonner'
9198
+ import { CmsThemeProvider } from '@cms/hooks/use-cms-theme'
9199
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
9348
9200
  import { NuqsAdapter } from 'nuqs/adapters/next/app'
9201
+ import { useState } from 'react'
9349
9202
 
9350
9203
  export function CmsProviders({ children }: { children: React.ReactNode }) {
9351
9204
  const [queryClient] = useState(
@@ -9385,15 +9238,15 @@ export const CmsSearch = () => {
9385
9238
  <div className="flex items-center gap-2 relative w-full max-w-[240px]">
9386
9239
  <Button
9387
9240
  variant="outline"
9388
- className="w-full text-left items-center pr-1! rounded-full"
9389
- size="sm"
9241
+ className="w-full text-left items-center pr-1.5! py-0 rounded-lg bg-white"
9242
+ size="lg"
9390
9243
  >
9391
- <Search className="shrink-0 size-3.5 -ml-0.5 text-muted-foreground" strokeWidth={2} />
9392
- <span className="w-full font-medium text-xs pt-px text-muted-foreground leading-0">
9393
- Find...
9244
+ <Search className="shrink-0 size-3.5 -ml-0.5 text-muted-foreground/70" />
9245
+ <span className="w-full font-normal text-sm text-muted-foreground/70 [text-box-trim:trim-both]">
9246
+ Quick search...
9394
9247
  </span>
9395
9248
  <div className="flex items-center gap-1 py-0.5 border rounded-full corner-squircle px-2 border-border bg-background">
9396
- <Command className="size-3! text-muted-foreground" strokeWidth={1.5} />
9249
+ <Command className="size-3! text-muted-foreground" />
9397
9250
  <span className="font-mono text-xs font-medium">K</span>
9398
9251
  </div>
9399
9252
  </Button>
@@ -9407,123 +9260,110 @@ CmsSearch.displayName = 'CmsSearch'
9407
9260
 
9408
9261
  // src/init/templates/components/layout/cms-sidebar.ts
9409
9262
  function cmsSidebarTemplate() {
9410
- return `import { getSetting } from '@/cms/lib/actions/settings'
9411
- import { getSession } from '@cms/auth/middleware'
9263
+ return `import { getSession } from '@cms/auth/middleware'
9412
9264
  import { Avatar, AvatarFallback, AvatarImage } from '@cms/components/ui/avatar'
9413
- import {
9414
- Collapsible,
9415
- CollapsibleContent,
9416
- CollapsibleTrigger,
9417
- } from '@cms/components/ui/collapsible'
9418
9265
  import {
9419
9266
  Sidebar,
9420
9267
  SidebarContent,
9421
9268
  SidebarFooter,
9269
+ SidebarGroup,
9270
+ SidebarGroupLabel,
9422
9271
  SidebarHeader,
9423
9272
  SidebarMenu,
9424
- SidebarMenuButton,
9425
9273
  SidebarMenuItem,
9426
- SidebarMenuSub,
9427
- SidebarMenuSubButton,
9428
- SidebarMenuSubItem,
9429
- SidebarRail,
9430
- SidebarTrigger,
9431
9274
  } from '@cms/components/ui/sidebar'
9432
9275
  import { cms } from '@cms/data/cms'
9433
9276
  import { type CmsNavigationItem, cmsNavigation } from '@cms/data/navigation'
9434
- import { ChevronRight, Settings, Users } from 'lucide-react'
9277
+ import { Settings, Users } from 'lucide-react'
9435
9278
  import Link from 'next/link'
9279
+ import { Fragment } from 'react'
9280
+ import { getSetting } from '@/cms/lib/actions/settings'
9281
+ import { CmsNavLink } from './cms-nav-link'
9282
+ import { CmsSearch } from './cms-search'
9436
9283
 
9437
- function NavItem({ item }: { item: CmsNavigationItem }) {
9438
- if (item.children && item.children.length > 0) {
9439
- return (
9440
- <Collapsible asChild defaultOpen className="group/collapsible border-y border-border py-2 px-2">
9441
- <SidebarMenuItem>
9442
- <CollapsibleTrigger asChild>
9443
- <SidebarMenuButton>
9444
- {item.icon && <item.icon className="size-3.5!" />}
9445
- <span>{item.label}</span>
9446
- <ChevronRight className="ml-auto size-3.5! transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
9447
- </SidebarMenuButton>
9448
- </CollapsibleTrigger>
9449
- <CollapsibleContent>
9450
- <SidebarMenuSub>
9451
- {item.children.map((child) => (
9452
- <SidebarMenuSubItem key={child.href}>
9453
- <SidebarMenuSubButton asChild>
9454
- <Link href={child.href}>
9455
- {child.icon && <child.icon className="size-3.5!" />}
9456
- <span>{child.label}</span>
9457
- </Link>
9458
- </SidebarMenuSubButton>
9459
- </SidebarMenuSubItem>
9460
- ))}
9461
- </SidebarMenuSub>
9462
- </CollapsibleContent>
9463
- </SidebarMenuItem>
9464
- </Collapsible>
9465
- )
9284
+ function groupNavItems(items: CmsNavigationItem[]) {
9285
+ const groups: { label: string | null; items: CmsNavigationItem[] }[] = []
9286
+ const groupMap = new Map<string | null, CmsNavigationItem[]>()
9287
+
9288
+ for (const item of items) {
9289
+ const key = item.group ?? null
9290
+ if (!groupMap.has(key)) {
9291
+ const arr: CmsNavigationItem[] = []
9292
+ groupMap.set(key, arr)
9293
+ groups.push({ label: key, items: arr })
9294
+ }
9295
+ groupMap.get(key)!.push(item)
9466
9296
  }
9467
9297
 
9468
- return (
9469
- <SidebarMenuItem className="px-2">
9470
- <SidebarMenuButton asChild>
9471
- <Link href={item.href}>
9472
- {item.icon && <item.icon className="size-3.5!" />}
9473
- <span>{item.label}</span>
9474
- </Link>
9475
- </SidebarMenuButton>
9476
- </SidebarMenuItem>
9477
- )
9298
+ return groups
9478
9299
  }
9479
9300
 
9480
9301
  export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
9481
9302
  const session = await getSession()
9482
9303
  const settings = await getSetting()
9483
9304
  const user = session?.user ?? null
9305
+ const groups = groupNavItems(cmsNavigation)
9484
9306
 
9485
9307
  return (
9486
9308
  <Sidebar collapsible="icon" {...props}>
9487
9309
  <SidebarHeader className="border-b border-border h-14 items-center flex w-full">
9488
9310
  <div className="flex items-center gap-2 w-full relative h-full">
9489
9311
  <Link href="/cms" className="flex items-center gap-2 w-full">
9490
- <Avatar className="size-6.5">
9312
+ <Avatar className="size-8">
9491
9313
  <AvatarImage src={'/favicon.ico'} />
9492
- <AvatarFallback className="text-sm font-semibold">
9314
+ <AvatarFallback className="text-sm font-semibold text-foreground">
9493
9315
  {settings?.siteName?.charAt(0) ?? cms.name?.charAt(0)}
9494
9316
  </AvatarFallback>
9495
9317
  </Avatar>
9496
- <div className="flex items-center gap-1 w-full group-data-[collapsible=icon]:hidden">
9497
- <span className="text-sm font-semibold line-clamp-1">{settings?.siteName ?? cms.name}</span>
9318
+ <div className="flex text-foreground items-center gap-1 w-full group-data-[collapsible=icon]:hidden">
9319
+ <span className="text-sm font-medium line-clamp-1">
9320
+ {settings?.siteName ?? cms.name}
9321
+ </span>
9498
9322
  </div>
9499
9323
  </Link>
9500
- <SidebarTrigger className="hidden md:flex" />
9501
9324
  </div>
9502
9325
  </SidebarHeader>
9503
- <SidebarContent className="gap-2">
9504
- <SidebarMenu className="py-2 gap-2">
9505
- {cmsNavigation.map((item) => (
9506
- <NavItem key={item.href + item.label} item={item} />
9507
- ))}
9508
- </SidebarMenu>
9509
- <SidebarMenu className="py-2 mt-auto border-t border-border px-2">
9510
- <SidebarMenuItem>
9511
- <SidebarMenuButton asChild>
9512
- <Link href="/cms/users">
9513
- <Users className="size-3.5!" />
9326
+ <SidebarContent>
9327
+ <SidebarGroup className="pb-1">
9328
+ <CmsSearch />
9329
+ </SidebarGroup>
9330
+
9331
+ {groups.map((group) => (
9332
+ <Fragment key={group.label ?? '_ungrouped'}>
9333
+ <SidebarGroup>
9334
+ {group.label && <SidebarGroupLabel>{group.label}</SidebarGroupLabel>}
9335
+ <SidebarMenu>
9336
+ {group.items.map((item) => (
9337
+ <SidebarMenuItem key={item.href}>
9338
+ <CmsNavLink href={item.href}>
9339
+ {item.icon && (
9340
+ <item.icon className="text-muted-foreground" absoluteStrokeWidth />
9341
+ )}
9342
+ <span>{item.label}</span>
9343
+ </CmsNavLink>
9344
+ </SidebarMenuItem>
9345
+ ))}
9346
+ </SidebarMenu>
9347
+ </SidebarGroup>
9348
+ </Fragment>
9349
+ ))}
9350
+
9351
+ <SidebarGroup className="mt-auto">
9352
+ <SidebarMenu>
9353
+ <SidebarMenuItem>
9354
+ <CmsNavLink href="/cms/users">
9355
+ <Users />
9514
9356
  <span>Users</span>
9515
- </Link>
9516
- </SidebarMenuButton>
9517
- </SidebarMenuItem>
9518
- <SidebarMenuItem>
9519
- <SidebarMenuButton asChild>
9520
- <Link href="/cms/settings">
9521
- <Settings className="size-3.5!" />
9357
+ </CmsNavLink>
9358
+ </SidebarMenuItem>
9359
+ <SidebarMenuItem>
9360
+ <CmsNavLink href="/cms/settings">
9361
+ <Settings />
9522
9362
  <span>Settings</span>
9523
- </Link>
9524
- </SidebarMenuButton>
9525
- </SidebarMenuItem>
9526
- </SidebarMenu>
9363
+ </CmsNavLink>
9364
+ </SidebarMenuItem>
9365
+ </SidebarMenu>
9366
+ </SidebarGroup>
9527
9367
  </SidebarContent>
9528
9368
  <SidebarFooter>
9529
9369
  {user && (
@@ -9535,7 +9375,6 @@ export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
9535
9375
  </div>
9536
9376
  )}
9537
9377
  </SidebarFooter>
9538
- <SidebarRail />
9539
9378
  </Sidebar>
9540
9379
  )
9541
9380
  }
@@ -9555,7 +9394,7 @@ import {
9555
9394
  AlertDialogFooter,
9556
9395
  AlertDialogHeader,
9557
9396
  AlertDialogTitle,
9558
- AlertDialogTrigger
9397
+ AlertDialogTrigger,
9559
9398
  } from '@cms/components/ui/alert-dialog'
9560
9399
  import { Button } from '@cms/components/ui/button'
9561
9400
  import { Trash2 } from 'lucide-react'
@@ -9577,7 +9416,7 @@ export function DeleteDialog({
9577
9416
  isPending = false,
9578
9417
  title = 'Are you sure?',
9579
9418
  description = 'This action cannot be undone. This will permanently delete this item.',
9580
- trigger
9419
+ trigger,
9581
9420
  }: DeleteDialogProps) {
9582
9421
  return (
9583
9422
  <AlertDialog open={open} onOpenChange={onOpenChange}>
@@ -9626,18 +9465,24 @@ export function DeleteButton({ onClick, label = 'item', count }: DeleteButtonPro
9626
9465
  function pageHeaderTemplate() {
9627
9466
  return `interface PageHeaderProps {
9628
9467
  title: string
9629
- description: string
9630
9468
  children?: React.ReactNode
9469
+ search?: React.ReactNode
9470
+ actions?: React.ReactNode
9471
+ back?: React.ReactNode
9631
9472
  }
9632
9473
 
9633
- export function PageHeader({ title, description, children }: PageHeaderProps) {
9474
+ export function PageHeader({ title, children, search, actions, back }: PageHeaderProps) {
9634
9475
  return (
9635
- <div className="flex items-center justify-between w-full">
9636
- <div className="flex flex-col">
9637
- <h2 className="text-base font-semibold tracking-tight">{title}</h2>
9638
- <p className="text-muted-foreground text-sm">{description}</p>
9476
+ <div className="grid grid-cols-3 items-center justify-between w-full h-14 px-4 border-b border-border">
9477
+ <div className="flex items-center justify-start gap-2 w-full">{back && back}</div>
9478
+ <div className="flex items-center justify-center gap-2">
9479
+ <h2 className="text-sm font-medium tracking-tight">{title}</h2>
9480
+ </div>
9481
+ <div className="flex items-center justify-end gap-2">
9482
+ {children && children}
9483
+ {search && search}
9484
+ {actions && actions}
9639
9485
  </div>
9640
- {children && <div className="flex items-center gap-2">{children}</div>}
9641
9486
  </div>
9642
9487
  )
9643
9488
  }
@@ -9656,7 +9501,7 @@ const statusStyles: Record<StatusVariant, string> = {
9656
9501
  success: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300',
9657
9502
  warning: 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300',
9658
9503
  error: 'bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300',
9659
- info: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300'
9504
+ info: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300',
9660
9505
  }
9661
9506
 
9662
9507
  interface StatusBadgeProps extends Omit<BadgeProps, 'variant'> {
@@ -9674,7 +9519,11 @@ export function StatusBadge({ status, className, ...props }: StatusBadgeProps) {
9674
9519
  }
9675
9520
 
9676
9521
  /** Map boolean values to status badges */
9677
- export function BooleanBadge({ value, trueLabel = 'Yes', falseLabel = 'No' }: {
9522
+ export function BooleanBadge({
9523
+ value,
9524
+ trueLabel = 'Yes',
9525
+ falseLabel = 'No',
9526
+ }: {
9678
9527
  value: boolean
9679
9528
  trueLabel?: string
9680
9529
  falseLabel?: string
@@ -9698,22 +9547,44 @@ function cmsDataTemplate(projectName) {
9698
9547
 
9699
9548
  // src/init/templates/data/navigation.ts
9700
9549
  function navigationDataTemplate() {
9701
- return `import { House } from 'lucide-react'
9702
- import type { LucideIcon } from 'lucide-react'
9550
+ return `import type { LucideIcon } from 'lucide-react'
9551
+ import { ChartSpline, FileText, House, ImagePlay, Tag } from 'lucide-react'
9703
9552
 
9704
9553
  export interface CmsNavigationItem {
9705
9554
  label: string
9706
9555
  href: string
9707
9556
  icon?: LucideIcon
9708
- children?: CmsNavigationItem[]
9557
+ group?: string
9709
9558
  }
9710
9559
 
9711
9560
  export const cmsNavigation: CmsNavigationItem[] = [
9712
9561
  {
9713
- label: 'Dashboard',
9562
+ label: 'Overview',
9714
9563
  href: '/cms',
9715
- icon: House
9716
- }
9564
+ icon: House,
9565
+ },
9566
+ {
9567
+ label: 'Analytics',
9568
+ href: '/cms/analytics',
9569
+ icon: ChartSpline,
9570
+ },
9571
+ {
9572
+ label: 'Media',
9573
+ href: '/cms/media',
9574
+ icon: ImagePlay,
9575
+ },
9576
+ {
9577
+ label: 'Categories',
9578
+ href: '/cms/categories',
9579
+ icon: Tag,
9580
+ group: 'Blog',
9581
+ },
9582
+ {
9583
+ label: 'Posts',
9584
+ href: '/cms/posts',
9585
+ icon: FileText,
9586
+ group: 'Blog',
9587
+ },
9717
9588
  ]
9718
9589
  `;
9719
9590
  }
@@ -9787,14 +9658,10 @@ export function CmsThemeProvider({ children }: { children: React.ReactNode }) {
9787
9658
 
9788
9659
  const value = React.useMemo(
9789
9660
  () => ({ theme, setTheme, resolvedTheme: resolved }),
9790
- [theme, setTheme, resolved]
9661
+ [theme, setTheme, resolved],
9791
9662
  )
9792
9663
 
9793
- return (
9794
- <CmsThemeContext.Provider value={value}>
9795
- {children}
9796
- </CmsThemeContext.Provider>
9797
- )
9664
+ return <CmsThemeContext.Provider value={value}>{children}</CmsThemeContext.Provider>
9798
9665
  }
9799
9666
 
9800
9667
  export function useTheme(): ThemeContext {
@@ -9834,11 +9701,11 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
9834
9701
  if (result.success && result.files) {
9835
9702
  const images: EditorImageUploadResult[] = result.files.map((f) => ({
9836
9703
  url: f.url,
9837
- filename: f.filename
9704
+ filename: f.filename,
9838
9705
  }))
9839
9706
  onImagesUploadedRef.current(images)
9840
9707
  }
9841
- }
9708
+ },
9842
9709
  })
9843
9710
 
9844
9711
  const isUploading = mutation.isPending
@@ -9849,21 +9716,19 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
9849
9716
  if (imageFiles.length === 0) return
9850
9717
  upload(imageFiles, 'images')
9851
9718
  },
9852
- [upload]
9719
+ [upload],
9853
9720
  )
9854
9721
 
9855
9722
  const handleDrop = React.useCallback(
9856
9723
  (e: React.DragEvent) => {
9857
9724
  e.preventDefault()
9858
9725
  e.stopPropagation()
9859
- const files = Array.from(e.dataTransfer.files).filter((f) =>
9860
- f.type.startsWith('image/')
9861
- )
9726
+ const files = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith('image/'))
9862
9727
  if (files.length > 0) {
9863
9728
  uploadImages(files)
9864
9729
  }
9865
9730
  },
9866
- [uploadImages]
9731
+ [uploadImages],
9867
9732
  )
9868
9733
 
9869
9734
  const openFilePicker = React.useCallback(() => {
@@ -9878,7 +9743,7 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
9878
9743
  }
9879
9744
  e.target.value = ''
9880
9745
  },
9881
- [uploadImages]
9746
+ [uploadImages],
9882
9747
  )
9883
9748
 
9884
9749
  return {
@@ -9888,7 +9753,7 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
9888
9753
  handleDrop,
9889
9754
  openFilePicker,
9890
9755
  fileInputRef,
9891
- handleFileInputChange
9756
+ handleFileInputChange,
9892
9757
  }
9893
9758
  }
9894
9759
  `;
@@ -9912,7 +9777,7 @@ export function useLocalStorage<T>(key: string) {
9912
9777
  // Silent failure for localStorage access errors
9913
9778
  }
9914
9779
  },
9915
- [prefixedKey]
9780
+ [prefixedKey],
9916
9781
  )
9917
9782
 
9918
9783
  const getItem = React.useCallback((): T | null => {
@@ -9945,18 +9810,36 @@ export function useLocalStorage<T>(key: string) {
9945
9810
  `;
9946
9811
  }
9947
9812
 
9813
+ // src/init/templates/hooks/use-mobile.ts
9814
+ function useMobileHookTemplate() {
9815
+ return `import * as React from 'react'
9816
+
9817
+ const MOBILE_BREAKPOINT = 768
9818
+
9819
+ export function useIsMobile() {
9820
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
9821
+
9822
+ React.useEffect(() => {
9823
+ const mql = window.matchMedia(\`(max-width: \${MOBILE_BREAKPOINT - 1}px)\`)
9824
+ const onChange = () => {
9825
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
9826
+ }
9827
+ mql.addEventListener('change', onChange)
9828
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
9829
+ return () => mql.removeEventListener('change', onChange)
9830
+ }, [])
9831
+
9832
+ return !!isMobile
9833
+ }
9834
+ `;
9835
+ }
9836
+
9948
9837
  // src/init/templates/hooks/use-upload.ts
9949
9838
  function useUploadHookTemplate() {
9950
9839
  return `'use client'
9951
9840
 
9952
- import type {
9953
- UploadFileResult,
9954
- UploadProgress
9955
- } from '@cms/types'
9956
- import {
9957
- type FileValidationConfig,
9958
- validateFiles
9959
- } from '@cms/utils/validation'
9841
+ import type { UploadFileResult, UploadProgress } from '@cms/types'
9842
+ import { type FileValidationConfig, validateFiles } from '@cms/utils/validation'
9960
9843
  import { type UseMutationResult, useMutation } from '@tanstack/react-query'
9961
9844
  import * as React from 'react'
9962
9845
 
@@ -10004,7 +9887,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10004
9887
  onProgress,
10005
9888
  onSuccess,
10006
9889
  onError,
10007
- prefix: defaultPrefix
9890
+ prefix: defaultPrefix,
10008
9891
  } = options
10009
9892
 
10010
9893
  const validationConfig = React.useMemo<FileValidationConfig>(() => {
@@ -10014,7 +9897,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10014
9897
  const parsedTypes = parseAcceptTypes(accept)
10015
9898
  if (config.allowedTypes && config.allowedTypes.length > 0) {
10016
9899
  config.allowedTypes = [...config.allowedTypes, ...parsedTypes].filter(
10017
- (v, i, a) => a.indexOf(v) === i
9900
+ (v, i, a) => a.indexOf(v) === i,
10018
9901
  )
10019
9902
  } else {
10020
9903
  config.allowedTypes = parsedTypes
@@ -10037,7 +9920,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10037
9920
  filename: file.name,
10038
9921
  progress: 0,
10039
9922
  loaded: 0,
10040
- total: file.size
9923
+ total: file.size,
10041
9924
  }))
10042
9925
  setProgress(initialProgress)
10043
9926
  onProgress?.(initialProgress)
@@ -10059,9 +9942,9 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10059
9942
  return {
10060
9943
  ...p,
10061
9944
  progress: newProgress,
10062
- loaded: Math.floor((p.total * newProgress) / 100)
9945
+ loaded: Math.floor((p.total * newProgress) / 100),
10063
9946
  }
10064
- })
9947
+ }),
10065
9948
  )
10066
9949
  }, 200)
10067
9950
 
@@ -10075,7 +9958,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10075
9958
 
10076
9959
  const response = await fetch('/api/cms/upload', {
10077
9960
  method: 'POST',
10078
- body: formData
9961
+ body: formData,
10079
9962
  })
10080
9963
 
10081
9964
  const result = (await response.json()) as UploadFileResult
@@ -10085,7 +9968,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10085
9968
  filename: file.name,
10086
9969
  progress: 100,
10087
9970
  loaded: file.size,
10088
- total: file.size
9971
+ total: file.size,
10089
9972
  }))
10090
9973
  setProgress(completeProgress)
10091
9974
  onProgress?.(completeProgress)
@@ -10107,14 +9990,14 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10107
9990
  onError: (error) => {
10108
9991
  onError?.(error)
10109
9992
  setProgress([])
10110
- }
9993
+ },
10111
9994
  })
10112
9995
 
10113
9996
  const upload = React.useCallback(
10114
9997
  (files: File[], prefix?: string) => {
10115
9998
  mutation.mutate({ files, prefix })
10116
9999
  },
10117
- [mutation]
10000
+ [mutation],
10118
10001
  )
10119
10002
 
10120
10003
  const validate = React.useCallback(
@@ -10122,10 +10005,10 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10122
10005
  const result = validateFiles(files, validationConfig)
10123
10006
  return {
10124
10007
  valid: result.valid,
10125
- errors: result.errors.map((e) => \`\${e.filename}: \${e.error}\`)
10008
+ errors: result.errors.map((e) => \`\${e.filename}: \${e.error}\`),
10126
10009
  }
10127
10010
  },
10128
- [validationConfig]
10011
+ [validationConfig],
10129
10012
  )
10130
10013
 
10131
10014
  return { mutation, progress, upload, validate }
@@ -10145,7 +10028,7 @@ export function useUsers() {
10145
10028
  return useQuery<UsersResponse>({
10146
10029
  queryKey: ['users'],
10147
10030
  queryFn: () => getUsers(),
10148
- staleTime: 0
10031
+ staleTime: 0,
10149
10032
  })
10150
10033
  }
10151
10034
  `;
@@ -10155,9 +10038,9 @@ export function useUsers() {
10155
10038
  function formSettingsActionTemplate() {
10156
10039
  return `'use server'
10157
10040
 
10158
- import { eq } from 'drizzle-orm'
10159
10041
  import db from '@cms/db'
10160
10042
  import { formSettings } from '@cms/db/schema'
10043
+ import { eq } from 'drizzle-orm'
10161
10044
 
10162
10045
  export interface FormSettingsData {
10163
10046
  id: number
@@ -10181,9 +10064,7 @@ export interface FormSettingsResult {
10181
10064
  settings?: FormSettingsData
10182
10065
  }
10183
10066
 
10184
- export async function getFormSettings(
10185
- formName: string
10186
- ): Promise<FormSettingsData | null> {
10067
+ export async function getFormSettings(formName: string): Promise<FormSettingsData | null> {
10187
10068
  try {
10188
10069
  const [settings] = await db
10189
10070
  .select()
@@ -10199,7 +10080,7 @@ export async function getFormSettings(
10199
10080
 
10200
10081
  export async function upsertFormSettings(
10201
10082
  formName: string,
10202
- data: UpsertFormSettingsInput
10083
+ data: UpsertFormSettingsInput,
10203
10084
  ): Promise<FormSettingsResult> {
10204
10085
  try {
10205
10086
  const existing = await getFormSettings(formName)
@@ -10230,8 +10111,7 @@ export async function upsertFormSettings(
10230
10111
  console.error(\`Error upserting form settings for \${formName}:\`, error)
10231
10112
  return {
10232
10113
  success: false,
10233
- error:
10234
- error instanceof Error ? error.message : 'Failed to save form settings',
10114
+ error: error instanceof Error ? error.message : 'Failed to save form settings',
10235
10115
  }
10236
10116
  }
10237
10117
  }
@@ -10247,7 +10127,7 @@ export async function getAllFormSettings(): Promise<FormSettingsData[]> {
10247
10127
  }
10248
10128
 
10249
10129
  export async function testFormWebhook(
10250
- formName: string
10130
+ formName: string,
10251
10131
  ): Promise<{ success: boolean; error?: string }> {
10252
10132
  try {
10253
10133
  const settings = await getFormSettings(formName)
@@ -10278,8 +10158,7 @@ export async function testFormWebhook(
10278
10158
  } catch (error) {
10279
10159
  return {
10280
10160
  success: false,
10281
- error:
10282
- error instanceof Error ? error.message : 'Failed to send test webhook',
10161
+ error: error instanceof Error ? error.message : 'Failed to send test webhook',
10283
10162
  }
10284
10163
  }
10285
10164
  }
@@ -10448,10 +10327,10 @@ export async function uploadImageFromUrl(
10448
10327
  function usersActionTemplate() {
10449
10328
  return `'use server'
10450
10329
 
10330
+ import { auth } from '@cms/auth'
10451
10331
  import db from '@cms/db'
10452
- import { user, account } from '@cms/db/schema'
10332
+ import { user } from '@cms/db/schema'
10453
10333
  import { eq } from 'drizzle-orm'
10454
- import { auth } from '@cms/auth'
10455
10334
 
10456
10335
  export interface UserData {
10457
10336
  id: string
@@ -10564,15 +10443,9 @@ export async function getUsers(): Promise<UsersResponse> {
10564
10443
  /**
10565
10444
  * Update a user's role
10566
10445
  */
10567
- export async function updateUserRole(
10568
- userId: string,
10569
- role: string,
10570
- ): Promise<UpdateUserRoleResult> {
10446
+ export async function updateUserRole(userId: string, role: string): Promise<UpdateUserRoleResult> {
10571
10447
  try {
10572
- await db
10573
- .update(user)
10574
- .set({ role, updatedAt: new Date() })
10575
- .where(eq(user.id, userId))
10448
+ await db.update(user).set({ role, updatedAt: new Date() }).where(eq(user.id, userId))
10576
10449
 
10577
10450
  return { success: true }
10578
10451
  } catch (error) {
@@ -10722,10 +10595,21 @@ function trimMathBlock(content: string): string {
10722
10595
  const shiki = createHighlighterCoreSync({
10723
10596
  themes: [githubDark, githubLight],
10724
10597
  langs: [
10725
- javascript, typescript, jsx, tsx, python, rust, go,
10726
- json, yaml, css, sql, shellscript, markdown
10598
+ javascript,
10599
+ typescript,
10600
+ jsx,
10601
+ tsx,
10602
+ python,
10603
+ rust,
10604
+ go,
10605
+ json,
10606
+ yaml,
10607
+ css,
10608
+ sql,
10609
+ shellscript,
10610
+ markdown,
10727
10611
  ],
10728
- engine: createJavaScriptRegexEngine()
10612
+ engine: createJavaScriptRegexEngine(),
10729
10613
  })
10730
10614
 
10731
10615
  const loadedLangs = shiki.getLoadedLanguages()
@@ -10767,11 +10651,11 @@ const md = MarkdownIt({
10767
10651
  lang: language,
10768
10652
  themes: { light: 'github-light', dark: 'github-dark' },
10769
10653
  defaultColor: false,
10770
- transformers: [transformerNotationHighlight(), transformerNotationDiff()]
10654
+ transformers: [transformerNotationHighlight(), transformerNotationDiff()],
10771
10655
  })
10772
10656
  const escapedCode = code.replace(/</g, '&lt;').replace(/>/g, '&gt;')
10773
10657
  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>\`
10774
- }
10658
+ },
10775
10659
  })
10776
10660
  .use(headingAnchorPlugin)
10777
10661
  .use(dollarmath, {
@@ -10784,7 +10668,7 @@ const md = MarkdownIt({
10784
10668
  return renderToString(content, {
10785
10669
  displayMode,
10786
10670
  throwOnError: false,
10787
- strict: 'ignore'
10671
+ strict: 'ignore',
10788
10672
  })
10789
10673
  },
10790
10674
  labelNormalizer(label: string) {
@@ -10792,7 +10676,7 @@ const md = MarkdownIt({
10792
10676
  },
10793
10677
  labelRenderer(label: string) {
10794
10678
  return \`<a href="#\${label}" class="mathlabel" title="Permalink to this equation">\xB6</a>\`
10795
- }
10679
+ },
10796
10680
  })
10797
10681
 
10798
10682
  export function renderMarkdownSync(src: string): string {
@@ -10887,7 +10771,7 @@ export interface AuthSession {
10887
10771
  export enum UserRole {
10888
10772
  ADMIN = 'admin',
10889
10773
  EDITOR = 'editor',
10890
- MEMBER = 'member'
10774
+ MEMBER = 'member',
10891
10775
  }
10892
10776
 
10893
10777
  export interface UserWithRole extends AuthUser {
@@ -10896,17 +10780,11 @@ export interface UserWithRole extends AuthUser {
10896
10780
 
10897
10781
  /** Type guard to check if a value is a valid UserRole */
10898
10782
  export function isUserRole(value: unknown): value is UserRole {
10899
- return (
10900
- typeof value === 'string' &&
10901
- Object.values(UserRole).includes(value as UserRole)
10902
- )
10783
+ return typeof value === 'string' && Object.values(UserRole).includes(value as UserRole)
10903
10784
  }
10904
10785
 
10905
10786
  /** Check if user has one of the allowed roles */
10906
- export function hasRequiredRole(
10907
- userRole: UserRole,
10908
- allowedRoles: UserRole[]
10909
- ): boolean {
10787
+ export function hasRequiredRole(userRole: UserRole, allowedRoles: UserRole[]): boolean {
10910
10788
  return allowedRoles.includes(userRole)
10911
10789
  }
10912
10790
 
@@ -11092,7 +10970,7 @@ export function createMetadata({
11092
10970
  description,
11093
10971
  path,
11094
10972
  ogImage,
11095
- noIndex = false
10973
+ noIndex = false,
11096
10974
  }: CreateMetadataOptions): Metadata {
11097
10975
  const metadata: Metadata = {
11098
10976
  title,
@@ -11101,14 +10979,14 @@ export function createMetadata({
11101
10979
  title,
11102
10980
  description,
11103
10981
  type: 'website',
11104
- ...(ogImage && { images: [{ url: ogImage }] })
10982
+ ...(ogImage && { images: [{ url: ogImage }] }),
11105
10983
  },
11106
10984
  twitter: {
11107
10985
  card: ogImage ? 'summary_large_image' : 'summary',
11108
10986
  title,
11109
10987
  description,
11110
- ...(ogImage && { images: [ogImage] })
11111
- }
10988
+ ...(ogImage && { images: [ogImage] }),
10989
+ },
11112
10990
  }
11113
10991
 
11114
10992
  if (path) {
@@ -11132,7 +11010,7 @@ export function generateArticleSchema({
11132
11010
  imageUrl,
11133
11011
  datePublished,
11134
11012
  dateModified,
11135
- authorName
11013
+ authorName,
11136
11014
  }: {
11137
11015
  title: string
11138
11016
  description: string
@@ -11152,8 +11030,8 @@ export function generateArticleSchema({
11152
11030
  datePublished,
11153
11031
  ...(dateModified && { dateModified }),
11154
11032
  ...(authorName && {
11155
- author: { '@type': 'Person', name: authorName }
11156
- })
11033
+ author: { '@type': 'Person', name: authorName },
11034
+ }),
11157
11035
  }
11158
11036
  }
11159
11037
 
@@ -11206,20 +11084,16 @@ function isFileTypeAllowed(file: File, allowedTypes: string[]): boolean {
11206
11084
  */
11207
11085
  export function validateFiles(
11208
11086
  files: File[],
11209
- config: FileValidationConfig = {}
11087
+ config: FileValidationConfig = {},
11210
11088
  ): FileValidationResult {
11211
- const {
11212
- maxSizeInBytes = DEFAULT_MAX_SIZE,
11213
- allowedTypes,
11214
- maxFiles = DEFAULT_MAX_FILES
11215
- } = config
11089
+ const { maxSizeInBytes = DEFAULT_MAX_SIZE, allowedTypes, maxFiles = DEFAULT_MAX_FILES } = config
11216
11090
 
11217
11091
  const errors: FileValidationError[] = []
11218
11092
 
11219
11093
  if (files.length > maxFiles) {
11220
11094
  errors.push({
11221
11095
  filename: '',
11222
- error: \`Too many files. Maximum is \${maxFiles}.\`
11096
+ error: \`Too many files. Maximum is \${maxFiles}.\`,
11223
11097
  })
11224
11098
  }
11225
11099
 
@@ -11228,14 +11102,14 @@ export function validateFiles(
11228
11102
  const maxMB = Math.round(maxSizeInBytes / (1024 * 1024))
11229
11103
  errors.push({
11230
11104
  filename: file.name,
11231
- error: \`File exceeds maximum size of \${maxMB}MB.\`
11105
+ error: \`File exceeds maximum size of \${maxMB}MB.\`,
11232
11106
  })
11233
11107
  }
11234
11108
 
11235
11109
  if (allowedTypes && allowedTypes.length > 0 && !isFileTypeAllowed(file, allowedTypes)) {
11236
11110
  errors.push({
11237
11111
  filename: file.name,
11238
- error: \`File type "\${file.type || 'unknown'}" is not allowed.\`
11112
+ error: \`File type "\${file.type || 'unknown'}" is not allowed.\`,
11239
11113
  })
11240
11114
  }
11241
11115
  }
@@ -11289,17 +11163,15 @@ function webhookUtilTemplate() {
11289
11163
  */
11290
11164
  export function sendWebhook(
11291
11165
  webhookUrl: string | null | undefined,
11292
- payload: Record<string, unknown>
11166
+ payload: Record<string, unknown>,
11293
11167
  ): void {
11294
- if (!webhookUrl) return
11295
- // Fire-and-forget: runs in background, doesn't block
11168
+ if (!webhookUrl) return // Fire-and-forget: runs in background, doesn't block
11296
11169
  ;(async () => {
11297
11170
  try {
11298
11171
  const formData = new URLSearchParams()
11299
11172
  for (const [key, value] of Object.entries(payload)) {
11300
11173
  if (value === null || value === undefined) continue
11301
- const stringValue =
11302
- typeof value === 'object' ? JSON.stringify(value) : String(value)
11174
+ const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value)
11303
11175
  formData.append(key, stringValue)
11304
11176
  }
11305
11177
  await fetch(webhookUrl, {
@@ -11327,6 +11199,7 @@ function scaffoldComponents({ cwd, config }) {
11327
11199
  }
11328
11200
  write("cms-globals.css", cmsGlobalsCssTemplate());
11329
11201
  write("components/layout/cms-providers.tsx", cmsProvidersTemplate());
11202
+ write("components/layout/cms-nav-link.tsx", cmsNavLinkTemplate());
11330
11203
  write("components/layout/cms-sidebar.tsx", cmsSidebarTemplate());
11331
11204
  write("components/layout/cms-header.tsx", cmsHeaderTemplate());
11332
11205
  write("components/layout/cms-search.tsx", cmsSearchTemplate());
@@ -11352,6 +11225,7 @@ function scaffoldComponents({ cwd, config }) {
11352
11225
  write("hooks/use-local-storage.ts", useLocalStorageHookTemplate());
11353
11226
  write("hooks/use-cms-theme.tsx", useCmsThemeTemplate());
11354
11227
  write("hooks/use-users.ts", useUsersHookTemplate());
11228
+ write("hooks/use-mobile.ts", useMobileHookTemplate());
11355
11229
  const projectName = detectProjectName(cwd);
11356
11230
  write("data/cms.ts", cmsDataTemplate(projectName));
11357
11231
  write("data/navigation.ts", navigationDataTemplate());
@@ -11588,6 +11462,7 @@ var CORE_DEPS = [
11588
11462
  "nuqs",
11589
11463
  "sonner",
11590
11464
  // Styling utilities
11465
+ "geist",
11591
11466
  "class-variance-authority",
11592
11467
  "clsx",
11593
11468
  "tailwind-merge",
@@ -11833,24 +11708,18 @@ import path32 from "path";
11833
11708
 
11834
11709
  // src/init/templates/pages/authenticated-layout.ts
11835
11710
  function authenticatedLayoutTemplate() {
11836
- return `import { CmsHeader } from '@cms/components/layout/cms-header'
11711
+ return `import { requireRole } from '@cms/auth/middleware'
11837
11712
  import { CmsSidebar } from '@cms/components/layout/cms-sidebar'
11838
- import { requireRole } from '@cms/auth/middleware'
11839
- import { UserRole } from '@cms/types/auth'
11840
11713
  import { SidebarInset, SidebarProvider } from '@cms/components/ui/sidebar'
11714
+ import { UserRole } from '@cms/types/auth'
11841
11715
 
11842
- export default async function CmsAuthLayout({
11843
- children
11844
- }: {
11845
- children: React.ReactNode
11846
- }) {
11716
+ export default async function CmsAuthLayout({ children }: { children: React.ReactNode }) {
11847
11717
  await requireRole([UserRole.ADMIN, UserRole.EDITOR])
11848
11718
 
11849
11719
  return (
11850
11720
  <SidebarProvider>
11851
11721
  <CmsSidebar />
11852
11722
  <SidebarInset>
11853
- <CmsHeader />
11854
11723
  <main>{children}</main>
11855
11724
  </SidebarInset>
11856
11725
  </SidebarProvider>
@@ -11863,11 +11732,17 @@ export default async function CmsAuthLayout({
11863
11732
  function cmsLayoutTemplate() {
11864
11733
  return `import '@cms/cms-globals.css'
11865
11734
  import { CmsProviders } from '@cms/components/layout/cms-providers'
11735
+ import { GeistMono } from 'geist/font/mono'
11736
+ import { GeistSans } from 'geist/font/sans'
11866
11737
 
11867
11738
  export default function CmsLayout({ children }: { children: React.ReactNode }) {
11868
11739
  return (
11869
11740
  <CmsProviders>
11870
- <div className="cms-root min-h-screen">{children}</div>
11741
+ <div
11742
+ className={\`cms-root min-h-screen antialiased \${GeistSans.variable} \${GeistMono.variable}\`}
11743
+ >
11744
+ {children}
11745
+ </div>
11871
11746
  </CmsProviders>
11872
11747
  )
11873
11748
  }
@@ -11877,8 +11752,8 @@ export default function CmsLayout({ children }: { children: React.ReactNode }) {
11877
11752
  // src/init/templates/pages/dashboard-page.ts
11878
11753
  function dashboardPageTemplate() {
11879
11754
  return `import { PageHeader } from '@cms/components/shared/page-header'
11880
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cms/components/ui/card'
11881
11755
  import { Badge } from '@cms/components/ui/badge'
11756
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cms/components/ui/card'
11882
11757
  import { FileText, Settings, Users } from 'lucide-react'
11883
11758
  import Link from 'next/link'
11884
11759
 
@@ -11887,32 +11762,27 @@ const quickLinks = [
11887
11762
  title: 'Users',
11888
11763
  description: 'Manage admin users and roles',
11889
11764
  href: '/cms/users',
11890
- icon: Users
11765
+ icon: Users,
11891
11766
  },
11892
11767
  {
11893
11768
  title: 'Settings',
11894
11769
  description: 'Configure CMS settings',
11895
11770
  href: '/cms/settings',
11896
- icon: Settings
11771
+ icon: Settings,
11897
11772
  },
11898
11773
  {
11899
11774
  title: 'Generate',
11900
11775
  description: 'Add a new resource from a schema',
11901
11776
  href: '#',
11902
11777
  icon: FileText,
11903
- hint: 'npx betterstart generate <schema>'
11904
- }
11778
+ hint: 'npx betterstart generate <schema>',
11779
+ },
11905
11780
  ]
11906
11781
 
11907
11782
  export default function DashboardPage() {
11908
11783
  return (
11909
11784
  <div className="flex flex-col">
11910
- <div className="flex items-center justify-between bg-card px-6 py-4 border-b">
11911
- <PageHeader
11912
- title="Dashboard"
11913
- description="Welcome to your CMS admin panel"
11914
- />
11915
- </div>
11785
+ <PageHeader title="Dashboard" />
11916
11786
  <div className="p-6 space-y-6">
11917
11787
  <div className="grid gap-4 md:grid-cols-3">
11918
11788
  {quickLinks.map((link) => (
@@ -11982,7 +11852,7 @@ import { LoginForm } from './login-form'
11982
11852
 
11983
11853
  export const metadata: Metadata = {
11984
11854
  title: 'CMS Login',
11985
- robots: { index: false, follow: false }
11855
+ robots: { index: false, follow: false },
11986
11856
  }
11987
11857
 
11988
11858
  export default function LoginPage() {
@@ -11991,9 +11861,7 @@ export default function LoginPage() {
11991
11861
  <div className="w-full max-w-sm">
11992
11862
  <div className="mb-8 text-center">
11993
11863
  <h1 className="text-2xl font-semibold tracking-tight">CMS</h1>
11994
- <p className="text-muted-foreground text-sm mt-1">
11995
- Sign in to access the admin panel
11996
- </p>
11864
+ <p className="text-muted-foreground text-sm mt-1">Sign in to access the admin panel</p>
11997
11865
  </div>
11998
11866
  <LoginForm />
11999
11867
  </div>
@@ -12028,7 +11896,7 @@ export function LoginForm() {
12028
11896
  try {
12029
11897
  const result = await authClient.signIn.email({
12030
11898
  email,
12031
- password
11899
+ password,
12032
11900
  })
12033
11901
 
12034
11902
  if (result.error) {
@@ -12048,9 +11916,7 @@ export function LoginForm() {
12048
11916
  return (
12049
11917
  <form onSubmit={handleSubmit} className="space-y-5">
12050
11918
  {error && (
12051
- <div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">
12052
- {error}
12053
- </div>
11919
+ <div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">{error}</div>
12054
11920
  )}
12055
11921
 
12056
11922
  <div className="space-y-2">
@@ -12095,6 +11961,7 @@ export function LoginForm() {
12095
11961
  function createUserDialogTemplate() {
12096
11962
  return `'use client'
12097
11963
 
11964
+ import { createUser } from '@cms/actions/users'
12098
11965
  import { Button } from '@cms/components/ui/button'
12099
11966
  import {
12100
11967
  Dialog,
@@ -12102,11 +11969,10 @@ import {
12102
11969
  DialogDescription,
12103
11970
  DialogHeader,
12104
11971
  DialogTitle,
12105
- DialogTrigger
11972
+ DialogTrigger,
12106
11973
  } from '@cms/components/ui/dialog'
12107
11974
  import { Input } from '@cms/components/ui/input'
12108
11975
  import { Label } from '@cms/components/ui/label'
12109
- import { createUser } from '@cms/actions/users'
12110
11976
  import { useQueryClient } from '@tanstack/react-query'
12111
11977
  import { Loader2, UserPlus } from 'lucide-react'
12112
11978
  import * as React from 'react'
@@ -12216,6 +12082,7 @@ export function CreateUserDialog() {
12216
12082
  function editRoleDialogTemplate() {
12217
12083
  return `'use client'
12218
12084
 
12085
+ import { updateUserRole } from '@cms/actions/users'
12219
12086
  import { Button } from '@cms/components/ui/button'
12220
12087
  import {
12221
12088
  Dialog,
@@ -12223,7 +12090,7 @@ import {
12223
12090
  DialogDescription,
12224
12091
  DialogHeader,
12225
12092
  DialogTitle,
12226
- DialogTrigger
12093
+ DialogTrigger,
12227
12094
  } from '@cms/components/ui/dialog'
12228
12095
  import { Label } from '@cms/components/ui/label'
12229
12096
  import {
@@ -12231,9 +12098,8 @@ import {
12231
12098
  SelectContent,
12232
12099
  SelectItem,
12233
12100
  SelectTrigger,
12234
- SelectValue
12101
+ SelectValue,
12235
12102
  } from '@cms/components/ui/select'
12236
- import { updateUserRole } from '@cms/actions/users'
12237
12103
  import { UserRole } from '@cms/types/auth'
12238
12104
  import { useQueryClient } from '@tanstack/react-query'
12239
12105
  import { Loader2 } from 'lucide-react'
@@ -12247,12 +12113,7 @@ interface EditRoleDialogProps {
12247
12113
  children: React.ReactNode
12248
12114
  }
12249
12115
 
12250
- export function EditRoleDialog({
12251
- userId,
12252
- currentRole,
12253
- userName,
12254
- children
12255
- }: EditRoleDialogProps) {
12116
+ export function EditRoleDialog({ userId, currentRole, userName, children }: EditRoleDialogProps) {
12256
12117
  const [open, setOpen] = React.useState(false)
12257
12118
  const [role, setRole] = React.useState(currentRole)
12258
12119
  const [isPending, startTransition] = React.useTransition()
@@ -12281,9 +12142,7 @@ export function EditRoleDialog({
12281
12142
  <DialogContent className="sm:max-w-[350px]">
12282
12143
  <DialogHeader>
12283
12144
  <DialogTitle>Edit Role</DialogTitle>
12284
- <DialogDescription>
12285
- Change the role for {userName}
12286
- </DialogDescription>
12145
+ <DialogDescription>Change the role for {userName}</DialogDescription>
12287
12146
  </DialogHeader>
12288
12147
  <div className="space-y-4">
12289
12148
  <div className="space-y-2">
@@ -12300,17 +12159,10 @@ export function EditRoleDialog({
12300
12159
  </Select>
12301
12160
  </div>
12302
12161
  <div className="flex justify-end gap-2">
12303
- <Button
12304
- variant="outline"
12305
- onClick={() => setOpen(false)}
12306
- disabled={isPending}
12307
- >
12162
+ <Button variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
12308
12163
  Cancel
12309
12164
  </Button>
12310
- <Button
12311
- onClick={handleSave}
12312
- disabled={isPending || role === currentRole}
12313
- >
12165
+ <Button onClick={handleSave} disabled={isPending || role === currentRole}>
12314
12166
  {isPending && <Loader2 className="size-4 mr-1 animate-spin" />}
12315
12167
  Save
12316
12168
  </Button>
@@ -12327,11 +12179,7 @@ export function EditRoleDialog({
12327
12179
  function usersColumnsTemplate() {
12328
12180
  return `'use client'
12329
12181
 
12330
- import React from 'react'
12331
- import {
12332
- Avatar,
12333
- AvatarFallback
12334
- } from '@cms/components/ui/avatar'
12182
+ import { deleteUser } from '@cms/actions/users'
12335
12183
  import {
12336
12184
  AlertDialog,
12337
12185
  AlertDialogAction,
@@ -12343,6 +12191,7 @@ import {
12343
12191
  AlertDialogTitle,
12344
12192
  AlertDialogTrigger,
12345
12193
  } from '@cms/components/ui/alert-dialog'
12194
+ import { Avatar, AvatarFallback } from '@cms/components/ui/avatar'
12346
12195
  import { Badge } from '@cms/components/ui/badge'
12347
12196
  import { Button } from '@cms/components/ui/button'
12348
12197
  import {
@@ -12351,14 +12200,14 @@ import {
12351
12200
  DropdownMenuItem,
12352
12201
  DropdownMenuLabel,
12353
12202
  DropdownMenuSeparator,
12354
- DropdownMenuTrigger
12203
+ DropdownMenuTrigger,
12355
12204
  } from '@cms/components/ui/dropdown-menu'
12356
12205
  import type { UserData } from '@cms/types/auth'
12357
- import type { ColumnDef } from '@tanstack/react-table'
12358
12206
  import { useQueryClient } from '@tanstack/react-query'
12207
+ import type { ColumnDef } from '@tanstack/react-table'
12359
12208
  import { ArrowUpDown, Edit, MoreHorizontal, Trash } from 'lucide-react'
12209
+ import React from 'react'
12360
12210
  import { toast } from 'sonner'
12361
- import { deleteUser } from '@cms/actions/users'
12362
12211
  import { EditRoleDialog } from './edit-role-dialog'
12363
12212
 
12364
12213
  function getInitials(nameOrEmail: string): string {
@@ -12409,10 +12258,7 @@ function DeleteUserAction({
12409
12258
  return (
12410
12259
  <AlertDialog open={open} onOpenChange={setOpen}>
12411
12260
  <AlertDialogTrigger asChild>
12412
- <DropdownMenuItem
12413
- className="text-destructive"
12414
- onSelect={(e) => e.preventDefault()}
12415
- >
12261
+ <DropdownMenuItem className="text-destructive" onSelect={(e) => e.preventDefault()}>
12416
12262
  <Trash className="size-4 mr-2" />
12417
12263
  Delete user
12418
12264
  </DropdownMenuItem>
@@ -12421,8 +12267,8 @@ function DeleteUserAction({
12421
12267
  <AlertDialogHeader>
12422
12268
  <AlertDialogTitle>Are you sure?</AlertDialogTitle>
12423
12269
  <AlertDialogDescription>
12424
- This action cannot be undone. This will permanently delete{' '}
12425
- <strong>{userName}</strong> and all of their data.
12270
+ This action cannot be undone. This will permanently delete <strong>{userName}</strong>{' '}
12271
+ and all of their data.
12426
12272
  </AlertDialogDescription>
12427
12273
  </AlertDialogHeader>
12428
12274
  <AlertDialogFooter>
@@ -12470,7 +12316,7 @@ export const columns: ColumnDef<UserData>[] = [
12470
12316
  </div>
12471
12317
  </div>
12472
12318
  )
12473
- }
12319
+ },
12474
12320
  },
12475
12321
  {
12476
12322
  accessorKey: 'emailVerified',
@@ -12482,7 +12328,7 @@ export const columns: ColumnDef<UserData>[] = [
12482
12328
  {verified ? 'Verified' : 'Unverified'}
12483
12329
  </Badge>
12484
12330
  )
12485
- }
12331
+ },
12486
12332
  },
12487
12333
  {
12488
12334
  accessorKey: 'role',
@@ -12512,7 +12358,7 @@ export const columns: ColumnDef<UserData>[] = [
12512
12358
  </Badge>
12513
12359
  </EditRoleDialog>
12514
12360
  )
12515
- }
12361
+ },
12516
12362
  },
12517
12363
  {
12518
12364
  accessorKey: 'createdAt',
@@ -12533,11 +12379,11 @@ export const columns: ColumnDef<UserData>[] = [
12533
12379
  {date.toLocaleDateString('en-US', {
12534
12380
  month: 'short',
12535
12381
  day: 'numeric',
12536
- year: 'numeric'
12382
+ year: 'numeric',
12537
12383
  })}
12538
12384
  </div>
12539
12385
  )
12540
- }
12386
+ },
12541
12387
  },
12542
12388
  {
12543
12389
  id: 'actions',
@@ -12556,9 +12402,7 @@ export const columns: ColumnDef<UserData>[] = [
12556
12402
  </DropdownMenuTrigger>
12557
12403
  <DropdownMenuContent align="end">
12558
12404
  <DropdownMenuLabel>Actions</DropdownMenuLabel>
12559
- <DropdownMenuItem
12560
- onClick={() => navigator.clipboard.writeText(row.original.id)}
12561
- >
12405
+ <DropdownMenuItem onClick={() => navigator.clipboard.writeText(row.original.id)}>
12562
12406
  Copy user ID
12563
12407
  </DropdownMenuItem>
12564
12408
  <DropdownMenuSeparator />
@@ -12571,8 +12415,8 @@ export const columns: ColumnDef<UserData>[] = [
12571
12415
  </DropdownMenu>
12572
12416
  </div>
12573
12417
  )
12574
- }
12575
- }
12418
+ },
12419
+ },
12576
12420
  ]
12577
12421
  `;
12578
12422
  }
@@ -12588,10 +12432,7 @@ export default function UsersPage() {
12588
12432
  return (
12589
12433
  <div className="flex flex-col">
12590
12434
  <div className="flex items-center justify-between bg-card px-6 py-4 border-b">
12591
- <PageHeader
12592
- title="Users"
12593
- description="Manage all CMS users"
12594
- >
12435
+ <PageHeader title="Users">
12595
12436
  <CreateUserDialog />
12596
12437
  </PageHeader>
12597
12438
  </div>
@@ -12608,6 +12449,18 @@ export default function UsersPage() {
12608
12449
  function usersTableTemplate() {
12609
12450
  return `'use client'
12610
12451
 
12452
+ import { authClient } from '@cms/auth/client'
12453
+ import { Button } from '@cms/components/ui/button'
12454
+ import {
12455
+ Table,
12456
+ TableBody,
12457
+ TableCell,
12458
+ TableHead,
12459
+ TableHeader,
12460
+ TableRow,
12461
+ } from '@cms/components/ui/table'
12462
+ import { useUsers } from '@cms/hooks/use-users'
12463
+ import type { UserData } from '@cms/types/auth'
12611
12464
  import {
12612
12465
  type ColumnDef,
12613
12466
  type ColumnFiltersState,
@@ -12618,21 +12471,9 @@ import {
12618
12471
  getSortedRowModel,
12619
12472
  type SortingState,
12620
12473
  useReactTable,
12621
- type VisibilityState
12474
+ type VisibilityState,
12622
12475
  } from '@tanstack/react-table'
12623
12476
  import * as React from 'react'
12624
- import { Button } from '@cms/components/ui/button'
12625
- import {
12626
- Table,
12627
- TableBody,
12628
- TableCell,
12629
- TableHead,
12630
- TableHeader,
12631
- TableRow
12632
- } from '@cms/components/ui/table'
12633
- import { useUsers } from '@cms/hooks/use-users'
12634
- import { authClient } from '@cms/auth/client'
12635
- import type { UserData } from '@cms/types/auth'
12636
12477
 
12637
12478
  interface UsersTableProps<TValue> {
12638
12479
  columns: ColumnDef<UserData, TValue>[]
@@ -12662,11 +12503,11 @@ export function UsersTable<TValue>({ columns }: UsersTableProps<TValue>) {
12662
12503
  email: session.user.email,
12663
12504
  name: session.user.name,
12664
12505
  image: session.user.image,
12665
- role: (session.user as { role?: string }).role || 'member'
12506
+ role: (session.user as { role?: string }).role || 'member',
12666
12507
  }
12667
- : null
12508
+ : null,
12668
12509
  },
12669
- state: { sorting, columnFilters, columnVisibility }
12510
+ state: { sorting, columnFilters, columnVisibility },
12670
12511
  })
12671
12512
 
12672
12513
  return (
@@ -13606,24 +13447,29 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
13606
13447
  const { execFile } = await import("child_process");
13607
13448
  const tsxBin = path36.join(cwd, "node_modules", ".bin", "tsx");
13608
13449
  const runSeed2 = (overwrite) => new Promise((resolve, reject) => {
13609
- execFile(tsxBin, [seedPath], {
13610
- cwd,
13611
- env: {
13612
- ...process.env,
13613
- SEED_EMAIL: email,
13614
- SEED_PASSWORD: password3,
13615
- SEED_NAME: name || "Admin",
13616
- ...overwrite ? { SEED_OVERWRITE: "true" } : {}
13617
- }
13618
- }, (err, stdout, stderr) => {
13619
- if (err && "code" in err && err.code === 2) {
13620
- resolve({ code: 2, stdout });
13621
- } else if (err) {
13622
- reject(new Error(stderr || err.message));
13623
- } else {
13624
- resolve({ code: 0, stdout });
13450
+ execFile(
13451
+ tsxBin,
13452
+ [seedPath],
13453
+ {
13454
+ cwd,
13455
+ env: {
13456
+ ...process.env,
13457
+ SEED_EMAIL: email,
13458
+ SEED_PASSWORD: password3,
13459
+ SEED_NAME: name || "Admin",
13460
+ ...overwrite ? { SEED_OVERWRITE: "true" } : {}
13461
+ }
13462
+ },
13463
+ (err, stdout, stderr) => {
13464
+ if (err && "code" in err && err.code === 2) {
13465
+ resolve({ code: 2, stdout });
13466
+ } else if (err) {
13467
+ reject(new Error(stderr || err.message));
13468
+ } else {
13469
+ resolve({ code: 0, stdout });
13470
+ }
13625
13471
  }
13626
- });
13472
+ );
13627
13473
  });
13628
13474
  const spinner5 = clack.spinner();
13629
13475
  spinner5.start("Creating admin user...");
@@ -13674,7 +13520,7 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
13674
13520
  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(
13675
13521
  "--database-url <url>",
13676
13522
  "PostgreSQL database connection string (postgres:// or postgresql://)"
13677
- ).action(
13523
+ ).option("--force", "Overwrite all existing CMS files (nuclear option)").action(
13678
13524
  async (name, options) => {
13679
13525
  p4.intro(pc2.bgCyan(pc2.black(" BetterStart CMS ")));
13680
13526
  let cwd = process.cwd();
@@ -13689,9 +13535,35 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13689
13535
  p4.log.error("TypeScript is required. Please add a tsconfig.json first.");
13690
13536
  process.exit(1);
13691
13537
  }
13692
- if (project.conflicts.length > 0) {
13538
+ if (options.force) {
13539
+ const nukeDirs = ["cms", "app/(cms)"];
13540
+ const nukeFiles = ["cms.config.ts", "CMS.md", "drizzle.config.ts"];
13541
+ let nuked = 0;
13542
+ for (const dir of nukeDirs) {
13543
+ const fullPath = path37.resolve(cwd, dir);
13544
+ if (fs32.existsSync(fullPath)) {
13545
+ fs32.rmSync(fullPath, { recursive: true, force: true });
13546
+ nuked++;
13547
+ }
13548
+ }
13549
+ for (const file of nukeFiles) {
13550
+ const fullPath = path37.resolve(cwd, file);
13551
+ if (fs32.existsSync(fullPath)) {
13552
+ fs32.unlinkSync(fullPath);
13553
+ nuked++;
13554
+ }
13555
+ }
13556
+ if (nuked > 0) {
13557
+ p4.log.warn(`${pc2.yellow("Force mode:")} removed ${nuked} existing CMS paths`);
13558
+ }
13559
+ project = detectProject(cwd);
13560
+ } else if (project.conflicts.length > 0) {
13693
13561
  const conflictLines = project.conflicts.map((c) => `${pc2.yellow("\u25B2")} ${c}`);
13694
- conflictLines.push("", pc2.dim("Existing files will not be overwritten."));
13562
+ conflictLines.push(
13563
+ "",
13564
+ pc2.dim("Existing files will not be overwritten."),
13565
+ pc2.dim(`Use ${pc2.bold("--force")} to remove existing CMS files before scaffolding.`)
13566
+ );
13695
13567
  p4.note(conflictLines.join("\n"), pc2.yellow("Conflicts"));
13696
13568
  if (!options.yes) {
13697
13569
  const proceed = await p4.confirm({
@@ -13862,13 +13734,17 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13862
13734
  p4.note(noteLines.join("\n"), "Scaffolded CMS");
13863
13735
  const drizzleConfigPath = path37.join(cwd, "drizzle.config.ts");
13864
13736
  if (!dbFiles.includes("drizzle.config.ts") && fs32.existsSync(drizzleConfigPath)) {
13865
- if (!options.yes) {
13737
+ if (options.force) {
13738
+ const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-EDKOEZ6G.js");
13739
+ fs32.writeFileSync(drizzleConfigPath, drizzleConfigTemplate2(), "utf-8");
13740
+ p4.log.success("Updated drizzle.config.ts");
13741
+ } else if (!options.yes) {
13866
13742
  const overwrite = await p4.confirm({
13867
13743
  message: "drizzle.config.ts already exists. Overwrite with latest version?",
13868
13744
  initialValue: true
13869
13745
  });
13870
13746
  if (!p4.isCancel(overwrite) && overwrite) {
13871
- const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-KISB26BA.js");
13747
+ const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-EDKOEZ6G.js");
13872
13748
  fs32.writeFileSync(drizzleConfigPath, drizzleConfigTemplate2(), "utf-8");
13873
13749
  p4.log.success("Updated drizzle.config.ts");
13874
13750
  }
@@ -13985,7 +13861,12 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13985
13861
  seedEmail = credentials.email;
13986
13862
  seedPassword = credentials.password;
13987
13863
  s.start("Creating admin user");
13988
- let seedResult = await runSeed(cwd, config.paths?.cms ?? "./cms", credentials.email, credentials.password);
13864
+ let seedResult = await runSeed(
13865
+ cwd,
13866
+ config.paths?.cms ?? "./cms",
13867
+ credentials.email,
13868
+ credentials.password
13869
+ );
13989
13870
  if (seedResult.existingUser) {
13990
13871
  s.stop(`${pc2.yellow("\u25B2")} Admin user already exists (${seedResult.existingUser})`);
13991
13872
  const replace = await p4.confirm({
@@ -13994,7 +13875,13 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13994
13875
  });
13995
13876
  if (!p4.isCancel(replace) && replace) {
13996
13877
  s.start("Replacing admin user");
13997
- seedResult = await runSeed(cwd, config.paths?.cms ?? "./cms", credentials.email, credentials.password, true);
13878
+ seedResult = await runSeed(
13879
+ cwd,
13880
+ config.paths?.cms ?? "./cms",
13881
+ credentials.email,
13882
+ credentials.password,
13883
+ true
13884
+ );
13998
13885
  } else {
13999
13886
  seedSuccess = true;
14000
13887
  }
@@ -14187,8 +14074,7 @@ ${stderr}`;
14187
14074
  if (combined.includes("Failed to create user")) return "Auth API failed to create user";
14188
14075
  if (combined.includes("ECONNREFUSED") || combined.includes("connection refused"))
14189
14076
  return "Could not connect to database";
14190
- if (combined.includes("BETTERSTART_DATABASE_URL"))
14191
- return "Database URL is missing or invalid";
14077
+ if (combined.includes("BETTERSTART_DATABASE_URL")) return "Database URL is missing or invalid";
14192
14078
  if (combined.includes("password authentication failed"))
14193
14079
  return "Database authentication failed \u2014 check your connection string";
14194
14080
  if (combined.includes("does not exist") && combined.includes("relation"))
@@ -14443,41 +14329,12 @@ var removeCommand = new Command4("remove").alias("rm").description("Remove all g
14443
14329
  console.log("");
14444
14330
  });
14445
14331
 
14446
- // src/commands/update-deps.ts
14447
- import path39 from "path";
14448
- import * as clack2 from "@clack/prompts";
14449
- import { Command as Command5 } from "commander";
14450
- var updateDepsCommand = new Command5("update-deps").description("Install or update all CMS dependencies").option("--cwd <path>", "Project root path").action(async (options) => {
14451
- const cwd = options.cwd ? path39.resolve(options.cwd) : process.cwd();
14452
- clack2.intro("BetterStart Update Dependencies");
14453
- const pm = detectPackageManager(cwd);
14454
- clack2.log.info(`Package manager: ${pm}`);
14455
- const config = await resolveConfig(cwd);
14456
- const includeEmail = config.features?.email ?? true;
14457
- const s = clack2.spinner();
14458
- s.start("Installing dependencies...");
14459
- const result = await installDependenciesAsync({
14460
- cwd,
14461
- pm,
14462
- includeEmail,
14463
- includeBiome: false
14464
- });
14465
- if (result.success) {
14466
- s.stop(`Installed ${result.coreDeps.length} deps + ${result.devDeps.length} dev deps`);
14467
- } else {
14468
- s.stop("Dependency install failed");
14469
- clack2.log.error(result.error ?? "Unknown error");
14470
- process.exit(1);
14471
- }
14472
- clack2.outro("Dependencies updated");
14473
- });
14474
-
14475
14332
  // src/commands/uninstall.ts
14476
14333
  import fs35 from "fs";
14477
- import path40 from "path";
14334
+ import path39 from "path";
14478
14335
  import * as p5 from "@clack/prompts";
14336
+ import { Command as Command5 } from "commander";
14479
14337
  import pc3 from "picocolors";
14480
- import { Command as Command6 } from "commander";
14481
14338
 
14482
14339
  // src/commands/uninstall-cleaners.ts
14483
14340
  import fs34 from "fs";
@@ -14534,7 +14391,7 @@ function cleanTsconfig(tsconfigPath) {
14534
14391
  }
14535
14392
  if (removed.length === 0) return [];
14536
14393
  if (Object.keys(paths).length === 0) {
14537
- delete compilerOptions.paths;
14394
+ compilerOptions.paths = void 0;
14538
14395
  } else {
14539
14396
  compilerOptions.paths = paths;
14540
14397
  }
@@ -14588,14 +14445,14 @@ function cleanEnvFile(envPath) {
14588
14445
  }
14589
14446
  if (trimmed.startsWith("#") && !headerPattern.test(trimmed)) {
14590
14447
  const nextNonEmpty = findNextNonEmptyLine(lines, i + 1);
14591
- if (nextNonEmpty !== null && nextNonEmpty.match(/^BETTERSTART_\w+=/)) {
14448
+ if (nextNonEmpty?.match(/^BETTERSTART_\w+=/)) {
14592
14449
  continue;
14593
14450
  }
14594
14451
  }
14595
14452
  kept.push(line);
14596
14453
  }
14597
14454
  if (removed.length === 0) return [];
14598
- let result = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
14455
+ const result = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
14599
14456
  if (result === "") {
14600
14457
  fs34.unlinkSync(envPath);
14601
14458
  } else {
@@ -14625,7 +14482,7 @@ function findMainCss2(cwd) {
14625
14482
  "globals.css"
14626
14483
  ];
14627
14484
  for (const candidate of candidates) {
14628
- const filePath = path40.join(cwd, candidate);
14485
+ const filePath = path39.join(cwd, candidate);
14629
14486
  if (fs35.existsSync(filePath)) return filePath;
14630
14487
  }
14631
14488
  return void 0;
@@ -14641,11 +14498,11 @@ function isCLICreatedBiome(biomePath) {
14641
14498
  }
14642
14499
  function buildUninstallPlan(cwd) {
14643
14500
  const steps = [];
14644
- const hasSrc = fs35.existsSync(path40.join(cwd, "src"));
14501
+ const hasSrc = fs35.existsSync(path39.join(cwd, "src"));
14645
14502
  const appBase = hasSrc ? "src/app" : "app";
14646
14503
  const dirs = [];
14647
- const cmsDir = path40.join(cwd, "cms");
14648
- const cmsRouteGroup = path40.join(cwd, appBase, "(cms)");
14504
+ const cmsDir = path39.join(cwd, "cms");
14505
+ const cmsRouteGroup = path39.join(cwd, appBase, "(cms)");
14649
14506
  if (fs35.existsSync(cmsDir)) dirs.push("cms/");
14650
14507
  if (fs35.existsSync(cmsRouteGroup)) dirs.push(`${appBase}/(cms)/`);
14651
14508
  if (dirs.length > 0) {
@@ -14663,9 +14520,9 @@ function buildUninstallPlan(cwd) {
14663
14520
  const configFiles = [];
14664
14521
  const configPaths = [];
14665
14522
  const candidates = [
14666
- ["cms.config.ts", path40.join(cwd, "cms.config.ts")],
14667
- ["drizzle.config.ts", path40.join(cwd, "drizzle.config.ts")],
14668
- ["CMS.md", path40.join(cwd, "CMS.md")]
14523
+ ["cms.config.ts", path39.join(cwd, "cms.config.ts")],
14524
+ ["drizzle.config.ts", path39.join(cwd, "drizzle.config.ts")],
14525
+ ["CMS.md", path39.join(cwd, "CMS.md")]
14669
14526
  ];
14670
14527
  for (const [label, fullPath] of candidates) {
14671
14528
  if (fs35.existsSync(fullPath)) {
@@ -14673,7 +14530,7 @@ function buildUninstallPlan(cwd) {
14673
14530
  configPaths.push(fullPath);
14674
14531
  }
14675
14532
  }
14676
- const biomePath = path40.join(cwd, "biome.json");
14533
+ const biomePath = path39.join(cwd, "biome.json");
14677
14534
  if (isCLICreatedBiome(biomePath)) {
14678
14535
  configFiles.push("biome.json (CLI-created)");
14679
14536
  configPaths.push(biomePath);
@@ -14691,7 +14548,7 @@ function buildUninstallPlan(cwd) {
14691
14548
  }
14692
14549
  });
14693
14550
  }
14694
- const tsconfigPath = path40.join(cwd, "tsconfig.json");
14551
+ const tsconfigPath = path39.join(cwd, "tsconfig.json");
14695
14552
  if (fs35.existsSync(tsconfigPath)) {
14696
14553
  const content = fs35.readFileSync(tsconfigPath, "utf-8");
14697
14554
  const aliasMatches = content.match(/"@cms\//g);
@@ -14713,7 +14570,7 @@ function buildUninstallPlan(cwd) {
14713
14570
  const cssContent = fs35.readFileSync(cssFile, "utf-8");
14714
14571
  const sourceLines = cssContent.split("\n").filter((l) => /^@source\s+"[^"]*cms[^"]*";\s*$/.test(l));
14715
14572
  if (sourceLines.length > 0) {
14716
- const relCss = path40.relative(cwd, cssFile);
14573
+ const relCss = path39.relative(cwd, cssFile);
14717
14574
  steps.push({
14718
14575
  label: `CSS @source lines (${relCss})`,
14719
14576
  items: [`@source lines in ${relCss}`],
@@ -14725,7 +14582,7 @@ function buildUninstallPlan(cwd) {
14725
14582
  });
14726
14583
  }
14727
14584
  }
14728
- const envPath = path40.join(cwd, ".env.local");
14585
+ const envPath = path39.join(cwd, ".env.local");
14729
14586
  if (fs35.existsSync(envPath)) {
14730
14587
  const envContent = fs35.readFileSync(envPath, "utf-8");
14731
14588
  const bsVars = envContent.split("\n").filter((l) => l.trim().match(/^BETTERSTART_\w+=/)).map((l) => l.split("=")[0]);
@@ -14743,12 +14600,12 @@ function buildUninstallPlan(cwd) {
14743
14600
  }
14744
14601
  return steps;
14745
14602
  }
14746
- 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) => {
14747
- const cwd = options.cwd ? path40.resolve(options.cwd) : process.cwd();
14603
+ 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) => {
14604
+ const cwd = options.cwd ? path39.resolve(options.cwd) : process.cwd();
14748
14605
  p5.intro(pc3.bgRed(pc3.white(" BetterStart Uninstall ")));
14749
14606
  const steps = buildUninstallPlan(cwd);
14750
14607
  if (steps.length === 0) {
14751
- p5.log.success(pc3.green("\u2713") + " Nothing to remove \u2014 project is already clean.");
14608
+ p5.log.success(`${pc3.green("\u2713")} Nothing to remove \u2014 project is already clean.`);
14752
14609
  p5.outro("Done");
14753
14610
  return;
14754
14611
  }
@@ -14776,13 +14633,39 @@ var uninstallCommand = new Command6("uninstall").description("Remove all CMS fil
14776
14633
  }
14777
14634
  const parts = steps.map((step) => `${step.count} ${step.unit}`);
14778
14635
  s.stop(`Removed ${parts.join(", ")}`);
14779
- p5.note(
14780
- pc3.dim("Database tables were NOT dropped \u2014 drop them manually if needed."),
14781
- "Next steps"
14782
- );
14636
+ p5.note(pc3.dim("Database tables were NOT dropped \u2014 drop them manually if needed."), "Next steps");
14783
14637
  p5.outro("Uninstall complete");
14784
14638
  });
14785
14639
 
14640
+ // src/commands/update-deps.ts
14641
+ import path40 from "path";
14642
+ import * as clack2 from "@clack/prompts";
14643
+ import { Command as Command6 } from "commander";
14644
+ var updateDepsCommand = new Command6("update-deps").description("Install or update all CMS dependencies").option("--cwd <path>", "Project root path").action(async (options) => {
14645
+ const cwd = options.cwd ? path40.resolve(options.cwd) : process.cwd();
14646
+ clack2.intro("BetterStart Update Dependencies");
14647
+ const pm = detectPackageManager(cwd);
14648
+ clack2.log.info(`Package manager: ${pm}`);
14649
+ const config = await resolveConfig(cwd);
14650
+ const includeEmail = config.features?.email ?? true;
14651
+ const s = clack2.spinner();
14652
+ s.start("Installing dependencies...");
14653
+ const result = await installDependenciesAsync({
14654
+ cwd,
14655
+ pm,
14656
+ includeEmail,
14657
+ includeBiome: false
14658
+ });
14659
+ if (result.success) {
14660
+ s.stop(`Installed ${result.coreDeps.length} deps + ${result.devDeps.length} dev deps`);
14661
+ } else {
14662
+ s.stop("Dependency install failed");
14663
+ clack2.log.error(result.error ?? "Unknown error");
14664
+ process.exit(1);
14665
+ }
14666
+ clack2.outro("Dependencies updated");
14667
+ });
14668
+
14786
14669
  // src/commands/update-styles.ts
14787
14670
  import fs36 from "fs";
14788
14671
  import path41 from "path";