cortena-ui 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/index.d.ts +2004 -4
  2. package/dist/index.js +5949 -2
  3. package/dist/index.js.map +1 -1
  4. package/package.json +41 -5
  5. package/src/components/accordion.tsx +86 -0
  6. package/src/components/alert.tsx +81 -0
  7. package/src/components/avatar.tsx +53 -0
  8. package/src/components/badge.tsx +51 -0
  9. package/src/components/calendar.tsx +193 -0
  10. package/src/components/card.tsx +85 -0
  11. package/src/components/chart/chart.tsx +193 -0
  12. package/src/components/chart/container.tsx +324 -0
  13. package/src/components/chart/data.ts +101 -0
  14. package/src/components/chart/index.tsx +84 -0
  15. package/src/components/chart/nivo-charts.tsx +227 -0
  16. package/src/components/chart/nivo-theme.ts +136 -0
  17. package/src/components/chart/recharts-charts.tsx +232 -0
  18. package/src/components/chart/tokens.ts +210 -0
  19. package/src/components/chart/types.ts +142 -0
  20. package/src/components/checkbox.tsx +49 -0
  21. package/src/components/chip.tsx +49 -0
  22. package/src/components/combobox.tsx +405 -0
  23. package/src/components/command.tsx +225 -0
  24. package/src/components/data-table/data-table.tsx +731 -0
  25. package/src/components/data-table/export.ts +121 -0
  26. package/src/components/data-table/features.ts +123 -0
  27. package/src/components/data-table/index.tsx +45 -0
  28. package/src/components/data-table/parts.tsx +568 -0
  29. package/src/components/data-table/system-columns.tsx +91 -0
  30. package/src/components/data-table/types.ts +182 -0
  31. package/src/components/data-table/use-data-table.ts +224 -0
  32. package/src/components/data-table/use-server-source.ts +232 -0
  33. package/src/components/date-field.tsx +327 -0
  34. package/src/components/date-picker.tsx +230 -0
  35. package/src/components/dialog.tsx +146 -0
  36. package/src/components/dropdown-menu.tsx +314 -0
  37. package/src/components/dropzone.tsx +260 -0
  38. package/src/components/empty-state.tsx +91 -0
  39. package/src/components/error-banner.tsx +65 -0
  40. package/src/components/field.tsx +250 -0
  41. package/src/components/form.tsx +212 -0
  42. package/src/components/input.tsx +32 -0
  43. package/src/components/kbd.tsx +26 -0
  44. package/src/components/label.tsx +26 -0
  45. package/src/components/loading-skeleton.tsx +153 -0
  46. package/src/components/markdown.tsx +377 -0
  47. package/src/components/page-header.tsx +99 -0
  48. package/src/components/popover.tsx +133 -0
  49. package/src/components/progress.tsx +83 -0
  50. package/src/components/radio-group.tsx +48 -0
  51. package/src/components/scroll-area.tsx +74 -0
  52. package/src/components/section-card.tsx +75 -0
  53. package/src/components/segmented.tsx +81 -0
  54. package/src/components/select.tsx +222 -0
  55. package/src/components/separator.tsx +41 -0
  56. package/src/components/sheet.tsx +161 -0
  57. package/src/components/skeleton.tsx +22 -0
  58. package/src/components/sortable-list.tsx +275 -0
  59. package/src/components/spinner.tsx +32 -0
  60. package/src/components/status-dot.tsx +78 -0
  61. package/src/components/switch.tsx +48 -0
  62. package/src/components/tabs.tsx +109 -0
  63. package/src/components/textarea.tsx +75 -0
  64. package/src/components/toast.tsx +150 -0
  65. package/src/components/toolbar.tsx +63 -0
  66. package/src/components/tooltip.tsx +103 -0
  67. package/src/hooks/use-cortena-theme.ts +268 -0
  68. package/src/index.ts +91 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortena-ui",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "Cortena UI — the shared React component library for cortenaweb and every Cortena extension. shadcn over Base UI, styled entirely from cortena-design tokens.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -31,27 +31,54 @@
31
31
  "scripts": {
32
32
  "build": "tsdown",
33
33
  "check": "tsc --noEmit",
34
- "lint:design": "node ../../lint/cli.mjs src",
34
+ "lint:design": "node ../../lint/cli.mjs --exempt chart-import src",
35
35
  "test": "vitest run",
36
36
  "guide": "vite --config guide/vite.config.ts",
37
37
  "guide:build": "vite build --config guide/vite.config.ts",
38
38
  "prepack": "pnpm build"
39
39
  },
40
40
  "peerDependencies": {
41
- "cortena-design": ">=3.1.0",
41
+ "cortena-design": ">=3.2.0",
42
42
  "react": "^19.0.0",
43
- "react-dom": "^19.0.0"
43
+ "react-dom": "^19.0.0",
44
+ "zod": "^3.25.0 || ^4.0.0",
45
+ "@tanstack/react-query": "^5.0.0"
44
46
  },
45
47
  "dependencies": {
46
48
  "@base-ui/react": "^1.8.0",
49
+ "@dnd-kit/core": "^6.3.1",
50
+ "@dnd-kit/sortable": "^10.0.0",
51
+ "@dnd-kit/utilities": "^3.2.2",
52
+ "@hookform/resolvers": "^5.9.1",
53
+ "@nivo/bar": "^0.99.0",
54
+ "@nivo/calendar": "^0.99.0",
55
+ "@nivo/core": "^0.99.0",
56
+ "@nivo/heatmap": "^0.99.0",
57
+ "@nivo/line": "^0.99.0",
58
+ "@nivo/pie": "^0.99.0",
59
+ "@nivo/scatterplot": "^0.99.0",
60
+ "@nivo/sunburst": "^0.99.0",
61
+ "@nivo/treemap": "^0.99.0",
62
+ "@tanstack/react-table": "^9.2.4",
63
+ "@tanstack/react-virtual": "^3.14.10",
47
64
  "class-variance-authority": "^0.7.1",
48
65
  "clsx": "^2.1.1",
66
+ "cmdk": "^1.1.1",
67
+ "exceljs": "^4.4.0",
49
68
  "lucide-react": "^1.41.0",
69
+ "react-day-picker": "^10.0.1",
70
+ "react-dropzone": "^20.1.1",
71
+ "react-hook-form": "^7.87.0",
72
+ "react-markdown": "^10.1.0",
73
+ "recharts": "^3.10.1",
74
+ "rehype-sanitize": "^6.0.0",
75
+ "remark-gfm": "^4.0.1",
50
76
  "tailwind-merge": "^3.6.0",
51
77
  "tw-animate-css": "^1.4.0"
52
78
  },
53
79
  "devDependencies": {
54
80
  "@tailwindcss/vite": "^4.3.3",
81
+ "@tanstack/react-query": "^5.102.8",
55
82
  "@types/node": "^22.20.1",
56
83
  "@types/react": "^19.1.0",
57
84
  "@types/react-dom": "^19.1.0",
@@ -66,9 +93,18 @@
66
93
  "typescript": "^5.9.0",
67
94
  "vite": "^8.2.2",
68
95
  "vitest": "^5.0.0",
69
- "vitest-browser-react": "^2.3.0"
96
+ "vitest-browser-react": "^2.3.0",
97
+ "zod": "^4.5.4"
70
98
  },
71
99
  "publishConfig": {
72
100
  "access": "public"
101
+ },
102
+ "peerDependenciesMeta": {
103
+ "zod": {
104
+ "optional": true
105
+ },
106
+ "@tanstack/react-query": {
107
+ "optional": true
108
+ }
73
109
  }
74
110
  }
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
4
+ import { ChevronDown } from "lucide-react";
5
+ import { cn } from "@/lib/cn";
6
+
7
+ /**
8
+ * Accordion.
9
+ *
10
+ * Base UI's Accordion is a list of Collapsibles: `Root > Item > Header > Trigger`
11
+ * plus `Panel`. The panel animates on height through Base UI's
12
+ * `--accordion-panel-height`, and marks its first/last frame with
13
+ * `data-starting-style` / `data-ending-style`, which is where it collapses to 0.
14
+ *
15
+ * Radix users: `type="single" | "multiple"` is now the boolean `multiple`, and
16
+ * `value` is always an array. `collapsible` is the default. `data-state="open"`
17
+ * is `data-open` on the item and panel, `data-panel-open` on the trigger.
18
+ */
19
+ function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
20
+ return (
21
+ <AccordionPrimitive.Root
22
+ data-slot="accordion"
23
+ className={cn("flex w-full flex-col", className)}
24
+ {...props}
25
+ />
26
+ );
27
+ }
28
+
29
+ function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
30
+ return (
31
+ <AccordionPrimitive.Item
32
+ data-slot="accordion-item"
33
+ className={cn("border-b border-[var(--ds-border)] last:border-b-0", className)}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ function AccordionTrigger({ className, children, ...props }: AccordionPrimitive.Trigger.Props) {
40
+ return (
41
+ <AccordionPrimitive.Header data-slot="accordion-header" className="flex">
42
+ <AccordionPrimitive.Trigger
43
+ data-slot="accordion-trigger"
44
+ className={cn(
45
+ "flex flex-1 items-start justify-between gap-4 py-4 text-left",
46
+ "text-[length:var(--ds-text-body-sm)] font-medium text-[color:var(--ds-foreground)]",
47
+ "rounded-[var(--ds-radius-sm)] outline-none",
48
+ "transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]",
49
+ "hover:underline focus-visible:ring-[3px] focus-visible:ring-[var(--ds-ring)]/40",
50
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
51
+ "[&[data-panel-open]>svg]:rotate-180",
52
+ className,
53
+ )}
54
+ {...props}
55
+ >
56
+ {children}
57
+ <ChevronDown
58
+ aria-hidden
59
+ className={cn(
60
+ "pointer-events-none size-4 shrink-0 translate-y-0.5 text-[color:var(--ds-muted-foreground)]",
61
+ "transition-transform duration-[var(--ds-duration-base)] ease-[var(--ds-ease-out)]",
62
+ )}
63
+ />
64
+ </AccordionPrimitive.Trigger>
65
+ </AccordionPrimitive.Header>
66
+ );
67
+ }
68
+
69
+ function AccordionContent({ className, children, ...props }: AccordionPrimitive.Panel.Props) {
70
+ return (
71
+ <AccordionPrimitive.Panel
72
+ data-slot="accordion-content"
73
+ className={cn(
74
+ "h-[var(--accordion-panel-height,auto)] overflow-hidden",
75
+ "text-[length:var(--ds-text-body-sm)] text-[color:var(--ds-muted-foreground)]",
76
+ "transition-[height] duration-[var(--ds-duration-base)] ease-[var(--ds-ease-out)]",
77
+ "data-[starting-style]:h-0 data-[ending-style]:h-0",
78
+ )}
79
+ {...props}
80
+ >
81
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
82
+ </AccordionPrimitive.Panel>
83
+ );
84
+ }
85
+
86
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import type * as React from "react";
5
+ import { cn } from "@/lib/cn";
6
+
7
+ /**
8
+ * Alert — an inline callout. Tinted variants use the `-soft` fill for the
9
+ * background and the tone colour for the border and icon; the body text stays
10
+ * on the foreground token so it reads in both themes.
11
+ */
12
+ const alertVariants = cva(
13
+ [
14
+ "flex gap-3 rounded-[var(--ds-radius-md)] border px-3.5 py-3",
15
+ "text-[length:var(--ds-text-caption-lg)] leading-[var(--ds-text-caption-lg--line-height)]",
16
+ "text-[color:var(--ds-foreground)]",
17
+ ],
18
+ {
19
+ variants: {
20
+ variant: {
21
+ default:
22
+ "bg-[var(--ds-card)] border-[var(--ds-border-subtle)] [&_[data-slot=alert-icon]]:text-[color:var(--ds-muted-foreground)]",
23
+ success:
24
+ "bg-[var(--ds-success-soft)] border-[var(--ds-success)]/35 [&_[data-slot=alert-icon]]:text-[color:var(--ds-success)]",
25
+ warn: "bg-[var(--ds-warning-soft)] border-[var(--ds-warning)]/35 [&_[data-slot=alert-icon]]:text-[color:var(--ds-warning)]",
26
+ danger:
27
+ "bg-[var(--ds-destructive-soft)] border-[var(--ds-destructive)]/35 [&_[data-slot=alert-icon]]:text-[color:var(--ds-destructive)]",
28
+ info: "bg-[var(--ds-info-soft)] border-[var(--ds-info)]/35 [&_[data-slot=alert-icon]]:text-[color:var(--ds-info)]",
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ variant: "default",
33
+ },
34
+ },
35
+ );
36
+
37
+ export interface AlertProps
38
+ extends React.ComponentProps<"div">, VariantProps<typeof alertVariants> {}
39
+
40
+ function Alert({ className, variant, ...props }: AlertProps) {
41
+ return (
42
+ <div
43
+ data-slot="alert"
44
+ role="alert"
45
+ className={cn(alertVariants({ variant }), className)}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ function AlertIcon({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="alert-icon"
55
+ className={cn("shrink-0 self-start [&_svg]:size-4.5 [&_svg]:shrink-0", className)}
56
+ {...props}
57
+ />
58
+ );
59
+ }
60
+
61
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
62
+ return (
63
+ <div
64
+ data-slot="alert-title"
65
+ className={cn("self-start font-semibold text-[color:var(--ds-foreground)]", className)}
66
+ {...props}
67
+ />
68
+ );
69
+ }
70
+
71
+ function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
72
+ return (
73
+ <div
74
+ data-slot="alert-description"
75
+ className={cn("mt-0.5 self-start text-[color:var(--ds-muted-foreground)]", className)}
76
+ {...props}
77
+ />
78
+ );
79
+ }
80
+
81
+ export { Alert, AlertIcon, AlertTitle, AlertDescription, alertVariants };
@@ -0,0 +1,53 @@
1
+ "use client";
2
+
3
+ import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
4
+ import { cn } from "@/lib/cn";
5
+
6
+ /**
7
+ * Avatar.
8
+ *
9
+ * Base UI preloads the image and only mounts `<img>` once it has loaded, so
10
+ * the fallback is what paints while loading and after an error. Initials sit
11
+ * on the primary fill in inverse text.
12
+ *
13
+ * Radix users: `delayMs` on the fallback is now `delay`.
14
+ */
15
+ function Avatar({ className, ...props }: AvatarPrimitive.Root.Props) {
16
+ return (
17
+ <AvatarPrimitive.Root
18
+ data-slot="avatar"
19
+ className={cn(
20
+ "relative flex size-8 shrink-0 overflow-hidden rounded-[var(--ds-radius-full)]",
21
+ className,
22
+ )}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+
28
+ function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
29
+ return (
30
+ <AvatarPrimitive.Image
31
+ data-slot="avatar-image"
32
+ className={cn("aspect-square size-full object-cover", className)}
33
+ {...props}
34
+ />
35
+ );
36
+ }
37
+
38
+ function AvatarFallback({ className, ...props }: AvatarPrimitive.Fallback.Props) {
39
+ return (
40
+ <AvatarPrimitive.Fallback
41
+ data-slot="avatar-fallback"
42
+ className={cn(
43
+ "flex size-full select-none items-center justify-center rounded-[var(--ds-radius-full)]",
44
+ "bg-[var(--ds-primary)] text-[color:var(--ds-primary-foreground)]",
45
+ "text-[length:var(--ds-text-caption-lg)] font-semibold tracking-tight",
46
+ className,
47
+ )}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+
53
+ export { Avatar, AvatarImage, AvatarFallback };
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import type * as React from "react";
5
+ import { cn } from "@/lib/cn";
6
+
7
+ /**
8
+ * Badge — the mono, uppercase status label. Each tone pairs a `-soft` fill
9
+ * with its foreground token; `warn`, `danger` and `secondary` are aliases kept
10
+ * for cortenaweb call sites.
11
+ */
12
+ const badgeVariants = cva(
13
+ [
14
+ "inline-flex items-center gap-1.5 px-1.5 py-0.5 whitespace-nowrap",
15
+ "rounded-[var(--ds-radius-sm)]",
16
+ "font-[family-name:var(--ds-font-mono)] text-[length:var(--ds-text-caption-sm)] font-medium uppercase",
17
+ "tracking-[var(--ds-text-eyebrow--letter-spacing)] leading-[var(--ds-text-caption-sm--line-height)]",
18
+ "[&_svg]:size-3 [&_svg]:shrink-0",
19
+ ],
20
+ {
21
+ variants: {
22
+ variant: {
23
+ default: "bg-[var(--ds-primary-soft)] text-[color:var(--ds-primary-soft-foreground)]",
24
+ success: "bg-[var(--ds-success-soft)] text-[color:var(--ds-success)]",
25
+ warning: "bg-[var(--ds-warning-soft)] text-[color:var(--ds-warning)]",
26
+ warn: "bg-[var(--ds-warning-soft)] text-[color:var(--ds-warning)]",
27
+ danger: "bg-[var(--ds-destructive-soft)] text-[color:var(--ds-destructive)]",
28
+ destructive: "bg-[var(--ds-destructive-soft)] text-[color:var(--ds-destructive)]",
29
+ info: "bg-[var(--ds-info-soft)] text-[color:var(--ds-info)]",
30
+ neutral: "bg-[var(--ds-hover)] text-[color:var(--ds-muted-foreground)]",
31
+ secondary: "bg-[var(--ds-hover)] text-[color:var(--ds-muted-foreground)]",
32
+ outline:
33
+ "bg-transparent border border-[var(--ds-border)] text-[color:var(--ds-muted-foreground)]",
34
+ },
35
+ },
36
+ defaultVariants: {
37
+ variant: "default",
38
+ },
39
+ },
40
+ );
41
+
42
+ export interface BadgeProps
43
+ extends React.ComponentProps<"span">, VariantProps<typeof badgeVariants> {}
44
+
45
+ function Badge({ className, variant, ...props }: BadgeProps) {
46
+ return (
47
+ <span data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} />
48
+ );
49
+ }
50
+
51
+ export { Badge, badgeVariants };
@@ -0,0 +1,193 @@
1
+ "use client";
2
+
3
+ import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from "lucide-react";
4
+ import * as React from "react";
5
+ import {
6
+ DayPicker,
7
+ getDefaultClassNames,
8
+ type ChevronProps,
9
+ type DayButtonProps,
10
+ type DayPickerProps,
11
+ type NextMonthButtonProps,
12
+ type PreviousMonthButtonProps,
13
+ type RootProps,
14
+ } from "react-day-picker";
15
+ import { Button } from "@/components/button";
16
+ import { cn } from "@/lib/cn";
17
+
18
+ /**
19
+ * Calendar.
20
+ *
21
+ * The shadcn Calendar shape over react-day-picker 10. The library's stylesheet
22
+ * is not imported; every class comes from the `classNames` map below and every
23
+ * visual value in it is a cortena-design token. Structure (nav, caption, grid,
24
+ * dropdown caption) is DayPicker's; the day cells, nav buttons and chevrons are
25
+ * swapped for our Button and lucide icons through `components`.
26
+ *
27
+ * `showOutsideDays` defaults to true and `captionLayout` to `"label"`; pass
28
+ * `captionLayout="dropdown"` for native month/year selects in the caption.
29
+ */
30
+ export type CalendarProps = DayPickerProps;
31
+
32
+ function CalendarRoot({ className, rootRef, ...props }: RootProps) {
33
+ return <div data-slot="calendar" ref={rootRef} className={className} {...props} />;
34
+ }
35
+
36
+ function CalendarChevron({ className, orientation, size: _size, disabled, ...props }: ChevronProps) {
37
+ const Icon =
38
+ orientation === "left"
39
+ ? ChevronLeft
40
+ : orientation === "right"
41
+ ? ChevronRight
42
+ : orientation === "up"
43
+ ? ChevronUp
44
+ : ChevronDown;
45
+ return (
46
+ <Icon
47
+ data-slot="calendar-chevron"
48
+ aria-hidden
49
+ className={cn("size-4", disabled && "opacity-50", className)}
50
+ {...props}
51
+ />
52
+ );
53
+ }
54
+
55
+ function CalendarPreviousMonthButton(props: PreviousMonthButtonProps) {
56
+ return <Button data-slot="calendar-previous-month" variant="outline" size="icon-sm" {...props} />;
57
+ }
58
+
59
+ function CalendarNextMonthButton(props: NextMonthButtonProps) {
60
+ return <Button data-slot="calendar-next-month" variant="outline" size="icon-sm" {...props} />;
61
+ }
62
+
63
+ /**
64
+ * One day cell's button. Selection and range state are exposed as data
65
+ * attributes so the classes below (and a consumer's) can paint them; the
66
+ * `today` attribute is only set when the day is not selected, so the soft
67
+ * fill never competes with the selected fill.
68
+ */
69
+ function CalendarDayButton({ className, day, modifiers, ...props }: DayButtonProps) {
70
+ const ref = React.useRef<HTMLButtonElement>(null);
71
+ React.useEffect(() => {
72
+ if (modifiers.focused) ref.current?.focus();
73
+ }, [modifiers.focused]);
74
+
75
+ const inRange = modifiers.range_start || modifiers.range_end || modifiers.range_middle;
76
+ const selectedSingle = modifiers.selected && !inRange;
77
+ const filled = selectedSingle || modifiers.range_start || modifiers.range_end;
78
+
79
+ return (
80
+ <Button
81
+ ref={ref}
82
+ data-slot="calendar-day-button"
83
+ data-day={day.isoDate}
84
+ data-selected-single={selectedSingle || undefined}
85
+ data-range-start={modifiers.range_start || undefined}
86
+ data-range-end={modifiers.range_end || undefined}
87
+ data-range-middle={(modifiers.range_middle && !filled) || undefined}
88
+ data-today={(modifiers.today && !modifiers.selected) || undefined}
89
+ variant="ghost"
90
+ size="icon-sm"
91
+ className={cn(
92
+ "size-8 font-normal text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-foreground)]",
93
+ "rounded-[var(--ds-radius-sm)] leading-none",
94
+ // today: soft fill from the primary tint
95
+ "data-[today]:bg-[var(--ds-primary-soft)] data-[today]:text-[color:var(--ds-primary-soft-foreground)]",
96
+ // range interior: soft fill, square so the run reads as one band
97
+ "data-[range-middle]:rounded-none data-[range-middle]:bg-[var(--ds-primary-soft)] data-[range-middle]:text-[color:var(--ds-primary-soft-foreground)] data-[range-middle]:hover:bg-[var(--ds-primary-soft)] data-[range-middle]:hover:brightness-95",
98
+ // selected day and range ends: solid primary
99
+ "data-[selected-single]:bg-[var(--ds-primary)] data-[selected-single]:text-[color:var(--ds-primary-foreground)] data-[selected-single]:hover:bg-[var(--ds-primary-hover)] data-[selected-single]:hover:text-[color:var(--ds-primary-foreground)]",
100
+ "data-[range-start]:bg-[var(--ds-primary)] data-[range-start]:text-[color:var(--ds-primary-foreground)] data-[range-start]:hover:bg-[var(--ds-primary-hover)] data-[range-start]:hover:text-[color:var(--ds-primary-foreground)]",
101
+ "data-[range-end]:bg-[var(--ds-primary)] data-[range-end]:text-[color:var(--ds-primary-foreground)] data-[range-end]:hover:bg-[var(--ds-primary-hover)] data-[range-end]:hover:text-[color:var(--ds-primary-foreground)]",
102
+ // the cell's own flags, read from the parent <td>
103
+ "group-data-[outside]/day:text-[color:var(--ds-text-tertiary)]",
104
+ "group-data-[disabled]/day:text-[color:var(--ds-text-disabled)] group-data-[disabled]/day:opacity-50",
105
+ className,
106
+ )}
107
+ {...props}
108
+ />
109
+ );
110
+ }
111
+
112
+ function Calendar({
113
+ className,
114
+ classNames,
115
+ components,
116
+ showOutsideDays = true,
117
+ captionLayout = "label",
118
+ formatters,
119
+ ...props
120
+ }: CalendarProps) {
121
+ const defaults = getDefaultClassNames();
122
+ return (
123
+ <DayPicker
124
+ showOutsideDays={showOutsideDays}
125
+ captionLayout={captionLayout}
126
+ className={cn("w-fit p-3 text-[color:var(--ds-foreground)]", className)}
127
+ formatters={{
128
+ formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
129
+ ...formatters,
130
+ }}
131
+ classNames={{
132
+ root: cn("w-fit", defaults.root),
133
+ months: cn("relative flex flex-col gap-4 md:flex-row", defaults.months),
134
+ month: cn("flex w-full flex-col gap-3", defaults.month),
135
+ nav: cn("absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", defaults.nav),
136
+ button_previous: cn("aria-disabled:pointer-events-none aria-disabled:opacity-50", defaults.button_previous),
137
+ button_next: cn("aria-disabled:pointer-events-none aria-disabled:opacity-50", defaults.button_next),
138
+ month_caption: cn("flex h-8 w-full items-center justify-center px-9", defaults.month_caption),
139
+ caption_label: cn(
140
+ "select-none font-medium text-[length:var(--ds-text-body-sm)] text-[color:var(--ds-foreground)]",
141
+ captionLayout !== "label" &&
142
+ "flex h-8 items-center gap-1 rounded-[var(--ds-radius-sm)] pl-2 pr-1 [&>svg]:size-3.5 [&>svg]:text-[color:var(--ds-muted-foreground)]",
143
+ defaults.caption_label,
144
+ ),
145
+ dropdowns: cn(
146
+ "flex h-8 w-full items-center justify-center gap-1.5 font-medium text-[length:var(--ds-text-body-sm)]",
147
+ defaults.dropdowns,
148
+ ),
149
+ dropdown_root: cn(
150
+ "relative rounded-[var(--ds-radius-sm)] border border-[var(--ds-border)] bg-[var(--ds-muted)]",
151
+ "has-focus-visible:border-[var(--ds-ring)] has-focus-visible:ring-[3px] has-focus-visible:ring-[var(--ds-ring)]/40",
152
+ defaults.dropdown_root,
153
+ ),
154
+ dropdown: cn("absolute inset-0 cursor-pointer opacity-0", defaults.dropdown),
155
+ month_grid: cn("w-full border-collapse", defaults.month_grid),
156
+ weekdays: cn("flex", defaults.weekdays),
157
+ weekday: cn(
158
+ "flex-1 select-none font-normal text-[length:var(--ds-text-caption)] text-[color:var(--ds-muted-foreground)]",
159
+ defaults.weekday,
160
+ ),
161
+ week: cn("mt-1 flex w-full", defaults.week),
162
+ week_number_header: cn("w-8 select-none", defaults.week_number_header),
163
+ week_number: cn(
164
+ "select-none text-[length:var(--ds-text-caption)] text-[color:var(--ds-muted-foreground)]",
165
+ defaults.week_number,
166
+ ),
167
+ day: cn(
168
+ "group/day relative aspect-square size-8 select-none p-0 text-center",
169
+ defaults.day,
170
+ ),
171
+ range_start: cn("rounded-l-[var(--ds-radius-sm)] bg-[var(--ds-primary-soft)]", defaults.range_start),
172
+ range_middle: cn("rounded-none bg-[var(--ds-primary-soft)]", defaults.range_middle),
173
+ range_end: cn("rounded-r-[var(--ds-radius-sm)] bg-[var(--ds-primary-soft)]", defaults.range_end),
174
+ today: defaults.today,
175
+ outside: defaults.outside,
176
+ disabled: defaults.disabled,
177
+ hidden: cn("invisible", defaults.hidden),
178
+ ...classNames,
179
+ }}
180
+ components={{
181
+ Root: CalendarRoot,
182
+ Chevron: CalendarChevron,
183
+ PreviousMonthButton: CalendarPreviousMonthButton,
184
+ NextMonthButton: CalendarNextMonthButton,
185
+ DayButton: CalendarDayButton,
186
+ ...components,
187
+ }}
188
+ {...props}
189
+ />
190
+ );
191
+ }
192
+
193
+ export { Calendar, CalendarDayButton };
@@ -0,0 +1,85 @@
1
+ "use client";
2
+
3
+ import type * as React from "react";
4
+ import { cn } from "@/lib/cn";
5
+
6
+ export interface CardProps extends React.ComponentProps<"div"> {
7
+ /** Lift the border and surface on hover, for clickable cards. */
8
+ hover?: boolean;
9
+ /** Draw the border in the ring colour, for a selected or highlighted card. */
10
+ accent?: boolean;
11
+ }
12
+
13
+ function Card({ className, hover, accent, ...props }: CardProps) {
14
+ return (
15
+ <div
16
+ data-slot="card"
17
+ data-accent={accent ? "" : undefined}
18
+ className={cn(
19
+ "flex flex-col rounded-[var(--ds-radius-lg)] border bg-[var(--ds-card)] text-[color:var(--ds-foreground)]",
20
+ "transition-[border-color,background-color] duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]",
21
+ accent ? "border-[var(--ds-ring)]" : "border-[var(--ds-border-subtle)]",
22
+ hover && "hover:border-[var(--ds-border)] hover:bg-[var(--ds-popover)]",
23
+ className,
24
+ )}
25
+ {...props}
26
+ />
27
+ );
28
+ }
29
+
30
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
31
+ return (
32
+ <div
33
+ data-slot="card-header"
34
+ className={cn("flex flex-col gap-1.5 p-5 pb-3", className)}
35
+ {...props}
36
+ />
37
+ );
38
+ }
39
+
40
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
41
+ return (
42
+ <div
43
+ data-slot="card-title"
44
+ className={cn(
45
+ "text-[length:var(--ds-text-body-md)] font-semibold",
46
+ "leading-[var(--ds-text-h1--line-height)] tracking-[var(--ds-text-h1--letter-spacing)]",
47
+ "text-[color:var(--ds-foreground)]",
48
+ className,
49
+ )}
50
+ {...props}
51
+ />
52
+ );
53
+ }
54
+
55
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
56
+ return (
57
+ <div
58
+ data-slot="card-description"
59
+ className={cn(
60
+ "text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]",
61
+ className,
62
+ )}
63
+ {...props}
64
+ />
65
+ );
66
+ }
67
+
68
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
69
+ return <div data-slot="card-content" className={cn("p-5 pt-0", className)} {...props} />;
70
+ }
71
+
72
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
73
+ return (
74
+ <div
75
+ data-slot="card-footer"
76
+ className={cn(
77
+ "flex items-center gap-2 border-t border-[var(--ds-border-subtle)] p-4",
78
+ className,
79
+ )}
80
+ {...props}
81
+ />
82
+ );
83
+ }
84
+
85
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter };