@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,331 @@
1
+ "use client";
2
+ import { useState, useMemo, useCallback } from "react";
3
+
4
+ export type SortDirection = "asc" | "desc" | null;
5
+
6
+ export interface UseTableControlsOptions<T> {
7
+ /** Initial data array */
8
+ data: T[];
9
+ /** Initial items per page */
10
+ initialItemsPerPage?: number;
11
+ /** Initial sort key */
12
+ initialSortKey?: string | null;
13
+ /** Initial sort direction */
14
+ initialSortDirection?: SortDirection;
15
+ /** Custom sort function */
16
+ customSort?: (data: T[], key: string, direction: "asc" | "desc") => T[];
17
+ }
18
+
19
+ export interface UseTableControlsReturn<T> {
20
+ // Sorted and paginated data
21
+ currentData: T[];
22
+ // Sorting
23
+ sortKey: string | null;
24
+ sortDirection: SortDirection;
25
+ handleSort: (key: string, direction: SortDirection) => void;
26
+ // Pagination
27
+ currentPage: number;
28
+ totalPages: number;
29
+ totalItems: number;
30
+ itemsPerPage: number;
31
+ handlePageChange: (page: number) => void;
32
+ handlePageSizeChange: (pageSize: number) => void;
33
+ // Utilities
34
+ resetPagination: () => void;
35
+ resetSorting: () => void;
36
+ resetAll: () => void;
37
+ }
38
+
39
+ /**
40
+ * Hook for managing table sorting and pagination
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * const tableControls = useTableControls({
45
+ * data: users,
46
+ * initialItemsPerPage: 10
47
+ * });
48
+ *
49
+ * return (
50
+ * <>
51
+ * <Table>
52
+ * <TableHeader>
53
+ * <TableRow>
54
+ * <SortableTableHeader
55
+ * sortKey="name"
56
+ * currentSortKey={tableControls.sortKey}
57
+ * currentSortDirection={tableControls.sortDirection}
58
+ * onSort={tableControls.handleSort}
59
+ * >
60
+ * Name
61
+ * </SortableTableHeader>
62
+ * </TableRow>
63
+ * </TableHeader>
64
+ * <TableBody>
65
+ * {tableControls.currentData.map(user => (
66
+ * <TableRow key={user.id}>
67
+ * <TableCell>{user.name}</TableCell>
68
+ * </TableRow>
69
+ * ))}
70
+ * </TableBody>
71
+ * </Table>
72
+ * <TablePagination
73
+ * currentPage={tableControls.currentPage}
74
+ * totalPages={tableControls.totalPages}
75
+ * totalItems={tableControls.totalItems}
76
+ * itemsPerPage={tableControls.itemsPerPage}
77
+ * onPageChange={tableControls.handlePageChange}
78
+ * onPageSizeChange={tableControls.handlePageSizeChange}
79
+ * />
80
+ * </>
81
+ * );
82
+ * ```
83
+ */
84
+ export function useTableControls<T>({
85
+ data,
86
+ initialItemsPerPage = 10,
87
+ initialSortKey = null,
88
+ initialSortDirection = null,
89
+ customSort,
90
+ }: UseTableControlsOptions<T>): UseTableControlsReturn<T> {
91
+ // Sorting state
92
+ const [sortKey, setSortKey] = useState<string | null>(initialSortKey);
93
+ const [sortDirection, setSortDirection] =
94
+ useState<SortDirection>(initialSortDirection);
95
+
96
+ // Pagination state
97
+ const [currentPage, setCurrentPage] = useState(0);
98
+ const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage);
99
+
100
+ // Default sort function
101
+ const defaultSort = useCallback(
102
+ (data: T[], key: string, direction: "asc" | "desc"): T[] => {
103
+ return [...data].sort((a, b) => {
104
+ const aValue = (a as any)[key];
105
+ const bValue = (b as any)[key];
106
+
107
+ // Handle null/undefined values
108
+ if (aValue === null || aValue === undefined) return 1;
109
+ if (bValue === null || bValue === undefined) return -1;
110
+
111
+ // Handle different types
112
+ if (typeof aValue === "string" && typeof bValue === "string") {
113
+ const comparison = aValue
114
+ .toLowerCase()
115
+ .localeCompare(bValue.toLowerCase());
116
+ return direction === "asc" ? comparison : -comparison;
117
+ }
118
+
119
+ if (aValue < bValue) return direction === "asc" ? -1 : 1;
120
+ if (aValue > bValue) return direction === "asc" ? 1 : -1;
121
+ return 0;
122
+ });
123
+ },
124
+ [],
125
+ );
126
+
127
+ // Sort data
128
+ const sortedData = useMemo(() => {
129
+ if (!sortKey || !sortDirection) return data;
130
+
131
+ const sortFunction = customSort || defaultSort;
132
+ return sortFunction(data, sortKey, sortDirection);
133
+ }, [data, sortKey, sortDirection, customSort, defaultSort]);
134
+
135
+ // Calculate pagination
136
+ const totalItems = sortedData.length;
137
+ const totalPages = Math.ceil(totalItems / itemsPerPage);
138
+
139
+ // Ensure current page is valid
140
+ const validCurrentPage = Math.min(currentPage, Math.max(0, totalPages - 1));
141
+
142
+ // Paginate data
143
+ const currentData = useMemo(() => {
144
+ const start = validCurrentPage * itemsPerPage;
145
+ return sortedData.slice(start, start + itemsPerPage);
146
+ }, [sortedData, validCurrentPage, itemsPerPage]);
147
+
148
+ // Handlers
149
+ const handleSort = useCallback((key: string, direction: SortDirection) => {
150
+ setSortKey(direction ? key : null);
151
+ setSortDirection(direction);
152
+ // Reset to first page when sorting changes
153
+ setCurrentPage(0);
154
+ }, []);
155
+
156
+ const handlePageChange = useCallback((page: number) => {
157
+ setCurrentPage(page);
158
+ }, []);
159
+
160
+ const handlePageSizeChange = useCallback((pageSize: number) => {
161
+ setItemsPerPage(pageSize);
162
+ // Reset to first page when page size changes
163
+ setCurrentPage(0);
164
+ }, []);
165
+
166
+ // Utility functions
167
+ const resetPagination = useCallback(() => {
168
+ setCurrentPage(0);
169
+ setItemsPerPage(initialItemsPerPage);
170
+ }, [initialItemsPerPage]);
171
+
172
+ const resetSorting = useCallback(() => {
173
+ setSortKey(initialSortKey);
174
+ setSortDirection(initialSortDirection);
175
+ }, [initialSortKey, initialSortDirection]);
176
+
177
+ const resetAll = useCallback(() => {
178
+ resetPagination();
179
+ resetSorting();
180
+ }, [resetPagination, resetSorting]);
181
+
182
+ return {
183
+ // Data
184
+ currentData,
185
+ // Sorting
186
+ sortKey,
187
+ sortDirection,
188
+ handleSort,
189
+ // Pagination
190
+ currentPage: validCurrentPage,
191
+ totalPages,
192
+ totalItems,
193
+ itemsPerPage,
194
+ handlePageChange,
195
+ handlePageSizeChange,
196
+ // Utilities
197
+ resetPagination,
198
+ resetSorting,
199
+ resetAll,
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Hook for filtering table data
205
+ *
206
+ * @example
207
+ * ```tsx
208
+ * const { filteredData, filters, updateFilter, resetFilters } = useTableFilters({
209
+ * data: users,
210
+ * filterFunctions: {
211
+ * search: (item, value) =>
212
+ * item.name.toLowerCase().includes(value.toLowerCase()) ||
213
+ * item.email.toLowerCase().includes(value.toLowerCase()),
214
+ * status: (item, value) => value === 'all' || item.status === value,
215
+ * department: (item, value) => value === 'all' || item.department === value
216
+ * },
217
+ * initialFilters: {
218
+ * search: '',
219
+ * status: 'all',
220
+ * department: 'all'
221
+ * }
222
+ * });
223
+ * ```
224
+ */
225
+ export interface UseTableFiltersOptions<T, F extends Record<string, any>> {
226
+ /** Data to filter */
227
+ data: T[];
228
+ /** Filter functions for each filter key */
229
+ filterFunctions: {
230
+ [K in keyof F]: (item: T, value: F[K]) => boolean;
231
+ };
232
+ /** Initial filter values */
233
+ initialFilters: F;
234
+ }
235
+
236
+ export interface UseTableFiltersReturn<T, F> {
237
+ /** Filtered data */
238
+ filteredData: T[];
239
+ /** Current filter values */
240
+ filters: F;
241
+ /** Update a single filter */
242
+ updateFilter: <K extends keyof F>(key: K, value: F[K]) => void;
243
+ /** Update multiple filters at once */
244
+ updateFilters: (filters: Partial<F>) => void;
245
+ /** Reset all filters to initial values */
246
+ resetFilters: () => void;
247
+ /** Check if any filters are active */
248
+ hasActiveFilters: boolean;
249
+ }
250
+
251
+ export function useTableFilters<T, F extends Record<string, any>>({
252
+ data,
253
+ filterFunctions,
254
+ initialFilters,
255
+ }: UseTableFiltersOptions<T, F>): UseTableFiltersReturn<T, F> {
256
+ const [filters, setFilters] = useState<F>(initialFilters);
257
+
258
+ // Apply filters
259
+ const filteredData = useMemo(() => {
260
+ return data.filter((item) => {
261
+ for (const [key, filterFn] of Object.entries(filterFunctions)) {
262
+ const filterValue = filters[key as keyof F];
263
+ if (!filterFn(item, filterValue)) {
264
+ return false;
265
+ }
266
+ }
267
+ return true;
268
+ });
269
+ }, [data, filters, filterFunctions]);
270
+
271
+ // Update single filter
272
+ const updateFilter = useCallback(<K extends keyof F>(key: K, value: F[K]) => {
273
+ setFilters((prev) => ({ ...prev, [key]: value }));
274
+ }, []);
275
+
276
+ // Update multiple filters
277
+ const updateFilters = useCallback((newFilters: Partial<F>) => {
278
+ setFilters((prev) => ({ ...prev, ...newFilters }));
279
+ }, []);
280
+
281
+ // Reset filters
282
+ const resetFilters = useCallback(() => {
283
+ setFilters(initialFilters);
284
+ }, [initialFilters]);
285
+
286
+ // Check if any filters are active
287
+ const hasActiveFilters = useMemo(() => {
288
+ return Object.entries(filters).some(([key, value]) => {
289
+ return value !== initialFilters[key as keyof F];
290
+ });
291
+ }, [filters, initialFilters]);
292
+
293
+ return {
294
+ filteredData,
295
+ filters,
296
+ updateFilter,
297
+ updateFilters,
298
+ resetFilters,
299
+ hasActiveFilters,
300
+ };
301
+ }
302
+
303
+ /**
304
+ * Combine filtering with table controls
305
+ *
306
+ * @example
307
+ * ```tsx
308
+ * const filters = useTableFilters({ ... });
309
+ * const controls = useTableControls({
310
+ * data: filters.filteredData,
311
+ * ...
312
+ * });
313
+ * ```
314
+ */
315
+ export function useFilteredTableControls<T, F extends Record<string, any>>(
316
+ filterOptions: UseTableFiltersOptions<T, F>,
317
+ controlOptions: Omit<UseTableControlsOptions<T>, "data">,
318
+ ) {
319
+ const filters = useTableFilters(filterOptions);
320
+ const controls = useTableControls({
321
+ ...controlOptions,
322
+ data: filters.filteredData,
323
+ });
324
+
325
+ return {
326
+ ...filters,
327
+ ...controls,
328
+ // Override data with the current page data
329
+ data: controls.currentData,
330
+ };
331
+ }
@@ -0,0 +1,27 @@
1
+ import { Text } from "@/design-system/elements";
2
+
3
+ type TagProps = {
4
+ [key: string]: any;
5
+ };
6
+
7
+ export const Tag = ({ children, ...props }: TagProps) => {
8
+ return (
9
+ <Text
10
+ as="div"
11
+ display="flex"
12
+ alignItems="center"
13
+ gap="xxsmall"
14
+ p="xs"
15
+ py="xxxs"
16
+ color="secondary"
17
+ fontSize="small"
18
+ fontWeight="600"
19
+ shape="rounded"
20
+ width="fit-content"
21
+ flex="none"
22
+ {...props}
23
+ >
24
+ {children}
25
+ </Text>
26
+ );
27
+ };
@@ -0,0 +1,96 @@
1
+ "use client"
2
+ import { Box, Text } from "@/design-system/elements"
3
+ import { IconDotsAnim } from "@/design-system/icons"
4
+
5
+ type TextBreakProps = {
6
+ message?: string
7
+ colorSchema?: string
8
+ loading?: boolean
9
+ showBreaks?: boolean
10
+ position?: "left" | "center" | "right"
11
+ children?: React.ReactNode
12
+ contentProps?: {
13
+ [key: string]: any
14
+ }
15
+ breakProps?: {
16
+ [key: string]: any
17
+ }
18
+
19
+ [key: string]: any
20
+ }
21
+
22
+ export default function TextBreak({
23
+ message,
24
+ loading,
25
+ showBreaks = true,
26
+ colorSchema = "default",
27
+ position = "center",
28
+ children,
29
+ contentProps = {},
30
+ breakProps = {},
31
+ ...props
32
+ }: TextBreakProps) {
33
+ const noticePalette: any = {
34
+ default: ["tertiary", "secondary"],
35
+ attention: ["currentColor", "red"],
36
+ accent: ["currentColor", "accent"],
37
+ }
38
+
39
+ const content = message || children
40
+ const leftBreakFlex =
41
+ position === "right" ? "auto" : position === "center" ? "auto" : "0"
42
+ const rightBreakFlex =
43
+ position === "left" ? "auto" : position === "center" ? "auto" : "0"
44
+
45
+ return (
46
+ <Text
47
+ as="div"
48
+ color={noticePalette[colorSchema][1]}
49
+ display="flex"
50
+ alignItems={"center"}
51
+ width="100%"
52
+ fontSize="xsmall"
53
+ gap="xsmall"
54
+ {...props}
55
+ >
56
+ {showBreaks && position !== "left" && (
57
+ <Box
58
+ flex={leftBreakFlex}
59
+ width={"auto"}
60
+ height={"1px"}
61
+ bg={noticePalette[colorSchema][0]}
62
+ opacity="0.25"
63
+ {...breakProps}
64
+ />
65
+ )}
66
+ {content && (
67
+ <Text
68
+ as="div"
69
+ display="flex"
70
+ alignItems={"center"}
71
+ flex="none"
72
+ gap="xsmall"
73
+ px={position === "left" ? 0 : "small"}
74
+ py="xxxsmall"
75
+ shape="pill"
76
+ {...contentProps}
77
+ >
78
+ {loading && (
79
+ <IconDotsAnim color="currentColor" width="20" height="20" />
80
+ )}
81
+ <Text flex="none">{content}</Text>
82
+ </Text>
83
+ )}
84
+ {showBreaks && position !== "right" && (
85
+ <Box
86
+ flex={rightBreakFlex}
87
+ width={"auto"}
88
+ height={"1px"}
89
+ bg={noticePalette[colorSchema][0]}
90
+ opacity="0.25"
91
+ {...breakProps}
92
+ />
93
+ )}
94
+ </Text>
95
+ )
96
+ }
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import { useMemo, useRef } from "react";
4
+ import { motion, type MotionValue, useScroll, useTransform } from "motion/react";
5
+ import styled from "styled-components";
6
+
7
+ import { Text, type TextProps } from "@/design-system/elements";
8
+ import { useReducedMotion } from "@/design-system/hooks/useReducedMotion";
9
+
10
+ type ScrollOffset = NonNullable<Parameters<typeof useScroll>[0]>["offset"];
11
+
12
+ export interface TextRevealProps extends Omit<TextProps, "children" | "as"> {
13
+ children: string;
14
+ baseOpacity?: number;
15
+ offset?: ScrollOffset;
16
+ revealEnd?: number;
17
+ }
18
+
19
+ const DEFAULT_OFFSET: ScrollOffset = ["start end", "end start"];
20
+
21
+ interface RevealedWordProps {
22
+ children: string;
23
+ progress: MotionValue<number>;
24
+ range: [number, number];
25
+ baseOpacity: number;
26
+ }
27
+
28
+ const Word = styled.span`
29
+ position: relative;
30
+ display: inline-grid;
31
+ margin-right: 0.25em;
32
+ margin-bottom: 0.1em;
33
+ `;
34
+
35
+ const WordBase = styled.span<{ $baseOpacity: number }>`
36
+ grid-area: 1 / 1;
37
+ opacity: ${(props) => props.$baseOpacity};
38
+ `;
39
+
40
+ const WordForeground = styled(motion.span)`
41
+ grid-area: 1 / 1;
42
+ color: currentColor;
43
+ `;
44
+
45
+ function RevealedWord({ children, progress, range, baseOpacity }: RevealedWordProps) {
46
+ const opacity = useTransform(progress, range, [0, 1]);
47
+
48
+ return (
49
+ <Word aria-hidden="true">
50
+ <WordBase $baseOpacity={baseOpacity}>{children}</WordBase>
51
+ <WordForeground style={{ opacity }}>{children}</WordForeground>
52
+ </Word>
53
+ );
54
+ }
55
+
56
+ export default function TextReveal({
57
+ children,
58
+ baseOpacity = 0.18,
59
+ offset = DEFAULT_OFFSET,
60
+ revealEnd = 0.88,
61
+ ...props
62
+ }: TextRevealProps) {
63
+ const textRef = useRef<HTMLDivElement | null>(null);
64
+ const prefersReducedMotion = useReducedMotion();
65
+ const words = useMemo(
66
+ () => children.trim().split(/\s+/).filter(Boolean),
67
+ [children],
68
+ );
69
+
70
+ const { scrollYProgress } = useScroll({
71
+ target: textRef,
72
+ offset,
73
+ });
74
+
75
+ const clampedRevealEnd = Math.max(0.1, Math.min(revealEnd, 1));
76
+
77
+ if (prefersReducedMotion) {
78
+ return (
79
+ <Text as="div" ref={textRef} {...props}>
80
+ {children}
81
+ </Text>
82
+ );
83
+ }
84
+
85
+ return (
86
+ <Text as="div" ref={textRef} aria-label={children} {...props}>
87
+ {words.map((word, index) => {
88
+ const start = (index / words.length) * clampedRevealEnd;
89
+ const end = ((index + 1) / words.length) * clampedRevealEnd;
90
+
91
+ return (
92
+ <RevealedWord
93
+ key={`${word}-${index}`}
94
+ progress={scrollYProgress}
95
+ range={[start, end]}
96
+ baseOpacity={baseOpacity}
97
+ >
98
+ {word}
99
+ </RevealedWord>
100
+ );
101
+ })}
102
+ </Text>
103
+ );
104
+ }
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import React from "react";
3
+ import Box, { BoxProps } from "@/design-system/elements/box";
4
+ import Image from "next/image";
5
+
6
+ interface ThumbnailProps extends Omit<BoxProps, "as"> {
7
+ src: string;
8
+ alt: string;
9
+ }
10
+
11
+ const Thumbnail: React.FC<ThumbnailProps> = ({ src, alt, ...rest }) => {
12
+ return (
13
+ <Box position="relative" overflow="hidden" {...rest}>
14
+ <Box
15
+ as={Image}
16
+ src={src}
17
+ alt={alt}
18
+ fill
19
+ sizes="100rem"
20
+ style={{ objectFit: "cover" }}
21
+ />
22
+ </Box>
23
+ );
24
+ };
25
+
26
+ export default Thumbnail;
@@ -0,0 +1,112 @@
1
+ "use client";
2
+ import React from "react";
3
+ import { animate, motion, useMotionValue } from "motion/react";
4
+ import { Box } from "@/design-system/elements";
5
+ import useMeasure from "react-use-measure";
6
+
7
+ interface TickerProps {
8
+ children: React.ReactNode;
9
+ fastDuration?: number;
10
+ slowDuration?: number;
11
+ direction?: "left" | "right";
12
+ gap?: string | number;
13
+ className?: string;
14
+ }
15
+
16
+ export default function Ticker({
17
+ children,
18
+ fastDuration = 25,
19
+ slowDuration = 75,
20
+ direction = "left",
21
+ gap = "medium",
22
+ }: TickerProps) {
23
+ const [duration, setDuration] = React.useState(fastDuration);
24
+ const [ref, { width }] = useMeasure();
25
+ const xTranslation = useMotionValue(0);
26
+ const [mustFinish, setMustFinish] = React.useState(false);
27
+ const [rerender, setRerender] = React.useState(false);
28
+
29
+ // Convert direction string to multiplier
30
+ const directionMultiplier = direction === "left" ? -1 : 1;
31
+
32
+ // Convert children to array
33
+ const childrenArray = React.Children.toArray(children);
34
+
35
+ React.useEffect(() => {
36
+ if (!width) return;
37
+
38
+ let controls;
39
+ const finalPosition = (width / 2) * directionMultiplier;
40
+
41
+ if (mustFinish) {
42
+ const currentPosition = xTranslation.get();
43
+ const remainingDistance = finalPosition - currentPosition;
44
+ const progressRatio = Math.abs(remainingDistance / finalPosition);
45
+
46
+ controls = animate(xTranslation, finalPosition, {
47
+ ease: "linear",
48
+ duration: duration * progressRatio,
49
+ onComplete: () => {
50
+ setMustFinish(false);
51
+ setRerender(!rerender);
52
+ xTranslation.set(0);
53
+ },
54
+ });
55
+ } else {
56
+ controls = animate(xTranslation, finalPosition, {
57
+ ease: "linear",
58
+ duration: duration,
59
+ repeat: Infinity,
60
+ repeatType: "loop",
61
+ onRepeat: () => {
62
+ xTranslation.set(0);
63
+ },
64
+ });
65
+ }
66
+
67
+ return () => controls.stop();
68
+
69
+ // eslint-disable-next-line react-hooks/exhaustive-deps
70
+ }, [rerender, duration, width, directionMultiplier, mustFinish]);
71
+
72
+ const renderItems = React.useCallback(() => {
73
+ // Create two sets of items to ensure smooth looping
74
+ const itemSet = (
75
+ <Box display="flex" gap={gap}>
76
+ {childrenArray.map((item, index) => (
77
+ <Box key={`set-${index}`}>{item}</Box>
78
+ ))}
79
+ </Box>
80
+ );
81
+
82
+ return (
83
+ <>
84
+ {itemSet}
85
+ {itemSet}
86
+ </>
87
+ );
88
+ }, [childrenArray, gap]);
89
+
90
+ return (
91
+ <Box width="100%" height="100%" overflow="hidden">
92
+ <Box
93
+ as={motion.div}
94
+ ref={ref}
95
+ style={{ x: xTranslation }}
96
+ display="flex"
97
+ gap={gap}
98
+ width="fit-content"
99
+ onHoverStart={() => {
100
+ setMustFinish(true);
101
+ setDuration(slowDuration);
102
+ }}
103
+ onHoverEnd={() => {
104
+ setMustFinish(true);
105
+ setDuration(fastDuration);
106
+ }}
107
+ >
108
+ {renderItems()}
109
+ </Box>
110
+ </Box>
111
+ );
112
+ }