@olympusoss/canvas 2.20.2 → 4.0.0

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 (208) hide show
  1. package/package.json +41 -177
  2. package/src/cn.ts +3 -0
  3. package/src/index.ts +12 -603
  4. package/src/theme.ts +41 -0
  5. package/src/tokens.ts +11 -0
  6. package/styles/base.css +17 -0
  7. package/styles/canvas.css +69 -52
  8. package/styles/components/alert.css +66 -0
  9. package/styles/components/app-shell.css +46 -0
  10. package/styles/components/avatar.css +15 -0
  11. package/styles/components/badge.css +83 -0
  12. package/styles/components/breadcrumb.css +35 -0
  13. package/styles/components/button-group.css +23 -0
  14. package/styles/components/button.css +107 -0
  15. package/styles/components/calendar.css +73 -0
  16. package/styles/components/card.css +58 -0
  17. package/styles/components/checkbox.css +55 -0
  18. package/styles/components/code-block.css +18 -0
  19. package/styles/components/combobox.css +75 -0
  20. package/styles/components/command.css +94 -0
  21. package/styles/components/data-table.css +142 -0
  22. package/styles/components/dialog.css +72 -0
  23. package/styles/components/dropdown.css +54 -0
  24. package/styles/components/empty-state.css +17 -0
  25. package/styles/components/field.css +27 -0
  26. package/styles/components/filter-panel.css +58 -0
  27. package/styles/components/form.css +27 -0
  28. package/styles/components/icon.css +8 -0
  29. package/styles/components/input-group.css +45 -0
  30. package/styles/components/input.css +56 -0
  31. package/styles/components/kbd.css +15 -0
  32. package/styles/components/page-header.css +52 -0
  33. package/styles/components/pagination.css +48 -0
  34. package/styles/components/popover.css +14 -0
  35. package/styles/components/radio.css +28 -0
  36. package/styles/components/row-menu.css +69 -0
  37. package/styles/components/section-card.css +49 -0
  38. package/styles/components/select.css +57 -0
  39. package/styles/components/separator.css +32 -0
  40. package/styles/components/sheet.css +70 -0
  41. package/styles/components/sidebar.css +146 -0
  42. package/styles/components/skeleton.css +32 -0
  43. package/styles/components/spinner.css +26 -0
  44. package/styles/components/stat-card.css +71 -0
  45. package/styles/components/stepper.css +63 -0
  46. package/styles/components/switch.css +45 -0
  47. package/styles/components/tabs.css +40 -0
  48. package/styles/components/textarea.css +31 -0
  49. package/styles/components/toast.css +95 -0
  50. package/styles/components/tooltip.css +53 -0
  51. package/styles/components/topbar.css +24 -0
  52. package/styles/components/typography.css +105 -0
  53. package/styles/patterns/backdrops.css +35 -0
  54. package/styles/patterns/density.css +66 -0
  55. package/styles/patterns/focus.css +38 -0
  56. package/styles/patterns/glass.css +85 -0
  57. package/styles/patterns/high-contrast.css +70 -0
  58. package/styles/patterns/reduced-motion.css +12 -0
  59. package/styles/patterns/scrollbar.css +10 -0
  60. package/styles/reset.css +89 -0
  61. package/styles/tokens/colors.css +106 -0
  62. package/styles/tokens/motion.css +33 -0
  63. package/styles/tokens/radius.css +10 -0
  64. package/styles/tokens/shadows.css +35 -0
  65. package/styles/tokens/spacing.css +19 -0
  66. package/styles/tokens/typography.css +6 -0
  67. package/styles/tokens/z-index.css +12 -0
  68. package/tsconfig.json +20 -21
  69. package/README.md +0 -60
  70. package/src/components/atoms/README.md +0 -11
  71. package/src/components/atoms/aspect-ratio.tsx +0 -32
  72. package/src/components/atoms/avatar.tsx +0 -98
  73. package/src/components/atoms/badge.tsx +0 -44
  74. package/src/components/atoms/brand-mark.tsx +0 -74
  75. package/src/components/atoms/button.tsx +0 -105
  76. package/src/components/atoms/checkbox.tsx +0 -63
  77. package/src/components/atoms/flex-box.tsx +0 -105
  78. package/src/components/atoms/icon.tsx +0 -34
  79. package/src/components/atoms/input.tsx +0 -92
  80. package/src/components/atoms/label.tsx +0 -41
  81. package/src/components/atoms/logo.tsx +0 -89
  82. package/src/components/atoms/progress.tsx +0 -55
  83. package/src/components/atoms/radio-group.tsx +0 -122
  84. package/src/components/atoms/scroll-area.tsx +0 -106
  85. package/src/components/atoms/section.tsx +0 -48
  86. package/src/components/atoms/separator.tsx +0 -45
  87. package/src/components/atoms/skeleton.tsx +0 -17
  88. package/src/components/atoms/slider.tsx +0 -93
  89. package/src/components/atoms/spinner.tsx +0 -47
  90. package/src/components/atoms/switch.tsx +0 -60
  91. package/src/components/atoms/textarea.tsx +0 -78
  92. package/src/components/atoms/toggle.tsx +0 -80
  93. package/src/components/charts/activity-heatmap.tsx +0 -186
  94. package/src/components/charts/axes.tsx +0 -21
  95. package/src/components/charts/chart-container.tsx +0 -254
  96. package/src/components/charts/chart-legend.tsx +0 -67
  97. package/src/components/charts/chart-tooltip.tsx +0 -161
  98. package/src/components/charts/chart-types.tsx +0 -49
  99. package/src/components/charts/containers.tsx +0 -11
  100. package/src/components/charts/data.tsx +0 -16
  101. package/src/components/charts/details.tsx +0 -25
  102. package/src/components/charts/dot-pulse.tsx +0 -61
  103. package/src/components/charts/gauge.tsx +0 -106
  104. package/src/components/charts/grids.tsx +0 -8
  105. package/src/components/charts/index.ts +0 -62
  106. package/src/components/charts/labeled-bar-list.tsx +0 -85
  107. package/src/components/charts/metric-breakdown.tsx +0 -316
  108. package/src/components/charts/references.tsx +0 -8
  109. package/src/components/charts/service-health-list.tsx +0 -85
  110. package/src/components/charts/sparkline-area.tsx +0 -80
  111. package/src/components/charts/sparkline.tsx +0 -52
  112. package/src/components/charts/stacked-bar.tsx +0 -104
  113. package/src/components/charts/text.tsx +0 -10
  114. package/src/components/charts/world-heat-map-inner.tsx +0 -317
  115. package/src/components/charts/world-heat-map.tsx +0 -184
  116. package/src/components/molecules/README.md +0 -12
  117. package/src/components/molecules/action-bar.tsx +0 -73
  118. package/src/components/molecules/activity-item.tsx +0 -74
  119. package/src/components/molecules/alert.tsx +0 -86
  120. package/src/components/molecules/animated-background.tsx +0 -92
  121. package/src/components/molecules/auth-shell.tsx +0 -95
  122. package/src/components/molecules/brand-lockup.tsx +0 -48
  123. package/src/components/molecules/breadcrumb.tsx +0 -157
  124. package/src/components/molecules/button-group.tsx +0 -104
  125. package/src/components/molecules/calendar.tsx +0 -217
  126. package/src/components/molecules/card.tsx +0 -102
  127. package/src/components/molecules/client-brand.tsx +0 -95
  128. package/src/components/molecules/code-block.tsx +0 -86
  129. package/src/components/molecules/countdown-button.tsx +0 -92
  130. package/src/components/molecules/empty-state.tsx +0 -56
  131. package/src/components/molecules/error-state.tsx +0 -42
  132. package/src/components/molecules/field-display.tsx +0 -35
  133. package/src/components/molecules/input-otp.tsx +0 -74
  134. package/src/components/molecules/launcher-card.tsx +0 -152
  135. package/src/components/molecules/loading-state.tsx +0 -36
  136. package/src/components/molecules/notification-item.tsx +0 -67
  137. package/src/components/molecules/notification-list.tsx +0 -45
  138. package/src/components/molecules/number-badge.tsx +0 -53
  139. package/src/components/molecules/or-separator.tsx +0 -38
  140. package/src/components/molecules/page-header.tsx +0 -88
  141. package/src/components/molecules/page-tabs.tsx +0 -94
  142. package/src/components/molecules/pagination.tsx +0 -150
  143. package/src/components/molecules/password-input.tsx +0 -83
  144. package/src/components/molecules/password-strength-meter.tsx +0 -104
  145. package/src/components/molecules/phone-input.tsx +0 -200
  146. package/src/components/molecules/search-bar.tsx +0 -64
  147. package/src/components/molecules/secret-field.tsx +0 -158
  148. package/src/components/molecules/section-card.tsx +0 -91
  149. package/src/components/molecules/social-buttons.tsx +0 -165
  150. package/src/components/molecules/stat-card.tsx +0 -100
  151. package/src/components/molecules/status-badge.tsx +0 -42
  152. package/src/components/molecules/stepper.tsx +0 -96
  153. package/src/components/molecules/table.tsx +0 -157
  154. package/src/components/molecules/terminal.tsx +0 -74
  155. package/src/components/molecules/toggle-group.tsx +0 -145
  156. package/src/components/molecules/tooltip.tsx +0 -155
  157. package/src/components/molecules/user-avatar-chip.tsx +0 -71
  158. package/src/components/organisms/README.md +0 -14
  159. package/src/components/organisms/accordion.tsx +0 -154
  160. package/src/components/organisms/alert-dialog.tsx +0 -277
  161. package/src/components/organisms/carousel.tsx +0 -244
  162. package/src/components/organisms/collapsible.tsx +0 -69
  163. package/src/components/organisms/command.tsx +0 -144
  164. package/src/components/organisms/context-menu.tsx +0 -339
  165. package/src/components/organisms/dashboard-grid.tsx +0 -369
  166. package/src/components/organisms/data-table.tsx +0 -330
  167. package/src/components/organisms/dialog.tsx +0 -312
  168. package/src/components/organisms/drawer.tsx +0 -123
  169. package/src/components/organisms/dropdown-menu.tsx +0 -440
  170. package/src/components/organisms/editors/code-editor.tsx +0 -144
  171. package/src/components/organisms/editors/index.ts +0 -4
  172. package/src/components/organisms/editors/markdown-editor.tsx +0 -153
  173. package/src/components/organisms/editors/markdown-renderer.ts +0 -27
  174. package/src/components/organisms/editors/prose-canvas-classes.ts +0 -45
  175. package/src/components/organisms/editors/rich-text-editor.tsx +0 -126
  176. package/src/components/organisms/editors/toolbar/md-toolbar.tsx +0 -129
  177. package/src/components/organisms/editors/toolbar/rte-toolbar.tsx +0 -211
  178. package/src/components/organisms/editors/toolbar/toolbar-shell.tsx +0 -45
  179. package/src/components/organisms/editors/use-codemirror-theme.ts +0 -61
  180. package/src/components/organisms/error-boundary.tsx +0 -61
  181. package/src/components/organisms/form.tsx +0 -174
  182. package/src/components/organisms/hover-card.tsx +0 -115
  183. package/src/components/organisms/menubar.tsx +0 -498
  184. package/src/components/organisms/navbar.tsx +0 -104
  185. package/src/components/organisms/navigation-menu.tsx +0 -235
  186. package/src/components/organisms/popover.tsx +0 -149
  187. package/src/components/organisms/resizable.tsx +0 -58
  188. package/src/components/organisms/schema-form.tsx +0 -232
  189. package/src/components/organisms/select.tsx +0 -309
  190. package/src/components/organisms/sheet.tsx +0 -265
  191. package/src/components/organisms/sidebar.tsx +0 -1040
  192. package/src/components/organisms/sonner.tsx +0 -96
  193. package/src/components/organisms/tabs.tsx +0 -133
  194. package/src/components/organisms/theme-provider.tsx +0 -101
  195. package/src/hooks/use-mobile.tsx +0 -19
  196. package/src/lib/portal-container.tsx +0 -35
  197. package/src/lib/utils.ts +0 -6
  198. package/src/native.ts +0 -23
  199. package/src/tokens/colors.ts +0 -91
  200. package/src/tokens/index.ts +0 -3
  201. package/src/tokens/spacing.ts +0 -55
  202. package/src/tokens/typography.ts +0 -27
  203. package/styles/dashboard-grid.css +0 -47
  204. package/styles/fonts/Roboto-VariableFont_wdth_wght.ttf +0 -0
  205. package/styles/glass.css +0 -175
  206. package/styles/leaflet.css +0 -13
  207. package/styles/tokens.css +0 -317
  208. package/tailwind.config.ts +0 -70
@@ -1,150 +0,0 @@
1
- import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
2
- import * as React from "react";
3
-
4
- import { cn } from "../../lib/utils";
5
- import { type ButtonProps, buttonVariants } from "../atoms/button";
6
-
7
- export interface PaginationProps extends React.ComponentProps<"nav"> {
8
- /** A `<PaginationContent>` with `<PaginationItem>`s. */
9
- children?: React.ReactNode;
10
- /** Tailwind / CSS classes merged onto the `<nav>` via `cn()`. */
11
- className?: string;
12
- }
13
-
14
- const Pagination = ({ className, ...props }: PaginationProps) => (
15
- <nav
16
- role="navigation"
17
- aria-label="pagination"
18
- className={cn("mx-auto flex w-full justify-center", className)}
19
- {...props}
20
- />
21
- );
22
- Pagination.displayName = "Pagination";
23
-
24
- export interface PaginationContentProps extends React.ComponentProps<"ul"> {
25
- /** A flat list of `<PaginationItem>`s. */
26
- children?: React.ReactNode;
27
- /** Tailwind / CSS classes merged onto the `<ul>` via `cn()`. */
28
- className?: string;
29
- }
30
-
31
- const PaginationContent = React.forwardRef<HTMLUListElement, PaginationContentProps>(
32
- ({ className, ...props }, ref) => (
33
- <ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
34
- ),
35
- );
36
- PaginationContent.displayName = "PaginationContent";
37
-
38
- export interface PaginationItemProps extends React.ComponentProps<"li"> {
39
- /** Typically a `<PaginationLink>`, `<PaginationPrevious>`, `<PaginationNext>`, or `<PaginationEllipsis>`. */
40
- children?: React.ReactNode;
41
- /** Tailwind / CSS classes merged onto the `<li>` via `cn()`. */
42
- className?: string;
43
- }
44
-
45
- const PaginationItem = React.forwardRef<HTMLLIElement, PaginationItemProps>(
46
- ({ className, ...props }, ref) => <li ref={ref} className={cn("", className)} {...props} />,
47
- );
48
- PaginationItem.displayName = "PaginationItem";
49
-
50
- export interface PaginationLinkProps extends React.ComponentProps<"a"> {
51
- /**
52
- * Mark this link as the current page. Adds `aria-current="page"` and
53
- * applies the outline button variant.
54
- * @default false
55
- */
56
- isActive?: boolean;
57
- /**
58
- * Button-style size preset (inherited from `<Button>`).
59
- * @default "icon"
60
- */
61
- size?: ButtonProps["size"];
62
- /** Page number or label. */
63
- children?: React.ReactNode;
64
- /** Anchor target. */
65
- href?: string;
66
- /** Tailwind / CSS classes merged onto the link via `cn()`. */
67
- className?: string;
68
- }
69
-
70
- const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
71
- <a
72
- aria-current={isActive ? "page" : undefined}
73
- className={cn(
74
- buttonVariants({
75
- variant: isActive ? "outline" : "ghost",
76
- size,
77
- }),
78
- className,
79
- )}
80
- {...props}
81
- />
82
- );
83
- PaginationLink.displayName = "PaginationLink";
84
-
85
- export interface PaginationPreviousProps extends Omit<PaginationLinkProps, "size"> {
86
- /** Anchor target for the previous page. */
87
- href?: string;
88
- /** Tailwind / CSS classes merged via `cn()`. */
89
- className?: string;
90
- }
91
-
92
- const PaginationPrevious = ({ className, ...props }: PaginationPreviousProps) => (
93
- <PaginationLink
94
- aria-label="Go to previous page"
95
- size="default"
96
- className={cn("gap-1 pl-2.5", className)}
97
- {...props}
98
- >
99
- <ChevronLeft className="h-4 w-4" />
100
- <span>Previous</span>
101
- </PaginationLink>
102
- );
103
- PaginationPrevious.displayName = "PaginationPrevious";
104
-
105
- export interface PaginationNextProps extends Omit<PaginationLinkProps, "size"> {
106
- /** Anchor target for the next page. */
107
- href?: string;
108
- /** Tailwind / CSS classes merged via `cn()`. */
109
- className?: string;
110
- }
111
-
112
- const PaginationNext = ({ className, ...props }: PaginationNextProps) => (
113
- <PaginationLink
114
- aria-label="Go to next page"
115
- size="default"
116
- className={cn("gap-1 pr-2.5", className)}
117
- {...props}
118
- >
119
- <span>Next</span>
120
- <ChevronRight className="h-4 w-4" />
121
- </PaginationLink>
122
- );
123
- PaginationNext.displayName = "PaginationNext";
124
-
125
- export interface PaginationEllipsisProps extends React.ComponentProps<"span"> {
126
- /** Tailwind / CSS classes merged via `cn()`. */
127
- className?: string;
128
- }
129
-
130
- const PaginationEllipsis = ({ className, ...props }: PaginationEllipsisProps) => (
131
- <span
132
- aria-hidden
133
- className={cn("flex h-9 w-9 items-center justify-center", className)}
134
- {...props}
135
- >
136
- <MoreHorizontal className="h-4 w-4" />
137
- <span className="sr-only">More pages</span>
138
- </span>
139
- );
140
- PaginationEllipsis.displayName = "PaginationEllipsis";
141
-
142
- export {
143
- Pagination,
144
- PaginationContent,
145
- PaginationEllipsis,
146
- PaginationItem,
147
- PaginationLink,
148
- PaginationNext,
149
- PaginationPrevious,
150
- };
@@ -1,83 +0,0 @@
1
- "use client";
2
-
3
- import { Eye, EyeOff } from "lucide-react";
4
- import * as React from "react";
5
-
6
- import { cn } from "../../lib/utils";
7
- import { Button } from "../atoms/button";
8
- import { Input } from "../atoms/input";
9
-
10
- export interface PasswordInputProps
11
- extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
12
- /**
13
- * Initial visibility state. Pair with `onVisibilityChange` for a
14
- * controlled input.
15
- * @default false
16
- */
17
- defaultVisible?: boolean;
18
- /** Controlled visibility. Overrides `defaultVisible`. */
19
- visible?: boolean;
20
- /** Fired when the user clicks the eye toggle. */
21
- onVisibilityChange?: (visible: boolean) => void;
22
- /**
23
- * Accessible label on the eye toggle button.
24
- * @default "Toggle password visibility"
25
- */
26
- toggleLabel?: string;
27
- }
28
-
29
- /**
30
- * Password input with a show/hide eye toggle. Renders as an `Input` (atom)
31
- * with a `Button` (atom) absolutely positioned at the trailing edge.
32
- *
33
- * Use anywhere a password field is needed: login, registration, settings,
34
- * reset flows.
35
- */
36
- const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
37
- (
38
- {
39
- defaultVisible = false,
40
- visible: controlledVisible,
41
- onVisibilityChange,
42
- toggleLabel = "Toggle password visibility",
43
- className,
44
- ...props
45
- },
46
- ref,
47
- ) => {
48
- const [internalVisible, setInternalVisible] = React.useState(defaultVisible);
49
- const visible = controlledVisible ?? internalVisible;
50
-
51
- const toggle = () => {
52
- const next = !visible;
53
- if (controlledVisible === undefined) setInternalVisible(next);
54
- onVisibilityChange?.(next);
55
- };
56
-
57
- return (
58
- <div className="relative">
59
- <Input
60
- ref={ref}
61
- type={visible ? "text" : "password"}
62
- className={cn("pr-10", className)}
63
- {...props}
64
- />
65
- <Button
66
- type="button"
67
- variant="ghost"
68
- size="icon"
69
- onClick={toggle}
70
- aria-label={toggleLabel}
71
- aria-pressed={visible}
72
- className="absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2 text-muted-foreground hover:text-foreground"
73
- tabIndex={-1}
74
- >
75
- {visible ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
76
- </Button>
77
- </div>
78
- );
79
- },
80
- );
81
- PasswordInput.displayName = "PasswordInput";
82
-
83
- export { PasswordInput };
@@ -1,104 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
-
5
- import { cn } from "../../lib/utils";
6
-
7
- export type PasswordStrengthLevel = 0 | 1 | 2 | 3 | 4;
8
-
9
- export interface PasswordStrength {
10
- /** 0 = empty, 1 = weak, 2 = fair, 3 = good, 4 = strong. */
11
- level: PasswordStrengthLevel;
12
- /** Short human label shown next to the meter. */
13
- label: string;
14
- }
15
-
16
- /**
17
- * Score a password 0-4 based on length + character variety. Returns the
18
- * level plus a short label. Heuristic-only; for a true strength score use a
19
- * dictionary-aware estimator like `@zxcvbn-ts/core` and map its score here.
20
- */
21
- export function scorePassword(pw: string): PasswordStrength {
22
- if (!pw) return { level: 0, label: "" };
23
- let score = 0;
24
- if (pw.length >= 8) score++;
25
- if (pw.length >= 12) score++;
26
- if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) score++;
27
- if (/\d/.test(pw)) score++;
28
- if (/[^A-Za-z0-9]/.test(pw)) score++;
29
- if (pw.length < 6) score = Math.min(score, 1);
30
-
31
- // score is clamped into [1, 4]: `Math.max(1, …)` provides the floor,
32
- // `Math.min(4, …)` provides the ceiling. Higher-tier scorers can map
33
- // dictionary results into the same shape.
34
- const level = Math.min(4, Math.max(1, score)) as PasswordStrengthLevel;
35
- const labels = ["", "Weak", "Fair", "Good", "Strong"];
36
- return { level, label: labels[level] };
37
- }
38
-
39
- export interface PasswordStrengthMeterProps {
40
- /** Password value to score. Empty string renders nothing. */
41
- value: string;
42
- /**
43
- * Custom scorer. Override the default heuristic with a zxcvbn-based
44
- * scorer for production use.
45
- */
46
- score?: (pw: string) => PasswordStrength;
47
- /** Tailwind / CSS classes merged onto the root via `cn()`. */
48
- className?: string;
49
- /**
50
- * Hide the strength label text and show only the bar.
51
- * @default false
52
- */
53
- hideLabel?: boolean;
54
- }
55
-
56
- /**
57
- * Four-segment strength bar for a password input. Renders nothing for an
58
- * empty value. Use beneath a password input in registration and reset
59
- * flows.
60
- */
61
- const PasswordStrengthMeter = React.forwardRef<HTMLDivElement, PasswordStrengthMeterProps>(
62
- ({ value, score = scorePassword, className, hideLabel = false }, ref) => {
63
- const { level, label } = score(value);
64
- if (level === 0) return null;
65
- const segmentColors = [
66
- "bg-muted",
67
- "bg-destructive",
68
- "bg-amber-500",
69
- "bg-emerald-500",
70
- "bg-emerald-600",
71
- ];
72
- const labelColor =
73
- level === 1
74
- ? "text-destructive"
75
- : level === 2
76
- ? "text-amber-600 dark:text-amber-500"
77
- : "text-emerald-600 dark:text-emerald-500";
78
-
79
- return (
80
- <div ref={ref} className={cn("flex items-center gap-2", className)}>
81
- <div className="flex flex-1 gap-1" aria-hidden>
82
- {[1, 2, 3, 4].map((seg) => (
83
- <div
84
- key={seg}
85
- className={cn(
86
- "h-1 flex-1 rounded-full transition-colors",
87
- seg <= level ? segmentColors[level] : "bg-muted",
88
- )}
89
- />
90
- ))}
91
- </div>
92
- {!hideLabel && (
93
- <span className={cn("text-xs font-medium tabular-nums", labelColor)}>{label}</span>
94
- )}
95
- <span className="sr-only" role="status">
96
- Password strength: {label}
97
- </span>
98
- </div>
99
- );
100
- },
101
- );
102
- PasswordStrengthMeter.displayName = "PasswordStrengthMeter";
103
-
104
- export { PasswordStrengthMeter };
@@ -1,200 +0,0 @@
1
- "use client";
2
-
3
- import parsePhoneNumber, {
4
- type CountryCode,
5
- getCountries,
6
- getCountryCallingCode,
7
- isValidPhoneNumber,
8
- } from "libphonenumber-js";
9
- import * as React from "react";
10
-
11
- import { cn } from "../../lib/utils";
12
- import { Input } from "../atoms/input";
13
- import { Label } from "../atoms/label";
14
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../organisms/select";
15
-
16
- export interface PhoneInputProps {
17
- id: string;
18
- value?: string;
19
- onChange: (e164Value: string | undefined) => void;
20
- label?: string;
21
- placeholder?: string;
22
- disabled?: boolean;
23
- readonly?: boolean;
24
- required?: boolean;
25
- /** Default country ISO code if the value is empty. Default: "US". */
26
- defaultCountry?: CountryCode;
27
- /** Override the list of selectable countries (defaults to all). */
28
- countryCodes?: CountryCode[];
29
- /** Called with error string (or "" when valid/cleared). */
30
- onValidityChange?: (error: string) => void;
31
- className?: string;
32
- }
33
-
34
- interface CountryOption {
35
- code: CountryCode;
36
- name: string;
37
- callingCode: string;
38
- }
39
-
40
- /** Convert an ISO 3166-1 alpha-2 country code to its flag emoji. */
41
- function flagEmoji(code: string): string {
42
- const A = "A".charCodeAt(0);
43
- const offset = 0x1f1e6 - A;
44
- const upper = code.toUpperCase();
45
- /* c8 ignore next -- defensive: every CountryCode from libphonenumber is a 2-letter ISO code by construction; only reachable if a malformed string slips past TS at the call site */
46
- if (upper.length !== 2) return code;
47
- return String.fromCodePoint(upper.charCodeAt(0) + offset, upper.charCodeAt(1) + offset);
48
- }
49
-
50
- function buildCountryOptions(codes?: CountryCode[]): CountryOption[] {
51
- const list = codes ?? getCountries();
52
- const displayNames = new Intl.DisplayNames(["en"], { type: "region" });
53
- return list
54
- .map((code) => {
55
- const callingCode = getCountryCallingCode(code);
56
- /* c8 ignore next -- defensive: Intl.DisplayNames returns a name for every valid ISO country code in jsdom; the `|| code` fallback only fires if Intl data is stripped */
57
- const name = displayNames.of(code) || code;
58
- return { code, name, callingCode };
59
- })
60
- .sort((a, b) => a.name.localeCompare(b.name));
61
- }
62
-
63
- export function PhoneInput({
64
- id,
65
- value,
66
- onChange,
67
- label,
68
- placeholder,
69
- disabled,
70
- readonly,
71
- required,
72
- defaultCountry = "US",
73
- countryCodes,
74
- onValidityChange,
75
- className,
76
- }: PhoneInputProps) {
77
- const [selectedCountry, setSelectedCountry] = React.useState<CountryCode>(defaultCountry);
78
- const [localValue, setLocalValue] = React.useState("");
79
- const [error, setError] = React.useState("");
80
-
81
- const countryOptions = React.useMemo(() => buildCountryOptions(countryCodes), [countryCodes]);
82
-
83
- // Hydrate state from an external E.164 value.
84
- React.useEffect(() => {
85
- if (!value) {
86
- setLocalValue("");
87
- return;
88
- }
89
- try {
90
- const parsed = parsePhoneNumber(value);
91
- if (parsed) {
92
- if (parsed.country) setSelectedCountry(parsed.country);
93
- /* c8 ignore next -- defensive: a successfully-parsed PhoneNumber always exposes a nationalNumber; the `?? ""` fallback only fires if libphonenumber returns a partial object */
94
- setLocalValue(parsed.nationalNumber ?? "");
95
- } else {
96
- setLocalValue(value);
97
- }
98
- } catch {
99
- /* c8 ignore next -- libphonenumber never throws on arbitrary input in jsdom; only reachable if parser invariants change */
100
- setLocalValue(value);
101
- }
102
- }, [value]);
103
-
104
- const updateValidity = React.useCallback(
105
- (e164: string) => {
106
- if (!e164) {
107
- const err = required ? "Phone number is required" : "";
108
- setError(err);
109
- onValidityChange?.(err);
110
- return;
111
- }
112
- const valid = isValidPhoneNumber(e164);
113
- const err = valid ? "" : "Enter a valid phone number";
114
- setError(err);
115
- onValidityChange?.(err);
116
- },
117
- [required, onValidityChange],
118
- );
119
-
120
- const emit = (country: CountryCode, national: string) => {
121
- if (!national.trim()) {
122
- onChange(undefined);
123
- updateValidity("");
124
- return;
125
- }
126
- const callingCode = getCountryCallingCode(country);
127
- const e164 = `+${callingCode}${national.replace(/\D/g, "")}`;
128
- onChange(e164);
129
- updateValidity(e164);
130
- };
131
-
132
- return (
133
- <div className={cn("space-y-1.5", className)}>
134
- {label && (
135
- <Label htmlFor={id}>
136
- {label}
137
- {required && <span className="ml-0.5 text-destructive">*</span>}
138
- </Label>
139
- )}
140
- <div className="flex gap-2">
141
- <Select
142
- value={selectedCountry}
143
- onValueChange={(next) => {
144
- const code = next as CountryCode;
145
- setSelectedCountry(code);
146
- emit(code, localValue);
147
- }}
148
- disabled={disabled || readonly}
149
- >
150
- <SelectTrigger
151
- aria-label="Country"
152
- className="w-auto shrink-0 gap-1.5 px-2.5 font-mono text-sm"
153
- >
154
- <SelectValue>
155
- <span className="flex items-center gap-1.5">
156
- <span aria-hidden className="text-base leading-none">
157
- {flagEmoji(selectedCountry)}
158
- </span>
159
- <span>+{getCountryCallingCode(selectedCountry)}</span>
160
- </span>
161
- </SelectValue>
162
- </SelectTrigger>
163
- <SelectContent className="max-h-[320px] min-w-[260px]">
164
- {countryOptions.map((opt) => (
165
- <SelectItem key={opt.code} value={opt.code}>
166
- <span className="flex items-center gap-2">
167
- <span aria-hidden className="text-base leading-none">
168
- {flagEmoji(opt.code)}
169
- </span>
170
- <span>{opt.name}</span>
171
- <span className="font-mono text-xs text-muted-foreground">
172
- +{opt.callingCode}
173
- </span>
174
- </span>
175
- </SelectItem>
176
- ))}
177
- </SelectContent>
178
- </Select>
179
- <Input
180
- id={id}
181
- type="tel"
182
- inputMode="tel"
183
- value={localValue}
184
- onChange={(e) => {
185
- const v = e.target.value;
186
- setLocalValue(v);
187
- emit(selectedCountry, v);
188
- }}
189
- placeholder={placeholder}
190
- disabled={disabled}
191
- readOnly={readonly}
192
- className={cn("flex-1", error && "border-destructive focus-visible:ring-destructive")}
193
- />
194
- </div>
195
- {error && <p className="text-xs text-destructive">{error}</p>}
196
- </div>
197
- );
198
- }
199
-
200
- PhoneInput.displayName = "PhoneInput";
@@ -1,64 +0,0 @@
1
- "use client";
2
-
3
- import { Search, X } from "lucide-react";
4
- import * as React from "react";
5
-
6
- import { cn } from "../../lib/utils";
7
- import { Button } from "../atoms/button";
8
- import { Input } from "../atoms/input";
9
-
10
- export interface SearchBarProps
11
- extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> {
12
- value: string;
13
- onChange: (value: string) => void;
14
- onClear?: () => void;
15
- /**
16
- * Optional keyboard-shortcut hint rendered as a `<kbd>` on the right of
17
- * the input when empty. Hidden once the user types so the clear button
18
- * has the slot. Example: `shortcut="⌘K"`.
19
- */
20
- shortcut?: string;
21
- }
22
-
23
- const SearchBar = React.forwardRef<HTMLInputElement, SearchBarProps>(
24
- ({ value, onChange, onClear, shortcut, className, placeholder = "Search...", ...props }, ref) => {
25
- const showShortcut = !!shortcut && value.length === 0;
26
- return (
27
- <div className={cn("relative", className)}>
28
- <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
29
- <Input
30
- ref={ref}
31
- value={value}
32
- onChange={(e) => onChange(e.target.value)}
33
- placeholder={placeholder}
34
- className={cn("pl-9", showShortcut ? "pr-12" : "pr-9")}
35
- {...props}
36
- />
37
- {value && (
38
- <Button
39
- variant="ghost"
40
- size="icon"
41
- className="absolute right-1 top-1/2 h-6 w-6 -translate-y-1/2"
42
- onClick={() => {
43
- onChange("");
44
- onClear?.();
45
- }}
46
- >
47
- <X className="h-3 w-3" />
48
- </Button>
49
- )}
50
- {showShortcut && (
51
- <kbd
52
- aria-hidden
53
- className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rounded border border-border bg-muted/40 px-1.5 font-mono text-[10px] text-muted-foreground"
54
- >
55
- {shortcut}
56
- </kbd>
57
- )}
58
- </div>
59
- );
60
- },
61
- );
62
- SearchBar.displayName = "SearchBar";
63
-
64
- export { SearchBar };