@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.
Files changed (124) hide show
  1. package/dist/chunk-HDGMSYQS.js +26461 -0
  2. package/dist/chunk-HDGMSYQS.js.map +1 -0
  3. package/dist/chunk-PR4QN5HX.js +39 -0
  4. package/dist/chunk-PR4QN5HX.js.map +1 -0
  5. package/dist/form.d.ts +175 -0
  6. package/dist/form.js +5207 -0
  7. package/dist/form.js.map +1 -0
  8. package/dist/index.d.ts +1462 -0
  9. package/dist/index.js +81862 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/input-CZvh825j.d.ts +24 -0
  12. package/dist/qr-code-styling-3Y6LZH6V.js +1123 -0
  13. package/dist/qr-code-styling-3Y6LZH6V.js.map +1 -0
  14. package/package.json +79 -0
  15. package/src/components/form/checkbox-group-field.tsx +101 -0
  16. package/src/components/form/date-field.tsx +79 -0
  17. package/src/components/form/date-range-field.tsx +106 -0
  18. package/src/components/form/form-context.ts +10 -0
  19. package/src/components/form/form.tsx +54 -0
  20. package/src/components/form/number-field.tsx +69 -0
  21. package/src/components/form/select-field.tsx +76 -0
  22. package/src/components/form/submit-button.tsx +28 -0
  23. package/src/components/form/text-field.tsx +107 -0
  24. package/src/components/layout/dashboard-header.tsx +54 -0
  25. package/src/components/layout/dashboard-panel.tsx +34 -0
  26. package/src/components/theme-provider.tsx +403 -0
  27. package/src/components/ui/accordion.tsx +69 -0
  28. package/src/components/ui/alert-dialog.tsx +169 -0
  29. package/src/components/ui/alert.tsx +80 -0
  30. package/src/components/ui/animated-theme-toggler.tsx +265 -0
  31. package/src/components/ui/app-store-buttons.tsx +182 -0
  32. package/src/components/ui/aspect-ratio.tsx +23 -0
  33. package/src/components/ui/autocomplete.tsx +296 -0
  34. package/src/components/ui/avatar-group.tsx +95 -0
  35. package/src/components/ui/avatar.tsx +285 -0
  36. package/src/components/ui/badge-group.tsx +160 -0
  37. package/src/components/ui/badge.tsx +172 -0
  38. package/src/components/ui/breadcrumb.tsx +112 -0
  39. package/src/components/ui/button.tsx +77 -0
  40. package/src/components/ui/calendar.tsx +137 -0
  41. package/src/components/ui/card.tsx +244 -0
  42. package/src/components/ui/carousel.tsx +258 -0
  43. package/src/components/ui/chart.tsx +379 -0
  44. package/src/components/ui/checkbox-group.tsx +16 -0
  45. package/src/components/ui/checkbox.tsx +82 -0
  46. package/src/components/ui/collapsible.tsx +45 -0
  47. package/src/components/ui/combobox.tsx +411 -0
  48. package/src/components/ui/command.tsx +264 -0
  49. package/src/components/ui/context-menu.tsx +271 -0
  50. package/src/components/ui/credit-card.tsx +214 -0
  51. package/src/components/ui/dialog.tsx +196 -0
  52. package/src/components/ui/drawer.tsx +135 -0
  53. package/src/components/ui/empty.tsx +127 -0
  54. package/src/components/ui/featured-icon.tsx +149 -0
  55. package/src/components/ui/field.tsx +88 -0
  56. package/src/components/ui/fieldset.tsx +29 -0
  57. package/src/components/ui/form.tsx +17 -0
  58. package/src/components/ui/frame.tsx +82 -0
  59. package/src/components/ui/generic-empty.tsx +142 -0
  60. package/src/components/ui/group.tsx +97 -0
  61. package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
  62. package/src/components/ui/input-group.tsx +102 -0
  63. package/src/components/ui/input-otp.tsx +96 -0
  64. package/src/components/ui/input.tsx +66 -0
  65. package/src/components/ui/item.tsx +198 -0
  66. package/src/components/ui/kbd.tsx +30 -0
  67. package/src/components/ui/label.tsx +28 -0
  68. package/src/components/ui/menu.tsx +312 -0
  69. package/src/components/ui/menubar.tsx +93 -0
  70. package/src/components/ui/meter.tsx +67 -0
  71. package/src/components/ui/multi-select.tsx +308 -0
  72. package/src/components/ui/navigation-menu.tsx +143 -0
  73. package/src/components/ui/number-field.tsx +160 -0
  74. package/src/components/ui/pagination-controls.tsx +74 -0
  75. package/src/components/ui/pagination.tsx +149 -0
  76. package/src/components/ui/popover.tsx +119 -0
  77. package/src/components/ui/preview-card.tsx +55 -0
  78. package/src/components/ui/progress.tsx +289 -0
  79. package/src/components/ui/qr-code.tsx +150 -0
  80. package/src/components/ui/radio-group.tsx +103 -0
  81. package/src/components/ui/resizable.tsx +56 -0
  82. package/src/components/ui/scroll-area.tsx +90 -0
  83. package/src/components/ui/scroller.tsx +38 -0
  84. package/src/components/ui/section-header.tsx +118 -0
  85. package/src/components/ui/select.tsx +181 -0
  86. package/src/components/ui/separator.tsx +23 -0
  87. package/src/components/ui/sheet.tsx +224 -0
  88. package/src/components/ui/sidebar.tsx +744 -0
  89. package/src/components/ui/skeleton.tsx +16 -0
  90. package/src/components/ui/slider.tsx +108 -0
  91. package/src/components/ui/smooth-scroll.tsx +143 -0
  92. package/src/components/ui/social-button.tsx +247 -0
  93. package/src/components/ui/spinner-on-demand.tsx +32 -0
  94. package/src/components/ui/spinner.tsx +18 -0
  95. package/src/components/ui/stat.tsx +187 -0
  96. package/src/components/ui/stepper.tsx +167 -0
  97. package/src/components/ui/switch.tsx +56 -0
  98. package/src/components/ui/table.tsx +126 -0
  99. package/src/components/ui/tabs.tsx +90 -0
  100. package/src/components/ui/tag.tsx +229 -0
  101. package/src/components/ui/target-countdown.tsx +46 -0
  102. package/src/components/ui/text-editor.tsx +313 -0
  103. package/src/components/ui/textarea.tsx +51 -0
  104. package/src/components/ui/timeline.tsx +116 -0
  105. package/src/components/ui/toast.tsx +268 -0
  106. package/src/components/ui/toggle-group.tsx +101 -0
  107. package/src/components/ui/toggle.tsx +45 -0
  108. package/src/components/ui/toolbar.tsx +89 -0
  109. package/src/components/ui/tooltip.tsx +102 -0
  110. package/src/components/ui/vertical-scroll-fader.tsx +250 -0
  111. package/src/components/ui/video-player.tsx +275 -0
  112. package/src/components/upload/avatar-upload-base.tsx +131 -0
  113. package/src/components/upload/image-upload-base.tsx +112 -0
  114. package/src/form.ts +17 -0
  115. package/src/index.ts +125 -0
  116. package/src/lib/hooks/use-callback-ref.ts +15 -0
  117. package/src/lib/hooks/use-first-render.ts +11 -0
  118. package/src/lib/hooks/use-hover.ts +53 -0
  119. package/src/lib/hooks/use-is-tab-active.ts +17 -0
  120. package/src/lib/hooks/use-media-query.ts +164 -0
  121. package/src/lib/utils/css.ts +6 -0
  122. package/src/styles.css +300 -0
  123. package/src/types/helpers.ts +24 -0
  124. 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
+ };