create-croissant 0.1.25 → 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 +1 -1
- package/template/apps/web/src/components/app-sidebar.tsx +4 -1
- package/template/apps/web/src/routes/__root.tsx +5 -0
- package/template/apps/web/src/routes/_public/index.tsx +1 -1
- package/template/packages/ui/src/components/mode-toggle.tsx +39 -0
- package/template/packages/ui/src/components/theme-provider.tsx +93 -0
package/package.json
CHANGED
|
@@ -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
|
|
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>
|
|
@@ -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>
|
|
@@ -69,7 +69,7 @@ function App() {
|
|
|
69
69
|
) : (
|
|
70
70
|
<ul className="grid grid-cols-1 gap-2">
|
|
71
71
|
{planets.map((planet: Planet) => (
|
|
72
|
-
<li key={planet.id} className="rounded-md border p-3
|
|
72
|
+
<li key={planet.id} className="rounded-md border p-3 shadow-sm">
|
|
73
73
|
<span className="font-bold">{planet.name}</span> - {planet.description}
|
|
74
74
|
</li>
|
|
75
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
|
+
}
|
|
@@ -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
|
+
}
|