@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.
Files changed (165) hide show
  1. package/README.md +48 -0
  2. package/package.json +74 -0
  3. package/src/blocks/Accordion/index.tsx +158 -0
  4. package/src/blocks/AnimatedCarousel/index.tsx +188 -0
  5. package/src/blocks/AppleGlow/index.tsx +144 -0
  6. package/src/blocks/Avatar/index.tsx +167 -0
  7. package/src/blocks/Await/index.tsx +45 -0
  8. package/src/blocks/Cards/AnimatedCard/index.tsx +175 -0
  9. package/src/blocks/Cards/FluorescentCard/index.tsx +180 -0
  10. package/src/blocks/Cards/InfoCard/index.tsx +206 -0
  11. package/src/blocks/Cards/TickerCard/index.tsx +125 -0
  12. package/src/blocks/Carousel/index.tsx +216 -0
  13. package/src/blocks/Checkbox/index.tsx +101 -0
  14. package/src/blocks/Collection/index.tsx +59 -0
  15. package/src/blocks/Container/index.tsx +55 -0
  16. package/src/blocks/Controls/Control.tsx +67 -0
  17. package/src/blocks/Controls/index.tsx +11 -0
  18. package/src/blocks/CyclingNumber/index.tsx +78 -0
  19. package/src/blocks/DisplaySet/index.tsx +42 -0
  20. package/src/blocks/Divider/index.tsx +14 -0
  21. package/src/blocks/Draggable/index.tsx +266 -0
  22. package/src/blocks/Drawer/index.tsx +136 -0
  23. package/src/blocks/DynamicIsland/DynamicIsland.tsx +89 -0
  24. package/src/blocks/DynamicIsland/index.tsx +2 -0
  25. package/src/blocks/Fader/index.tsx +145 -0
  26. package/src/blocks/FamilyDrawer/README.md +116 -0
  27. package/src/blocks/FamilyDrawer/example.tsx +108 -0
  28. package/src/blocks/FamilyDrawer/index.tsx +119 -0
  29. package/src/blocks/FamilyDrawer/views/DefaultView.tsx +93 -0
  30. package/src/blocks/FamilyDrawer/views/KeyView.tsx +129 -0
  31. package/src/blocks/FamilyDrawer/views/PhraseView.tsx +129 -0
  32. package/src/blocks/FamilyDrawer/views/RemoveView.tsx +81 -0
  33. package/src/blocks/FieldSet/index.tsx +173 -0
  34. package/src/blocks/Filesystem/index.tsx +198 -0
  35. package/src/blocks/Gallery/Carousel/index.tsx +257 -0
  36. package/src/blocks/Gallery/Modal/index.tsx +83 -0
  37. package/src/blocks/Gallery/index.tsx +57 -0
  38. package/src/blocks/Gallery/utils/animationVariants.ts +18 -0
  39. package/src/blocks/Gallery/utils/aspectRatio.ts +14 -0
  40. package/src/blocks/Gallery/utils/downloadPhoto.ts +24 -0
  41. package/src/blocks/Gallery/utils/range.ts +11 -0
  42. package/src/blocks/GradientMesh/index.tsx +106 -0
  43. package/src/blocks/Group/index.tsx +152 -0
  44. package/src/blocks/Heading/index.tsx +111 -0
  45. package/src/blocks/HorizontalScroller/index.tsx +135 -0
  46. package/src/blocks/Icon/index.tsx +45 -0
  47. package/src/blocks/Indicator/index.tsx +27 -0
  48. package/src/blocks/InlineEditor/index.tsx +216 -0
  49. package/src/blocks/List/index.tsx +657 -0
  50. package/src/blocks/Main/index.tsx +17 -0
  51. package/src/blocks/Marquee/index.tsx +116 -0
  52. package/src/blocks/MaskedField/index.tsx +199 -0
  53. package/src/blocks/Menu/MenuContent.tsx +246 -0
  54. package/src/blocks/Menu/MenuContext.tsx +34 -0
  55. package/src/blocks/Menu/MenuItem.tsx +104 -0
  56. package/src/blocks/Menu/index.tsx +60 -0
  57. package/src/blocks/Modal/index.tsx +268 -0
  58. package/src/blocks/MorphingPopover/index.tsx +294 -0
  59. package/src/blocks/Overlay/Backdrop.tsx +48 -0
  60. package/src/blocks/Overlay/OverscrollGuard.tsx +36 -0
  61. package/src/blocks/Overlay/index.ts +2 -0
  62. package/src/blocks/Parallax/index.tsx +117 -0
  63. package/src/blocks/ParallaxSection/index.tsx +61 -0
  64. package/src/blocks/Placeholder/index.tsx +48 -0
  65. package/src/blocks/Popover/index.tsx +402 -0
  66. package/src/blocks/Progress/getProgressColor.ts +61 -0
  67. package/src/blocks/Progress/index.tsx +179 -0
  68. package/src/blocks/ProgressiveBlur/index.tsx +75 -0
  69. package/src/blocks/README.md +15 -0
  70. package/src/blocks/RenderAsset/index.tsx +18 -0
  71. package/src/blocks/ScrollContainer/index.tsx +93 -0
  72. package/src/blocks/ShinyText/index.tsx +72 -0
  73. package/src/blocks/Skeleton/index.tsx +71 -0
  74. package/src/blocks/Slider/SliderControls.tsx +119 -0
  75. package/src/blocks/Slider/index.tsx +140 -0
  76. package/src/blocks/Slider/useSlider.ts +126 -0
  77. package/src/blocks/Slideshow/index.tsx +177 -0
  78. package/src/blocks/Spotlight/index.tsx +144 -0
  79. package/src/blocks/Steps/StepIndicator.tsx +149 -0
  80. package/src/blocks/Steps/StepProgress.tsx +164 -0
  81. package/src/blocks/Steps/Steps.tsx +197 -0
  82. package/src/blocks/Steps/StepsNav.tsx +30 -0
  83. package/src/blocks/Steps/StepsTracker.tsx +80 -0
  84. package/src/blocks/Steps/hooks.ts +71 -0
  85. package/src/blocks/Steps/index.tsx +16 -0
  86. package/src/blocks/Steps/types.ts +71 -0
  87. package/src/blocks/StickySectionStack/index.tsx +136 -0
  88. package/src/blocks/Switch/index.tsx +85 -0
  89. package/src/blocks/SystemNotice/index.tsx +81 -0
  90. package/src/blocks/Table/README.md +251 -0
  91. package/src/blocks/Table/Table.tsx +207 -0
  92. package/src/blocks/Table/TablePagination.tsx +189 -0
  93. package/src/blocks/Table/index.ts +33 -0
  94. package/src/blocks/Table/useTableControls.ts +331 -0
  95. package/src/blocks/Tag/index.tsx +27 -0
  96. package/src/blocks/TextBreak/index.tsx +96 -0
  97. package/src/blocks/TextReveal/index.tsx +104 -0
  98. package/src/blocks/Thumbnail/index.tsx +26 -0
  99. package/src/blocks/Ticker/index.tsx +112 -0
  100. package/src/blocks/Toast/index.tsx +77 -0
  101. package/src/blocks/Tooltip/index.tsx +174 -0
  102. package/src/blocks/Underlay/index.tsx +104 -0
  103. package/src/blocks/Upload/Dropzone.tsx +92 -0
  104. package/src/blocks/Upload/UploadBtn.tsx +38 -0
  105. package/src/blocks/Upload/index.tsx +61 -0
  106. package/src/blocks/Upload/types.ts +37 -0
  107. package/src/blocks/VideoMarquee/index.tsx +511 -0
  108. package/src/blocks/index.ts +119 -0
  109. package/src/blocks/pagination/Pagination.tsx +148 -0
  110. package/src/blocks/pagination/PaginationList.tsx +41 -0
  111. package/src/blocks/pagination/index.ts +2 -0
  112. package/src/charts/BarChart.tsx +63 -0
  113. package/src/charts/PieChart.tsx +39 -0
  114. package/src/charts/index.ts +3 -0
  115. package/src/charts/utils.ts +103 -0
  116. package/src/docs/README.md +373 -0
  117. package/src/docs/reference/README.md +299 -0
  118. package/src/elements/box.ts +163 -0
  119. package/src/elements/button.ts +49 -0
  120. package/src/elements/field.ts +129 -0
  121. package/src/elements/index.ts +8 -0
  122. package/src/elements/text.ts +47 -0
  123. package/src/elements/utils.js +97 -0
  124. package/src/hooks/use-copy-to-clipboard.tsx +33 -0
  125. package/src/hooks/use-enter-submit.tsx +23 -0
  126. package/src/hooks/use-local-storage.ts +42 -0
  127. package/src/hooks/use-sidebar.tsx +109 -0
  128. package/src/hooks/useAnimatedText.ts +32 -0
  129. package/src/hooks/useAutosizeTextArea.ts +45 -0
  130. package/src/hooks/useBreakpoint.tsx +123 -0
  131. package/src/hooks/useClickOutside.tsx +38 -0
  132. package/src/hooks/useHover.tsx +33 -0
  133. package/src/hooks/useHoverList.tsx +17 -0
  134. package/src/hooks/useKeyboardShortcuts.ts +91 -0
  135. package/src/hooks/useKeypress.ts +27 -0
  136. package/src/hooks/useOverlay.ts +32 -0
  137. package/src/hooks/useReducedMotion.ts +25 -0
  138. package/src/hooks/useStandaloneMode.ts +35 -0
  139. package/src/hooks/useTouchDevice.ts +34 -0
  140. package/src/icons/index.tsx +129 -0
  141. package/src/index.ts +12 -0
  142. package/src/providers/DesignSystemProvider.tsx +35 -0
  143. package/src/providers/StyledComponentsRegistry.tsx +30 -0
  144. package/src/providers/index.ts +2 -0
  145. package/src/themes/README.md +30 -0
  146. package/src/themes/default/assets/badge-avatar.tsx +45 -0
  147. package/src/themes/default/assets/logo.tsx +42 -0
  148. package/src/themes/default/global.ts +138 -0
  149. package/src/themes/default/modes/dark/config.js +49 -0
  150. package/src/themes/default/modes/dark/skins.js +631 -0
  151. package/src/themes/default/modes/dark/theme.js +87 -0
  152. package/src/themes/default/modes/light/config.js +48 -0
  153. package/src/themes/default/modes/light/skins.js +1026 -0
  154. package/src/themes/default/modes/light/theme.js +74 -0
  155. package/src/themes/default/tokens/controls.js +53 -0
  156. package/src/themes/default/tokens/shadows.js +63 -0
  157. package/src/themes/default/tokens/shapes.js +37 -0
  158. package/src/themes/default/tokens/space.js +143 -0
  159. package/src/themes/default/tokens/spectre.js +16 -0
  160. package/src/themes/default/utils.js +523 -0
  161. package/src/themes/index.ts +11 -0
  162. package/src/types.ts +394 -0
  163. package/src/utils/overlayTheme.ts +61 -0
  164. package/src/utils/pickColor.ts +15 -0
  165. 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
+ }