@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.
- package/package.json +41 -177
- package/src/cn.ts +3 -0
- package/src/index.ts +12 -603
- package/src/theme.ts +41 -0
- package/src/tokens.ts +11 -0
- package/styles/base.css +17 -0
- package/styles/canvas.css +69 -52
- package/styles/components/alert.css +66 -0
- package/styles/components/app-shell.css +46 -0
- package/styles/components/avatar.css +15 -0
- package/styles/components/badge.css +83 -0
- package/styles/components/breadcrumb.css +35 -0
- package/styles/components/button-group.css +23 -0
- package/styles/components/button.css +107 -0
- package/styles/components/calendar.css +73 -0
- package/styles/components/card.css +58 -0
- package/styles/components/checkbox.css +55 -0
- package/styles/components/code-block.css +18 -0
- package/styles/components/combobox.css +75 -0
- package/styles/components/command.css +94 -0
- package/styles/components/data-table.css +142 -0
- package/styles/components/dialog.css +72 -0
- package/styles/components/dropdown.css +54 -0
- package/styles/components/empty-state.css +17 -0
- package/styles/components/field.css +27 -0
- package/styles/components/filter-panel.css +58 -0
- package/styles/components/form.css +27 -0
- package/styles/components/icon.css +8 -0
- package/styles/components/input-group.css +45 -0
- package/styles/components/input.css +56 -0
- package/styles/components/kbd.css +15 -0
- package/styles/components/page-header.css +52 -0
- package/styles/components/pagination.css +48 -0
- package/styles/components/popover.css +14 -0
- package/styles/components/radio.css +28 -0
- package/styles/components/row-menu.css +69 -0
- package/styles/components/section-card.css +49 -0
- package/styles/components/select.css +57 -0
- package/styles/components/separator.css +32 -0
- package/styles/components/sheet.css +70 -0
- package/styles/components/sidebar.css +146 -0
- package/styles/components/skeleton.css +32 -0
- package/styles/components/spinner.css +26 -0
- package/styles/components/stat-card.css +71 -0
- package/styles/components/stepper.css +63 -0
- package/styles/components/switch.css +45 -0
- package/styles/components/tabs.css +40 -0
- package/styles/components/textarea.css +31 -0
- package/styles/components/toast.css +95 -0
- package/styles/components/tooltip.css +53 -0
- package/styles/components/topbar.css +24 -0
- package/styles/components/typography.css +105 -0
- package/styles/patterns/backdrops.css +35 -0
- package/styles/patterns/density.css +66 -0
- package/styles/patterns/focus.css +38 -0
- package/styles/patterns/glass.css +85 -0
- package/styles/patterns/high-contrast.css +70 -0
- package/styles/patterns/reduced-motion.css +12 -0
- package/styles/patterns/scrollbar.css +10 -0
- package/styles/reset.css +89 -0
- package/styles/tokens/colors.css +106 -0
- package/styles/tokens/motion.css +33 -0
- package/styles/tokens/radius.css +10 -0
- package/styles/tokens/shadows.css +35 -0
- package/styles/tokens/spacing.css +19 -0
- package/styles/tokens/typography.css +6 -0
- package/styles/tokens/z-index.css +12 -0
- package/tsconfig.json +20 -21
- package/README.md +0 -60
- package/src/components/atoms/README.md +0 -11
- package/src/components/atoms/aspect-ratio.tsx +0 -32
- package/src/components/atoms/avatar.tsx +0 -98
- package/src/components/atoms/badge.tsx +0 -44
- package/src/components/atoms/brand-mark.tsx +0 -74
- package/src/components/atoms/button.tsx +0 -105
- package/src/components/atoms/checkbox.tsx +0 -63
- package/src/components/atoms/flex-box.tsx +0 -105
- package/src/components/atoms/icon.tsx +0 -34
- package/src/components/atoms/input.tsx +0 -92
- package/src/components/atoms/label.tsx +0 -41
- package/src/components/atoms/logo.tsx +0 -89
- package/src/components/atoms/progress.tsx +0 -55
- package/src/components/atoms/radio-group.tsx +0 -122
- package/src/components/atoms/scroll-area.tsx +0 -106
- package/src/components/atoms/section.tsx +0 -48
- package/src/components/atoms/separator.tsx +0 -45
- package/src/components/atoms/skeleton.tsx +0 -17
- package/src/components/atoms/slider.tsx +0 -93
- package/src/components/atoms/spinner.tsx +0 -47
- package/src/components/atoms/switch.tsx +0 -60
- package/src/components/atoms/textarea.tsx +0 -78
- package/src/components/atoms/toggle.tsx +0 -80
- package/src/components/charts/activity-heatmap.tsx +0 -186
- package/src/components/charts/axes.tsx +0 -21
- package/src/components/charts/chart-container.tsx +0 -254
- package/src/components/charts/chart-legend.tsx +0 -67
- package/src/components/charts/chart-tooltip.tsx +0 -161
- package/src/components/charts/chart-types.tsx +0 -49
- package/src/components/charts/containers.tsx +0 -11
- package/src/components/charts/data.tsx +0 -16
- package/src/components/charts/details.tsx +0 -25
- package/src/components/charts/dot-pulse.tsx +0 -61
- package/src/components/charts/gauge.tsx +0 -106
- package/src/components/charts/grids.tsx +0 -8
- package/src/components/charts/index.ts +0 -62
- package/src/components/charts/labeled-bar-list.tsx +0 -85
- package/src/components/charts/metric-breakdown.tsx +0 -316
- package/src/components/charts/references.tsx +0 -8
- package/src/components/charts/service-health-list.tsx +0 -85
- package/src/components/charts/sparkline-area.tsx +0 -80
- package/src/components/charts/sparkline.tsx +0 -52
- package/src/components/charts/stacked-bar.tsx +0 -104
- package/src/components/charts/text.tsx +0 -10
- package/src/components/charts/world-heat-map-inner.tsx +0 -317
- package/src/components/charts/world-heat-map.tsx +0 -184
- package/src/components/molecules/README.md +0 -12
- package/src/components/molecules/action-bar.tsx +0 -73
- package/src/components/molecules/activity-item.tsx +0 -74
- package/src/components/molecules/alert.tsx +0 -86
- package/src/components/molecules/animated-background.tsx +0 -92
- package/src/components/molecules/auth-shell.tsx +0 -95
- package/src/components/molecules/brand-lockup.tsx +0 -48
- package/src/components/molecules/breadcrumb.tsx +0 -157
- package/src/components/molecules/button-group.tsx +0 -104
- package/src/components/molecules/calendar.tsx +0 -217
- package/src/components/molecules/card.tsx +0 -102
- package/src/components/molecules/client-brand.tsx +0 -95
- package/src/components/molecules/code-block.tsx +0 -86
- package/src/components/molecules/countdown-button.tsx +0 -92
- package/src/components/molecules/empty-state.tsx +0 -56
- package/src/components/molecules/error-state.tsx +0 -42
- package/src/components/molecules/field-display.tsx +0 -35
- package/src/components/molecules/input-otp.tsx +0 -74
- package/src/components/molecules/launcher-card.tsx +0 -152
- package/src/components/molecules/loading-state.tsx +0 -36
- package/src/components/molecules/notification-item.tsx +0 -67
- package/src/components/molecules/notification-list.tsx +0 -45
- package/src/components/molecules/number-badge.tsx +0 -53
- package/src/components/molecules/or-separator.tsx +0 -38
- package/src/components/molecules/page-header.tsx +0 -88
- package/src/components/molecules/page-tabs.tsx +0 -94
- package/src/components/molecules/pagination.tsx +0 -150
- package/src/components/molecules/password-input.tsx +0 -83
- package/src/components/molecules/password-strength-meter.tsx +0 -104
- package/src/components/molecules/phone-input.tsx +0 -200
- package/src/components/molecules/search-bar.tsx +0 -64
- package/src/components/molecules/secret-field.tsx +0 -158
- package/src/components/molecules/section-card.tsx +0 -91
- package/src/components/molecules/social-buttons.tsx +0 -165
- package/src/components/molecules/stat-card.tsx +0 -100
- package/src/components/molecules/status-badge.tsx +0 -42
- package/src/components/molecules/stepper.tsx +0 -96
- package/src/components/molecules/table.tsx +0 -157
- package/src/components/molecules/terminal.tsx +0 -74
- package/src/components/molecules/toggle-group.tsx +0 -145
- package/src/components/molecules/tooltip.tsx +0 -155
- package/src/components/molecules/user-avatar-chip.tsx +0 -71
- package/src/components/organisms/README.md +0 -14
- package/src/components/organisms/accordion.tsx +0 -154
- package/src/components/organisms/alert-dialog.tsx +0 -277
- package/src/components/organisms/carousel.tsx +0 -244
- package/src/components/organisms/collapsible.tsx +0 -69
- package/src/components/organisms/command.tsx +0 -144
- package/src/components/organisms/context-menu.tsx +0 -339
- package/src/components/organisms/dashboard-grid.tsx +0 -369
- package/src/components/organisms/data-table.tsx +0 -330
- package/src/components/organisms/dialog.tsx +0 -312
- package/src/components/organisms/drawer.tsx +0 -123
- package/src/components/organisms/dropdown-menu.tsx +0 -440
- package/src/components/organisms/editors/code-editor.tsx +0 -144
- package/src/components/organisms/editors/index.ts +0 -4
- package/src/components/organisms/editors/markdown-editor.tsx +0 -153
- package/src/components/organisms/editors/markdown-renderer.ts +0 -27
- package/src/components/organisms/editors/prose-canvas-classes.ts +0 -45
- package/src/components/organisms/editors/rich-text-editor.tsx +0 -126
- package/src/components/organisms/editors/toolbar/md-toolbar.tsx +0 -129
- package/src/components/organisms/editors/toolbar/rte-toolbar.tsx +0 -211
- package/src/components/organisms/editors/toolbar/toolbar-shell.tsx +0 -45
- package/src/components/organisms/editors/use-codemirror-theme.ts +0 -61
- package/src/components/organisms/error-boundary.tsx +0 -61
- package/src/components/organisms/form.tsx +0 -174
- package/src/components/organisms/hover-card.tsx +0 -115
- package/src/components/organisms/menubar.tsx +0 -498
- package/src/components/organisms/navbar.tsx +0 -104
- package/src/components/organisms/navigation-menu.tsx +0 -235
- package/src/components/organisms/popover.tsx +0 -149
- package/src/components/organisms/resizable.tsx +0 -58
- package/src/components/organisms/schema-form.tsx +0 -232
- package/src/components/organisms/select.tsx +0 -309
- package/src/components/organisms/sheet.tsx +0 -265
- package/src/components/organisms/sidebar.tsx +0 -1040
- package/src/components/organisms/sonner.tsx +0 -96
- package/src/components/organisms/tabs.tsx +0 -133
- package/src/components/organisms/theme-provider.tsx +0 -101
- package/src/hooks/use-mobile.tsx +0 -19
- package/src/lib/portal-container.tsx +0 -35
- package/src/lib/utils.ts +0 -6
- package/src/native.ts +0 -23
- package/src/tokens/colors.ts +0 -91
- package/src/tokens/index.ts +0 -3
- package/src/tokens/spacing.ts +0 -55
- package/src/tokens/typography.ts +0 -27
- package/styles/dashboard-grid.css +0 -47
- package/styles/fonts/Roboto-VariableFont_wdth_wght.ttf +0 -0
- package/styles/glass.css +0 -175
- package/styles/leaflet.css +0 -13
- package/styles/tokens.css +0 -317
- 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 };
|