@ponti-studios/ui 2.0.2 → 3.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/README.md +36 -3
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.js +3 -0
- package/dist/components/data-display/accordion.d.ts +10 -0
- package/dist/components/data-display/accordion.js +53 -0
- package/{src/components/data-display/index.ts → dist/components/data-display/index.d.ts} +1 -10
- package/dist/components/data-display/index.js +8 -0
- package/dist/components/data-display/metric-card.d.ts +9 -0
- package/dist/components/data-display/metric-card.js +25 -0
- package/dist/components/data-display/sort-controls.d.ts +13 -0
- package/dist/components/data-display/sort-controls.js +57 -0
- package/dist/components/data-display/sort-row.d.ts +10 -0
- package/dist/components/data-display/sort-row.js +52 -0
- package/dist/components/data-display/table.d.ts +10 -0
- package/dist/components/data-display/table.js +57 -0
- package/dist/components/feedback/alert.d.ts +9 -0
- package/dist/components/feedback/alert.js +38 -0
- package/dist/components/feedback/empty-state.d.ts +18 -0
- package/dist/components/feedback/empty-state.js +36 -0
- package/{src/components/feedback/index.ts → dist/components/feedback/index.d.ts} +2 -0
- package/dist/components/feedback/index.js +7 -0
- package/dist/components/feedback/progress.d.ts +6 -0
- package/dist/components/feedback/progress.js +25 -0
- package/dist/components/feedback/skeleton.d.ts +2 -0
- package/dist/components/feedback/skeleton.js +11 -0
- package/dist/components/feedback/spinner.d.ts +14 -0
- package/dist/components/feedback/spinner.js +27 -0
- package/dist/components/feedback/update-guard.d.ts +13 -0
- package/dist/components/feedback/update-guard.js +38 -0
- package/dist/components/filters/active-filters-bar.d.ts +14 -0
- package/dist/components/filters/active-filters-bar.js +44 -0
- package/dist/components/filters/filter-chip.d.ts +11 -0
- package/dist/components/filters/filter-chip.js +33 -0
- package/dist/components/filters/filter-select.d.ts +17 -0
- package/dist/components/filters/filter-select.js +46 -0
- package/dist/components/filters/index.js +4 -0
- package/dist/components/forms/checkbox.d.ts +4 -0
- package/dist/components/forms/checkbox.js +24 -0
- package/dist/components/forms/date-month-select.d.ts +11 -0
- package/dist/components/forms/date-month-select.js +47 -0
- package/dist/components/forms/date-picker.d.ts +60 -0
- package/dist/components/forms/date-picker.js +193 -0
- package/dist/components/forms/date-utils.d.ts +14 -0
- package/dist/components/forms/date-utils.js +18 -0
- package/dist/components/forms/drop-zone.d.ts +42 -0
- package/dist/components/forms/drop-zone.js +184 -0
- package/{src/components/forms/index.ts → dist/components/forms/index.d.ts} +3 -9
- package/dist/components/forms/index.js +17 -0
- package/dist/components/forms/input.d.ts +3 -0
- package/dist/components/forms/input.js +15 -0
- package/dist/components/forms/label.d.ts +3 -0
- package/dist/components/forms/label.js +14 -0
- package/dist/components/forms/passkey-management.d.ts +14 -0
- package/dist/components/forms/passkey-management.js +132 -0
- package/dist/components/forms/radio-group.d.ts +6 -0
- package/dist/components/forms/radio-group.js +28 -0
- package/dist/components/forms/select.d.ts +18 -0
- package/dist/components/forms/select.js +103 -0
- package/dist/components/forms/slider.d.ts +9 -0
- package/dist/components/forms/slider.js +25 -0
- package/dist/components/forms/stepper.d.ts +15 -0
- package/dist/components/forms/stepper.js +41 -0
- package/dist/components/forms/switch.d.ts +6 -0
- package/dist/components/forms/switch.js +21 -0
- package/dist/components/forms/text-field.d.ts +15 -0
- package/dist/components/forms/text-field.js +46 -0
- package/dist/components/forms/text-field.types.d.ts +8 -0
- package/dist/components/forms/textarea.d.ts +3 -0
- package/dist/components/forms/textarea.js +14 -0
- package/dist/components/layout/index.js +5 -0
- package/dist/components/layout/particle-background.d.ts +19 -0
- package/dist/components/layout/particle-background.js +171 -0
- package/dist/components/layout/scroll-area.d.ts +9 -0
- package/dist/components/layout/scroll-area.js +35 -0
- package/dist/components/layout/section-intro.d.ts +12 -0
- package/dist/components/layout/section-intro.js +28 -0
- package/dist/components/layout/surface-panel.d.ts +7 -0
- package/dist/components/layout/surface-panel.js +11 -0
- package/dist/components/navigation/app-navigation.d.ts +67 -0
- package/dist/components/navigation/app-navigation.js +106 -0
- package/{src/components/navigation/index.ts → dist/components/navigation/index.d.ts} +1 -7
- package/dist/components/navigation/index.js +4 -0
- package/dist/components/navigation/pagination-controls.d.ts +7 -0
- package/dist/components/navigation/pagination-controls.js +50 -0
- package/dist/components/navigation/tabs.d.ts +7 -0
- package/dist/components/navigation/tabs.js +35 -0
- package/dist/components/overlays/bottom-sheet.d.ts +20 -0
- package/dist/components/overlays/bottom-sheet.js +101 -0
- package/dist/components/overlays/command.d.ts +29 -0
- package/dist/components/overlays/command.js +123 -0
- package/dist/components/overlays/dialog.d.ts +30 -0
- package/dist/components/overlays/dialog.js +105 -0
- package/dist/components/overlays/dropdown-menu.d.ts +29 -0
- package/dist/components/overlays/dropdown-menu.js +128 -0
- package/dist/components/overlays/index.d.ts +7 -0
- package/dist/components/overlays/index.js +8 -0
- package/dist/components/overlays/popover.d.ts +14 -0
- package/dist/components/overlays/popover.js +56 -0
- package/dist/components/overlays/sheet.d.ts +27 -0
- package/dist/components/overlays/sheet.js +81 -0
- package/dist/components/overlays/tooltip.d.ts +10 -0
- package/dist/components/overlays/tooltip.js +23 -0
- package/dist/components/primitives/avatar.d.ts +14 -0
- package/dist/components/primitives/avatar.js +72 -0
- package/dist/components/primitives/badge.d.ts +7 -0
- package/dist/components/primitives/badge.js +27 -0
- package/dist/components/primitives/button.d.ts +13 -0
- package/dist/components/primitives/button.js +113 -0
- package/{src/components/primitives/button.types.ts → dist/components/primitives/button.types.d.ts} +4 -6
- package/dist/components/primitives/card.d.ts +9 -0
- package/dist/components/primitives/card.js +56 -0
- package/{src/components/primitives/index.ts → dist/components/primitives/index.d.ts} +1 -9
- package/dist/components/primitives/index.js +7 -0
- package/dist/components/primitives/segmented-control.d.ts +20 -0
- package/dist/components/primitives/segmented-control.js +27 -0
- package/dist/components/primitives/separator.d.ts +4 -0
- package/dist/components/primitives/separator.js +16 -0
- package/dist/constants/chart-colors.d.ts +33 -0
- package/dist/constants/chart-colors.js +31 -0
- package/dist/hooks/index.js +9 -0
- package/{src/hooks/sort.types.ts → dist/hooks/sort.types.d.ts} +2 -4
- package/dist/hooks/use-api-client.d.ts +30 -0
- package/dist/hooks/use-api-client.js +106 -0
- package/dist/hooks/use-debounced-value.d.ts +1 -0
- package/dist/hooks/use-debounced-value.js +12 -0
- package/dist/hooks/use-derived-filter-state.d.ts +20 -0
- package/dist/hooks/use-derived-filter-state.js +39 -0
- package/dist/hooks/use-filter-state.d.ts +12 -0
- package/dist/hooks/use-filter-state.js +37 -0
- package/dist/hooks/use-media-query.d.ts +6 -0
- package/dist/hooks/use-media-query.js +27 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-mobile.js +14 -0
- package/dist/hooks/use-sort.d.ts +11 -0
- package/dist/hooks/use-sort.js +14 -0
- package/{src/index.ts → dist/index.d.ts} +4 -32
- package/dist/index.js +15 -0
- package/dist/integrations/service-worker.d.ts +2 -0
- package/dist/integrations/service-worker.js +2 -0
- package/dist/lib/auth.d.ts +2 -0
- package/dist/lib/auth.js +7 -0
- package/dist/lib/clipboard.d.ts +1 -0
- package/dist/lib/clipboard.js +10 -0
- package/dist/lib/index.js +4 -0
- package/{src/lib/slot.tsx → dist/lib/slot.d.ts} +2 -15
- package/dist/lib/slot.js +14 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +8 -0
- package/dist/native/components.d.ts +41 -0
- package/dist/native/components.js +135 -0
- package/dist/native/index.js +3 -0
- package/dist/native/theme.d.ts +19 -0
- package/dist/native/theme.js +58 -0
- package/dist/styles/tokens/css-vars.d.ts +7 -0
- package/dist/styles/tokens/css-vars.js +5 -0
- package/dist/styles/tokens/generated/dark.colors.generated.d.ts +40 -0
- package/dist/styles/tokens/generated/dark.colors.generated.js +39 -0
- package/dist/styles/tokens/generated/foundations.generated.d.ts +323 -0
- package/dist/styles/tokens/generated/foundations.generated.js +453 -0
- package/dist/styles/tokens/generated/light.colors.generated.d.ts +40 -0
- package/dist/styles/tokens/generated/light.colors.generated.js +39 -0
- package/dist/styles/tokens/index.d.ts +317 -0
- package/dist/styles/tokens/index.js +14 -0
- package/dist/styles.css +1 -1
- package/dist/translations/en.d.ts +34 -0
- package/dist/translations/index.d.ts +39 -0
- package/dist/translations/ui-provider.d.ts +9 -0
- package/dist/translations/ui-provider.js +16 -0
- package/dist/types/upload.d.ts +36 -0
- package/package.json +50 -34
- package/src/components/data-display/accordion.stories.tsx +0 -57
- package/src/components/data-display/accordion.tsx +0 -86
- package/src/components/data-display/metric-card.stories.tsx +0 -48
- package/src/components/data-display/metric-card.tsx +0 -20
- package/src/components/data-display/sort-controls.stories.tsx +0 -45
- package/src/components/data-display/sort-controls.tsx +0 -96
- package/src/components/data-display/sort-row.tsx +0 -88
- package/src/components/data-display/table.stories.tsx +0 -75
- package/src/components/data-display/table.tsx +0 -80
- package/src/components/feedback/alert.stories.tsx +0 -51
- package/src/components/feedback/alert.tsx +0 -64
- package/src/components/feedback/empty-state.tsx +0 -76
- package/src/components/feedback/progress.stories.tsx +0 -54
- package/src/components/feedback/progress.tsx +0 -41
- package/src/components/feedback/skeleton.stories.tsx +0 -36
- package/src/components/feedback/skeleton.tsx +0 -7
- package/src/components/feedback/spinner.stories.tsx +0 -52
- package/src/components/feedback/spinner.tsx +0 -47
- package/src/components/feedback/update-guard.stories.tsx +0 -28
- package/src/components/feedback/update-guard.tsx +0 -57
- package/src/components/filters/active-filters-bar.stories.tsx +0 -114
- package/src/components/filters/active-filters-bar.tsx +0 -61
- package/src/components/filters/filter-chip.tsx +0 -57
- package/src/components/filters/filter-select.stories.tsx +0 -71
- package/src/components/filters/filter-select.tsx +0 -68
- package/src/components/forms/checkbox.stories.tsx +0 -35
- package/src/components/forms/checkbox.tsx +0 -38
- package/src/components/forms/date-month-select.stories.tsx +0 -26
- package/src/components/forms/date-month-select.tsx +0 -79
- package/src/components/forms/date-picker.stories.tsx +0 -188
- package/src/components/forms/date-picker.tsx +0 -303
- package/src/components/forms/date-utils.ts +0 -37
- package/src/components/forms/drop-zone.stories.tsx +0 -140
- package/src/components/forms/drop-zone.tsx +0 -298
- package/src/components/forms/input.stories.tsx +0 -47
- package/src/components/forms/input.tsx +0 -24
- package/src/components/forms/label.stories.tsx +0 -16
- package/src/components/forms/label.tsx +0 -25
- package/src/components/forms/passkey-management.stories.tsx +0 -85
- package/src/components/forms/passkey-management.tsx +0 -168
- package/src/components/forms/radio-group.stories.tsx +0 -46
- package/src/components/forms/radio-group.tsx +0 -44
- package/src/components/forms/select.stories.tsx +0 -207
- package/src/components/forms/select.tsx +0 -182
- package/src/components/forms/slider.stories.tsx +0 -70
- package/src/components/forms/slider.tsx +0 -50
- package/src/components/forms/stepper.stories.tsx +0 -111
- package/src/components/forms/stepper.tsx +0 -66
- package/src/components/forms/switch.stories.tsx +0 -144
- package/src/components/forms/switch.tsx +0 -39
- package/src/components/forms/text-field.stories.tsx +0 -87
- package/src/components/forms/text-field.tsx +0 -75
- package/src/components/forms/text-field.types.ts +0 -9
- package/src/components/forms/textarea.stories.tsx +0 -150
- package/src/components/forms/textarea.tsx +0 -22
- package/src/components/layout/particle-background.stories.tsx +0 -38
- package/src/components/layout/particle-background.tsx +0 -378
- package/src/components/layout/scroll-area.stories.tsx +0 -91
- package/src/components/layout/scroll-area.tsx +0 -79
- package/src/components/layout/section-intro.stories.tsx +0 -29
- package/src/components/layout/section-intro.tsx +0 -31
- package/src/components/layout/surface-panel.stories.tsx +0 -32
- package/src/components/layout/surface-panel.tsx +0 -23
- package/src/components/navigation/app-navigation.stories.tsx +0 -217
- package/src/components/navigation/app-navigation.tsx +0 -234
- package/src/components/navigation/pagination-controls.stories.tsx +0 -41
- package/src/components/navigation/pagination-controls.tsx +0 -46
- package/src/components/navigation/tabs.stories.tsx +0 -48
- package/src/components/navigation/tabs.tsx +0 -52
- package/src/components/overlays/bottom-sheet.stories.tsx +0 -52
- package/src/components/overlays/bottom-sheet.tsx +0 -179
- package/src/components/overlays/command.stories.tsx +0 -65
- package/src/components/overlays/command.tsx +0 -227
- package/src/components/overlays/dialog.stories.tsx +0 -148
- package/src/components/overlays/dialog.tsx +0 -203
- package/src/components/overlays/dropdown-menu.stories.tsx +0 -74
- package/src/components/overlays/dropdown-menu.tsx +0 -236
- package/src/components/overlays/index.ts +0 -56
- package/src/components/overlays/popover.stories.tsx +0 -51
- package/src/components/overlays/popover.tsx +0 -78
- package/src/components/overlays/sheet.stories.tsx +0 -68
- package/src/components/overlays/sheet.tsx +0 -155
- package/src/components/overlays/tooltip.stories.tsx +0 -29
- package/src/components/overlays/tooltip.tsx +0 -41
- package/src/components/primitives/avatar-placeholder.svg +0 -13
- package/src/components/primitives/avatar.stories.tsx +0 -72
- package/src/components/primitives/avatar.tsx +0 -136
- package/src/components/primitives/badge.stories.tsx +0 -43
- package/src/components/primitives/badge.tsx +0 -43
- package/src/components/primitives/button.stories.tsx +0 -113
- package/src/components/primitives/button.tsx +0 -131
- package/src/components/primitives/card.stories.tsx +0 -59
- package/src/components/primitives/card.tsx +0 -66
- package/src/components/primitives/segmented-control.stories.tsx +0 -54
- package/src/components/primitives/segmented-control.tsx +0 -73
- package/src/components/primitives/separator.stories.tsx +0 -34
- package/src/components/primitives/separator.tsx +0 -27
- package/src/constants/chart-colors.ts +0 -39
- package/src/env.d.ts +0 -9
- package/src/hooks/use-api-client.ts +0 -122
- package/src/hooks/use-debounced-value.ts +0 -12
- package/src/hooks/use-derived-filter-state.ts +0 -70
- package/src/hooks/use-filter-state.ts +0 -66
- package/src/hooks/use-media-query.ts +0 -69
- package/src/hooks/use-mobile.tsx +0 -19
- package/src/hooks/use-sort.ts +0 -20
- package/src/lib/auth.ts +0 -5
- package/src/lib/clipboard.ts +0 -8
- package/src/lib/utils.ts +0 -6
- package/src/native/components.tsx +0 -198
- package/src/native/theme.tsx +0 -69
- package/src/storybook/controls.ts +0 -117
- package/src/storybook/options.ts +0 -52
- package/src/styles/build.ts +0 -1
- package/src/styles/index.css +0 -423
- package/src/styles/nokia.css +0 -36
- package/src/styles/package.css +0 -3
- package/src/styles/tokens/chart-colors.stories.tsx +0 -189
- package/src/styles/tokens/color-systems.stories.tsx +0 -190
- package/src/styles/tokens/css-vars.ts +0 -15
- package/src/styles/tokens/generated/dark-class.css +0 -39
- package/src/styles/tokens/generated/dark.colors.generated.ts +0 -39
- package/src/styles/tokens/generated/dark.css +0 -39
- package/src/styles/tokens/generated/foundations.css +0 -149
- package/src/styles/tokens/generated/foundations.generated.ts +0 -454
- package/src/styles/tokens/generated/light.colors.generated.ts +0 -39
- package/src/styles/tokens/generated/light.css +0 -39
- package/src/styles/tokens/index.ts +0 -70
- package/src/styles/tokens/layout.stories.tsx +0 -50
- package/src/styles/tokens/scripts/check-foundations.mjs +0 -126
- package/src/styles/tokens/scripts/check-source.mjs +0 -53
- package/src/styles/tokens/source/dark.tokens.json +0 -138
- package/src/styles/tokens/source/foundations.tokens.json +0 -344
- package/src/styles/tokens/source/light.tokens.json +0 -138
- package/src/styles/tokens/style-dictionary.dark-class.json +0 -16
- package/src/styles/tokens/style-dictionary.dark-storybook.json +0 -16
- package/src/styles/tokens/style-dictionary.dark.json +0 -21
- package/src/styles/tokens/style-dictionary.foundations.json +0 -21
- package/src/styles/tokens/style-dictionary.light-storybook.json +0 -16
- package/src/styles/tokens/style-dictionary.light.json +0 -21
- package/src/styles/tokens/typography.stories.tsx +0 -157
- package/src/translations/en.ts +0 -34
- package/src/translations/index.ts +0 -32
- package/src/types/upload.ts +0 -42
- /package/{src/components/filters/index.ts → dist/components/filters/index.d.ts} +0 -0
- /package/{src/components/layout/index.ts → dist/components/layout/index.d.ts} +0 -0
- /package/{src/hooks/index.ts → dist/hooks/index.d.ts} +0 -0
- /package/{src/lib/index.ts → dist/lib/index.d.ts} +0 -0
- /package/{src/native/index.ts → dist/native/index.d.ts} +0 -0
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import { FileIcon, UploadIcon, XIcon } from "lucide-react";
|
|
2
|
-
import {
|
|
3
|
-
useCallback,
|
|
4
|
-
useId,
|
|
5
|
-
useRef,
|
|
6
|
-
useState,
|
|
7
|
-
type ChangeEvent,
|
|
8
|
-
type DragEvent,
|
|
9
|
-
type KeyboardEvent,
|
|
10
|
-
type ReactNode,
|
|
11
|
-
} from "react";
|
|
12
|
-
|
|
13
|
-
import { cn } from "../../lib/utils";
|
|
14
|
-
import { Progress } from "../feedback/progress";
|
|
15
|
-
import { Button } from "../primitives/button";
|
|
16
|
-
|
|
17
|
-
export type DropZoneStatus = "empty" | "armed" | "busy" | "failed";
|
|
18
|
-
|
|
19
|
-
export type DropZoneFileInfo = {
|
|
20
|
-
name: string;
|
|
21
|
-
size: number;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export type DropZoneProps = {
|
|
25
|
-
status: DropZoneStatus;
|
|
26
|
-
/** Selected file metadata for armed / busy / failed (with file retained). */
|
|
27
|
-
file?: DropZoneFileInfo | null;
|
|
28
|
-
error?: string | null;
|
|
29
|
-
notice?: string | null;
|
|
30
|
-
/** File input accept attribute, e.g. `.pdf,application/pdf`. */
|
|
31
|
-
accept?: string;
|
|
32
|
-
multiple?: boolean;
|
|
33
|
-
disabled?: boolean;
|
|
34
|
-
className?: string;
|
|
35
|
-
/** Empty idle copy */
|
|
36
|
-
emptyLabel?: ReactNode;
|
|
37
|
-
emptyHint?: ReactNode;
|
|
38
|
-
draggingLabel?: ReactNode;
|
|
39
|
-
/** Busy copy */
|
|
40
|
-
busyLabel?: ReactNode;
|
|
41
|
-
busyDescription?: ReactNode;
|
|
42
|
-
/** Action labels — parent owns domain language */
|
|
43
|
-
submitLabel?: string;
|
|
44
|
-
retryLabel?: string;
|
|
45
|
-
clearLabel?: string;
|
|
46
|
-
cancelLabel?: string;
|
|
47
|
-
/** Extra content under error (e.g. sign-in link). */
|
|
48
|
-
failedActions?: ReactNode;
|
|
49
|
-
onFiles: (files: File[]) => void;
|
|
50
|
-
onSubmit?: () => void;
|
|
51
|
-
onClear?: () => void;
|
|
52
|
-
onCancel?: () => void;
|
|
53
|
-
onRetry?: () => void;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
function formatFileSize(bytes: number) {
|
|
57
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
58
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
59
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Generic file drop surface. Parent owns validation, network, and domain copy.
|
|
64
|
-
* Modes: empty | armed | busy | failed, plus an internal drag overlay.
|
|
65
|
-
*/
|
|
66
|
-
export function DropZone({
|
|
67
|
-
status,
|
|
68
|
-
file = null,
|
|
69
|
-
error = null,
|
|
70
|
-
notice = null,
|
|
71
|
-
accept,
|
|
72
|
-
multiple = false,
|
|
73
|
-
disabled = false,
|
|
74
|
-
className,
|
|
75
|
-
emptyLabel = (
|
|
76
|
-
<>
|
|
77
|
-
Drop a file here, or <span className="text-accent-text font-medium">browse</span>
|
|
78
|
-
</>
|
|
79
|
-
),
|
|
80
|
-
emptyHint,
|
|
81
|
-
draggingLabel = "Release to attach",
|
|
82
|
-
busyLabel = "Processing…",
|
|
83
|
-
busyDescription,
|
|
84
|
-
submitLabel = "Upload",
|
|
85
|
-
retryLabel = "Try again",
|
|
86
|
-
clearLabel = "Use a different file",
|
|
87
|
-
cancelLabel = "Cancel",
|
|
88
|
-
failedActions,
|
|
89
|
-
onFiles,
|
|
90
|
-
onSubmit,
|
|
91
|
-
onClear,
|
|
92
|
-
onCancel,
|
|
93
|
-
onRetry,
|
|
94
|
-
}: DropZoneProps) {
|
|
95
|
-
const inputId = useId();
|
|
96
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
97
|
-
const zoneRef = useRef<HTMLDivElement>(null);
|
|
98
|
-
const [dragging, setDragging] = useState(false);
|
|
99
|
-
|
|
100
|
-
const isBusy = status === "busy";
|
|
101
|
-
const isInteractive = !disabled && !isBusy;
|
|
102
|
-
|
|
103
|
-
const openPicker = useCallback(() => {
|
|
104
|
-
if (!isInteractive) return;
|
|
105
|
-
inputRef.current?.click();
|
|
106
|
-
}, [isInteractive]);
|
|
107
|
-
|
|
108
|
-
const handleFiles = useCallback(
|
|
109
|
-
(list: FileList | File[] | null) => {
|
|
110
|
-
if (!list || !isInteractive) return;
|
|
111
|
-
const files = Array.from(list);
|
|
112
|
-
if (files.length === 0) return;
|
|
113
|
-
onFiles(files);
|
|
114
|
-
},
|
|
115
|
-
[isInteractive, onFiles],
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
const onDragEnter = (event: DragEvent) => {
|
|
119
|
-
event.preventDefault();
|
|
120
|
-
event.stopPropagation();
|
|
121
|
-
if (!isInteractive) return;
|
|
122
|
-
setDragging(true);
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const onDragOver = (event: DragEvent) => {
|
|
126
|
-
event.preventDefault();
|
|
127
|
-
event.stopPropagation();
|
|
128
|
-
if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const onDragLeave = (event: DragEvent) => {
|
|
132
|
-
event.preventDefault();
|
|
133
|
-
event.stopPropagation();
|
|
134
|
-
const next = event.relatedTarget;
|
|
135
|
-
if (next instanceof Node && zoneRef.current?.contains(next)) return;
|
|
136
|
-
setDragging(false);
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const onDrop = (event: DragEvent) => {
|
|
140
|
-
event.preventDefault();
|
|
141
|
-
event.stopPropagation();
|
|
142
|
-
setDragging(false);
|
|
143
|
-
if (!isInteractive) return;
|
|
144
|
-
handleFiles(event.dataTransfer.files);
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const onInputChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
148
|
-
handleFiles(event.target.files);
|
|
149
|
-
event.target.value = "";
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
153
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
154
|
-
event.preventDefault();
|
|
155
|
-
openPicker();
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const showDragging = dragging && isInteractive;
|
|
160
|
-
const frameTone = cn(
|
|
161
|
-
showDragging && "border-ring bg-primary/5",
|
|
162
|
-
status === "failed" && !showDragging && "border-destructive bg-destructive/5",
|
|
163
|
-
status === "busy" && "border-border bg-muted/30",
|
|
164
|
-
status !== "failed" && status !== "busy" && !showDragging && "border-border bg-card",
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
return (
|
|
168
|
-
<div className={cn("w-full space-y-3", className)}>
|
|
169
|
-
<input
|
|
170
|
-
ref={inputRef}
|
|
171
|
-
id={inputId}
|
|
172
|
-
type="file"
|
|
173
|
-
accept={accept}
|
|
174
|
-
multiple={multiple}
|
|
175
|
-
disabled={!isInteractive}
|
|
176
|
-
className="pointer-events-none sr-only"
|
|
177
|
-
onChange={onInputChange}
|
|
178
|
-
tabIndex={-1}
|
|
179
|
-
aria-hidden
|
|
180
|
-
/>
|
|
181
|
-
|
|
182
|
-
<div
|
|
183
|
-
ref={zoneRef}
|
|
184
|
-
role="button"
|
|
185
|
-
tabIndex={isInteractive ? 0 : -1}
|
|
186
|
-
aria-disabled={!isInteractive}
|
|
187
|
-
aria-busy={isBusy}
|
|
188
|
-
aria-label="File upload drop zone"
|
|
189
|
-
className={cn(
|
|
190
|
-
"relative flex min-h-44 w-full flex-col items-center justify-center overflow-hidden rounded-2xl border border-dashed p-6 text-center outline-none",
|
|
191
|
-
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2",
|
|
192
|
-
"motion-safe:transition-[transform,opacity] motion-safe:duration-200 motion-safe:ease-in-out motion-reduce:transition-none",
|
|
193
|
-
isInteractive ? "cursor-pointer" : "cursor-default",
|
|
194
|
-
showDragging ? "scale-[1.01] motion-reduce:scale-100" : "scale-100",
|
|
195
|
-
frameTone,
|
|
196
|
-
)}
|
|
197
|
-
onClick={isInteractive ? openPicker : undefined}
|
|
198
|
-
onKeyDown={isInteractive ? onKeyDown : undefined}
|
|
199
|
-
onDragEnter={onDragEnter}
|
|
200
|
-
onDragOver={onDragOver}
|
|
201
|
-
onDragLeave={onDragLeave}
|
|
202
|
-
onDrop={onDrop}
|
|
203
|
-
>
|
|
204
|
-
<div className="flex w-full max-w-sm flex-col items-center gap-3">
|
|
205
|
-
{showDragging ? (
|
|
206
|
-
<>
|
|
207
|
-
<UploadIcon
|
|
208
|
-
className="text-accent-text size-8 motion-safe:transition-[transform,opacity] motion-safe:duration-200 motion-safe:ease-in-out motion-reduce:transition-none motion-safe:-translate-y-0.5 motion-reduce:translate-y-0"
|
|
209
|
-
aria-hidden
|
|
210
|
-
/>
|
|
211
|
-
<p className="body-3 text-foreground">{draggingLabel}</p>
|
|
212
|
-
</>
|
|
213
|
-
) : null}
|
|
214
|
-
|
|
215
|
-
{!showDragging && status === "empty" ? (
|
|
216
|
-
<>
|
|
217
|
-
<UploadIcon className="size-8 text-muted-foreground" aria-hidden />
|
|
218
|
-
<div className="space-y-1">
|
|
219
|
-
<p className="body-3 text-foreground">{emptyLabel}</p>
|
|
220
|
-
{emptyHint ? <p className="body-4 text-muted-foreground">{emptyHint}</p> : null}
|
|
221
|
-
</div>
|
|
222
|
-
</>
|
|
223
|
-
) : null}
|
|
224
|
-
|
|
225
|
-
{!showDragging && status === "armed" && file ? (
|
|
226
|
-
<>
|
|
227
|
-
<FileIcon className="size-8 text-muted-foreground" aria-hidden />
|
|
228
|
-
<div className="min-w-0 max-w-full space-y-1">
|
|
229
|
-
<p className="subheading-4 truncate text-foreground">{file.name}</p>
|
|
230
|
-
<p className="body-4 text-muted-foreground">{formatFileSize(file.size)}</p>
|
|
231
|
-
</div>
|
|
232
|
-
</>
|
|
233
|
-
) : null}
|
|
234
|
-
|
|
235
|
-
{!showDragging && status === "busy" ? (
|
|
236
|
-
<div className="w-full space-y-3">
|
|
237
|
-
<div className="space-y-1">
|
|
238
|
-
<p className="subheading-4 text-foreground">{busyLabel}</p>
|
|
239
|
-
{busyDescription ? (
|
|
240
|
-
<p className="body-4 text-muted-foreground">{busyDescription}</p>
|
|
241
|
-
) : null}
|
|
242
|
-
{file ? <p className="body-4 truncate text-muted-foreground">{file.name}</p> : null}
|
|
243
|
-
</div>
|
|
244
|
-
<Progress
|
|
245
|
-
className="h-1.5 opacity-80 motion-safe:animate-pulse"
|
|
246
|
-
aria-label={typeof busyLabel === "string" ? busyLabel : "Processing"}
|
|
247
|
-
value={50}
|
|
248
|
-
/>
|
|
249
|
-
</div>
|
|
250
|
-
) : null}
|
|
251
|
-
|
|
252
|
-
{!showDragging && status === "failed" ? (
|
|
253
|
-
<div className="w-full space-y-2">
|
|
254
|
-
{file ? <p className="subheading-4 truncate text-foreground">{file.name}</p> : null}
|
|
255
|
-
{error ? <p className="body-3 text-destructive-text">{error}</p> : null}
|
|
256
|
-
{failedActions}
|
|
257
|
-
</div>
|
|
258
|
-
) : null}
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
|
|
262
|
-
{notice ? <p className="body-4 text-muted-foreground">{notice}</p> : null}
|
|
263
|
-
|
|
264
|
-
<div className="grid gap-2">
|
|
265
|
-
{status === "armed" && onSubmit ? (
|
|
266
|
-
<Button type="button" className="w-full" onClick={onSubmit}>
|
|
267
|
-
<UploadIcon className="size-4" />
|
|
268
|
-
{submitLabel}
|
|
269
|
-
</Button>
|
|
270
|
-
) : null}
|
|
271
|
-
{status === "failed" && onRetry ? (
|
|
272
|
-
<Button type="button" className="w-full" onClick={onRetry}>
|
|
273
|
-
{retryLabel}
|
|
274
|
-
</Button>
|
|
275
|
-
) : null}
|
|
276
|
-
{status === "busy" && onCancel ? (
|
|
277
|
-
<Button type="button" variant="outline" className="w-full" onClick={onCancel}>
|
|
278
|
-
<XIcon className="size-4" />
|
|
279
|
-
{cancelLabel}
|
|
280
|
-
</Button>
|
|
281
|
-
) : null}
|
|
282
|
-
{(status === "armed" || status === "failed") && onClear ? (
|
|
283
|
-
<Button
|
|
284
|
-
type="button"
|
|
285
|
-
variant="ghost"
|
|
286
|
-
className="w-full"
|
|
287
|
-
onClick={(event) => {
|
|
288
|
-
event.stopPropagation();
|
|
289
|
-
onClear();
|
|
290
|
-
}}
|
|
291
|
-
>
|
|
292
|
-
{clearLabel}
|
|
293
|
-
</Button>
|
|
294
|
-
) : null}
|
|
295
|
-
</div>
|
|
296
|
-
</div>
|
|
297
|
-
);
|
|
298
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
|
|
3
|
-
import { booleanControl, selectControl, textControl } from "../../storybook/controls";
|
|
4
|
-
import { inputTypeOptions } from "../../storybook/options";
|
|
5
|
-
import { Input } from "./input";
|
|
6
|
-
|
|
7
|
-
const meta = {
|
|
8
|
-
title: "Forms/Input",
|
|
9
|
-
component: Input,
|
|
10
|
-
tags: ["autodocs"],
|
|
11
|
-
argTypes: {
|
|
12
|
-
type: selectControl(inputTypeOptions, "HTML input type attribute", {
|
|
13
|
-
defaultValue: "text",
|
|
14
|
-
}),
|
|
15
|
-
placeholder: textControl("Placeholder text shown when the input is empty"),
|
|
16
|
-
disabled: booleanControl("Prevents user interaction and applies disabled styling", false),
|
|
17
|
-
required: booleanControl("Marks the input as required for form submission", false),
|
|
18
|
-
},
|
|
19
|
-
} satisfies Meta<typeof Input>;
|
|
20
|
-
|
|
21
|
-
export default meta;
|
|
22
|
-
type Story = StoryObj<typeof meta>;
|
|
23
|
-
|
|
24
|
-
export const Default: Story = {
|
|
25
|
-
args: { placeholder: "Enter text…" },
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export const WithLabel: Story = {
|
|
29
|
-
render: () => (
|
|
30
|
-
<div className="flex w-72 flex-col gap-2">
|
|
31
|
-
<label htmlFor="email" className="text-foreground text-sm leading-none font-medium">
|
|
32
|
-
Email
|
|
33
|
-
</label>
|
|
34
|
-
<Input id="email" type="email" placeholder="you@example.com" />
|
|
35
|
-
</div>
|
|
36
|
-
),
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export const States: Story = {
|
|
40
|
-
render: () => (
|
|
41
|
-
<div className="flex w-72 flex-col gap-4">
|
|
42
|
-
<Input aria-label="Default input" placeholder="Default" />
|
|
43
|
-
<Input aria-label="Disabled input" placeholder="Disabled" disabled />
|
|
44
|
-
<Input aria-label="Input with value" defaultValue="With value" />
|
|
45
|
-
</div>
|
|
46
|
-
),
|
|
47
|
-
};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
import { cn } from "../../lib/utils";
|
|
4
|
-
|
|
5
|
-
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
6
|
-
return (
|
|
7
|
-
<input
|
|
8
|
-
type={type}
|
|
9
|
-
data-slot="input"
|
|
10
|
-
className={cn(
|
|
11
|
-
"bg-background text-foreground border-input h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base transition-colors md:text-sm",
|
|
12
|
-
"file:text-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium",
|
|
13
|
-
"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground",
|
|
14
|
-
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
|
15
|
-
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
16
|
-
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
|
|
17
|
-
className,
|
|
18
|
-
)}
|
|
19
|
-
{...props}
|
|
20
|
-
/>
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export { Input };
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
|
|
3
|
-
import { Label } from "./label";
|
|
4
|
-
|
|
5
|
-
const meta = {
|
|
6
|
-
title: "Forms/Label",
|
|
7
|
-
component: Label,
|
|
8
|
-
tags: ["autodocs"],
|
|
9
|
-
} satisfies Meta<typeof Label>;
|
|
10
|
-
|
|
11
|
-
export default meta;
|
|
12
|
-
type Story = StoryObj<typeof meta>;
|
|
13
|
-
|
|
14
|
-
export const Default: Story = {
|
|
15
|
-
args: { children: "Email address", htmlFor: "email" },
|
|
16
|
-
};
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
|
|
5
|
-
import { cn } from "../../lib/utils";
|
|
6
|
-
|
|
7
|
-
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
|
8
|
-
return (
|
|
9
|
-
// Association happens at usage (htmlFor or wrapping); the wrapper
|
|
10
|
-
// itself cannot know its control.
|
|
11
|
-
// oxlint-disable-next-line jsx-a11y/label-has-associated-control
|
|
12
|
-
<label
|
|
13
|
-
data-slot="label"
|
|
14
|
-
className={cn(
|
|
15
|
-
"text-sm leading-none font-medium select-none",
|
|
16
|
-
"peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
17
|
-
"group-has-disabled:cursor-not-allowed group-has-disabled:opacity-50",
|
|
18
|
-
className,
|
|
19
|
-
)}
|
|
20
|
-
{...props}
|
|
21
|
-
/>
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export { Label };
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
|
|
4
|
-
import { PasskeyManagement, type PasskeyRecord } from "./passkey-management";
|
|
5
|
-
|
|
6
|
-
const meta = {
|
|
7
|
-
title: "Forms/PasskeyManagement",
|
|
8
|
-
component: PasskeyManagement,
|
|
9
|
-
tags: ["autodocs"],
|
|
10
|
-
} satisfies Meta<typeof PasskeyManagement>;
|
|
11
|
-
|
|
12
|
-
export default meta;
|
|
13
|
-
type Story = StoryObj<typeof meta>;
|
|
14
|
-
|
|
15
|
-
const initialPasskeys: PasskeyRecord[] = [
|
|
16
|
-
{ id: "1", name: "MacBook Pro", createdAt: "2025-01-15T00:00:00.000Z" },
|
|
17
|
-
{ id: "2", name: "iPhone", createdAt: "2025-03-02T00:00:00.000Z" },
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
function PasskeyManagementPreview({ withDeleteError = false }: { withDeleteError?: boolean }) {
|
|
21
|
-
const [passkeys, setPasskeys] = useState(initialPasskeys);
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<div className="w-96">
|
|
25
|
-
<PasskeyManagement
|
|
26
|
-
passkeys={passkeys}
|
|
27
|
-
onAdd={async () => {
|
|
28
|
-
setPasskeys((prev) => [
|
|
29
|
-
...prev,
|
|
30
|
-
{
|
|
31
|
-
id: String(prev.length + 1),
|
|
32
|
-
name: "New device",
|
|
33
|
-
createdAt: new Date().toISOString(),
|
|
34
|
-
},
|
|
35
|
-
]);
|
|
36
|
-
return true;
|
|
37
|
-
}}
|
|
38
|
-
onDelete={async (id) => {
|
|
39
|
-
if (withDeleteError && id === "2") return false;
|
|
40
|
-
setPasskeys((prev) => prev.filter((pk) => pk.id !== id));
|
|
41
|
-
return true;
|
|
42
|
-
}}
|
|
43
|
-
/>
|
|
44
|
-
</div>
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export const Default: Story = {
|
|
49
|
-
args: {
|
|
50
|
-
passkeys: initialPasskeys,
|
|
51
|
-
onAdd: async () => true,
|
|
52
|
-
onDelete: async () => true,
|
|
53
|
-
},
|
|
54
|
-
render: () => <PasskeyManagementPreview />,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export const Empty: Story = {
|
|
58
|
-
args: {
|
|
59
|
-
passkeys: [],
|
|
60
|
-
onAdd: async () => true,
|
|
61
|
-
onDelete: async () => true,
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export const Loading: Story = {
|
|
66
|
-
args: {
|
|
67
|
-
isLoading: true,
|
|
68
|
-
onAdd: async () => true,
|
|
69
|
-
onDelete: async () => true,
|
|
70
|
-
},
|
|
71
|
-
render: (args) => (
|
|
72
|
-
<div className="w-[calc(100vw-2rem)] max-w-md">
|
|
73
|
-
<PasskeyManagement {...args} />
|
|
74
|
-
</div>
|
|
75
|
-
),
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export const WithError: Story = {
|
|
79
|
-
args: {
|
|
80
|
-
passkeys: initialPasskeys,
|
|
81
|
-
onAdd: async () => true,
|
|
82
|
-
onDelete: async () => true,
|
|
83
|
-
},
|
|
84
|
-
render: () => <PasskeyManagementPreview withDeleteError />,
|
|
85
|
-
};
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { KeyRound, Plus, Trash2, X } from "lucide-react";
|
|
2
|
-
import { useCallback, useState } from "react";
|
|
3
|
-
|
|
4
|
-
import { Spinner } from "../feedback/spinner";
|
|
5
|
-
import { Button } from "../primitives/button";
|
|
6
|
-
import { Card, CardHeader } from "../primitives/card";
|
|
7
|
-
|
|
8
|
-
export interface PasskeyRecord {
|
|
9
|
-
id: string;
|
|
10
|
-
name?: string;
|
|
11
|
-
createdAt?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface PasskeyManagementProps {
|
|
15
|
-
passkeys?: PasskeyRecord[];
|
|
16
|
-
isLoading?: boolean;
|
|
17
|
-
error?: string | null;
|
|
18
|
-
onAdd: () => Promise<boolean>;
|
|
19
|
-
onDelete: (id: string) => Promise<boolean>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type PasskeyActionError = {
|
|
23
|
-
message: string;
|
|
24
|
-
passkeyId?: string;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/** List, add, and remove passkeys. The host app owns the WebAuthn ceremony via `onAdd`/`onDelete`. */
|
|
28
|
-
export function PasskeyManagement({
|
|
29
|
-
passkeys: passkeysProp,
|
|
30
|
-
isLoading = false,
|
|
31
|
-
error: externalError,
|
|
32
|
-
onAdd,
|
|
33
|
-
onDelete,
|
|
34
|
-
}: PasskeyManagementProps) {
|
|
35
|
-
const [adding, setAdding] = useState(false);
|
|
36
|
-
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
37
|
-
const [actionError, setActionError] = useState<PasskeyActionError | null>(null);
|
|
38
|
-
|
|
39
|
-
const passkeys = passkeysProp ?? [];
|
|
40
|
-
const error = externalError ?? (actionError?.passkeyId ? null : actionError?.message);
|
|
41
|
-
|
|
42
|
-
const handleAdd = useCallback(async () => {
|
|
43
|
-
setAdding(true);
|
|
44
|
-
setActionError(null);
|
|
45
|
-
try {
|
|
46
|
-
const success = await onAdd();
|
|
47
|
-
if (!success) {
|
|
48
|
-
setActionError({ message: "Passkey registration was cancelled or failed." });
|
|
49
|
-
}
|
|
50
|
-
} catch {
|
|
51
|
-
setActionError({ message: "An error occurred during passkey registration." });
|
|
52
|
-
} finally {
|
|
53
|
-
setAdding(false);
|
|
54
|
-
}
|
|
55
|
-
}, [onAdd]);
|
|
56
|
-
|
|
57
|
-
const handleDelete = useCallback(
|
|
58
|
-
async (id: string) => {
|
|
59
|
-
setDeletingId(id);
|
|
60
|
-
setActionError(null);
|
|
61
|
-
try {
|
|
62
|
-
const success = await onDelete(id);
|
|
63
|
-
if (!success) {
|
|
64
|
-
throw new Error("Failed to delete passkey");
|
|
65
|
-
}
|
|
66
|
-
} catch {
|
|
67
|
-
setActionError({
|
|
68
|
-
message: "Could not delete passkey. Please try again.",
|
|
69
|
-
passkeyId: id,
|
|
70
|
-
});
|
|
71
|
-
} finally {
|
|
72
|
-
setDeletingId(null);
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
[onDelete],
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<Card aria-labelledby="passkey-heading" className="gap-0 overflow-hidden w-full">
|
|
80
|
-
<CardHeader className="flex-row items-center border-b p-4">
|
|
81
|
-
<h2 id="passkey-heading" className="text-foreground text-sm font-medium">
|
|
82
|
-
Passkeys
|
|
83
|
-
</h2>
|
|
84
|
-
<Button
|
|
85
|
-
type="button"
|
|
86
|
-
size="sm"
|
|
87
|
-
onClick={() => void handleAdd()}
|
|
88
|
-
isLoading={adding}
|
|
89
|
-
loadingLabel="Adding passkey"
|
|
90
|
-
aria-label="Add a passkey"
|
|
91
|
-
className="ml-auto"
|
|
92
|
-
>
|
|
93
|
-
<Plus className="size-4" aria-hidden />
|
|
94
|
-
</Button>
|
|
95
|
-
</CardHeader>
|
|
96
|
-
|
|
97
|
-
{error ? (
|
|
98
|
-
<p role="alert" className="text-destructive-text border-b px-4 py-3 text-sm">
|
|
99
|
-
{error}
|
|
100
|
-
</p>
|
|
101
|
-
) : null}
|
|
102
|
-
|
|
103
|
-
{isLoading ? (
|
|
104
|
-
<Spinner presentation="centered" size="sm" label="Loading passkeys" />
|
|
105
|
-
) : passkeys.length === 0 ? (
|
|
106
|
-
<div className="text-muted-foreground flex items-center gap-3 p-4 text-sm">
|
|
107
|
-
<KeyRound className="size-4 shrink-0" aria-hidden />
|
|
108
|
-
<span>No passkeys registered. Add one to sign in faster.</span>
|
|
109
|
-
</div>
|
|
110
|
-
) : (
|
|
111
|
-
<ul className="divide-border divide-y" aria-label="Registered passkeys">
|
|
112
|
-
{passkeys.map((pk) => {
|
|
113
|
-
const rowError = actionError?.passkeyId === pk.id ? actionError.message : null;
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<li key={pk.id} className="flex flex-col gap-2 px-4 py-3 text-sm">
|
|
117
|
-
<div className="flex items-center justify-between gap-4">
|
|
118
|
-
<div className="flex min-w-0 items-center gap-3">
|
|
119
|
-
<KeyRound className="text-muted-foreground size-4 shrink-0" aria-hidden />
|
|
120
|
-
<div className="min-w-0">
|
|
121
|
-
<span className="font-medium">{pk.name ?? "Passkey"}</span>
|
|
122
|
-
{pk.createdAt ? (
|
|
123
|
-
<span className="text-muted-foreground ml-2 text-xs">
|
|
124
|
-
Added {new Date(pk.createdAt).toLocaleDateString()}
|
|
125
|
-
</span>
|
|
126
|
-
) : null}
|
|
127
|
-
</div>
|
|
128
|
-
</div>
|
|
129
|
-
<button
|
|
130
|
-
type="button"
|
|
131
|
-
onClick={() => void handleDelete(pk.id)}
|
|
132
|
-
disabled={deletingId === pk.id}
|
|
133
|
-
aria-busy={deletingId === pk.id}
|
|
134
|
-
aria-label={`Remove passkey ${pk.name ?? pk.id}`}
|
|
135
|
-
className="hover:text-destructive-text text-muted-foreground focus-visible:ring-ring rounded-sm focus-visible:ring-2 focus-visible:outline-none disabled:opacity-50"
|
|
136
|
-
>
|
|
137
|
-
{deletingId === pk.id ? (
|
|
138
|
-
<Spinner size="sm" aria-hidden />
|
|
139
|
-
) : (
|
|
140
|
-
<Trash2 className="size-4" aria-hidden />
|
|
141
|
-
)}
|
|
142
|
-
</button>
|
|
143
|
-
</div>
|
|
144
|
-
{rowError ? (
|
|
145
|
-
<div
|
|
146
|
-
role="alert"
|
|
147
|
-
className="bg-destructive/5 text-destructive-text flex items-start justify-between gap-3 rounded-md px-3 py-2 text-sm"
|
|
148
|
-
>
|
|
149
|
-
<span>{rowError}</span>
|
|
150
|
-
<button
|
|
151
|
-
type="button"
|
|
152
|
-
onClick={() => setActionError(null)}
|
|
153
|
-
aria-label="Dismiss passkey error"
|
|
154
|
-
title="Dismiss error"
|
|
155
|
-
className="hover:text-destructive focus-visible:ring-ring -m-1 shrink-0 rounded-sm p-1 focus-visible:ring-2 focus-visible:outline-none"
|
|
156
|
-
>
|
|
157
|
-
<X className="size-4" aria-hidden />
|
|
158
|
-
</button>
|
|
159
|
-
</div>
|
|
160
|
-
) : null}
|
|
161
|
-
</li>
|
|
162
|
-
);
|
|
163
|
-
})}
|
|
164
|
-
</ul>
|
|
165
|
-
)}
|
|
166
|
-
</Card>
|
|
167
|
-
);
|
|
168
|
-
}
|