create-croissant 0.1.24 → 0.1.26

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.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.24",
6
+ "version": "0.1.26",
7
7
  "description": "Scaffold a new project using the Croissant Stack",
8
8
  "repository": {
9
9
  "type": "git",
@@ -16,6 +16,8 @@ import {
16
16
  SidebarRail,
17
17
  } from "@workspace/ui/components/sidebar"
18
18
  import { Avatar, AvatarFallback, AvatarImage } from "@workspace/ui/components/avatar"
19
+ import { ModeToggle } from "@workspace/ui/components/mode-toggle"
20
+
19
21
  import { authClient } from "@/lib/auth-client"
20
22
 
21
23
  // This is sample data.
@@ -113,8 +115,9 @@ export function AppSidebar({ items = authNavItems, ...props }: AppSidebarProps)
113
115
  return (
114
116
  <Sidebar {...props}>
115
117
  <SidebarHeader>
116
- <div className="flex items-center gap-2 px-4 py-2">
118
+ <div className="flex items-center justify-between px-4 py-2">
117
119
  <span className="font-bold">Croissant Stack</span>
120
+ <ModeToggle />
118
121
  </div>
119
122
  </SidebarHeader>
120
123
  <SidebarContent>
@@ -163,25 +166,25 @@ export function AppSidebar({ items = authNavItems, ...props }: AppSidebarProps)
163
166
  <span className="truncate text-xs">{user.email}</span>
164
167
  </div>
165
168
  <div className="flex items-center gap-1 ml-auto">
166
- <Link
167
- to="/account"
168
- className="p-1 rounded hover:bg-sidebar-accent-foreground/10"
169
+ <SidebarMenuButton
170
+ render={<Link to="/account" />}
171
+ className="p-1 h-auto w-auto rounded hover:bg-sidebar-accent-foreground/10"
169
172
  title="Account Settings"
170
173
  >
171
174
  <Settings className="h-4 w-4" />
172
- </Link>
173
- <button
175
+ </SidebarMenuButton>
176
+ <SidebarMenuButton
174
177
  onClick={async (e) => {
175
178
  e.preventDefault()
176
179
  e.stopPropagation()
177
180
  await authClient.signOut()
178
181
  window.location.reload()
179
182
  }}
180
- className="p-1 rounded hover:bg-sidebar-accent-foreground/10"
183
+ className="p-1 h-auto w-auto rounded hover:bg-sidebar-accent-foreground/10"
181
184
  title="Sign Out"
182
185
  >
183
186
  <LogOut className="h-4 w-4" />
184
- </button>
187
+ </SidebarMenuButton>
185
188
  </div>
186
189
  </div>
187
190
  </SidebarMenuButton>
@@ -1,9 +1,11 @@
1
1
  import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router"
2
2
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
3
3
  import { Toaster } from "@workspace/ui/components/sonner"
4
+ import { ThemeProvider } from "@workspace/ui/components/theme-provider"
4
5
 
5
6
  import appCss from "@workspace/ui/globals.css?url"
6
7
 
8
+
7
9
  const queryClient = new QueryClient()
8
10
 
9
11
  export const Route = createRootRoute({
@@ -37,10 +39,13 @@ function RootDocument({ children }: { children: React.ReactNode }) {
37
39
  <HeadContent />
38
40
  </head>
39
41
  <body>
42
+ <ThemeProvider defaultTheme="system" storageKey="theme">
43
+
40
44
  <QueryClientProvider client={queryClient}>
41
45
  {children}
42
46
  <Toaster />
43
47
  </QueryClientProvider>
48
+ </ThemeProvider>
44
49
  <Scripts />
45
50
  </body>
46
51
  </html>
@@ -1,7 +1,17 @@
1
1
  import { createFileRoute, redirect } from "@tanstack/react-router"
2
+ import { createServerFn } from "@tanstack/react-start"
2
3
  import { getSessionFn } from "@/lib/auth-utils"
3
4
  import { orpc } from "@/lib/orpc"
4
5
 
6
+ const getSecretData = createServerFn({ method: "GET" }).handler(async () => {
7
+ try {
8
+ const secretData = await orpc.getSecretData()
9
+ return { secretData }
10
+ } catch (err) {
11
+ return { secretData: null, error: "Failed to fetch secret data" }
12
+ }
13
+ })
14
+
5
15
  export const Route = createFileRoute("/_auth/examples/ssr-orpc-auth")({
6
16
  beforeLoad: async () => {
7
17
  const session = await getSessionFn()
@@ -15,14 +25,7 @@ export const Route = createFileRoute("/_auth/examples/ssr-orpc-auth")({
15
25
  }
16
26
  return { session }
17
27
  },
18
- loader: async () => {
19
- try {
20
- const secretData = await orpc.getSecretData()
21
- return { secretData }
22
- } catch (err) {
23
- return { secretData: null, error: "Failed to fetch secret data" }
24
- }
25
- },
28
+ loader: () => getSecretData(),
26
29
  component: SSRORPCAuth,
27
30
  })
28
31
 
@@ -1,6 +1,20 @@
1
1
  import { createFileRoute } from "@tanstack/react-router"
2
+ import { createServerFn } from "@tanstack/react-start"
2
3
  import { orpc } from "@/lib/orpc"
3
4
 
5
+ const getISRData = createServerFn({ method: "GET" }).handler(async () => {
6
+ // In a real ISR scenario, this would be cached on the server
7
+ // For this example, we'll fetch planets via oRPC
8
+ const planets = await orpc.planets.getPlanets()
9
+
10
+ return {
11
+ generatedAt: new Date().toISOString(),
12
+ planets,
13
+ message:
14
+ "This page is an example of ISR. In a production build with proper configuration, this data would be cached and updated in the background.",
15
+ }
16
+ })
17
+
4
18
  export const Route = createFileRoute("/_public/examples/isr")({
5
19
  head: () => ({
6
20
  meta: [
@@ -9,21 +23,12 @@ export const Route = createFileRoute("/_public/examples/isr")({
9
23
  },
10
24
  {
11
25
  name: "description",
12
- content: "Experience high-performance page loads with ISR in Croissant Stack.",
26
+ content:
27
+ "Experience high-performance page loads with ISR in Croissant Stack.",
13
28
  },
14
29
  ],
15
30
  }),
16
- loader: async () => {
17
- // In a real ISR scenario, this would be cached on the server
18
- // For this example, we'll fetch planets via oRPC
19
- const planets = await orpc.planets.getPlanets()
20
-
21
- return {
22
- generatedAt: new Date().toISOString(),
23
- planets,
24
- message: "This page is an example of ISR. In a production build with proper configuration, this data would be cached and updated in the background.",
25
- }
26
- },
31
+ loader: () => getISRData(),
27
32
  headers: () => ({
28
33
  // Cache at CDN for 10 seconds, allow stale content for up to 1 minute
29
34
  "Cache-Control": "public, max-age=10, s-maxage=10, stale-while-revalidate=60",
@@ -1,5 +1,6 @@
1
1
  import * as React from "react"
2
2
  import { createFileRoute, useRouter } from "@tanstack/react-router"
3
+ import { createServerFn } from "@tanstack/react-start"
3
4
  import { Check, Pencil, Plus, Trash2 } from "lucide-react"
4
5
  import { toast } from "sonner"
5
6
  import { useForm } from "@tanstack/react-form"
@@ -27,6 +28,11 @@ const planetSchema = z.object({
27
28
  diameter: z.string().refine((val) => !isNaN(parseFloat(val)), "Must be a number"),
28
29
  })
29
30
 
31
+ const getPlanets = createServerFn({ method: "GET" }).handler(async () => {
32
+ const planets = await orpc.planets.getPlanets()
33
+ return { planets }
34
+ })
35
+
30
36
  export const Route = createFileRoute("/_public/examples/ssr-orpc")({
31
37
  head: () => ({
32
38
  meta: [
@@ -35,14 +41,12 @@ export const Route = createFileRoute("/_public/examples/ssr-orpc")({
35
41
  },
36
42
  {
37
43
  name: "description",
38
- content: "Learn how to use Server-Side Rendering (SSR) with oRPC in Croissant Stack.",
44
+ content:
45
+ "Learn how to use Server-Side Rendering (SSR) with oRPC in Croissant Stack.",
39
46
  },
40
47
  ],
41
48
  }),
42
- loader: async () => {
43
- const planets = await orpc.planets.getPlanets()
44
- return { planets }
45
- },
49
+ loader: () => getPlanets(),
46
50
  component: SSRORPC,
47
51
  })
48
52
 
@@ -1,4 +1,5 @@
1
1
  import { Link, createFileRoute } from "@tanstack/react-router"
2
+ import { createServerFn } from "@tanstack/react-start"
2
3
  import { Button } from "@workspace/ui/components/button"
3
4
  import type { router } from "@workspace/orpc/router"
4
5
  import type { InferRouterOutputs } from "@orpc/server"
@@ -7,6 +8,17 @@ import { orpc } from "@/lib/orpc"
7
8
  type Outputs = InferRouterOutputs<typeof router>
8
9
  type Planet = Outputs["planets"]["getPlanets"][number]
9
10
 
11
+ const getHomeData = createServerFn({ method: "GET" }).handler(async () => {
12
+ const [helloRes, planets] = await Promise.all([
13
+ orpc.hello({ name: "Croissant Stack" }),
14
+ orpc.planets.getPlanets(),
15
+ ])
16
+ return {
17
+ message: helloRes.message,
18
+ planets,
19
+ }
20
+ })
21
+
10
22
  export const Route = createFileRoute("/_public/")({
11
23
  head: () => ({
12
24
  meta: [
@@ -15,7 +27,8 @@ export const Route = createFileRoute("/_public/")({
15
27
  },
16
28
  {
17
29
  name: "description",
18
- content: "Build full-stack applications faster with Croissant Stack. Featuring TanStack Start, oRPC, and Better Auth.",
30
+ content:
31
+ "Build full-stack applications faster with Croissant Stack. Featuring TanStack Start, oRPC, and Better Auth.",
19
32
  },
20
33
  {
21
34
  property: "og:title",
@@ -31,18 +44,10 @@ export const Route = createFileRoute("/_public/")({
31
44
  },
32
45
  ],
33
46
  }),
34
- loader: async () => {
35
- const [helloRes, planets] = await Promise.all([
36
- orpc.hello({ name: "Croissant Stack" }),
37
- orpc.planets.getPlanets(),
38
- ])
39
- return {
40
- message: helloRes.message,
41
- planets,
42
- }
43
- },
47
+ loader: () => getHomeData(),
44
48
  headers: () => ({
45
- "Cache-Control": "public, max-age=10, s-maxage=10, stale-while-revalidate=60",
49
+ "Cache-Control":
50
+ "public, max-age=10, s-maxage=10, stale-while-revalidate=60",
46
51
  }),
47
52
  component: App,
48
53
  })
@@ -64,7 +69,7 @@ function App() {
64
69
  ) : (
65
70
  <ul className="grid grid-cols-1 gap-2">
66
71
  {planets.map((planet: Planet) => (
67
- <li key={planet.id} className="rounded-md border p-3 bg-white shadow-sm">
72
+ <li key={planet.id} className="rounded-md border p-3 shadow-sm">
68
73
  <span className="font-bold">{planet.name}</span> - {planet.description}
69
74
  </li>
70
75
  ))}
@@ -0,0 +1,39 @@
1
+ import { Moon, Sun } from "lucide-react"
2
+
3
+ import { Button } from "@workspace/ui/components/button"
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from "@workspace/ui/components/dropdown-menu"
10
+ import { useTheme } from "@workspace/ui/components/theme-provider"
11
+
12
+ export function ModeToggle() {
13
+ const { setTheme } = useTheme()
14
+
15
+ return (
16
+ <DropdownMenu>
17
+ <DropdownMenuTrigger
18
+ render={
19
+ <Button variant="outline" size="icon">
20
+ <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
21
+ <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
22
+ <span className="sr-only">Toggle theme</span>
23
+ </Button>
24
+ }
25
+ />
26
+ <DropdownMenuContent align="end">
27
+ <DropdownMenuItem onClick={() => setTheme("light")}>
28
+ Light
29
+ </DropdownMenuItem>
30
+ <DropdownMenuItem onClick={() => setTheme("dark")}>
31
+ Dark
32
+ </DropdownMenuItem>
33
+ <DropdownMenuItem onClick={() => setTheme("system")}>
34
+ System
35
+ </DropdownMenuItem>
36
+ </DropdownMenuContent>
37
+ </DropdownMenu>
38
+ )
39
+ }
@@ -506,18 +506,28 @@ function SidebarMenuButton({
506
506
  size = "default",
507
507
  tooltip,
508
508
  className,
509
+ onClick,
509
510
  ...props
510
511
  }: useRender.ComponentProps<"button"> &
511
512
  React.ComponentProps<"button"> & {
512
513
  isActive?: boolean
513
514
  tooltip?: string | React.ComponentProps<typeof TooltipContent>
514
515
  } & VariantProps<typeof sidebarMenuButtonVariants>) {
515
- const { isMobile, state } = useSidebar()
516
+ const { isMobile, state, setOpenMobile } = useSidebar()
517
+
518
+ const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
519
+ if (isMobile) {
520
+ setOpenMobile(false)
521
+ }
522
+ onClick?.(event)
523
+ }
524
+
516
525
  const comp = useRender({
517
526
  defaultTagName: "button",
518
527
  props: mergeProps<"button">(
519
528
  {
520
529
  className: cn(sidebarMenuButtonVariants({ variant, size }), className),
530
+ onClick: handleClick,
521
531
  },
522
532
  props
523
533
  ),
@@ -0,0 +1,93 @@
1
+ import { createContext, useContext, useEffect, useState } from "react"
2
+ import { ScriptOnce } from "@tanstack/react-router"
3
+
4
+ type Theme = "dark" | "light" | "system"
5
+
6
+ type ThemeProviderProps = {
7
+ children: React.ReactNode
8
+ defaultTheme?: Theme
9
+ storageKey?: string
10
+ }
11
+
12
+ type ThemeProviderState = {
13
+ theme: Theme
14
+ setTheme: (theme: Theme) => void
15
+ }
16
+
17
+ function getThemeScript(storageKey: string, defaultTheme: Theme) {
18
+ const key = JSON.stringify(storageKey)
19
+ const fallback = JSON.stringify(defaultTheme)
20
+
21
+ return `(function(){try{var t=localStorage.getItem(${key});if(t!=='light'&&t!=='dark'&&t!=='system'){t=${fallback}}var d=matchMedia('(prefers-color-scheme: dark)').matches;var r=t==='system'?(d?'dark':'light'):t;var e=document.documentElement;e.classList.add(r);e.style.colorScheme=r}catch(e){}})();`
22
+ }
23
+
24
+ const ThemeProviderContext = createContext<ThemeProviderState | undefined>(
25
+ undefined
26
+ )
27
+
28
+ function applyTheme(theme: Theme) {
29
+ const root = document.documentElement
30
+ root.classList.remove("light", "dark")
31
+
32
+ const resolved =
33
+ theme === "system"
34
+ ? window.matchMedia("(prefers-color-scheme: dark)").matches
35
+ ? "dark"
36
+ : "light"
37
+ : theme
38
+
39
+ root.classList.add(resolved)
40
+ root.style.colorScheme = resolved
41
+ }
42
+
43
+ export function ThemeProvider({
44
+ children,
45
+ defaultTheme = "system",
46
+ storageKey = "theme",
47
+ }: ThemeProviderProps) {
48
+ const [theme, setThemeState] = useState<Theme>(defaultTheme)
49
+ const [mounted, setMounted] = useState(false)
50
+
51
+ useEffect(() => {
52
+ const stored = localStorage.getItem(storageKey)
53
+ setThemeState(
54
+ stored === "light" || stored === "dark" || stored === "system"
55
+ ? stored
56
+ : defaultTheme
57
+ )
58
+ setMounted(true)
59
+ }, [defaultTheme, storageKey])
60
+
61
+ useEffect(() => {
62
+ if (!mounted) return
63
+ applyTheme(theme)
64
+ }, [theme, mounted])
65
+
66
+ useEffect(() => {
67
+ if (!mounted || theme !== "system") return
68
+
69
+ const media = window.matchMedia("(prefers-color-scheme: dark)")
70
+ const onChange = () => applyTheme("system")
71
+ media.addEventListener("change", onChange)
72
+ return () => media.removeEventListener("change", onChange)
73
+ }, [theme, mounted])
74
+
75
+ const setTheme = (next: Theme) => {
76
+ localStorage.setItem(storageKey, next)
77
+ setThemeState(next)
78
+ }
79
+
80
+ return (
81
+ <ThemeProviderContext value={{ theme, setTheme }}>
82
+ <ScriptOnce>{getThemeScript(storageKey, defaultTheme)}</ScriptOnce>
83
+ {children}
84
+ </ThemeProviderContext>
85
+ )
86
+ }
87
+
88
+ export function useTheme() {
89
+ const context = useContext(ThemeProviderContext)
90
+ if (context === undefined)
91
+ throw new Error("useTheme must be used within a ThemeProvider")
92
+ return context
93
+ }