@vuer-ai/vuer-uikit 0.0.96 → 0.0.98
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 +106 -7
- package/cli/dial-cli.js +49 -7
- package/dist/SyncScroll/SyncScroll.cjs +10 -10
- package/dist/SyncScroll/SyncScroll.mjs +3 -3
- package/dist/SyncScroll/index.cjs +10 -10
- package/dist/SyncScroll/index.mjs +3 -3
- package/dist/chunk-3HEZVWRW.mjs +62 -0
- package/dist/chunk-4KWGGESI.cjs +494 -0
- package/dist/{chunk-CCMKL2OA.cjs → chunk-7GWDO25E.cjs} +2 -2
- package/dist/chunk-A5LCX2UQ.cjs +208 -0
- package/dist/chunk-BEJIZ56L.mjs +300 -0
- package/dist/chunk-C7VGRU3O.mjs +283 -0
- package/dist/{chunk-LONOMMFA.cjs → chunk-LJMNHTTG.cjs} +21 -21
- package/dist/chunk-O66RESRR.cjs +285 -0
- package/dist/{chunk-RINTUFYQ.cjs → chunk-RMK6W774.cjs} +24 -19
- package/dist/{chunk-BFQ2WL5U.mjs → chunk-TTYSYGVE.mjs} +2 -2
- package/dist/chunk-VA3PEYFM.mjs +489 -0
- package/dist/chunk-VBBJSIY7.cjs +308 -0
- package/dist/{chunk-AIINOWEH.mjs → chunk-W4JCKCW7.mjs} +5 -5
- package/dist/chunk-WWGF6TBZ.mjs +206 -0
- package/dist/chunk-ZGN4UEJR.cjs +679 -0
- package/dist/chunk-ZQLRMOUW.mjs +661 -0
- package/dist/dial/DialPanel.cjs +24 -24
- package/dist/dial/DialPanel.mjs +23 -23
- package/dist/dial/DialProvider.cjs +3 -3
- package/dist/dial/DialProvider.d.cts +1 -0
- package/dist/dial/DialProvider.d.ts +1 -0
- package/dist/dial/DialProvider.example.cjs +72 -0
- package/dist/dial/DialProvider.example.d.cts +7 -0
- package/dist/dial/DialProvider.example.d.ts +7 -0
- package/dist/dial/DialProvider.example.mjs +68 -0
- package/dist/dial/DialProvider.mjs +1 -1
- package/dist/dial/index.cjs +42 -42
- package/dist/dial/index.mjs +23 -23
- package/dist/dial/wrapped-inputs/ControlledInputs.cjs +27 -27
- package/dist/dial/wrapped-inputs/ControlledInputs.mjs +23 -23
- package/dist/dial/wrapped-inputs/DialInputs.cjs +34 -34
- package/dist/dial/wrapped-inputs/DialInputs.mjs +23 -23
- package/dist/dial/wrapped-inputs/DialVectorInput.cjs +24 -24
- package/dist/dial/wrapped-inputs/DialVectorInput.mjs +23 -23
- package/dist/dial/wrapped-inputs/index.cjs +39 -39
- package/dist/dial/wrapped-inputs/index.mjs +23 -23
- package/dist/highlight-cursor/cursor-provider.cjs +3 -3
- package/dist/highlight-cursor/cursor-provider.mjs +2 -2
- package/dist/highlight-cursor/enhanced-components.cjs +10 -10
- package/dist/highlight-cursor/enhanced-components.mjs +5 -5
- package/dist/highlight-cursor/index.cjs +16 -16
- package/dist/highlight-cursor/index.mjs +6 -6
- package/dist/hooks/index.cjs +5 -5
- package/dist/hooks/index.mjs +1 -1
- package/dist/index.cjs +190 -190
- package/dist/index.mjs +23 -23
- package/dist/ui/UIKitBadge.cjs +6 -6
- package/dist/ui/UIKitBadge.mjs +2 -2
- package/dist/ui/avatar.cjs +1 -1
- package/dist/ui/avatar.mjs +1 -1
- package/dist/ui/badge.cjs +1 -1
- package/dist/ui/badge.d.cts +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/badge.mjs +1 -1
- package/dist/ui/button.cjs +1 -1
- package/dist/ui/button.mjs +1 -1
- package/dist/ui/card.cjs +1 -1
- package/dist/ui/card.mjs +1 -1
- package/dist/ui/checkbox.cjs +1 -1
- package/dist/ui/checkbox.mjs +1 -1
- package/dist/ui/collapsible.cjs +1 -1
- package/dist/ui/collapsible.mjs +1 -1
- package/dist/ui/drawer.cjs +1 -1
- package/dist/ui/drawer.mjs +1 -1
- package/dist/ui/dropdown.cjs +1 -1
- package/dist/ui/dropdown.mjs +1 -1
- package/dist/ui/index.cjs +107 -107
- package/dist/ui/index.mjs +17 -17
- package/dist/ui/inputs/color-input.cjs +1 -1
- package/dist/ui/inputs/color-input.mjs +1 -1
- package/dist/ui/inputs/index.cjs +11 -11
- package/dist/ui/inputs/index.mjs +3 -3
- package/dist/ui/inputs/input-numbers.cjs +1 -1
- package/dist/ui/inputs/input-numbers.mjs +1 -1
- package/dist/ui/inputs/input.cjs +1 -1
- package/dist/ui/inputs/input.d.cts +1 -1
- package/dist/ui/inputs/input.d.ts +1 -1
- package/dist/ui/inputs/input.mjs +1 -1
- package/dist/ui/inputs/number-inputs/CmInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/CmInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/DegInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/DegInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/EulerDegInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/EulerDegInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/EulerInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/EulerInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/EulerRadInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/EulerRadInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/InchInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/InchInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/IntInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/IntInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/KVectorInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/KVectorInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/QuaternionInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/QuaternionInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/RadInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/RadInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/TimeInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/TimeInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/Vec3Input.cjs +1 -1
- package/dist/ui/inputs/number-inputs/Vec3Input.mjs +1 -1
- package/dist/ui/inputs/number-inputs/VectorInput.cjs +1 -1
- package/dist/ui/inputs/number-inputs/VectorInput.mjs +1 -1
- package/dist/ui/inputs/number-inputs/index.cjs +11 -11
- package/dist/ui/inputs/number-inputs/index.mjs +3 -3
- package/dist/ui/inputs/presets-input.cjs +1 -1
- package/dist/ui/inputs/presets-input.mjs +1 -1
- package/dist/ui/label.cjs +1 -1
- package/dist/ui/label.mjs +1 -1
- package/dist/ui/layout.cjs +1 -1
- package/dist/ui/layout.mjs +1 -1
- package/dist/ui/layouts/dock-layout/DockLayoutView.cjs +1 -1
- package/dist/ui/layouts/dock-layout/DockLayoutView.mjs +1 -1
- package/dist/ui/layouts/dock-layout/LayoutSlots.cjs +1 -1
- package/dist/ui/layouts/dock-layout/LayoutSlots.mjs +1 -1
- package/dist/ui/layouts/dock-layout/index.cjs +1 -1
- package/dist/ui/layouts/dock-layout/index.mjs +1 -1
- package/dist/ui/layouts/index.cjs +2 -2
- package/dist/ui/layouts/index.mjs +2 -2
- package/dist/ui/layouts/liquid-layout/LayoutSlots.cjs +1 -1
- package/dist/ui/layouts/liquid-layout/LayoutSlots.mjs +1 -1
- package/dist/ui/layouts/liquid-layout/LiquidLayoutView.cjs +1 -1
- package/dist/ui/layouts/liquid-layout/LiquidLayoutView.mjs +1 -1
- package/dist/ui/layouts/liquid-layout/index.cjs +1 -1
- package/dist/ui/layouts/liquid-layout/index.mjs +1 -1
- package/dist/ui/modal.cjs +1 -1
- package/dist/ui/modal.mjs +1 -1
- package/dist/ui/navigation.cjs +1 -1
- package/dist/ui/navigation.mjs +1 -1
- package/dist/ui/pagination.cjs +1 -1
- package/dist/ui/pagination.mjs +1 -1
- package/dist/ui/panel.cjs +1 -1
- package/dist/ui/panel.mjs +1 -1
- package/dist/ui/popover.cjs +1 -1
- package/dist/ui/popover.mjs +1 -1
- package/dist/ui/radio-group.cjs +1 -1
- package/dist/ui/radio-group.mjs +1 -1
- package/dist/ui/resizable.cjs +1 -1
- package/dist/ui/resizable.mjs +1 -1
- package/dist/ui/select.cjs +1 -1
- package/dist/ui/select.d.cts +1 -1
- package/dist/ui/select.d.ts +1 -1
- package/dist/ui/select.mjs +1 -1
- package/dist/ui/separator.cjs +1 -1
- package/dist/ui/separator.mjs +1 -1
- package/dist/ui/sheet.cjs +1 -1
- package/dist/ui/sheet.mjs +1 -1
- package/dist/ui/sidebar.cjs +26 -26
- package/dist/ui/sidebar.mjs +2 -2
- package/dist/ui/simple-tree-view.cjs +1 -1
- package/dist/ui/simple-tree-view.mjs +1 -1
- package/dist/ui/skeleton.cjs +1 -1
- package/dist/ui/skeleton.mjs +1 -1
- package/dist/ui/slider.cjs +1 -1
- package/dist/ui/slider.mjs +1 -1
- package/dist/ui/switch.cjs +1 -1
- package/dist/ui/switch.mjs +1 -1
- package/dist/ui/table.cjs +1 -1
- package/dist/ui/table.mjs +1 -1
- package/dist/ui/tabs.cjs +1 -1
- package/dist/ui/tabs.mjs +1 -1
- package/dist/ui/textarea.cjs +1 -1
- package/dist/ui/textarea.d.cts +1 -1
- package/dist/ui/textarea.d.ts +1 -1
- package/dist/ui/textarea.mjs +1 -1
- package/dist/ui/theme/ThemeToggles.cjs +1 -1
- package/dist/ui/theme/ThemeToggles.mjs +1 -1
- package/dist/ui/theme/index.cjs +1 -1
- package/dist/ui/theme/index.mjs +1 -1
- package/dist/ui/toggle-buttons.cjs +1 -1
- package/dist/ui/toggle-buttons.mjs +1 -1
- package/dist/ui/toggle-group.cjs +1 -1
- package/dist/ui/toggle-group.mjs +1 -1
- package/dist/ui/toggle.cjs +1 -1
- package/dist/ui/toggle.mjs +1 -1
- package/dist/ui/toolbar.cjs +1 -1
- package/dist/ui/toolbar.mjs +1 -1
- package/dist/ui/tooltip.cjs +1 -1
- package/dist/ui/tooltip.mjs +1 -1
- package/dist/ui/tree-view/TreeSearchBar.cjs +1 -1
- package/dist/ui/tree-view/TreeSearchBar.mjs +1 -1
- package/dist/ui/tree-view/TreeView.cjs +1 -1
- package/dist/ui/tree-view/TreeView.mjs +1 -1
- package/dist/ui/tree-view/index.cjs +6 -6
- package/dist/ui/tree-view/index.mjs +2 -2
- package/dist/ui/tree-view-legacy.cjs +9 -9
- package/dist/ui/tree-view-legacy.mjs +5 -5
- package/dist/ui/waterfall/CursorOverlay.cjs +1 -1
- package/dist/ui/waterfall/CursorOverlay.mjs +1 -1
- package/dist/ui/waterfall/TimelineEvent.cjs +1 -1
- package/dist/ui/waterfall/TimelineEvent.mjs +1 -1
- package/dist/ui/waterfall/TimelineProcessBar.cjs +1 -1
- package/dist/ui/waterfall/TimelineProcessBar.mjs +1 -1
- package/dist/ui/waterfall/Wedges.cjs +1 -1
- package/dist/ui/waterfall/Wedges.mjs +1 -1
- package/dist/ui/waterfall/index.cjs +8 -8
- package/dist/ui/waterfall/index.mjs +7 -7
- package/package.json +28 -2
- package/src/SyncScroll/README.md +283 -0
- package/src/SyncScroll/SyncScroll.tsx +361 -0
- package/src/SyncScroll/index.ts +22 -0
- package/src/SyncScroll/useSyncScroll.tsx +226 -0
- package/src/cli/dial-cli.ts +1133 -0
- package/src/dial/DialPanel.tsx +208 -0
- package/src/dial/DialProvider.example.tsx +80 -0
- package/src/dial/DialProvider.tsx +138 -0
- package/src/dial/index.ts +26 -0
- package/src/dial/wrapped-inputs/ControlledInputs.tsx +176 -0
- package/src/dial/wrapped-inputs/DialInputs.tsx +401 -0
- package/src/dial/wrapped-inputs/DialVectorInput.tsx +125 -0
- package/src/dial/wrapped-inputs/index.ts +25 -0
- package/src/highlight-cursor/cursor-context.tsx +23 -0
- package/src/highlight-cursor/cursor-provider.tsx +264 -0
- package/src/highlight-cursor/enhanced-components.tsx +16 -0
- package/src/highlight-cursor/index.ts +21 -0
- package/src/highlight-cursor/tabs-cursor-context.tsx +121 -0
- package/src/highlight-cursor/types.ts +40 -0
- package/src/highlight-cursor/with-cursor.tsx +144 -0
- package/src/hooks/clientOnly.tsx +54 -0
- package/src/hooks/cn.ts +33 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useDocument.tsx +18 -0
- package/src/hooks/useDragSelect.ts +116 -0
- package/src/hooks/useIsMobile.ts +35 -0
- package/src/hooks/useLocalStorage.ts +122 -0
- package/src/hooks/useLocation.tsx +95 -0
- package/src/hooks/useQueryParams.tsx +31 -0
- package/src/hooks/useWindow.tsx +18 -0
- package/src/index.css +5 -0
- package/src/index.ts +5 -0
- package/src/styles/.editorconfig +0 -0
- package/src/styles/theme.css +152 -0
- package/src/styles/toast.css +67 -0
- package/src/styles/variables.css +229 -0
- package/src/ui/UIKitBadge.tsx +171 -0
- package/src/ui/avatar.tsx +221 -0
- package/src/ui/badge.tsx +74 -0
- package/src/ui/button.tsx +143 -0
- package/src/ui/card.tsx +146 -0
- package/src/ui/checkbox.tsx +78 -0
- package/src/ui/collapsible.tsx +43 -0
- package/src/ui/drag-selectable/DragSelectProvider.tsx +178 -0
- package/src/ui/drag-selectable/createSelectable.tsx +43 -0
- package/src/ui/drag-selectable/index.ts +2 -0
- package/src/ui/drawer.tsx +137 -0
- package/src/ui/dropdown.tsx +707 -0
- package/src/ui/icons/MouseCursorIcons.tsx +39 -0
- package/src/ui/icons/cursor.tsx +38 -0
- package/src/ui/icons/index.ts +2 -0
- package/src/ui/index.ts +45 -0
- package/src/ui/inputs/color-input.tsx +61 -0
- package/src/ui/inputs/index.tsx +5 -0
- package/src/ui/inputs/input-numbers.tsx +394 -0
- package/src/ui/inputs/input.tsx +155 -0
- package/src/ui/inputs/number-inputs/CmInput.tsx +26 -0
- package/src/ui/inputs/number-inputs/DegInput.tsx +26 -0
- package/src/ui/inputs/number-inputs/EulerDegInput.tsx +14 -0
- package/src/ui/inputs/number-inputs/EulerInput.tsx +30 -0
- package/src/ui/inputs/number-inputs/EulerRadInput.tsx +14 -0
- package/src/ui/inputs/number-inputs/InchInput.tsx +26 -0
- package/src/ui/inputs/number-inputs/IntInput.tsx +46 -0
- package/src/ui/inputs/number-inputs/KVectorInput.tsx +20 -0
- package/src/ui/inputs/number-inputs/QuaternionInput.tsx +27 -0
- package/src/ui/inputs/number-inputs/RadInput.tsx +26 -0
- package/src/ui/inputs/number-inputs/TimeInput.tsx +26 -0
- package/src/ui/inputs/number-inputs/Vec3Input.tsx +26 -0
- package/src/ui/inputs/number-inputs/VectorInput.tsx +38 -0
- package/src/ui/inputs/number-inputs/index.ts +38 -0
- package/src/ui/inputs/presets-input.tsx +59 -0
- package/src/ui/label.tsx +35 -0
- package/src/ui/layout.tsx +43 -0
- package/src/ui/layouts/dock-layout/DockLayoutView.tsx +128 -0
- package/src/ui/layouts/dock-layout/LayoutSlots.tsx +85 -0
- package/src/ui/layouts/dock-layout/index.tsx +2 -0
- package/src/ui/layouts/index.ts +2 -0
- package/src/ui/layouts/liquid-layout/LayoutSlots.tsx +82 -0
- package/src/ui/layouts/liquid-layout/LiquidLayoutView.tsx +76 -0
- package/src/ui/layouts/liquid-layout/index.ts +1 -0
- package/src/ui/modal.tsx +211 -0
- package/src/ui/navigation.tsx +86 -0
- package/src/ui/pagination.tsx +129 -0
- package/src/ui/panel.tsx +146 -0
- package/src/ui/popover.tsx +112 -0
- package/src/ui/radio-group.tsx +63 -0
- package/src/ui/resizable.tsx +52 -0
- package/src/ui/select.tsx +365 -0
- package/src/ui/separator.tsx +26 -0
- package/src/ui/sheet.tsx +257 -0
- package/src/ui/sidebar.tsx +695 -0
- package/src/ui/simple-tree-view.tsx +604 -0
- package/src/ui/skeleton.tsx +15 -0
- package/src/ui/slider.tsx +204 -0
- package/src/ui/switch.tsx +45 -0
- package/src/ui/table.tsx +99 -0
- package/src/ui/tabs.tsx +192 -0
- package/src/ui/textarea.tsx +55 -0
- package/src/ui/theme/ThemeProvider.tsx +319 -0
- package/src/ui/theme/ThemeToggles.tsx +84 -0
- package/src/ui/theme/index.ts +21 -0
- package/src/ui/theme/themeScript.tsx +179 -0
- package/src/ui/theme/types.ts +61 -0
- package/src/ui/toast.tsx +30 -0
- package/src/ui/toggle-buttons.tsx +290 -0
- package/src/ui/toggle-group.tsx +236 -0
- package/src/ui/toggle.tsx +94 -0
- package/src/ui/toolbar.tsx +54 -0
- package/src/ui/tooltip.tsx +171 -0
- package/src/ui/tree-view/TreeSearchBar.tsx +88 -0
- package/src/ui/tree-view/TreeView.tsx +232 -0
- package/src/ui/tree-view/hooks.tsx +289 -0
- package/src/ui/tree-view/index.ts +4 -0
- package/src/ui/tree-view/types.ts +23 -0
- package/src/ui/tree-view-legacy.tsx +644 -0
- package/src/ui/version-badge.tsx +0 -0
- package/src/ui/waterfall/CursorOverlay.tsx +96 -0
- package/src/ui/waterfall/NavigationControls.tsx +42 -0
- package/src/ui/waterfall/Tick.tsx +51 -0
- package/src/ui/waterfall/TimeRuleEventDot.tsx +19 -0
- package/src/ui/waterfall/TimelineEvent.tsx +60 -0
- package/src/ui/waterfall/TimelineProcessBar.tsx +207 -0
- package/src/ui/waterfall/Wedges.tsx +67 -0
- package/src/ui/waterfall/WheelZoomContext.tsx +128 -0
- package/src/ui/waterfall/hooks/useTimelineState.tsx +134 -0
- package/src/ui/waterfall/hooks/useViewport.tsx +293 -0
- package/src/ui/waterfall/index.tsx +326 -0
- package/src/ui/waterfall/types.ts +20 -0
- package/src/ui/waterfall/utils.ts +91 -0
- package/dist/chunk-W2YAQV5N.mjs +0 -57
- package/dist/{chunk-QLCEHV4Q.mjs → chunk-2OZK5DY5.mjs} +2 -2
- package/dist/{chunk-Z35DNFRZ.cjs → chunk-7IS37C3P.cjs} +2 -2
- package/dist/{chunk-4AOAH77D.mjs → chunk-A2PCEL5S.mjs} +2 -2
- package/dist/{chunk-K4VD5PPY.mjs → chunk-BIUDC66P.mjs} +1 -1
- package/dist/{chunk-RKJR6RZU.cjs → chunk-OYNLQTHW.cjs} +1 -1
- package/dist/{chunk-VE3XLQLO.cjs → chunk-QUFZWF4E.cjs} +2 -2
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
PropsWithChildren,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
10
|
+
|
|
11
|
+
import { ThemeScript } from "./themeScript";
|
|
12
|
+
import type { Attribute, ThemeProviderProps, UseThemeProps, BaseTheme, ComputedTheme } from "./types";
|
|
13
|
+
|
|
14
|
+
export const colorSchemes = ["light", "dark"] as const;
|
|
15
|
+
export const MEDIA = "(prefers-color-scheme: dark)";
|
|
16
|
+
export const isServer = typeof window === "undefined";
|
|
17
|
+
export const ThemeContext = createContext<UseThemeProps | undefined>(undefined);
|
|
18
|
+
|
|
19
|
+
export const saveToLS = (key: string, value: string) => {
|
|
20
|
+
// Save to storage
|
|
21
|
+
try {
|
|
22
|
+
localStorage.setItem(key, value);
|
|
23
|
+
} catch {
|
|
24
|
+
// Unsupported
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const getFromLS = (key: string, fallback: string): string => {
|
|
29
|
+
if (isServer) return fallback;
|
|
30
|
+
try {
|
|
31
|
+
return localStorage.getItem(key) || fallback;
|
|
32
|
+
} catch {
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Calculate the actual theme name
|
|
39
|
+
*/
|
|
40
|
+
export const computeTheme = (baseTheme: BaseTheme, isLiquid: boolean, systemIsDark: boolean): ComputedTheme => {
|
|
41
|
+
let resolvedBase: "light" | "dark";
|
|
42
|
+
|
|
43
|
+
if (baseTheme === "system") {
|
|
44
|
+
resolvedBase = systemIsDark ? "dark" : "light";
|
|
45
|
+
} else {
|
|
46
|
+
resolvedBase = baseTheme;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return isLiquid ? `liquid-${resolvedBase}` as ComputedTheme : resolvedBase;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse old theme string to new state structure
|
|
54
|
+
*/
|
|
55
|
+
export const parseOldTheme = (oldTheme: string): { baseTheme: BaseTheme; isLiquid: boolean } => {
|
|
56
|
+
if (oldTheme === "system") {
|
|
57
|
+
return { baseTheme: "system", isLiquid: false };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (oldTheme.startsWith("liquid-")) {
|
|
61
|
+
const base = oldTheme.replace("liquid-", "") as "light" | "dark";
|
|
62
|
+
return { baseTheme: base, isLiquid: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { baseTheme: oldTheme as BaseTheme, isLiquid: false };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Hook to read and update theme state from `ThemeProvider`.
|
|
72
|
+
* Throws if used outside a provider.
|
|
73
|
+
*/
|
|
74
|
+
export const useTheme = () => {
|
|
75
|
+
const context = useContext(ThemeContext);
|
|
76
|
+
|
|
77
|
+
if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider");
|
|
78
|
+
|
|
79
|
+
return context;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Provides theme context and DOM attribute synchronization for theming.
|
|
84
|
+
* Nesting providers is supported; inner providers will passthrough.
|
|
85
|
+
*/
|
|
86
|
+
export const ThemeProvider = (props: PropsWithChildren<ThemeProviderProps>) => {
|
|
87
|
+
const context = useContext(ThemeContext);
|
|
88
|
+
|
|
89
|
+
// Ignore nested context providers, just passthrough children
|
|
90
|
+
if (context) return <>{props.children}</>;
|
|
91
|
+
return <Theme {...props} />;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const defaultThemes = ["light", "dark", "liquid-light", "liquid-dark"];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Internal component implementing theme persistence, system preference,
|
|
98
|
+
* and DOM attribute updates.
|
|
99
|
+
*/
|
|
100
|
+
export const Theme = ({
|
|
101
|
+
forcedTheme,
|
|
102
|
+
disableTransitionOnChange = false,
|
|
103
|
+
enableSystem = true,
|
|
104
|
+
enableColorScheme = false,
|
|
105
|
+
storageKey = "vuer-uikit-theme",
|
|
106
|
+
defaultBaseTheme = enableSystem ? "system" : "light",
|
|
107
|
+
defaultIsLiquid = true,
|
|
108
|
+
attribute = "class",
|
|
109
|
+
value,
|
|
110
|
+
children,
|
|
111
|
+
nonce,
|
|
112
|
+
scriptProps,
|
|
113
|
+
}: PropsWithChildren<ThemeProviderProps>) => {
|
|
114
|
+
const themes = defaultThemes;
|
|
115
|
+
|
|
116
|
+
const initialState = useMemo(() => {
|
|
117
|
+
return { baseTheme: defaultBaseTheme!, isLiquid: defaultIsLiquid! };
|
|
118
|
+
}, [defaultBaseTheme, defaultIsLiquid]);
|
|
119
|
+
|
|
120
|
+
const [baseTheme, setBaseThemeState] = useState<BaseTheme>(() => {
|
|
121
|
+
const stored = getFromLS(`${storageKey}-base`, "");
|
|
122
|
+
if (!stored) {
|
|
123
|
+
return initialState.baseTheme;
|
|
124
|
+
}
|
|
125
|
+
return (["light", "dark", "system"].includes(stored) ? stored : initialState.baseTheme) as BaseTheme;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const [isLiquid, setIsLiquidState] = useState<boolean>(() => {
|
|
129
|
+
const stored = getFromLS(`${storageKey}-liquid`, "");
|
|
130
|
+
if (!stored) {
|
|
131
|
+
return initialState.isLiquid;
|
|
132
|
+
}
|
|
133
|
+
return stored === "true";
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const [systemIsDark, setSystemIsDark] = useState(() => {
|
|
137
|
+
if (isServer) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return window.matchMedia(MEDIA).matches;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const attrs = !value ? themes : Object.values(value);
|
|
144
|
+
|
|
145
|
+
const computedTheme = useMemo(() =>
|
|
146
|
+
computeTheme(baseTheme, isLiquid, systemIsDark),
|
|
147
|
+
[baseTheme, isLiquid, systemIsDark]
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const resolvedTheme = useMemo((): ComputedTheme => {
|
|
151
|
+
if (forcedTheme) {
|
|
152
|
+
const parsed = parseOldTheme(forcedTheme);
|
|
153
|
+
return computeTheme(parsed.baseTheme, parsed.isLiquid, systemIsDark);
|
|
154
|
+
}
|
|
155
|
+
return computedTheme;
|
|
156
|
+
}, [forcedTheme, computedTheme, systemIsDark]);
|
|
157
|
+
|
|
158
|
+
const applyTheme = useCallback(
|
|
159
|
+
(theme: string) => {
|
|
160
|
+
if (!theme) return;
|
|
161
|
+
|
|
162
|
+
const name = value ? value[theme] : theme;
|
|
163
|
+
const enable = disableTransitionOnChange ? disableAnimation(nonce) : null;
|
|
164
|
+
const d = document.documentElement;
|
|
165
|
+
|
|
166
|
+
const handleAttribute = (attr: Attribute) => {
|
|
167
|
+
if (attr === "class") {
|
|
168
|
+
d.classList.remove(...attrs);
|
|
169
|
+
if (name) d.classList.add(name);
|
|
170
|
+
} else if (attr.startsWith("data-")) {
|
|
171
|
+
if (name) {
|
|
172
|
+
d.setAttribute(attr, name);
|
|
173
|
+
} else {
|
|
174
|
+
d.removeAttribute(attr);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
if (Array.isArray(attribute)) attribute.forEach(handleAttribute);
|
|
180
|
+
else handleAttribute(attribute);
|
|
181
|
+
|
|
182
|
+
if (enableColorScheme) {
|
|
183
|
+
// Extract base color scheme from theme name
|
|
184
|
+
const isDark = theme.includes("dark");
|
|
185
|
+
d.style.colorScheme = isDark ? "dark" : "light";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
enable?.();
|
|
189
|
+
},
|
|
190
|
+
[attribute, attrs, disableTransitionOnChange, enableColorScheme, nonce, value],
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const setBaseTheme = useCallback((newBaseTheme: BaseTheme) => {
|
|
194
|
+
setBaseThemeState(newBaseTheme);
|
|
195
|
+
saveToLS(`${storageKey}-base`, newBaseTheme);
|
|
196
|
+
}, [storageKey]);
|
|
197
|
+
|
|
198
|
+
const toggleLiquid = useCallback(() => {
|
|
199
|
+
setIsLiquidState(prev => {
|
|
200
|
+
const newValue = !prev;
|
|
201
|
+
saveToLS(`${storageKey}-liquid`, String(newValue));
|
|
202
|
+
return newValue;
|
|
203
|
+
});
|
|
204
|
+
}, [storageKey]);
|
|
205
|
+
|
|
206
|
+
const handleMediaQuery = useCallback(
|
|
207
|
+
(e: MediaQueryListEvent | MediaQueryList) => {
|
|
208
|
+
const isDark = e.matches;
|
|
209
|
+
setSystemIsDark(isDark);
|
|
210
|
+
},
|
|
211
|
+
[],
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Always listen to System preference
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
const media = window.matchMedia(MEDIA);
|
|
217
|
+
|
|
218
|
+
// Intentionally use deprecated listener methods to support iOS & old browsers
|
|
219
|
+
media.addListener(handleMediaQuery);
|
|
220
|
+
handleMediaQuery(media);
|
|
221
|
+
|
|
222
|
+
return () => media.removeListener(handleMediaQuery);
|
|
223
|
+
}, [handleMediaQuery]);
|
|
224
|
+
|
|
225
|
+
// localStorage event handling
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
const handleStorage = (e: StorageEvent) => {
|
|
228
|
+
if (e.key === `${storageKey}-base` && e.newValue) {
|
|
229
|
+
setBaseThemeState(e.newValue as BaseTheme);
|
|
230
|
+
} else if (e.key === `${storageKey}-liquid` && e.newValue !== null) {
|
|
231
|
+
setIsLiquidState(e.newValue === "true");
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
window.addEventListener("storage", handleStorage);
|
|
236
|
+
return () => window.removeEventListener("storage", handleStorage);
|
|
237
|
+
}, [storageKey]);
|
|
238
|
+
|
|
239
|
+
// Whenever theme changes, apply it
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
applyTheme(resolvedTheme);
|
|
242
|
+
}, [applyTheme, resolvedTheme]);
|
|
243
|
+
|
|
244
|
+
const contextValue = useMemo(
|
|
245
|
+
() => ({
|
|
246
|
+
baseTheme,
|
|
247
|
+
setBaseTheme,
|
|
248
|
+
isLiquid,
|
|
249
|
+
toggleLiquid,
|
|
250
|
+
computedTheme,
|
|
251
|
+
resolvedTheme,
|
|
252
|
+
systemTheme: (enableSystem ? (systemIsDark ? "dark" : "light") : undefined) as "light" | "dark" | undefined,
|
|
253
|
+
forcedTheme,
|
|
254
|
+
storageKey,
|
|
255
|
+
}),
|
|
256
|
+
[
|
|
257
|
+
baseTheme, setBaseTheme, isLiquid, toggleLiquid, computedTheme, resolvedTheme,
|
|
258
|
+
systemIsDark, enableSystem, forcedTheme, storageKey
|
|
259
|
+
],
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<ThemeContext.Provider value={contextValue}>
|
|
264
|
+
<ThemeScript
|
|
265
|
+
{...{
|
|
266
|
+
forcedTheme,
|
|
267
|
+
storageKey,
|
|
268
|
+
attribute,
|
|
269
|
+
enableSystem,
|
|
270
|
+
enableColorScheme,
|
|
271
|
+
defaultTheme: initialState.baseTheme === "system"
|
|
272
|
+
? "system"
|
|
273
|
+
: (initialState.isLiquid ? `liquid-${initialState.baseTheme}` : initialState.baseTheme),
|
|
274
|
+
value,
|
|
275
|
+
themes,
|
|
276
|
+
nonce,
|
|
277
|
+
scriptProps,
|
|
278
|
+
}}
|
|
279
|
+
/>
|
|
280
|
+
|
|
281
|
+
{children}
|
|
282
|
+
</ThemeContext.Provider>
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Helpers
|
|
287
|
+
export const getTheme = (key: string, fallback: string) => {
|
|
288
|
+
return getFromLS(key, fallback);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
export const disableAnimation = (nonce?: string) => {
|
|
292
|
+
const css = document.createElement("style");
|
|
293
|
+
if (nonce) css.setAttribute("nonce", nonce);
|
|
294
|
+
css.appendChild(
|
|
295
|
+
document.createTextNode(
|
|
296
|
+
`*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}`,
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
document.head.appendChild(css);
|
|
300
|
+
|
|
301
|
+
return () => {
|
|
302
|
+
// Force restyle
|
|
303
|
+
(() => window.getComputedStyle(document.body))();
|
|
304
|
+
|
|
305
|
+
// Wait for next tick before removing
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
if (css && css.parentNode) {
|
|
308
|
+
css.parentNode.removeChild(css);
|
|
309
|
+
}
|
|
310
|
+
}, 1);
|
|
311
|
+
};
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
export const getSystemTheme = (e?: MediaQueryList | MediaQueryListEvent) => {
|
|
315
|
+
if (isServer) return "liquid-light"; // Default to liquid-light on server
|
|
316
|
+
if (!e) e = window.matchMedia(MEDIA);
|
|
317
|
+
const isDark = e.matches;
|
|
318
|
+
return isDark ? "liquid-dark" : "liquid-light";
|
|
319
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Liquid toggle component
|
|
2
|
+
import { Droplet, Moon, Sun, SunMoon } from "lucide-react";
|
|
3
|
+
import React, { type ComponentProps } from "react";
|
|
4
|
+
|
|
5
|
+
import { Button } from "../button";
|
|
6
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip";
|
|
7
|
+
import { useTheme } from "./ThemeProvider";
|
|
8
|
+
import { cn, createClientOnlyComponent, useDocument } from "../../hooks";
|
|
9
|
+
|
|
10
|
+
type LiquidToggleProps = Omit<ComponentProps<typeof Button>, "onClick" | "children">;
|
|
11
|
+
|
|
12
|
+
export const LiquidToggle: React.FC<LiquidToggleProps> = ({ className, ...props }) => {
|
|
13
|
+
const { isLiquid, toggleLiquid } = useTheme();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Tooltip>
|
|
17
|
+
<TooltipTrigger asChild>
|
|
18
|
+
<Button
|
|
19
|
+
icon
|
|
20
|
+
variant="ghost"
|
|
21
|
+
value={isLiquid}
|
|
22
|
+
aria-label={`Toggle liquid theme: ${isLiquid ? "on" : "off"}`}
|
|
23
|
+
{...props}
|
|
24
|
+
onClick={toggleLiquid}
|
|
25
|
+
className={className}
|
|
26
|
+
>
|
|
27
|
+
<Droplet size={20} />
|
|
28
|
+
</Button>
|
|
29
|
+
</TooltipTrigger>
|
|
30
|
+
<TooltipContent>Toggle liquid theme</TooltipContent>
|
|
31
|
+
</Tooltip>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Theme color toggle component for light/dark/system
|
|
36
|
+
type ThemeColorToggleProps = Omit<ComponentProps<typeof Button>, "onClick" | "children">;
|
|
37
|
+
|
|
38
|
+
export const ThemeColorToggle: React.FC<ThemeColorToggleProps> = (props) => {
|
|
39
|
+
const { baseTheme, setBaseTheme, resolvedTheme } = useTheme();
|
|
40
|
+
|
|
41
|
+
const handleThemeChange = () => {
|
|
42
|
+
if (baseTheme === "light") {
|
|
43
|
+
setBaseTheme("dark");
|
|
44
|
+
} else if (baseTheme === "dark") {
|
|
45
|
+
setBaseTheme("system");
|
|
46
|
+
} else if (baseTheme === "system") {
|
|
47
|
+
setBaseTheme("light");
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const isDark = resolvedTheme.includes("dark");
|
|
52
|
+
const isSystem = baseTheme === "system";
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Button
|
|
56
|
+
icon
|
|
57
|
+
variant="ghost"
|
|
58
|
+
aria-label={`Current theme: ${isSystem ? "system" : isDark ? "dark" : "light"}`}
|
|
59
|
+
{...props}
|
|
60
|
+
onClick={handleThemeChange}
|
|
61
|
+
>
|
|
62
|
+
{isSystem ? <SunMoon size={20} /> : isDark ? <Moon size={20} /> : <Sun size={20} />}
|
|
63
|
+
</Button>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type ThemeTogglesProps = {
|
|
68
|
+
className?: string;
|
|
69
|
+
liquidToggleProps?: Omit<LiquidToggleProps, "className">;
|
|
70
|
+
colorToggleProps?: Omit<ThemeColorToggleProps, "className">;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const ThemeToggles: React.FC<ThemeTogglesProps> = ({
|
|
74
|
+
className,
|
|
75
|
+
liquidToggleProps = {},
|
|
76
|
+
colorToggleProps = {}
|
|
77
|
+
}) => {
|
|
78
|
+
return (
|
|
79
|
+
<div className={cn("flex gap-1", className)}>
|
|
80
|
+
<LiquidToggle {...liquidToggleProps} />
|
|
81
|
+
<ThemeColorToggle {...colorToggleProps} />
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
useTheme,
|
|
3
|
+
ThemeProvider,
|
|
4
|
+
defaultThemes,
|
|
5
|
+
computeTheme,
|
|
6
|
+
parseOldTheme
|
|
7
|
+
} from "./ThemeProvider";
|
|
8
|
+
export {
|
|
9
|
+
LiquidToggle,
|
|
10
|
+
ThemeColorToggle,
|
|
11
|
+
ThemeToggles,
|
|
12
|
+
} from "./ThemeToggles";
|
|
13
|
+
export { ThemeScript } from "./themeScript";
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
Attribute,
|
|
17
|
+
ThemeProviderProps,
|
|
18
|
+
UseThemeProps,
|
|
19
|
+
BaseTheme,
|
|
20
|
+
ComputedTheme
|
|
21
|
+
} from "./types";
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { memo } from "react";
|
|
2
|
+
|
|
3
|
+
import { Attribute, ThemeProviderProps } from "./types";
|
|
4
|
+
|
|
5
|
+
const themeScript = (
|
|
6
|
+
defaultTheme: string,
|
|
7
|
+
storageKey: string,
|
|
8
|
+
attribute: Attribute | Attribute[],
|
|
9
|
+
themes: string[],
|
|
10
|
+
value?: Record<string, string> | undefined,
|
|
11
|
+
forcedTheme?: string | undefined,
|
|
12
|
+
enableSystem?: boolean | undefined,
|
|
13
|
+
enableColorScheme?: boolean | undefined,
|
|
14
|
+
) => {
|
|
15
|
+
const el = document.documentElement;
|
|
16
|
+
|
|
17
|
+
function updateDOM(theme: string) {
|
|
18
|
+
const attributes = Array.isArray(attribute) ? attribute : [attribute];
|
|
19
|
+
|
|
20
|
+
attributes.forEach((attr) => {
|
|
21
|
+
const isClass = attr === "class";
|
|
22
|
+
const classes = isClass && value ? themes.map((t) => value[t] || t) : themes;
|
|
23
|
+
if (isClass) {
|
|
24
|
+
el.classList.remove(...classes);
|
|
25
|
+
el.classList.add(value && value[theme] ? value[theme] : theme);
|
|
26
|
+
} else {
|
|
27
|
+
el.setAttribute(attr, theme);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
setColorScheme(theme);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function setColorScheme(theme: string) {
|
|
35
|
+
if (enableColorScheme) {
|
|
36
|
+
// Extract base color scheme from theme name
|
|
37
|
+
const isDark = theme.includes("dark");
|
|
38
|
+
el.style.colorScheme = isDark ? "dark" : "light";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getSystemTheme() {
|
|
43
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
44
|
+
? "liquid-dark"
|
|
45
|
+
: "liquid-light";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseOldTheme(oldTheme: string): { baseTheme: string; isLiquid: boolean } {
|
|
49
|
+
if (oldTheme === "system") {
|
|
50
|
+
return { baseTheme: "system", isLiquid: false };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (oldTheme.startsWith("liquid-")) {
|
|
54
|
+
const base = oldTheme.replace("liquid-", "");
|
|
55
|
+
return { baseTheme: base, isLiquid: true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { baseTheme: oldTheme, isLiquid: false };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function computeTheme(baseTheme: string, isLiquid: boolean, systemIsDark: boolean): string {
|
|
62
|
+
let resolvedBase: string;
|
|
63
|
+
|
|
64
|
+
if (baseTheme === "system") {
|
|
65
|
+
resolvedBase = systemIsDark ? "dark" : "light";
|
|
66
|
+
} else {
|
|
67
|
+
resolvedBase = baseTheme;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return isLiquid ? `liquid-${resolvedBase}` : resolvedBase;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getStoredState(): { baseTheme: string; isLiquid: boolean } {
|
|
74
|
+
try {
|
|
75
|
+
// Try to read from new storage format
|
|
76
|
+
const baseTheme = localStorage.getItem(`${storageKey}-base`);
|
|
77
|
+
const isLiquid = localStorage.getItem(`${storageKey}-liquid`);
|
|
78
|
+
|
|
79
|
+
if (baseTheme !== null && isLiquid !== null) {
|
|
80
|
+
return {
|
|
81
|
+
baseTheme,
|
|
82
|
+
isLiquid: isLiquid === "true"
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const oldTheme = localStorage.getItem(storageKey);
|
|
87
|
+
if (oldTheme) {
|
|
88
|
+
return parseOldTheme(oldTheme);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const defaultBaseTheme = enableSystem ? "system" : "light";
|
|
92
|
+
return {
|
|
93
|
+
baseTheme: defaultBaseTheme,
|
|
94
|
+
isLiquid: true
|
|
95
|
+
};
|
|
96
|
+
} catch {
|
|
97
|
+
// Default values on error
|
|
98
|
+
const defaultBaseTheme = enableSystem ? "system" : "light";
|
|
99
|
+
return {
|
|
100
|
+
baseTheme: defaultBaseTheme,
|
|
101
|
+
isLiquid: true
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (forcedTheme) {
|
|
107
|
+
updateDOM(forcedTheme);
|
|
108
|
+
} else {
|
|
109
|
+
const { baseTheme, isLiquid } = getStoredState();
|
|
110
|
+
|
|
111
|
+
const hasStoredBase = localStorage.getItem(`${storageKey}-base`) !== null;
|
|
112
|
+
const hasStoredLiquid = localStorage.getItem(`${storageKey}-liquid`) !== null;
|
|
113
|
+
|
|
114
|
+
if (!hasStoredBase || !hasStoredLiquid) {
|
|
115
|
+
try {
|
|
116
|
+
if (!hasStoredBase) {
|
|
117
|
+
localStorage.setItem(`${storageKey}-base`, baseTheme);
|
|
118
|
+
}
|
|
119
|
+
if (!hasStoredLiquid) {
|
|
120
|
+
localStorage.setItem(`${storageKey}-liquid`, String(isLiquid));
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const systemIsDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
127
|
+
const computedTheme = computeTheme(baseTheme, isLiquid, systemIsDark);
|
|
128
|
+
updateDOM(computedTheme);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
interface ThemeScriptProps extends ThemeProviderProps {
|
|
133
|
+
themes: string[];
|
|
134
|
+
storageKey: string;
|
|
135
|
+
defaultTheme: string;
|
|
136
|
+
attribute: Attribute | Attribute[];
|
|
137
|
+
forcedTheme?: string;
|
|
138
|
+
enableSystem?: boolean;
|
|
139
|
+
enableColorScheme?: boolean;
|
|
140
|
+
value?: Record<string, string>;
|
|
141
|
+
nonce?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const ThemeScript = memo(
|
|
145
|
+
({
|
|
146
|
+
forcedTheme,
|
|
147
|
+
storageKey,
|
|
148
|
+
attribute,
|
|
149
|
+
enableSystem,
|
|
150
|
+
enableColorScheme,
|
|
151
|
+
defaultTheme,
|
|
152
|
+
value,
|
|
153
|
+
themes,
|
|
154
|
+
nonce,
|
|
155
|
+
scriptProps,
|
|
156
|
+
}: ThemeScriptProps) => {
|
|
157
|
+
const scriptArgs = JSON.stringify([
|
|
158
|
+
defaultTheme,
|
|
159
|
+
storageKey,
|
|
160
|
+
attribute,
|
|
161
|
+
themes,
|
|
162
|
+
value,
|
|
163
|
+
forcedTheme,
|
|
164
|
+
enableSystem,
|
|
165
|
+
enableColorScheme,
|
|
166
|
+
]).slice(1, -1);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<script
|
|
170
|
+
{...scriptProps}
|
|
171
|
+
suppressHydrationWarning
|
|
172
|
+
nonce={typeof window === "undefined" ? nonce : ""}
|
|
173
|
+
dangerouslySetInnerHTML={{ __html: `(${themeScript.toString()})(${scriptArgs})` }}
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
export { ThemeScript };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { DetailedHTMLProps, Dispatch, ScriptHTMLAttributes, SetStateAction } from "react";
|
|
2
|
+
|
|
3
|
+
type DataAttribute = `data-${string}`;
|
|
4
|
+
|
|
5
|
+
interface ScriptProps
|
|
6
|
+
extends DetailedHTMLProps<ScriptHTMLAttributes<HTMLScriptElement>, HTMLScriptElement> {
|
|
7
|
+
[dataAttribute: DataAttribute]: never;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type BaseTheme = "dark" | "light" | "system";
|
|
11
|
+
|
|
12
|
+
/** Computed actual theme type */
|
|
13
|
+
export type ComputedTheme = "light" | "dark" | "liquid-light" | "liquid-dark";
|
|
14
|
+
|
|
15
|
+
export interface UseThemeProps {
|
|
16
|
+
/** Base theme color */
|
|
17
|
+
baseTheme: BaseTheme;
|
|
18
|
+
/** Set base theme color */
|
|
19
|
+
setBaseTheme: (theme: BaseTheme) => void;
|
|
20
|
+
/** Whether liquid mode is enabled */
|
|
21
|
+
isLiquid: boolean;
|
|
22
|
+
/** Toggle liquid mode */
|
|
23
|
+
toggleLiquid: () => void;
|
|
24
|
+
/** Computed actual theme name */
|
|
25
|
+
computedTheme: ComputedTheme;
|
|
26
|
+
/** Resolved theme (handles system theme) */
|
|
27
|
+
resolvedTheme: ComputedTheme;
|
|
28
|
+
/** System theme preference (only valid when baseTheme is system) */
|
|
29
|
+
systemTheme?: "dark" | "light" | undefined;
|
|
30
|
+
/** Forced theme name */
|
|
31
|
+
forcedTheme?: string | undefined;
|
|
32
|
+
/** Local storage key */
|
|
33
|
+
storageKey: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type Attribute = DataAttribute | "class";
|
|
37
|
+
|
|
38
|
+
export interface ThemeProviderProps {
|
|
39
|
+
/** Default base theme */
|
|
40
|
+
defaultBaseTheme?: BaseTheme | undefined;
|
|
41
|
+
/** Default liquid mode enabled */
|
|
42
|
+
defaultIsLiquid?: boolean | undefined;
|
|
43
|
+
/** Forced theme name */
|
|
44
|
+
forcedTheme?: string | undefined;
|
|
45
|
+
/** Enable system theme detection */
|
|
46
|
+
enableSystem?: boolean | undefined;
|
|
47
|
+
/** Disable CSS transitions when switching themes */
|
|
48
|
+
disableTransitionOnChange?: boolean | undefined;
|
|
49
|
+
/** Enable color scheme indication for built-in UI elements */
|
|
50
|
+
enableColorScheme?: boolean | undefined;
|
|
51
|
+
/** Local storage key prefix */
|
|
52
|
+
storageKey?: string | undefined;
|
|
53
|
+
/** HTML attribute name */
|
|
54
|
+
attribute?: Attribute | Attribute[] | undefined;
|
|
55
|
+
/** Theme name to attribute value mapping */
|
|
56
|
+
value?: Record<string, string> | undefined;
|
|
57
|
+
/** CSP nonce */
|
|
58
|
+
nonce?: string;
|
|
59
|
+
/** Script tag properties */
|
|
60
|
+
scriptProps?: ScriptProps;
|
|
61
|
+
}
|
package/src/ui/toast.tsx
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Toaster as Sonner, ToasterProps, toast } from "sonner";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Client-only Toaster wrapper applying default classes.
|
|
6
|
+
*/
|
|
7
|
+
const Toaster = ({ ...props }: ToasterProps) => {
|
|
8
|
+
const [isClient, setIsClient] = useState(false);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
setIsClient(true);
|
|
12
|
+
}, []);
|
|
13
|
+
|
|
14
|
+
// Don't render anything during SSR
|
|
15
|
+
if (!isClient) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Sonner
|
|
21
|
+
toastOptions={{
|
|
22
|
+
className: "vk-toast liquid:liquid-bg",
|
|
23
|
+
descriptionClassName: "vk-toast-description",
|
|
24
|
+
}}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export { Toaster, toast };
|