@ponti-studios/ui 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +27 -0
  2. package/dist/index.js +2118 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +130 -0
  5. package/src/animations.css +437 -0
  6. package/src/components/data-display/accordion.stories.tsx +57 -0
  7. package/src/components/data-display/accordion.tsx +65 -0
  8. package/src/components/data-display/filter-chip.tsx +41 -0
  9. package/src/components/data-display/index.ts +13 -0
  10. package/src/components/data-display/table.stories.tsx +75 -0
  11. package/src/components/data-display/table.tsx +84 -0
  12. package/src/components/feedback/alert.stories.tsx +51 -0
  13. package/src/components/feedback/alert.tsx +60 -0
  14. package/src/components/feedback/empty-state.tsx +66 -0
  15. package/src/components/feedback/index.ts +6 -0
  16. package/src/components/feedback/progress.stories.tsx +37 -0
  17. package/src/components/feedback/progress.tsx +30 -0
  18. package/src/components/feedback/spinner.stories.tsx +52 -0
  19. package/src/components/feedback/spinner.tsx +48 -0
  20. package/src/components/forms/calendar.stories.tsx +30 -0
  21. package/src/components/forms/calendar.tsx +67 -0
  22. package/src/components/forms/date-picker.stories.tsx +48 -0
  23. package/src/components/forms/date-picker.tsx +60 -0
  24. package/src/components/forms/field.stories.tsx +62 -0
  25. package/src/components/forms/field.tsx +65 -0
  26. package/src/components/forms/field.types.ts +7 -0
  27. package/src/components/forms/index.ts +24 -0
  28. package/src/components/forms/input.stories.tsx +47 -0
  29. package/src/components/forms/input.tsx +20 -0
  30. package/src/components/forms/radio-group.stories.tsx +48 -0
  31. package/src/components/forms/radio-group.tsx +38 -0
  32. package/src/components/forms/select.stories.tsx +88 -0
  33. package/src/components/forms/select.tsx +147 -0
  34. package/src/components/forms/slider.stories.tsx +67 -0
  35. package/src/components/forms/slider.tsx +25 -0
  36. package/src/components/forms/stepper.stories.tsx +111 -0
  37. package/src/components/forms/stepper.tsx +66 -0
  38. package/src/components/forms/switch.stories.tsx +144 -0
  39. package/src/components/forms/switch.tsx +36 -0
  40. package/src/components/forms/text-field.stories.tsx +87 -0
  41. package/src/components/forms/text-field.tsx +40 -0
  42. package/src/components/forms/text-field.types.ts +9 -0
  43. package/src/components/forms/textarea.stories.tsx +150 -0
  44. package/src/components/forms/textarea.tsx +18 -0
  45. package/src/components/layout/index.ts +2 -0
  46. package/src/components/layout/scroll-area.stories.tsx +83 -0
  47. package/src/components/layout/scroll-area.tsx +40 -0
  48. package/src/components/navigation/app-navigation.stories.tsx +81 -0
  49. package/src/components/navigation/app-navigation.tsx +155 -0
  50. package/src/components/navigation/index.ts +3 -0
  51. package/src/components/navigation/tabs.stories.tsx +69 -0
  52. package/src/components/navigation/tabs.tsx +49 -0
  53. package/src/components/overlays/alert-dialog.stories.tsx +45 -0
  54. package/src/components/overlays/alert-dialog.tsx +119 -0
  55. package/src/components/overlays/command.stories.tsx +65 -0
  56. package/src/components/overlays/command.tsx +130 -0
  57. package/src/components/overlays/dialog.stories.tsx +111 -0
  58. package/src/components/overlays/dialog.tsx +162 -0
  59. package/src/components/overlays/dropdown-menu.stories.tsx +79 -0
  60. package/src/components/overlays/dropdown-menu.tsx +214 -0
  61. package/src/components/overlays/index.ts +65 -0
  62. package/src/components/overlays/popover.stories.tsx +39 -0
  63. package/src/components/overlays/popover.tsx +40 -0
  64. package/src/components/overlays/sheet.stories.tsx +68 -0
  65. package/src/components/overlays/sheet.tsx +98 -0
  66. package/src/components/primitives/avatar.stories.tsx +71 -0
  67. package/src/components/primitives/avatar.tsx +132 -0
  68. package/src/components/primitives/badge.stories.tsx +33 -0
  69. package/src/components/primitives/badge.tsx +44 -0
  70. package/src/components/primitives/button.stories.tsx +113 -0
  71. package/src/components/primitives/button.tsx +95 -0
  72. package/src/components/primitives/button.types.ts +10 -0
  73. package/src/components/primitives/card.stories.tsx +59 -0
  74. package/src/components/primitives/card.tsx +69 -0
  75. package/src/components/primitives/index.ts +13 -0
  76. package/src/components/primitives/label.stories.tsx +47 -0
  77. package/src/components/primitives/label.tsx +21 -0
  78. package/src/constants/chart-colors.ts +39 -0
  79. package/src/env.d.ts +9 -0
  80. package/src/hooks/index.ts +6 -0
  81. package/src/hooks/sort.types.ts +12 -0
  82. package/src/hooks/use-api-client.ts +122 -0
  83. package/src/hooks/use-debounced-value.ts +12 -0
  84. package/src/hooks/use-filter-state.ts +68 -0
  85. package/src/hooks/use-media-query.ts +65 -0
  86. package/src/hooks/use-mobile.tsx +19 -0
  87. package/src/index.ts +25 -0
  88. package/src/lib/auth.ts +5 -0
  89. package/src/lib/clipboard.ts +8 -0
  90. package/src/lib/create-memory-storage.ts +24 -0
  91. package/src/lib/index.ts +4 -0
  92. package/src/lib/utils.ts +6 -0
  93. package/src/storybook/controls.ts +117 -0
  94. package/src/storybook/options.ts +52 -0
  95. package/src/styles.css +1052 -0
  96. package/src/tokens/color-systems.stories.tsx +133 -0
  97. package/src/tokens/colors.ts +156 -0
  98. package/src/tokens/foundations.stories.tsx +151 -0
  99. package/src/tokens/index.ts +16 -0
  100. package/src/tokens/layout.stories.tsx +118 -0
  101. package/src/tokens/motion.ts +51 -0
  102. package/src/tokens/radii.ts +10 -0
  103. package/src/tokens/shadows.ts +34 -0
  104. package/src/tokens/spacing.ts +30 -0
  105. package/src/tokens/typography.ts +42 -0
  106. package/src/translations/en.ts +34 -0
  107. package/src/translations/index.ts +32 -0
  108. package/src/types/upload.ts +42 -0
@@ -0,0 +1,84 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
6
+ ({ className, ...props }, ref) => (
7
+ <div className="w-full overflow-auto">
8
+ <table ref={ref} className={cn("body-3 w-full caption-bottom", className)} {...props} />
9
+ </div>
10
+ ),
11
+ );
12
+ Table.displayName = "Table";
13
+
14
+ const TableHeader = React.forwardRef<
15
+ HTMLTableSectionElement,
16
+ React.HTMLAttributes<HTMLTableSectionElement>
17
+ >(({ className, ...props }, ref) => (
18
+ <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
19
+ ));
20
+ TableHeader.displayName = "TableHeader";
21
+
22
+ const TableBody = React.forwardRef<
23
+ HTMLTableSectionElement,
24
+ React.HTMLAttributes<HTMLTableSectionElement>
25
+ >(({ className, ...props }, ref) => (
26
+ <tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
27
+ ));
28
+ TableBody.displayName = "TableBody";
29
+
30
+ const TableFooter = React.forwardRef<
31
+ HTMLTableSectionElement,
32
+ React.HTMLAttributes<HTMLTableSectionElement>
33
+ >(({ className, ...props }, ref) => (
34
+ <tfoot ref={ref} className={cn("bg-accent text-on-accent font-medium", className)} {...props} />
35
+ ));
36
+ TableFooter.displayName = "TableFooter";
37
+
38
+ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
39
+ ({ className, ...props }, ref) => (
40
+ <tr
41
+ ref={ref}
42
+ className={cn("data-[state=selected]:bg-inset border-default border-b", className)}
43
+ {...props}
44
+ />
45
+ ),
46
+ );
47
+ TableRow.displayName = "TableRow";
48
+
49
+ const TableHead = React.forwardRef<
50
+ HTMLTableCellElement,
51
+ React.ThHTMLAttributes<HTMLTableCellElement>
52
+ >(({ className, ...props }, ref) => (
53
+ <th
54
+ ref={ref}
55
+ className={cn(
56
+ "ui-eyebrow h-9 px-3 text-left align-middle [&:has([role=checkbox])]:pr-0",
57
+ className,
58
+ )}
59
+ {...props}
60
+ />
61
+ ));
62
+ TableHead.displayName = "TableHead";
63
+
64
+ const TableCell = React.forwardRef<
65
+ HTMLTableCellElement,
66
+ React.TdHTMLAttributes<HTMLTableCellElement>
67
+ >(({ className, ...props }, ref) => (
68
+ <td
69
+ ref={ref}
70
+ className={cn("px-3 py-2.5 align-middle [&:has([role=checkbox])]:pr-0", className)}
71
+ {...props}
72
+ />
73
+ ));
74
+ TableCell.displayName = "TableCell";
75
+
76
+ const TableCaption = React.forwardRef<
77
+ HTMLTableCaptionElement,
78
+ React.HTMLAttributes<HTMLTableCaptionElement>
79
+ >(({ className, ...props }, ref) => (
80
+ <caption ref={ref} className={cn("body-4 text-secondary mt-4", className)} {...props} />
81
+ ));
82
+ TableCaption.displayName = "TableCaption";
83
+
84
+ export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };
@@ -0,0 +1,51 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { AlertCircle, Info, Terminal } from "lucide-react";
3
+
4
+ import { Alert, AlertDescription, AlertTitle } from "./alert";
5
+
6
+ const meta = {
7
+ title: "Feedback/Alert",
8
+ component: Alert,
9
+ tags: ["autodocs"],
10
+ } satisfies Meta<typeof Alert>;
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ export const Default: Story = {
16
+ render: () => (
17
+ <Alert>
18
+ <Terminal className="size-4" />
19
+ <AlertTitle>Heads up!</AlertTitle>
20
+ <AlertDescription>You can add components to your app using the cli.</AlertDescription>
21
+ </Alert>
22
+ ),
23
+ };
24
+
25
+ export const Destructive: Story = {
26
+ render: () => (
27
+ <Alert variant="destructive">
28
+ <AlertCircle className="size-4" />
29
+ <AlertTitle>Error</AlertTitle>
30
+ <AlertDescription>Your session has expired. Please log in again.</AlertDescription>
31
+ </Alert>
32
+ ),
33
+ };
34
+
35
+ export const WithoutIcon: Story = {
36
+ render: () => (
37
+ <Alert>
38
+ <AlertTitle>Note</AlertTitle>
39
+ <AlertDescription>This alert has no icon.</AlertDescription>
40
+ </Alert>
41
+ ),
42
+ };
43
+
44
+ export const DescriptionOnly: Story = {
45
+ render: () => (
46
+ <Alert>
47
+ <Info className="size-4" />
48
+ <AlertDescription>A simple informational message without a title.</AlertDescription>
49
+ </Alert>
50
+ ),
51
+ };
@@ -0,0 +1,60 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import * as React from "react";
3
+
4
+ import { cn } from "../../lib/utils";
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-md border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-panel text-primary",
12
+ destructive:
13
+ "text-destructive bg-panel [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ },
20
+ );
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn("subheading-4 col-start-2 line-clamp-1 min-h-4", className)}
42
+ {...props}
43
+ />
44
+ );
45
+ }
46
+
47
+ function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
48
+ return (
49
+ <div
50
+ data-slot="alert-description"
51
+ className={cn(
52
+ "text-secondary body-3 col-start-2 grid justify-items-start gap-1 [&_p]:leading-relaxed",
53
+ className,
54
+ )}
55
+ {...props}
56
+ />
57
+ );
58
+ }
59
+
60
+ export { Alert, AlertTitle, AlertDescription };
@@ -0,0 +1,66 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ export interface EmptyStateProps {
6
+ icon?: ReactNode;
7
+ title: ReactNode;
8
+ description?: ReactNode;
9
+ action?: ReactNode;
10
+ children?: ReactNode;
11
+ className?: string;
12
+ layout?: "centered" | "inline";
13
+ size?: "md" | "lg";
14
+ variant?: "default" | "dashed" | "quiet" | "search";
15
+ }
16
+
17
+ /**
18
+ * EmptyState communicates an absence of content and gives the host application
19
+ * room for an optional next action. Domain copy and recovery behavior stay with
20
+ * the calling feature.
21
+ */
22
+ export function EmptyState({
23
+ icon,
24
+ title,
25
+ description,
26
+ action,
27
+ children,
28
+ className,
29
+ layout = "centered",
30
+ size = "md",
31
+ variant = "default",
32
+ }: EmptyStateProps) {
33
+ const centered = layout === "centered";
34
+ const large = size === "lg";
35
+
36
+ return (
37
+ <section
38
+ className={cn(
39
+ "rounded-lg px-5 py-8",
40
+ variant === "default" && "border-subtle bg-panel border",
41
+ variant === "dashed" && "border-subtle bg-panel border border-dashed",
42
+ variant === "search" && "border-subtle bg-surface border border-dashed",
43
+ variant === "quiet" && "bg-surface",
44
+ centered
45
+ ? cn(
46
+ "flex flex-col items-center justify-center text-center",
47
+ large ? "min-h-80 px-6 py-10" : "min-h-64",
48
+ )
49
+ : "flex flex-col gap-3",
50
+ className,
51
+ )}
52
+ >
53
+ {icon ? (
54
+ <div className="text-secondary mb-3 flex size-11 items-center justify-center" aria-hidden>
55
+ {icon}
56
+ </div>
57
+ ) : null}
58
+ <div className={cn("space-y-2", centered && "max-w-[34ch]")}>
59
+ <h2 className={cn("text-primary", large ? "heading-3" : "heading-4")}>{title}</h2>
60
+ {description ? <p className="body-3 text-secondary">{description}</p> : null}
61
+ </div>
62
+ {children ? <div className="mt-3">{children}</div> : null}
63
+ {action ? <div className="mt-4">{action}</div> : null}
64
+ </section>
65
+ );
66
+ }
@@ -0,0 +1,6 @@
1
+ export { Alert, AlertDescription, AlertTitle } from "./alert";
2
+ export { EmptyState } from "./empty-state";
3
+ export type { EmptyStateProps } from "./empty-state";
4
+ export { Progress } from "./progress";
5
+ export { Spinner } from "./spinner";
6
+ export type { SpinnerProps } from "./spinner";
@@ -0,0 +1,37 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { Progress } from "./progress";
4
+
5
+ const meta = {
6
+ title: "Feedback/Progress",
7
+ component: Progress,
8
+ tags: ["autodocs"],
9
+ } satisfies Meta<typeof Progress>;
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof meta>;
13
+
14
+ export const Default: Story = {
15
+ args: {
16
+ value: 60,
17
+ },
18
+ };
19
+
20
+ export const Empty: Story = {
21
+ args: { value: 0 },
22
+ };
23
+
24
+ export const Complete: Story = {
25
+ args: { value: 100 },
26
+ };
27
+
28
+ export const Partial: Story = {
29
+ render: () => (
30
+ <div className="flex w-64 flex-col gap-4">
31
+ <Progress value={10} />
32
+ <Progress value={33} />
33
+ <Progress value={66} />
34
+ <Progress value={90} />
35
+ </div>
36
+ ),
37
+ };
@@ -0,0 +1,30 @@
1
+ import * as ProgressPrimitive from "@radix-ui/react-progress";
2
+ import * as React from "react";
3
+
4
+ import { cn } from "../../lib/utils";
5
+
6
+ function Progress({
7
+ className,
8
+ value,
9
+ indicatorClassName,
10
+ ...props
11
+ }: React.ComponentProps<typeof ProgressPrimitive.Root> & {
12
+ indicatorClassName?: string;
13
+ }) {
14
+ return (
15
+ <ProgressPrimitive.Root
16
+ data-slot="progress"
17
+ className={cn("bg-accent/20 relative h-2 w-full overflow-hidden rounded-full", className)}
18
+ value={value}
19
+ {...props}
20
+ >
21
+ <ProgressPrimitive.Indicator
22
+ data-slot="progress-indicator"
23
+ className={cn("bg-accent h-full w-full flex-1 transition-all", indicatorClassName)}
24
+ style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
25
+ />
26
+ </ProgressPrimitive.Root>
27
+ );
28
+ }
29
+
30
+ export { Progress };
@@ -0,0 +1,52 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { textControl } from "../../storybook/controls";
4
+ import { Spinner } from "./spinner";
5
+
6
+ const meta = {
7
+ title: "Feedback/Spinner",
8
+ component: Spinner,
9
+ tags: ["autodocs"],
10
+ argTypes: {
11
+ label: textControl("Visible + accessible label; pass false to hide the text", "Loading"),
12
+ },
13
+ } satisfies Meta<typeof Spinner>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const Default: Story = {
19
+ args: {
20
+ label: false,
21
+ },
22
+ };
23
+
24
+ export const CustomLabel: Story = {
25
+ args: {
26
+ label: "Fetching results…",
27
+ },
28
+ };
29
+
30
+ export const NoLabel: Story = {
31
+ args: {
32
+ label: false,
33
+ },
34
+ };
35
+
36
+ export const Centered: Story = {
37
+ args: {
38
+ label: "Loading",
39
+ presentation: "centered",
40
+ },
41
+ };
42
+
43
+ export const Sizes: Story = {
44
+ render: () => (
45
+ <div className="flex items-center gap-4">
46
+ <Spinner size="sm" />
47
+ <Spinner size="md" />
48
+ <Spinner size="lg" />
49
+ <Spinner size="xl" />
50
+ </div>
51
+ ),
52
+ };
@@ -0,0 +1,48 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ const sizeClasses = {
6
+ sm: "size-4",
7
+ md: "size-5",
8
+ lg: "size-8",
9
+ xl: "size-12",
10
+ } as const;
11
+
12
+ export interface SpinnerProps extends React.ComponentProps<"div"> {
13
+ label?: string | false;
14
+ presentation?: "inline" | "centered";
15
+ size?: keyof typeof sizeClasses;
16
+ }
17
+
18
+ export function Spinner({
19
+ label = false,
20
+ presentation = "inline",
21
+ size = "md",
22
+ className,
23
+ ...props
24
+ }: SpinnerProps) {
25
+ const accessibleLabel = typeof label === "string" ? label : "Loading";
26
+
27
+ return (
28
+ <div
29
+ className={cn(
30
+ "inline-flex items-center justify-center gap-2",
31
+ presentation === "centered" && "flex w-full py-12",
32
+ className,
33
+ )}
34
+ role="status"
35
+ aria-label={accessibleLabel}
36
+ {...props}
37
+ >
38
+ <span
39
+ className={cn(
40
+ "border-default border-t-accent rounded-full border-2 motion-safe:animate-spin",
41
+ sizeClasses[size],
42
+ )}
43
+ aria-hidden="true"
44
+ />
45
+ {label && <span className="text-secondary text-xs">{label}</span>}
46
+ </div>
47
+ );
48
+ }
@@ -0,0 +1,30 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { useState } from "react";
3
+
4
+ import { Calendar } from "./calendar";
5
+
6
+ const meta = {
7
+ title: "Forms/Calendar",
8
+ component: Calendar,
9
+ tags: ["autodocs"],
10
+ } satisfies Meta<typeof Calendar>;
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ export const Default: Story = {
16
+ render: () => {
17
+ const [date, setDate] = useState<Date | undefined>(new Date());
18
+ return (
19
+ <Calendar mode="single" selected={date} onSelect={setDate} className="rounded-md border" />
20
+ );
21
+ },
22
+ };
23
+
24
+ export const WithoutOutsideDays: Story = {
25
+ render: () => <Calendar mode="single" showOutsideDays={false} className="rounded-md border" />,
26
+ };
27
+
28
+ export const RangeMode: Story = {
29
+ render: () => <Calendar mode="range" className="rounded-md border" />,
30
+ };
@@ -0,0 +1,67 @@
1
+ import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from "lucide-react";
2
+ import type * as React from "react";
3
+ import { DayPicker } from "react-day-picker";
4
+
5
+ import { cn } from "../../lib/utils";
6
+
7
+ export type CalendarProps = React.ComponentProps<typeof DayPicker>;
8
+
9
+ function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
10
+ return (
11
+ <DayPicker
12
+ showOutsideDays={showOutsideDays}
13
+ className={cn("relative w-fit rounded-md p-4", className)}
14
+ classNames={{
15
+ root: "text-primary",
16
+ months: "flex flex-col gap-4 sm:flex-row",
17
+ month: "flex flex-col gap-4",
18
+ month_caption: "relative flex h-8 items-center justify-center",
19
+ caption_label: "text-sm font-medium",
20
+ nav: "absolute inset-x-4 top-4 flex items-center justify-between",
21
+ button_previous:
22
+ "inline-flex size-8 p-2 items-center justify-center rounded-full border border-default bg-canvas text-primary disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50",
23
+ button_next:
24
+ "inline-flex size-8 p-2 items-center justify-center rounded-full border border-default bg-canvas text-primary disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50",
25
+ month_grid: "w-full border-collapse",
26
+ weekdays: "flex",
27
+ weekday: "flex size-8 items-center justify-center text-[0.8rem] font-normal text-secondary",
28
+ week: "mt-1 flex w-full",
29
+ day: "relative size-8 p-0 text-center text-lg focus-within:z-20",
30
+ day_button:
31
+ "inline-flex size-6 items-center justify-center rounded-md text-sm font-normal disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50",
32
+ selected:
33
+ "[&>button]:bg-accent [&>button]:text-on-accent [&>button]:hover:bg-accent [&>button]:hover:text-on-accent",
34
+ today: "[&>button]:bg-accent [&>button]:text-on-accent",
35
+ outside: "text-secondary opacity-50 [&>button]:text-secondary",
36
+ disabled: "text-secondary opacity-50",
37
+ range_start: "rounded-l-md bg-accent [&>button]:bg-accent [&>button]:text-on-accent",
38
+ range_middle: "bg-accent [&>button]:rounded-none [&>button]:text-on-accent",
39
+ range_end: "rounded-r-md bg-accent [&>button]:bg-accent [&>button]:text-on-accent",
40
+ hidden: "invisible",
41
+ ...classNames,
42
+ }}
43
+ components={{
44
+ Chevron: ({ className: iconClassName, orientation, ...iconProps }) => {
45
+ if (orientation === "left") {
46
+ return <ChevronLeft className={cn("size-4", iconClassName)} {...iconProps} />;
47
+ }
48
+
49
+ if (orientation === "right") {
50
+ return <ChevronRight className={cn("size-4", iconClassName)} {...iconProps} />;
51
+ }
52
+
53
+ if (orientation === "up") {
54
+ return <ChevronUp className={cn("size-4", iconClassName)} {...iconProps} />;
55
+ }
56
+
57
+ return <ChevronDown className={cn("size-4", iconClassName)} {...iconProps} />;
58
+ },
59
+ }}
60
+ {...props}
61
+ />
62
+ );
63
+ }
64
+
65
+ Calendar.displayName = "Calendar";
66
+
67
+ export { Calendar };
@@ -0,0 +1,48 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { useState } from "react";
3
+
4
+ import { DatePicker } from "./date-picker";
5
+
6
+ const meta = {
7
+ title: "Forms/DatePicker",
8
+ component: DatePicker,
9
+ tags: ["autodocs"],
10
+ } satisfies Meta<typeof DatePicker>;
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ export const Default: Story = {
16
+ args: {
17
+ value: undefined,
18
+ onSelect: () => {},
19
+ placeholder: "Pick a date",
20
+ },
21
+ render: (args) => {
22
+ const [date, setDate] = useState<Date | undefined>(args.value);
23
+ return <DatePicker {...args} value={date} onSelect={setDate} />;
24
+ },
25
+ };
26
+
27
+ export const WithLabel: Story = {
28
+ args: {
29
+ value: new Date(),
30
+ onSelect: () => {},
31
+ label: "Due Date",
32
+ showLabel: true,
33
+ },
34
+ render: (args) => {
35
+ const [date, setDate] = useState<Date | undefined>(args.value);
36
+ return <DatePicker {...args} value={date} onSelect={setDate} />;
37
+ },
38
+ };
39
+
40
+ export const Disabled: Story = {
41
+ args: {
42
+ value: new Date(),
43
+ onSelect: () => {},
44
+ disabled: true,
45
+ label: "Locked Date",
46
+ showLabel: true,
47
+ },
48
+ };
@@ -0,0 +1,60 @@
1
+ import { format } from "date-fns";
2
+ import { CalendarIcon } from "lucide-react";
3
+
4
+ import { Button } from "../primitives/button";
5
+ import { Calendar } from "../forms/calendar";
6
+ import { Popover, PopoverContent, PopoverTrigger } from "../overlays/popover";
7
+
8
+ export interface DatePickerProps {
9
+ value: Date | undefined;
10
+ onSelect: (date: Date | undefined) => void;
11
+ disabled?: boolean;
12
+ id?: string;
13
+ placeholder?: string;
14
+ label?: string;
15
+ dateFormat?: string;
16
+ showLabel?: boolean;
17
+ containerClassName?: string;
18
+ popoverAlign?: "start" | "center" | "end";
19
+ variant?: "outline" | "default" | "destructive" | "secondary" | "ghost" | "link";
20
+ }
21
+
22
+ export function DatePicker({
23
+ value,
24
+ onSelect,
25
+ disabled = false,
26
+ id = "date-picker",
27
+ placeholder = "Pick a date",
28
+ label,
29
+ dateFormat = "MMM d, yyyy",
30
+ showLabel = true,
31
+ containerClassName = "flex-1 min-w-0",
32
+ popoverAlign = "start",
33
+ variant = "outline",
34
+ }: DatePickerProps) {
35
+ return (
36
+ <div className={containerClassName}>
37
+ {showLabel && label ? (
38
+ <label htmlFor={id} className="text-primary mb-2 block text-sm font-medium">
39
+ {label}
40
+ </label>
41
+ ) : null}
42
+ <Popover>
43
+ <PopoverTrigger asChild>
44
+ <Button
45
+ id={id}
46
+ variant={variant}
47
+ disabled={disabled}
48
+ className="bg-inset placeholder:text-secondary w-full justify-start text-left font-normal backdrop-blur-sm"
49
+ >
50
+ <CalendarIcon className="mr-2 size-4" />
51
+ {value ? format(value, dateFormat) : <span>{placeholder}</span>}
52
+ </Button>
53
+ </PopoverTrigger>
54
+ <PopoverContent className="bg-canvas w-auto p-0" align={popoverAlign}>
55
+ <Calendar mode="single" selected={value} onSelect={onSelect} />
56
+ </PopoverContent>
57
+ </Popover>
58
+ </div>
59
+ );
60
+ }