@noya-app/noya-designsystem 0.1.39 → 0.1.40

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 (50) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +10 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +416 -321
  5. package/dist/index.d.ts +416 -321
  6. package/dist/index.js +9558 -11324
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +5694 -7465
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +4 -4
  11. package/src/components/ActivityIndicator.tsx +13 -4
  12. package/src/components/AnimatePresence.tsx +261 -0
  13. package/src/components/Avatar.tsx +202 -80
  14. package/src/components/Button.tsx +10 -6
  15. package/src/components/Checkbox.tsx +6 -1
  16. package/src/components/Chip.tsx +88 -132
  17. package/src/components/{InputFieldWithCompletions.tsx → Combobox.tsx} +10 -3
  18. package/src/components/ContextMenu.tsx +9 -5
  19. package/src/components/Dialog.tsx +20 -23
  20. package/src/components/Divider.tsx +15 -7
  21. package/src/components/DraggableMenuButton.tsx +9 -5
  22. package/src/components/DropdownMenu.tsx +8 -5
  23. package/src/components/FillInputField.tsx +5 -1
  24. package/src/components/FillPreviewBackground.tsx +1 -5
  25. package/src/components/GridView.tsx +71 -46
  26. package/src/components/InputField.tsx +140 -75
  27. package/src/components/InspectorPrimitives.tsx +17 -11
  28. package/src/components/Label.tsx +5 -1
  29. package/src/components/ListView.tsx +28 -15
  30. package/src/components/Progress.tsx +10 -7
  31. package/src/components/SelectMenu.tsx +20 -8
  32. package/src/components/Slider.tsx +70 -8
  33. package/src/components/Sortable.tsx +3 -3
  34. package/src/components/Switch.tsx +19 -2
  35. package/src/components/Text.tsx +5 -1
  36. package/src/components/TextArea.tsx +5 -1
  37. package/src/components/TreeView.tsx +4 -10
  38. package/src/components/UserPointer.tsx +170 -0
  39. package/src/components/WorkspaceLayout.tsx +5 -0
  40. package/src/components/internal/Menu.tsx +14 -5
  41. package/src/components/internal/TextInput.tsx +13 -0
  42. package/src/contexts/DialogContext.tsx +30 -18
  43. package/src/hooks/useTheme.ts +50 -0
  44. package/src/index.css +16 -13
  45. package/src/index.tsx +14 -26
  46. package/src/utils/classNames.ts +5 -0
  47. package/src/utils/colorFromString.ts +44 -0
  48. package/tailwind.config.ts +16 -13
  49. package/src/hooks/useDarkMode.ts +0 -14
  50. package/src/utils/tailwind.ts +0 -9
@@ -33,6 +33,7 @@ interface Props {
33
33
  hasLeftSidebar?: boolean;
34
34
  onChangeLayoutState?: (layoutState: PanelLayoutState) => void;
35
35
  leftSidebarCanResize?: boolean;
36
+ rightSidebarCanResize?: boolean;
36
37
  id?: string;
37
38
  className?: string;
38
39
  style?: React.CSSProperties;
@@ -74,6 +75,7 @@ const WorkspaceLayoutWithTheme = forwardRef(function WorkspaceLayoutWithTheme(
74
75
  leftSidebarMinSize,
75
76
  rightSidebarMinSize,
76
77
  leftSidebarCanResize = false,
78
+ rightSidebarCanResize = false,
77
79
  id,
78
80
  className,
79
81
  style,
@@ -261,6 +263,9 @@ const WorkspaceLayoutWithTheme = forwardRef(function WorkspaceLayoutWithTheme(
261
263
  ref={rightSidebarRef}
262
264
  minSize={rightSidebarMinPercentage ?? rightSidebarPercentage}
263
265
  defaultSize={rightSidebarPercentage}
266
+ {...(!rightSidebarCanResize && {
267
+ maxSize: rightSidebarPercentage,
268
+ })}
264
269
  collapsible
265
270
  style={{
266
271
  display: "flex",
@@ -3,7 +3,6 @@ import React, { memo, ReactNode } from "react";
3
3
  import { useDesignSystemConfiguration } from "../../contexts/DesignSystemConfiguration";
4
4
  import withSeparatorElements from "../../utils/withSeparatorElements";
5
5
  import { IconName } from "../Icons";
6
- import { cn } from "../../utils/tailwind";
7
6
  import { textStyles } from "../Text";
8
7
 
9
8
  export const SEPARATOR_ITEM = "separator";
@@ -108,7 +107,7 @@ export const styles = {
108
107
  shadow-[0_2px_4px_rgba(0,0,0,0.2),_0_0_12px_rgba(0,0,0,0.1)]
109
108
  p-1
110
109
  border border-popover-divider
111
- z-50
110
+ z-menu
112
111
  `,
113
112
  };
114
113
 
@@ -135,9 +134,19 @@ export function getKeyboardShortcutsForMenuItems<T extends string>(
135
134
  );
136
135
  }
137
136
 
138
- const ShortcutElement = ({children, fixedWidth}: {children?: React.ReactNode; fixedWidth?: boolean}) => (
139
- <kbd className={cn(textStyles.small, "text-text-disabled", fixedWidth && "w-[0.9rem] text-center")}>{children}</kbd>
140
- )
137
+ const ShortcutElement = ({
138
+ children,
139
+ fixedWidth,
140
+ }: {
141
+ children?: React.ReactNode;
142
+ fixedWidth?: boolean;
143
+ }) => (
144
+ <kbd
145
+ className={`${textStyles.small} text-text-disabled ${fixedWidth && "w-[0.9rem] text-center"}`}
146
+ >
147
+ {children}
148
+ </kbd>
149
+ );
141
150
 
142
151
  export const KeyboardShortcut = memo(function KeyboardShortcut({
143
152
  shortcut,
@@ -60,6 +60,8 @@ const ReadOnlyTextInput = forwardRef(function ReadOnlyTextInput(
60
60
 
61
61
  type ControlledProps = Props & {
62
62
  onChange: (value: string) => void;
63
+ onFocus?: FocusEventHandler;
64
+ onBlur?: FocusEventHandler;
63
65
  };
64
66
 
65
67
  const ControlledTextInput = forwardRef(function ControlledTextInput(
@@ -70,6 +72,7 @@ const ControlledTextInput = forwardRef(function ControlledTextInput(
70
72
  onFocusChange,
71
73
  onBlur,
72
74
  onFocusCapture,
75
+ onFocus,
73
76
  ...rest
74
77
  }: ControlledProps,
75
78
  forwardedRef: ForwardedRef<HTMLInputElement>
@@ -92,6 +95,15 @@ const ControlledTextInput = forwardRef(function ControlledTextInput(
92
95
  [onFocusCapture, onFocusChange]
93
96
  );
94
97
 
98
+ const handleFocus = useCallback(
99
+ (event: React.FocusEvent<HTMLInputElement>) => {
100
+ onFocus?.(event);
101
+
102
+ onFocusChange?.(true);
103
+ },
104
+ [onFocus, onFocusChange]
105
+ );
106
+
95
107
  return (
96
108
  <input
97
109
  ref={forwardedRef}
@@ -105,6 +117,7 @@ const ControlledTextInput = forwardRef(function ControlledTextInput(
105
117
  )}
106
118
  onBlur={handleBlur}
107
119
  onFocusCapture={handleFocusCapture}
120
+ onFocus={handleFocus}
108
121
  />
109
122
  );
110
123
  });
@@ -26,8 +26,14 @@ function createDeferredPromise<T>() {
26
26
 
27
27
  export type DialogContextValue = {
28
28
  openInputDialog(
29
- title: string,
30
- inputValue?: string
29
+ options:
30
+ | string
31
+ | {
32
+ title: string;
33
+ description?: ReactNode;
34
+ placeholder?: string;
35
+ initialValue?: string;
36
+ }
31
37
  ): Promise<string | undefined>;
32
38
 
33
39
  containsElement(element: HTMLElement): boolean;
@@ -43,6 +49,8 @@ export const DialogProvider = function DialogProvider({
43
49
  const [contents, setContents] = useState<
44
50
  | {
45
51
  title: string;
52
+ description?: ReactNode;
53
+ placeholder?: string;
46
54
  inputValue: string;
47
55
  resolve: (value: string | undefined) => void;
48
56
  }
@@ -64,23 +72,25 @@ export const DialogProvider = function DialogProvider({
64
72
  setContents(undefined);
65
73
  }, [contents]);
66
74
 
67
- const open: DialogContextValue["openInputDialog"] = useCallback(
68
- (title, inputValue = "") => {
69
- const { promise, resolve } = createDeferredPromise<string | undefined>();
75
+ const open: DialogContextValue["openInputDialog"] = useCallback((options) => {
76
+ const { promise, resolve } = createDeferredPromise<string | undefined>();
70
77
 
71
- setContents({
72
- title,
73
- inputValue,
74
- resolve: (value) => {
75
- resolve(value);
76
- setContents(undefined);
77
- },
78
- });
78
+ const { title, description, initialValue, placeholder } =
79
+ typeof options === "string" ? { title: options } : options;
79
80
 
80
- return promise;
81
- },
82
- []
83
- );
81
+ setContents({
82
+ title,
83
+ description,
84
+ inputValue: initialValue ?? "",
85
+ placeholder,
86
+ resolve: (value) => {
87
+ resolve(value);
88
+ setContents(undefined);
89
+ },
90
+ });
91
+
92
+ return promise;
93
+ }, []);
84
94
 
85
95
  const handleKeyDown = useCallback(
86
96
  (event: React.KeyboardEvent<HTMLInputElement>) => {
@@ -116,6 +126,7 @@ export const DialogProvider = function DialogProvider({
116
126
  <Dialog
117
127
  ref={dialogRef}
118
128
  title={contents?.title}
129
+ description={contents?.description}
119
130
  open={isOpen}
120
131
  onOpenChange={useCallback(
121
132
  (isOpen: boolean) => {
@@ -138,6 +149,7 @@ export const DialogProvider = function DialogProvider({
138
149
  <InputField.Root>
139
150
  <InputField.Input
140
151
  ref={inputRef}
152
+ placeholder={contents?.placeholder}
141
153
  value={contents?.inputValue ?? ""}
142
154
  onChange={(value) => {
143
155
  setContents((contents) =>
@@ -153,7 +165,7 @@ export const DialogProvider = function DialogProvider({
153
165
  />
154
166
  </InputField.Root>
155
167
  <Spacer.Vertical size={20} />
156
- <div className="flex-1 flex-row items-center">
168
+ <div className="flex flex-1 flex-row items-center">
157
169
  <Spacer.Horizontal />
158
170
  <Button onClick={close}>Cancel</Button>
159
171
  <Spacer.Horizontal size={16} />
@@ -0,0 +1,50 @@
1
+ import { useState, useEffect } from "react";
2
+
3
+ /** @default light */
4
+ export type UseThemeReturnType = "dark" | "light";
5
+
6
+ /**
7
+ * Hook to get the current theme of the app via the `data-theme` attribute on the body element.
8
+ * @returns The current theme.
9
+ */
10
+ export function useTheme() {
11
+ const [theme, setTheme] = useState<UseThemeReturnType>("light");
12
+
13
+ const checkTheme = () => {
14
+ const currentTheme = document.body.getAttribute("data-theme");
15
+ switch (currentTheme) {
16
+ case "dark":
17
+ setTheme("dark");
18
+ break;
19
+ case "light":
20
+ setTheme("light");
21
+ break;
22
+ }
23
+ };
24
+
25
+ useEffect(() => {
26
+ checkTheme();
27
+
28
+ // Setup observer for theme changes
29
+ const observer = new MutationObserver((mutations) => {
30
+ mutations.forEach((mutation) => {
31
+ if (
32
+ mutation.type === "attributes" &&
33
+ mutation.attributeName === "data-theme"
34
+ ) {
35
+ checkTheme();
36
+ }
37
+ });
38
+ });
39
+
40
+ // Observe body element for theme changes
41
+ observer.observe(document.body, {
42
+ attributes: true,
43
+ attributeFilter: ["data-theme"],
44
+ });
45
+
46
+ return () => observer.disconnect();
47
+ }, []);
48
+
49
+ return theme;
50
+ }
package/src/index.css CHANGED
@@ -38,7 +38,7 @@
38
38
  --listview-raised-background: rgba(0, 0, 0, 0.03);
39
39
  --listview-editing-background: #fff;
40
40
  --slider-background: white;
41
- --slider-border: #bbb;
41
+ --slider-border: #9698ac;
42
42
  --radio-group-background: white;
43
43
  --mask: rgb(12, 193, 67);
44
44
  --transparent-checker: rgba(255, 255, 255, 0.8);
@@ -63,19 +63,21 @@
63
63
  --icon-selected: rgb(220, 220, 220);
64
64
  --warning: rgb(251, 211, 0);
65
65
  --radio-group-item: rgb(139, 139, 139);
66
- --dot: rgba(0,0,0,0.25);
67
- --row-highlight: #3390FF10;
66
+ --dot: rgba(0, 0, 0, 0.25);
67
+ --row-highlight: #3390ff10;
68
68
  --table-row-background: var(--background);
69
69
  --interactable-z-index: 2;
70
70
  --label-z-index: 3;
71
- --chip-primary-bg: rgba(238,229,255,0.2);
72
- --chip-secondary-bg: rgba(205,238,231,0.2);
73
- --chip-error-bg: rgba(255,219,219,0.2);
74
- --chip-default-bg: rgba(0,0,0,0.1);
75
- --chip-primary-shadow: rgb(238,229,255);
76
- --chip-secondary-shadow: rgb(205,238,231);
77
- --chip-error-shadow: rgb(255,219,219);
78
- --chip-default-shadow: rgb(0,0,0);
71
+ --menu-z-index: 5000;
72
+ --chip-primary-bg: rgba(238, 229, 255, 0.2);
73
+ --chip-secondary-bg: rgba(205, 238, 231, 0.2);
74
+ --chip-error-bg: rgba(255, 219, 219, 0.2);
75
+ --chip-default-bg: rgba(0, 0, 0, 0.1);
76
+ --chip-primary-shadow: rgb(238, 229, 255);
77
+ --chip-secondary-shadow: rgb(205, 238, 231);
78
+ --chip-error-shadow: rgb(255, 219, 219);
79
+ --chip-default-shadow: rgb(0, 0, 0);
80
+ --floating-button: rgb(248, 248, 250);
79
81
  }
80
82
 
81
83
  [data-theme="dark"] {
@@ -113,6 +115,7 @@
113
115
  --active-background: rgba(181, 178, 255, 0.08);
114
116
  --thumbnail-background: #1f1d33;
115
117
  --thumbnail-shadow: #1f1d3366;
116
- --dot: rgba(255,255,255,0.15);
118
+ --dot: rgba(255, 255, 255, 0.15);
117
119
  --table-row-background: var(--sidebar-background);
118
- }
120
+ --floating-button: #333333;
121
+ }
package/src/index.tsx CHANGED
@@ -1,24 +1,11 @@
1
- // These types propagate generics through memo and forwardRef to support generic components
2
- //
3
- // https://stackoverflow.com/questions/60386614/how-to-use-props-with-generics-with-react-memo/60389122#60389122
4
- // https://stackoverflow.com/questions/58469229/react-with-typescript-generics-while-using-react-forwardref/58473012
5
- declare module "react" {
6
- function memo<A, B>(
7
- Component: (props: A) => B
8
- ): (props: A) => React.ReactElement | null;
9
-
10
- function forwardRef<T, P = {}>(
11
- render: (props: P, ref: React.ForwardedRef<T>) => React.ReactElement | null
12
- ): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
13
- }
14
-
15
- // Theme
16
1
  // Components
17
2
  export * from "./components/ActivityIndicator";
3
+ export * from "./components/AnimatePresence";
18
4
  export * from "./components/Avatar";
19
5
  export * from "./components/Button";
20
6
  export * from "./components/Checkbox";
21
7
  export * from "./components/Chip";
8
+ export * from "./components/Combobox";
22
9
  export * from "./components/ContextMenu";
23
10
  export * from "./components/Dialog";
24
11
  export * from "./components/Divider";
@@ -32,8 +19,13 @@ export * from "./components/GridView";
32
19
  export * from "./components/IconButton";
33
20
  export * from "./components/Icons";
34
21
  export * from "./components/InputField";
35
- export * from "./components/InputFieldWithCompletions";
36
22
  export * from "./components/InspectorContainer";
23
+ export { KeyboardShortcut, SEPARATOR_ITEM } from "./components/internal/Menu";
24
+ export type {
25
+ ExtractMenuItemType,
26
+ MenuItem,
27
+ RegularMenuItem,
28
+ } from "./components/internal/Menu";
37
29
  export * from "./components/Label";
38
30
  export * from "./components/LabeledElementView";
39
31
  export * from "./components/ListView";
@@ -52,30 +44,26 @@ export * from "./components/TextArea";
52
44
  export * from "./components/Toast";
53
45
  export * from "./components/Tooltip";
54
46
  export * from "./components/TreeView";
47
+ export * from "./components/UserPointer";
55
48
  export * from "./components/WorkspaceLayout";
56
- export { KeyboardShortcut, SEPARATOR_ITEM } from "./components/internal/Menu";
57
- export type {
58
- ExtractMenuItemType,
59
- MenuItem,
60
- RegularMenuItem,
61
- } from "./components/internal/Menu";
62
49
  // Contexts
63
50
  export * from "./contexts/DesignSystemConfiguration";
64
51
  export * from "./contexts/DialogContext";
65
52
  export * from "./contexts/FloatingWindowContext";
66
- export * from "./contexts/ImageDataContext";
67
53
  export * from "./contexts/GlobalInputBlurContext";
54
+ export * from "./contexts/ImageDataContext";
68
55
  // Hooks
69
56
  export * from "./hooks/useHover";
70
57
  export * from "./hooks/usePlatform";
58
+ export * from "./hooks/usePreservePanelSize";
59
+ export * from "./hooks/useTheme";
71
60
  export * from "./mediaQuery";
72
61
  export * from "./theme";
73
- export { cn } from './utils/tailwind';
62
+ export * from "./utils/classNames";
74
63
  export * from "./utils/createSectionedMenu";
75
64
  export * from "./utils/getGradientBackground";
76
- export * from "./hooks/usePreservePanelSize";
77
- export * from './hooks/useDarkMode';
78
65
  // Utils
66
+ export * from "./utils/colorFromString";
79
67
  export * from "./utils/completions";
80
68
  export * from "./utils/fuzzyScorer";
81
69
  export * from "./utils/sketchColor";
@@ -0,0 +1,5 @@
1
+ export function cx(
2
+ ...args: Array<string | number | boolean | null | undefined>
3
+ ): string {
4
+ return args.filter(Boolean).join(" ").trim();
5
+ }
@@ -0,0 +1,44 @@
1
+ export const colorForStringValues: Record<string, string> = {
2
+ red: "#ef4444", // red-500
3
+ orange: "#f97316", // orange-500
4
+ amber: "#f59e0b", // amber-500
5
+ yellow: "#eab308", // yellow-500
6
+ lime: "#84cc16", // lime-500
7
+ green: "#22c55e", // green-500
8
+ emerald: "#10b981", // emerald-500
9
+ teal: "#14b8a6", // teal-500
10
+ cyan: "#06b6d4", // cyan-500
11
+ sky: "#0ea5e9", // sky-500
12
+ blue: "#3b82f6", // blue-500
13
+ indigo: "#6366f1", // indigo-500
14
+ violet: "#8b5cf6", // violet-500
15
+ purple: "#a855f7", // purple-500
16
+ fuchsia: "#d946ef", // fuchsia-500
17
+ pink: "#ec4899", // pink-500
18
+ rose: "#f43f5e", // rose-500
19
+ };
20
+
21
+ function hashCode(string: string) {
22
+ let hash = 0;
23
+ for (const char of string) {
24
+ hash = char.charCodeAt(0) + ((hash << 5) - hash);
25
+ }
26
+ return hash;
27
+ }
28
+
29
+ function elementFromHash<T>(array: T[], hash: number): T {
30
+ return array[Math.abs(hash) % array.length];
31
+ }
32
+
33
+ /**
34
+ * Create a background color and initials from a name
35
+ * Adapted from (MIT) https://mui.com/material-ui/react-avatar/
36
+ */
37
+ export function colorFromString(str: string) {
38
+ const hash = hashCode(str);
39
+ const backgroundColor = elementFromHash(
40
+ Object.values(colorForStringValues),
41
+ hash
42
+ );
43
+ return backgroundColor;
44
+ }
@@ -74,6 +74,7 @@ const config = {
74
74
  "chip-secondary-shadow": "var(--chip-secondary-shadow)",
75
75
  "chip-error-shadow": "var(--chip-error-shadow)",
76
76
  "chip-default-shadow": "var(--chip-default-shadow)",
77
+ "floating-button": "var(--floating-button)",
77
78
  },
78
79
  fontFamily: {
79
80
  sans: [
@@ -95,79 +96,79 @@ const config = {
95
96
  // old: typeScale = [3.052, 2.441, 1.953, 1.563, 1.25, 1, 0.85]; // Major third
96
97
  fontSize: {
97
98
  title: [
98
- "3.052rem",
99
+ "3.052rem", // 48.83px
99
100
  {
100
101
  lineHeight: "1.4",
101
102
  letterSpacing: "-0.05em",
102
103
  },
103
104
  ],
104
105
  subtitle: [
105
- "1.563rem",
106
+ "1.563rem", // 25px
106
107
  {
107
108
  lineHeight: "1.75",
108
109
  letterSpacing: "-0.05em",
109
110
  },
110
111
  ],
111
112
  heading1: [
112
- "1.953rem",
113
+ "1.953rem", // 31.25px
113
114
  {
114
115
  lineHeight: "1.6",
115
116
  letterSpacing: "-0.025em",
116
117
  },
117
118
  ],
118
119
  heading2: [
119
- "1.563rem",
120
+ "1.563rem", // 25px
120
121
  {
121
122
  lineHeight: "1.5",
122
123
  letterSpacing: "-0.025em",
123
124
  },
124
125
  ],
125
126
  heading3: [
126
- "1.25rem",
127
+ "1.25rem", // 20px
127
128
  {
128
129
  lineHeight: "1.4",
129
130
  letterSpacing: "-0.025em",
130
131
  },
131
132
  ],
132
133
  heading4: [
133
- "1rem",
134
+ "1rem", // 16px
134
135
  {
135
136
  lineHeight: "1.4",
136
137
  },
137
138
  ],
138
139
  heading5: [
139
- "0.85rem",
140
+ "0.85rem", // 13.6px
140
141
  {
141
142
  lineHeight: "1.4",
142
143
  },
143
144
  ],
144
145
  body: [
145
- "1",
146
+ "1", // 16px (1 = 1rem)
146
147
  {
147
148
  lineHeight: "1.4",
148
149
  },
149
150
  ],
150
151
  small: [
151
- "1",
152
+ "1", // 16px (1 = 1rem)
152
153
  {
153
154
  lineHeight: "19px",
154
155
  },
155
156
  ],
156
157
  code: [
157
- "90%",
158
+ "90%", // 14.4px
158
159
  {
159
160
  lineHeight: "1.5",
160
161
  },
161
162
  ],
162
163
  button: [
163
- "0.8rem",
164
+ "0.8rem", // 12.8px
164
165
  {
165
166
  lineHeight: "1.4",
166
167
  letterSpacing: "-0.2px",
167
168
  },
168
169
  ],
169
170
  label: [
170
- "0.62rem",
171
+ "0.62rem", // 9.92px
171
172
  {
172
173
  lineHeight: "19px",
173
174
  letterSpacing: "-0.3px",
@@ -211,7 +212,8 @@ const config = {
211
212
  zIndex: {
212
213
  interactable: "var(--interactable-z-index)",
213
214
  label: "var(--label-z-index)",
214
- }
215
+ menu: "var(--menu-z-index)",
216
+ },
215
217
  },
216
218
  },
217
219
  safelist: [
@@ -231,6 +233,7 @@ const config = {
231
233
  "border-gray-200",
232
234
  "gap-1.5",
233
235
  "gap-toolbar-separator",
236
+ "-mr-1",
234
237
  ],
235
238
  } satisfies Config;
236
239
 
@@ -1,14 +0,0 @@
1
- import { useState, useEffect } from "react"
2
-
3
- export function useDarkMode(elementRef: React.RefObject<HTMLElement>) {
4
- const [isDark, setIsDark] = useState(false)
5
-
6
- useEffect(() => {
7
- if (!elementRef.current) return;
8
-
9
- const hasDarkTheme = elementRef.current.closest('[data-theme="dark"]') !== null
10
- setIsDark(hasDarkTheme)
11
- }, [elementRef])
12
-
13
- return { isDark, elementRef }
14
- }
@@ -1,9 +0,0 @@
1
- import { twMerge } from "tailwind-merge";
2
-
3
- /**
4
- * Utility for merging classes conditionally. Right side will override left side if there are any conflicts.
5
- * Usage: cn('bg-red-500 text-white', 'bg-blue-500') // Returns: 'text-white bg-blue-500'
6
- * @danger do not use this function to concatenate class names. Use it to merge classes conditionally.
7
- */
8
- export const cn = (...classes: (string | boolean | undefined | null)[]) =>
9
- twMerge(classes.filter(Boolean) as string[]);