@cntyclub/ui-react 0.1.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/dist/chunk-HDGMSYQS.js +26461 -0
- package/dist/chunk-HDGMSYQS.js.map +1 -0
- package/dist/chunk-PR4QN5HX.js +39 -0
- package/dist/chunk-PR4QN5HX.js.map +1 -0
- package/dist/form.d.ts +175 -0
- package/dist/form.js +5207 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +1462 -0
- package/dist/index.js +81862 -0
- package/dist/index.js.map +1 -0
- package/dist/input-CZvh825j.d.ts +24 -0
- package/dist/qr-code-styling-3Y6LZH6V.js +1123 -0
- package/dist/qr-code-styling-3Y6LZH6V.js.map +1 -0
- package/package.json +79 -0
- package/src/components/form/checkbox-group-field.tsx +101 -0
- package/src/components/form/date-field.tsx +79 -0
- package/src/components/form/date-range-field.tsx +106 -0
- package/src/components/form/form-context.ts +10 -0
- package/src/components/form/form.tsx +54 -0
- package/src/components/form/number-field.tsx +69 -0
- package/src/components/form/select-field.tsx +76 -0
- package/src/components/form/submit-button.tsx +28 -0
- package/src/components/form/text-field.tsx +107 -0
- package/src/components/layout/dashboard-header.tsx +54 -0
- package/src/components/layout/dashboard-panel.tsx +34 -0
- package/src/components/theme-provider.tsx +403 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +169 -0
- package/src/components/ui/alert.tsx +80 -0
- package/src/components/ui/animated-theme-toggler.tsx +265 -0
- package/src/components/ui/app-store-buttons.tsx +182 -0
- package/src/components/ui/aspect-ratio.tsx +23 -0
- package/src/components/ui/autocomplete.tsx +296 -0
- package/src/components/ui/avatar-group.tsx +95 -0
- package/src/components/ui/avatar.tsx +285 -0
- package/src/components/ui/badge-group.tsx +160 -0
- package/src/components/ui/badge.tsx +172 -0
- package/src/components/ui/breadcrumb.tsx +112 -0
- package/src/components/ui/button.tsx +77 -0
- package/src/components/ui/calendar.tsx +137 -0
- package/src/components/ui/card.tsx +244 -0
- package/src/components/ui/carousel.tsx +258 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/checkbox-group.tsx +16 -0
- package/src/components/ui/checkbox.tsx +82 -0
- package/src/components/ui/collapsible.tsx +45 -0
- package/src/components/ui/combobox.tsx +411 -0
- package/src/components/ui/command.tsx +264 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/credit-card.tsx +214 -0
- package/src/components/ui/dialog.tsx +196 -0
- package/src/components/ui/drawer.tsx +135 -0
- package/src/components/ui/empty.tsx +127 -0
- package/src/components/ui/featured-icon.tsx +149 -0
- package/src/components/ui/field.tsx +88 -0
- package/src/components/ui/fieldset.tsx +29 -0
- package/src/components/ui/form.tsx +17 -0
- package/src/components/ui/frame.tsx +82 -0
- package/src/components/ui/generic-empty.tsx +142 -0
- package/src/components/ui/group.tsx +97 -0
- package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
- package/src/components/ui/input-group.tsx +102 -0
- package/src/components/ui/input-otp.tsx +96 -0
- package/src/components/ui/input.tsx +66 -0
- package/src/components/ui/item.tsx +198 -0
- package/src/components/ui/kbd.tsx +30 -0
- package/src/components/ui/label.tsx +28 -0
- package/src/components/ui/menu.tsx +312 -0
- package/src/components/ui/menubar.tsx +93 -0
- package/src/components/ui/meter.tsx +67 -0
- package/src/components/ui/multi-select.tsx +308 -0
- package/src/components/ui/navigation-menu.tsx +143 -0
- package/src/components/ui/number-field.tsx +160 -0
- package/src/components/ui/pagination-controls.tsx +74 -0
- package/src/components/ui/pagination.tsx +149 -0
- package/src/components/ui/popover.tsx +119 -0
- package/src/components/ui/preview-card.tsx +55 -0
- package/src/components/ui/progress.tsx +289 -0
- package/src/components/ui/qr-code.tsx +150 -0
- package/src/components/ui/radio-group.tsx +103 -0
- package/src/components/ui/resizable.tsx +56 -0
- package/src/components/ui/scroll-area.tsx +90 -0
- package/src/components/ui/scroller.tsx +38 -0
- package/src/components/ui/section-header.tsx +118 -0
- package/src/components/ui/select.tsx +181 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +224 -0
- package/src/components/ui/sidebar.tsx +744 -0
- package/src/components/ui/skeleton.tsx +16 -0
- package/src/components/ui/slider.tsx +108 -0
- package/src/components/ui/smooth-scroll.tsx +143 -0
- package/src/components/ui/social-button.tsx +247 -0
- package/src/components/ui/spinner-on-demand.tsx +32 -0
- package/src/components/ui/spinner.tsx +18 -0
- package/src/components/ui/stat.tsx +187 -0
- package/src/components/ui/stepper.tsx +167 -0
- package/src/components/ui/switch.tsx +56 -0
- package/src/components/ui/table.tsx +126 -0
- package/src/components/ui/tabs.tsx +90 -0
- package/src/components/ui/tag.tsx +229 -0
- package/src/components/ui/target-countdown.tsx +46 -0
- package/src/components/ui/text-editor.tsx +313 -0
- package/src/components/ui/textarea.tsx +51 -0
- package/src/components/ui/timeline.tsx +116 -0
- package/src/components/ui/toast.tsx +268 -0
- package/src/components/ui/toggle-group.tsx +101 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/toolbar.tsx +89 -0
- package/src/components/ui/tooltip.tsx +102 -0
- package/src/components/ui/vertical-scroll-fader.tsx +250 -0
- package/src/components/ui/video-player.tsx +275 -0
- package/src/components/upload/avatar-upload-base.tsx +131 -0
- package/src/components/upload/image-upload-base.tsx +112 -0
- package/src/form.ts +17 -0
- package/src/index.ts +125 -0
- package/src/lib/hooks/use-callback-ref.ts +15 -0
- package/src/lib/hooks/use-first-render.ts +11 -0
- package/src/lib/hooks/use-hover.ts +53 -0
- package/src/lib/hooks/use-is-tab-active.ts +17 -0
- package/src/lib/hooks/use-media-query.ts +164 -0
- package/src/lib/utils/css.ts +6 -0
- package/src/styles.css +300 -0
- package/src/types/helpers.ts +24 -0
- package/src/types/react.d.ts +7 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { StandardSchemaV1Issue } from "@tanstack/react-form";
|
|
4
|
+
import {
|
|
5
|
+
Field,
|
|
6
|
+
FieldControl,
|
|
7
|
+
FieldError,
|
|
8
|
+
FieldLabel,
|
|
9
|
+
} from "../ui/field";
|
|
10
|
+
import { Input } from "../ui/input";
|
|
11
|
+
import {
|
|
12
|
+
InputGroup,
|
|
13
|
+
InputGroupAddon,
|
|
14
|
+
InputGroupInput,
|
|
15
|
+
} from "../ui/input-group";
|
|
16
|
+
|
|
17
|
+
export function TextField({
|
|
18
|
+
field,
|
|
19
|
+
label,
|
|
20
|
+
placeholder,
|
|
21
|
+
type = "text",
|
|
22
|
+
inlineLabel,
|
|
23
|
+
leadingIcon,
|
|
24
|
+
trailingIcon,
|
|
25
|
+
}: {
|
|
26
|
+
field: {
|
|
27
|
+
name: string;
|
|
28
|
+
state: {
|
|
29
|
+
value: string;
|
|
30
|
+
meta: {
|
|
31
|
+
readonly errors: ReadonlyArray<
|
|
32
|
+
string | StandardSchemaV1Issue | undefined
|
|
33
|
+
>;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
handleChange: (nextValue: string) => void;
|
|
37
|
+
handleBlur: () => void;
|
|
38
|
+
};
|
|
39
|
+
label: string;
|
|
40
|
+
placeholder?: string;
|
|
41
|
+
type?: React.ComponentProps<typeof Input>["type"];
|
|
42
|
+
/** Render label inside the input border as a block-start addon */
|
|
43
|
+
inlineLabel?: boolean;
|
|
44
|
+
/** Content rendered as an inline-start addon (e.g. an icon) */
|
|
45
|
+
leadingIcon?: React.ReactNode;
|
|
46
|
+
/** Content rendered as an inline-end addon (e.g. an icon) */
|
|
47
|
+
trailingIcon?: React.ReactNode;
|
|
48
|
+
}) {
|
|
49
|
+
const errors = field.state.meta.errors
|
|
50
|
+
.map((err) => (typeof err === "string" ? err : err?.message))
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
|
|
53
|
+
const useInputGroup = inlineLabel || leadingIcon || trailingIcon;
|
|
54
|
+
|
|
55
|
+
if (useInputGroup) {
|
|
56
|
+
return (
|
|
57
|
+
<Field name={field.name}>
|
|
58
|
+
{!inlineLabel && <FieldLabel>{label}</FieldLabel>}
|
|
59
|
+
<InputGroup>
|
|
60
|
+
{inlineLabel && (
|
|
61
|
+
<InputGroupAddon align="block-start">
|
|
62
|
+
<FieldLabel>{label}</FieldLabel>
|
|
63
|
+
</InputGroupAddon>
|
|
64
|
+
)}
|
|
65
|
+
{leadingIcon && (
|
|
66
|
+
<InputGroupAddon align="inline-start">
|
|
67
|
+
{leadingIcon}
|
|
68
|
+
</InputGroupAddon>
|
|
69
|
+
)}
|
|
70
|
+
<FieldControl
|
|
71
|
+
render={
|
|
72
|
+
<InputGroupInput
|
|
73
|
+
value={field.state.value}
|
|
74
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
75
|
+
onBlur={field.handleBlur}
|
|
76
|
+
placeholder={placeholder}
|
|
77
|
+
type={type}
|
|
78
|
+
/>
|
|
79
|
+
}
|
|
80
|
+
/>
|
|
81
|
+
{trailingIcon && (
|
|
82
|
+
<InputGroupAddon align="inline-end">{trailingIcon}</InputGroupAddon>
|
|
83
|
+
)}
|
|
84
|
+
</InputGroup>
|
|
85
|
+
<FieldError match={!!errors.length}>{errors.join(", ")}</FieldError>
|
|
86
|
+
</Field>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Field name={field.name}>
|
|
92
|
+
<FieldLabel>{label}</FieldLabel>
|
|
93
|
+
<FieldControl
|
|
94
|
+
render={
|
|
95
|
+
<Input
|
|
96
|
+
value={field.state.value}
|
|
97
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
98
|
+
onBlur={field.handleBlur}
|
|
99
|
+
placeholder={placeholder}
|
|
100
|
+
type={type}
|
|
101
|
+
/>
|
|
102
|
+
}
|
|
103
|
+
/>
|
|
104
|
+
<FieldError match={!!errors.length}>{errors.join(", ")}</FieldError>
|
|
105
|
+
</Field>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Slot, Slottable } from "@radix-ui/react-slot";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { isValidElement } from "react";
|
|
4
|
+
import type { AsChildProps } from "../../types/helpers";
|
|
5
|
+
|
|
6
|
+
interface DashboardHeaderProps extends Omit<AsChildProps<"div">, "title"> {
|
|
7
|
+
title?: React.ReactNode;
|
|
8
|
+
subtitle?: React.ReactNode;
|
|
9
|
+
rightActions?: React.ReactNode;
|
|
10
|
+
breadcrumb?: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function DashboardHeader({
|
|
14
|
+
title,
|
|
15
|
+
subtitle,
|
|
16
|
+
className,
|
|
17
|
+
children,
|
|
18
|
+
asChild,
|
|
19
|
+
rightActions,
|
|
20
|
+
breadcrumb,
|
|
21
|
+
...props
|
|
22
|
+
}: DashboardHeaderProps) {
|
|
23
|
+
const TitleComp = isValidElement(title) ? Slot : "div";
|
|
24
|
+
const Comp = asChild ? Slot : "div";
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Comp
|
|
28
|
+
className={clsx("flex flex-col border-b rounded-t-lg", className)}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
<div className={clsx("flex flex-col gap-4 p-4")}>
|
|
32
|
+
{breadcrumb}
|
|
33
|
+
{(title || subtitle || rightActions) && (
|
|
34
|
+
<div className="flex flex-col gap-2">
|
|
35
|
+
<div className="flex items-center gap-3 min-h-8">
|
|
36
|
+
{title && (
|
|
37
|
+
<TitleComp className="grow min-w-0 text-lg font-medium leading-7 tracking-[-0.18px] text-foreground">
|
|
38
|
+
{title}
|
|
39
|
+
</TitleComp>
|
|
40
|
+
)}
|
|
41
|
+
{rightActions}
|
|
42
|
+
</div>
|
|
43
|
+
{subtitle && (
|
|
44
|
+
<div className="hidden md:block text-sm font-normal leading-5 text-muted-foreground">
|
|
45
|
+
{subtitle}
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
<Slottable>{children}</Slottable>
|
|
51
|
+
</div>
|
|
52
|
+
</Comp>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import type { AsChildProps } from "../../types/helpers";
|
|
4
|
+
import { ScrollArea } from "../ui/scroll-area";
|
|
5
|
+
|
|
6
|
+
interface DashboardPanelProps extends AsChildProps<"div"> {
|
|
7
|
+
header?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function DashboardPanel({
|
|
11
|
+
className,
|
|
12
|
+
asChild,
|
|
13
|
+
children,
|
|
14
|
+
header,
|
|
15
|
+
...props
|
|
16
|
+
}: DashboardPanelProps) {
|
|
17
|
+
const Comp = asChild ? Slot : "div";
|
|
18
|
+
return (
|
|
19
|
+
<Comp
|
|
20
|
+
className={clsx(
|
|
21
|
+
"flex flex-col grow min-h-0 min-w-0 bg-background rounded border",
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
{header}
|
|
27
|
+
<ScrollArea className="flex grow flex-col min-h-0 *:data-[slot=scroll-area-viewport]:p-4">
|
|
28
|
+
{children}
|
|
29
|
+
</ScrollArea>
|
|
30
|
+
</Comp>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default DashboardPanel;
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
This file is adapted from next-themes to work with tanstack start.
|
|
5
|
+
next-themes can be found at https://github.com/pacocoursey/next-themes under the MIT license.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createContext,
|
|
10
|
+
memo,
|
|
11
|
+
useCallback,
|
|
12
|
+
useContext,
|
|
13
|
+
useEffect,
|
|
14
|
+
useMemo,
|
|
15
|
+
useState,
|
|
16
|
+
} from "react";
|
|
17
|
+
|
|
18
|
+
interface ValueObject {
|
|
19
|
+
[themeName: string]: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UseThemeProps {
|
|
23
|
+
/** List of all available theme names */
|
|
24
|
+
themes: string[];
|
|
25
|
+
/** Forced theme name for the current page */
|
|
26
|
+
forcedTheme?: string | undefined;
|
|
27
|
+
/** Update the theme */
|
|
28
|
+
setTheme: React.Dispatch<React.SetStateAction<string>>;
|
|
29
|
+
/** Active theme name */
|
|
30
|
+
theme?: string | undefined;
|
|
31
|
+
/** If `enableSystem` is true and the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical to `theme` */
|
|
32
|
+
resolvedTheme?: string | undefined;
|
|
33
|
+
/** If enableSystem is true, returns the System theme preference ("dark" or "light"), regardless what the active theme is */
|
|
34
|
+
systemTheme?: "dark" | "light" | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type Attribute = `data-${string}` | "class";
|
|
38
|
+
|
|
39
|
+
export interface ThemeProviderProps extends React.PropsWithChildren {
|
|
40
|
+
/** List of all available theme names */
|
|
41
|
+
themes?: string[] | undefined;
|
|
42
|
+
/** Forced theme name for the current page */
|
|
43
|
+
forcedTheme?: string | undefined;
|
|
44
|
+
/** Whether to switch between dark and light themes based on prefers-color-scheme */
|
|
45
|
+
enableSystem?: boolean | undefined;
|
|
46
|
+
/** Disable all CSS transitions when switching themes */
|
|
47
|
+
disableTransitionOnChange?: boolean | undefined;
|
|
48
|
+
/** Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons */
|
|
49
|
+
enableColorScheme?: boolean | undefined;
|
|
50
|
+
/** Key used to store theme setting in localStorage */
|
|
51
|
+
storageKey?: string | undefined;
|
|
52
|
+
/** Default theme name (for v0.0.12 and lower the default was light). If `enableSystem` is false, the default theme is light */
|
|
53
|
+
defaultTheme?: string | undefined;
|
|
54
|
+
/** HTML attribute modified based on the active theme. Accepts `class`, `data-*` (meaning any data attribute, `data-mode`, `data-color`, etc.), or an array which could include both */
|
|
55
|
+
attribute?: Attribute | Attribute[] | undefined;
|
|
56
|
+
/** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */
|
|
57
|
+
value?: ValueObject | undefined;
|
|
58
|
+
/** Nonce string to pass to the inline script for CSP headers */
|
|
59
|
+
nonce?: string | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const colorSchemes = ["light", "dark"];
|
|
63
|
+
const MEDIA = "(prefers-color-scheme: dark)";
|
|
64
|
+
const isServer = typeof window === "undefined";
|
|
65
|
+
const ThemeContext = createContext<UseThemeProps | undefined>(undefined);
|
|
66
|
+
const defaultContext: UseThemeProps = { setTheme: (_) => {}, themes: [] };
|
|
67
|
+
|
|
68
|
+
export const useTheme = () => useContext(ThemeContext) ?? defaultContext;
|
|
69
|
+
|
|
70
|
+
export const ThemeProvider = (props: ThemeProviderProps): React.ReactNode => {
|
|
71
|
+
const context = useContext(ThemeContext);
|
|
72
|
+
|
|
73
|
+
// Ignore nested context providers, just passthrough children
|
|
74
|
+
if (context) return props.children;
|
|
75
|
+
return <Theme {...props} />;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const defaultThemes = ["light", "dark"];
|
|
79
|
+
|
|
80
|
+
export const ThemeContextOverride = ({
|
|
81
|
+
children,
|
|
82
|
+
value,
|
|
83
|
+
}: {
|
|
84
|
+
children: React.ReactNode;
|
|
85
|
+
value: string;
|
|
86
|
+
}) => {
|
|
87
|
+
const context = useContext(ThemeContext);
|
|
88
|
+
|
|
89
|
+
if (!context) return children;
|
|
90
|
+
return (
|
|
91
|
+
<ThemeContext.Provider
|
|
92
|
+
value={{
|
|
93
|
+
...context,
|
|
94
|
+
resolvedTheme: value,
|
|
95
|
+
forcedTheme: value,
|
|
96
|
+
theme: value,
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
{children}
|
|
100
|
+
</ThemeContext.Provider>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const Theme = ({
|
|
105
|
+
forcedTheme,
|
|
106
|
+
disableTransitionOnChange = false,
|
|
107
|
+
enableSystem = true,
|
|
108
|
+
enableColorScheme = true,
|
|
109
|
+
storageKey = "theme",
|
|
110
|
+
themes = defaultThemes,
|
|
111
|
+
defaultTheme = enableSystem ? "system" : "light",
|
|
112
|
+
attribute = "data-theme",
|
|
113
|
+
value,
|
|
114
|
+
children,
|
|
115
|
+
nonce,
|
|
116
|
+
}: ThemeProviderProps) => {
|
|
117
|
+
const [theme, setThemeState] = useState(() =>
|
|
118
|
+
getTheme(storageKey, defaultTheme),
|
|
119
|
+
);
|
|
120
|
+
const [resolvedTheme, setResolvedTheme] = useState(() =>
|
|
121
|
+
theme === "system" ? getSystemTheme() : theme,
|
|
122
|
+
);
|
|
123
|
+
const attrs = !value ? themes : Object.values(value);
|
|
124
|
+
|
|
125
|
+
// apply selected theme function (light, dark, system)
|
|
126
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Code is copied from the library
|
|
127
|
+
const applyTheme = useCallback((theme: string | undefined) => {
|
|
128
|
+
let resolved = theme;
|
|
129
|
+
if (!resolved) return;
|
|
130
|
+
|
|
131
|
+
// If theme is system, resolve it before setting theme
|
|
132
|
+
if (theme === "system" && enableSystem) {
|
|
133
|
+
resolved = getSystemTheme();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const name = value ? value[resolved] : resolved;
|
|
137
|
+
const enable = disableTransitionOnChange ? disableAnimation() : null;
|
|
138
|
+
const d = document.documentElement;
|
|
139
|
+
|
|
140
|
+
const handleAttribute = (attr: Attribute) => {
|
|
141
|
+
if (attr === "class") {
|
|
142
|
+
d.classList.remove(...attrs);
|
|
143
|
+
if (name) d.classList.add(name);
|
|
144
|
+
} else if (attr.startsWith("data-")) {
|
|
145
|
+
if (name) {
|
|
146
|
+
d.setAttribute(attr, name);
|
|
147
|
+
} else {
|
|
148
|
+
d.removeAttribute(attr);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (Array.isArray(attribute)) attribute.forEach(handleAttribute);
|
|
154
|
+
else handleAttribute(attribute);
|
|
155
|
+
|
|
156
|
+
if (enableColorScheme) {
|
|
157
|
+
const fallback = colorSchemes.includes(defaultTheme)
|
|
158
|
+
? defaultTheme
|
|
159
|
+
: null;
|
|
160
|
+
const colorScheme = colorSchemes.includes(resolved) ? resolved : fallback;
|
|
161
|
+
// @ts-expect-error
|
|
162
|
+
d.style.colorScheme = colorScheme;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
enable?.();
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
// Set theme state and save to local storage
|
|
169
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Code is copied from the library
|
|
170
|
+
const setTheme = useCallback(
|
|
171
|
+
// biome-ignore lint/suspicious/noExplicitAny: Code is copied from the library
|
|
172
|
+
(value: any) => {
|
|
173
|
+
const newTheme = typeof value === "function" ? value(theme) : value;
|
|
174
|
+
setThemeState(newTheme);
|
|
175
|
+
|
|
176
|
+
// Save to storage
|
|
177
|
+
try {
|
|
178
|
+
localStorage.setItem(storageKey, newTheme);
|
|
179
|
+
} catch {
|
|
180
|
+
// Unsupported
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
[theme],
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Code is copied from the library
|
|
187
|
+
const handleMediaQuery = useCallback(
|
|
188
|
+
(e: MediaQueryListEvent | MediaQueryList) => {
|
|
189
|
+
const resolved = getSystemTheme(e);
|
|
190
|
+
setResolvedTheme(resolved);
|
|
191
|
+
|
|
192
|
+
if (theme === "system" && enableSystem && !forcedTheme) {
|
|
193
|
+
applyTheme("system");
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
[theme, forcedTheme],
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Always listen to System preference
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
const media = window.matchMedia(MEDIA);
|
|
202
|
+
|
|
203
|
+
// Intentionally use deprecated listener methods to support iOS & old browsers
|
|
204
|
+
media.addListener(handleMediaQuery);
|
|
205
|
+
handleMediaQuery(media);
|
|
206
|
+
|
|
207
|
+
return () => media.removeListener(handleMediaQuery);
|
|
208
|
+
}, [handleMediaQuery]);
|
|
209
|
+
|
|
210
|
+
// localStorage event handling, allow to sync theme changes between tabs
|
|
211
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: The code is copied
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
const handleStorage = (e: StorageEvent) => {
|
|
214
|
+
if (e.key !== storageKey) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// If default theme set, use it if localstorage === null (happens on local storage manual deletion)
|
|
219
|
+
const theme = e.newValue || defaultTheme;
|
|
220
|
+
setTheme(theme);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
window.addEventListener("storage", handleStorage);
|
|
224
|
+
return () => window.removeEventListener("storage", handleStorage);
|
|
225
|
+
}, [setTheme]);
|
|
226
|
+
|
|
227
|
+
// Whenever theme or forcedTheme changes, apply it
|
|
228
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: The code is copied
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
applyTheme(forcedTheme ?? theme);
|
|
231
|
+
}, [forcedTheme, theme]);
|
|
232
|
+
|
|
233
|
+
const providerValue = useMemo(
|
|
234
|
+
() => ({
|
|
235
|
+
theme,
|
|
236
|
+
setTheme,
|
|
237
|
+
forcedTheme,
|
|
238
|
+
themes: enableSystem ? [...themes, "system"] : themes,
|
|
239
|
+
resolvedTheme: theme === "system" ? resolvedTheme : theme,
|
|
240
|
+
systemTheme: (enableSystem ? resolvedTheme : undefined) as
|
|
241
|
+
| "light"
|
|
242
|
+
| "dark"
|
|
243
|
+
| undefined,
|
|
244
|
+
}),
|
|
245
|
+
[theme, setTheme, forcedTheme, enableSystem, themes, resolvedTheme],
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<ThemeContext.Provider value={providerValue}>
|
|
250
|
+
<ThemeScript
|
|
251
|
+
{...{
|
|
252
|
+
forcedTheme,
|
|
253
|
+
storageKey,
|
|
254
|
+
attribute,
|
|
255
|
+
enableSystem,
|
|
256
|
+
enableColorScheme,
|
|
257
|
+
defaultTheme,
|
|
258
|
+
value,
|
|
259
|
+
themes,
|
|
260
|
+
nonce,
|
|
261
|
+
}}
|
|
262
|
+
/>
|
|
263
|
+
{children}
|
|
264
|
+
</ThemeContext.Provider>
|
|
265
|
+
);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const ThemeScript = memo(
|
|
269
|
+
({
|
|
270
|
+
forcedTheme,
|
|
271
|
+
storageKey,
|
|
272
|
+
attribute,
|
|
273
|
+
enableSystem,
|
|
274
|
+
enableColorScheme,
|
|
275
|
+
defaultTheme,
|
|
276
|
+
value,
|
|
277
|
+
themes,
|
|
278
|
+
nonce,
|
|
279
|
+
}: Omit<ThemeProviderProps, "children"> & { defaultTheme: string }) => {
|
|
280
|
+
const scriptArgs = JSON.stringify([
|
|
281
|
+
attribute,
|
|
282
|
+
storageKey,
|
|
283
|
+
defaultTheme,
|
|
284
|
+
forcedTheme,
|
|
285
|
+
themes,
|
|
286
|
+
value,
|
|
287
|
+
enableSystem,
|
|
288
|
+
enableColorScheme,
|
|
289
|
+
]).slice(1, -1);
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<script
|
|
293
|
+
suppressHydrationWarning
|
|
294
|
+
nonce={typeof window === "undefined" ? nonce : ""}
|
|
295
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: Needed to inject script before hydration
|
|
296
|
+
dangerouslySetInnerHTML={{
|
|
297
|
+
__html: `(${script.toString()})(${scriptArgs})`,
|
|
298
|
+
}}
|
|
299
|
+
/>
|
|
300
|
+
// <></>
|
|
301
|
+
);
|
|
302
|
+
},
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Helpers
|
|
306
|
+
const getTheme = (key: string, fallback?: string) => {
|
|
307
|
+
if (isServer) return undefined;
|
|
308
|
+
let theme: string | undefined;
|
|
309
|
+
try {
|
|
310
|
+
theme = localStorage.getItem(key) || undefined;
|
|
311
|
+
} catch {
|
|
312
|
+
// Unsupported
|
|
313
|
+
}
|
|
314
|
+
return theme || fallback;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const disableAnimation = () => {
|
|
318
|
+
const css = document.createElement("style");
|
|
319
|
+
css.appendChild(
|
|
320
|
+
document.createTextNode(
|
|
321
|
+
"*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}",
|
|
322
|
+
),
|
|
323
|
+
);
|
|
324
|
+
document.head.appendChild(css);
|
|
325
|
+
|
|
326
|
+
return () => {
|
|
327
|
+
// Force restyle
|
|
328
|
+
(() => window.getComputedStyle(document.body))();
|
|
329
|
+
|
|
330
|
+
// Wait for next tick before removing
|
|
331
|
+
setTimeout(() => {
|
|
332
|
+
document.head.removeChild(css);
|
|
333
|
+
}, 1);
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const getSystemTheme = (e?: MediaQueryList | MediaQueryListEvent) => {
|
|
338
|
+
const event = e ?? window.matchMedia(MEDIA);
|
|
339
|
+
const isDark = event.matches;
|
|
340
|
+
const systemTheme = isDark ? "dark" : "light";
|
|
341
|
+
return systemTheme;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/*
|
|
345
|
+
This file is adapted from next-themes to work with tanstack start.
|
|
346
|
+
next-themes can be found at https://github.com/pacocoursey/next-themes under the MIT license.
|
|
347
|
+
*/
|
|
348
|
+
|
|
349
|
+
// biome-ignore lint/suspicious/noExplicitAny: Code is copied from the library
|
|
350
|
+
export const script: (...args: any[]) => void = (
|
|
351
|
+
attribute,
|
|
352
|
+
storageKey,
|
|
353
|
+
defaultTheme,
|
|
354
|
+
forcedTheme,
|
|
355
|
+
themes,
|
|
356
|
+
value,
|
|
357
|
+
enableSystem,
|
|
358
|
+
enableColorScheme,
|
|
359
|
+
) => {
|
|
360
|
+
const el = document.documentElement;
|
|
361
|
+
const systemThemes = ["light", "dark"];
|
|
362
|
+
const isClass = attribute === "class";
|
|
363
|
+
const classes =
|
|
364
|
+
isClass && value
|
|
365
|
+
? themes.map((t: string | number) => value[t] || t)
|
|
366
|
+
: themes;
|
|
367
|
+
|
|
368
|
+
function updateDOM(theme: string) {
|
|
369
|
+
if (isClass) {
|
|
370
|
+
el.classList.remove(...classes);
|
|
371
|
+
el.classList.add(theme);
|
|
372
|
+
} else {
|
|
373
|
+
el.setAttribute(attribute, theme);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
setColorScheme(theme);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function setColorScheme(theme: string) {
|
|
380
|
+
if (enableColorScheme && systemThemes.includes(theme)) {
|
|
381
|
+
el.style.colorScheme = theme;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getSystemTheme() {
|
|
386
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
387
|
+
? "dark"
|
|
388
|
+
: "light";
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (forcedTheme) {
|
|
392
|
+
updateDOM(forcedTheme);
|
|
393
|
+
} else {
|
|
394
|
+
try {
|
|
395
|
+
const themeName = localStorage.getItem(storageKey) || defaultTheme;
|
|
396
|
+
const isSystem = enableSystem && themeName === "system";
|
|
397
|
+
const theme = isSystem ? getSystemTheme() : themeName;
|
|
398
|
+
updateDOM(theme);
|
|
399
|
+
} catch {
|
|
400
|
+
//
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
|
|
4
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils/css";
|
|
7
|
+
|
|
8
|
+
function Accordion(props: AccordionPrimitive.Root.Props) {
|
|
9
|
+
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
|
|
13
|
+
return (
|
|
14
|
+
<AccordionPrimitive.Item
|
|
15
|
+
className={cn("border-b last:border-b-0", className)}
|
|
16
|
+
data-slot="accordion-item"
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function AccordionTrigger({
|
|
23
|
+
className,
|
|
24
|
+
children,
|
|
25
|
+
...props
|
|
26
|
+
}: AccordionPrimitive.Trigger.Props) {
|
|
27
|
+
return (
|
|
28
|
+
<AccordionPrimitive.Header className="flex">
|
|
29
|
+
<AccordionPrimitive.Trigger
|
|
30
|
+
className={cn(
|
|
31
|
+
"flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left font-medium text-sm outline-none transition-all focus-visible:ring-[3px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-64 data-panel-open:*:data-[slot=accordion-indicator]:rotate-180",
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
data-slot="accordion-trigger"
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
<ChevronDownIcon
|
|
39
|
+
className="pointer-events-none size-4 shrink-0 translate-y-0.5 opacity-80 transition-transform duration-200 ease-in-out"
|
|
40
|
+
data-slot="accordion-indicator"
|
|
41
|
+
/>
|
|
42
|
+
</AccordionPrimitive.Trigger>
|
|
43
|
+
</AccordionPrimitive.Header>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function AccordionPanel({
|
|
48
|
+
className,
|
|
49
|
+
children,
|
|
50
|
+
...props
|
|
51
|
+
}: AccordionPrimitive.Panel.Props) {
|
|
52
|
+
return (
|
|
53
|
+
<AccordionPrimitive.Panel
|
|
54
|
+
className="h-(--accordion-panel-height) overflow-hidden text-muted-foreground text-sm transition-[height] duration-200 ease-in-out data-ending-style:h-0 data-starting-style:h-0"
|
|
55
|
+
data-slot="accordion-panel"
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
|
59
|
+
</AccordionPrimitive.Panel>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
Accordion,
|
|
65
|
+
AccordionItem,
|
|
66
|
+
AccordionTrigger,
|
|
67
|
+
AccordionPanel,
|
|
68
|
+
AccordionPanel as AccordionContent,
|
|
69
|
+
};
|