@reactberry/system 2.0.0-beta
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 +48 -0
- package/package.json +74 -0
- package/src/blocks/Accordion/index.tsx +158 -0
- package/src/blocks/AnimatedCarousel/index.tsx +188 -0
- package/src/blocks/AppleGlow/index.tsx +144 -0
- package/src/blocks/Avatar/index.tsx +167 -0
- package/src/blocks/Await/index.tsx +45 -0
- package/src/blocks/Cards/AnimatedCard/index.tsx +175 -0
- package/src/blocks/Cards/FluorescentCard/index.tsx +180 -0
- package/src/blocks/Cards/InfoCard/index.tsx +206 -0
- package/src/blocks/Cards/TickerCard/index.tsx +125 -0
- package/src/blocks/Carousel/index.tsx +216 -0
- package/src/blocks/Checkbox/index.tsx +101 -0
- package/src/blocks/Collection/index.tsx +59 -0
- package/src/blocks/Container/index.tsx +55 -0
- package/src/blocks/Controls/Control.tsx +67 -0
- package/src/blocks/Controls/index.tsx +11 -0
- package/src/blocks/CyclingNumber/index.tsx +78 -0
- package/src/blocks/DisplaySet/index.tsx +42 -0
- package/src/blocks/Divider/index.tsx +14 -0
- package/src/blocks/Draggable/index.tsx +266 -0
- package/src/blocks/Drawer/index.tsx +136 -0
- package/src/blocks/DynamicIsland/DynamicIsland.tsx +89 -0
- package/src/blocks/DynamicIsland/index.tsx +2 -0
- package/src/blocks/Fader/index.tsx +145 -0
- package/src/blocks/FamilyDrawer/README.md +116 -0
- package/src/blocks/FamilyDrawer/example.tsx +108 -0
- package/src/blocks/FamilyDrawer/index.tsx +119 -0
- package/src/blocks/FamilyDrawer/views/DefaultView.tsx +93 -0
- package/src/blocks/FamilyDrawer/views/KeyView.tsx +129 -0
- package/src/blocks/FamilyDrawer/views/PhraseView.tsx +129 -0
- package/src/blocks/FamilyDrawer/views/RemoveView.tsx +81 -0
- package/src/blocks/FieldSet/index.tsx +173 -0
- package/src/blocks/Filesystem/index.tsx +198 -0
- package/src/blocks/Gallery/Carousel/index.tsx +257 -0
- package/src/blocks/Gallery/Modal/index.tsx +83 -0
- package/src/blocks/Gallery/index.tsx +57 -0
- package/src/blocks/Gallery/utils/animationVariants.ts +18 -0
- package/src/blocks/Gallery/utils/aspectRatio.ts +14 -0
- package/src/blocks/Gallery/utils/downloadPhoto.ts +24 -0
- package/src/blocks/Gallery/utils/range.ts +11 -0
- package/src/blocks/GradientMesh/index.tsx +106 -0
- package/src/blocks/Group/index.tsx +152 -0
- package/src/blocks/Heading/index.tsx +111 -0
- package/src/blocks/HorizontalScroller/index.tsx +135 -0
- package/src/blocks/Icon/index.tsx +45 -0
- package/src/blocks/Indicator/index.tsx +27 -0
- package/src/blocks/InlineEditor/index.tsx +216 -0
- package/src/blocks/List/index.tsx +657 -0
- package/src/blocks/Main/index.tsx +17 -0
- package/src/blocks/Marquee/index.tsx +116 -0
- package/src/blocks/MaskedField/index.tsx +199 -0
- package/src/blocks/Menu/MenuContent.tsx +246 -0
- package/src/blocks/Menu/MenuContext.tsx +34 -0
- package/src/blocks/Menu/MenuItem.tsx +104 -0
- package/src/blocks/Menu/index.tsx +60 -0
- package/src/blocks/Modal/index.tsx +268 -0
- package/src/blocks/MorphingPopover/index.tsx +294 -0
- package/src/blocks/Overlay/Backdrop.tsx +48 -0
- package/src/blocks/Overlay/OverscrollGuard.tsx +36 -0
- package/src/blocks/Overlay/index.ts +2 -0
- package/src/blocks/Parallax/index.tsx +117 -0
- package/src/blocks/ParallaxSection/index.tsx +61 -0
- package/src/blocks/Placeholder/index.tsx +48 -0
- package/src/blocks/Popover/index.tsx +402 -0
- package/src/blocks/Progress/getProgressColor.ts +61 -0
- package/src/blocks/Progress/index.tsx +179 -0
- package/src/blocks/ProgressiveBlur/index.tsx +75 -0
- package/src/blocks/README.md +15 -0
- package/src/blocks/RenderAsset/index.tsx +18 -0
- package/src/blocks/ScrollContainer/index.tsx +93 -0
- package/src/blocks/ShinyText/index.tsx +72 -0
- package/src/blocks/Skeleton/index.tsx +71 -0
- package/src/blocks/Slider/SliderControls.tsx +119 -0
- package/src/blocks/Slider/index.tsx +140 -0
- package/src/blocks/Slider/useSlider.ts +126 -0
- package/src/blocks/Slideshow/index.tsx +177 -0
- package/src/blocks/Spotlight/index.tsx +144 -0
- package/src/blocks/Steps/StepIndicator.tsx +149 -0
- package/src/blocks/Steps/StepProgress.tsx +164 -0
- package/src/blocks/Steps/Steps.tsx +197 -0
- package/src/blocks/Steps/StepsNav.tsx +30 -0
- package/src/blocks/Steps/StepsTracker.tsx +80 -0
- package/src/blocks/Steps/hooks.ts +71 -0
- package/src/blocks/Steps/index.tsx +16 -0
- package/src/blocks/Steps/types.ts +71 -0
- package/src/blocks/StickySectionStack/index.tsx +136 -0
- package/src/blocks/Switch/index.tsx +85 -0
- package/src/blocks/SystemNotice/index.tsx +81 -0
- package/src/blocks/Table/README.md +251 -0
- package/src/blocks/Table/Table.tsx +207 -0
- package/src/blocks/Table/TablePagination.tsx +189 -0
- package/src/blocks/Table/index.ts +33 -0
- package/src/blocks/Table/useTableControls.ts +331 -0
- package/src/blocks/Tag/index.tsx +27 -0
- package/src/blocks/TextBreak/index.tsx +96 -0
- package/src/blocks/TextReveal/index.tsx +104 -0
- package/src/blocks/Thumbnail/index.tsx +26 -0
- package/src/blocks/Ticker/index.tsx +112 -0
- package/src/blocks/Toast/index.tsx +77 -0
- package/src/blocks/Tooltip/index.tsx +174 -0
- package/src/blocks/Underlay/index.tsx +104 -0
- package/src/blocks/Upload/Dropzone.tsx +92 -0
- package/src/blocks/Upload/UploadBtn.tsx +38 -0
- package/src/blocks/Upload/index.tsx +61 -0
- package/src/blocks/Upload/types.ts +37 -0
- package/src/blocks/VideoMarquee/index.tsx +511 -0
- package/src/blocks/index.ts +119 -0
- package/src/blocks/pagination/Pagination.tsx +148 -0
- package/src/blocks/pagination/PaginationList.tsx +41 -0
- package/src/blocks/pagination/index.ts +2 -0
- package/src/charts/BarChart.tsx +63 -0
- package/src/charts/PieChart.tsx +39 -0
- package/src/charts/index.ts +3 -0
- package/src/charts/utils.ts +103 -0
- package/src/docs/README.md +373 -0
- package/src/docs/reference/README.md +299 -0
- package/src/elements/box.ts +163 -0
- package/src/elements/button.ts +49 -0
- package/src/elements/field.ts +129 -0
- package/src/elements/index.ts +8 -0
- package/src/elements/text.ts +47 -0
- package/src/elements/utils.js +97 -0
- package/src/hooks/use-copy-to-clipboard.tsx +33 -0
- package/src/hooks/use-enter-submit.tsx +23 -0
- package/src/hooks/use-local-storage.ts +42 -0
- package/src/hooks/use-sidebar.tsx +109 -0
- package/src/hooks/useAnimatedText.ts +32 -0
- package/src/hooks/useAutosizeTextArea.ts +45 -0
- package/src/hooks/useBreakpoint.tsx +123 -0
- package/src/hooks/useClickOutside.tsx +38 -0
- package/src/hooks/useHover.tsx +33 -0
- package/src/hooks/useHoverList.tsx +17 -0
- package/src/hooks/useKeyboardShortcuts.ts +91 -0
- package/src/hooks/useKeypress.ts +27 -0
- package/src/hooks/useOverlay.ts +32 -0
- package/src/hooks/useReducedMotion.ts +25 -0
- package/src/hooks/useStandaloneMode.ts +35 -0
- package/src/hooks/useTouchDevice.ts +34 -0
- package/src/icons/index.tsx +129 -0
- package/src/index.ts +12 -0
- package/src/providers/DesignSystemProvider.tsx +35 -0
- package/src/providers/StyledComponentsRegistry.tsx +30 -0
- package/src/providers/index.ts +2 -0
- package/src/themes/README.md +30 -0
- package/src/themes/default/assets/badge-avatar.tsx +45 -0
- package/src/themes/default/assets/logo.tsx +42 -0
- package/src/themes/default/global.ts +138 -0
- package/src/themes/default/modes/dark/config.js +49 -0
- package/src/themes/default/modes/dark/skins.js +631 -0
- package/src/themes/default/modes/dark/theme.js +87 -0
- package/src/themes/default/modes/light/config.js +48 -0
- package/src/themes/default/modes/light/skins.js +1026 -0
- package/src/themes/default/modes/light/theme.js +74 -0
- package/src/themes/default/tokens/controls.js +53 -0
- package/src/themes/default/tokens/shadows.js +63 -0
- package/src/themes/default/tokens/shapes.js +37 -0
- package/src/themes/default/tokens/space.js +143 -0
- package/src/themes/default/tokens/spectre.js +16 -0
- package/src/themes/default/utils.js +523 -0
- package/src/themes/index.ts +11 -0
- package/src/types.ts +394 -0
- package/src/utils/overlayTheme.ts +61 -0
- package/src/utils/pickColor.ts +15 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { animate } from "motion/react";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
const delimiter = ""; // or " " to split by word
|
|
7
|
+
|
|
8
|
+
export function useAnimatedText(text: string) {
|
|
9
|
+
const [cursor, setCursor] = useState(0);
|
|
10
|
+
const [startingCursor, setStartingCursor] = useState(0);
|
|
11
|
+
const [prevText, setPrevText] = useState(text);
|
|
12
|
+
|
|
13
|
+
if (prevText !== text) {
|
|
14
|
+
setPrevText(text);
|
|
15
|
+
setStartingCursor(text.startsWith(prevText) ? cursor : 0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const controls = animate(startingCursor, text.split(delimiter).length, {
|
|
20
|
+
// Tweak the animation here
|
|
21
|
+
duration: 4,
|
|
22
|
+
ease: "easeOut",
|
|
23
|
+
onUpdate(latest) {
|
|
24
|
+
setCursor(Math.floor(latest));
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return () => controls.stop();
|
|
29
|
+
}, [startingCursor, text]);
|
|
30
|
+
|
|
31
|
+
return text.split(delimiter).slice(0, cursor).join(delimiter);
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
interface UseAutosizeTextAreaOptions {
|
|
4
|
+
minRows?: number;
|
|
5
|
+
maxRows?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook that automatically adjusts textarea height based on content
|
|
10
|
+
* @param textareaRef - Reference to the textarea element
|
|
11
|
+
* @param value - Current value of the textarea
|
|
12
|
+
* @param minRows - Minimum number of rows (default: 1)
|
|
13
|
+
* @param maxRows - Maximum number of rows (default: 10)
|
|
14
|
+
*/
|
|
15
|
+
export function useAutosizeTextArea(
|
|
16
|
+
textareaRef: HTMLTextAreaElement | null,
|
|
17
|
+
value: string,
|
|
18
|
+
options: UseAutosizeTextAreaOptions = {}
|
|
19
|
+
): void {
|
|
20
|
+
const { minRows = 1, maxRows = 10 } = options;
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!textareaRef) return;
|
|
24
|
+
|
|
25
|
+
// Reset height to auto to get the correct scrollHeight
|
|
26
|
+
textareaRef.style.height = "auto";
|
|
27
|
+
|
|
28
|
+
// Get the line height
|
|
29
|
+
const lineHeight = parseInt(
|
|
30
|
+
window.getComputedStyle(textareaRef).lineHeight,
|
|
31
|
+
10
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Calculate the number of rows needed
|
|
35
|
+
const scrollHeight = textareaRef.scrollHeight;
|
|
36
|
+
const currentRows = Math.ceil(scrollHeight / lineHeight);
|
|
37
|
+
|
|
38
|
+
// Constrain between min and max rows
|
|
39
|
+
const rowsToShow = Math.max(minRows, Math.min(currentRows, maxRows));
|
|
40
|
+
|
|
41
|
+
// Set the height based on rows
|
|
42
|
+
textareaRef.style.height = `${rowsToShow * lineHeight}px`;
|
|
43
|
+
}, [textareaRef, value, minRows, maxRows]);
|
|
44
|
+
}
|
|
45
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useState,
|
|
7
|
+
useEffect,
|
|
8
|
+
ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
|
|
11
|
+
type Breakpoint = "_" | "xs" | "sm" | "md" | "lg" | "xl";
|
|
12
|
+
|
|
13
|
+
interface BreakpointContextType {
|
|
14
|
+
breakpoint: Breakpoint;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const BreakpointContext = createContext<BreakpointContextType | undefined>(
|
|
18
|
+
undefined,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// Hardcoded order - design system breakpoints are stable
|
|
22
|
+
const BREAKPOINT_ORDER: Breakpoint[] = ["_", "xs", "sm", "md", "lg", "xl"];
|
|
23
|
+
const BREAKPOINT_KEYS = ["xl", "lg", "md", "sm", "xs"] as const;
|
|
24
|
+
|
|
25
|
+
// Type-safe breakpoint mapping - derived from design system tokens
|
|
26
|
+
const BREAKPOINT_VALUES: Record<Exclude<Breakpoint, "_">, string> = {
|
|
27
|
+
xs: "32rem",
|
|
28
|
+
sm: "48rem",
|
|
29
|
+
md: "64rem",
|
|
30
|
+
lg: "80rem",
|
|
31
|
+
xl: "96rem",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Provider to wrap your app - add to root layout
|
|
36
|
+
* Only ONE set of listeners for the entire app
|
|
37
|
+
*/
|
|
38
|
+
export function BreakpointProvider({ children }: { children: ReactNode }) {
|
|
39
|
+
const [breakpoint, setBreakpoint] = useState<Breakpoint>("_");
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const queries: Record<string, MediaQueryList> = {};
|
|
43
|
+
|
|
44
|
+
BREAKPOINT_KEYS.forEach((key) => {
|
|
45
|
+
queries[key] = window.matchMedia(
|
|
46
|
+
`(min-width: ${BREAKPOINT_VALUES[key]})`,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const updateBreakpoint = (): void => {
|
|
51
|
+
for (const key of BREAKPOINT_KEYS) {
|
|
52
|
+
if (queries[key]?.matches) {
|
|
53
|
+
setBreakpoint(key as Breakpoint);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
setBreakpoint("_");
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
updateBreakpoint();
|
|
61
|
+
|
|
62
|
+
const handleChange = (): void => updateBreakpoint();
|
|
63
|
+
|
|
64
|
+
BREAKPOINT_KEYS.forEach((key) => {
|
|
65
|
+
queries[key]?.addEventListener("change", handleChange);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
BREAKPOINT_KEYS.forEach((key) => {
|
|
70
|
+
queries[key]?.removeEventListener("change", handleChange);
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<BreakpointContext.Provider value={{ breakpoint }}>
|
|
77
|
+
{children}
|
|
78
|
+
</BreakpointContext.Provider>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Hook to check if current width is at or above a specific breakpoint
|
|
84
|
+
* @param targetBreakpoint Breakpoint to check against
|
|
85
|
+
* @returns boolean - true if current width >= breakpoint
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* const isMd = useBreakpoint("md"); // true if md and above
|
|
89
|
+
* const isDesktop = useBreakpoint("lg"); // true if lg and above
|
|
90
|
+
*/
|
|
91
|
+
export function useBreakpoint(targetBreakpoint: Breakpoint): boolean {
|
|
92
|
+
const context = useContext(BreakpointContext);
|
|
93
|
+
|
|
94
|
+
if (!context) {
|
|
95
|
+
throw new Error("useBreakpoint must be used within BreakpointProvider");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const currentIndex = BREAKPOINT_ORDER.indexOf(context.breakpoint);
|
|
99
|
+
const targetIndex = BREAKPOINT_ORDER.indexOf(targetBreakpoint);
|
|
100
|
+
|
|
101
|
+
return currentIndex >= targetIndex;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Hook to check if current breakpoint matches exactly a specific breakpoint
|
|
106
|
+
* @param targetBreakpoint Breakpoint to match
|
|
107
|
+
* @returns boolean - true if current breakpoint === targetBreakpoint
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* const isMobileOnly = useShowOnBreakpoint("_"); // true only on mobile
|
|
111
|
+
* const isTabletOnly = useShowOnBreakpoint("sm"); // true only on tablet
|
|
112
|
+
*/
|
|
113
|
+
export function useShowOnBreakpoint(targetBreakpoint: Breakpoint): boolean {
|
|
114
|
+
const context = useContext(BreakpointContext);
|
|
115
|
+
|
|
116
|
+
if (!context) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
"useShowOnBreakpoint must be used within BreakpointProvider",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return context.breakpoint === targetBreakpoint;
|
|
123
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useEffect, type RefObject } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Call `handler` when a mouse or touch event occurs outside the given ref.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const ref = useRef<HTMLDivElement | null>(null);
|
|
8
|
+
* useClickOutside(ref, () => setOpen(false), open);
|
|
9
|
+
*/
|
|
10
|
+
export function useClickOutside<T extends HTMLElement = HTMLElement>(
|
|
11
|
+
ref: RefObject<T | null>,
|
|
12
|
+
handler: (event: MouseEvent | TouchEvent) => void,
|
|
13
|
+
enabled: boolean = true,
|
|
14
|
+
): void {
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!enabled) return;
|
|
17
|
+
|
|
18
|
+
const handleEvent = (event: MouseEvent | TouchEvent) => {
|
|
19
|
+
const node = ref.current;
|
|
20
|
+
if (!node) return;
|
|
21
|
+
|
|
22
|
+
if (node.contains(event.target as Node)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
handler(event);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
document.addEventListener("mousedown", handleEvent);
|
|
30
|
+
document.addEventListener("touchstart", handleEvent);
|
|
31
|
+
|
|
32
|
+
return () => {
|
|
33
|
+
document.removeEventListener("mousedown", handleEvent);
|
|
34
|
+
document.removeEventListener("touchstart", handleEvent);
|
|
35
|
+
};
|
|
36
|
+
}, [ref, handler, enabled]);
|
|
37
|
+
}
|
|
38
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useRef,
|
|
3
|
+
useState,
|
|
4
|
+
useEffect,
|
|
5
|
+
useCallback,
|
|
6
|
+
type MutableRefObject,
|
|
7
|
+
} from "react";
|
|
8
|
+
|
|
9
|
+
export function useHover<T extends HTMLElement = HTMLDivElement>(): [
|
|
10
|
+
MutableRefObject<T | null>,
|
|
11
|
+
boolean
|
|
12
|
+
] {
|
|
13
|
+
const [hovered, setHovered] = useState(false);
|
|
14
|
+
const ref = useRef<T>(null);
|
|
15
|
+
|
|
16
|
+
const handleMouseOver = useCallback(() => setHovered(true), []);
|
|
17
|
+
const handleMouseOut = useCallback(() => setHovered(false), []);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const node = ref.current;
|
|
21
|
+
if (!node) return;
|
|
22
|
+
|
|
23
|
+
node.addEventListener("mouseenter", handleMouseOver);
|
|
24
|
+
node.addEventListener("mouseleave", handleMouseOut);
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
node.removeEventListener("mouseenter", handleMouseOver);
|
|
28
|
+
node.removeEventListener("mouseleave", handleMouseOut);
|
|
29
|
+
};
|
|
30
|
+
}, [handleMouseOver, handleMouseOut]);
|
|
31
|
+
|
|
32
|
+
return [ref, hovered];
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useState, type MouseEventHandler } from "react";
|
|
2
|
+
|
|
3
|
+
export type HoverProps = {
|
|
4
|
+
onMouseEnter: MouseEventHandler<HTMLElement>;
|
|
5
|
+
onMouseLeave: MouseEventHandler<HTMLElement>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function useHoverList() {
|
|
9
|
+
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
|
10
|
+
|
|
11
|
+
const getHoverProps = (index: number): HoverProps => ({
|
|
12
|
+
onMouseEnter: () => setHoveredIndex(index),
|
|
13
|
+
onMouseLeave: () => setHoveredIndex(null),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return { hoveredIndex, getHoverProps };
|
|
17
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
interface KeyboardShortcuts {
|
|
4
|
+
onSend?: () => void;
|
|
5
|
+
onSaveDraft?: () => void;
|
|
6
|
+
onPreview?: () => void;
|
|
7
|
+
onSchedule?: () => void;
|
|
8
|
+
onAddAttachment?: () => void;
|
|
9
|
+
onToggleMode?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useKeyboardShortcuts({
|
|
13
|
+
onSend,
|
|
14
|
+
onSaveDraft,
|
|
15
|
+
onPreview,
|
|
16
|
+
onSchedule,
|
|
17
|
+
onAddAttachment,
|
|
18
|
+
onToggleMode
|
|
19
|
+
}: KeyboardShortcuts) {
|
|
20
|
+
const handleKeyDown = useCallback((event: KeyboardEvent) => {
|
|
21
|
+
const { key, metaKey, ctrlKey, shiftKey } = event;
|
|
22
|
+
const cmdOrCtrl = metaKey || ctrlKey;
|
|
23
|
+
|
|
24
|
+
// Cmd/Ctrl + Enter: Send email
|
|
25
|
+
if (cmdOrCtrl && key === 'Enter' && onSend) {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
onSend();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Cmd/Ctrl + S: Save draft
|
|
32
|
+
if (cmdOrCtrl && key === 's' && onSaveDraft) {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
onSaveDraft();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Cmd/Ctrl + P: Preview
|
|
39
|
+
if (cmdOrCtrl && key === 'p' && onPreview) {
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
onPreview();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Cmd/Ctrl + Shift + S: Schedule
|
|
46
|
+
if (cmdOrCtrl && shiftKey && key === 'S' && onSchedule) {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
onSchedule();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Cmd/Ctrl + U: Add attachment
|
|
53
|
+
if (cmdOrCtrl && key === 'u' && onAddAttachment) {
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
onAddAttachment();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Cmd/Ctrl + M: Toggle between editor and preview mode
|
|
60
|
+
if (cmdOrCtrl && key === 'm' && onToggleMode) {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
onToggleMode();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Escape: Close any open dialogs (handled by parent component)
|
|
67
|
+
if (key === 'Escape') {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
// Let parent component handle this
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}, [onSend, onSaveDraft, onPreview, onSchedule, onAddAttachment, onToggleMode]);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
76
|
+
return () => {
|
|
77
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
78
|
+
};
|
|
79
|
+
}, [handleKeyDown]);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
shortcuts: {
|
|
83
|
+
send: '⌘+Enter',
|
|
84
|
+
saveDraft: '⌘+S',
|
|
85
|
+
preview: '⌘+P',
|
|
86
|
+
schedule: '⌘+⇧+S',
|
|
87
|
+
addAttachment: '⌘+U',
|
|
88
|
+
toggleMode: '⌘+M'
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect, useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom hook to handle keyboard events
|
|
5
|
+
* @param targetKey - The key to listen for (e.g., "ArrowRight", "ArrowLeft")
|
|
6
|
+
* @param callback - The function to execute when the key is pressed
|
|
7
|
+
*/
|
|
8
|
+
export const useKeypress = (targetKey: string, callback: () => void) => {
|
|
9
|
+
const handleKeyPress = useCallback(
|
|
10
|
+
(event: KeyboardEvent) => {
|
|
11
|
+
if (event.key === targetKey) {
|
|
12
|
+
callback();
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
[targetKey, callback]
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
// Add event listener when component mounts
|
|
20
|
+
window.addEventListener("keydown", handleKeyPress);
|
|
21
|
+
|
|
22
|
+
// Remove event listener when component unmounts
|
|
23
|
+
return () => {
|
|
24
|
+
window.removeEventListener("keydown", handleKeyPress);
|
|
25
|
+
};
|
|
26
|
+
}, [handleKeyPress]);
|
|
27
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { pushThemeColor, popThemeColor } from "@/design-system/utils/overlayTheme";
|
|
5
|
+
import { useStandaloneMode } from "@/design-system/hooks/useStandaloneMode";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Shared behavioral hook for overlay components (Modal, Drawer, etc.).
|
|
9
|
+
*
|
|
10
|
+
* Handles three concerns:
|
|
11
|
+
* 1. SSR-safe mounting (portal rendering requires `document.body`)
|
|
12
|
+
* 2. Ref-counted theme-color tinting + body scroll lock
|
|
13
|
+
* 3. PWA standalone-mode detection for safe-area padding
|
|
14
|
+
*
|
|
15
|
+
* @param isOpen Whether the overlay is currently visible. Defaults to `true`
|
|
16
|
+
* (Modal is always open when mounted; Drawer passes `isSidebarOpen`).
|
|
17
|
+
*/
|
|
18
|
+
export function useOverlay(isOpen: boolean = true) {
|
|
19
|
+
const [mounted, setMounted] = useState(false);
|
|
20
|
+
const isStandalone = useStandaloneMode();
|
|
21
|
+
|
|
22
|
+
useEffect(() => setMounted(true), []);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (isOpen) {
|
|
26
|
+
pushThemeColor();
|
|
27
|
+
return () => popThemeColor();
|
|
28
|
+
}
|
|
29
|
+
}, [isOpen]);
|
|
30
|
+
|
|
31
|
+
return { mounted, isStandalone };
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to detect user's reduced motion preference.
|
|
7
|
+
* Returns true if the user prefers reduced motion.
|
|
8
|
+
*/
|
|
9
|
+
export function useReducedMotion(): boolean {
|
|
10
|
+
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
14
|
+
setPrefersReducedMotion(mediaQuery.matches);
|
|
15
|
+
|
|
16
|
+
const handler = (event: MediaQueryListEvent) => {
|
|
17
|
+
setPrefersReducedMotion(event.matches);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
mediaQuery.addEventListener("change", handler);
|
|
21
|
+
return () => mediaQuery.removeEventListener("change", handler);
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
return prefersReducedMotion;
|
|
25
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects whether the app is running in standalone (PWA) mode.
|
|
7
|
+
*
|
|
8
|
+
* Checks two signals:
|
|
9
|
+
* 1. The standard `(display-mode: standalone)` media query (Chrome, Edge, Firefox).
|
|
10
|
+
* 2. The iOS-specific `navigator.standalone` boolean (Safari on iOS).
|
|
11
|
+
*
|
|
12
|
+
* Listens for media-query changes so the value updates dynamically,
|
|
13
|
+
* though in practice display-mode rarely changes during a session.
|
|
14
|
+
*/
|
|
15
|
+
export function useStandaloneMode(): boolean {
|
|
16
|
+
const [isStandalone, setIsStandalone] = useState(false);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const iosStandalone =
|
|
20
|
+
"standalone" in navigator && (navigator as any).standalone === true;
|
|
21
|
+
|
|
22
|
+
const displayModeQuery = window.matchMedia("(display-mode: standalone)");
|
|
23
|
+
|
|
24
|
+
const update = () => {
|
|
25
|
+
setIsStandalone(iosStandalone || displayModeQuery.matches);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
update();
|
|
29
|
+
|
|
30
|
+
displayModeQuery.addEventListener("change", update);
|
|
31
|
+
return () => displayModeQuery.removeEventListener("change", update);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
return isStandalone;
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects whether the current device uses touch as its primary input.
|
|
7
|
+
*
|
|
8
|
+
* Combines hardware capability (maxTouchPoints) with the CSS pointer: coarse
|
|
9
|
+
* media query so that hybrid devices (e.g. Surface Pro with trackpad) return
|
|
10
|
+
* false when a fine pointer is the primary input.
|
|
11
|
+
*
|
|
12
|
+
* Listens for media-query changes so the value updates if the user detaches
|
|
13
|
+
* a keyboard / mouse mid-session.
|
|
14
|
+
*/
|
|
15
|
+
export function useTouchDevice(): boolean {
|
|
16
|
+
const [isTouchDevice, setIsTouchDevice] = useState(false);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const hasTouchCapability =
|
|
20
|
+
"ontouchstart" in window || navigator.maxTouchPoints > 0;
|
|
21
|
+
const coarsePointerQuery = window.matchMedia("(pointer: coarse)");
|
|
22
|
+
|
|
23
|
+
const update = () => {
|
|
24
|
+
setIsTouchDevice(hasTouchCapability && coarsePointerQuery.matches);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
update();
|
|
28
|
+
|
|
29
|
+
coarsePointerQuery.addEventListener("change", update);
|
|
30
|
+
return () => coarsePointerQuery.removeEventListener("change", update);
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
return isTouchDevice;
|
|
34
|
+
}
|