@exegia/corpora-ui 0.5.0
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/README.md +21 -0
- package/dist-lib/components/blocks/auth/auth-shell.d.ts +56 -0
- package/dist-lib/components/blocks/auth/code-auth-block.d.ts +22 -0
- package/dist-lib/components/blocks/auth/forgot-password-block.d.ts +15 -0
- package/dist-lib/components/blocks/auth/login-block.d.ts +25 -0
- package/dist-lib/components/blocks/auth/signup-block.d.ts +26 -0
- package/dist-lib/components/composed/password-input.d.ts +30 -0
- package/dist-lib/components/composed/social-providers.d.ts +22 -0
- package/dist-lib/components/docs/code-block.d.ts +4 -0
- package/dist-lib/components/docs/component-preview.d.ts +10 -0
- package/dist-lib/components/docs/demo-controls.d.ts +28 -0
- package/dist-lib/components/docs/props-table.d.ts +4 -0
- package/dist-lib/components/theme-provider.d.ts +15 -0
- package/dist-lib/components/ui/button.d.ts +28 -0
- package/dist-lib/components/ui/card.d.ts +16 -0
- package/dist-lib/components/ui/checkbox.d.ts +7 -0
- package/dist-lib/components/ui/field.d.ts +10 -0
- package/dist-lib/components/ui/frame.d.ts +7 -0
- package/dist-lib/components/ui/glasscn/glass-button.d.ts +6 -0
- package/dist-lib/components/ui/glasscn/liquid-glass.d.ts +31 -0
- package/dist-lib/components/ui/input-group.d.ts +13 -0
- package/dist-lib/components/ui/input.d.ts +9 -0
- package/dist-lib/components/ui/label.d.ts +11 -0
- package/dist-lib/components/ui/otp-field.d.ts +11 -0
- package/dist-lib/components/ui/separator.d.ts +4 -0
- package/dist-lib/components/ui/spinner.d.ts +3 -0
- package/dist-lib/components/ui/textarea.d.ts +8 -0
- package/dist-lib/index.d.ts +29 -0
- package/dist-lib/index.js +1617 -0
- package/dist-lib/index.js.map +1 -0
- package/dist-lib/lib/auth-accent.d.ts +33 -0
- package/dist-lib/lib/glass-variants.d.ts +6 -0
- package/dist-lib/lib/sound.d.ts +13 -0
- package/dist-lib/lib/utils.d.ts +2 -0
- package/dist-lib/lib/utils.test.d.ts +1 -0
- package/package.json +80 -0
- package/src/components/blocks/.gitkeep +0 -0
- package/src/components/blocks/auth/auth-shell.tsx +266 -0
- package/src/components/blocks/auth/code-auth-block.tsx +190 -0
- package/src/components/blocks/auth/forgot-password-block.tsx +123 -0
- package/src/components/blocks/auth/login-block.tsx +239 -0
- package/src/components/blocks/auth/signup-block.tsx +260 -0
- package/src/components/composed/.gitkeep +0 -0
- package/src/components/composed/password-input.tsx +179 -0
- package/src/components/composed/social-providers.tsx +89 -0
- package/src/components/docs/code-block.tsx +8 -0
- package/src/components/docs/component-preview.tsx +28 -0
- package/src/components/docs/demo-controls.tsx +118 -0
- package/src/components/docs/props-table.tsx +35 -0
- package/src/components/theme-provider.tsx +230 -0
- package/src/components/ui/button.tsx +199 -0
- package/src/components/ui/card.tsx +253 -0
- package/src/components/ui/checkbox.tsx +83 -0
- package/src/components/ui/field.tsx +89 -0
- package/src/components/ui/frame.tsx +87 -0
- package/src/components/ui/glasscn/glass-button.tsx +25 -0
- package/src/components/ui/glasscn/liquid-glass.tsx +276 -0
- package/src/components/ui/input-group.tsx +105 -0
- package/src/components/ui/input.tsx +70 -0
- package/src/components/ui/label.tsx +36 -0
- package/src/components/ui/otp-field.tsx +88 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/spinner.tsx +17 -0
- package/src/components/ui/textarea.tsx +58 -0
- package/src/index.css +171 -0
- package/src/index.ts +37 -0
- package/src/lib/auth-accent.ts +49 -0
- package/src/lib/glass-variants.ts +57 -0
- package/src/lib/sound.ts +39 -0
- package/src/lib/utils.test.ts +22 -0
- package/src/lib/utils.ts +6 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
RiAppleFill,
|
|
5
|
+
RiGithubFill,
|
|
6
|
+
RiGoogleFill,
|
|
7
|
+
RiTwitterXFill,
|
|
8
|
+
} from "@remixicon/react";
|
|
9
|
+
import type * as React from "react";
|
|
10
|
+
|
|
11
|
+
import { Button } from "@/components/ui/button";
|
|
12
|
+
import { cn } from "@/lib/utils";
|
|
13
|
+
|
|
14
|
+
export type SocialProvider = "google" | "apple" | "github" | "x";
|
|
15
|
+
|
|
16
|
+
const PROVIDERS: Record<
|
|
17
|
+
SocialProvider,
|
|
18
|
+
{ label: string; Icon: React.ComponentType<{ className?: string }> }
|
|
19
|
+
> = {
|
|
20
|
+
google: { label: "Google", Icon: RiGoogleFill },
|
|
21
|
+
apple: { label: "Apple", Icon: RiAppleFill },
|
|
22
|
+
github: { label: "GitHub", Icon: RiGithubFill },
|
|
23
|
+
x: { label: "X", Icon: RiTwitterXFill },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const ACTION_LABELS = {
|
|
27
|
+
login: "Login with",
|
|
28
|
+
signup: "Sign up with",
|
|
29
|
+
continue: "Continue with",
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export interface SocialProvidersProps {
|
|
33
|
+
providers?: SocialProvider[];
|
|
34
|
+
/** Verb used in the stacked layout labels. */
|
|
35
|
+
action?: keyof typeof ACTION_LABELS;
|
|
36
|
+
/** "stack" = full-width labeled buttons; "row" = icon-only buttons. */
|
|
37
|
+
layout?: "stack" | "row";
|
|
38
|
+
/** Shows the matching button in its loading state and disables the rest. */
|
|
39
|
+
loadingProvider?: SocialProvider | null;
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
onSelect?: (provider: SocialProvider) => void;
|
|
42
|
+
className?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Social auth buttons (coss particle p-button-38). UI only — wire onSelect. */
|
|
46
|
+
export function SocialProviders({
|
|
47
|
+
providers = ["google", "apple", "github"],
|
|
48
|
+
action = "continue",
|
|
49
|
+
layout = "stack",
|
|
50
|
+
loadingProvider = null,
|
|
51
|
+
disabled = false,
|
|
52
|
+
onSelect,
|
|
53
|
+
className,
|
|
54
|
+
}: SocialProvidersProps) {
|
|
55
|
+
const row = layout === "row";
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
className={cn(
|
|
60
|
+
"flex w-full gap-2",
|
|
61
|
+
row ? "flex-row justify-center" : "flex-col",
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
{providers.map((provider) => {
|
|
66
|
+
const { label, Icon } = PROVIDERS[provider];
|
|
67
|
+
return (
|
|
68
|
+
<Button
|
|
69
|
+
key={provider}
|
|
70
|
+
variant="outline"
|
|
71
|
+
size={row ? "icon" : "default"}
|
|
72
|
+
className={row ? undefined : "w-full"}
|
|
73
|
+
aria-label={`${ACTION_LABELS[action]} ${label}`}
|
|
74
|
+
loading={loadingProvider === provider}
|
|
75
|
+
disabled={disabled || (loadingProvider !== null && loadingProvider !== provider)}
|
|
76
|
+
onClick={() => onSelect?.(provider)}
|
|
77
|
+
>
|
|
78
|
+
<Icon aria-hidden="true" />
|
|
79
|
+
{!row && (
|
|
80
|
+
<span className="flex-1">
|
|
81
|
+
{ACTION_LABELS[action]} {label}
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
</Button>
|
|
85
|
+
);
|
|
86
|
+
})}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Plain code snippet shell — syntax highlighting can be layered in later. */
|
|
2
|
+
export function CodeBlock({ code }: { code: string }) {
|
|
3
|
+
return (
|
|
4
|
+
<pre className="overflow-x-auto rounded-lg border bg-muted/50 p-4 font-mono text-xs leading-relaxed">
|
|
5
|
+
<code>{code}</code>
|
|
6
|
+
</pre>
|
|
7
|
+
)
|
|
8
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import type { RegistryEntry } from "@/registry"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Renders an entry's lazy demo (created at registry module scope) inside a
|
|
7
|
+
* bordered canvas. Falls back to a placeholder while the demo is not
|
|
8
|
+
* implemented yet.
|
|
9
|
+
*/
|
|
10
|
+
export function ComponentPreview({ entry }: { entry: RegistryEntry }) {
|
|
11
|
+
const Demo = entry.preview
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex min-h-48 items-center justify-center rounded-lg border p-8">
|
|
15
|
+
{Demo ? (
|
|
16
|
+
<React.Suspense
|
|
17
|
+
fallback={<p className="text-sm text-muted-foreground">Loading…</p>}
|
|
18
|
+
>
|
|
19
|
+
<Demo />
|
|
20
|
+
</React.Suspense>
|
|
21
|
+
) : (
|
|
22
|
+
<p className="text-sm text-muted-foreground">
|
|
23
|
+
Preview coming soon ({entry.status}).
|
|
24
|
+
</p>
|
|
25
|
+
)}
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type * as React from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight playground controls for registry demos. Plain native elements
|
|
5
|
+
* on purpose — docs-only, never part of the published library.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function DemoSelect<T extends string>({
|
|
9
|
+
label,
|
|
10
|
+
value,
|
|
11
|
+
options,
|
|
12
|
+
onChange,
|
|
13
|
+
}: {
|
|
14
|
+
label: string
|
|
15
|
+
value: T
|
|
16
|
+
options: readonly T[]
|
|
17
|
+
onChange: (value: T) => void
|
|
18
|
+
}) {
|
|
19
|
+
return (
|
|
20
|
+
<label className="flex flex-col gap-1 text-xs text-muted-foreground">
|
|
21
|
+
{label}
|
|
22
|
+
<select
|
|
23
|
+
value={value}
|
|
24
|
+
onChange={(event) => onChange(event.target.value as T)}
|
|
25
|
+
className="h-7 rounded-md border bg-background px-2 text-xs text-foreground"
|
|
26
|
+
>
|
|
27
|
+
{options.map((option) => (
|
|
28
|
+
<option key={option} value={option}>
|
|
29
|
+
{option}
|
|
30
|
+
</option>
|
|
31
|
+
))}
|
|
32
|
+
</select>
|
|
33
|
+
</label>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Stand-in brand mark for demos of the auth blocks' `logo` slot. Deliberately
|
|
39
|
+
* unbranded — it exists to show the slot's spacing and default sizing, not to
|
|
40
|
+
* represent a real product.
|
|
41
|
+
*/
|
|
42
|
+
export function DemoBrandMark() {
|
|
43
|
+
return (
|
|
44
|
+
<svg
|
|
45
|
+
aria-label="Example logo"
|
|
46
|
+
role="img"
|
|
47
|
+
viewBox="0 0 32 32"
|
|
48
|
+
className="text-foreground"
|
|
49
|
+
>
|
|
50
|
+
<rect
|
|
51
|
+
x="2"
|
|
52
|
+
y="2"
|
|
53
|
+
width="28"
|
|
54
|
+
height="28"
|
|
55
|
+
rx="9"
|
|
56
|
+
fill="none"
|
|
57
|
+
stroke="currentColor"
|
|
58
|
+
strokeWidth="2.5"
|
|
59
|
+
/>
|
|
60
|
+
<path
|
|
61
|
+
d="M11 20.5V11.5h4.5a3.5 3.5 0 0 1 0 7H11"
|
|
62
|
+
fill="none"
|
|
63
|
+
stroke="currentColor"
|
|
64
|
+
strokeWidth="2.5"
|
|
65
|
+
strokeLinecap="round"
|
|
66
|
+
strokeLinejoin="round"
|
|
67
|
+
/>
|
|
68
|
+
</svg>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function DemoToggle({
|
|
73
|
+
label,
|
|
74
|
+
checked,
|
|
75
|
+
onChange,
|
|
76
|
+
}: {
|
|
77
|
+
label: string
|
|
78
|
+
checked: boolean
|
|
79
|
+
onChange: (checked: boolean) => void
|
|
80
|
+
}) {
|
|
81
|
+
return (
|
|
82
|
+
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
83
|
+
<input
|
|
84
|
+
type="checkbox"
|
|
85
|
+
checked={checked}
|
|
86
|
+
onChange={(event) => onChange(event.target.checked)}
|
|
87
|
+
/>
|
|
88
|
+
{label}
|
|
89
|
+
</label>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Standard layout: centered preview canvas above a wrapping controls row. */
|
|
94
|
+
export function DemoStage({
|
|
95
|
+
controls,
|
|
96
|
+
children,
|
|
97
|
+
canvasClassName,
|
|
98
|
+
}: {
|
|
99
|
+
controls: React.ReactNode
|
|
100
|
+
children: React.ReactNode
|
|
101
|
+
canvasClassName?: string
|
|
102
|
+
}) {
|
|
103
|
+
return (
|
|
104
|
+
<div className="flex w-full flex-col items-center gap-6">
|
|
105
|
+
<div
|
|
106
|
+
className={
|
|
107
|
+
canvasClassName ??
|
|
108
|
+
"flex min-h-28 w-full items-center justify-center rounded-lg"
|
|
109
|
+
}
|
|
110
|
+
>
|
|
111
|
+
{children}
|
|
112
|
+
</div>
|
|
113
|
+
<div className="flex flex-wrap items-end justify-center gap-x-4 gap-y-3">
|
|
114
|
+
{controls}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PropDef } from "@/registry"
|
|
2
|
+
|
|
3
|
+
export function PropsTable({ props }: { props: PropDef[] }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="overflow-x-auto rounded-lg border">
|
|
6
|
+
<table className="w-full text-left text-sm">
|
|
7
|
+
<thead className="border-b bg-muted/50">
|
|
8
|
+
<tr>
|
|
9
|
+
<th className="p-3 font-medium">Prop</th>
|
|
10
|
+
<th className="p-3 font-medium">Type</th>
|
|
11
|
+
<th className="p-3 font-medium">Default</th>
|
|
12
|
+
<th className="p-3 font-medium">Description</th>
|
|
13
|
+
</tr>
|
|
14
|
+
</thead>
|
|
15
|
+
<tbody>
|
|
16
|
+
{props.map((prop) => (
|
|
17
|
+
<tr key={prop.name} className="border-b last:border-b-0">
|
|
18
|
+
<td className="p-3 font-mono text-xs">
|
|
19
|
+
{prop.name}
|
|
20
|
+
{prop.required ? <span className="text-destructive">*</span> : null}
|
|
21
|
+
</td>
|
|
22
|
+
<td className="p-3 font-mono text-xs text-muted-foreground">
|
|
23
|
+
{prop.type}
|
|
24
|
+
</td>
|
|
25
|
+
<td className="p-3 font-mono text-xs text-muted-foreground">
|
|
26
|
+
{prop.default ?? "—"}
|
|
27
|
+
</td>
|
|
28
|
+
<td className="p-3 text-muted-foreground">{prop.description}</td>
|
|
29
|
+
</tr>
|
|
30
|
+
))}
|
|
31
|
+
</tbody>
|
|
32
|
+
</table>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/* eslint-disable react-refresh/only-export-components */
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
|
|
4
|
+
type Theme = "dark" | "light" | "system"
|
|
5
|
+
type ResolvedTheme = "dark" | "light"
|
|
6
|
+
|
|
7
|
+
type ThemeProviderProps = {
|
|
8
|
+
children: React.ReactNode
|
|
9
|
+
defaultTheme?: Theme
|
|
10
|
+
storageKey?: string
|
|
11
|
+
disableTransitionOnChange?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type ThemeProviderState = {
|
|
15
|
+
theme: Theme
|
|
16
|
+
setTheme: (theme: Theme) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)"
|
|
20
|
+
const THEME_VALUES: Theme[] = ["dark", "light", "system"]
|
|
21
|
+
|
|
22
|
+
const ThemeProviderContext = React.createContext<
|
|
23
|
+
ThemeProviderState | undefined
|
|
24
|
+
>(undefined)
|
|
25
|
+
|
|
26
|
+
function isTheme(value: string | null): value is Theme {
|
|
27
|
+
if (value === null) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return THEME_VALUES.includes(value as Theme)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getSystemTheme(): ResolvedTheme {
|
|
35
|
+
if (window.matchMedia(COLOR_SCHEME_QUERY).matches) {
|
|
36
|
+
return "dark"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return "light"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function disableTransitionsTemporarily() {
|
|
43
|
+
const style = document.createElement("style")
|
|
44
|
+
style.appendChild(
|
|
45
|
+
document.createTextNode(
|
|
46
|
+
"*,*::before,*::after{-webkit-transition:none!important;transition:none!important}"
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
document.head.appendChild(style)
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
window.getComputedStyle(document.body)
|
|
53
|
+
requestAnimationFrame(() => {
|
|
54
|
+
requestAnimationFrame(() => {
|
|
55
|
+
style.remove()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isEditableTarget(target: EventTarget | null) {
|
|
62
|
+
if (!(target instanceof HTMLElement)) {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (target.isContentEditable) {
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const editableParent = target.closest(
|
|
71
|
+
"input, textarea, select, [contenteditable='true']"
|
|
72
|
+
)
|
|
73
|
+
if (editableParent) {
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function ThemeProvider({
|
|
81
|
+
children,
|
|
82
|
+
defaultTheme = "system",
|
|
83
|
+
storageKey = "theme",
|
|
84
|
+
disableTransitionOnChange = true,
|
|
85
|
+
...props
|
|
86
|
+
}: ThemeProviderProps) {
|
|
87
|
+
const [theme, setThemeState] = React.useState<Theme>(() => {
|
|
88
|
+
const storedTheme = localStorage.getItem(storageKey)
|
|
89
|
+
if (isTheme(storedTheme)) {
|
|
90
|
+
return storedTheme
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return defaultTheme
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const setTheme = React.useCallback(
|
|
97
|
+
(nextTheme: Theme) => {
|
|
98
|
+
localStorage.setItem(storageKey, nextTheme)
|
|
99
|
+
setThemeState(nextTheme)
|
|
100
|
+
},
|
|
101
|
+
[storageKey]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const applyTheme = React.useCallback(
|
|
105
|
+
(nextTheme: Theme) => {
|
|
106
|
+
const root = document.documentElement
|
|
107
|
+
const resolvedTheme =
|
|
108
|
+
nextTheme === "system" ? getSystemTheme() : nextTheme
|
|
109
|
+
const restoreTransitions = disableTransitionOnChange
|
|
110
|
+
? disableTransitionsTemporarily()
|
|
111
|
+
: null
|
|
112
|
+
|
|
113
|
+
root.classList.remove("light", "dark")
|
|
114
|
+
root.classList.add(resolvedTheme)
|
|
115
|
+
|
|
116
|
+
if (restoreTransitions) {
|
|
117
|
+
restoreTransitions()
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
[disableTransitionOnChange]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
React.useEffect(() => {
|
|
124
|
+
applyTheme(theme)
|
|
125
|
+
|
|
126
|
+
if (theme !== "system") {
|
|
127
|
+
return undefined
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY)
|
|
131
|
+
const handleChange = () => {
|
|
132
|
+
applyTheme("system")
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
mediaQuery.addEventListener("change", handleChange)
|
|
136
|
+
|
|
137
|
+
return () => {
|
|
138
|
+
mediaQuery.removeEventListener("change", handleChange)
|
|
139
|
+
}
|
|
140
|
+
}, [theme, applyTheme])
|
|
141
|
+
|
|
142
|
+
React.useEffect(() => {
|
|
143
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
144
|
+
if (event.repeat) {
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (event.metaKey || event.ctrlKey || event.altKey) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (isEditableTarget(event.target)) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (event.key.toLowerCase() !== "d") {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
setThemeState((currentTheme) => {
|
|
161
|
+
const nextTheme =
|
|
162
|
+
currentTheme === "dark"
|
|
163
|
+
? "light"
|
|
164
|
+
: currentTheme === "light"
|
|
165
|
+
? "dark"
|
|
166
|
+
: getSystemTheme() === "dark"
|
|
167
|
+
? "light"
|
|
168
|
+
: "dark"
|
|
169
|
+
|
|
170
|
+
localStorage.setItem(storageKey, nextTheme)
|
|
171
|
+
return nextTheme
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
window.addEventListener("keydown", handleKeyDown)
|
|
176
|
+
|
|
177
|
+
return () => {
|
|
178
|
+
window.removeEventListener("keydown", handleKeyDown)
|
|
179
|
+
}
|
|
180
|
+
}, [storageKey])
|
|
181
|
+
|
|
182
|
+
React.useEffect(() => {
|
|
183
|
+
const handleStorageChange = (event: StorageEvent) => {
|
|
184
|
+
if (event.storageArea !== localStorage) {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (event.key !== storageKey) {
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (isTheme(event.newValue)) {
|
|
193
|
+
setThemeState(event.newValue)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
setThemeState(defaultTheme)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
window.addEventListener("storage", handleStorageChange)
|
|
201
|
+
|
|
202
|
+
return () => {
|
|
203
|
+
window.removeEventListener("storage", handleStorageChange)
|
|
204
|
+
}
|
|
205
|
+
}, [defaultTheme, storageKey])
|
|
206
|
+
|
|
207
|
+
const value = React.useMemo(
|
|
208
|
+
() => ({
|
|
209
|
+
theme,
|
|
210
|
+
setTheme,
|
|
211
|
+
}),
|
|
212
|
+
[theme, setTheme]
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<ThemeProviderContext.Provider {...props} value={value}>
|
|
217
|
+
{children}
|
|
218
|
+
</ThemeProviderContext.Provider>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const useTheme = () => {
|
|
223
|
+
const context = React.useContext(ThemeProviderContext)
|
|
224
|
+
|
|
225
|
+
if (context === undefined) {
|
|
226
|
+
throw new Error("useTheme must be used within a ThemeProvider")
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return context
|
|
230
|
+
}
|