@vvedantb/ui 0.17.1 → 0.18.1

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 (130) hide show
  1. package/README.md +251 -3
  2. package/package.json +13 -5
  3. package/shell/src/SidebarResize.tsx +1 -4
  4. package/src/ai-elements/__snapshots__/queue.test.tsx.snap +11 -0
  5. package/src/ai-elements/activity-question-utils.ts +26 -0
  6. package/src/ai-elements/activity-question.tsx +159 -0
  7. package/src/ai-elements/activity-shared.tsx +459 -0
  8. package/src/ai-elements/activity-step-detail.tsx +115 -0
  9. package/src/ai-elements/activity-step-label.ts +981 -0
  10. package/src/ai-elements/activity-tasks-utils.ts +199 -0
  11. package/src/ai-elements/activity-tasks.tsx +770 -0
  12. package/src/ai-elements/artifact.tsx +162 -52
  13. package/src/ai-elements/attachments.tsx +62 -32
  14. package/src/ai-elements/code-block.tsx +76 -22
  15. package/src/ai-elements/context.tsx +370 -0
  16. package/src/ai-elements/conversation.tsx +186 -62
  17. package/src/ai-elements/file-chip.tsx +53 -0
  18. package/src/ai-elements/message.tsx +161 -50
  19. package/src/ai-elements/model-picker-content.tsx +505 -0
  20. package/src/ai-elements/model-picker-types.ts +56 -0
  21. package/src/ai-elements/model-picker.tsx +301 -0
  22. package/src/ai-elements/model-selector.tsx +102 -21
  23. package/src/ai-elements/plan.tsx +130 -0
  24. package/src/ai-elements/prompt-input-settings.tsx +144 -0
  25. package/src/ai-elements/prompt-input-speech.tsx +189 -0
  26. package/src/ai-elements/prompt-input-traits.tsx +281 -0
  27. package/src/ai-elements/prompt-input.tsx +320 -134
  28. package/src/ai-elements/provider-icon.tsx +133 -0
  29. package/src/ai-elements/queue.tsx +293 -9
  30. package/src/ai-elements/shimmer.tsx +72 -1
  31. package/src/ai-elements/simple-model-ladder.tsx +108 -0
  32. package/src/ai-elements/speech-input.tsx +5 -3
  33. package/src/ai-elements/test-results.tsx +510 -0
  34. package/src/ai-elements/trace.tsx +243 -0
  35. package/src/ai-elements/use-streaming-disclosure.ts +50 -0
  36. package/src/ai-elements/web-preview.tsx +349 -0
  37. package/src/composer/input-area/ComposerSlashMenu.tsx +1 -1
  38. package/src/index.ts +66 -12
  39. package/src/kibo/avatar-stack.tsx +40 -0
  40. package/src/kibo/contribution-graph.tsx +539 -0
  41. package/src/kibo/cursor.tsx +72 -0
  42. package/src/kibo/gantt-features.tsx +439 -0
  43. package/src/kibo/gantt-header.tsx +181 -0
  44. package/src/kibo/gantt-provider.tsx +657 -0
  45. package/src/kibo/gantt-sidebar.tsx +157 -0
  46. package/src/kibo/gantt-timeline.tsx +154 -0
  47. package/src/kibo/index.ts +151 -0
  48. package/src/kibo/kanban.tsx +338 -0
  49. package/src/kibo/kanbanDropTarget.ts +25 -0
  50. package/src/kibo/list.tsx +162 -0
  51. package/src/kibo/theme-switcher.tsx +109 -0
  52. package/src/kibo/video-player.tsx +237 -0
  53. package/src/markdown/Markdown.tsx +112 -0
  54. package/src/markdown/MarkdownBlockquote.tsx +45 -0
  55. package/src/markdown/MarkdownCodeBlock.tsx +197 -0
  56. package/src/markdown/MarkdownInlineCode.tsx +40 -0
  57. package/src/markdown/MarkdownLink.tsx +92 -0
  58. package/src/markdown/codeFence.ts +57 -0
  59. package/src/markdown/codeHighlight.ts +138 -0
  60. package/src/markdown/incrementalBlocks.ts +182 -0
  61. package/src/markdown/index.ts +3 -0
  62. package/src/markdown/markdownClipboard.ts +178 -0
  63. package/src/markdown/mermaid.ts +155 -0
  64. package/src/markdown/rehypeGithubAlerts.ts +80 -0
  65. package/src/motion/presets.ts +24 -0
  66. package/src/ui/__snapshots__/alt-wrappers.test.tsx.snap +19 -0
  67. package/src/ui/__snapshots__/sonner-dom.test.tsx.snap +5 -0
  68. package/src/ui/_menu-parts.tsx +29 -0
  69. package/src/ui/accordion.alt.tsx +46 -53
  70. package/src/ui/accordion.tsx +59 -10
  71. package/src/ui/avatar.tsx +56 -0
  72. package/src/ui/badge.tsx +45 -21
  73. package/src/ui/border-beam.tsx +97 -0
  74. package/src/ui/button-group.tsx +50 -21
  75. package/src/ui/button.tsx +67 -45
  76. package/src/ui/calendar.tsx +48 -5
  77. package/src/ui/card.tsx +77 -8
  78. package/src/ui/carousel.tsx +60 -0
  79. package/src/ui/centered-spinner.tsx +30 -0
  80. package/src/ui/checkbox.tsx +34 -16
  81. package/src/ui/clear-input.alt.tsx +33 -75
  82. package/src/ui/clear-input.tsx +140 -15
  83. package/src/ui/collapsible.tsx +40 -2
  84. package/src/ui/command.tsx +143 -70
  85. package/src/ui/context-menu.tsx +159 -93
  86. package/src/ui/crossfade-icon.tsx +123 -0
  87. package/src/ui/dialog.tsx +154 -70
  88. package/src/ui/dropdown-menu.tsx +177 -117
  89. package/src/ui/hover-card.tsx +59 -17
  90. package/src/ui/input-group.alt.tsx +83 -83
  91. package/src/ui/input-group.tsx +129 -58
  92. package/src/ui/input.tsx +13 -4
  93. package/src/ui/label.tsx +11 -8
  94. package/src/ui/list-row.tsx +232 -0
  95. package/src/ui/loading-state.tsx +215 -0
  96. package/src/ui/popover.tsx +56 -23
  97. package/src/ui/progress.tsx +28 -16
  98. package/src/ui/refresh-spin-icon.tsx +41 -0
  99. package/src/ui/search-input.tsx +66 -15
  100. package/src/ui/select.tsx +171 -100
  101. package/src/ui/separator.tsx +23 -14
  102. package/src/ui/slider.tsx +21 -7
  103. package/src/ui/sonner.tsx +68 -30
  104. package/src/ui/spinner.tsx +229 -11
  105. package/src/ui/status-dot.tsx +84 -0
  106. package/src/ui/surface.tsx +49 -0
  107. package/src/ui/switch.tsx +29 -15
  108. package/src/ui/table.tsx +38 -16
  109. package/src/ui/tabs.tsx +123 -35
  110. package/src/ui/tabsSliding.ts +33 -1
  111. package/src/ui/textarea.tsx +13 -4
  112. package/src/ui/tooltip.tsx +50 -14
  113. package/src/ui/ui-config.tsx +149 -0
  114. package/src/utils/cat-color.ts +123 -0
  115. package/src/utils/dayjs.ts +6 -0
  116. package/src/utils/gesture.ts +59 -0
  117. package/src/utils/runtimeVisibility.ts +167 -0
  118. package/src/utils/sharedClock.ts +96 -0
  119. package/src/utils/surface-radius.ts +16 -0
  120. package/src/utils/useDragSensors.ts +58 -0
  121. package/tokens/examples/tokens-moved/Fixture.tsx +283 -0
  122. package/tokens/examples/tokens-moved/README.md +20 -0
  123. package/tokens/examples/tokens-moved/index.html +12 -0
  124. package/tokens/examples/tokens-moved/main.tsx +6 -0
  125. package/tokens/examples/tokens-moved/package.json +14 -0
  126. package/tokens/examples/tokens-moved/styles.css +12 -0
  127. package/tokens/examples/tokens-moved/tsconfig.json +14 -0
  128. package/tokens/examples/tokens-moved/vite.config.ts +11 -0
  129. package/tokens/src/css/border-beam.css +116 -0
  130. package/tokens/src/css/markdown.css +318 -0
package/src/ui/badge.tsx CHANGED
@@ -1,26 +1,36 @@
1
1
  import { cva, type VariantProps } from "class-variance-authority";
2
- import { cn } from "../utils/cn";
2
+ import { useCn, useUiConfig } from "./ui-config";
3
3
 
4
- const badgeVariants = cva(
5
- "inline-flex items-center gap-1 rounded-full border border-transparent px-2.5 py-1 text-xs font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background",
6
- {
7
- variants: {
8
- variant: {
9
- default: "border-border bg-default text-foreground",
10
- secondary: "border-separator bg-default text-default-foreground",
11
- destructive: "border-danger/25 bg-danger/12 text-danger",
12
- // Neutral border with a faint danger tint, e.g. a revoked or expired status.
13
- "destructive-soft": "border-border bg-danger/10 text-danger",
14
- outline: "border-separator bg-transparent text-muted-foreground",
15
- success: "border-success/25 bg-success/12 text-success",
16
- warning: "border-warning/25 bg-warning/12 text-warning",
17
- },
18
- },
19
- defaultVariants: {
20
- variant: "default",
21
- },
4
+ const badgeBaseClasses =
5
+ "inline-flex items-center gap-1 rounded-full border border-transparent px-2.5 py-1 text-xs font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background";
6
+
7
+ const badgeVariantClasses = {
8
+ default: "border-border bg-default text-foreground",
9
+ secondary: "border-separator bg-default text-default-foreground",
10
+ destructive: "border-danger/25 bg-danger/12 text-danger",
11
+ // Neutral border with a faint danger tint, e.g. a revoked or expired status.
12
+ "destructive-soft": "border-border bg-danger/10 text-danger",
13
+ outline: "border-separator bg-transparent text-muted-foreground",
14
+ success: "border-success/25 bg-success/12 text-success",
15
+ warning: "border-warning/25 bg-warning/12 text-warning",
16
+ };
17
+
18
+ const badgeVariants = cva(badgeBaseClasses, {
19
+ variants: {
20
+ variant: badgeVariantClasses,
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
22
24
  },
23
- );
25
+ });
26
+
27
+ /** `UiConfig.Badge`: each given class string replaces the built-in one. */
28
+ export interface BadgeConfig {
29
+ classes?: {
30
+ base?: string;
31
+ variant?: Partial<Record<keyof typeof badgeVariantClasses, string>>;
32
+ };
33
+ }
24
34
 
25
35
  export interface BadgeProps
26
36
  extends
@@ -28,8 +38,22 @@ export interface BadgeProps
28
38
  VariantProps<typeof badgeVariants> {}
29
39
 
30
40
  function Badge({ className, variant, ...props }: BadgeProps) {
41
+ const classes = useUiConfig("Badge")?.classes;
42
+ const merge = useCn();
43
+ const v = variant ?? "default";
31
44
  return (
32
- <div className={cn(badgeVariants({ variant }), className)} {...props} />
45
+ <div
46
+ className={
47
+ classes
48
+ ? merge(
49
+ classes.base ?? badgeBaseClasses,
50
+ classes.variant?.[v] ?? badgeVariantClasses[v],
51
+ className,
52
+ )
53
+ : merge(badgeVariants({ variant }), className)
54
+ }
55
+ {...props}
56
+ />
33
57
  );
34
58
  }
35
59
 
@@ -0,0 +1,97 @@
1
+ import type { ComponentPropsWithRef, ReactNode } from "react";
2
+ import { AnimatePresence, m } from "motion/react";
3
+
4
+ import { cn } from "../utils/cn";
5
+ import { tokenTransition } from "../motion/presets";
6
+ import { assignRef } from "../utils/ref";
7
+ import { useAnimOffscreenRef } from "../utils/runtimeVisibility";
8
+ import { useCn } from "./ui-config";
9
+
10
+ export type BorderBeamSize = "sm" | "md" | "lg";
11
+
12
+ /** Beam colour: neutral, the in-progress hue, or a two-colour sweep. */
13
+ export type BorderBeamColorVariant = "mono" | "progress" | "colorful";
14
+
15
+ export type BorderBeamProps = ComponentPropsWithRef<"div"> & {
16
+ children: ReactNode;
17
+ /**
18
+ * Runs the beam while true. Toggle this rather than mounting/unmounting
19
+ * <BorderBeam/> — the wrapper is a real DOM node, so conditional rendering
20
+ * would remount the whole subtree.
21
+ */
22
+ active: boolean;
23
+ size?: BorderBeamSize;
24
+ colorVariant?: BorderBeamColorVariant;
25
+ /**
26
+ * Wrapper classes. Give it the same radius as the child (e.g. `rounded-control`)
27
+ * — the beam inherits the wrapper's radius.
28
+ */
29
+ className?: string;
30
+ };
31
+
32
+ // `beam-*`, not `border-beam-*` — tailwind-merge reads a `border-` prefix as a
33
+ // border-colour utility and would drop the base class as a variant conflict.
34
+ const sizeClass: Record<BorderBeamSize, string> = {
35
+ sm: "beam-sm",
36
+ md: "",
37
+ lg: "beam-lg",
38
+ };
39
+
40
+ const colorClass: Record<BorderBeamColorVariant, string> = {
41
+ mono: "",
42
+ progress: "beam-progress",
43
+ colorful: "beam-colorful",
44
+ };
45
+
46
+ /**
47
+ * Wraps a surface in a hairline of light that travels around its border while
48
+ * `active`. The child needs an opaque background: it is what hides the
49
+ * gradient's interior so only the ring reads.
50
+ *
51
+ * The beam is CSS: import `@vvedantb/ui/tokens/css/border-beam.css` (or ship
52
+ * your own `.beam-*` rules). Colours are CSS variables on the wrapper or any
53
+ * ancestor: `--beam-color` (mono), `--beam-progress-color` (progress) and
54
+ * `--beam-colorful-from` / `--beam-colorful-to` (colorful).
55
+ *
56
+ * Spreads the rest props and ref onto the wrapper div so the whole thing can
57
+ * sit under a Radix `asChild` trigger.
58
+ */
59
+ export function BorderBeam({
60
+ children,
61
+ active,
62
+ size = "md",
63
+ colorVariant = "mono",
64
+ className,
65
+ ref,
66
+ ...rest
67
+ }: BorderBeamProps) {
68
+ const setOffscreen = useAnimOffscreenRef<HTMLDivElement>();
69
+ const merge = useCn();
70
+ return (
71
+ <div
72
+ className={merge("relative", className)}
73
+ ref={(node) => {
74
+ setOffscreen(node);
75
+ assignRef(ref, node);
76
+ }}
77
+ {...rest}
78
+ >
79
+ <AnimatePresence initial={false}>
80
+ {active ? (
81
+ <m.span
82
+ aria-hidden="true"
83
+ data-anim-chrome=""
84
+ className={cn("beam-root", sizeClass[size], colorClass[colorVariant])}
85
+ initial={{ opacity: 0 }}
86
+ animate={{ opacity: 1 }}
87
+ exit={{ opacity: 0 }}
88
+ transition={tokenTransition.slow}
89
+ >
90
+ <span className="beam" />
91
+ </m.span>
92
+ ) : null}
93
+ </AnimatePresence>
94
+ {children}
95
+ </div>
96
+ );
97
+ }
@@ -2,37 +2,60 @@ import type * as React from "react";
2
2
  import { cva, type VariantProps } from "class-variance-authority";
3
3
  import { Slot } from "radix-ui";
4
4
 
5
- import { cn } from "../utils/cn";
6
5
  import { Separator } from "./separator";
6
+ import { useCn, useUiConfig } from "./ui-config";
7
7
 
8
- const buttonGroupVariants = cva(
9
- "flex w-fit items-stretch focus-visible:*:z-10 focus-visible:*:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 [&>[data-slot=select-trigger]:last-of-type]:has-[select[aria-hidden=true]:last-child]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
10
- {
11
- variants: {
12
- orientation: {
13
- horizontal:
14
- "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
15
- vertical:
16
- "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
17
- },
18
- },
19
- defaultVariants: {
20
- orientation: "horizontal",
21
- },
8
+ /** `UiConfig.ButtonGroup`: each given class string replaces the built-in one. */
9
+ export interface ButtonGroupConfig {
10
+ classes?: {
11
+ base?: string;
12
+ orientation?: Partial<Record<"horizontal" | "vertical", string>>;
13
+ text?: string;
14
+ separator?: string;
15
+ };
16
+ }
17
+
18
+ const buttonGroupBaseClasses =
19
+ "flex w-fit items-stretch focus-visible:*:z-10 focus-visible:*:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 [&>[data-slot=select-trigger]:last-of-type]:has-[select[aria-hidden=true]:last-child]:rounded-r-md has-[>[data-slot=button-group]]:gap-2";
20
+
21
+ const buttonGroupOrientationClasses = {
22
+ horizontal:
23
+ "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
24
+ vertical:
25
+ "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
26
+ };
27
+
28
+ const buttonGroupVariants = cva(buttonGroupBaseClasses, {
29
+ variants: {
30
+ orientation: buttonGroupOrientationClasses,
31
+ },
32
+ defaultVariants: {
33
+ orientation: "horizontal",
22
34
  },
23
- );
35
+ });
24
36
 
25
37
  function ButtonGroup({
26
38
  className,
27
39
  orientation,
28
40
  ...props
29
41
  }: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
42
+ const classes = useUiConfig("ButtonGroup")?.classes;
43
+ const merge = useCn();
44
+ const o = orientation ?? "horizontal";
30
45
  return (
31
46
  <div
32
47
  role="group"
33
48
  data-slot="button-group"
34
49
  data-orientation={orientation}
35
- className={cn(buttonGroupVariants({ orientation }), className)}
50
+ className={
51
+ classes
52
+ ? merge(
53
+ classes.base ?? buttonGroupBaseClasses,
54
+ classes.orientation?.[o] ?? buttonGroupOrientationClasses[o],
55
+ className,
56
+ )
57
+ : merge(buttonGroupVariants({ orientation }), className)
58
+ }
36
59
  {...props}
37
60
  />
38
61
  );
@@ -46,11 +69,14 @@ function ButtonGroupText({
46
69
  asChild?: boolean;
47
70
  }) {
48
71
  const Comp = asChild ? Slot.Root : "div";
72
+ const base = useUiConfig("ButtonGroup")?.classes?.text;
73
+ const merge = useCn();
49
74
 
50
75
  return (
51
76
  <Comp
52
- className={cn(
53
- "bg-surface-secondary flex items-center gap-2 rounded-md px-4 text-sm font-medium [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
77
+ className={merge(
78
+ base ??
79
+ "bg-surface-secondary flex items-center gap-2 rounded-md px-4 text-sm font-medium [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
54
80
  className,
55
81
  )}
56
82
  {...props}
@@ -63,12 +89,15 @@ function ButtonGroupSeparator({
63
89
  orientation = "vertical",
64
90
  ...props
65
91
  }: React.ComponentProps<typeof Separator>) {
92
+ const base = useUiConfig("ButtonGroup")?.classes?.separator;
93
+ const merge = useCn();
66
94
  return (
67
95
  <Separator
68
96
  data-slot="button-group-separator"
69
97
  orientation={orientation}
70
- className={cn(
71
- "bg-separator relative m-0! self-stretch data-[orientation=vertical]:h-auto",
98
+ className={merge(
99
+ base ??
100
+ "bg-separator relative m-0! self-stretch data-[orientation=vertical]:h-auto",
72
101
  className,
73
102
  )}
74
103
  {...props}
package/src/ui/button.tsx CHANGED
@@ -1,45 +1,63 @@
1
1
  import * as React from "react";
2
2
  import { Slot } from "@radix-ui/react-slot";
3
3
  import { cva, type VariantProps } from "class-variance-authority";
4
- import { cn } from "../utils/cn";
4
+ import { Spinner } from "./spinner";
5
+ import { useCn, useUiConfig } from "./ui-config";
5
6
 
6
7
  const ghostClass =
7
8
  "text-muted-foreground hover:bg-surface-tertiary hover:text-foreground";
8
9
 
9
- const buttonVariants = cva(
10
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0",
11
- {
12
- variants: {
13
- variant: {
14
- default:
15
- "bg-primary text-primary-foreground hover:-translate-y-0.5 hover:opacity-90",
16
- destructive:
17
- "bg-[oklch(47.2%_0.195_25.7)] text-white hover:bg-[oklch(42.5%_0.185_25.7)]",
18
- outline:
19
- "border border-border bg-transparent text-foreground hover:bg-default",
20
- secondary: "bg-default text-default-foreground hover:bg-default/78",
21
- ghost: ghostClass,
22
- link: "text-primary underline-offset-4 hover:underline active:scale-100",
23
- // Ghost plus an override; tailwind-merge drops the clashing ghost classes.
24
- flat: `${ghostClass} bg-transparent text-muted hover:bg-surface-tertiary hover:text-foreground`, // check-tokens-allow: text-muted
25
- success: `${ghostClass} bg-success text-success-foreground hover:bg-success/90`,
26
- warning: `${ghostClass} bg-warning text-warning-foreground hover:bg-warning/90`,
27
- },
28
- size: {
29
- default: "h-10 px-4 py-2",
30
- sm: "h-8 rounded-lg px-3 text-xs max-sm:hit-target",
31
- lg: "h-11 px-6",
32
- icon: "h-10 w-10",
33
- "icon-sm": "h-8 w-8 rounded-lg max-sm:hit-target",
34
- "icon-xs": "h-7 w-7 rounded-md hit-target",
35
- },
36
- },
37
- defaultVariants: {
38
- variant: "default",
39
- size: "default",
40
- },
10
+ const buttonBaseClasses =
11
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0";
12
+
13
+ const buttonVariantClasses = {
14
+ default:
15
+ "bg-primary text-primary-foreground hover:-translate-y-0.5 hover:opacity-90",
16
+ destructive:
17
+ "bg-[oklch(47.2%_0.195_25.7)] text-white hover:bg-[oklch(42.5%_0.185_25.7)]",
18
+ outline:
19
+ "border border-border bg-transparent text-foreground hover:bg-default",
20
+ secondary: "bg-default text-default-foreground hover:bg-default/78",
21
+ ghost: ghostClass,
22
+ link: "text-primary underline-offset-4 hover:underline active:scale-100",
23
+ // Ghost plus an override; tailwind-merge drops the clashing ghost classes.
24
+ flat: `${ghostClass} bg-transparent text-muted hover:bg-surface-tertiary hover:text-foreground`, // check-tokens-allow: text-muted
25
+ success: `${ghostClass} bg-success text-success-foreground hover:bg-success/90`,
26
+ warning: `${ghostClass} bg-warning text-warning-foreground hover:bg-warning/90`,
27
+ };
28
+
29
+ const buttonSizeClasses = {
30
+ default: "h-10 px-4 py-2",
31
+ xs: "h-7 rounded-md px-2 text-xs hit-target [&_svg:not([class*='size-'])]:size-3.5",
32
+ sm: "h-8 rounded-lg px-3 text-xs max-sm:hit-target",
33
+ lg: "h-11 px-6",
34
+ icon: "h-10 w-10",
35
+ "icon-sm": "h-8 w-8 rounded-lg max-sm:hit-target",
36
+ "icon-xs": "h-7 w-7 rounded-md hit-target",
37
+ };
38
+
39
+ const buttonVariants = cva(buttonBaseClasses, {
40
+ variants: {
41
+ variant: buttonVariantClasses,
42
+ size: buttonSizeClasses,
41
43
  },
42
- );
44
+ defaultVariants: {
45
+ variant: "default",
46
+ size: "default",
47
+ },
48
+ });
49
+
50
+ type ButtonVariant = keyof typeof buttonVariantClasses;
51
+ type ButtonSize = keyof typeof buttonSizeClasses;
52
+
53
+ /** `UiConfig.Button`: each given class string replaces the built-in one. */
54
+ export interface ButtonConfig {
55
+ classes?: {
56
+ base?: string;
57
+ variant?: Partial<Record<ButtonVariant, string>>;
58
+ size?: Partial<Record<ButtonSize, string>>;
59
+ };
60
+ }
43
61
 
44
62
  export interface ButtonProps
45
63
  extends
@@ -54,31 +72,35 @@ export interface ButtonProps
54
72
  loading?: boolean;
55
73
  }
56
74
 
57
- function ButtonSpinner() {
58
- return (
59
- <svg className="h-4 w-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
60
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
61
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c3.042 1.135 5.824 3 7.938l3-2.647z" />
62
- </svg>
63
- );
64
- }
65
-
66
75
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
67
76
  (
68
77
  { className, variant, size, asChild = false, startContent, endContent, loading, children, ...props },
69
78
  ref,
70
79
  ) => {
71
80
  const Comp = asChild ? Slot : "button";
81
+ const classes = useUiConfig("Button")?.classes;
82
+ const merge = useCn();
83
+ const v = variant ?? "default";
84
+ const s = size ?? "default";
72
85
  return (
73
86
  <Comp
74
- className={cn(buttonVariants({ variant, size, className }))}
87
+ className={
88
+ classes
89
+ ? merge(
90
+ classes.base ?? buttonBaseClasses,
91
+ classes.variant?.[v] ?? buttonVariantClasses[v],
92
+ classes.size?.[s] ?? buttonSizeClasses[s],
93
+ className,
94
+ )
95
+ : merge(buttonVariants({ variant, size, className }))
96
+ }
75
97
  ref={ref}
76
98
  {...props}
77
99
  {...(loading ? { disabled: true, "data-loading": "true" } : null)}
78
100
  >
79
101
  {asChild ? children : (
80
102
  <>
81
- {loading ? <ButtonSpinner /> : startContent}
103
+ {loading ? <Spinner variant="ring" /> : startContent}
82
104
  {children}
83
105
  {endContent}
84
106
  </>
@@ -6,6 +6,9 @@ import {
6
6
  } from "@tabler/icons-react";
7
7
  import {
8
8
  type Chevron,
9
+ type ClassNames,
10
+ type CustomComponents,
11
+ type DayButton,
9
12
  DayPicker,
10
13
  type Root,
11
14
  getDefaultClassNames,
@@ -14,6 +17,18 @@ import { cn } from "../utils/cn";
14
17
  import {
15
18
  pickerFocusClass,
16
19
  } from "./_picker-classes";
20
+ import { useCn, useUiConfig } from "./ui-config";
21
+
22
+ /**
23
+ * `UiConfig.Calendar`. `classes.base` replaces the built-in root `className`
24
+ * classes; every other key replaces the built-in value of that DayPicker
25
+ * `classNames` key (a caller `classNames` still wins). `components` is the
26
+ * default for the prop, applied over the built-in Root and Chevron.
27
+ */
28
+ export interface CalendarConfig {
29
+ classes?: Partial<ClassNames> & { base?: string };
30
+ components?: Partial<CustomComponents>;
31
+ }
17
32
 
18
33
  // shadcn's v9 calendar on package tokens: no background of its own (it takes
19
34
  // the popover or card surface it sits on), selected days use the picker
@@ -57,14 +72,19 @@ function Calendar({
57
72
  ...props
58
73
  }: React.ComponentProps<typeof DayPicker>) {
59
74
  const defaultClassNames = getDefaultClassNames();
75
+ const config = useUiConfig("Calendar");
76
+ const merge = useCn();
77
+ const { base, ...configClassNames } = config?.classes ?? {};
60
78
 
61
79
  return (
62
80
  <DayPicker
63
81
  showOutsideDays={showOutsideDays}
64
- className={cn(
65
- "group/calendar p-1 text-foreground [--cell-size:--spacing(8)]",
66
- String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
67
- String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
82
+ className={merge(
83
+ base ?? [
84
+ "group/calendar p-1 text-foreground [--cell-size:--spacing(8)]",
85
+ String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
86
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
87
+ ],
68
88
  className,
69
89
  )}
70
90
  captionLayout={captionLayout}
@@ -146,11 +166,13 @@ function Calendar({
146
166
  range_middle: cn("rounded-none", defaultClassNames.range_middle),
147
167
  range_end: cn("rounded-r-lg", defaultClassNames.range_end),
148
168
  hidden: cn("invisible", defaultClassNames.hidden),
169
+ ...configClassNames,
149
170
  ...classNames,
150
171
  }}
151
172
  components={{
152
173
  Root: CalendarRoot,
153
174
  Chevron: CalendarChevron,
175
+ ...config?.components,
154
176
  ...components,
155
177
  }}
156
178
  {...props}
@@ -158,4 +180,25 @@ function Calendar({
158
180
  );
159
181
  }
160
182
 
161
- export { Calendar };
183
+ /**
184
+ * DayPicker's day button plus `data-selected`, so its classes can style the
185
+ * selected day on the button itself (`data-[selected=true]:bg-primary`).
186
+ * Pass it as `components.DayButton` (or through `UiConfig.Calendar.components`).
187
+ */
188
+ function CalendarDayButton({
189
+ day: _day,
190
+ modifiers,
191
+ ...props
192
+ }: React.ComponentProps<typeof DayButton>) {
193
+ const ref = React.useRef<HTMLButtonElement>(null);
194
+
195
+ // DayPicker decides which day has focus; moving DOM focus to match is what
196
+ // its own DayButton does.
197
+ React.useEffect(() => {
198
+ if (modifiers.focused) ref.current?.focus();
199
+ }, [modifiers.focused]);
200
+
201
+ return <button ref={ref} data-selected={modifiers.selected} {...props} />;
202
+ }
203
+
204
+ export { Calendar, CalendarDayButton };
package/src/ui/card.tsx CHANGED
@@ -1,5 +1,28 @@
1
1
  import * as React from "react";
2
- import { cn } from "../utils/cn";
2
+ import { useCn, useUiConfig } from "./ui-config";
3
+
4
+ export interface CardConfig {
5
+ /** Replaces the built-in classes of a part. A part left out keeps its default. */
6
+ classes?: {
7
+ root?: string;
8
+ header?: string;
9
+ title?: string;
10
+ description?: string;
11
+ content?: string;
12
+ footer?: string;
13
+ action?: string;
14
+ };
15
+ }
16
+
17
+ /** The app's class string for `part` (else `fallback`), merged with the caller `className`. */
18
+ function useCardClass(
19
+ part: keyof NonNullable<CardConfig["classes"]>,
20
+ fallback: string,
21
+ className: string | undefined,
22
+ ): string {
23
+ const merge = useCn();
24
+ return merge(useUiConfig("Card")?.classes?.[part] ?? fallback, className);
25
+ }
3
26
 
4
27
  const Card = React.forwardRef<
5
28
  HTMLDivElement,
@@ -7,10 +30,7 @@ const Card = React.forwardRef<
7
30
  >(({ className, ...props }, ref) => (
8
31
  <div
9
32
  ref={ref}
10
- className={cn(
11
- "rounded-lg bg-surface-card text-surface-foreground",
12
- className,
13
- )}
33
+ className={useCardClass("root", "rounded-lg bg-surface-card text-surface-foreground", className)}
14
34
  {...props}
15
35
  />
16
36
  ));
@@ -22,18 +42,67 @@ const CardHeader = React.forwardRef<
22
42
  >(({ className, ...props }, ref) => (
23
43
  <div
24
44
  ref={ref}
25
- className={cn("flex flex-col space-y-1.5 p-6", className)}
45
+ className={useCardClass("header", "flex flex-col space-y-1.5 p-6", className)}
26
46
  {...props}
27
47
  />
28
48
  ));
29
49
  CardHeader.displayName = "CardHeader";
30
50
 
51
+ const CardTitle = React.forwardRef<
52
+ HTMLDivElement,
53
+ React.HTMLAttributes<HTMLDivElement>
54
+ >(({ className, ...props }, ref) => (
55
+ <div
56
+ ref={ref}
57
+ className={useCardClass("title", "font-semibold leading-none tracking-[-0.02em]", className)}
58
+ {...props}
59
+ />
60
+ ));
61
+ CardTitle.displayName = "CardTitle";
62
+
63
+ const CardDescription = React.forwardRef<
64
+ HTMLDivElement,
65
+ React.HTMLAttributes<HTMLDivElement>
66
+ >(({ className, ...props }, ref) => (
67
+ <div
68
+ ref={ref}
69
+ className={useCardClass("description", "text-sm text-muted-foreground", className)}
70
+ {...props}
71
+ />
72
+ ));
73
+ CardDescription.displayName = "CardDescription";
74
+
31
75
  const CardContent = React.forwardRef<
32
76
  HTMLDivElement,
33
77
  React.HTMLAttributes<HTMLDivElement>
34
78
  >(({ className, ...props }, ref) => (
35
- <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
79
+ <div ref={ref} className={useCardClass("content", "p-6 pt-0", className)} {...props} />
36
80
  ));
37
81
  CardContent.displayName = "CardContent";
38
82
 
39
- export { Card, CardHeader, CardContent };
83
+ const CardFooter = React.forwardRef<
84
+ HTMLDivElement,
85
+ React.HTMLAttributes<HTMLDivElement>
86
+ >(({ className, ...props }, ref) => (
87
+ <div
88
+ ref={ref}
89
+ className={useCardClass("footer", "flex items-center p-6 pt-0", className)}
90
+ {...props}
91
+ />
92
+ ));
93
+ CardFooter.displayName = "CardFooter";
94
+
95
+ /** Top-right slot of a grid header (`grid-cols-[1fr_auto]`): spans the title and description rows. */
96
+ const CardAction = React.forwardRef<
97
+ HTMLDivElement,
98
+ React.HTMLAttributes<HTMLDivElement>
99
+ >(({ className, ...props }, ref) => (
100
+ <div
101
+ ref={ref}
102
+ className={useCardClass("action", "col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
103
+ {...props}
104
+ />
105
+ ));
106
+ CardAction.displayName = "CardAction";
107
+
108
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, CardAction };