@boyernick/standard-ui-react 0.1.1-canary.9 → 0.2.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 (82) hide show
  1. package/package.json +5 -3
  2. package/src/accordion.tsx +5 -2
  3. package/src/alert-dialog.tsx +1 -1
  4. package/src/attachment.tsx +5 -2
  5. package/src/autocomplete.tsx +4 -3
  6. package/src/block-editor.tsx +1747 -0
  7. package/src/brand.tsx +2 -2
  8. package/src/breadcrumb.tsx +14 -7
  9. package/src/button.tsx +22 -11
  10. package/src/calendar.tsx +6 -3
  11. package/src/carousel.tsx +220 -53
  12. package/src/checkbox.tsx +3 -2
  13. package/src/code-block.tsx +66 -5
  14. package/src/collapsible.tsx +4 -2
  15. package/src/combobox.tsx +4 -3
  16. package/src/command.tsx +27 -12
  17. package/src/dialog.tsx +33 -10
  18. package/src/empty.tsx +4 -4
  19. package/src/field.tsx +4 -3
  20. package/src/filter-group.tsx +143 -0
  21. package/src/icons.tsx +28 -1
  22. package/src/illustrations.tsx +219 -141
  23. package/src/index.ts +238 -18
  24. package/src/input.tsx +8 -5
  25. package/src/kbd.tsx +10 -4
  26. package/src/lib/focus.ts +41 -0
  27. package/src/lib/popup.ts +1 -1
  28. package/src/lifeline/company-icon.tsx +71 -0
  29. package/src/lifeline/index.ts +42 -0
  30. package/src/lifeline/lifeline-data.ts +97 -0
  31. package/src/lifeline/lifeline-desktop.tsx +204 -0
  32. package/src/lifeline/lifeline-event.tsx +108 -0
  33. package/src/lifeline/lifeline-fireworks.tsx +302 -0
  34. package/src/lifeline/lifeline-hover-image.tsx +269 -0
  35. package/src/lifeline/lifeline-icons.tsx +39 -0
  36. package/src/lifeline/lifeline-intro-timing.ts +105 -0
  37. package/src/lifeline/lifeline-labels.tsx +24 -0
  38. package/src/lifeline/lifeline-layout.ts +1 -0
  39. package/src/lifeline/lifeline-legend.tsx +31 -0
  40. package/src/lifeline/lifeline-lightbox.tsx +309 -0
  41. package/src/lifeline/lifeline-marker.tsx +183 -0
  42. package/src/lifeline/lifeline-people.tsx +99 -0
  43. package/src/lifeline/lifeline-photos.tsx +366 -0
  44. package/src/lifeline/lifeline-shell.tsx +135 -0
  45. package/src/lifeline/lifeline-utils.ts +83 -0
  46. package/src/lifeline/lifeline-vertical.tsx +482 -0
  47. package/src/lifeline/lifeline.tsx +69 -0
  48. package/src/lifeline/types.ts +112 -0
  49. package/src/lifeline/use-lifeline-intro.ts +130 -0
  50. package/src/lifeline/use-lifeline-scroll.ts +1011 -0
  51. package/src/lifeline/use-lifeline-vertical-scroll.ts +271 -0
  52. package/src/menubar.tsx +9 -2
  53. package/src/minimap.tsx +296 -0
  54. package/src/modal.tsx +608 -0
  55. package/src/navigation-menu.tsx +338 -67
  56. package/src/number-field.tsx +336 -61
  57. package/src/otp-field.tsx +4 -3
  58. package/src/pagination.tsx +262 -42
  59. package/src/password-protection.tsx +345 -0
  60. package/src/popover.tsx +1 -1
  61. package/src/progress.tsx +5 -0
  62. package/src/questionnaire.tsx +284 -0
  63. package/src/radio.tsx +4 -2
  64. package/src/scroll-area.tsx +4 -1
  65. package/src/select.tsx +5 -2
  66. package/src/sidebar.tsx +113 -28
  67. package/src/slider.tsx +66 -6
  68. package/src/sounds.tsx +6 -10
  69. package/src/spinner.tsx +105 -32
  70. package/src/switch.tsx +4 -1
  71. package/src/table.tsx +82 -15
  72. package/src/tabs.tsx +189 -38
  73. package/src/text-animate.tsx +71 -28
  74. package/src/textarea.tsx +6 -1
  75. package/src/timeline.tsx +378 -0
  76. package/src/toast.tsx +214 -35
  77. package/src/toggle.tsx +4 -2
  78. package/src/toolbar.tsx +12 -7
  79. package/src/tooltip.tsx +43 -3
  80. package/src/video-player.tsx +396 -127
  81. package/src/image-modal.tsx +0 -110
  82. package/src/markdown-editor.tsx +0 -302
package/src/sounds.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  "use client"
2
2
 
3
+ import { IconPlay } from "@central-icons-react/round-filled-radius-2-stroke-2/IconPlay"
3
4
  import {
4
5
  createContext,
5
6
  useCallback,
@@ -28,7 +29,7 @@ const SOUND_PRESETS: SoundPreset[] = [
28
29
  {
29
30
  id: "click",
30
31
  label: "Click",
31
- description: "Light confirmation for toggles and secondary actions.",
32
+ description: "Confirmation for toggles and secondary actions.",
32
33
  play: (ctx, when, destination) => {
33
34
  const osc = ctx.createOscillator()
34
35
  const gain = ctx.createGain()
@@ -219,14 +220,15 @@ export const Sound = ({
219
220
  >
220
221
  <Button
221
222
  type="button"
222
- variant="outline"
223
- size="sm"
223
+ variant="secondary"
224
+ size="md"
224
225
  iconOnly
225
226
  rounded
227
+ className="shrink-0 hover:scale-[1.04] active:scale-95"
226
228
  aria-label={`Play ${resolvedLabel}`}
227
229
  onClick={handlePlay}
228
230
  >
229
- <PlayIcon />
231
+ <IconPlay size={12} mode="raw" className="size-3" aria-hidden />
230
232
  </Button>
231
233
  <div className="min-w-0 flex-1">
232
234
  <p className="text-sm-strong text-fg-primary">{resolvedLabel}</p>
@@ -262,9 +264,3 @@ export const SoundToggle = ({
262
264
  </Button>
263
265
  )
264
266
  }
265
-
266
- const PlayIcon = () => (
267
- <svg viewBox="0 0 24 24" fill="none" className="size-4" aria-hidden>
268
- <path d="M9 7.5v9l8-4.5-8-4.5Z" fill="currentColor" />
269
- </svg>
270
- )
package/src/spinner.tsx CHANGED
@@ -2,44 +2,117 @@ import { cva, type VariantProps } from "class-variance-authority";
2
2
  import type { ComponentProps } from "react";
3
3
  import { cn } from "./lib/cn";
4
4
 
5
+ const SPINNER_SPOKES = Array.from({ length: 12 }, (_, index) => index);
6
+
5
7
  const spinnerVariants = cva(
6
- "animate-spin text-current motion-reduce:animate-none",
7
- { variants: {
8
- size: {
9
- sm: "size-3.5",
10
- md: "size-4",
11
- lg: "size-5",
8
+ "text-current motion-reduce:animate-none",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ ring: "animate-spin",
13
+ arc: "animate-spin",
14
+ dots: "",
15
+ spokes: "",
16
+ },
17
+ size: {
18
+ sm: "size-3.5",
19
+ md: "size-4",
20
+ lg: "size-5",
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ variant: "ring",
25
+ size: "md",
12
26
  },
13
27
  },
14
- defaultVariants: {
15
- size: "md",
16
- },
17
- });
28
+ );
18
29
 
19
30
  export type SpinnerProps = ComponentProps<"svg"> &
20
31
  VariantProps<typeof spinnerVariants>;
21
32
 
22
- export const Spinner = ({ className, size, ...props }: SpinnerProps) => (
23
- <svg
24
- role="status"
25
- aria-label="Loading"
26
- className={cn(spinnerVariants({ size }), className)}
27
- viewBox="0 0 24 24"
28
- fill="none"
29
- {...props}
30
- >
31
- <path
32
- d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
33
- stroke="currentColor"
34
- strokeOpacity="0.3"
35
- strokeWidth="2"
36
- />
37
- <path
38
- d="M21 12C21 16.9706 16.9706 21 12 21"
39
- stroke="currentColor"
40
- strokeWidth="2"
41
- />
42
- </svg>
43
- );
33
+ export const Spinner = ({
34
+ className,
35
+ variant,
36
+ size,
37
+ ...props
38
+ }: SpinnerProps) => {
39
+ const resolvedVariant = variant ?? "ring";
40
+
41
+ return (
42
+ <svg
43
+ role="status"
44
+ aria-label="Loading"
45
+ className={cn(
46
+ spinnerVariants({ variant: resolvedVariant, size }),
47
+ className,
48
+ )}
49
+ viewBox="0 0 24 24"
50
+ fill="none"
51
+ {...props}
52
+ >
53
+ {resolvedVariant === "spokes" ? (
54
+ <g stroke="currentColor" strokeWidth="2.25" strokeLinecap="round">
55
+ {SPINNER_SPOKES.map((spoke) => (
56
+ <line
57
+ key={spoke}
58
+ x1="12"
59
+ y1="3"
60
+ x2="12"
61
+ y2="6.25"
62
+ transform={`rotate(${spoke * 30} 12 12)`}
63
+ className="animate-[spinner-spoke_1.2s_linear_infinite] motion-reduce:animate-none motion-reduce:opacity-40"
64
+ style={{ animationDelay: `${spoke * -0.1}s` }}
65
+ />
66
+ ))}
67
+ </g>
68
+ ) : resolvedVariant === "dots" ? (
69
+ <g fill="currentColor">
70
+ <circle
71
+ cx="5"
72
+ cy="12"
73
+ r="2.25"
74
+ className="animate-[spinner-dot_1.2s_var(--ease-passive)_infinite] [animation-delay:-0.32s] motion-reduce:animate-none"
75
+ />
76
+ <circle
77
+ cx="12"
78
+ cy="12"
79
+ r="2.25"
80
+ className="animate-[spinner-dot_1.2s_var(--ease-passive)_infinite] [animation-delay:-0.16s] motion-reduce:animate-none"
81
+ />
82
+ <circle
83
+ cx="19"
84
+ cy="12"
85
+ r="2.25"
86
+ className="animate-[spinner-dot_1.2s_var(--ease-passive)_infinite] motion-reduce:animate-none"
87
+ />
88
+ </g>
89
+ ) : resolvedVariant === "arc" ? (
90
+ <circle
91
+ cx="12"
92
+ cy="12"
93
+ r="9"
94
+ stroke="currentColor"
95
+ strokeWidth="2.5"
96
+ strokeLinecap="round"
97
+ strokeDasharray="42 15"
98
+ />
99
+ ) : (
100
+ <>
101
+ <path
102
+ d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
103
+ stroke="currentColor"
104
+ strokeOpacity="0.3"
105
+ strokeWidth="2"
106
+ />
107
+ <path
108
+ d="M21 12C21 16.9706 16.9706 21 12 21"
109
+ stroke="currentColor"
110
+ strokeWidth="2"
111
+ />
112
+ </>
113
+ )}
114
+ </svg>
115
+ );
116
+ };
44
117
 
45
118
  export { spinnerVariants };
package/src/switch.tsx CHANGED
@@ -3,13 +3,16 @@
3
3
  import { Switch as BaseSwitch } from "@base-ui/react/switch"
4
4
  import type { ComponentProps } from "react"
5
5
  import { cn } from "./lib/cn"
6
+ import { focusRing, focusRingBorder } from "./lib/focus"
6
7
 
7
8
  export type SwitchProps = ComponentProps<typeof BaseSwitch.Root>
8
9
 
9
10
  export const Switch = ({ className, ...props }: SwitchProps) => (
10
11
  <BaseSwitch.Root
11
12
  className={cn(
12
- "relative inline-flex h-6 w-10 shrink-0 items-center rounded-full bg-background-quaternary transition-colors duration-[var(--duration-sm)] ease-enter motion-reduce:transition-none outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-checked:bg-brand-primary data-disabled:cursor-not-allowed data-disabled:opacity-50",
13
+ "relative inline-flex h-6 w-10 shrink-0 items-center rounded-full bg-background-quaternary transition-colors duration-[var(--duration-sm)] ease-enter motion-reduce:transition-none data-checked:bg-brand-primary data-disabled:cursor-not-allowed data-disabled:opacity-50",
14
+ focusRingBorder,
15
+ focusRing,
13
16
  className,
14
17
  )}
15
18
  {...props}
package/src/table.tsx CHANGED
@@ -1,8 +1,60 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
1
2
  import type { ComponentProps } from "react";
2
3
  import { cn } from "./lib/cn";
3
4
 
4
- export type TableProps = ComponentProps<"table">;
5
- export type TableHeaderProps = ComponentProps<"thead">;
5
+ const tableVariants = cva(
6
+ "group/table w-full text-sm text-fg-primary",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "border-collapse",
11
+ grid: [
12
+ "border-separate border-spacing-0",
13
+ "[&_tr]:border-b-0",
14
+ "[&_tr>*]:border-r [&_tr>*]:border-b [&_tr>*]:border-border-primary",
15
+ "[&_tr>*:last-child]:border-r-0",
16
+ "[&_tbody_tr:last-child>*]:border-b-0 [&_tfoot_tr:last-child>*]:border-b-0",
17
+ ],
18
+ },
19
+ density: {
20
+ default: "",
21
+ compact: "[&_th]:h-8 [&_th]:px-3 [&_td]:px-3 [&_td]:py-2",
22
+ },
23
+ striped: {
24
+ true: "[&_tbody_tr:nth-child(even)]:bg-background-secondary",
25
+ false: "",
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ variant: "default",
30
+ density: "default",
31
+ striped: false,
32
+ },
33
+ },
34
+ );
35
+
36
+ const tableHeaderVariants = cva(
37
+ "bg-background-secondary [&_tr]:border-b [&_tr]:border-border-primary",
38
+ {
39
+ variants: {
40
+ sticky: {
41
+ true: "[&_th]:sticky [&_th]:top-0 [&_th]:z-10 [&_th]:bg-background-secondary [&_th]:py-2.5",
42
+ false: "",
43
+ },
44
+ },
45
+ defaultVariants: {
46
+ sticky: false,
47
+ },
48
+ },
49
+ );
50
+
51
+ export type TableProps = ComponentProps<"table"> &
52
+ VariantProps<typeof tableVariants> & {
53
+ /** Classes for the bordered horizontal-scroll container. */
54
+ containerClassName?: string;
55
+ };
56
+ export type TableHeaderProps = ComponentProps<"thead"> &
57
+ VariantProps<typeof tableHeaderVariants>;
6
58
  export type TableBodyProps = ComponentProps<"tbody">;
7
59
  export type TableFooterProps = ComponentProps<"tfoot">;
8
60
  export type TableRowProps = ComponentProps<"tr">;
@@ -10,24 +62,37 @@ export type TableHeadProps = ComponentProps<"th">;
10
62
  export type TableCellProps = ComponentProps<"td">;
11
63
  export type TableCaptionProps = ComponentProps<"caption">;
12
64
 
13
- export const Table = ({ className, ...props }: TableProps) => (
14
- <div className="w-full overflow-x-auto rounded-lg border border-border-primary bg-surface">
65
+ export const Table = ({
66
+ className,
67
+ containerClassName,
68
+ variant,
69
+ density,
70
+ striped,
71
+ ...props
72
+ }: TableProps) => (
73
+ <div
74
+ className={cn(
75
+ "w-full overflow-auto rounded-lg border border-border-primary bg-surface",
76
+ containerClassName,
77
+ )}
78
+ >
15
79
  <table
16
- className={cn(
17
- "w-full border-collapse text-sm text-fg-primary",
18
- className,
19
- )}
80
+ data-variant={variant ?? "default"}
81
+ data-density={density ?? "default"}
82
+ data-striped={striped || undefined}
83
+ className={cn(tableVariants({ variant, density, striped }), className)}
20
84
  {...props}
21
85
  />
22
86
  </div>
23
87
  );
24
88
 
25
- export const TableHeader = ({ className, ...props }: TableHeaderProps) => (
89
+ export const TableHeader = ({
90
+ className,
91
+ sticky,
92
+ ...props
93
+ }: TableHeaderProps) => (
26
94
  <thead
27
- className={cn(
28
- "bg-background-secondary [&_tr]:border-b [&_tr]:border-border-primary",
29
- className,
30
- )}
95
+ className={cn(tableHeaderVariants({ sticky }), className)}
31
96
  {...props}
32
97
  />
33
98
  );
@@ -39,7 +104,7 @@ export const TableBody = ({ className, ...props }: TableBodyProps) => (
39
104
  export const TableFooter = ({ className, ...props }: TableFooterProps) => (
40
105
  <tfoot
41
106
  className={cn(
42
- "border-t border-border-primary bg-background-secondary font-medium",
107
+ "border-t border-border-primary bg-background-secondary font-medium [&_tr]:hover:bg-transparent",
43
108
  className,
44
109
  )}
45
110
  {...props}
@@ -49,7 +114,7 @@ export const TableFooter = ({ className, ...props }: TableFooterProps) => (
49
114
  export const TableRow = ({ className, ...props }: TableRowProps) => (
50
115
  <tr
51
116
  className={cn(
52
- "border-b border-border-primary hover:bg-background-secondary",
117
+ "border-b border-border-primary transition-colors duration-[var(--duration-sm)] ease-enter hover:bg-background-secondary aria-selected:bg-background-tertiary data-[selected]:bg-background-tertiary data-[state=selected]:bg-background-tertiary",
53
118
  className,
54
119
  )}
55
120
  {...props}
@@ -79,3 +144,5 @@ export const TableCaption = ({ className, ...props }: TableCaptionProps) => (
79
144
  {...props}
80
145
  />
81
146
  );
147
+
148
+ export { tableHeaderVariants, tableVariants };
package/src/tabs.tsx CHANGED
@@ -1,62 +1,213 @@
1
1
  "use client"
2
2
 
3
3
  import { Tabs as BaseTabs } from "@base-ui/react/tabs"
4
- import type { ComponentProps } from "react"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+ import { createContext, type ComponentProps, useContext } from "react"
5
6
  import { cn } from "./lib/cn"
7
+ import { focusRing, focusRingBorder } from "./lib/focus"
6
8
  import { motion } from "./lib/motion"
7
9
 
8
- export type TabsProps = ComponentProps<typeof BaseTabs.Root>
9
- export type TabsListProps = ComponentProps<typeof BaseTabs.List>
10
- export type TabsTabProps = ComponentProps<typeof BaseTabs.Tab>
11
- export type TabsIndicatorProps = ComponentProps<typeof BaseTabs.Indicator>
12
- export type TabsPanelProps = ComponentProps<typeof BaseTabs.Panel>
10
+ const tabsListVariants = cva("relative flex items-center", {
11
+ variants: {
12
+ variant: {
13
+ underline:
14
+ "w-full gap-0.5 border-b border-border-primary data-[orientation=vertical]:w-40 data-[orientation=vertical]:shrink-0 data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch data-[orientation=vertical]:gap-0 data-[orientation=vertical]:border-r data-[orientation=vertical]:border-b-0",
15
+ segmented:
16
+ "w-fit rounded-lg bg-background-tertiary p-0.5 data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch",
17
+ pill: "w-fit gap-1 data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "underline",
22
+ },
23
+ })
13
24
 
14
- export const Tabs = ({ className, ...props }: TabsProps) => (
15
- <BaseTabs.Root className={cn("flex w-full flex-col", className)} {...props} />
25
+ const tabsTabVariants = cva(
26
+ cn(
27
+ "relative z-10 inline-flex shrink-0 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap text-fg-tertiary select-none hover:text-fg-primary data-active:text-fg-primary data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
28
+ focusRingBorder,
29
+ focusRing,
30
+ ),
31
+ {
32
+ variants: {
33
+ variant: {
34
+ underline:
35
+ "data-[orientation=horizontal]:pb-1 data-[orientation=vertical]:w-full data-[orientation=vertical]:justify-start",
36
+ segmented: "rounded-md",
37
+ pill: "rounded-full bg-background-tertiary data-active:bg-transparent",
38
+ },
39
+ size: {
40
+ sm: "text-xs [&_svg]:size-3.5",
41
+ md: "text-sm [&_svg]:size-4",
42
+ lg: "text-md [&_svg]:size-4",
43
+ },
44
+ },
45
+ compoundVariants: [
46
+ {
47
+ variant: "underline",
48
+ size: "sm",
49
+ class: "h-8 px-2.5 data-[orientation=horizontal]:h-9",
50
+ },
51
+ {
52
+ variant: "underline",
53
+ size: "md",
54
+ class: "h-10 px-3 data-[orientation=horizontal]:h-11",
55
+ },
56
+ {
57
+ variant: "underline",
58
+ size: "lg",
59
+ class: "h-12 px-3.5 data-[orientation=horizontal]:h-13",
60
+ },
61
+ { variant: "segmented", size: "sm", class: "h-7 px-2.5" },
62
+ { variant: "segmented", size: "md", class: "h-8 px-3" },
63
+ { variant: "segmented", size: "lg", class: "h-9 px-3.5" },
64
+ { variant: "pill", size: "sm", class: "h-7 px-2.5" },
65
+ { variant: "pill", size: "md", class: "h-8 px-3" },
66
+ { variant: "pill", size: "lg", class: "h-9 px-3.5" },
67
+ ],
68
+ defaultVariants: {
69
+ variant: "underline",
70
+ size: "md",
71
+ },
72
+ },
16
73
  )
17
74
 
18
- export const TabsList = ({ className, ...props }: TabsListProps) => (
19
- <BaseTabs.List
20
- className={cn(
21
- "relative flex gap-0.5 border-b border-border-primary",
22
- className,
23
- )}
24
- {...props}
25
- />
26
- )
75
+ const tabsIndicatorVariants = cva("absolute z-0", {
76
+ variants: {
77
+ variant: {
78
+ underline:
79
+ "bottom-0 left-0 h-0.5 w-(--active-tab-width) translate-x-(--active-tab-left) rounded-full bg-brand-primary data-[orientation=vertical]:top-0 data-[orientation=vertical]:right-0 data-[orientation=vertical]:bottom-auto data-[orientation=vertical]:left-auto data-[orientation=vertical]:h-(--active-tab-height) data-[orientation=vertical]:w-0.5 data-[orientation=vertical]:translate-x-0 data-[orientation=vertical]:translate-y-(--active-tab-top)",
80
+ segmented:
81
+ "inset-y-0.5 left-0 w-(--active-tab-width) translate-x-(--active-tab-left) rounded-md bg-surface-raised shadow-hairline data-[orientation=vertical]:inset-x-0.5 data-[orientation=vertical]:top-0 data-[orientation=vertical]:bottom-auto data-[orientation=vertical]:h-(--active-tab-height) data-[orientation=vertical]:w-auto data-[orientation=vertical]:translate-x-0 data-[orientation=vertical]:translate-y-(--active-tab-top)",
82
+ pill: "inset-y-0 left-0 w-(--active-tab-width) translate-x-(--active-tab-left) rounded-full bg-surface-raised shadow-hairline data-[orientation=vertical]:inset-x-0 data-[orientation=vertical]:top-0 data-[orientation=vertical]:bottom-auto data-[orientation=vertical]:h-(--active-tab-height) data-[orientation=vertical]:w-auto data-[orientation=vertical]:translate-x-0 data-[orientation=vertical]:translate-y-(--active-tab-top)",
83
+ },
84
+ },
85
+ defaultVariants: {
86
+ variant: "underline",
87
+ },
88
+ })
27
89
 
28
- export const TabsTab = ({ className, ...props }: TabsTabProps) => (
29
- <BaseTabs.Tab
30
- className={cn(
31
- "relative cursor-pointer px-3 py-2.5 text-sm text-fg-tertiary outline-none select-none",
32
- motion.colors,
33
- "hover:text-fg-primary outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-active:text-fg-primary data-disabled:cursor-not-allowed data-disabled:opacity-50",
34
- className,
35
- )}
36
- {...props}
37
- />
90
+ export type TabsVariant = NonNullable<
91
+ VariantProps<typeof tabsListVariants>["variant"]
92
+ >
93
+ export type TabsSize = NonNullable<
94
+ VariantProps<typeof tabsTabVariants>["size"]
95
+ >
96
+
97
+ type TabsStyleContextValue = {
98
+ variant: TabsVariant
99
+ size: TabsSize
100
+ }
101
+
102
+ const TabsStyleContext = createContext<TabsStyleContextValue>({
103
+ variant: "underline",
104
+ size: "md",
105
+ })
106
+
107
+ export type TabsProps = ComponentProps<typeof BaseTabs.Root> & {
108
+ /** Visual treatment shared by the list, tabs, and indicator. */
109
+ variant?: TabsVariant
110
+ /** Height and typography shared by every tab. */
111
+ size?: TabsSize
112
+ }
113
+ export type TabsListProps = ComponentProps<typeof BaseTabs.List> &
114
+ VariantProps<typeof tabsListVariants>
115
+ export type TabsTabProps = ComponentProps<typeof BaseTabs.Tab> &
116
+ VariantProps<typeof tabsTabVariants>
117
+ export type TabsIndicatorProps = ComponentProps<typeof BaseTabs.Indicator> &
118
+ VariantProps<typeof tabsIndicatorVariants>
119
+ export type TabsPanelProps = ComponentProps<typeof BaseTabs.Panel>
120
+
121
+ export const Tabs = ({
122
+ className,
123
+ variant = "underline",
124
+ size = "md",
125
+ ...props
126
+ }: TabsProps) => (
127
+ <TabsStyleContext.Provider value={{ variant, size }}>
128
+ <BaseTabs.Root
129
+ data-slot="tabs"
130
+ className={cn(
131
+ "flex w-full flex-col data-[orientation=vertical]:flex-row data-[orientation=vertical]:gap-6",
132
+ className,
133
+ )}
134
+ {...props}
135
+ />
136
+ </TabsStyleContext.Provider>
38
137
  )
39
138
 
139
+ export const TabsList = ({
140
+ className,
141
+ variant,
142
+ ...props
143
+ }: TabsListProps) => {
144
+ const styles = useContext(TabsStyleContext)
145
+
146
+ return (
147
+ <BaseTabs.List
148
+ data-slot="tabs-list"
149
+ className={cn(
150
+ tabsListVariants({ variant: variant ?? styles.variant }),
151
+ className,
152
+ )}
153
+ {...props}
154
+ />
155
+ )
156
+ }
157
+
158
+ export const TabsTab = ({
159
+ className,
160
+ variant,
161
+ size,
162
+ ...props
163
+ }: TabsTabProps) => {
164
+ const styles = useContext(TabsStyleContext)
165
+
166
+ return (
167
+ <BaseTabs.Tab
168
+ data-slot="tabs-tab"
169
+ className={cn(
170
+ tabsTabVariants({
171
+ variant: variant ?? styles.variant,
172
+ size: size ?? styles.size,
173
+ }),
174
+ motion.colors,
175
+ className,
176
+ )}
177
+ {...props}
178
+ />
179
+ )
180
+ }
181
+
40
182
  export const TabsIndicator = ({
41
183
  className,
184
+ variant,
42
185
  ...props
43
- }: TabsIndicatorProps) => (
44
- <BaseTabs.Indicator
45
- className={cn(
46
- "absolute bottom-0 left-0 z-10 h-0.5 w-(--active-tab-width) translate-x-(--active-tab-left) rounded-full bg-brand-primary",
47
- motion.tabsIndicator,
48
- className,
49
- )}
50
- {...props}
51
- />
52
- )
186
+ }: TabsIndicatorProps) => {
187
+ const styles = useContext(TabsStyleContext)
188
+
189
+ return (
190
+ <BaseTabs.Indicator
191
+ data-slot="tabs-indicator"
192
+ className={cn(
193
+ tabsIndicatorVariants({ variant: variant ?? styles.variant }),
194
+ motion.tabsIndicator,
195
+ className,
196
+ )}
197
+ {...props}
198
+ />
199
+ )
200
+ }
53
201
 
54
202
  export const TabsPanel = ({ className, ...props }: TabsPanelProps) => (
55
203
  <BaseTabs.Panel
204
+ data-slot="tabs-panel"
56
205
  className={cn(
57
- "pt-4 text-sm leading-relaxed text-fg-secondary outline-none focus-visible:outline-none",
206
+ "pt-4 text-sm leading-relaxed text-fg-secondary outline-none focus-visible:outline-none data-[orientation=vertical]:pt-0",
58
207
  className,
59
208
  )}
60
209
  {...props}
61
210
  />
62
211
  )
212
+
213
+ export { tabsIndicatorVariants, tabsListVariants, tabsTabVariants }