@betterstart/cli 0.1.28 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/{chunk-SAPJG4NO.js → chunk-6JCWMKSY.js} +7 -4
  2. package/dist/{chunk-SAPJG4NO.js.map → chunk-6JCWMKSY.js.map} +1 -1
  3. package/dist/cli.js +742 -869
  4. package/dist/cli.js.map +1 -1
  5. package/dist/drizzle-config-EDKOEZ6G.js +7 -0
  6. package/package.json +1 -1
  7. package/templates/ui/accordion.tsx +73 -42
  8. package/templates/ui/alert-dialog.tsx +155 -90
  9. package/templates/ui/alert.tsx +46 -26
  10. package/templates/ui/aspect-ratio.tsx +4 -2
  11. package/templates/ui/avatar.tsx +92 -43
  12. package/templates/ui/badge.tsx +27 -12
  13. package/templates/ui/breadcrumb.tsx +63 -60
  14. package/templates/ui/button-group.tsx +8 -8
  15. package/templates/ui/button.tsx +44 -26
  16. package/templates/ui/calendar.tsx +43 -34
  17. package/templates/ui/card.tsx +71 -34
  18. package/templates/ui/carousel.tsx +111 -115
  19. package/templates/ui/chart.tsx +197 -207
  20. package/templates/ui/checkbox.tsx +21 -20
  21. package/templates/ui/collapsible.tsx +14 -4
  22. package/templates/ui/combobox.tsx +272 -0
  23. package/templates/ui/command.tsx +139 -101
  24. package/templates/ui/context-menu.tsx +214 -156
  25. package/templates/ui/dialog.tsx +118 -77
  26. package/templates/ui/direction.tsx +20 -0
  27. package/templates/ui/drawer.tsx +89 -69
  28. package/templates/ui/dropdown-menu.tsx +228 -164
  29. package/templates/ui/empty.tsx +8 -5
  30. package/templates/ui/field.tsx +25 -32
  31. package/templates/ui/hover-card.tsx +29 -20
  32. package/templates/ui/input-group.tsx +20 -37
  33. package/templates/ui/input-otp.tsx +57 -42
  34. package/templates/ui/input.tsx +14 -17
  35. package/templates/ui/item.tsx +27 -17
  36. package/templates/ui/kbd.tsx +1 -3
  37. package/templates/ui/label.tsx +14 -14
  38. package/templates/ui/markdown-editor.tsx +1 -1
  39. package/templates/ui/menubar.tsx +220 -188
  40. package/templates/ui/native-select.tsx +42 -0
  41. package/templates/ui/navigation-menu.tsx +130 -90
  42. package/templates/ui/pagination.tsx +88 -73
  43. package/templates/ui/popover.tsx +67 -26
  44. package/templates/ui/progress.tsx +24 -18
  45. package/templates/ui/radio-group.tsx +26 -20
  46. package/templates/ui/resizable.tsx +29 -29
  47. package/templates/ui/scroll-area.tsx +47 -38
  48. package/templates/ui/select.tsx +158 -125
  49. package/templates/ui/separator.tsx +21 -19
  50. package/templates/ui/sheet.tsx +104 -95
  51. package/templates/ui/sidebar.tsx +77 -183
  52. package/templates/ui/skeleton.tsx +8 -2
  53. package/templates/ui/slider.tsx +46 -17
  54. package/templates/ui/sonner.tsx +19 -9
  55. package/templates/ui/spinner.tsx +2 -2
  56. package/templates/ui/switch.tsx +24 -20
  57. package/templates/ui/table.tsx +68 -73
  58. package/templates/ui/tabs.tsx +71 -46
  59. package/templates/ui/textarea.tsx +13 -16
  60. package/templates/ui/toggle-group.tsx +57 -28
  61. package/templates/ui/toggle.tsx +21 -20
  62. package/templates/ui/tooltip.tsx +44 -23
  63. package/dist/drizzle-config-KISB26BA.js +0 -7
  64. package/templates/ui/use-mobile.tsx +0 -19
  65. /package/dist/{drizzle-config-KISB26BA.js.map → drizzle-config-EDKOEZ6G.js.map} +0 -0
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  drizzleConfigTemplate
4
- } from "./chunk-SAPJG4NO.js";
4
+ } from "./chunk-6JCWMKSY.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { Command as Command8 } from "commander";
@@ -1404,6 +1404,7 @@ function generatePageContent(pascal, kebab, camel, label) {
1404
1404
 
1405
1405
  import type { ${pascal}SubmissionData } from '@cms/actions/${kebab}-form'
1406
1406
  import { deleteBulk${pascal}Submissions } from '@cms/actions/${kebab}-form'
1407
+ import { PageHeader } from '@cms/components/shared/page-header'
1407
1408
  import { Button } from '@cms/components/ui/button'
1408
1409
  import { Input } from '@cms/components/ui/input'
1409
1410
  import {
@@ -1417,11 +1418,11 @@ import {
1417
1418
  AlertDialogTitle,
1418
1419
  AlertDialogTrigger,
1419
1420
  } from '@cms/components/ui/alert-dialog'
1420
- import { PageHeader } from '@cms/components/shared/page-header'
1421
1421
  import { useQueryClient } from '@tanstack/react-query'
1422
1422
  import type { ColumnDef } from '@tanstack/react-table'
1423
- import { Search, Settings, Trash2 } from 'lucide-react'
1423
+ import { ChevronLeft, Search, Settings, Trash2 } from 'lucide-react'
1424
1424
  import Link from 'next/link'
1425
+ import { useRouter } from 'next/navigation'
1425
1426
  import { parseAsString, useQueryState } from 'nuqs'
1426
1427
  import { startTransition, useCallback, useState, useTransition } from 'react'
1427
1428
  import { toast } from 'sonner'
@@ -1434,6 +1435,7 @@ interface ${pascal}SubmissionsPageContentProps<TValue> {
1434
1435
  export function ${pascal}SubmissionsPageContent<TValue>({
1435
1436
  columns,
1436
1437
  }: ${pascal}SubmissionsPageContentProps<TValue>) {
1438
+ const router = useRouter()
1437
1439
  const queryClient = useQueryClient()
1438
1440
  const [search, setSearch] = useQueryState('q', parseAsString.withDefault(''))
1439
1441
  const [selectedIds, setSelectedIds] = useState<number[]>([])
@@ -1470,33 +1472,21 @@ export function ${pascal}SubmissionsPageContent<TValue>({
1470
1472
 
1471
1473
  return (
1472
1474
  <>
1473
- <div className="flex items-center justify-between border-b px-6 py-4">
1474
- <PageHeader title="${label}" description="View and manage form submissions" />
1475
- <div className="flex items-center gap-2">
1476
- <form action={searchAction} className="flex items-center gap-2">
1477
- <div className="relative">
1478
- <Search className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" />
1479
- <Input
1480
- key={search}
1481
- name="search"
1482
- placeholder="Search submissions..."
1483
- defaultValue={search}
1484
- className="w-64 pl-9"
1485
- />
1486
- </div>
1487
- <Button type="submit" variant="outline">Search</Button>
1488
- </form>
1489
- <Button variant="outline" asChild>
1490
- <Link href="/cms/forms/${kebab}/settings">
1491
- <Settings className="size-3.5" />
1492
- Settings
1493
- </Link>
1494
- </Button>
1475
+ <PageHeader title="${label}" back={<Button variant="ghost" size="icon" onClick={() => router.back()}><ChevronLeft /></Button>} search={<form action={searchAction} className="flex items-center gap-2 relative">
1476
+ <Search className="text-muted-foreground/70 pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" />
1477
+ <Input
1478
+ key={search}
1479
+ name="search"
1480
+ placeholder="Search submissions..."
1481
+ defaultValue={search}
1482
+ className="w-64 pl-9 bg-white rounded-full"
1483
+ />
1484
+ </form>} actions={<div className="flex items-center gap-2">
1495
1485
  {selectedIds.length > 0 && (
1496
1486
  <AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
1497
1487
  <AlertDialogTrigger asChild>
1498
- <Button variant="destructive">
1499
- <Trash2 className="size-3.5" />
1488
+ <Button variant="destructive" size="default">
1489
+ <Trash2 className="size-3.5 -ml-0.5" strokeWidth={2} />
1500
1490
  Delete {selectedIds.length}
1501
1491
  </Button>
1502
1492
  </AlertDialogTrigger>
@@ -1521,9 +1511,13 @@ export function ${pascal}SubmissionsPageContent<TValue>({
1521
1511
  </AlertDialogContent>
1522
1512
  </AlertDialog>
1523
1513
  )}
1524
- </div>
1525
- </div>
1526
-
1514
+ <Button variant="outline" asChild>
1515
+ <Link href="/cms/forms/${kebab}/settings">
1516
+ <Settings className="size-3.5" />
1517
+ Settings
1518
+ </Link>
1519
+ </Button>
1520
+ </div>} />
1527
1521
  <main className="space-y-4 p-6">
1528
1522
  <${pascal}SubmissionsTable
1529
1523
  columns={columns}
@@ -1597,8 +1591,9 @@ function generateViewPage(pascal, kebab, fields, label, includeDynamic) {
1597
1591
  </div>
1598
1592
  )}` : "";
1599
1593
  return `import { get${pascal}Submission } from '@cms/actions/${kebab}-form'
1594
+ import { PageHeader } from '@cms/components/shared/page-header'
1600
1595
  import { Button } from '@cms/components/ui/button'
1601
- import { ArrowLeft } from 'lucide-react'
1596
+ import { ChevronLeft } from 'lucide-react'
1602
1597
  import Link from 'next/link'
1603
1598
  import { notFound } from 'next/navigation'
1604
1599
 
@@ -1615,21 +1610,9 @@ export default async function Page({ params }: PageProps) {
1615
1610
  }
1616
1611
 
1617
1612
  return (
1618
- <div className="space-y-6 p-6">
1619
- <div className="flex items-center justify-between">
1620
- <div>
1621
- <h1 className="text-2xl font-bold">${label} Submission</h1>
1622
- <p className="text-muted-foreground">Viewing submission #{id}</p>
1623
- </div>
1624
- <Button variant="outline" asChild>
1625
- <Link href="/cms/forms/${kebab}">
1626
- <ArrowLeft className="size-4" />
1627
- Back to ${label}
1628
- </Link>
1629
- </Button>
1630
- </div>
1631
-
1632
- <div className="rounded-lg border p-6 space-y-4">
1613
+ <>
1614
+ <PageHeader title="${label} Submission" back={<Button variant="ghost" size="icon" asChild><Link href="/cms/forms/${kebab}"><ChevronLeft /></Link></Button>} />
1615
+ <div className="rounded-lg border p-6 mx-6 mt-6 space-y-4">
1633
1616
  ${fieldItems}${customFieldsSection}
1634
1617
  <div className="space-y-1 border-t pt-4">
1635
1618
  <p className="text-sm font-medium text-muted-foreground">Submitted At</p>
@@ -1641,7 +1624,7 @@ ${fieldItems}${customFieldsSection}
1641
1624
  </p>
1642
1625
  </div>
1643
1626
  </div>
1644
- </div>
1627
+ </>
1645
1628
  )
1646
1629
  }
1647
1630
  `;
@@ -1654,17 +1637,19 @@ import {
1654
1637
  testFormWebhook,
1655
1638
  upsertFormSettings,
1656
1639
  } from '@cms/actions/form-settings'
1640
+ import { PageHeader } from '@cms/components/shared/page-header'
1657
1641
  import { Button } from '@cms/components/ui/button'
1658
1642
  import { Input } from '@cms/components/ui/input'
1659
1643
  import { Label } from '@cms/components/ui/label'
1660
1644
  import { Switch } from '@cms/components/ui/switch'
1661
1645
  import { Textarea } from '@cms/components/ui/textarea'
1662
- import { ArrowLeft, Loader2 } from 'lucide-react'
1663
- import Link from 'next/link'
1646
+ import { ChevronLeft, Loader2 } from 'lucide-react'
1647
+ import { useRouter } from 'next/navigation'
1664
1648
  import { useEffect, useState, useTransition } from 'react'
1665
1649
  import { toast } from 'sonner'
1666
1650
 
1667
1651
  export default function ${pascal}SettingsPage() {
1652
+ const router = useRouter()
1668
1653
  const [notificationEmails, setNotificationEmails] = useState('')
1669
1654
  const [webhookUrl, setWebhookUrl] = useState('')
1670
1655
  const [webhookEnabled, setWebhookEnabled] = useState(false)
@@ -1718,23 +1703,9 @@ export default function ${pascal}SettingsPage() {
1718
1703
  }
1719
1704
 
1720
1705
  return (
1721
- <div className="space-y-6 p-6">
1722
- <div className="flex items-center justify-between">
1723
- <div>
1724
- <h1 className="text-2xl font-bold">${label} Settings</h1>
1725
- <p className="text-muted-foreground">
1726
- Configure notifications and webhooks for this form
1727
- </p>
1728
- </div>
1729
- <Button variant="outline" asChild>
1730
- <Link href="/cms/forms/${kebab}">
1731
- <ArrowLeft className="size-4" />
1732
- Back to ${label}
1733
- </Link>
1734
- </Button>
1735
- </div>
1736
-
1737
- <div className="space-y-6 rounded-lg border p-6">
1706
+ <>
1707
+ <PageHeader title="${label} Settings" back={<Button variant="ghost" size="icon" onClick={() => router.back()}><ChevronLeft /></Button>} />
1708
+ <div className="space-y-6 rounded-lg border p-6 mx-6 mt-6">
1738
1709
  <div className="space-y-2">
1739
1710
  <Label htmlFor="notificationEmails">Notification Emails</Label>
1740
1711
  <Textarea
@@ -1804,7 +1775,7 @@ export default function ${pascal}SettingsPage() {
1804
1775
  </Button>
1805
1776
  </div>
1806
1777
  </div>
1807
- </div>
1778
+ </>
1808
1779
  )
1809
1780
  }
1810
1781
  `;
@@ -1866,55 +1837,47 @@ function parseSingleItem(str) {
1866
1837
  const labelMatch = str.match(/label:\s*['"]([^'"]+)['"]/);
1867
1838
  const hrefMatch = str.match(/href:\s*['"]([^'"]+)['"]/);
1868
1839
  const iconMatch = str.match(/icon:\s*(\w+)/);
1840
+ const groupMatch = str.match(/group:\s*['"]([^'"]+)['"]/);
1869
1841
  if (!labelMatch || !hrefMatch) return null;
1870
1842
  const item = { label: labelMatch[1], href: hrefMatch[1] };
1871
1843
  if (iconMatch) item.icon = iconMatch[1];
1872
- const childrenMatch = str.match(/children:\s*\[([\s\S]*?)\]/);
1873
- if (childrenMatch) {
1874
- item.children = parseItemsBlock(childrenMatch[1]);
1875
- }
1844
+ if (groupMatch) item.group = groupMatch[1];
1876
1845
  return item;
1877
1846
  }
1878
1847
  function generateNavigationCode(items, iconImports) {
1879
1848
  const lines = [];
1880
- lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
1881
1849
  lines.push("import type { LucideIcon } from 'lucide-react'");
1850
+ lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
1882
1851
  lines.push("");
1883
1852
  lines.push("export interface CmsNavigationItem {");
1884
1853
  lines.push(" label: string");
1885
1854
  lines.push(" href: string");
1886
1855
  lines.push(" icon?: LucideIcon");
1887
- lines.push(" children?: CmsNavigationItem[]");
1856
+ lines.push(" group?: string");
1888
1857
  lines.push("}");
1889
1858
  lines.push("");
1890
1859
  lines.push("export const cmsNavigation: CmsNavigationItem[] = [");
1891
1860
  for (let i = 0; i < items.length; i++) {
1892
- appendItem(lines, items[i], 2, i === items.length - 1);
1861
+ appendItem(lines, items[i], i === items.length - 1);
1893
1862
  }
1894
1863
  lines.push("]");
1895
1864
  lines.push("");
1896
1865
  return lines.join("\n");
1897
1866
  }
1898
- function appendItem(lines, item, indent, isLast) {
1899
- const pad = " ".repeat(indent);
1900
- if (item.children && item.children.length > 0) {
1901
- lines.push(`${pad}{`);
1902
- lines.push(`${pad} label: '${item.label}',`);
1903
- lines.push(`${pad} href: '${item.href}',`);
1904
- if (item.icon) lines.push(`${pad} icon: ${item.icon},`);
1905
- lines.push(`${pad} children: [`);
1906
- for (let j = 0; j < item.children.length; j++) {
1907
- appendItem(lines, item.children[j], indent + 4, j === item.children.length - 1);
1908
- }
1909
- lines.push(`${pad} ]`);
1910
- lines.push(`${pad}}${isLast ? "" : ","}`);
1911
- } else {
1912
- lines.push(`${pad}{`);
1913
- lines.push(`${pad} label: '${item.label}',`);
1914
- lines.push(`${pad} href: '${item.href}'${item.icon ? "," : ""}`);
1915
- if (item.icon) lines.push(`${pad} icon: ${item.icon}`);
1916
- lines.push(`${pad}}${isLast ? "" : ","}`);
1917
- }
1867
+ function appendItem(lines, item, isLast) {
1868
+ lines.push(" {");
1869
+ lines.push(` label: '${item.label}',`);
1870
+ const hasMore = item.icon != null || item.group != null;
1871
+ lines.push(` href: '${item.href}'${hasMore ? "," : ""}`);
1872
+ if (item.icon && item.group) {
1873
+ lines.push(` icon: ${item.icon},`);
1874
+ lines.push(` group: '${item.group}'`);
1875
+ } else if (item.icon) {
1876
+ lines.push(` icon: ${item.icon}`);
1877
+ } else if (item.group) {
1878
+ lines.push(` group: '${item.group}'`);
1879
+ }
1880
+ lines.push(` }${isLast ? "" : ","}`);
1918
1881
  }
1919
1882
  function toKebabCase2(str) {
1920
1883
  return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
@@ -1929,45 +1892,35 @@ function updateFormNavigation(schema, cwd, cmsDir, options = {}) {
1929
1892
  items = parsed.items;
1930
1893
  iconImports = parsed.iconImports;
1931
1894
  }
1932
- let formsGroup = items.find((item) => item.label === "Forms");
1933
- if (!formsGroup) {
1934
- formsGroup = {
1935
- label: "Forms",
1936
- href: "#",
1937
- icon: "FileInput",
1938
- children: []
1939
- };
1940
- items.push(formsGroup);
1941
- }
1942
- if (!formsGroup.children) {
1943
- formsGroup.children = [];
1944
- }
1945
1895
  const kebab = toKebabCase2(schema.name);
1946
1896
  const formHref = `/cms/forms/${kebab}`;
1947
- const existingIndex = formsGroup.children.findIndex((c) => c.href === formHref);
1948
- const newChild = {
1897
+ const existingIndex = items.findIndex((item) => item.href === formHref);
1898
+ const newItem = {
1949
1899
  label: schema.label,
1950
1900
  href: formHref,
1951
- icon: "Inbox"
1901
+ icon: "Inbox",
1902
+ group: "Forms"
1952
1903
  };
1953
1904
  if (existingIndex >= 0) {
1954
1905
  if (options.force) {
1955
- formsGroup.children[existingIndex] = newChild;
1906
+ items[existingIndex] = newItem;
1956
1907
  } else {
1957
1908
  return { files: [] };
1958
1909
  }
1959
1910
  } else {
1960
- formsGroup.children.push(newChild);
1961
- formsGroup.children.sort((a, b) => a.label.localeCompare(b.label));
1911
+ items.push(newItem);
1962
1912
  }
1963
1913
  const dashboard = items.find((item) => item.href === "/cms");
1964
1914
  const others = items.filter((item) => item.href !== "/cms");
1965
- others.sort((a, b) => a.label.localeCompare(b.label));
1915
+ others.sort((a, b) => {
1916
+ if (!a.group && b.group) return -1;
1917
+ if (a.group && !b.group) return 1;
1918
+ if (a.group && b.group && a.group !== b.group) return a.group.localeCompare(b.group);
1919
+ return a.label.localeCompare(b.label);
1920
+ });
1966
1921
  items = [...dashboard ? [dashboard] : [], ...others];
1967
- for (const icon of ["FileInput", "Inbox"]) {
1968
- if (!iconImports.includes(icon)) {
1969
- iconImports.push(icon);
1970
- }
1922
+ if (!iconImports.includes("Inbox")) {
1923
+ iconImports.push("Inbox");
1971
1924
  }
1972
1925
  iconImports.sort();
1973
1926
  const dir = path5.dirname(navFilePath);
@@ -2423,7 +2376,7 @@ function generateFormComponent(schema, cwd, cmsDir, options) {
2423
2376
  import { zodResolver } from '@hookform/resolvers/zod'
2424
2377
  import { useState } from 'react'
2425
2378
  import { useForm } from 'react-hook-form'
2426
- import { z } from 'zod'
2379
+ import { z } from 'zod/v3'
2427
2380
  import { create${pascal}Submission } from '@cms/actions/${formName}-form'
2428
2381
  import { Button } from '@cms/components/ui/button'
2429
2382
  import {
@@ -4517,7 +4470,7 @@ function generateCreatePage(schema, cwd, pagesDir, options = {}) {
4517
4470
  const Singular = toPascalCase8(singular);
4518
4471
  const singLabel = singularizeLabel(schema.label);
4519
4472
  const kebabName = toKebabCase4(schema.name);
4520
- const content = `import { ArrowLeft } from 'lucide-react'
4473
+ const content = `import { ChevronLeft } from 'lucide-react'
4521
4474
  import Link from 'next/link'
4522
4475
  import { connection } from 'next/server'
4523
4476
  import { PageHeader } from '@cms/components/shared/page-header'
@@ -4528,23 +4481,12 @@ export default async function Create${Singular}Page() {
4528
4481
  await connection()
4529
4482
 
4530
4483
  return (
4531
- <div className="flex flex-col w-full pb-20">
4532
- <div className="flex items-center justify-between bg-card px-6 py-4 border-b">
4533
- <PageHeader
4534
- title="Create ${singLabel}"
4535
- description="Add a new ${singLabel.toLowerCase()} to the system"
4536
- />
4537
- <Button variant="outline" asChild>
4538
- <Link href="/cms/${schema.name}">
4539
- <ArrowLeft className="size-4" />
4540
- Back to ${schema.label}
4541
- </Link>
4542
- </Button>
4543
- </div>
4544
- <main className="container mx-auto max-w-5xl p-6">
4484
+ <>
4485
+ <PageHeader title="Create ${singLabel}" back={<Button variant="ghost" size="icon" asChild><Link href="/cms/${schema.name}"><ChevronLeft /></Link></Button>} />
4486
+ <main className="container mx-auto max-w-5xl p-6 pb-20">
4545
4487
  <${Singular}Form key={Date.now()} />
4546
4488
  </main>
4547
- </div>
4489
+ </>
4548
4490
  )
4549
4491
  }
4550
4492
  `;
@@ -4838,7 +4780,7 @@ function generateEditPage(schema, cwd, pagesDir, options = {}) {
4838
4780
  const Singular = toPascalCase10(singular);
4839
4781
  const camelSingular = toCamelCase6(singular);
4840
4782
  const kebabName = toKebabCase5(schema.name);
4841
- const content = `import { ArrowLeft } from 'lucide-react'
4783
+ const content = `import { ChevronLeft } from 'lucide-react'
4842
4784
  import Link from 'next/link'
4843
4785
  import { notFound } from 'next/navigation'
4844
4786
  import { PageHeader } from '@cms/components/shared/page-header'
@@ -4861,23 +4803,12 @@ export default async function Edit${Singular}Page({ params }: PageProps) {
4861
4803
  }
4862
4804
 
4863
4805
  return (
4864
- <div className="flex flex-col w-full pb-20">
4865
- <div className="flex items-center justify-between bg-card px-6 py-4 border-b">
4866
- <PageHeader
4867
- title="Edit ${Singular}"
4868
- description="Update ${singular} information"
4869
- />
4870
- <Button variant="outline" asChild>
4871
- <Link href="/cms/${schema.name}">
4872
- <ArrowLeft className="size-4" />
4873
- Back to ${schema.label}
4874
- </Link>
4875
- </Button>
4876
- </div>
4877
- <main className="container mx-auto max-w-5xl p-6">
4806
+ <>
4807
+ <PageHeader title="Edit ${Singular}" back={<Button variant="ghost" size="icon" asChild><Link href="/cms/${schema.name}"><ChevronLeft /></Link></Button>} />
4808
+ <main className="container mx-auto max-w-5xl p-6 pb-20">
4878
4809
  <${Singular}Form key={${camelSingular}.id} initialData={${camelSingular}} />
4879
4810
  </main>
4880
- </div>
4811
+ </>
4881
4812
  )
4882
4813
  }
4883
4814
  `;
@@ -5868,7 +5799,7 @@ import { ${lucideIcons.join(", ")} } from 'lucide-react'` : ""}
5868
5799
  import { useRouter } from 'next/navigation'
5869
5800
  import {${hasNestedList ? " useFieldArray," : ""} useForm } from 'react-hook-form'
5870
5801
  import { toast } from 'sonner'
5871
- import { z } from 'zod'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
5802
+ import { z } from 'zod/v3'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
5872
5803
  ${relHookImports ? `${relHookImports}
5873
5804
  ` : ""}${uiImports.join("\n")}
5874
5805
  import type {
@@ -6195,7 +6126,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'${lucideIcons
6195
6126
  import { ${lucideIcons.join(", ")} } from 'lucide-react'` : ""}
6196
6127
  import {${hasNestedList ? " useFieldArray," : ""} useForm } from 'react-hook-form'
6197
6128
  import { toast } from 'sonner'
6198
- import { z } from 'zod'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
6129
+ import { z } from 'zod/v3'${hasTabsField ? "\nimport { useQueryState } from 'nuqs'" : ""}${hasRelationship ? "\nimport { cn } from '@cms/utils/cn'" : ""}
6199
6130
  ${relHookImports ? `${relHookImports}
6200
6131
  ` : ""}${uiImports.join("\n")}
6201
6132
  import type {
@@ -6486,60 +6417,52 @@ function parseSingleItem2(str) {
6486
6417
  const labelMatch = str.match(/label:\s*['"]([^'"]+)['"]/);
6487
6418
  const hrefMatch = str.match(/href:\s*['"]([^'"]+)['"]/);
6488
6419
  const iconMatch = str.match(/icon:\s*(\w+)/);
6420
+ const groupMatch = str.match(/group:\s*['"]([^'"]+)['"]/);
6489
6421
  if (!labelMatch || !hrefMatch) return null;
6490
6422
  const item = {
6491
6423
  label: labelMatch[1],
6492
6424
  href: hrefMatch[1]
6493
6425
  };
6494
6426
  if (iconMatch) item.icon = iconMatch[1];
6495
- const childrenMatch = str.match(/children:\s*\[([\s\S]*?)\]/);
6496
- if (childrenMatch) {
6497
- item.children = parseItemsBlock2(childrenMatch[1]);
6498
- }
6427
+ if (groupMatch) item.group = groupMatch[1];
6499
6428
  return item;
6500
6429
  }
6501
6430
  function generateNavigationCode2(items, iconImports) {
6502
6431
  const lines = [];
6503
- lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
6504
6432
  lines.push("import type { LucideIcon } from 'lucide-react'");
6433
+ lines.push(`import { ${iconImports.join(", ")} } from 'lucide-react'`);
6505
6434
  lines.push("");
6506
6435
  lines.push("export interface CmsNavigationItem {");
6507
6436
  lines.push(" label: string");
6508
6437
  lines.push(" href: string");
6509
6438
  lines.push(" icon?: LucideIcon");
6510
- lines.push(" children?: CmsNavigationItem[]");
6439
+ lines.push(" group?: string");
6511
6440
  lines.push("}");
6512
6441
  lines.push("");
6513
6442
  lines.push("export const cmsNavigation: CmsNavigationItem[] = [");
6514
6443
  for (let i = 0; i < items.length; i++) {
6515
6444
  const item = items[i];
6516
6445
  const isLast = i === items.length - 1;
6517
- appendItem2(lines, item, 2, isLast);
6446
+ appendItem2(lines, item, isLast);
6518
6447
  }
6519
6448
  lines.push("]");
6520
6449
  lines.push("");
6521
6450
  return lines.join("\n");
6522
6451
  }
6523
- function appendItem2(lines, item, indent, isLast) {
6524
- const pad = " ".repeat(indent);
6525
- if (item.children && item.children.length > 0) {
6526
- lines.push(`${pad}{`);
6527
- lines.push(`${pad} label: '${item.label}',`);
6528
- lines.push(`${pad} href: '${item.href}',`);
6529
- if (item.icon) lines.push(`${pad} icon: ${item.icon},`);
6530
- lines.push(`${pad} children: [`);
6531
- for (let j = 0; j < item.children.length; j++) {
6532
- appendItem2(lines, item.children[j], indent + 4, j === item.children.length - 1);
6533
- }
6534
- lines.push(`${pad} ]`);
6535
- lines.push(`${pad}}${isLast ? "" : ","}`);
6536
- } else {
6537
- lines.push(`${pad}{`);
6538
- lines.push(`${pad} label: '${item.label}',`);
6539
- lines.push(`${pad} href: '${item.href}'${item.icon ? "," : ""}`);
6540
- if (item.icon) lines.push(`${pad} icon: ${item.icon}`);
6541
- lines.push(`${pad}}${isLast ? "" : ","}`);
6542
- }
6452
+ function appendItem2(lines, item, isLast) {
6453
+ lines.push(" {");
6454
+ lines.push(` label: '${item.label}',`);
6455
+ const hasMore = item.icon != null || item.group != null;
6456
+ lines.push(` href: '${item.href}'${hasMore ? "," : ""}`);
6457
+ if (item.icon && item.group) {
6458
+ lines.push(` icon: ${item.icon},`);
6459
+ lines.push(` group: '${item.group}'`);
6460
+ } else if (item.icon) {
6461
+ lines.push(` icon: ${item.icon}`);
6462
+ } else if (item.group) {
6463
+ lines.push(` group: '${item.group}'`);
6464
+ }
6465
+ lines.push(` }${isLast ? "" : ","}`);
6543
6466
  }
6544
6467
  function updateNavigation(schema, cwd, cmsDir, options = {}) {
6545
6468
  const navFilePath = path15.join(cwd, cmsDir, "data", "navigation.ts");
@@ -6561,48 +6484,26 @@ function updateNavigation(schema, cwd, cmsDir, options = {}) {
6561
6484
  icon: schema.icon
6562
6485
  };
6563
6486
  if (schema.navGroup) {
6564
- let 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
- }
6487
+ newItem.group = schema.navGroup.label;
6488
+ }
6489
+ const existingIndex = items.findIndex((item) => item.href === entityHref);
6490
+ if (existingIndex >= 0) {
6491
+ if (options.force) {
6492
+ items[existingIndex] = newItem;
6584
6493
  } else {
6585
- 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);
6494
+ return { files: [] };
6590
6495
  }
6591
6496
  } else {
6592
- const existingIndex = items.findIndex((item) => item.href === entityHref);
6593
- if (existingIndex >= 0) {
6594
- if (options.force) {
6595
- items[existingIndex] = newItem;
6596
- } else {
6597
- return { files: [] };
6598
- }
6599
- } else {
6600
- items.push(newItem);
6601
- }
6497
+ items.push(newItem);
6602
6498
  }
6603
6499
  const dashboard = items.find((item) => item.href === "/cms");
6604
6500
  const others = items.filter((item) => item.href !== "/cms");
6605
- others.sort((a, b) => a.label.localeCompare(b.label));
6501
+ others.sort((a, b) => {
6502
+ if (!a.group && b.group) return -1;
6503
+ if (a.group && !b.group) return 1;
6504
+ if (a.group && b.group && a.group !== b.group) return a.group.localeCompare(b.group);
6505
+ return a.label.localeCompare(b.label);
6506
+ });
6606
6507
  items = [...dashboard ? [dashboard] : [], ...others];
6607
6508
  if (schema.icon && !iconImports.includes(schema.icon)) {
6608
6509
  iconImports.push(schema.icon);
@@ -6721,8 +6622,8 @@ function generatePageContent2(schema, cwd, pagesDir, options = {}) {
6721
6622
  const hasCreate = schema.actions?.create ?? false;
6722
6623
  const hasDelete = schema.actions?.delete ?? false;
6723
6624
  const hasFilters = schema.filters && schema.filters.length > 0;
6724
- const lucideIcons = ["Search"];
6725
- if (hasCreate) lucideIcons.push("FilePlus");
6625
+ const lucideIcons = ["ChevronLeft", "Search", "CornerDownLeft"];
6626
+ if (hasCreate) lucideIcons.push("Plus");
6726
6627
  if (hasDelete) lucideIcons.push("Trash2");
6727
6628
  if (hasFilters) lucideIcons.push("Check", "ChevronsUpDown");
6728
6629
  let imports = `'use client'
@@ -6730,7 +6631,8 @@ function generatePageContent2(schema, cwd, pagesDir, options = {}) {
6730
6631
  import { useQueryClient } from '@tanstack/react-query'
6731
6632
  import type { ColumnDef } from '@tanstack/react-table'
6732
6633
  import { ${lucideIcons.join(", ")} } from 'lucide-react'
6733
- ${hasCreate ? "import Link from 'next/link'\n" : ""}import { parseAsString${hasDelete ? ", parseAsArrayOf, parseAsInteger" : ""}, useQueryState } from 'nuqs'
6634
+ ${hasCreate ? "import Link from 'next/link'\n" : ""}import { useRouter } from 'next/navigation'
6635
+ import { parseAsString${hasDelete ? ", parseAsArrayOf, parseAsInteger" : ""}, useQueryState } from 'nuqs'
6734
6636
  import * as React from 'react'
6735
6637
  import { useFormStatus } from 'react-dom'
6736
6638
  ${hasDelete ? "import { toast } from 'sonner'\n" : ""}import { PageHeader } from '@cms/components/shared/page-header'
@@ -6830,7 +6732,7 @@ ${filterLogic}` : ""}`;
6830
6732
  })
6831
6733
  }
6832
6734
  ` : "";
6833
- const filterDropdowns = hasFilters ? schema.filters.map(
6735
+ const _filterDropdowns = hasFilters ? schema.filters.map(
6834
6736
  (f) => ` <Popover open={${f.field}ComboboxOpen} onOpenChange={set${toPascalCase14(f.field)}ComboboxOpen}>
6835
6737
  <PopoverTrigger asChild>
6836
6738
  <Button
@@ -6893,19 +6795,17 @@ ${filterLogic}` : ""}`;
6893
6795
  </PopoverContent>
6894
6796
  </Popover>`
6895
6797
  ).join("\n") : "";
6896
- const searchInput = ` <form action={searchAction} className="flex items-center gap-2">
6897
- <div className="relative">
6898
- <Search className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" />
6899
- <Input
6900
- key={search}
6901
- name="search"
6902
- placeholder="Search ${schema.label.toLowerCase()}..."
6903
- defaultValue={search}
6904
- className="w-64 pl-9"
6905
- />
6906
- </div>
6907
- <SearchButton />
6908
- </form>`;
6798
+ const searchInput = `<form action={searchAction} className="flex items-center gap-2 relative">
6799
+ <Search className="text-muted-foreground/70 pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" />
6800
+ <Input
6801
+ key={search}
6802
+ name="search"
6803
+ placeholder="Search ${schema.label.toLowerCase()}..."
6804
+ defaultValue={search}
6805
+ className="w-64 pl-9 bg-white rounded-lg"
6806
+ />
6807
+ <CornerDownLeft className="text-muted-foreground/70 pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2" />
6808
+ </form>`;
6909
6809
  const deleteButton = hasDelete ? ` {selectedIds.length > 0 && (
6910
6810
  <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
6911
6811
  <AlertDialogTrigger asChild>
@@ -6941,7 +6841,7 @@ ${filterLogic}` : ""}`;
6941
6841
  )}` : "";
6942
6842
  const createButton = hasCreate ? ` <Button asChild>
6943
6843
  <Link href="/cms/${schema.name}/new">
6944
- <FilePlus className="size-3.5 -ml-0.5" strokeWidth={2} />
6844
+ <Plus className="size-3.5 -ml-0.5" strokeWidth={2} />
6945
6845
  Create ${singularizeLabel2(schema.label)}
6946
6846
  </Link>
6947
6847
  </Button>` : "";
@@ -6956,20 +6856,12 @@ ${searchButton}interface ${Plural}PageContentProps<TValue> {
6956
6856
  export function ${Plural}PageContent<TValue>({
6957
6857
  columns
6958
6858
  }: ${Plural}PageContentProps<TValue>) {
6859
+ const router = useRouter()
6959
6860
  const queryClient = useQueryClient()
6960
6861
  ${searchLogic}${deleteLogic}
6961
6862
  return (
6962
6863
  <>
6963
- <div className="flex items-center justify-between bg-card px-6 py-4 border-b">
6964
- <PageHeader title="${schema.label}" description="${schema.description}" />
6965
- <div className="flex items-center gap-2">
6966
- ${filterDropdowns ? `${filterDropdowns}
6967
- ` : ""}${searchInput}
6968
- ${deleteButton}
6969
- ${createButton}
6970
- </div>
6971
- </div>
6972
-
6864
+ <PageHeader title="${schema.label}" back={<Button variant="ghost" size="icon" onClick={() => router.back()}><ChevronLeft /></Button>} search={${searchInput}} actions={<div className="flex items-center gap-2">${deleteButton} ${createButton}</div>} />
6973
6865
  <main className="space-y-6 p-6">
6974
6866
  <${Plural}Table ${tableProps} />
6975
6867
  </main>
@@ -7018,14 +6910,12 @@ export default async function ${PageName}Page() {
7018
6910
  const data = await get${Singular}()
7019
6911
 
7020
6912
  return (
7021
- <div className="flex flex-col w-full pb-20">
7022
- <div className="flex items-center justify-between bg-card px-6 py-4 border-b">
7023
- <PageHeader title="${schema.label}" description="Manage ${schema.label.toLowerCase()}" />
7024
- </div>
7025
- <main className="container mx-auto max-w-5xl p-6">
6913
+ <>
6914
+ <PageHeader title="${schema.label}" />
6915
+ <main className="container mx-auto max-w-5xl p-6 pb-20">
7026
6916
  <${Singular}Form initialData={data} />
7027
6917
  </main>
7028
- </div>
6918
+ </>
7029
6919
  )
7030
6920
  }
7031
6921
  `;
@@ -8059,8 +7949,8 @@ export const { GET, POST } = toNextJsHandler(auth)
8059
7949
  function uploadRouteTemplate() {
8060
7950
  return `import { PutObjectCommand } from '@aws-sdk/client-s3'
8061
7951
  import { BUCKET_NAME, generateFilePath, getPublicUrl, getR2Client } from '@cms/lib/r2'
8062
- import { validateFiles } from '@cms/utils/validation'
8063
7952
  import type { UploadedFile } from '@cms/types'
7953
+ import { validateFiles } from '@cms/utils/validation'
8064
7954
  import { type NextRequest, NextResponse } from 'next/server'
8065
7955
 
8066
7956
  export async function POST(request: NextRequest) {
@@ -8080,27 +7970,22 @@ export async function POST(request: NextRequest) {
8080
7970
  }
8081
7971
 
8082
7972
  if (files.length === 0) {
8083
- return NextResponse.json(
8084
- { success: false, error: 'No files provided' },
8085
- { status: 400 }
8086
- )
7973
+ return NextResponse.json({ success: false, error: 'No files provided' }, { status: 400 })
8087
7974
  }
8088
7975
 
8089
7976
  const validation = validateFiles(files, {
8090
7977
  maxSizeInBytes,
8091
7978
  allowedTypes,
8092
- maxFiles: 10
7979
+ maxFiles: 10,
8093
7980
  })
8094
7981
 
8095
7982
  if (!validation.valid) {
8096
7983
  return NextResponse.json(
8097
7984
  {
8098
7985
  success: false,
8099
- error: validation.errors
8100
- .map((e) => \`\${e.filename}: \${e.error}\`)
8101
- .join('; ')
7986
+ error: validation.errors.map((e) => \`\${e.filename}: \${e.error}\`).join('; '),
8102
7987
  },
8103
- { status: 400 }
7988
+ { status: 400 },
8104
7989
  )
8105
7990
  }
8106
7991
 
@@ -8116,7 +8001,7 @@ export async function POST(request: NextRequest) {
8116
8001
  Key: key,
8117
8002
  Body: buffer,
8118
8003
  ContentType: file.type,
8119
- ContentLength: file.size
8004
+ ContentLength: file.size,
8120
8005
  })
8121
8006
 
8122
8007
  await getR2Client().send(command)
@@ -8126,18 +8011,14 @@ export async function POST(request: NextRequest) {
8126
8011
  url: getPublicUrl(key),
8127
8012
  filename: file.name,
8128
8013
  size: file.size,
8129
- contentType: file.type
8014
+ contentType: file.type,
8130
8015
  })
8131
8016
  }
8132
8017
 
8133
8018
  return NextResponse.json({ success: true, files: uploadedFiles })
8134
8019
  } 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
- )
8020
+ const message = error instanceof Error ? error.message : 'Failed to upload files'
8021
+ return NextResponse.json({ success: false, error: message }, { status: 500 })
8141
8022
  }
8142
8023
  }
8143
8024
  `;
@@ -8217,7 +8098,9 @@ function authClientTemplate() {
8217
8098
  import { createAuthClient } from 'better-auth/react'
8218
8099
 
8219
8100
  export const authClient = createAuthClient({
8220
- baseURL: process.env.NEXT_PUBLIC_BETTERSTART_AUTH_URL || (typeof window !== 'undefined' ? window.location.origin : ''),
8101
+ baseURL:
8102
+ process.env.NEXT_PUBLIC_BETTERSTART_AUTH_URL ||
8103
+ (typeof window !== 'undefined' ? window.location.origin : ''),
8221
8104
  basePath: '/api/cms/auth',
8222
8105
  })
8223
8106
 
@@ -8238,10 +8121,7 @@ export enum UserRole {
8238
8121
  }
8239
8122
 
8240
8123
  export function isUserRole(value: unknown): value is UserRole {
8241
- return (
8242
- typeof value === 'string' &&
8243
- Object.values(UserRole).includes(value as UserRole)
8244
- )
8124
+ return typeof value === 'string' && Object.values(UserRole).includes(value as UserRole)
8245
8125
  }
8246
8126
 
8247
8127
  /**
@@ -8696,184 +8576,121 @@ function hasEnvBetterstartVars(cwd) {
8696
8576
  // src/init/templates/components/cms-globals.ts
8697
8577
  function cmsGlobalsCssTemplate() {
8698
8578
  return `@import "tailwindcss";
8579
+ @import "tw-animate-css";
8580
+ @import "shadcn/tailwind.css";
8699
8581
 
8700
8582
  @custom-variant dark (&:is(.dark *));
8701
8583
 
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
8584
  @theme inline {
8827
8585
  --color-background: var(--background);
8828
8586
  --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
8587
  --font-sans: var(--font-sans);
8861
- --font-mono: var(--font-mono);
8862
- --font-serif: var(--font-serif);
8863
-
8588
+ --font-mono: var(--font-geist-mono);
8589
+ --color-sidebar-ring: var(--sidebar-ring);
8590
+ --color-sidebar-border: var(--sidebar-border);
8591
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
8592
+ --color-sidebar-accent: var(--sidebar-accent);
8593
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
8594
+ --color-sidebar-primary: var(--sidebar-primary);
8595
+ --color-sidebar-foreground: var(--sidebar-foreground);
8596
+ --color-sidebar: var(--sidebar);
8597
+ --color-chart-5: var(--chart-5);
8598
+ --color-chart-4: var(--chart-4);
8599
+ --color-chart-3: var(--chart-3);
8600
+ --color-chart-2: var(--chart-2);
8601
+ --color-chart-1: var(--chart-1);
8602
+ --color-ring: var(--ring);
8603
+ --color-input: var(--input);
8604
+ --color-border: var(--border);
8605
+ --color-destructive: var(--destructive);
8606
+ --color-accent-foreground: var(--accent-foreground);
8607
+ --color-accent: var(--accent);
8608
+ --color-muted-foreground: var(--muted-foreground);
8609
+ --color-muted: var(--muted);
8610
+ --color-secondary-foreground: var(--secondary-foreground);
8611
+ --color-secondary: var(--secondary);
8612
+ --color-primary-foreground: var(--primary-foreground);
8613
+ --color-primary: var(--primary);
8614
+ --color-popover-foreground: var(--popover-foreground);
8615
+ --color-popover: var(--popover);
8616
+ --color-card-foreground: var(--card-foreground);
8617
+ --color-card: var(--card);
8864
8618
  --radius-sm: calc(var(--radius) - 4px);
8865
8619
  --radius-md: calc(var(--radius) - 2px);
8866
8620
  --radius-lg: var(--radius);
8867
8621
  --radius-xl: calc(var(--radius) + 4px);
8622
+ --radius-2xl: calc(var(--radius) + 8px);
8623
+ --radius-3xl: calc(var(--radius) + 12px);
8624
+ --radius-4xl: calc(var(--radius) + 16px);
8625
+ }
8626
+
8627
+ :root {
8628
+ --background: oklch(1 0 0);
8629
+ --foreground: oklch(0.141 0.005 285.823);
8630
+ --card: oklch(1 0 0);
8631
+ --card-foreground: oklch(0.141 0.005 285.823);
8632
+ --popover: oklch(1 0 0);
8633
+ --popover-foreground: oklch(0.141 0.005 285.823);
8634
+ --primary: oklch(0.21 0.006 285.885);
8635
+ --primary-foreground: oklch(0.985 0 0);
8636
+ --secondary: oklch(0.967 0.001 286.375);
8637
+ --secondary-foreground: oklch(0.21 0.006 285.885);
8638
+ --muted: oklch(0.967 0.001 286.375);
8639
+ --muted-foreground: oklch(0.552 0.016 285.938);
8640
+ --accent: oklch(0.967 0.001 286.375);
8641
+ --accent-foreground: oklch(0.21 0.006 285.885);
8642
+ --destructive: oklch(0.577 0.245 27.325);
8643
+ --border: oklch(0.92 0.004 286.32);
8644
+ --input: oklch(0.92 0.004 286.32);
8645
+ --ring: oklch(0.705 0.015 286.067);
8646
+ --chart-1: oklch(0.646 0.222 41.116);
8647
+ --chart-2: oklch(0.6 0.118 184.704);
8648
+ --chart-3: oklch(0.398 0.07 227.392);
8649
+ --chart-4: oklch(0.828 0.189 84.429);
8650
+ --chart-5: oklch(0.769 0.188 70.08);
8651
+ --radius: 0.625rem;
8652
+ --sidebar: oklch(0.985 0 0);
8653
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
8654
+ --sidebar-primary: oklch(0.21 0.006 285.885);
8655
+ --sidebar-primary-foreground: oklch(0.985 0 0);
8656
+ --sidebar-accent: oklch(0.967 0.001 286.375);
8657
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
8658
+ --sidebar-border: oklch(0.92 0.004 286.32);
8659
+ --sidebar-ring: oklch(0.705 0.015 286.067);
8660
+ }
8868
8661
 
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);
8662
+ .dark {
8663
+ --background: oklch(0.141 0.005 285.823);
8664
+ --foreground: oklch(0.985 0 0);
8665
+ --card: oklch(0.21 0.006 285.885);
8666
+ --card-foreground: oklch(0.985 0 0);
8667
+ --popover: oklch(0.21 0.006 285.885);
8668
+ --popover-foreground: oklch(0.985 0 0);
8669
+ --primary: oklch(0.92 0.004 286.32);
8670
+ --primary-foreground: oklch(0.21 0.006 285.885);
8671
+ --secondary: oklch(0.274 0.006 286.033);
8672
+ --secondary-foreground: oklch(0.985 0 0);
8673
+ --muted: oklch(0.274 0.006 286.033);
8674
+ --muted-foreground: oklch(0.705 0.015 286.067);
8675
+ --accent: oklch(0.274 0.006 286.033);
8676
+ --accent-foreground: oklch(0.985 0 0);
8677
+ --destructive: oklch(0.704 0.191 22.216);
8678
+ --border: oklch(1 0 0 / 10%);
8679
+ --input: oklch(1 0 0 / 15%);
8680
+ --ring: oklch(0.552 0.016 285.938);
8681
+ --chart-1: oklch(0.488 0.243 264.376);
8682
+ --chart-2: oklch(0.696 0.17 162.48);
8683
+ --chart-3: oklch(0.769 0.188 70.08);
8684
+ --chart-4: oklch(0.627 0.265 303.9);
8685
+ --chart-5: oklch(0.645 0.246 16.439);
8686
+ --sidebar: oklch(0.21 0.006 285.885);
8687
+ --sidebar-foreground: oklch(0.985 0 0);
8688
+ --sidebar-primary: oklch(0.488 0.243 264.376);
8689
+ --sidebar-primary-foreground: oklch(0.985 0 0);
8690
+ --sidebar-accent: oklch(0.274 0.006 286.033);
8691
+ --sidebar-accent-foreground: oklch(0.985 0 0);
8692
+ --sidebar-border: oklch(1 0 0 / 10%);
8693
+ --sidebar-ring: oklch(0.552 0.016 285.938);
8877
8694
  }
8878
8695
 
8879
8696
  @layer base {
@@ -8881,7 +8698,7 @@ function cmsGlobalsCssTemplate() {
8881
8698
  @apply border-border outline-ring/50;
8882
8699
  }
8883
8700
  body {
8884
- @apply bg-background text-foreground antialiased;
8701
+ @apply bg-background text-foreground;
8885
8702
  }
8886
8703
  }
8887
8704
 
@@ -8914,6 +8731,14 @@ function cmsGlobalsCssTemplate() {
8914
8731
  function dataTableTemplate() {
8915
8732
  return `'use client'
8916
8733
 
8734
+ import {
8735
+ Table,
8736
+ TableBody,
8737
+ TableCell,
8738
+ TableHead,
8739
+ TableHeader,
8740
+ TableRow,
8741
+ } from '@cms/components/ui/table'
8917
8742
  import {
8918
8743
  type ColumnDef,
8919
8744
  type ColumnFiltersState,
@@ -8923,20 +8748,11 @@ import {
8923
8748
  getPaginationRowModel,
8924
8749
  getSortedRowModel,
8925
8750
  type SortingState,
8926
- type Table as TanstackTable,
8927
8751
  useReactTable,
8928
- type VisibilityState
8752
+ type VisibilityState,
8929
8753
  } from '@tanstack/react-table'
8930
8754
  import { parseAsInteger, useQueryState } from 'nuqs'
8931
8755
  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
8756
  import { DataTablePagination } from './data-table-pagination'
8941
8757
 
8942
8758
  interface DataTableProps<TData, TValue> {
@@ -8962,7 +8778,7 @@ export function DataTable<TData, TValue>({
8962
8778
  selectedIds,
8963
8779
  onSelectedIdsChange,
8964
8780
  meta,
8965
- getId = (row) => (row as { id: number }).id
8781
+ getId = (row) => (row as { id: number }).id,
8966
8782
  }: DataTableProps<TData, TValue>) {
8967
8783
  const [sorting, setSorting] = React.useState<SortingState>([])
8968
8784
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
@@ -8985,27 +8801,38 @@ export function DataTable<TData, TValue>({
8985
8801
  }, [selectedIds, data, getId])
8986
8802
 
8987
8803
  const handleRowSelectionChange = React.useCallback(
8988
- (updater: Record<string, boolean> | ((old: Record<string, boolean>) => Record<string, boolean>)) => {
8804
+ (
8805
+ updater:
8806
+ | Record<string, boolean>
8807
+ | ((old: Record<string, boolean>) => Record<string, boolean>),
8808
+ ) => {
8989
8809
  if (!onSelectedIdsChange) return
8990
8810
  const newSelection = typeof updater === 'function' ? updater(rowSelection) : updater
8991
8811
  const newIds = Object.keys(newSelection)
8992
8812
  .filter((key) => newSelection[key])
8993
- .map((key) => getId(data[Number.parseInt(key)]))
8813
+ .map((key) => getId(data[Number.parseInt(key, 10)]))
8994
8814
  .filter(Boolean)
8995
8815
  onSelectedIdsChange(newIds)
8996
8816
  },
8997
- [data, rowSelection, onSelectedIdsChange, getId]
8817
+ [data, rowSelection, onSelectedIdsChange, getId],
8998
8818
  )
8999
8819
 
9000
8820
  const handlePaginationChange = React.useCallback(
9001
- (updater: { pageIndex: number; pageSize: number } | ((old: { pageIndex: number; pageSize: number }) => { pageIndex: number; pageSize: number })) => {
8821
+ (
8822
+ updater:
8823
+ | { pageIndex: number; pageSize: number }
8824
+ | ((old: { pageIndex: number; pageSize: number }) => {
8825
+ pageIndex: number
8826
+ pageSize: number
8827
+ }),
8828
+ ) => {
9002
8829
  const current = { pageIndex, pageSize: effectivePageSize }
9003
8830
  const next = typeof updater === 'function' ? updater(current) : updater
9004
8831
  React.startTransition(() => {
9005
8832
  setPageIndex(next.pageIndex)
9006
8833
  })
9007
8834
  },
9008
- [pageIndex, effectivePageSize, setPageIndex]
8835
+ [pageIndex, effectivePageSize, setPageIndex],
9009
8836
  )
9010
8837
 
9011
8838
  const table = useReactTable({
@@ -9028,9 +8855,9 @@ export function DataTable<TData, TValue>({
9028
8855
  rowSelection,
9029
8856
  pagination: {
9030
8857
  pageIndex,
9031
- pageSize: effectivePageSize
9032
- }
9033
- }
8858
+ pageSize: effectivePageSize,
8859
+ },
8860
+ },
9034
8861
  })
9035
8862
 
9036
8863
  return (
@@ -9097,23 +8924,23 @@ export type { DataTableProps }
9097
8924
  function dataTablePaginationTemplate() {
9098
8925
  return `'use client'
9099
8926
 
9100
- import type { Table } from '@tanstack/react-table'
9101
- import * as React from 'react'
9102
8927
  import { Button } from '@cms/components/ui/button'
9103
8928
  import {
9104
8929
  Select,
9105
8930
  SelectContent,
9106
8931
  SelectItem,
9107
8932
  SelectTrigger,
9108
- SelectValue
8933
+ SelectValue,
9109
8934
  } from '@cms/components/ui/select'
8935
+ import type { Table } from '@tanstack/react-table'
8936
+ import * as React from 'react'
9110
8937
 
9111
8938
  const PAGE_SIZE_OPTIONS = [
9112
8939
  { value: '10', label: '10' },
9113
8940
  { value: '20', label: '20' },
9114
8941
  { value: '50', label: '50' },
9115
8942
  { value: '100', label: '100' },
9116
- { value: 'all', label: 'All' }
8943
+ { value: 'all', label: 'All' },
9117
8944
  ]
9118
8945
 
9119
8946
  interface DataTablePaginationProps<TData> {
@@ -9125,7 +8952,7 @@ interface DataTablePaginationProps<TData> {
9125
8952
  export function DataTablePagination<TData>({
9126
8953
  table,
9127
8954
  pageSize,
9128
- setPageSize
8955
+ setPageSize,
9129
8956
  }: DataTablePaginationProps<TData>) {
9130
8957
  const handlePageSizeChange = React.useCallback(
9131
8958
  (value: string) => {
@@ -9138,7 +8965,7 @@ export function DataTablePagination<TData>({
9138
8965
  table.setPageIndex(0)
9139
8966
  })
9140
8967
  },
9141
- [setPageSize, table]
8968
+ [setPageSize, table],
9142
8969
  )
9143
8970
 
9144
8971
  return (
@@ -9192,11 +9019,11 @@ export function DataTablePagination<TData>({
9192
9019
  function dataTableToolbarTemplate() {
9193
9020
  return `'use client'
9194
9021
 
9195
- import { ArrowUpDown, Save, Search } from 'lucide-react'
9196
- import * as React from 'react'
9197
- import { useFormStatus } from 'react-dom'
9198
9022
  import { Button } from '@cms/components/ui/button'
9199
9023
  import { Input } from '@cms/components/ui/input'
9024
+ import { ArrowUpDown, Save, Search } from 'lucide-react'
9025
+ import type * as React from 'react'
9026
+ import { useFormStatus } from 'react-dom'
9200
9027
 
9201
9028
  function SearchButton() {
9202
9029
  const { pending } = useFormStatus()
@@ -9218,7 +9045,7 @@ export function DataTableToolbar({
9218
9045
  search,
9219
9046
  onSearch,
9220
9047
  searchPlaceholder = 'Search...',
9221
- children
9048
+ children,
9222
9049
  }: DataTableToolbarProps) {
9223
9050
  return (
9224
9051
  <div className="flex items-center gap-2">
@@ -9255,7 +9082,7 @@ export function ReorderControls({
9255
9082
  onSave,
9256
9083
  onCancel,
9257
9084
  hasChanges,
9258
- isSaving
9085
+ isSaving,
9259
9086
  }: ReorderControlsProps) {
9260
9087
  return (
9261
9088
  <div className="flex items-center gap-2">
@@ -9270,26 +9097,14 @@ export function ReorderControls({
9270
9097
  </Button>
9271
9098
  {reorderMode && (
9272
9099
  <>
9273
- <Button
9274
- variant="default"
9275
- size="sm"
9276
- onClick={onSave}
9277
- disabled={!hasChanges || isSaving}
9278
- >
9100
+ <Button variant="default" size="sm" onClick={onSave} disabled={!hasChanges || isSaving}>
9279
9101
  <Save className="size-4 mr-1" />
9280
9102
  {isSaving ? 'Saving...' : 'Save'}
9281
9103
  </Button>
9282
- <Button
9283
- variant="outline"
9284
- size="sm"
9285
- onClick={onCancel}
9286
- disabled={isSaving}
9287
- >
9104
+ <Button variant="outline" size="sm" onClick={onCancel} disabled={isSaving}>
9288
9105
  Cancel
9289
9106
  </Button>
9290
- {hasChanges && (
9291
- <span className="text-sm text-muted-foreground">Unsaved changes</span>
9292
- )}
9107
+ {hasChanges && <span className="text-sm text-muted-foreground">Unsaved changes</span>}
9293
9108
  </>
9294
9109
  )}
9295
9110
  </div>
@@ -9302,7 +9117,6 @@ export function ReorderControls({
9302
9117
  function cmsHeaderTemplate() {
9303
9118
  return `'use client'
9304
9119
 
9305
- import { CmsSearch } from '@cms/components/layout/cms-search'
9306
9120
  import { Button } from '@cms/components/ui/button'
9307
9121
  import { SidebarTrigger, useSidebar } from '@cms/components/ui/sidebar'
9308
9122
  import { useTheme } from '@cms/hooks/use-cms-theme'
@@ -9313,11 +9127,10 @@ export function CmsHeader() {
9313
9127
  const { state } = useSidebar()
9314
9128
 
9315
9129
  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">
9130
+ <header className="flex h-14 shrink-0 items-center gap-2 border-b border-border w-full sticky top-0 z-50">
9317
9131
  <div className="flex items-center px-5 gap-1 flex-1 w-full justify-between">
9318
9132
  <div className="flex items-center gap-2 w-full">
9319
9133
  {state === 'collapsed' && <SidebarTrigger />}
9320
- <CmsSearch />
9321
9134
  </div>
9322
9135
  <div className="flex items-center gap-2 ml-auto">
9323
9136
  <Button
@@ -9337,19 +9150,49 @@ export function CmsHeader() {
9337
9150
  `;
9338
9151
  }
9339
9152
 
9340
- // src/init/templates/components/layout/cms-providers.ts
9341
- function cmsProvidersTemplate() {
9153
+ // src/init/templates/components/layout/cms-nav-link.ts
9154
+ function cmsNavLinkTemplate() {
9342
9155
  return `'use client'
9343
9156
 
9344
- import { useState } from 'react'
9345
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
9346
- import { CmsThemeProvider } from '@cms/hooks/use-cms-theme'
9347
- import { Toaster } from '@cms/components/ui/sonner'
9348
- import { NuqsAdapter } from 'nuqs/adapters/next/app'
9157
+ import { SidebarMenuButton } from '@cms/components/ui/sidebar'
9158
+ import Link from 'next/link'
9159
+ import { usePathname } from 'next/navigation'
9349
9160
 
9350
- export function CmsProviders({ children }: { children: React.ReactNode }) {
9351
- const [queryClient] = useState(
9352
- () =>
9161
+ export function CmsNavLink({
9162
+ href,
9163
+ children,
9164
+ }: {
9165
+ href: string
9166
+ children: React.ReactNode
9167
+ }) {
9168
+ const pathname = usePathname()
9169
+ const isActive =
9170
+ href === '/cms'
9171
+ ? pathname === '/cms'
9172
+ : pathname === href || pathname.startsWith(href + '/')
9173
+
9174
+ return (
9175
+ <SidebarMenuButton asChild isActive={isActive}>
9176
+ <Link href={href}>{children}</Link>
9177
+ </SidebarMenuButton>
9178
+ )
9179
+ }
9180
+ `;
9181
+ }
9182
+
9183
+ // src/init/templates/components/layout/cms-providers.ts
9184
+ function cmsProvidersTemplate() {
9185
+ return `'use client'
9186
+
9187
+ import { Toaster } from '@cms/components/ui/sonner'
9188
+ import { CmsThemeProvider } from '@cms/hooks/use-cms-theme'
9189
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
9190
+ import { NuqsAdapter } from 'nuqs/adapters/next/app'
9191
+ import { useState } from 'react'
9192
+
9193
+ export function CmsProviders({ children }: { children: React.ReactNode }) {
9194
+ const [queryClient] = useState(
9195
+ () =>
9353
9196
  new QueryClient({
9354
9197
  defaultOptions: {
9355
9198
  queries: {
@@ -9385,15 +9228,15 @@ export const CmsSearch = () => {
9385
9228
  <div className="flex items-center gap-2 relative w-full max-w-[240px]">
9386
9229
  <Button
9387
9230
  variant="outline"
9388
- className="w-full text-left items-center pr-1! rounded-full"
9389
- size="sm"
9231
+ className="w-full text-left items-center pr-1.5! py-0 rounded-lg bg-white"
9232
+ size="lg"
9390
9233
  >
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...
9234
+ <Search className="shrink-0 size-3.5 -ml-0.5 text-muted-foreground/70" />
9235
+ <span className="w-full font-normal text-sm text-muted-foreground/70 [text-box-trim:trim-both]">
9236
+ Quick search...
9394
9237
  </span>
9395
9238
  <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} />
9239
+ <Command className="size-3! text-muted-foreground" />
9397
9240
  <span className="font-mono text-xs font-medium">K</span>
9398
9241
  </div>
9399
9242
  </Button>
@@ -9407,123 +9250,110 @@ CmsSearch.displayName = 'CmsSearch'
9407
9250
 
9408
9251
  // src/init/templates/components/layout/cms-sidebar.ts
9409
9252
  function cmsSidebarTemplate() {
9410
- return `import { getSetting } from '@/cms/lib/actions/settings'
9411
- import { getSession } from '@cms/auth/middleware'
9253
+ return `import { getSession } from '@cms/auth/middleware'
9412
9254
  import { Avatar, AvatarFallback, AvatarImage } from '@cms/components/ui/avatar'
9413
- import {
9414
- Collapsible,
9415
- CollapsibleContent,
9416
- CollapsibleTrigger,
9417
- } from '@cms/components/ui/collapsible'
9418
9255
  import {
9419
9256
  Sidebar,
9420
9257
  SidebarContent,
9421
9258
  SidebarFooter,
9259
+ SidebarGroup,
9260
+ SidebarGroupLabel,
9422
9261
  SidebarHeader,
9423
9262
  SidebarMenu,
9424
- SidebarMenuButton,
9425
9263
  SidebarMenuItem,
9426
- SidebarMenuSub,
9427
- SidebarMenuSubButton,
9428
- SidebarMenuSubItem,
9429
- SidebarRail,
9430
- SidebarTrigger,
9431
9264
  } from '@cms/components/ui/sidebar'
9432
9265
  import { cms } from '@cms/data/cms'
9433
9266
  import { type CmsNavigationItem, cmsNavigation } from '@cms/data/navigation'
9434
- import { ChevronRight, Settings, Users } from 'lucide-react'
9267
+ import { Settings, Users } from 'lucide-react'
9435
9268
  import Link from 'next/link'
9269
+ import { Fragment } from 'react'
9270
+ import { getSetting } from '@/cms/lib/actions/settings'
9271
+ import { CmsNavLink } from './cms-nav-link'
9272
+ import { CmsSearch } from './cms-search'
9436
9273
 
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
- )
9274
+ function groupNavItems(items: CmsNavigationItem[]) {
9275
+ const groups: { label: string | null; items: CmsNavigationItem[] }[] = []
9276
+ const groupMap = new Map<string | null, CmsNavigationItem[]>()
9277
+
9278
+ for (const item of items) {
9279
+ const key = item.group ?? null
9280
+ if (!groupMap.has(key)) {
9281
+ const arr: CmsNavigationItem[] = []
9282
+ groupMap.set(key, arr)
9283
+ groups.push({ label: key, items: arr })
9284
+ }
9285
+ groupMap.get(key)!.push(item)
9466
9286
  }
9467
9287
 
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
- )
9288
+ return groups
9478
9289
  }
9479
9290
 
9480
9291
  export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
9481
9292
  const session = await getSession()
9482
9293
  const settings = await getSetting()
9483
9294
  const user = session?.user ?? null
9295
+ const groups = groupNavItems(cmsNavigation)
9484
9296
 
9485
9297
  return (
9486
9298
  <Sidebar collapsible="icon" {...props}>
9487
9299
  <SidebarHeader className="border-b border-border h-14 items-center flex w-full">
9488
9300
  <div className="flex items-center gap-2 w-full relative h-full">
9489
9301
  <Link href="/cms" className="flex items-center gap-2 w-full">
9490
- <Avatar className="size-6.5">
9302
+ <Avatar className="size-8">
9491
9303
  <AvatarImage src={'/favicon.ico'} />
9492
- <AvatarFallback className="text-sm font-semibold">
9304
+ <AvatarFallback className="text-sm font-semibold text-foreground">
9493
9305
  {settings?.siteName?.charAt(0) ?? cms.name?.charAt(0)}
9494
9306
  </AvatarFallback>
9495
9307
  </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>
9308
+ <div className="flex text-foreground items-center gap-1 w-full group-data-[collapsible=icon]:hidden">
9309
+ <span className="text-sm font-medium line-clamp-1">
9310
+ {settings?.siteName ?? cms.name}
9311
+ </span>
9498
9312
  </div>
9499
9313
  </Link>
9500
- <SidebarTrigger className="hidden md:flex" />
9501
9314
  </div>
9502
9315
  </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!" />
9316
+ <SidebarContent>
9317
+ <SidebarGroup className="pb-1">
9318
+ <CmsSearch />
9319
+ </SidebarGroup>
9320
+
9321
+ {groups.map((group) => (
9322
+ <Fragment key={group.label ?? '_ungrouped'}>
9323
+ <SidebarGroup>
9324
+ {group.label && <SidebarGroupLabel>{group.label}</SidebarGroupLabel>}
9325
+ <SidebarMenu>
9326
+ {group.items.map((item) => (
9327
+ <SidebarMenuItem key={item.href}>
9328
+ <CmsNavLink href={item.href}>
9329
+ {item.icon && (
9330
+ <item.icon className="text-muted-foreground" absoluteStrokeWidth />
9331
+ )}
9332
+ <span>{item.label}</span>
9333
+ </CmsNavLink>
9334
+ </SidebarMenuItem>
9335
+ ))}
9336
+ </SidebarMenu>
9337
+ </SidebarGroup>
9338
+ </Fragment>
9339
+ ))}
9340
+
9341
+ <SidebarGroup className="mt-auto">
9342
+ <SidebarMenu>
9343
+ <SidebarMenuItem>
9344
+ <CmsNavLink href="/cms/users">
9345
+ <Users />
9514
9346
  <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!" />
9347
+ </CmsNavLink>
9348
+ </SidebarMenuItem>
9349
+ <SidebarMenuItem>
9350
+ <CmsNavLink href="/cms/settings">
9351
+ <Settings />
9522
9352
  <span>Settings</span>
9523
- </Link>
9524
- </SidebarMenuButton>
9525
- </SidebarMenuItem>
9526
- </SidebarMenu>
9353
+ </CmsNavLink>
9354
+ </SidebarMenuItem>
9355
+ </SidebarMenu>
9356
+ </SidebarGroup>
9527
9357
  </SidebarContent>
9528
9358
  <SidebarFooter>
9529
9359
  {user && (
@@ -9535,7 +9365,6 @@ export async function CmsSidebar(props: React.ComponentProps<typeof Sidebar>) {
9535
9365
  </div>
9536
9366
  )}
9537
9367
  </SidebarFooter>
9538
- <SidebarRail />
9539
9368
  </Sidebar>
9540
9369
  )
9541
9370
  }
@@ -9555,7 +9384,7 @@ import {
9555
9384
  AlertDialogFooter,
9556
9385
  AlertDialogHeader,
9557
9386
  AlertDialogTitle,
9558
- AlertDialogTrigger
9387
+ AlertDialogTrigger,
9559
9388
  } from '@cms/components/ui/alert-dialog'
9560
9389
  import { Button } from '@cms/components/ui/button'
9561
9390
  import { Trash2 } from 'lucide-react'
@@ -9577,7 +9406,7 @@ export function DeleteDialog({
9577
9406
  isPending = false,
9578
9407
  title = 'Are you sure?',
9579
9408
  description = 'This action cannot be undone. This will permanently delete this item.',
9580
- trigger
9409
+ trigger,
9581
9410
  }: DeleteDialogProps) {
9582
9411
  return (
9583
9412
  <AlertDialog open={open} onOpenChange={onOpenChange}>
@@ -9626,18 +9455,24 @@ export function DeleteButton({ onClick, label = 'item', count }: DeleteButtonPro
9626
9455
  function pageHeaderTemplate() {
9627
9456
  return `interface PageHeaderProps {
9628
9457
  title: string
9629
- description: string
9630
9458
  children?: React.ReactNode
9459
+ search?: React.ReactNode
9460
+ actions?: React.ReactNode
9461
+ back?: React.ReactNode
9631
9462
  }
9632
9463
 
9633
- export function PageHeader({ title, description, children }: PageHeaderProps) {
9464
+ export function PageHeader({ title, children, search, actions, back }: PageHeaderProps) {
9634
9465
  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>
9466
+ <div className="grid grid-cols-3 items-center justify-between w-full h-14 px-4 border-b border-border">
9467
+ <div className="flex items-center justify-start gap-2 w-full">{back && back}</div>
9468
+ <div className="flex items-center justify-center gap-2">
9469
+ <h2 className="text-sm font-medium tracking-tight">{title}</h2>
9470
+ </div>
9471
+ <div className="flex items-center justify-end gap-2">
9472
+ {children && children}
9473
+ {search && search}
9474
+ {actions && actions}
9639
9475
  </div>
9640
- {children && <div className="flex items-center gap-2">{children}</div>}
9641
9476
  </div>
9642
9477
  )
9643
9478
  }
@@ -9656,7 +9491,7 @@ const statusStyles: Record<StatusVariant, string> = {
9656
9491
  success: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300',
9657
9492
  warning: 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300',
9658
9493
  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'
9494
+ info: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300',
9660
9495
  }
9661
9496
 
9662
9497
  interface StatusBadgeProps extends Omit<BadgeProps, 'variant'> {
@@ -9674,7 +9509,11 @@ export function StatusBadge({ status, className, ...props }: StatusBadgeProps) {
9674
9509
  }
9675
9510
 
9676
9511
  /** Map boolean values to status badges */
9677
- export function BooleanBadge({ value, trueLabel = 'Yes', falseLabel = 'No' }: {
9512
+ export function BooleanBadge({
9513
+ value,
9514
+ trueLabel = 'Yes',
9515
+ falseLabel = 'No',
9516
+ }: {
9678
9517
  value: boolean
9679
9518
  trueLabel?: string
9680
9519
  falseLabel?: string
@@ -9698,22 +9537,44 @@ function cmsDataTemplate(projectName) {
9698
9537
 
9699
9538
  // src/init/templates/data/navigation.ts
9700
9539
  function navigationDataTemplate() {
9701
- return `import { House } from 'lucide-react'
9702
- import type { LucideIcon } from 'lucide-react'
9540
+ return `import type { LucideIcon } from 'lucide-react'
9541
+ import { ChartSpline, FileText, House, ImagePlay, Tag } from 'lucide-react'
9703
9542
 
9704
9543
  export interface CmsNavigationItem {
9705
9544
  label: string
9706
9545
  href: string
9707
9546
  icon?: LucideIcon
9708
- children?: CmsNavigationItem[]
9547
+ group?: string
9709
9548
  }
9710
9549
 
9711
9550
  export const cmsNavigation: CmsNavigationItem[] = [
9712
9551
  {
9713
- label: 'Dashboard',
9552
+ label: 'Overview',
9714
9553
  href: '/cms',
9715
- icon: House
9716
- }
9554
+ icon: House,
9555
+ },
9556
+ {
9557
+ label: 'Analytics',
9558
+ href: '/cms/analytics',
9559
+ icon: ChartSpline,
9560
+ },
9561
+ {
9562
+ label: 'Media',
9563
+ href: '/cms/media',
9564
+ icon: ImagePlay,
9565
+ },
9566
+ {
9567
+ label: 'Categories',
9568
+ href: '/cms/categories',
9569
+ icon: Tag,
9570
+ group: 'Blog',
9571
+ },
9572
+ {
9573
+ label: 'Posts',
9574
+ href: '/cms/posts',
9575
+ icon: FileText,
9576
+ group: 'Blog',
9577
+ },
9717
9578
  ]
9718
9579
  `;
9719
9580
  }
@@ -9787,14 +9648,10 @@ export function CmsThemeProvider({ children }: { children: React.ReactNode }) {
9787
9648
 
9788
9649
  const value = React.useMemo(
9789
9650
  () => ({ theme, setTheme, resolvedTheme: resolved }),
9790
- [theme, setTheme, resolved]
9651
+ [theme, setTheme, resolved],
9791
9652
  )
9792
9653
 
9793
- return (
9794
- <CmsThemeContext.Provider value={value}>
9795
- {children}
9796
- </CmsThemeContext.Provider>
9797
- )
9654
+ return <CmsThemeContext.Provider value={value}>{children}</CmsThemeContext.Provider>
9798
9655
  }
9799
9656
 
9800
9657
  export function useTheme(): ThemeContext {
@@ -9834,11 +9691,11 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
9834
9691
  if (result.success && result.files) {
9835
9692
  const images: EditorImageUploadResult[] = result.files.map((f) => ({
9836
9693
  url: f.url,
9837
- filename: f.filename
9694
+ filename: f.filename,
9838
9695
  }))
9839
9696
  onImagesUploadedRef.current(images)
9840
9697
  }
9841
- }
9698
+ },
9842
9699
  })
9843
9700
 
9844
9701
  const isUploading = mutation.isPending
@@ -9849,21 +9706,19 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
9849
9706
  if (imageFiles.length === 0) return
9850
9707
  upload(imageFiles, 'images')
9851
9708
  },
9852
- [upload]
9709
+ [upload],
9853
9710
  )
9854
9711
 
9855
9712
  const handleDrop = React.useCallback(
9856
9713
  (e: React.DragEvent) => {
9857
9714
  e.preventDefault()
9858
9715
  e.stopPropagation()
9859
- const files = Array.from(e.dataTransfer.files).filter((f) =>
9860
- f.type.startsWith('image/')
9861
- )
9716
+ const files = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith('image/'))
9862
9717
  if (files.length > 0) {
9863
9718
  uploadImages(files)
9864
9719
  }
9865
9720
  },
9866
- [uploadImages]
9721
+ [uploadImages],
9867
9722
  )
9868
9723
 
9869
9724
  const openFilePicker = React.useCallback(() => {
@@ -9878,7 +9733,7 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
9878
9733
  }
9879
9734
  e.target.value = ''
9880
9735
  },
9881
- [uploadImages]
9736
+ [uploadImages],
9882
9737
  )
9883
9738
 
9884
9739
  return {
@@ -9888,7 +9743,7 @@ export function useEditorImageUpload({ onImagesUploaded }: UseEditorImageUploadO
9888
9743
  handleDrop,
9889
9744
  openFilePicker,
9890
9745
  fileInputRef,
9891
- handleFileInputChange
9746
+ handleFileInputChange,
9892
9747
  }
9893
9748
  }
9894
9749
  `;
@@ -9912,7 +9767,7 @@ export function useLocalStorage<T>(key: string) {
9912
9767
  // Silent failure for localStorage access errors
9913
9768
  }
9914
9769
  },
9915
- [prefixedKey]
9770
+ [prefixedKey],
9916
9771
  )
9917
9772
 
9918
9773
  const getItem = React.useCallback((): T | null => {
@@ -9945,18 +9800,36 @@ export function useLocalStorage<T>(key: string) {
9945
9800
  `;
9946
9801
  }
9947
9802
 
9803
+ // src/init/templates/hooks/use-mobile.ts
9804
+ function useMobileHookTemplate() {
9805
+ return `import * as React from 'react'
9806
+
9807
+ const MOBILE_BREAKPOINT = 768
9808
+
9809
+ export function useIsMobile() {
9810
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
9811
+
9812
+ React.useEffect(() => {
9813
+ const mql = window.matchMedia(\`(max-width: \${MOBILE_BREAKPOINT - 1}px)\`)
9814
+ const onChange = () => {
9815
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
9816
+ }
9817
+ mql.addEventListener('change', onChange)
9818
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
9819
+ return () => mql.removeEventListener('change', onChange)
9820
+ }, [])
9821
+
9822
+ return !!isMobile
9823
+ }
9824
+ `;
9825
+ }
9826
+
9948
9827
  // src/init/templates/hooks/use-upload.ts
9949
9828
  function useUploadHookTemplate() {
9950
9829
  return `'use client'
9951
9830
 
9952
- import type {
9953
- UploadFileResult,
9954
- UploadProgress
9955
- } from '@cms/types'
9956
- import {
9957
- type FileValidationConfig,
9958
- validateFiles
9959
- } from '@cms/utils/validation'
9831
+ import type { UploadFileResult, UploadProgress } from '@cms/types'
9832
+ import { type FileValidationConfig, validateFiles } from '@cms/utils/validation'
9960
9833
  import { type UseMutationResult, useMutation } from '@tanstack/react-query'
9961
9834
  import * as React from 'react'
9962
9835
 
@@ -10004,7 +9877,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10004
9877
  onProgress,
10005
9878
  onSuccess,
10006
9879
  onError,
10007
- prefix: defaultPrefix
9880
+ prefix: defaultPrefix,
10008
9881
  } = options
10009
9882
 
10010
9883
  const validationConfig = React.useMemo<FileValidationConfig>(() => {
@@ -10014,7 +9887,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10014
9887
  const parsedTypes = parseAcceptTypes(accept)
10015
9888
  if (config.allowedTypes && config.allowedTypes.length > 0) {
10016
9889
  config.allowedTypes = [...config.allowedTypes, ...parsedTypes].filter(
10017
- (v, i, a) => a.indexOf(v) === i
9890
+ (v, i, a) => a.indexOf(v) === i,
10018
9891
  )
10019
9892
  } else {
10020
9893
  config.allowedTypes = parsedTypes
@@ -10037,7 +9910,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10037
9910
  filename: file.name,
10038
9911
  progress: 0,
10039
9912
  loaded: 0,
10040
- total: file.size
9913
+ total: file.size,
10041
9914
  }))
10042
9915
  setProgress(initialProgress)
10043
9916
  onProgress?.(initialProgress)
@@ -10059,9 +9932,9 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10059
9932
  return {
10060
9933
  ...p,
10061
9934
  progress: newProgress,
10062
- loaded: Math.floor((p.total * newProgress) / 100)
9935
+ loaded: Math.floor((p.total * newProgress) / 100),
10063
9936
  }
10064
- })
9937
+ }),
10065
9938
  )
10066
9939
  }, 200)
10067
9940
 
@@ -10075,7 +9948,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10075
9948
 
10076
9949
  const response = await fetch('/api/cms/upload', {
10077
9950
  method: 'POST',
10078
- body: formData
9951
+ body: formData,
10079
9952
  })
10080
9953
 
10081
9954
  const result = (await response.json()) as UploadFileResult
@@ -10085,7 +9958,7 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10085
9958
  filename: file.name,
10086
9959
  progress: 100,
10087
9960
  loaded: file.size,
10088
- total: file.size
9961
+ total: file.size,
10089
9962
  }))
10090
9963
  setProgress(completeProgress)
10091
9964
  onProgress?.(completeProgress)
@@ -10107,14 +9980,14 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10107
9980
  onError: (error) => {
10108
9981
  onError?.(error)
10109
9982
  setProgress([])
10110
- }
9983
+ },
10111
9984
  })
10112
9985
 
10113
9986
  const upload = React.useCallback(
10114
9987
  (files: File[], prefix?: string) => {
10115
9988
  mutation.mutate({ files, prefix })
10116
9989
  },
10117
- [mutation]
9990
+ [mutation],
10118
9991
  )
10119
9992
 
10120
9993
  const validate = React.useCallback(
@@ -10122,10 +9995,10 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
10122
9995
  const result = validateFiles(files, validationConfig)
10123
9996
  return {
10124
9997
  valid: result.valid,
10125
- errors: result.errors.map((e) => \`\${e.filename}: \${e.error}\`)
9998
+ errors: result.errors.map((e) => \`\${e.filename}: \${e.error}\`),
10126
9999
  }
10127
10000
  },
10128
- [validationConfig]
10001
+ [validationConfig],
10129
10002
  )
10130
10003
 
10131
10004
  return { mutation, progress, upload, validate }
@@ -10145,7 +10018,7 @@ export function useUsers() {
10145
10018
  return useQuery<UsersResponse>({
10146
10019
  queryKey: ['users'],
10147
10020
  queryFn: () => getUsers(),
10148
- staleTime: 0
10021
+ staleTime: 0,
10149
10022
  })
10150
10023
  }
10151
10024
  `;
@@ -10155,9 +10028,9 @@ export function useUsers() {
10155
10028
  function formSettingsActionTemplate() {
10156
10029
  return `'use server'
10157
10030
 
10158
- import { eq } from 'drizzle-orm'
10159
10031
  import db from '@cms/db'
10160
10032
  import { formSettings } from '@cms/db/schema'
10033
+ import { eq } from 'drizzle-orm'
10161
10034
 
10162
10035
  export interface FormSettingsData {
10163
10036
  id: number
@@ -10181,9 +10054,7 @@ export interface FormSettingsResult {
10181
10054
  settings?: FormSettingsData
10182
10055
  }
10183
10056
 
10184
- export async function getFormSettings(
10185
- formName: string
10186
- ): Promise<FormSettingsData | null> {
10057
+ export async function getFormSettings(formName: string): Promise<FormSettingsData | null> {
10187
10058
  try {
10188
10059
  const [settings] = await db
10189
10060
  .select()
@@ -10199,7 +10070,7 @@ export async function getFormSettings(
10199
10070
 
10200
10071
  export async function upsertFormSettings(
10201
10072
  formName: string,
10202
- data: UpsertFormSettingsInput
10073
+ data: UpsertFormSettingsInput,
10203
10074
  ): Promise<FormSettingsResult> {
10204
10075
  try {
10205
10076
  const existing = await getFormSettings(formName)
@@ -10230,8 +10101,7 @@ export async function upsertFormSettings(
10230
10101
  console.error(\`Error upserting form settings for \${formName}:\`, error)
10231
10102
  return {
10232
10103
  success: false,
10233
- error:
10234
- error instanceof Error ? error.message : 'Failed to save form settings',
10104
+ error: error instanceof Error ? error.message : 'Failed to save form settings',
10235
10105
  }
10236
10106
  }
10237
10107
  }
@@ -10247,7 +10117,7 @@ export async function getAllFormSettings(): Promise<FormSettingsData[]> {
10247
10117
  }
10248
10118
 
10249
10119
  export async function testFormWebhook(
10250
- formName: string
10120
+ formName: string,
10251
10121
  ): Promise<{ success: boolean; error?: string }> {
10252
10122
  try {
10253
10123
  const settings = await getFormSettings(formName)
@@ -10278,8 +10148,7 @@ export async function testFormWebhook(
10278
10148
  } catch (error) {
10279
10149
  return {
10280
10150
  success: false,
10281
- error:
10282
- error instanceof Error ? error.message : 'Failed to send test webhook',
10151
+ error: error instanceof Error ? error.message : 'Failed to send test webhook',
10283
10152
  }
10284
10153
  }
10285
10154
  }
@@ -10448,10 +10317,10 @@ export async function uploadImageFromUrl(
10448
10317
  function usersActionTemplate() {
10449
10318
  return `'use server'
10450
10319
 
10320
+ import { auth } from '@cms/auth'
10451
10321
  import db from '@cms/db'
10452
- import { user, account } from '@cms/db/schema'
10322
+ import { user } from '@cms/db/schema'
10453
10323
  import { eq } from 'drizzle-orm'
10454
- import { auth } from '@cms/auth'
10455
10324
 
10456
10325
  export interface UserData {
10457
10326
  id: string
@@ -10564,15 +10433,9 @@ export async function getUsers(): Promise<UsersResponse> {
10564
10433
  /**
10565
10434
  * Update a user's role
10566
10435
  */
10567
- export async function updateUserRole(
10568
- userId: string,
10569
- role: string,
10570
- ): Promise<UpdateUserRoleResult> {
10436
+ export async function updateUserRole(userId: string, role: string): Promise<UpdateUserRoleResult> {
10571
10437
  try {
10572
- await db
10573
- .update(user)
10574
- .set({ role, updatedAt: new Date() })
10575
- .where(eq(user.id, userId))
10438
+ await db.update(user).set({ role, updatedAt: new Date() }).where(eq(user.id, userId))
10576
10439
 
10577
10440
  return { success: true }
10578
10441
  } catch (error) {
@@ -10722,10 +10585,21 @@ function trimMathBlock(content: string): string {
10722
10585
  const shiki = createHighlighterCoreSync({
10723
10586
  themes: [githubDark, githubLight],
10724
10587
  langs: [
10725
- javascript, typescript, jsx, tsx, python, rust, go,
10726
- json, yaml, css, sql, shellscript, markdown
10588
+ javascript,
10589
+ typescript,
10590
+ jsx,
10591
+ tsx,
10592
+ python,
10593
+ rust,
10594
+ go,
10595
+ json,
10596
+ yaml,
10597
+ css,
10598
+ sql,
10599
+ shellscript,
10600
+ markdown,
10727
10601
  ],
10728
- engine: createJavaScriptRegexEngine()
10602
+ engine: createJavaScriptRegexEngine(),
10729
10603
  })
10730
10604
 
10731
10605
  const loadedLangs = shiki.getLoadedLanguages()
@@ -10767,11 +10641,11 @@ const md = MarkdownIt({
10767
10641
  lang: language,
10768
10642
  themes: { light: 'github-light', dark: 'github-dark' },
10769
10643
  defaultColor: false,
10770
- transformers: [transformerNotationHighlight(), transformerNotationDiff()]
10644
+ transformers: [transformerNotationHighlight(), transformerNotationDiff()],
10771
10645
  })
10772
10646
  const escapedCode = code.replace(/</g, '&lt;').replace(/>/g, '&gt;')
10773
10647
  return \`<div class="code-block-wrapper not-prose" data-lang="\${language}"><button class="copy-button" data-code="\${escapedCode.replace(/"/g, '&quot;')}" aria-label="Copy code">Copy</button>\${highlighted}</div>\`
10774
- }
10648
+ },
10775
10649
  })
10776
10650
  .use(headingAnchorPlugin)
10777
10651
  .use(dollarmath, {
@@ -10784,7 +10658,7 @@ const md = MarkdownIt({
10784
10658
  return renderToString(content, {
10785
10659
  displayMode,
10786
10660
  throwOnError: false,
10787
- strict: 'ignore'
10661
+ strict: 'ignore',
10788
10662
  })
10789
10663
  },
10790
10664
  labelNormalizer(label: string) {
@@ -10792,7 +10666,7 @@ const md = MarkdownIt({
10792
10666
  },
10793
10667
  labelRenderer(label: string) {
10794
10668
  return \`<a href="#\${label}" class="mathlabel" title="Permalink to this equation">\xB6</a>\`
10795
- }
10669
+ },
10796
10670
  })
10797
10671
 
10798
10672
  export function renderMarkdownSync(src: string): string {
@@ -10887,7 +10761,7 @@ export interface AuthSession {
10887
10761
  export enum UserRole {
10888
10762
  ADMIN = 'admin',
10889
10763
  EDITOR = 'editor',
10890
- MEMBER = 'member'
10764
+ MEMBER = 'member',
10891
10765
  }
10892
10766
 
10893
10767
  export interface UserWithRole extends AuthUser {
@@ -10896,17 +10770,11 @@ export interface UserWithRole extends AuthUser {
10896
10770
 
10897
10771
  /** Type guard to check if a value is a valid UserRole */
10898
10772
  export function isUserRole(value: unknown): value is UserRole {
10899
- return (
10900
- typeof value === 'string' &&
10901
- Object.values(UserRole).includes(value as UserRole)
10902
- )
10773
+ return typeof value === 'string' && Object.values(UserRole).includes(value as UserRole)
10903
10774
  }
10904
10775
 
10905
10776
  /** Check if user has one of the allowed roles */
10906
- export function hasRequiredRole(
10907
- userRole: UserRole,
10908
- allowedRoles: UserRole[]
10909
- ): boolean {
10777
+ export function hasRequiredRole(userRole: UserRole, allowedRoles: UserRole[]): boolean {
10910
10778
  return allowedRoles.includes(userRole)
10911
10779
  }
10912
10780
 
@@ -11092,7 +10960,7 @@ export function createMetadata({
11092
10960
  description,
11093
10961
  path,
11094
10962
  ogImage,
11095
- noIndex = false
10963
+ noIndex = false,
11096
10964
  }: CreateMetadataOptions): Metadata {
11097
10965
  const metadata: Metadata = {
11098
10966
  title,
@@ -11101,14 +10969,14 @@ export function createMetadata({
11101
10969
  title,
11102
10970
  description,
11103
10971
  type: 'website',
11104
- ...(ogImage && { images: [{ url: ogImage }] })
10972
+ ...(ogImage && { images: [{ url: ogImage }] }),
11105
10973
  },
11106
10974
  twitter: {
11107
10975
  card: ogImage ? 'summary_large_image' : 'summary',
11108
10976
  title,
11109
10977
  description,
11110
- ...(ogImage && { images: [ogImage] })
11111
- }
10978
+ ...(ogImage && { images: [ogImage] }),
10979
+ },
11112
10980
  }
11113
10981
 
11114
10982
  if (path) {
@@ -11132,7 +11000,7 @@ export function generateArticleSchema({
11132
11000
  imageUrl,
11133
11001
  datePublished,
11134
11002
  dateModified,
11135
- authorName
11003
+ authorName,
11136
11004
  }: {
11137
11005
  title: string
11138
11006
  description: string
@@ -11152,8 +11020,8 @@ export function generateArticleSchema({
11152
11020
  datePublished,
11153
11021
  ...(dateModified && { dateModified }),
11154
11022
  ...(authorName && {
11155
- author: { '@type': 'Person', name: authorName }
11156
- })
11023
+ author: { '@type': 'Person', name: authorName },
11024
+ }),
11157
11025
  }
11158
11026
  }
11159
11027
 
@@ -11206,20 +11074,16 @@ function isFileTypeAllowed(file: File, allowedTypes: string[]): boolean {
11206
11074
  */
11207
11075
  export function validateFiles(
11208
11076
  files: File[],
11209
- config: FileValidationConfig = {}
11077
+ config: FileValidationConfig = {},
11210
11078
  ): FileValidationResult {
11211
- const {
11212
- maxSizeInBytes = DEFAULT_MAX_SIZE,
11213
- allowedTypes,
11214
- maxFiles = DEFAULT_MAX_FILES
11215
- } = config
11079
+ const { maxSizeInBytes = DEFAULT_MAX_SIZE, allowedTypes, maxFiles = DEFAULT_MAX_FILES } = config
11216
11080
 
11217
11081
  const errors: FileValidationError[] = []
11218
11082
 
11219
11083
  if (files.length > maxFiles) {
11220
11084
  errors.push({
11221
11085
  filename: '',
11222
- error: \`Too many files. Maximum is \${maxFiles}.\`
11086
+ error: \`Too many files. Maximum is \${maxFiles}.\`,
11223
11087
  })
11224
11088
  }
11225
11089
 
@@ -11228,14 +11092,14 @@ export function validateFiles(
11228
11092
  const maxMB = Math.round(maxSizeInBytes / (1024 * 1024))
11229
11093
  errors.push({
11230
11094
  filename: file.name,
11231
- error: \`File exceeds maximum size of \${maxMB}MB.\`
11095
+ error: \`File exceeds maximum size of \${maxMB}MB.\`,
11232
11096
  })
11233
11097
  }
11234
11098
 
11235
11099
  if (allowedTypes && allowedTypes.length > 0 && !isFileTypeAllowed(file, allowedTypes)) {
11236
11100
  errors.push({
11237
11101
  filename: file.name,
11238
- error: \`File type "\${file.type || 'unknown'}" is not allowed.\`
11102
+ error: \`File type "\${file.type || 'unknown'}" is not allowed.\`,
11239
11103
  })
11240
11104
  }
11241
11105
  }
@@ -11289,17 +11153,15 @@ function webhookUtilTemplate() {
11289
11153
  */
11290
11154
  export function sendWebhook(
11291
11155
  webhookUrl: string | null | undefined,
11292
- payload: Record<string, unknown>
11156
+ payload: Record<string, unknown>,
11293
11157
  ): void {
11294
- if (!webhookUrl) return
11295
- // Fire-and-forget: runs in background, doesn't block
11158
+ if (!webhookUrl) return // Fire-and-forget: runs in background, doesn't block
11296
11159
  ;(async () => {
11297
11160
  try {
11298
11161
  const formData = new URLSearchParams()
11299
11162
  for (const [key, value] of Object.entries(payload)) {
11300
11163
  if (value === null || value === undefined) continue
11301
- const stringValue =
11302
- typeof value === 'object' ? JSON.stringify(value) : String(value)
11164
+ const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value)
11303
11165
  formData.append(key, stringValue)
11304
11166
  }
11305
11167
  await fetch(webhookUrl, {
@@ -11327,6 +11189,7 @@ function scaffoldComponents({ cwd, config }) {
11327
11189
  }
11328
11190
  write("cms-globals.css", cmsGlobalsCssTemplate());
11329
11191
  write("components/layout/cms-providers.tsx", cmsProvidersTemplate());
11192
+ write("components/layout/cms-nav-link.tsx", cmsNavLinkTemplate());
11330
11193
  write("components/layout/cms-sidebar.tsx", cmsSidebarTemplate());
11331
11194
  write("components/layout/cms-header.tsx", cmsHeaderTemplate());
11332
11195
  write("components/layout/cms-search.tsx", cmsSearchTemplate());
@@ -11352,6 +11215,7 @@ function scaffoldComponents({ cwd, config }) {
11352
11215
  write("hooks/use-local-storage.ts", useLocalStorageHookTemplate());
11353
11216
  write("hooks/use-cms-theme.tsx", useCmsThemeTemplate());
11354
11217
  write("hooks/use-users.ts", useUsersHookTemplate());
11218
+ write("hooks/use-mobile.ts", useMobileHookTemplate());
11355
11219
  const projectName = detectProjectName(cwd);
11356
11220
  write("data/cms.ts", cmsDataTemplate(projectName));
11357
11221
  write("data/navigation.ts", navigationDataTemplate());
@@ -11588,6 +11452,7 @@ var CORE_DEPS = [
11588
11452
  "nuqs",
11589
11453
  "sonner",
11590
11454
  // Styling utilities
11455
+ "geist",
11591
11456
  "class-variance-authority",
11592
11457
  "clsx",
11593
11458
  "tailwind-merge",
@@ -11833,24 +11698,18 @@ import path32 from "path";
11833
11698
 
11834
11699
  // src/init/templates/pages/authenticated-layout.ts
11835
11700
  function authenticatedLayoutTemplate() {
11836
- return `import { CmsHeader } from '@cms/components/layout/cms-header'
11701
+ return `import { requireRole } from '@cms/auth/middleware'
11837
11702
  import { CmsSidebar } from '@cms/components/layout/cms-sidebar'
11838
- import { requireRole } from '@cms/auth/middleware'
11839
- import { UserRole } from '@cms/types/auth'
11840
11703
  import { SidebarInset, SidebarProvider } from '@cms/components/ui/sidebar'
11704
+ import { UserRole } from '@cms/types/auth'
11841
11705
 
11842
- export default async function CmsAuthLayout({
11843
- children
11844
- }: {
11845
- children: React.ReactNode
11846
- }) {
11706
+ export default async function CmsAuthLayout({ children }: { children: React.ReactNode }) {
11847
11707
  await requireRole([UserRole.ADMIN, UserRole.EDITOR])
11848
11708
 
11849
11709
  return (
11850
11710
  <SidebarProvider>
11851
11711
  <CmsSidebar />
11852
11712
  <SidebarInset>
11853
- <CmsHeader />
11854
11713
  <main>{children}</main>
11855
11714
  </SidebarInset>
11856
11715
  </SidebarProvider>
@@ -11863,11 +11722,17 @@ export default async function CmsAuthLayout({
11863
11722
  function cmsLayoutTemplate() {
11864
11723
  return `import '@cms/cms-globals.css'
11865
11724
  import { CmsProviders } from '@cms/components/layout/cms-providers'
11725
+ import { GeistMono } from 'geist/font/mono'
11726
+ import { GeistSans } from 'geist/font/sans'
11866
11727
 
11867
11728
  export default function CmsLayout({ children }: { children: React.ReactNode }) {
11868
11729
  return (
11869
11730
  <CmsProviders>
11870
- <div className="cms-root min-h-screen">{children}</div>
11731
+ <div
11732
+ className={\`cms-root min-h-screen antialiased \${GeistSans.variable} \${GeistMono.variable}\`}
11733
+ >
11734
+ {children}
11735
+ </div>
11871
11736
  </CmsProviders>
11872
11737
  )
11873
11738
  }
@@ -11877,8 +11742,8 @@ export default function CmsLayout({ children }: { children: React.ReactNode }) {
11877
11742
  // src/init/templates/pages/dashboard-page.ts
11878
11743
  function dashboardPageTemplate() {
11879
11744
  return `import { PageHeader } from '@cms/components/shared/page-header'
11880
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cms/components/ui/card'
11881
11745
  import { Badge } from '@cms/components/ui/badge'
11746
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cms/components/ui/card'
11882
11747
  import { FileText, Settings, Users } from 'lucide-react'
11883
11748
  import Link from 'next/link'
11884
11749
 
@@ -11887,32 +11752,27 @@ const quickLinks = [
11887
11752
  title: 'Users',
11888
11753
  description: 'Manage admin users and roles',
11889
11754
  href: '/cms/users',
11890
- icon: Users
11755
+ icon: Users,
11891
11756
  },
11892
11757
  {
11893
11758
  title: 'Settings',
11894
11759
  description: 'Configure CMS settings',
11895
11760
  href: '/cms/settings',
11896
- icon: Settings
11761
+ icon: Settings,
11897
11762
  },
11898
11763
  {
11899
11764
  title: 'Generate',
11900
11765
  description: 'Add a new resource from a schema',
11901
11766
  href: '#',
11902
11767
  icon: FileText,
11903
- hint: 'npx betterstart generate <schema>'
11904
- }
11768
+ hint: 'npx betterstart generate <schema>',
11769
+ },
11905
11770
  ]
11906
11771
 
11907
11772
  export default function DashboardPage() {
11908
11773
  return (
11909
11774
  <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>
11775
+ <PageHeader title="Dashboard" />
11916
11776
  <div className="p-6 space-y-6">
11917
11777
  <div className="grid gap-4 md:grid-cols-3">
11918
11778
  {quickLinks.map((link) => (
@@ -11982,7 +11842,7 @@ import { LoginForm } from './login-form'
11982
11842
 
11983
11843
  export const metadata: Metadata = {
11984
11844
  title: 'CMS Login',
11985
- robots: { index: false, follow: false }
11845
+ robots: { index: false, follow: false },
11986
11846
  }
11987
11847
 
11988
11848
  export default function LoginPage() {
@@ -11991,9 +11851,7 @@ export default function LoginPage() {
11991
11851
  <div className="w-full max-w-sm">
11992
11852
  <div className="mb-8 text-center">
11993
11853
  <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>
11854
+ <p className="text-muted-foreground text-sm mt-1">Sign in to access the admin panel</p>
11997
11855
  </div>
11998
11856
  <LoginForm />
11999
11857
  </div>
@@ -12028,7 +11886,7 @@ export function LoginForm() {
12028
11886
  try {
12029
11887
  const result = await authClient.signIn.email({
12030
11888
  email,
12031
- password
11889
+ password,
12032
11890
  })
12033
11891
 
12034
11892
  if (result.error) {
@@ -12048,9 +11906,7 @@ export function LoginForm() {
12048
11906
  return (
12049
11907
  <form onSubmit={handleSubmit} className="space-y-5">
12050
11908
  {error && (
12051
- <div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">
12052
- {error}
12053
- </div>
11909
+ <div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md">{error}</div>
12054
11910
  )}
12055
11911
 
12056
11912
  <div className="space-y-2">
@@ -12095,6 +11951,7 @@ export function LoginForm() {
12095
11951
  function createUserDialogTemplate() {
12096
11952
  return `'use client'
12097
11953
 
11954
+ import { createUser } from '@cms/actions/users'
12098
11955
  import { Button } from '@cms/components/ui/button'
12099
11956
  import {
12100
11957
  Dialog,
@@ -12102,11 +11959,10 @@ import {
12102
11959
  DialogDescription,
12103
11960
  DialogHeader,
12104
11961
  DialogTitle,
12105
- DialogTrigger
11962
+ DialogTrigger,
12106
11963
  } from '@cms/components/ui/dialog'
12107
11964
  import { Input } from '@cms/components/ui/input'
12108
11965
  import { Label } from '@cms/components/ui/label'
12109
- import { createUser } from '@cms/actions/users'
12110
11966
  import { useQueryClient } from '@tanstack/react-query'
12111
11967
  import { Loader2, UserPlus } from 'lucide-react'
12112
11968
  import * as React from 'react'
@@ -12216,6 +12072,7 @@ export function CreateUserDialog() {
12216
12072
  function editRoleDialogTemplate() {
12217
12073
  return `'use client'
12218
12074
 
12075
+ import { updateUserRole } from '@cms/actions/users'
12219
12076
  import { Button } from '@cms/components/ui/button'
12220
12077
  import {
12221
12078
  Dialog,
@@ -12223,7 +12080,7 @@ import {
12223
12080
  DialogDescription,
12224
12081
  DialogHeader,
12225
12082
  DialogTitle,
12226
- DialogTrigger
12083
+ DialogTrigger,
12227
12084
  } from '@cms/components/ui/dialog'
12228
12085
  import { Label } from '@cms/components/ui/label'
12229
12086
  import {
@@ -12231,9 +12088,8 @@ import {
12231
12088
  SelectContent,
12232
12089
  SelectItem,
12233
12090
  SelectTrigger,
12234
- SelectValue
12091
+ SelectValue,
12235
12092
  } from '@cms/components/ui/select'
12236
- import { updateUserRole } from '@cms/actions/users'
12237
12093
  import { UserRole } from '@cms/types/auth'
12238
12094
  import { useQueryClient } from '@tanstack/react-query'
12239
12095
  import { Loader2 } from 'lucide-react'
@@ -12247,12 +12103,7 @@ interface EditRoleDialogProps {
12247
12103
  children: React.ReactNode
12248
12104
  }
12249
12105
 
12250
- export function EditRoleDialog({
12251
- userId,
12252
- currentRole,
12253
- userName,
12254
- children
12255
- }: EditRoleDialogProps) {
12106
+ export function EditRoleDialog({ userId, currentRole, userName, children }: EditRoleDialogProps) {
12256
12107
  const [open, setOpen] = React.useState(false)
12257
12108
  const [role, setRole] = React.useState(currentRole)
12258
12109
  const [isPending, startTransition] = React.useTransition()
@@ -12281,9 +12132,7 @@ export function EditRoleDialog({
12281
12132
  <DialogContent className="sm:max-w-[350px]">
12282
12133
  <DialogHeader>
12283
12134
  <DialogTitle>Edit Role</DialogTitle>
12284
- <DialogDescription>
12285
- Change the role for {userName}
12286
- </DialogDescription>
12135
+ <DialogDescription>Change the role for {userName}</DialogDescription>
12287
12136
  </DialogHeader>
12288
12137
  <div className="space-y-4">
12289
12138
  <div className="space-y-2">
@@ -12300,17 +12149,10 @@ export function EditRoleDialog({
12300
12149
  </Select>
12301
12150
  </div>
12302
12151
  <div className="flex justify-end gap-2">
12303
- <Button
12304
- variant="outline"
12305
- onClick={() => setOpen(false)}
12306
- disabled={isPending}
12307
- >
12152
+ <Button variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
12308
12153
  Cancel
12309
12154
  </Button>
12310
- <Button
12311
- onClick={handleSave}
12312
- disabled={isPending || role === currentRole}
12313
- >
12155
+ <Button onClick={handleSave} disabled={isPending || role === currentRole}>
12314
12156
  {isPending && <Loader2 className="size-4 mr-1 animate-spin" />}
12315
12157
  Save
12316
12158
  </Button>
@@ -12327,11 +12169,7 @@ export function EditRoleDialog({
12327
12169
  function usersColumnsTemplate() {
12328
12170
  return `'use client'
12329
12171
 
12330
- import React from 'react'
12331
- import {
12332
- Avatar,
12333
- AvatarFallback
12334
- } from '@cms/components/ui/avatar'
12172
+ import { deleteUser } from '@cms/actions/users'
12335
12173
  import {
12336
12174
  AlertDialog,
12337
12175
  AlertDialogAction,
@@ -12343,6 +12181,7 @@ import {
12343
12181
  AlertDialogTitle,
12344
12182
  AlertDialogTrigger,
12345
12183
  } from '@cms/components/ui/alert-dialog'
12184
+ import { Avatar, AvatarFallback } from '@cms/components/ui/avatar'
12346
12185
  import { Badge } from '@cms/components/ui/badge'
12347
12186
  import { Button } from '@cms/components/ui/button'
12348
12187
  import {
@@ -12351,14 +12190,14 @@ import {
12351
12190
  DropdownMenuItem,
12352
12191
  DropdownMenuLabel,
12353
12192
  DropdownMenuSeparator,
12354
- DropdownMenuTrigger
12193
+ DropdownMenuTrigger,
12355
12194
  } from '@cms/components/ui/dropdown-menu'
12356
12195
  import type { UserData } from '@cms/types/auth'
12357
- import type { ColumnDef } from '@tanstack/react-table'
12358
12196
  import { useQueryClient } from '@tanstack/react-query'
12197
+ import type { ColumnDef } from '@tanstack/react-table'
12359
12198
  import { ArrowUpDown, Edit, MoreHorizontal, Trash } from 'lucide-react'
12199
+ import React from 'react'
12360
12200
  import { toast } from 'sonner'
12361
- import { deleteUser } from '@cms/actions/users'
12362
12201
  import { EditRoleDialog } from './edit-role-dialog'
12363
12202
 
12364
12203
  function getInitials(nameOrEmail: string): string {
@@ -12409,10 +12248,7 @@ function DeleteUserAction({
12409
12248
  return (
12410
12249
  <AlertDialog open={open} onOpenChange={setOpen}>
12411
12250
  <AlertDialogTrigger asChild>
12412
- <DropdownMenuItem
12413
- className="text-destructive"
12414
- onSelect={(e) => e.preventDefault()}
12415
- >
12251
+ <DropdownMenuItem className="text-destructive" onSelect={(e) => e.preventDefault()}>
12416
12252
  <Trash className="size-4 mr-2" />
12417
12253
  Delete user
12418
12254
  </DropdownMenuItem>
@@ -12421,8 +12257,8 @@ function DeleteUserAction({
12421
12257
  <AlertDialogHeader>
12422
12258
  <AlertDialogTitle>Are you sure?</AlertDialogTitle>
12423
12259
  <AlertDialogDescription>
12424
- This action cannot be undone. This will permanently delete{' '}
12425
- <strong>{userName}</strong> and all of their data.
12260
+ This action cannot be undone. This will permanently delete <strong>{userName}</strong>{' '}
12261
+ and all of their data.
12426
12262
  </AlertDialogDescription>
12427
12263
  </AlertDialogHeader>
12428
12264
  <AlertDialogFooter>
@@ -12470,7 +12306,7 @@ export const columns: ColumnDef<UserData>[] = [
12470
12306
  </div>
12471
12307
  </div>
12472
12308
  )
12473
- }
12309
+ },
12474
12310
  },
12475
12311
  {
12476
12312
  accessorKey: 'emailVerified',
@@ -12482,7 +12318,7 @@ export const columns: ColumnDef<UserData>[] = [
12482
12318
  {verified ? 'Verified' : 'Unverified'}
12483
12319
  </Badge>
12484
12320
  )
12485
- }
12321
+ },
12486
12322
  },
12487
12323
  {
12488
12324
  accessorKey: 'role',
@@ -12512,7 +12348,7 @@ export const columns: ColumnDef<UserData>[] = [
12512
12348
  </Badge>
12513
12349
  </EditRoleDialog>
12514
12350
  )
12515
- }
12351
+ },
12516
12352
  },
12517
12353
  {
12518
12354
  accessorKey: 'createdAt',
@@ -12533,11 +12369,11 @@ export const columns: ColumnDef<UserData>[] = [
12533
12369
  {date.toLocaleDateString('en-US', {
12534
12370
  month: 'short',
12535
12371
  day: 'numeric',
12536
- year: 'numeric'
12372
+ year: 'numeric',
12537
12373
  })}
12538
12374
  </div>
12539
12375
  )
12540
- }
12376
+ },
12541
12377
  },
12542
12378
  {
12543
12379
  id: 'actions',
@@ -12556,9 +12392,7 @@ export const columns: ColumnDef<UserData>[] = [
12556
12392
  </DropdownMenuTrigger>
12557
12393
  <DropdownMenuContent align="end">
12558
12394
  <DropdownMenuLabel>Actions</DropdownMenuLabel>
12559
- <DropdownMenuItem
12560
- onClick={() => navigator.clipboard.writeText(row.original.id)}
12561
- >
12395
+ <DropdownMenuItem onClick={() => navigator.clipboard.writeText(row.original.id)}>
12562
12396
  Copy user ID
12563
12397
  </DropdownMenuItem>
12564
12398
  <DropdownMenuSeparator />
@@ -12571,8 +12405,8 @@ export const columns: ColumnDef<UserData>[] = [
12571
12405
  </DropdownMenu>
12572
12406
  </div>
12573
12407
  )
12574
- }
12575
- }
12408
+ },
12409
+ },
12576
12410
  ]
12577
12411
  `;
12578
12412
  }
@@ -12588,10 +12422,7 @@ export default function UsersPage() {
12588
12422
  return (
12589
12423
  <div className="flex flex-col">
12590
12424
  <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
- >
12425
+ <PageHeader title="Users">
12595
12426
  <CreateUserDialog />
12596
12427
  </PageHeader>
12597
12428
  </div>
@@ -12608,6 +12439,18 @@ export default function UsersPage() {
12608
12439
  function usersTableTemplate() {
12609
12440
  return `'use client'
12610
12441
 
12442
+ import { authClient } from '@cms/auth/client'
12443
+ import { Button } from '@cms/components/ui/button'
12444
+ import {
12445
+ Table,
12446
+ TableBody,
12447
+ TableCell,
12448
+ TableHead,
12449
+ TableHeader,
12450
+ TableRow,
12451
+ } from '@cms/components/ui/table'
12452
+ import { useUsers } from '@cms/hooks/use-users'
12453
+ import type { UserData } from '@cms/types/auth'
12611
12454
  import {
12612
12455
  type ColumnDef,
12613
12456
  type ColumnFiltersState,
@@ -12618,21 +12461,9 @@ import {
12618
12461
  getSortedRowModel,
12619
12462
  type SortingState,
12620
12463
  useReactTable,
12621
- type VisibilityState
12464
+ type VisibilityState,
12622
12465
  } from '@tanstack/react-table'
12623
12466
  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
12467
 
12637
12468
  interface UsersTableProps<TValue> {
12638
12469
  columns: ColumnDef<UserData, TValue>[]
@@ -12662,11 +12493,11 @@ export function UsersTable<TValue>({ columns }: UsersTableProps<TValue>) {
12662
12493
  email: session.user.email,
12663
12494
  name: session.user.name,
12664
12495
  image: session.user.image,
12665
- role: (session.user as { role?: string }).role || 'member'
12496
+ role: (session.user as { role?: string }).role || 'member',
12666
12497
  }
12667
- : null
12498
+ : null,
12668
12499
  },
12669
- state: { sorting, columnFilters, columnVisibility }
12500
+ state: { sorting, columnFilters, columnVisibility },
12670
12501
  })
12671
12502
 
12672
12503
  return (
@@ -13606,24 +13437,29 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
13606
13437
  const { execFile } = await import("child_process");
13607
13438
  const tsxBin = path36.join(cwd, "node_modules", ".bin", "tsx");
13608
13439
  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 });
13440
+ execFile(
13441
+ tsxBin,
13442
+ [seedPath],
13443
+ {
13444
+ cwd,
13445
+ env: {
13446
+ ...process.env,
13447
+ SEED_EMAIL: email,
13448
+ SEED_PASSWORD: password3,
13449
+ SEED_NAME: name || "Admin",
13450
+ ...overwrite ? { SEED_OVERWRITE: "true" } : {}
13451
+ }
13452
+ },
13453
+ (err, stdout, stderr) => {
13454
+ if (err && "code" in err && err.code === 2) {
13455
+ resolve({ code: 2, stdout });
13456
+ } else if (err) {
13457
+ reject(new Error(stderr || err.message));
13458
+ } else {
13459
+ resolve({ code: 0, stdout });
13460
+ }
13625
13461
  }
13626
- });
13462
+ );
13627
13463
  });
13628
13464
  const spinner5 = clack.spinner();
13629
13465
  spinner5.start("Creating admin user...");
@@ -13674,7 +13510,7 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
13674
13510
  var initCommand = new Command3("init").description("Scaffold CMS into a new or existing Next.js project").argument("[name]", "Project name (creates new directory if fresh project)").option("--preset <preset>", "Starter preset: blank, blog, or full", "blog").option("-y, --yes", "Skip all prompts (accept defaults)").option(
13675
13511
  "--database-url <url>",
13676
13512
  "PostgreSQL database connection string (postgres:// or postgresql://)"
13677
- ).action(
13513
+ ).option("--force", "Overwrite all existing CMS files (nuclear option)").action(
13678
13514
  async (name, options) => {
13679
13515
  p4.intro(pc2.bgCyan(pc2.black(" BetterStart CMS ")));
13680
13516
  let cwd = process.cwd();
@@ -13689,9 +13525,35 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13689
13525
  p4.log.error("TypeScript is required. Please add a tsconfig.json first.");
13690
13526
  process.exit(1);
13691
13527
  }
13692
- if (project.conflicts.length > 0) {
13528
+ if (options.force) {
13529
+ const nukeDirs = ["cms", "app/(cms)"];
13530
+ const nukeFiles = ["cms.config.ts", "CMS.md", "drizzle.config.ts"];
13531
+ let nuked = 0;
13532
+ for (const dir of nukeDirs) {
13533
+ const fullPath = path37.resolve(cwd, dir);
13534
+ if (fs32.existsSync(fullPath)) {
13535
+ fs32.rmSync(fullPath, { recursive: true, force: true });
13536
+ nuked++;
13537
+ }
13538
+ }
13539
+ for (const file of nukeFiles) {
13540
+ const fullPath = path37.resolve(cwd, file);
13541
+ if (fs32.existsSync(fullPath)) {
13542
+ fs32.unlinkSync(fullPath);
13543
+ nuked++;
13544
+ }
13545
+ }
13546
+ if (nuked > 0) {
13547
+ p4.log.warn(`${pc2.yellow("Force mode:")} removed ${nuked} existing CMS paths`);
13548
+ }
13549
+ project = detectProject(cwd);
13550
+ } else if (project.conflicts.length > 0) {
13693
13551
  const conflictLines = project.conflicts.map((c) => `${pc2.yellow("\u25B2")} ${c}`);
13694
- conflictLines.push("", pc2.dim("Existing files will not be overwritten."));
13552
+ conflictLines.push(
13553
+ "",
13554
+ pc2.dim("Existing files will not be overwritten."),
13555
+ pc2.dim(`Use ${pc2.bold("--force")} to remove existing CMS files before scaffolding.`)
13556
+ );
13695
13557
  p4.note(conflictLines.join("\n"), pc2.yellow("Conflicts"));
13696
13558
  if (!options.yes) {
13697
13559
  const proceed = await p4.confirm({
@@ -13862,13 +13724,17 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13862
13724
  p4.note(noteLines.join("\n"), "Scaffolded CMS");
13863
13725
  const drizzleConfigPath = path37.join(cwd, "drizzle.config.ts");
13864
13726
  if (!dbFiles.includes("drizzle.config.ts") && fs32.existsSync(drizzleConfigPath)) {
13865
- if (!options.yes) {
13727
+ if (options.force) {
13728
+ const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-EDKOEZ6G.js");
13729
+ fs32.writeFileSync(drizzleConfigPath, drizzleConfigTemplate2(), "utf-8");
13730
+ p4.log.success("Updated drizzle.config.ts");
13731
+ } else if (!options.yes) {
13866
13732
  const overwrite = await p4.confirm({
13867
13733
  message: "drizzle.config.ts already exists. Overwrite with latest version?",
13868
13734
  initialValue: true
13869
13735
  });
13870
13736
  if (!p4.isCancel(overwrite) && overwrite) {
13871
- const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-KISB26BA.js");
13737
+ const { drizzleConfigTemplate: drizzleConfigTemplate2 } = await import("./drizzle-config-EDKOEZ6G.js");
13872
13738
  fs32.writeFileSync(drizzleConfigPath, drizzleConfigTemplate2(), "utf-8");
13873
13739
  p4.log.success("Updated drizzle.config.ts");
13874
13740
  }
@@ -13985,7 +13851,12 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13985
13851
  seedEmail = credentials.email;
13986
13852
  seedPassword = credentials.password;
13987
13853
  s.start("Creating admin user");
13988
- let seedResult = await runSeed(cwd, config.paths?.cms ?? "./cms", credentials.email, credentials.password);
13854
+ let seedResult = await runSeed(
13855
+ cwd,
13856
+ config.paths?.cms ?? "./cms",
13857
+ credentials.email,
13858
+ credentials.password
13859
+ );
13989
13860
  if (seedResult.existingUser) {
13990
13861
  s.stop(`${pc2.yellow("\u25B2")} Admin user already exists (${seedResult.existingUser})`);
13991
13862
  const replace = await p4.confirm({
@@ -13994,7 +13865,13 @@ var initCommand = new Command3("init").description("Scaffold CMS into a new or e
13994
13865
  });
13995
13866
  if (!p4.isCancel(replace) && replace) {
13996
13867
  s.start("Replacing admin user");
13997
- seedResult = await runSeed(cwd, config.paths?.cms ?? "./cms", credentials.email, credentials.password, true);
13868
+ seedResult = await runSeed(
13869
+ cwd,
13870
+ config.paths?.cms ?? "./cms",
13871
+ credentials.email,
13872
+ credentials.password,
13873
+ true
13874
+ );
13998
13875
  } else {
13999
13876
  seedSuccess = true;
14000
13877
  }
@@ -14187,8 +14064,7 @@ ${stderr}`;
14187
14064
  if (combined.includes("Failed to create user")) return "Auth API failed to create user";
14188
14065
  if (combined.includes("ECONNREFUSED") || combined.includes("connection refused"))
14189
14066
  return "Could not connect to database";
14190
- if (combined.includes("BETTERSTART_DATABASE_URL"))
14191
- return "Database URL is missing or invalid";
14067
+ if (combined.includes("BETTERSTART_DATABASE_URL")) return "Database URL is missing or invalid";
14192
14068
  if (combined.includes("password authentication failed"))
14193
14069
  return "Database authentication failed \u2014 check your connection string";
14194
14070
  if (combined.includes("does not exist") && combined.includes("relation"))
@@ -14443,41 +14319,12 @@ var removeCommand = new Command4("remove").alias("rm").description("Remove all g
14443
14319
  console.log("");
14444
14320
  });
14445
14321
 
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
14322
  // src/commands/uninstall.ts
14476
14323
  import fs35 from "fs";
14477
- import path40 from "path";
14324
+ import path39 from "path";
14478
14325
  import * as p5 from "@clack/prompts";
14326
+ import { Command as Command5 } from "commander";
14479
14327
  import pc3 from "picocolors";
14480
- import { Command as Command6 } from "commander";
14481
14328
 
14482
14329
  // src/commands/uninstall-cleaners.ts
14483
14330
  import fs34 from "fs";
@@ -14534,7 +14381,7 @@ function cleanTsconfig(tsconfigPath) {
14534
14381
  }
14535
14382
  if (removed.length === 0) return [];
14536
14383
  if (Object.keys(paths).length === 0) {
14537
- delete compilerOptions.paths;
14384
+ compilerOptions.paths = void 0;
14538
14385
  } else {
14539
14386
  compilerOptions.paths = paths;
14540
14387
  }
@@ -14588,14 +14435,14 @@ function cleanEnvFile(envPath) {
14588
14435
  }
14589
14436
  if (trimmed.startsWith("#") && !headerPattern.test(trimmed)) {
14590
14437
  const nextNonEmpty = findNextNonEmptyLine(lines, i + 1);
14591
- if (nextNonEmpty !== null && nextNonEmpty.match(/^BETTERSTART_\w+=/)) {
14438
+ if (nextNonEmpty?.match(/^BETTERSTART_\w+=/)) {
14592
14439
  continue;
14593
14440
  }
14594
14441
  }
14595
14442
  kept.push(line);
14596
14443
  }
14597
14444
  if (removed.length === 0) return [];
14598
- let result = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
14445
+ const result = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
14599
14446
  if (result === "") {
14600
14447
  fs34.unlinkSync(envPath);
14601
14448
  } else {
@@ -14625,7 +14472,7 @@ function findMainCss2(cwd) {
14625
14472
  "globals.css"
14626
14473
  ];
14627
14474
  for (const candidate of candidates) {
14628
- const filePath = path40.join(cwd, candidate);
14475
+ const filePath = path39.join(cwd, candidate);
14629
14476
  if (fs35.existsSync(filePath)) return filePath;
14630
14477
  }
14631
14478
  return void 0;
@@ -14641,11 +14488,11 @@ function isCLICreatedBiome(biomePath) {
14641
14488
  }
14642
14489
  function buildUninstallPlan(cwd) {
14643
14490
  const steps = [];
14644
- const hasSrc = fs35.existsSync(path40.join(cwd, "src"));
14491
+ const hasSrc = fs35.existsSync(path39.join(cwd, "src"));
14645
14492
  const appBase = hasSrc ? "src/app" : "app";
14646
14493
  const dirs = [];
14647
- const cmsDir = path40.join(cwd, "cms");
14648
- const cmsRouteGroup = path40.join(cwd, appBase, "(cms)");
14494
+ const cmsDir = path39.join(cwd, "cms");
14495
+ const cmsRouteGroup = path39.join(cwd, appBase, "(cms)");
14649
14496
  if (fs35.existsSync(cmsDir)) dirs.push("cms/");
14650
14497
  if (fs35.existsSync(cmsRouteGroup)) dirs.push(`${appBase}/(cms)/`);
14651
14498
  if (dirs.length > 0) {
@@ -14663,9 +14510,9 @@ function buildUninstallPlan(cwd) {
14663
14510
  const configFiles = [];
14664
14511
  const configPaths = [];
14665
14512
  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")]
14513
+ ["cms.config.ts", path39.join(cwd, "cms.config.ts")],
14514
+ ["drizzle.config.ts", path39.join(cwd, "drizzle.config.ts")],
14515
+ ["CMS.md", path39.join(cwd, "CMS.md")]
14669
14516
  ];
14670
14517
  for (const [label, fullPath] of candidates) {
14671
14518
  if (fs35.existsSync(fullPath)) {
@@ -14673,7 +14520,7 @@ function buildUninstallPlan(cwd) {
14673
14520
  configPaths.push(fullPath);
14674
14521
  }
14675
14522
  }
14676
- const biomePath = path40.join(cwd, "biome.json");
14523
+ const biomePath = path39.join(cwd, "biome.json");
14677
14524
  if (isCLICreatedBiome(biomePath)) {
14678
14525
  configFiles.push("biome.json (CLI-created)");
14679
14526
  configPaths.push(biomePath);
@@ -14691,7 +14538,7 @@ function buildUninstallPlan(cwd) {
14691
14538
  }
14692
14539
  });
14693
14540
  }
14694
- const tsconfigPath = path40.join(cwd, "tsconfig.json");
14541
+ const tsconfigPath = path39.join(cwd, "tsconfig.json");
14695
14542
  if (fs35.existsSync(tsconfigPath)) {
14696
14543
  const content = fs35.readFileSync(tsconfigPath, "utf-8");
14697
14544
  const aliasMatches = content.match(/"@cms\//g);
@@ -14713,7 +14560,7 @@ function buildUninstallPlan(cwd) {
14713
14560
  const cssContent = fs35.readFileSync(cssFile, "utf-8");
14714
14561
  const sourceLines = cssContent.split("\n").filter((l) => /^@source\s+"[^"]*cms[^"]*";\s*$/.test(l));
14715
14562
  if (sourceLines.length > 0) {
14716
- const relCss = path40.relative(cwd, cssFile);
14563
+ const relCss = path39.relative(cwd, cssFile);
14717
14564
  steps.push({
14718
14565
  label: `CSS @source lines (${relCss})`,
14719
14566
  items: [`@source lines in ${relCss}`],
@@ -14725,7 +14572,7 @@ function buildUninstallPlan(cwd) {
14725
14572
  });
14726
14573
  }
14727
14574
  }
14728
- const envPath = path40.join(cwd, ".env.local");
14575
+ const envPath = path39.join(cwd, ".env.local");
14729
14576
  if (fs35.existsSync(envPath)) {
14730
14577
  const envContent = fs35.readFileSync(envPath, "utf-8");
14731
14578
  const bsVars = envContent.split("\n").filter((l) => l.trim().match(/^BETTERSTART_\w+=/)).map((l) => l.split("=")[0]);
@@ -14743,12 +14590,12 @@ function buildUninstallPlan(cwd) {
14743
14590
  }
14744
14591
  return steps;
14745
14592
  }
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();
14593
+ var uninstallCommand = new Command5("uninstall").description("Remove all CMS files and undo modifications made by betterstart init").option("-f, --force", "Skip all confirmation prompts", false).option("--cwd <path>", "Project root path").action(async (options) => {
14594
+ const cwd = options.cwd ? path39.resolve(options.cwd) : process.cwd();
14748
14595
  p5.intro(pc3.bgRed(pc3.white(" BetterStart Uninstall ")));
14749
14596
  const steps = buildUninstallPlan(cwd);
14750
14597
  if (steps.length === 0) {
14751
- p5.log.success(pc3.green("\u2713") + " Nothing to remove \u2014 project is already clean.");
14598
+ p5.log.success(`${pc3.green("\u2713")} Nothing to remove \u2014 project is already clean.`);
14752
14599
  p5.outro("Done");
14753
14600
  return;
14754
14601
  }
@@ -14776,13 +14623,39 @@ var uninstallCommand = new Command6("uninstall").description("Remove all CMS fil
14776
14623
  }
14777
14624
  const parts = steps.map((step) => `${step.count} ${step.unit}`);
14778
14625
  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
- );
14626
+ p5.note(pc3.dim("Database tables were NOT dropped \u2014 drop them manually if needed."), "Next steps");
14783
14627
  p5.outro("Uninstall complete");
14784
14628
  });
14785
14629
 
14630
+ // src/commands/update-deps.ts
14631
+ import path40 from "path";
14632
+ import * as clack2 from "@clack/prompts";
14633
+ import { Command as Command6 } from "commander";
14634
+ var updateDepsCommand = new Command6("update-deps").description("Install or update all CMS dependencies").option("--cwd <path>", "Project root path").action(async (options) => {
14635
+ const cwd = options.cwd ? path40.resolve(options.cwd) : process.cwd();
14636
+ clack2.intro("BetterStart Update Dependencies");
14637
+ const pm = detectPackageManager(cwd);
14638
+ clack2.log.info(`Package manager: ${pm}`);
14639
+ const config = await resolveConfig(cwd);
14640
+ const includeEmail = config.features?.email ?? true;
14641
+ const s = clack2.spinner();
14642
+ s.start("Installing dependencies...");
14643
+ const result = await installDependenciesAsync({
14644
+ cwd,
14645
+ pm,
14646
+ includeEmail,
14647
+ includeBiome: false
14648
+ });
14649
+ if (result.success) {
14650
+ s.stop(`Installed ${result.coreDeps.length} deps + ${result.devDeps.length} dev deps`);
14651
+ } else {
14652
+ s.stop("Dependency install failed");
14653
+ clack2.log.error(result.error ?? "Unknown error");
14654
+ process.exit(1);
14655
+ }
14656
+ clack2.outro("Dependencies updated");
14657
+ });
14658
+
14786
14659
  // src/commands/update-styles.ts
14787
14660
  import fs36 from "fs";
14788
14661
  import path41 from "path";