@bearmenu/ui 0.5.2 → 0.6.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/dist/{chunk-ERMIA7NP.js → chunk-7PPCH42H.js} +3 -3
- package/dist/{chunk-HLQ2IRE7.js → chunk-BAZFVSSG.js} +3 -3
- package/dist/chunk-D5QHEHXJ.js +26 -0
- package/dist/{chunk-UISA4CIG.js → chunk-N2KNBPAQ.js} +2 -2
- package/dist/{chunk-7H54PTVH.js → chunk-PSR7N36A.js} +38 -10
- package/dist/chunk-RIDXFY6M.js +38 -0
- package/dist/chunk-UUJVXN2O.js +82 -0
- package/dist/chunk-WKMLZDUT.js +122 -0
- package/dist/chunk-XK2ZDQVE.js +122 -0
- package/dist/{chunk-4KOMHDZD.js → chunk-Y5D5LP4A.js} +18 -3
- package/dist/chunk-YJ6OBKZJ.js +32 -0
- package/dist/{chunk-ESO2EM3L.js → chunk-ZBBUKXM5.js} +2 -2
- package/dist/chunk-ZFNOCP6W.js +106 -0
- package/dist/components/accordion.js +11 -5
- package/dist/components/alert-dialog.js +5 -3
- package/dist/components/alert.d.ts +5 -2
- package/dist/components/alert.js +25 -9
- package/dist/components/avatar.d.ts +17 -2
- package/dist/components/avatar.js +91 -12
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/breadcrumb.js +5 -2
- package/dist/components/button-group.d.ts +14 -0
- package/dist/components/button-group.js +39 -0
- package/dist/components/button.d.ts +4 -2
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.js +3 -3
- package/dist/components/card.d.ts +13 -5
- package/dist/components/card.js +2 -2
- package/dist/components/carousel.d.ts +20 -3
- package/dist/components/carousel.js +72 -12
- package/dist/components/checkbox.js +1 -1
- package/dist/components/circular-progress.d.ts +16 -0
- package/dist/components/circular-progress.js +105 -0
- package/dist/components/combobox.d.ts +27 -0
- package/dist/components/combobox.js +102 -0
- package/dist/components/command.d.ts +29 -16
- package/dist/components/command.js +2 -2
- package/dist/components/currency-input.d.ts +1 -0
- package/dist/components/currency-input.js +73 -52
- package/dist/components/date-input.d.ts +10 -0
- package/dist/components/date-input.js +40 -0
- package/dist/components/description-list.d.ts +15 -0
- package/dist/components/description-list.js +67 -0
- package/dist/components/dialog.d.ts +13 -2
- package/dist/components/dialog.js +1 -1
- package/dist/components/drawer.d.ts +5 -2
- package/dist/components/drawer.js +47 -9
- package/dist/components/dropdown-menu.d.ts +7 -2
- package/dist/components/dropdown-menu.js +22 -6
- package/dist/components/empty-state.js +2 -2
- package/dist/components/field.d.ts +22 -0
- package/dist/components/field.js +130 -0
- package/dist/components/form.d.ts +2 -1
- package/dist/components/form.js +11 -1
- package/dist/components/haptic-button.js +3 -3
- package/dist/components/input-group.d.ts +18 -0
- package/dist/components/input-group.js +92 -0
- package/dist/components/input.d.ts +8 -2
- package/dist/components/input.js +1 -1
- package/dist/components/item.d.ts +23 -0
- package/dist/components/item.js +120 -0
- package/dist/components/kbd.d.ts +13 -0
- package/dist/components/kbd.js +49 -0
- package/dist/components/lazy-markdown.d.ts +3 -1
- package/dist/components/lazy-markdown.js +7 -7
- package/dist/components/link-button.js +1 -1
- package/dist/components/multi-select-combobox.js +92 -83
- package/dist/components/multi-select-menu.d.ts +0 -4
- package/dist/components/multi-select-menu.js +28 -17
- package/dist/components/native-select.d.ts +12 -0
- package/dist/components/native-select.js +49 -0
- package/dist/components/pagination.d.ts +5 -2
- package/dist/components/pagination.js +66 -7
- package/dist/components/place-about-tab.js +4 -4
- package/dist/components/place-card.js +1 -1
- package/dist/components/place-details-mobile.js +4 -4
- package/dist/components/popover.d.ts +7 -1
- package/dist/components/popover.js +1 -1
- package/dist/components/progress.d.ts +5 -1
- package/dist/components/progress.js +17 -3
- package/dist/components/radio-group.js +12 -3
- package/dist/components/rating.d.ts +2 -0
- package/dist/components/rating.js +88 -70
- package/dist/components/search-bar.d.ts +1 -2
- package/dist/components/search-bar.js +37 -60
- package/dist/components/searchable-select.js +76 -72
- package/dist/components/select.d.ts +5 -1
- package/dist/components/select.js +30 -14
- package/dist/components/selection-bar.d.ts +11 -0
- package/dist/components/selection-bar.js +65 -0
- package/dist/components/separator.js +3 -2
- package/dist/components/sheet.d.ts +1 -1
- package/dist/components/sheet.js +1 -1
- package/dist/components/skeleton-card.js +1 -1
- package/dist/components/skeleton.js +1 -1
- package/dist/components/slider.js +21 -4
- package/dist/components/sliding-panels.js +9 -6
- package/dist/components/spinner.d.ts +17 -0
- package/dist/components/spinner.js +50 -0
- package/dist/components/stepper.d.ts +18 -0
- package/dist/components/stepper.js +199 -0
- package/dist/components/switch.js +4 -3
- package/dist/components/table.d.ts +23 -5
- package/dist/components/table.js +121 -35
- package/dist/components/tabs.d.ts +3 -1
- package/dist/components/tabs.js +37 -9
- package/dist/components/tag-group.d.ts +9 -0
- package/dist/components/tag-group.js +26 -0
- package/dist/components/tag.d.ts +17 -0
- package/dist/components/tag.js +61 -0
- package/dist/components/text.d.ts +3 -3
- package/dist/components/text.js +1 -1
- package/dist/components/textarea.js +2 -1
- package/dist/components/toast.js +1 -1
- package/dist/components/toaster.js +1 -1
- package/dist/components/toggle-group.js +1 -1
- package/dist/components/toggle.js +1 -1
- package/dist/components/tooltip.d.ts +3 -2
- package/dist/components/tooltip.js +16 -2
- package/package.json +44 -26
- package/src/components/accordion.tsx +11 -6
- package/src/components/alert-dialog.tsx +4 -2
- package/src/components/alert.tsx +20 -8
- package/src/components/avatar.tsx +106 -13
- package/src/components/badge.tsx +12 -11
- package/src/components/breadcrumb.tsx +5 -2
- package/src/components/button-group.stories.tsx +52 -0
- package/src/components/button-group.tsx +40 -0
- package/src/components/button.test.tsx +174 -0
- package/src/components/button.tsx +42 -19
- package/src/components/calendar.tsx +2 -2
- package/src/components/card.tsx +55 -13
- package/src/components/carousel.tsx +85 -13
- package/src/components/checkbox.test.tsx +58 -0
- package/src/components/checkbox.tsx +8 -3
- package/src/components/circular-progress.stories.tsx +39 -0
- package/src/components/circular-progress.tsx +102 -0
- package/src/components/collapsible.stories.tsx +7 -3
- package/src/components/combobox.stories.tsx +82 -0
- package/src/components/combobox.test.tsx +118 -0
- package/src/components/combobox.tsx +136 -0
- package/src/components/command.test.tsx +91 -0
- package/src/components/command.tsx +38 -15
- package/src/components/currency-input.test.tsx +63 -0
- package/src/components/currency-input.tsx +84 -66
- package/src/components/date-input.stories.tsx +31 -0
- package/src/components/date-input.tsx +43 -0
- package/src/components/description-list.stories.tsx +62 -0
- package/src/components/description-list.tsx +68 -0
- package/src/components/dialog.test.tsx +84 -0
- package/src/components/dialog.tsx +64 -24
- package/src/components/drawer.tsx +72 -23
- package/src/components/dropdown-menu.test.tsx +180 -0
- package/src/components/dropdown-menu.tsx +31 -6
- package/src/components/field.stories.tsx +36 -0
- package/src/components/field.test.tsx +157 -0
- package/src/components/field.tsx +146 -0
- package/src/components/form.stories.tsx +3 -2
- package/src/components/form.test.tsx +95 -0
- package/src/components/form.tsx +9 -0
- package/src/components/haptic-button.tsx +1 -1
- package/src/components/input-group.stories.tsx +61 -0
- package/src/components/input-group.tsx +99 -0
- package/src/components/input.test.tsx +103 -0
- package/src/components/input.tsx +24 -7
- package/src/components/item.stories.tsx +52 -0
- package/src/components/item.tsx +115 -0
- package/src/components/kbd.stories.tsx +33 -0
- package/src/components/kbd.tsx +46 -0
- package/src/components/lazy-markdown.tsx +10 -7
- package/src/components/multi-select-combobox.test.tsx +101 -0
- package/src/components/multi-select-combobox.tsx +85 -82
- package/src/components/multi-select-menu.tsx +49 -41
- package/src/components/native-select.stories.tsx +52 -0
- package/src/components/native-select.tsx +45 -0
- package/src/components/pagination.tsx +64 -5
- package/src/components/popover.tsx +66 -3
- package/src/components/progress.tsx +27 -4
- package/src/components/radio-group.test.tsx +64 -0
- package/src/components/radio-group.tsx +11 -3
- package/src/components/rating.test.tsx +112 -0
- package/src/components/rating.tsx +97 -82
- package/src/components/search-bar.stories.tsx +1 -32
- package/src/components/search-bar.tsx +22 -59
- package/src/components/searchable-select.tsx +69 -73
- package/src/components/select.tsx +34 -19
- package/src/components/selection-bar.stories.tsx +37 -0
- package/src/components/selection-bar.tsx +65 -0
- package/src/components/separator.tsx +5 -2
- package/src/components/sheet.tsx +1 -1
- package/src/components/skeleton.tsx +2 -2
- package/src/components/slider.test.tsx +45 -0
- package/src/components/slider.tsx +31 -12
- package/src/components/sliding-panels.tsx +8 -5
- package/src/components/spinner.stories.tsx +35 -0
- package/src/components/spinner.tsx +50 -0
- package/src/components/stepper.stories.tsx +71 -0
- package/src/components/stepper.tsx +220 -0
- package/src/components/switch.test.tsx +52 -0
- package/src/components/switch.tsx +4 -3
- package/src/components/table.tsx +140 -35
- package/src/components/tabs.test.tsx +107 -0
- package/src/components/tabs.tsx +70 -23
- package/src/components/tag-group.stories.tsx +39 -0
- package/src/components/tag-group.tsx +28 -0
- package/src/components/tag.stories.tsx +50 -0
- package/src/components/tag.tsx +63 -0
- package/src/components/text.test.tsx +1 -1
- package/src/components/text.tsx +19 -3
- package/src/components/textarea.test.tsx +48 -0
- package/src/components/textarea.tsx +2 -1
- package/src/components/toast.tsx +4 -3
- package/src/components/toggle.tsx +2 -2
- package/src/components/tooltip.tsx +12 -2
- package/src/globals.css +126 -90
- package/dist/chunk-2BMJRXQX.js +0 -80
- package/dist/chunk-4AVS6CV4.js +0 -89
- package/dist/chunk-4UO73SKC.js +0 -26
- package/dist/chunk-CW24PLDW.js +0 -24
- package/dist/chunk-IQASCH2V.js +0 -25
- package/dist/chunk-NKHH23YK.js +0 -31
- package/dist/chunk-QJ54Z7TC.js +0 -83
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Stepper } from "./stepper";
|
|
4
|
+
import { Button } from "./button";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/Stepper",
|
|
8
|
+
component: Stepper,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
} satisfies Meta<typeof Stepper>;
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof meta>;
|
|
14
|
+
|
|
15
|
+
const steps = [
|
|
16
|
+
{ id: "account", title: "Account" },
|
|
17
|
+
{ id: "details", title: "Details" },
|
|
18
|
+
{ id: "review", title: "Review" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const Default: Story = {
|
|
22
|
+
render: () => (
|
|
23
|
+
<div className="w-[36rem]">
|
|
24
|
+
<Stepper steps={steps} activeStep={1} />
|
|
25
|
+
</div>
|
|
26
|
+
),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Complete: Story = {
|
|
30
|
+
render: () => (
|
|
31
|
+
<div className="w-[36rem]">
|
|
32
|
+
<Stepper steps={steps} activeStep={steps.length} />
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Interactive: Story = {
|
|
38
|
+
render: () => {
|
|
39
|
+
const [active, setActive] = useState(0);
|
|
40
|
+
const [furthest, setFurthest] = useState(0);
|
|
41
|
+
return (
|
|
42
|
+
<div className="w-[36rem] space-y-6">
|
|
43
|
+
<Stepper
|
|
44
|
+
steps={steps}
|
|
45
|
+
activeStep={active}
|
|
46
|
+
furthestStep={furthest}
|
|
47
|
+
onStepClick={(i) => setActive(i)}
|
|
48
|
+
/>
|
|
49
|
+
<div className="flex gap-2">
|
|
50
|
+
<Button
|
|
51
|
+
variant="outline"
|
|
52
|
+
disabled={active === 0}
|
|
53
|
+
onClick={() => setActive((a) => Math.max(0, a - 1))}
|
|
54
|
+
>
|
|
55
|
+
Back
|
|
56
|
+
</Button>
|
|
57
|
+
<Button
|
|
58
|
+
disabled={active === steps.length - 1}
|
|
59
|
+
onClick={() => {
|
|
60
|
+
const next = Math.min(steps.length - 1, active + 1);
|
|
61
|
+
setActive(next);
|
|
62
|
+
setFurthest((f) => Math.max(f, next));
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
Next
|
|
66
|
+
</Button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
export interface StepperStep {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type StepStatus = "complete" | "current" | "upcoming";
|
|
13
|
+
|
|
14
|
+
type StepperProps = React.HTMLAttributes<HTMLElement> & {
|
|
15
|
+
steps: StepperStep[];
|
|
16
|
+
activeStep: number;
|
|
17
|
+
/** Highest step the user has reached (enables clicking back). Defaults to activeStep. */
|
|
18
|
+
furthestStep?: number;
|
|
19
|
+
/** When provided, steps render as buttons. */
|
|
20
|
+
onStepClick?: (index: number) => void;
|
|
21
|
+
ariaLabel?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const Stepper = React.forwardRef<HTMLElement, StepperProps>(
|
|
25
|
+
(
|
|
26
|
+
{
|
|
27
|
+
className,
|
|
28
|
+
steps,
|
|
29
|
+
activeStep,
|
|
30
|
+
furthestStep,
|
|
31
|
+
onStepClick,
|
|
32
|
+
ariaLabel = "Progress",
|
|
33
|
+
...props
|
|
34
|
+
},
|
|
35
|
+
ref,
|
|
36
|
+
) => {
|
|
37
|
+
const reachable = furthestStep ?? activeStep;
|
|
38
|
+
const isInteractive = typeof onStepClick === "function";
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<nav
|
|
42
|
+
ref={ref as React.Ref<HTMLElement>}
|
|
43
|
+
aria-label={ariaLabel}
|
|
44
|
+
className={cn("w-full", className)}
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
{/* Desktop: circle + label, animated connectors */}
|
|
48
|
+
<ol className="hidden md:flex w-full items-center" data-slot="stepper">
|
|
49
|
+
{steps.map((step, i) => {
|
|
50
|
+
const status: StepStatus =
|
|
51
|
+
i < activeStep ? "complete" : i === activeStep ? "current" : "upcoming";
|
|
52
|
+
const isLast = i === steps.length - 1;
|
|
53
|
+
const canClick = isInteractive && i <= reachable;
|
|
54
|
+
|
|
55
|
+
const Item = canClick ? "button" : "div";
|
|
56
|
+
const itemProps = canClick
|
|
57
|
+
? {
|
|
58
|
+
type: "button" as const,
|
|
59
|
+
onClick: () => onStepClick?.(i),
|
|
60
|
+
disabled: !canClick,
|
|
61
|
+
}
|
|
62
|
+
: {};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<li key={step.id} className="contents" data-status={status}>
|
|
66
|
+
<Item
|
|
67
|
+
{...itemProps}
|
|
68
|
+
aria-current={status === "current" ? "step" : undefined}
|
|
69
|
+
className={cn(
|
|
70
|
+
"group inline-flex items-center gap-2 rounded-full text-sm transition-opacity",
|
|
71
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
72
|
+
canClick && status !== "current" && "hover:opacity-80",
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
<StepCircle index={i} status={status} />
|
|
76
|
+
<span
|
|
77
|
+
className={cn(
|
|
78
|
+
"whitespace-nowrap transition-colors",
|
|
79
|
+
status === "current" && "font-semibold text-foreground",
|
|
80
|
+
status === "complete" && "font-medium text-foreground/80",
|
|
81
|
+
status === "upcoming" && "text-foreground/40",
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
{step.title}
|
|
85
|
+
</span>
|
|
86
|
+
</Item>
|
|
87
|
+
|
|
88
|
+
{!isLast && <StepConnector filled={i < activeStep} />}
|
|
89
|
+
</li>
|
|
90
|
+
);
|
|
91
|
+
})}
|
|
92
|
+
</ol>
|
|
93
|
+
|
|
94
|
+
{/* Mobile: numeric dots + active label below */}
|
|
95
|
+
<div className="md:hidden">
|
|
96
|
+
<ol className="flex items-center gap-1.5" role="list">
|
|
97
|
+
{steps.map((step, i) => {
|
|
98
|
+
const status: StepStatus =
|
|
99
|
+
i < activeStep ? "complete" : i === activeStep ? "current" : "upcoming";
|
|
100
|
+
const canClick = isInteractive && i <= reachable;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<li key={step.id} className="flex-1">
|
|
104
|
+
{canClick ? (
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
onClick={() => onStepClick?.(i)}
|
|
108
|
+
aria-label={`${i + 1}. ${step.title}`}
|
|
109
|
+
aria-current={status === "current" ? "step" : undefined}
|
|
110
|
+
className={cn(
|
|
111
|
+
"block w-full h-1.5 rounded-full transition-colors",
|
|
112
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
113
|
+
status === "current" && "bg-foreground",
|
|
114
|
+
status === "complete" && "bg-foreground/50",
|
|
115
|
+
status === "upcoming" && "bg-foreground/15",
|
|
116
|
+
)}
|
|
117
|
+
/>
|
|
118
|
+
) : (
|
|
119
|
+
<div
|
|
120
|
+
aria-label={`${i + 1}. ${step.title}`}
|
|
121
|
+
aria-current={status === "current" ? "step" : undefined}
|
|
122
|
+
className={cn(
|
|
123
|
+
"block w-full h-1.5 rounded-full transition-colors",
|
|
124
|
+
status === "current" && "bg-foreground",
|
|
125
|
+
status === "complete" && "bg-foreground/50",
|
|
126
|
+
status === "upcoming" && "bg-foreground/15",
|
|
127
|
+
)}
|
|
128
|
+
/>
|
|
129
|
+
)}
|
|
130
|
+
</li>
|
|
131
|
+
);
|
|
132
|
+
})}
|
|
133
|
+
</ol>
|
|
134
|
+
<p className="mt-3 text-xs uppercase tracking-[0.12em] text-foreground/40">
|
|
135
|
+
{`${activeStep + 1} / ${steps.length}`}
|
|
136
|
+
</p>
|
|
137
|
+
<p className="mt-1 text-base font-semibold text-foreground">
|
|
138
|
+
{steps[activeStep]?.title}
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
</nav>
|
|
142
|
+
);
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
Stepper.displayName = "Stepper";
|
|
146
|
+
|
|
147
|
+
function StepCircle({ index, status }: { index: number; status: StepStatus }) {
|
|
148
|
+
const isFilled = status !== "upcoming";
|
|
149
|
+
const isComplete = status === "complete";
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<span
|
|
153
|
+
aria-hidden="true"
|
|
154
|
+
className="relative inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full"
|
|
155
|
+
>
|
|
156
|
+
{/* Outline (visible when upcoming, fades when filled) */}
|
|
157
|
+
<span
|
|
158
|
+
className={cn(
|
|
159
|
+
"absolute inset-0 rounded-full border transition-opacity duration-300",
|
|
160
|
+
isFilled ? "opacity-0 border-transparent" : "opacity-100 border-foreground/25",
|
|
161
|
+
)}
|
|
162
|
+
/>
|
|
163
|
+
{/* Filled background — scales in from center, synced with check stroke */}
|
|
164
|
+
<span
|
|
165
|
+
className={cn(
|
|
166
|
+
"absolute inset-0 rounded-full bg-foreground origin-center transition-transform duration-300 ease-out",
|
|
167
|
+
isFilled ? "scale-100" : "scale-0",
|
|
168
|
+
)}
|
|
169
|
+
/>
|
|
170
|
+
{/* Number (visible when not complete) */}
|
|
171
|
+
<span
|
|
172
|
+
className={cn(
|
|
173
|
+
"relative text-xs font-semibold transition-opacity duration-200",
|
|
174
|
+
isComplete ? "opacity-0" : "opacity-100",
|
|
175
|
+
status === "current" ? "text-background" : "text-foreground/40",
|
|
176
|
+
)}
|
|
177
|
+
>
|
|
178
|
+
{index + 1}
|
|
179
|
+
</span>
|
|
180
|
+
{/* Animated check — short stroke first, then long stroke */}
|
|
181
|
+
<svg
|
|
182
|
+
className="pointer-events-none absolute inset-0 m-auto h-3.5 w-3.5 text-background"
|
|
183
|
+
viewBox="0 0 24 24"
|
|
184
|
+
fill="none"
|
|
185
|
+
stroke="currentColor"
|
|
186
|
+
strokeWidth="3"
|
|
187
|
+
strokeLinecap="round"
|
|
188
|
+
strokeLinejoin="round"
|
|
189
|
+
>
|
|
190
|
+
<path
|
|
191
|
+
d="M5 12 L10 17 L20 7"
|
|
192
|
+
pathLength={1}
|
|
193
|
+
strokeDasharray="1"
|
|
194
|
+
strokeDashoffset={isComplete ? 0 : 1}
|
|
195
|
+
style={{
|
|
196
|
+
transition: isComplete
|
|
197
|
+
? "stroke-dashoffset 450ms cubic-bezier(0.65, 0, 0.35, 1) 180ms"
|
|
198
|
+
: "stroke-dashoffset 150ms ease-in",
|
|
199
|
+
}}
|
|
200
|
+
/>
|
|
201
|
+
</svg>
|
|
202
|
+
</span>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function StepConnector({ filled }: { filled: boolean }) {
|
|
207
|
+
return (
|
|
208
|
+
<div
|
|
209
|
+
aria-hidden="true"
|
|
210
|
+
className="relative h-1 flex-1 mx-3 overflow-hidden rounded-full bg-foreground/10"
|
|
211
|
+
>
|
|
212
|
+
<div
|
|
213
|
+
className="absolute inset-y-0 left-0 rounded-full bg-foreground/50 transition-[width] duration-500 ease-out"
|
|
214
|
+
style={{ width: filled ? "100%" : "0%" }}
|
|
215
|
+
/>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export { Stepper };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Switch } from "./switch";
|
|
5
|
+
|
|
6
|
+
describe("Switch", () => {
|
|
7
|
+
it("renders unchecked by default", () => {
|
|
8
|
+
render(<Switch aria-label="notifications" />);
|
|
9
|
+
expect(screen.getByRole("switch")).toHaveAttribute("data-state", "unchecked");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("renders checked when prop is set", () => {
|
|
13
|
+
render(<Switch aria-label="x" checked onCheckedChange={() => {}} />);
|
|
14
|
+
expect(screen.getByRole("switch")).toHaveAttribute("data-state", "checked");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("toggles on click", async () => {
|
|
18
|
+
const user = userEvent.setup();
|
|
19
|
+
const onCheckedChange = vi.fn();
|
|
20
|
+
render(<Switch aria-label="x" onCheckedChange={onCheckedChange} />);
|
|
21
|
+
await user.click(screen.getByRole("switch"));
|
|
22
|
+
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("ignores clicks when disabled", async () => {
|
|
26
|
+
const user = userEvent.setup();
|
|
27
|
+
const onCheckedChange = vi.fn();
|
|
28
|
+
render(<Switch aria-label="x" disabled onCheckedChange={onCheckedChange} />);
|
|
29
|
+
await user.click(screen.getByRole("switch"));
|
|
30
|
+
expect(onCheckedChange).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("toggles via Space key when focused", async () => {
|
|
34
|
+
const user = userEvent.setup();
|
|
35
|
+
const onCheckedChange = vi.fn();
|
|
36
|
+
render(<Switch aria-label="x" onCheckedChange={onCheckedChange} />);
|
|
37
|
+
const sw = screen.getByRole("switch");
|
|
38
|
+
sw.focus();
|
|
39
|
+
await user.keyboard(" ");
|
|
40
|
+
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("supports aria-invalid", () => {
|
|
44
|
+
render(<Switch aria-label="x" aria-invalid />);
|
|
45
|
+
expect(screen.getByRole("switch")).toHaveAttribute("aria-invalid", "true");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("merges className", () => {
|
|
49
|
+
render(<Switch aria-label="x" className="custom" />);
|
|
50
|
+
expect(screen.getByRole("switch").className).toContain("custom");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -9,16 +9,17 @@ const Switch = React.forwardRef<
|
|
|
9
9
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
|
|
10
10
|
>(({ className, ...props }, ref) => (
|
|
11
11
|
<SwitchPrimitive.Root
|
|
12
|
+
ref={ref}
|
|
13
|
+
data-slot="switch"
|
|
12
14
|
className={cn(
|
|
13
|
-
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-
|
|
15
|
+
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input aria-invalid:ring-2 aria-invalid:ring-destructive/40",
|
|
14
16
|
className
|
|
15
17
|
)}
|
|
16
18
|
{...props}
|
|
17
|
-
ref={ref}
|
|
18
19
|
>
|
|
19
20
|
<SwitchPrimitive.Thumb
|
|
20
21
|
className={cn(
|
|
21
|
-
"pointer-events-none block
|
|
22
|
+
"pointer-events-none block size-5 rounded-full bg-background shadow-md ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 data-[state=checked]:bg-primary-foreground"
|
|
22
23
|
)}
|
|
23
24
|
/>
|
|
24
25
|
</SwitchPrimitive.Root>
|
package/src/components/table.tsx
CHANGED
|
@@ -1,20 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import * as React from "react";
|
|
4
|
+
import { ChevronDown, ChevronUp, ChevronsUpDown } from "lucide-react";
|
|
2
5
|
import { cn } from "../lib/utils";
|
|
3
6
|
|
|
4
|
-
const Table = React.forwardRef<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
const Table = React.forwardRef<
|
|
8
|
+
HTMLTableElement,
|
|
9
|
+
React.HTMLAttributes<HTMLTableElement> & { containerClassName?: string }
|
|
10
|
+
>(({ className, containerClassName, ...props }, ref) => (
|
|
11
|
+
<div className={cn("relative w-full overflow-auto rounded-xl border border-border", containerClassName)}>
|
|
12
|
+
<table
|
|
13
|
+
ref={ref}
|
|
14
|
+
data-slot="table"
|
|
15
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
</div>
|
|
19
|
+
));
|
|
11
20
|
Table.displayName = "Table";
|
|
12
21
|
|
|
13
22
|
const TableHeader = React.forwardRef<
|
|
14
23
|
HTMLTableSectionElement,
|
|
15
|
-
React.HTMLAttributes<HTMLTableSectionElement>
|
|
16
|
-
>(({ className, ...props }, ref) => (
|
|
17
|
-
<thead
|
|
24
|
+
React.HTMLAttributes<HTMLTableSectionElement> & { sticky?: boolean }
|
|
25
|
+
>(({ className, sticky, ...props }, ref) => (
|
|
26
|
+
<thead
|
|
27
|
+
ref={ref}
|
|
28
|
+
data-slot="table-header"
|
|
29
|
+
className={cn(
|
|
30
|
+
"bg-muted/50 [&_tr]:border-b [&_tr]:border-border",
|
|
31
|
+
sticky && "sticky top-0 z-10",
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
18
36
|
));
|
|
19
37
|
TableHeader.displayName = "TableHeader";
|
|
20
38
|
|
|
@@ -22,7 +40,12 @@ const TableBody = React.forwardRef<
|
|
|
22
40
|
HTMLTableSectionElement,
|
|
23
41
|
React.HTMLAttributes<HTMLTableSectionElement>
|
|
24
42
|
>(({ className, ...props }, ref) => (
|
|
25
|
-
<tbody
|
|
43
|
+
<tbody
|
|
44
|
+
ref={ref}
|
|
45
|
+
data-slot="table-body"
|
|
46
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
26
49
|
));
|
|
27
50
|
TableBody.displayName = "TableBody";
|
|
28
51
|
|
|
@@ -32,39 +55,88 @@ const TableFooter = React.forwardRef<
|
|
|
32
55
|
>(({ className, ...props }, ref) => (
|
|
33
56
|
<tfoot
|
|
34
57
|
ref={ref}
|
|
35
|
-
|
|
58
|
+
data-slot="table-footer"
|
|
59
|
+
className={cn("border-t border-border bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
|
36
60
|
{...props}
|
|
37
61
|
/>
|
|
38
62
|
));
|
|
39
63
|
TableFooter.displayName = "TableFooter";
|
|
40
64
|
|
|
41
|
-
const TableRow = React.forwardRef<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"border-b border-border/50 transition-all duration-300 hover:bg-muted/30 hover:backdrop-blur-sm data-[state=selected]:bg-muted/50",
|
|
47
|
-
className
|
|
48
|
-
)}
|
|
49
|
-
{...props}
|
|
50
|
-
/>
|
|
51
|
-
)
|
|
52
|
-
);
|
|
53
|
-
TableRow.displayName = "TableRow";
|
|
54
|
-
|
|
55
|
-
const TableHead = React.forwardRef<
|
|
56
|
-
HTMLTableCellElement,
|
|
57
|
-
React.ThHTMLAttributes<HTMLTableCellElement>
|
|
58
|
-
>(({ className, ...props }, ref) => (
|
|
59
|
-
<th
|
|
65
|
+
const TableRow = React.forwardRef<
|
|
66
|
+
HTMLTableRowElement,
|
|
67
|
+
React.HTMLAttributes<HTMLTableRowElement> & { interactive?: boolean }
|
|
68
|
+
>(({ className, interactive, ...props }, ref) => (
|
|
69
|
+
<tr
|
|
60
70
|
ref={ref}
|
|
71
|
+
data-slot="table-row"
|
|
61
72
|
className={cn(
|
|
62
|
-
"
|
|
73
|
+
"border-b border-border transition-colors data-[state=selected]:bg-muted",
|
|
74
|
+
interactive && "cursor-pointer hover:bg-muted/50",
|
|
63
75
|
className
|
|
64
76
|
)}
|
|
65
77
|
{...props}
|
|
66
78
|
/>
|
|
67
79
|
));
|
|
80
|
+
TableRow.displayName = "TableRow";
|
|
81
|
+
|
|
82
|
+
type SortDirection = "asc" | "desc" | "none";
|
|
83
|
+
|
|
84
|
+
type TableHeadProps = React.ThHTMLAttributes<HTMLTableCellElement> & {
|
|
85
|
+
sortable?: boolean;
|
|
86
|
+
sortDirection?: SortDirection;
|
|
87
|
+
onSort?: () => void;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const TableHead = React.forwardRef<HTMLTableCellElement, TableHeadProps>(
|
|
91
|
+
({ className, sortable, sortDirection = "none", onSort, children, ...props }, ref) => {
|
|
92
|
+
if (!sortable) {
|
|
93
|
+
return (
|
|
94
|
+
<th
|
|
95
|
+
ref={ref}
|
|
96
|
+
data-slot="table-head"
|
|
97
|
+
className={cn(
|
|
98
|
+
"h-11 px-4 text-left align-middle text-xs font-semibold uppercase tracking-wide text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
|
99
|
+
className
|
|
100
|
+
)}
|
|
101
|
+
{...props}
|
|
102
|
+
>
|
|
103
|
+
{children}
|
|
104
|
+
</th>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const ariaSort =
|
|
109
|
+
sortDirection === "asc" ? "ascending" : sortDirection === "desc" ? "descending" : "none";
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<th
|
|
113
|
+
ref={ref}
|
|
114
|
+
data-slot="table-head"
|
|
115
|
+
aria-sort={ariaSort}
|
|
116
|
+
className={cn(
|
|
117
|
+
"h-11 px-4 text-left align-middle text-xs font-semibold uppercase tracking-wide text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
|
118
|
+
className
|
|
119
|
+
)}
|
|
120
|
+
{...props}
|
|
121
|
+
>
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
onClick={onSort}
|
|
125
|
+
className="inline-flex items-center gap-1 rounded-sm text-inherit transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
126
|
+
>
|
|
127
|
+
{children}
|
|
128
|
+
{sortDirection === "asc" ? (
|
|
129
|
+
<ChevronUp className="size-3.5" aria-hidden />
|
|
130
|
+
) : sortDirection === "desc" ? (
|
|
131
|
+
<ChevronDown className="size-3.5" aria-hidden />
|
|
132
|
+
) : (
|
|
133
|
+
<ChevronsUpDown className="size-3.5 opacity-50" aria-hidden />
|
|
134
|
+
)}
|
|
135
|
+
</button>
|
|
136
|
+
</th>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
);
|
|
68
140
|
TableHead.displayName = "TableHead";
|
|
69
141
|
|
|
70
142
|
const TableCell = React.forwardRef<
|
|
@@ -73,7 +145,8 @@ const TableCell = React.forwardRef<
|
|
|
73
145
|
>(({ className, ...props }, ref) => (
|
|
74
146
|
<td
|
|
75
147
|
ref={ref}
|
|
76
|
-
|
|
148
|
+
data-slot="table-cell"
|
|
149
|
+
className={cn("p-4 align-middle text-sm [&:has([role=checkbox])]:pr-0", className)}
|
|
77
150
|
{...props}
|
|
78
151
|
/>
|
|
79
152
|
));
|
|
@@ -83,8 +156,40 @@ const TableCaption = React.forwardRef<
|
|
|
83
156
|
HTMLTableCaptionElement,
|
|
84
157
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
|
85
158
|
>(({ className, ...props }, ref) => (
|
|
86
|
-
<caption
|
|
159
|
+
<caption
|
|
160
|
+
ref={ref}
|
|
161
|
+
data-slot="table-caption"
|
|
162
|
+
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
163
|
+
{...props}
|
|
164
|
+
/>
|
|
87
165
|
));
|
|
88
166
|
TableCaption.displayName = "TableCaption";
|
|
89
167
|
|
|
90
|
-
|
|
168
|
+
const TableEmpty = ({
|
|
169
|
+
colSpan,
|
|
170
|
+
children = "No results.",
|
|
171
|
+
className,
|
|
172
|
+
...props
|
|
173
|
+
}: React.ComponentProps<"tr"> & { colSpan: number }) => (
|
|
174
|
+
<tr data-slot="table-empty" {...props}>
|
|
175
|
+
<td
|
|
176
|
+
colSpan={colSpan}
|
|
177
|
+
className={cn("h-24 text-center text-sm text-muted-foreground", className)}
|
|
178
|
+
>
|
|
179
|
+
{children}
|
|
180
|
+
</td>
|
|
181
|
+
</tr>
|
|
182
|
+
);
|
|
183
|
+
TableEmpty.displayName = "TableEmpty";
|
|
184
|
+
|
|
185
|
+
export {
|
|
186
|
+
Table,
|
|
187
|
+
TableHeader,
|
|
188
|
+
TableBody,
|
|
189
|
+
TableFooter,
|
|
190
|
+
TableHead,
|
|
191
|
+
TableRow,
|
|
192
|
+
TableCell,
|
|
193
|
+
TableCaption,
|
|
194
|
+
TableEmpty,
|
|
195
|
+
};
|