@studiocubics/components 0.0.1

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 (140) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +71 -0
  3. package/eslint.config.js +21 -0
  4. package/package.json +66 -0
  5. package/rollup.config.js +34 -0
  6. package/src/Cards/Card/Card.module.css +27 -0
  7. package/src/Cards/Card/Card.tsx +105 -0
  8. package/src/Cards/CollectionItemCard/CollectionItemCard.module.css +84 -0
  9. package/src/Cards/CollectionItemCard/CollectionItemCard.tsx +170 -0
  10. package/src/Cards/CollectionItemCard/CollectionItemCardActions.tsx +85 -0
  11. package/src/Cards/CollectionItemCard/_index.ts +2 -0
  12. package/src/Cards/GlassCard/GlassCard.module.css +71 -0
  13. package/src/Cards/GlassCard/GlassCard.tsx +80 -0
  14. package/src/Cards/_index.ts +3 -0
  15. package/src/Display/Accordion/Accordion.module.css +69 -0
  16. package/src/Display/Accordion/Accordion.tsx +61 -0
  17. package/src/Display/Accordion/AccordionItem.tsx +135 -0
  18. package/src/Display/Accordion/_index.ts +2 -0
  19. package/src/Display/Chip/Chip.module.css +64 -0
  20. package/src/Display/Chip/Chip.tsx +105 -0
  21. package/src/Display/IdentityDisplay/IdentityDisplay.module.css +95 -0
  22. package/src/Display/IdentityDisplay/IdentityDisplay.tsx +119 -0
  23. package/src/Display/InputErrors/InputErrors.module.css +6 -0
  24. package/src/Display/InputErrors/InputErrors.tsx +52 -0
  25. package/src/Display/Kbd/Kbd.module.css +29 -0
  26. package/src/Display/Kbd/Kbd.tsx +39 -0
  27. package/src/Display/Kbd/_index.ts +2 -0
  28. package/src/Display/Kbd/buttonList.tsx +246 -0
  29. package/src/Display/LabeledValue/LabeledValue.module.css +32 -0
  30. package/src/Display/LabeledValue/LabeledValue.tsx +20 -0
  31. package/src/Display/List/List.module.css +143 -0
  32. package/src/Display/List/List.tsx +298 -0
  33. package/src/Display/PasswordStrength/PasswordStrength.module.css +45 -0
  34. package/src/Display/PasswordStrength/PasswordStrength.tsx +41 -0
  35. package/src/Display/PasswordStrength/usePasswordStrength.tsx +77 -0
  36. package/src/Display/Skeleton/Skeleton.module.css +54 -0
  37. package/src/Display/Skeleton/Skeleton.tsx +28 -0
  38. package/src/Display/Toast/Toaster.tsx +58 -0
  39. package/src/Display/Toast/_index.ts +2 -0
  40. package/src/Display/Toast/toast.ts +44 -0
  41. package/src/Display/Tooltip/Tooltip.module.css +128 -0
  42. package/src/Display/Tooltip/Tooltip.tsx +93 -0
  43. package/src/Display/Tooltip/getArrowDirection.ts +55 -0
  44. package/src/Display/Tooltip/useTooltip.tsx +63 -0
  45. package/src/Display/_index.ts +12 -0
  46. package/src/Forms/ConfirmationForm/ConfirmationForm.module.css +23 -0
  47. package/src/Forms/ConfirmationForm/ConfirmationForm.tsx +60 -0
  48. package/src/Forms/_index.ts +1 -0
  49. package/src/Inputs/Button/Button.module.css +131 -0
  50. package/src/Inputs/Button/Button.tsx +178 -0
  51. package/src/Inputs/Checkbox/Checkbox.module.css +77 -0
  52. package/src/Inputs/Checkbox/Checkbox.tsx +191 -0
  53. package/src/Inputs/Checkbox/CheckboxGroup/CheckboxGroup.module.css +10 -0
  54. package/src/Inputs/Checkbox/CheckboxGroup/CheckboxGroup.tsx +83 -0
  55. package/src/Inputs/Checkbox/CheckboxSelectAll.tsx +34 -0
  56. package/src/Inputs/Checkbox/_index.ts +3 -0
  57. package/src/Inputs/PasswordInput/PasswordInput.module.css +111 -0
  58. package/src/Inputs/PasswordInput/PasswordInput.tsx +229 -0
  59. package/src/Inputs/Select/Select.module.css +138 -0
  60. package/src/Inputs/Select/Select.tsx +136 -0
  61. package/src/Inputs/Switch/Switch.module.css +119 -0
  62. package/src/Inputs/Switch/Switch.tsx +195 -0
  63. package/src/Inputs/TextAreaInput/TextAreaInput.module.css +65 -0
  64. package/src/Inputs/TextAreaInput/TextAreaInput.tsx +97 -0
  65. package/src/Inputs/TextInput/TextInput.module.css +112 -0
  66. package/src/Inputs/TextInput/TextInput.tsx +142 -0
  67. package/src/Inputs/ThemeToggle/ThemeToggleListItem.tsx +80 -0
  68. package/src/Inputs/ThemeToggle/_index.ts +1 -0
  69. package/src/Inputs/_index.ts +8 -0
  70. package/src/Layout/Dialog/Dialog.module.css +15 -0
  71. package/src/Layout/Dialog/Dialog.tsx +115 -0
  72. package/src/Layout/PageLayout/PageLayout.module.css +20 -0
  73. package/src/Layout/PageLayout/PageLayout.tsx +79 -0
  74. package/src/Layout/PageLayoutPagination/PageLayoutPagination.module.css +5 -0
  75. package/src/Layout/PageLayoutPagination/PageLayoutPagination.tsx +40 -0
  76. package/src/Layout/PageLayoutTabs/PageLayoutTabs.module.css +3 -0
  77. package/src/Layout/PageLayoutTabs/PageLayoutTabs.tsx +62 -0
  78. package/src/Layout/Popover/Popover.module.css +9 -0
  79. package/src/Layout/Popover/Popover.tsx +145 -0
  80. package/src/Layout/SectionWrapper/SectionWrapper.module.css +31 -0
  81. package/src/Layout/SectionWrapper/SectionWrapper.tsx +62 -0
  82. package/src/Layout/Sidebar/Sidebar.module.css +17 -0
  83. package/src/Layout/Sidebar/Sidebar.tsx +39 -0
  84. package/src/Layout/Sidebar/SidebarBody/SidebarBody.module.css +31 -0
  85. package/src/Layout/Sidebar/SidebarBody/SidebarBody.tsx +18 -0
  86. package/src/Layout/Sidebar/SidebarDrawer/SidebarDrawer.module.css +20 -0
  87. package/src/Layout/Sidebar/SidebarDrawer/SidebarDrawer.tsx +19 -0
  88. package/src/Layout/Sidebar/SidebarFooter/SidebarFooter.module.css +35 -0
  89. package/src/Layout/Sidebar/SidebarFooter/SidebarFooter.tsx +19 -0
  90. package/src/Layout/Sidebar/SidebarHeader/SidebarHeader.tsx +14 -0
  91. package/src/Layout/Sidebar/SidebarViewport/SidebarViewport.module.css +12 -0
  92. package/src/Layout/Sidebar/SidebarViewport/SidebarViewport.tsx +11 -0
  93. package/src/Layout/Sidebar/_index.ts +6 -0
  94. package/src/Layout/Table/Table.module.css +46 -0
  95. package/src/Layout/Table/Table.tsx +222 -0
  96. package/src/Layout/Table/TableFooter.tsx +4 -0
  97. package/src/Layout/Table/TableHeader.tsx +4 -0
  98. package/src/Layout/Table/_index.ts +5 -0
  99. package/src/Layout/Table/tableUtils.ts +142 -0
  100. package/src/Layout/Table/types.ts +48 -0
  101. package/src/Layout/_index.ts +8 -0
  102. package/src/Misc/Cursor/Cursor.module.css +31 -0
  103. package/src/Misc/Cursor/Cursor.tsx +77 -0
  104. package/src/Misc/Logos.tsx +230 -0
  105. package/src/Misc/PoweredByBanner/PoweredByBanner.module.css +20 -0
  106. package/src/Misc/PoweredByBanner/PoweredByBanner.tsx +17 -0
  107. package/src/Misc/Ripple/Ripple.module.css +25 -0
  108. package/src/Misc/Ripple/Ripple.tsx +126 -0
  109. package/src/Misc/Spinner/Spinner.module.css +38 -0
  110. package/src/Misc/Spinner/Spinner.tsx +36 -0
  111. package/src/Misc/TransitionAnimation/TransitionAnimation.module.css +131 -0
  112. package/src/Misc/TransitionAnimation/TransitionAnimation.tsx +166 -0
  113. package/src/Misc/_index.ts +6 -0
  114. package/src/Navigation/Breadcrumbs/Breadcrumbs.module.css +22 -0
  115. package/src/Navigation/Breadcrumbs/Breadcrumbs.tsx +127 -0
  116. package/src/Navigation/Breadcrumbs/BreadcrumbsItem.tsx +31 -0
  117. package/src/Navigation/Breadcrumbs/_index.ts +3 -0
  118. package/src/Navigation/Breadcrumbs/useBreadcrumbs.tsx +74 -0
  119. package/src/Navigation/Pagination/Pagination.module.css +41 -0
  120. package/src/Navigation/Pagination/Pagination.tsx +187 -0
  121. package/src/Navigation/Pagination/PaginationItem.tsx +28 -0
  122. package/src/Navigation/Pagination/_index.ts +3 -0
  123. package/src/Navigation/Pagination/usePagination.tsx +65 -0
  124. package/src/Navigation/Tabs/Tab/Tab.module.css +43 -0
  125. package/src/Navigation/Tabs/Tab/Tab.tsx +155 -0
  126. package/src/Navigation/Tabs/Tabs.tsx +37 -0
  127. package/src/Navigation/Tabs/TabsBar/TabsBar.module.css +47 -0
  128. package/src/Navigation/Tabs/TabsBar/TabsBar.tsx +92 -0
  129. package/src/Navigation/Tabs/_index.ts +3 -0
  130. package/src/Navigation/_index.ts +3 -0
  131. package/src/Typography/ClampedText/ClampedText.module.css +5 -0
  132. package/src/Typography/ClampedText/ClampedText.tsx +77 -0
  133. package/src/Typography/CopyableText/CopyableText.module.css +21 -0
  134. package/src/Typography/CopyableText/CopyableText.tsx +120 -0
  135. package/src/Typography/PageTitle/PageTitle.module.css +47 -0
  136. package/src/Typography/PageTitle/PageTitle.tsx +35 -0
  137. package/src/Typography/_index.ts +3 -0
  138. package/src/declaration.d.ts +4 -0
  139. package/src/index.ts +8 -0
  140. package/tsconfig.json +32 -0
@@ -0,0 +1,298 @@
1
+ "use client";
2
+
3
+ import {
4
+ type ComponentProps,
5
+ createContext,
6
+ type ElementType,
7
+ type ReactNode,
8
+ useContext,
9
+ useEffect,
10
+ useRef,
11
+ useState,
12
+ } from "react";
13
+ import styles from "./List.module.css";
14
+ import { cn, mergeRefs } from "@studiocubics/utils";
15
+ import {
16
+ eventWithRipple,
17
+ useRipple,
18
+ type UseRippleProps,
19
+ } from "../../Misc/_index";
20
+ import { useDisclosure } from "@studiocubics/hooks";
21
+ import { GlassCard, type GlassCardProps } from "../../Cards/_index";
22
+ import type { SetState } from "@studiocubics/types";
23
+
24
+ interface ListContextProps {
25
+ activeListItem: HTMLLIElement | null;
26
+ setActiveListItem: SetState<HTMLLIElement | null>;
27
+ }
28
+ export type ListProps = {
29
+ ordered?: boolean;
30
+ className?: string;
31
+ secondary?: boolean;
32
+ renderMarker?: boolean;
33
+
34
+ slotProps?: {
35
+ marker?: GlassCardProps;
36
+ };
37
+ } & (
38
+ | {
39
+ listData?: ListItemProps[];
40
+ children?: undefined;
41
+ listItemProps?: ListItemProps;
42
+ }
43
+ | {
44
+ children?: ReactNode;
45
+ listData?: undefined;
46
+ listItemProps?: undefined;
47
+ }
48
+ );
49
+
50
+ const ListContext = createContext<ListContextProps | null>(null);
51
+
52
+ export function useList() {
53
+ const c = useContext(ListContext);
54
+ if (!c) throw new Error("Components must be wrapped in <List/>");
55
+ return c;
56
+ }
57
+
58
+ export function List(props: ListProps) {
59
+ const {
60
+ children,
61
+ ordered = false,
62
+ secondary = false,
63
+ listData = [],
64
+ slotProps = {},
65
+ className,
66
+ renderMarker = true,
67
+ listItemProps = {},
68
+ } = props;
69
+ const [activeListItem, setActiveListItem] =
70
+ useState<ListContextProps["activeListItem"]>(null);
71
+ const markerRef = useRef<HTMLDivElement>(null);
72
+ const rootRef = useRef<HTMLOListElement>(null);
73
+
74
+ const rootClass = cn(
75
+ styles.root,
76
+ className,
77
+ secondary ? styles.secondary : "",
78
+ );
79
+ const body = listData.length
80
+ ? listData.map((ld, i) => <ListItem key={i} {...listItemProps} {...ld} />)
81
+ : children;
82
+
83
+ const marker = renderMarker ? (
84
+ <GlassCard
85
+ {...slotProps.marker}
86
+ ref={markerRef}
87
+ className={cn(styles.marker, slotProps.marker?.className)}
88
+ />
89
+ ) : null;
90
+ const Component = ordered ? "ol" : "ul";
91
+
92
+ useEffect(() => {
93
+ if (!rootRef.current || !markerRef.current || !activeListItem) return;
94
+
95
+ const updateMarkerPosition = () => {
96
+ if (!rootRef.current || !markerRef.current || !activeListItem) return;
97
+
98
+ const marker = markerRef.current;
99
+ const tabRect = activeListItem.getBoundingClientRect();
100
+ const rootRect = rootRef.current.getBoundingClientRect();
101
+
102
+ // Account for scroll offset
103
+ const scrollLeft = rootRef.current.scrollLeft;
104
+ const scrollTop = rootRef.current.scrollTop;
105
+
106
+ marker.style.display = "block";
107
+ marker.style.width = `${tabRect.width}px`;
108
+ marker.style.height = `${tabRect.height}px`;
109
+ marker.style.left = `${tabRect.left - rootRect.left + scrollLeft}px`;
110
+ marker.style.top = `${tabRect.top - rootRect.top + scrollTop}px`;
111
+ };
112
+
113
+ updateMarkerPosition();
114
+
115
+ const container = rootRef.current;
116
+
117
+ // Update marker position when container resizes
118
+ const resizeObserver = new ResizeObserver(updateMarkerPosition);
119
+ resizeObserver.observe(container);
120
+
121
+ return () => {
122
+ resizeObserver.disconnect();
123
+ };
124
+ }, [activeListItem]);
125
+ return (
126
+ <ListContext.Provider value={{ activeListItem, setActiveListItem }}>
127
+ <Component ref={rootRef} className={rootClass}>
128
+ {body}
129
+ {marker}
130
+ </Component>
131
+ </ListContext.Provider>
132
+ );
133
+ }
134
+
135
+ export interface ListItemProps extends ComponentProps<"li"> {
136
+ startIcon?: ReactNode;
137
+ endIcon?: ReactNode;
138
+ dropDownIcon?: ReactNode;
139
+ selected?: boolean;
140
+ shortened?: boolean;
141
+ shortenedIcon?: "start" | "end";
142
+ href?: ComponentProps<"a">["href"];
143
+ disabled?: boolean;
144
+ color?: "primary" | "secondary" | "error";
145
+ slotProps?: {
146
+ ripple?: UseRippleProps;
147
+ content?: ComponentProps<"span">;
148
+ startIcon?: ComponentProps<"span">;
149
+ endIcon?: ComponentProps<"span">;
150
+ dropDownIcon?: ComponentProps<"span">;
151
+ linkComponent?: ComponentProps<"a">;
152
+ };
153
+ childNodes?: Array<ListItemProps>;
154
+ LinkComponent?: ElementType<ComponentProps<any>>;
155
+ }
156
+ function ChevronDown() {
157
+ return (
158
+ <svg
159
+ xmlns="http://www.w3.org/2000/svg"
160
+ width="16"
161
+ height="16"
162
+ viewBox="0 0 24 24"
163
+ fill="none"
164
+ stroke="currentColor"
165
+ strokeWidth="2"
166
+ strokeLinecap="round"
167
+ strokeLinejoin="round"
168
+ className="lucide lucide-chevron-down-icon lucide-chevron-down"
169
+ >
170
+ <path d="m6 9 6 6 6-6" />
171
+ </svg>
172
+ );
173
+ }
174
+ // Create the base component with forwardRef
175
+ export function ListItem(props: ListItemProps) {
176
+ const {
177
+ className,
178
+ children,
179
+ onTouchStart,
180
+ onClick,
181
+ shortened = false,
182
+ shortenedIcon = "start",
183
+ selected = false,
184
+ dropDownIcon = <ChevronDown />,
185
+ color,
186
+ startIcon,
187
+ endIcon,
188
+ disabled,
189
+ childNodes = [],
190
+ href,
191
+ LinkComponent = "a",
192
+ slotProps: _slotProps,
193
+ ref,
194
+ ...restProps
195
+ } = props;
196
+ const slotProps: NonNullable<ListItemProps["slotProps"]> = _slotProps ?? {};
197
+ const { setActiveListItem } = useList();
198
+ const listItemRef = useRef<HTMLLIElement>(null);
199
+
200
+ const { rippleElements, createRipple } = useRipple(slotProps.ripple);
201
+ const { open, handleToggle } = useDisclosure();
202
+
203
+ const clickable =
204
+ !disabled && (!!href || !!onClick || (!shortened && !!childNodes.length));
205
+
206
+ const componentProps = {
207
+ className: cn(
208
+ className,
209
+ styles.listItem,
210
+ shortened ? styles.shortened : "",
211
+ selected ? styles.selected : "",
212
+ clickable ? styles.clickable : "",
213
+ disabled ? styles.disabled : "",
214
+ ),
215
+ onTouchStart: disabled
216
+ ? undefined
217
+ : eventWithRipple(createRipple, onTouchStart, handleToggle),
218
+ onClick: disabled
219
+ ? undefined
220
+ : eventWithRipple(createRipple, onClick, handleToggle),
221
+ "data-color": color,
222
+ ref: mergeRefs(ref, listItemRef),
223
+ ...restProps,
224
+ };
225
+
226
+ const body = (
227
+ <>
228
+ {clickable && rippleElements}
229
+ {startIcon && (
230
+ <span
231
+ {...slotProps.startIcon}
232
+ className={cn(
233
+ styles.iconContainer,
234
+ shortenedIcon == "start" ? styles.primaryIcon : "",
235
+ slotProps.startIcon?.className,
236
+ )}
237
+ >
238
+ {startIcon}
239
+ </span>
240
+ )}
241
+ <span
242
+ {...slotProps.content}
243
+ className={cn(styles.content, slotProps.content?.className)}
244
+ >
245
+ {children}
246
+ </span>
247
+ {endIcon && (
248
+ <span
249
+ {...slotProps.endIcon}
250
+ className={cn(
251
+ styles.iconContainer,
252
+ shortenedIcon == "end" ? styles.primaryIcon : "",
253
+ slotProps.endIcon?.className,
254
+ )}
255
+ >
256
+ {endIcon}
257
+ </span>
258
+ )}
259
+ {!!childNodes.length && (
260
+ <span
261
+ {...slotProps.dropDownIcon}
262
+ className={cn(
263
+ styles.iconContainer,
264
+ styles.dropDownIcon,
265
+ slotProps.dropDownIcon?.className,
266
+ open ? styles.openSublist : "",
267
+ )}
268
+ >
269
+ {dropDownIcon}
270
+ </span>
271
+ )}
272
+ </>
273
+ );
274
+
275
+ useEffect(() => {
276
+ if (!listItemRef.current) return;
277
+ if (selected) setActiveListItem(listItemRef.current);
278
+ }, [selected]);
279
+
280
+ return (
281
+ <>
282
+ {href ? (
283
+ <LinkComponent href={href} disabled={disabled} {...componentProps}>
284
+ {body}
285
+ </LinkComponent>
286
+ ) : (
287
+ <li {...componentProps}>{body}</li>
288
+ )}
289
+ {!!childNodes.length && !shortened && open && (
290
+ <List secondary>
291
+ {childNodes.map((c, i) => (
292
+ <ListItem key={i} {...c} />
293
+ ))}
294
+ </List>
295
+ )}
296
+ </>
297
+ );
298
+ }
@@ -0,0 +1,45 @@
1
+ .root {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--spacing-gap);
5
+ flex-wrap: wrap;
6
+ padding-inline: var(--spacing-gap);
7
+ scale: 1 0;
8
+ transition: scale var(--transition-time) var(--transition-tf);
9
+ }
10
+ .title {
11
+ font-size: 0.5rem;
12
+ flex: 0 0 auto;
13
+ }
14
+ .strengthBars {
15
+ flex: 1 1 50%;
16
+ display: flex;
17
+ align-items: flex-start;
18
+ gap: var(--spacing-gap);
19
+ }
20
+ .bar {
21
+ transition: all var(--transition-time) var(--transition-tf);
22
+ background-color: currentColor;
23
+ scale: 0 0;
24
+ flex: 1 1 25%;
25
+ transform-origin: top left;
26
+ border-radius: 999px;
27
+ }
28
+ .weak {
29
+ height: 3px;
30
+ opacity: 0.3;
31
+ }
32
+ .medium {
33
+ height: 5px;
34
+ opacity: 0.5;
35
+ }
36
+ .good {
37
+ height: 7px;
38
+ opacity: 0.8;
39
+ }
40
+ .strong {
41
+ height: 9px;
42
+ }
43
+ .show {
44
+ scale: 1 1;
45
+ }
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import styles from "./PasswordStrength.module.css";
4
+ import { cn, remap } from "@studiocubics/utils";
5
+ import type { ComponentProps } from "react";
6
+
7
+ export const STRENGTH_MESSAGES = ["", "Weak", "Medium", "Good", "Strong"];
8
+
9
+ const Bar = ({ className, ...rest }: ComponentProps<"span">) => {
10
+ return <span className={cn(styles.bar, className)} {...rest} />;
11
+ };
12
+
13
+ export function PasswordStrength({ strength }: { strength?: number | null }) {
14
+ const remappedStrength = strength ? remap(strength, [1, 5], [1, 4]) : 0;
15
+ const strengthMsg = STRENGTH_MESSAGES[remappedStrength];
16
+ return (
17
+ <div className={cn(styles.root, !!strength ? styles.show : "")}>
18
+ <p className={cn(styles.title)}>{strengthMsg}</p>
19
+ <div className={cn(styles.strengthBars)}>
20
+ <Bar
21
+ className={cn(styles.weak, remappedStrength >= 1 ? styles.show : "")}
22
+ />
23
+ <Bar
24
+ className={cn(
25
+ styles.medium,
26
+ remappedStrength >= 2 ? styles.show : "",
27
+ )}
28
+ />
29
+ <Bar
30
+ className={cn(styles.good, remappedStrength >= 3 ? styles.show : "")}
31
+ />
32
+ <Bar
33
+ className={cn(
34
+ styles.strong,
35
+ remappedStrength == 4 ? styles.show : "",
36
+ )}
37
+ />
38
+ </div>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,77 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ export const PASSWORD_REGEX = {
4
+ ASCII_NO_SPACE: { ex: /^[!-~]+$/, score: 0.5 },
5
+ LOWERCASE: { ex: /[a-z]/, score: 0.5 },
6
+ UPPERCASE: { ex: /[A-Z]/, score: 1 },
7
+ DIGIT: { ex: /\d/, score: 1 },
8
+ SPECIAL: { ex: /[!@#$%^&*()_\-+=\[\]{};:'",.<>/?\\|`~]/, score: 1 },
9
+ NO_TRIPLE_REPEAT: { ex: /^(?!.*(.)\1\1).*$/, score: 1 },
10
+ } as const;
11
+
12
+ export type PasswordTest = keyof typeof PASSWORD_REGEX;
13
+
14
+ export const ALL_PASSWORD_TESTS: PasswordTest[] = [
15
+ "ASCII_NO_SPACE",
16
+ "DIGIT",
17
+ "LOWERCASE",
18
+ "NO_TRIPLE_REPEAT",
19
+ "SPECIAL",
20
+ "UPPERCASE",
21
+ ];
22
+ export interface PasswordStrengthProps {
23
+ password?: string;
24
+ requiredTests?: PasswordTest[];
25
+ disableStrengthMeter?: boolean;
26
+ /**
27
+ * Minimum length of the password when it should start checking,
28
+ * before reaching the minimum length the strength will always be 1
29
+ */
30
+ minLength?: number;
31
+ }
32
+
33
+ export function usePasswordStrength({
34
+ password,
35
+ requiredTests = [],
36
+ disableStrengthMeter = true,
37
+ minLength = 8,
38
+ }: PasswordStrengthProps) {
39
+ const [strength, setStrength] = useState<number | null>(null);
40
+ const [testsPassed, setPassed] = useState<PasswordTest[]>([]);
41
+
42
+ useEffect(() => {
43
+ if (!password) {
44
+ setStrength(null);
45
+ setPassed([]);
46
+ return;
47
+ }
48
+
49
+ const results: PasswordTest[] = [];
50
+
51
+ for (const [key, regex] of Object.entries(PASSWORD_REGEX)) {
52
+ if (regex.ex.test(password)) results.push(key as PasswordTest);
53
+ }
54
+
55
+ setPassed(results);
56
+
57
+ // REQUIRED TESTS GATE
58
+ const allRequiredPassed = results.every((test) =>
59
+ requiredTests.includes(test),
60
+ );
61
+
62
+ if (!allRequiredPassed || password.length < minLength) {
63
+ setStrength(1); // Weak
64
+ return;
65
+ }
66
+
67
+ // Otherwise score by total passed tests
68
+ const totalScore = Object.entries(PASSWORD_REGEX).reduce(
69
+ (acc, [key, value]) =>
70
+ results.includes(key as PasswordTest) ? acc + value.score : acc,
71
+ 0,
72
+ );
73
+ setStrength(totalScore);
74
+ }, [password, requiredTests]);
75
+
76
+ if (!disableStrengthMeter) return { strength, testsPassed };
77
+ }
@@ -0,0 +1,54 @@
1
+ .root {
2
+ --skeleton-color-main: var(--color-surface);
3
+ --skeleton-color-wave: color-mix(
4
+ in srgb,
5
+ var(--color-surface) 80%,
6
+ var(--color-primary) 10%
7
+ );
8
+
9
+ background-image: linear-gradient(
10
+ to right,
11
+ var(--skeleton-color-main) 0%,
12
+ var(--skeleton-color-wave) 50%,
13
+ var(--skeleton-color-main) 100%
14
+ );
15
+ background-size: 1000px 1000px;
16
+ background-repeat: repeat-x;
17
+ animation: throb 2s var(--transition-tf) infinite;
18
+ /* animation: wave 0.8s linear infinite forwards; */
19
+ border-radius: var(--shape-br-md);
20
+ }
21
+ .large {
22
+ font-size: 2rem;
23
+ }
24
+ .medium {
25
+ font-size: 1.2rem;
26
+ }
27
+ .small {
28
+ font-size: 0.75rem;
29
+ }
30
+
31
+ @keyframes wave {
32
+ 0% {
33
+ background-position: 0% 0%;
34
+ }
35
+
36
+ 100% {
37
+ background-position: -200% 0%;
38
+ }
39
+ }
40
+ @keyframes throb {
41
+ 0% {
42
+ background: var(--color-surface);
43
+ }
44
+ 50% {
45
+ background: color-mix(
46
+ in srgb,
47
+ var(--color-primary) 10%,
48
+ var(--color-surface) 100%
49
+ );
50
+ }
51
+ 100% {
52
+ background: var(--color-surface);
53
+ }
54
+ }
@@ -0,0 +1,28 @@
1
+ import type { ComponentProps, CSSProperties } from "react";
2
+ import styles from "./Skeleton.module.css";
3
+ import { cn } from "@studiocubics/utils";
4
+ export interface SkeletonProps extends ComponentProps<"span"> {
5
+ type?: "text" | "card";
6
+ size?: "small" | "medium" | "large";
7
+ width?: CSSProperties["width"];
8
+ height?: CSSProperties["height"];
9
+ }
10
+
11
+ export function Skeleton(props: SkeletonProps) {
12
+ const {
13
+ className,
14
+ style,
15
+ type = "text",
16
+ size = "medium",
17
+ width = "100%",
18
+ height = "1.5em",
19
+ ...rest
20
+ } = props;
21
+ return (
22
+ <span
23
+ className={cn(className, styles.root, styles[size], styles[type])}
24
+ style={{ width, height, ...style }}
25
+ {...rest}
26
+ ></span>
27
+ );
28
+ }
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { subscribe, dismiss, type Toast } from "./toast";
5
+
6
+ export function Toaster() {
7
+ const [toasts, setToasts] = useState<Toast[]>([]);
8
+
9
+ useEffect(() => {
10
+ return () => {
11
+ subscribe(setToasts);
12
+ };
13
+ }, []);
14
+
15
+ if (toasts.length === 0) return null;
16
+
17
+ return (
18
+ <div style={containerStyle}>
19
+ {toasts.map((t) => (
20
+ <div key={t.id} style={toastStyle}>
21
+ <span>{t.message}</span>
22
+ <button onClick={() => dismiss(t.id)} style={closeStyle}>
23
+ ×
24
+ </button>
25
+ </div>
26
+ ))}
27
+ </div>
28
+ );
29
+ }
30
+
31
+ const containerStyle: React.CSSProperties = {
32
+ position: "fixed",
33
+ top: 16,
34
+ right: 16,
35
+ display: "flex",
36
+ flexDirection: "column",
37
+ gap: 8,
38
+ zIndex: 1000,
39
+ };
40
+
41
+ const toastStyle: React.CSSProperties = {
42
+ background: "#111",
43
+ color: "#fff",
44
+ padding: "12px 16px",
45
+ borderRadius: 6,
46
+ minWidth: 200,
47
+ display: "flex",
48
+ justifyContent: "space-between",
49
+ alignItems: "center",
50
+ };
51
+
52
+ const closeStyle: React.CSSProperties = {
53
+ background: "transparent",
54
+ border: "none",
55
+ color: "#fff",
56
+ fontSize: 16,
57
+ cursor: "pointer",
58
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./Toaster";
2
+ export * from "./toast";
@@ -0,0 +1,44 @@
1
+ export type Toast = {
2
+ id: string;
3
+ message: string;
4
+ duration?: number;
5
+ };
6
+
7
+ type Listener = (toasts: Toast[]) => void;
8
+
9
+ let toasts: Toast[] = [];
10
+ const listeners = new Set<Listener>();
11
+
12
+ function emit() {
13
+ listeners.forEach((l) => l(toasts));
14
+ }
15
+
16
+ export function subscribe(listener: Listener) {
17
+ listeners.add(listener);
18
+ return () => listeners.delete(listener);
19
+ }
20
+
21
+ export function toast(message: string, options?: { duration?: number }) {
22
+ const id = crypto.randomUUID();
23
+
24
+ const t: Required<Toast> = {
25
+ id,
26
+ message,
27
+ duration: options?.duration ?? 3000,
28
+ };
29
+
30
+ toasts = [...toasts, t];
31
+ emit();
32
+
33
+ // Its a toast not a lecture!
34
+ if (t.duration <= 20000) {
35
+ setTimeout(() => {
36
+ dismiss(id);
37
+ }, t.duration);
38
+ }
39
+ }
40
+
41
+ export function dismiss(id: string) {
42
+ toasts = toasts.filter((t) => t.id !== id);
43
+ emit();
44
+ }