rechta-ds 0.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 (58) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.github/workflows/release.yml +53 -0
  3. package/.github/workflows/storybook.yml +34 -0
  4. package/.storybook/main.ts +17 -0
  5. package/.storybook/preview.ts +35 -0
  6. package/CHANGELOG.md +65 -0
  7. package/CONTRIBUTING.md +106 -0
  8. package/README.md +206 -0
  9. package/package.json +30 -0
  10. package/packages/tokens/build.js +357 -0
  11. package/packages/tokens/package.json +44 -0
  12. package/packages/tokens/src/tokens.json +1538 -0
  13. package/packages/ui/.storybook/main.ts +17 -0
  14. package/packages/ui/.storybook/preview.tsx +37 -0
  15. package/packages/ui/package.json +109 -0
  16. package/packages/ui/postcss.config.js +6 -0
  17. package/packages/ui/src/components/atoms/Avatar.tsx +139 -0
  18. package/packages/ui/src/components/atoms/Badge.tsx +62 -0
  19. package/packages/ui/src/components/atoms/Button.tsx +125 -0
  20. package/packages/ui/src/components/atoms/Input.tsx +116 -0
  21. package/packages/ui/src/components/atoms/Misc.tsx +128 -0
  22. package/packages/ui/src/components/atoms/Toggle.tsx +191 -0
  23. package/packages/ui/src/components/atoms/Typography.tsx +178 -0
  24. package/packages/ui/src/components/atoms/index.ts +7 -0
  25. package/packages/ui/src/components/charts/Charts.tsx +380 -0
  26. package/packages/ui/src/components/charts/DataTable.tsx +222 -0
  27. package/packages/ui/src/components/charts/index.ts +19 -0
  28. package/packages/ui/src/components/molecules/Accordion.tsx +93 -0
  29. package/packages/ui/src/components/molecules/Card.tsx +100 -0
  30. package/packages/ui/src/components/molecules/PricingCard.tsx +196 -0
  31. package/packages/ui/src/components/molecules/TestimonialCard.tsx +85 -0
  32. package/packages/ui/src/components/molecules/Tooltip.tsx +71 -0
  33. package/packages/ui/src/components/molecules/index.ts +5 -0
  34. package/packages/ui/src/components/organisms/FeatureTabs.tsx +196 -0
  35. package/packages/ui/src/components/organisms/LogoMarquee.tsx +119 -0
  36. package/packages/ui/src/components/organisms/Navbar.tsx +194 -0
  37. package/packages/ui/src/components/organisms/index.ts +3 -0
  38. package/packages/ui/src/index.ts +15 -0
  39. package/packages/ui/src/lib/utils.ts +12 -0
  40. package/packages/ui/src/stories/atoms/Avatar.stories.tsx +49 -0
  41. package/packages/ui/src/stories/atoms/Badge.stories.tsx +68 -0
  42. package/packages/ui/src/stories/atoms/Button.stories.tsx +98 -0
  43. package/packages/ui/src/stories/atoms/Input.stories.tsx +66 -0
  44. package/packages/ui/src/stories/atoms/Toggle.stories.tsx +36 -0
  45. package/packages/ui/src/stories/molecules/Accordion.stories.tsx +47 -0
  46. package/packages/ui/src/stories/molecules/Card.stories.tsx +84 -0
  47. package/packages/ui/src/stories/molecules/PricingCard.stories.tsx +62 -0
  48. package/packages/ui/src/stories/molecules/TestimonialCard.stories.tsx +52 -0
  49. package/packages/ui/src/stories/molecules/Tooltip.stories.tsx +66 -0
  50. package/packages/ui/src/stories/organisms/LogoMarquee.stories.tsx +33 -0
  51. package/packages/ui/src/stories/organisms/Navbar.stories.tsx +37 -0
  52. package/packages/ui/src/styles/globals.css +220 -0
  53. package/packages/ui/tailwind.config.ts +68 -0
  54. package/packages/ui/tsconfig.json +23 -0
  55. package/packages/ui/tsup.config.ts +24 -0
  56. package/packages/ui/vite.config.ts +17 -0
  57. package/pnpm-workspace.yaml +2 -0
  58. package/turbo.json +33 -0
@@ -0,0 +1,17 @@
1
+ import type { StorybookConfig } from '@storybook/react-vite';
2
+
3
+ const config: StorybookConfig = {
4
+ stories: ['../src/stories/**/*.stories.@(ts|tsx)'],
5
+ addons: [
6
+ '@storybook/addon-essentials',
7
+ '@storybook/addon-interactions',
8
+ '@storybook/addon-links',
9
+ ],
10
+ framework: {
11
+ name: '@storybook/react-vite',
12
+ options: {},
13
+ },
14
+ docs: { autodocs: 'tag' },
15
+ };
16
+
17
+ export default config;
@@ -0,0 +1,37 @@
1
+ import type { Preview, Decorator } from '@storybook/react';
2
+ import React from 'react';
3
+ import '../src/styles/globals.css';
4
+
5
+ const withTheme: Decorator = (Story, context) => {
6
+ const bg = context.globals?.backgrounds?.value;
7
+ const theme = bg === '#FFFFFC' ? 'light' : 'dark';
8
+ return (
9
+ <div data-theme={theme} style={{ padding: '2rem', minHeight: '100vh', background: theme === 'dark' ? '#000' : '#FFFFFC' }}>
10
+ <Story />
11
+ </div>
12
+ );
13
+ };
14
+
15
+ const preview: Preview = {
16
+ parameters: {
17
+ backgrounds: {
18
+ default: 'dark',
19
+ values: [
20
+ { name: 'dark', value: '#000000' },
21
+ { name: 'light', value: '#FFFFFC' },
22
+ { name: 'surface', value: '#111111' },
23
+ ],
24
+ },
25
+ layout: 'centered',
26
+ actions: { argTypesRegex: '^on[A-Z].*' },
27
+ controls: {
28
+ matchers: {
29
+ color: /(background|color)$/i,
30
+ date: /Date$/i,
31
+ },
32
+ },
33
+ },
34
+ decorators: [withTheme],
35
+ };
36
+
37
+ export default preview;
@@ -0,0 +1,109 @@
1
+ {
2
+ "name": "@rechta/ui",
3
+ "version": "2.1.0",
4
+ "description": "Rechta Design System \u2014 React component library built on shadcn/ui + Radix UI",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ },
15
+ "./atoms": {
16
+ "import": "./dist/atoms/index.js",
17
+ "types": "./dist/atoms/index.d.ts"
18
+ },
19
+ "./molecules": {
20
+ "import": "./dist/molecules/index.js",
21
+ "types": "./dist/molecules/index.d.ts"
22
+ },
23
+ "./organisms": {
24
+ "import": "./dist/organisms/index.js",
25
+ "types": "./dist/organisms/index.d.ts"
26
+ },
27
+ "./charts": {
28
+ "import": "./dist/charts/index.js",
29
+ "types": "./dist/charts/index.d.ts"
30
+ }
31
+ },
32
+ "sideEffects": false,
33
+ "scripts": {
34
+ "build": "tsup",
35
+ "dev": "tsup --watch",
36
+ "storybook": "storybook dev -p 6006 --config-dir .storybook",
37
+ "build-storybook": "storybook build --config-dir .storybook",
38
+ "lint": "eslint src --ext .ts,.tsx",
39
+ "typecheck": "tsc --noEmit",
40
+ "clean": "rm -rf dist"
41
+ },
42
+ "dependencies": {
43
+ "@radix-ui/react-accordion": "^1.2.0",
44
+ "@radix-ui/react-avatar": "^1.1.0",
45
+ "@radix-ui/react-checkbox": "^1.1.0",
46
+ "@radix-ui/react-dialog": "^1.1.1",
47
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
48
+ "@radix-ui/react-label": "^2.1.0",
49
+ "@radix-ui/react-popover": "^1.1.1",
50
+ "@radix-ui/react-radio-group": "^1.2.0",
51
+ "@radix-ui/react-select": "^2.1.1",
52
+ "@radix-ui/react-separator": "^1.1.0",
53
+ "@radix-ui/react-slot": "^1.1.0",
54
+ "@radix-ui/react-switch": "^1.1.0",
55
+ "@radix-ui/react-tabs": "^1.1.0",
56
+ "@radix-ui/react-toast": "^1.2.1",
57
+ "@radix-ui/react-tooltip": "^1.1.2",
58
+ "class-variance-authority": "^0.7.0",
59
+ "clsx": "^2.1.1",
60
+ "lucide-react": "^0.441.0",
61
+ "recharts": "^2.12.7",
62
+ "tailwind-merge": "^2.5.2"
63
+ },
64
+ "peerDependencies": {
65
+ "react": "^18.0.0 || ^19.0.0",
66
+ "react-dom": "^18.0.0 || ^19.0.0"
67
+ },
68
+ "devDependencies": {
69
+ "@rechta/tokens": "workspace:*",
70
+ "@storybook/addon-essentials": "^8.3.3",
71
+ "@storybook/addon-interactions": "^8.3.3",
72
+ "@storybook/addon-links": "^8.3.3",
73
+ "@storybook/react": "^8.3.3",
74
+ "@storybook/react-vite": "^8.3.3",
75
+ "@storybook/test": "^8.3.3",
76
+ "@types/react": "^18.3.5",
77
+ "@types/react-dom": "^18.3.0",
78
+ "autoprefixer": "^10.4.20",
79
+ "postcss": "^8.4.45",
80
+ "storybook": "^8.3.3",
81
+ "tailwindcss": "^3.4.11",
82
+ "tsup": "^8.2.4",
83
+ "typescript": "^5.5.4",
84
+ "vite": "^5.4.3",
85
+ "@vitejs/plugin-react": "^4.3.1"
86
+ },
87
+ "files": [
88
+ "dist",
89
+ "README.md"
90
+ ],
91
+ "keywords": [
92
+ "rechta",
93
+ "design-system",
94
+ "react",
95
+ "ui",
96
+ "components",
97
+ "shadcn",
98
+ "radix",
99
+ "tailwind"
100
+ ],
101
+ "repository": {
102
+ "type": "git",
103
+ "url": "https://github.com/your-org/rechta-ds",
104
+ "directory": "packages/ui"
105
+ },
106
+ "publishConfig": {
107
+ "access": "public"
108
+ }
109
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -0,0 +1,139 @@
1
+ import * as React from 'react';
2
+ import * as AvatarPrimitive from '@radix-ui/react-avatar';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+ import { cn } from '../../lib/utils';
5
+
6
+ const avatarVariants = cva(
7
+ 'relative inline-flex shrink-0 overflow-hidden select-none',
8
+ {
9
+ variants: {
10
+ size: {
11
+ xs: 'size-6 rounded-md text-[10px]',
12
+ sm: 'size-8 rounded-lg text-xs',
13
+ md: 'size-10 rounded-lg text-sm',
14
+ lg: 'size-12 rounded-xl text-base',
15
+ xl: 'size-16 rounded-2xl text-lg',
16
+ '2xl': 'size-20 rounded-2xl text-xl',
17
+ },
18
+ shape: {
19
+ square: '',
20
+ rounded: '',
21
+ circle: 'rounded-full',
22
+ },
23
+ ring: {
24
+ none: '',
25
+ brand: 'ring-2 ring-brand-500 ring-offset-2 ring-offset-bg-base',
26
+ accent: 'ring-2 ring-accent-400 ring-offset-2 ring-offset-bg-base',
27
+ white: 'ring-2 ring-white/20 ring-offset-1 ring-offset-bg-base',
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ size: 'md',
32
+ shape: 'circle',
33
+ ring: 'none',
34
+ },
35
+ }
36
+ );
37
+
38
+ export interface AvatarProps
39
+ extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,
40
+ VariantProps<typeof avatarVariants> {
41
+ src?: string;
42
+ alt?: string;
43
+ fallback?: string;
44
+ status?: 'online' | 'offline' | 'away' | 'busy';
45
+ }
46
+
47
+ const statusColors = {
48
+ online: 'bg-brand-500',
49
+ offline: 'bg-text-tertiary',
50
+ away: 'bg-amber-400',
51
+ busy: 'bg-red-400',
52
+ };
53
+
54
+ const Avatar = React.forwardRef<
55
+ React.ElementRef<typeof AvatarPrimitive.Root>,
56
+ AvatarProps
57
+ >(({ className, size, shape, ring, src, alt, fallback, status, ...props }, ref) => {
58
+ return (
59
+ <div className="relative inline-flex">
60
+ <AvatarPrimitive.Root
61
+ ref={ref}
62
+ className={cn(avatarVariants({ size, shape, ring, className }))}
63
+ {...props}
64
+ >
65
+ <AvatarPrimitive.Image
66
+ src={src}
67
+ alt={alt}
68
+ className="aspect-square size-full object-cover"
69
+ />
70
+ <AvatarPrimitive.Fallback
71
+ className={cn(
72
+ 'flex size-full items-center justify-center font-display font-semibold uppercase',
73
+ 'bg-surface-raised text-text-secondary border border-border'
74
+ )}
75
+ delayMs={100}
76
+ >
77
+ {fallback?.slice(0, 2) ?? '??'}
78
+ </AvatarPrimitive.Fallback>
79
+ </AvatarPrimitive.Root>
80
+
81
+ {status && (
82
+ <span
83
+ className={cn(
84
+ 'absolute bottom-0 right-0 rounded-full border-2 border-bg-base',
85
+ statusColors[status],
86
+ size === 'xs' || size === 'sm' ? 'size-2' : 'size-2.5'
87
+ )}
88
+ />
89
+ )}
90
+ </div>
91
+ );
92
+ });
93
+ Avatar.displayName = 'Avatar';
94
+
95
+ // ─── Avatar Group ─────────────────────────────────────────────────────────────
96
+ export interface AvatarGroupProps {
97
+ avatars: Array<{ src?: string; alt?: string; fallback?: string }>;
98
+ max?: number;
99
+ size?: AvatarProps['size'];
100
+ className?: string;
101
+ }
102
+
103
+ const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
104
+ ({ avatars, max = 4, size = 'sm', className }, ref) => {
105
+ const shown = avatars.slice(0, max);
106
+ const overflow = avatars.length - max;
107
+
108
+ return (
109
+ <div ref={ref} className={cn('flex -space-x-2', className)}>
110
+ {shown.map((avatar, i) => (
111
+ <Avatar
112
+ key={i}
113
+ src={avatar.src}
114
+ alt={avatar.alt}
115
+ fallback={avatar.fallback}
116
+ size={size}
117
+ ring="white"
118
+ className="hover:z-10 hover:scale-110 transition-transform duration-150"
119
+ />
120
+ ))}
121
+ {overflow > 0 && (
122
+ <div
123
+ className={cn(
124
+ avatarVariants({ size, shape: 'circle', ring: 'white' }),
125
+ 'flex items-center justify-center',
126
+ 'bg-surface-raised text-text-secondary border border-border',
127
+ 'font-mono text-xs font-medium z-10'
128
+ )}
129
+ >
130
+ +{overflow}
131
+ </div>
132
+ )}
133
+ </div>
134
+ );
135
+ }
136
+ );
137
+ AvatarGroup.displayName = 'AvatarGroup';
138
+
139
+ export { Avatar, AvatarGroup, avatarVariants };
@@ -0,0 +1,62 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { cn } from '../../lib/utils';
4
+
5
+ /**
6
+ * Badge — @rechta/ui
7
+ *
8
+ * Accessibility:
9
+ * - All text tokens meet WCAG AA (≥4.5:1) against their background
10
+ * - dark theme: text-brand (#09E85E) = 8.6:1, text-blue (#5BABFF) = 5.9:1
11
+ * - light theme: text-brand (#047A2B) = 7.1:1, text-blue (#1A5EB8) = 6.4:1
12
+ * - Status warning: dark #FBBF24 = 12.4:1, light #92570A = 6.5:1
13
+ * - Status error: dark #F87171 = 7.4:1, light #C41D1D = 7.0:1
14
+ * - Solid: black text on malachite = ≥5:1, porcelain on blue = ≥4.5:1
15
+ * - When used to convey state (e.g. "Online"), add aria-label to parent
16
+ */
17
+ const badgeVariants = cva(
18
+ [
19
+ 'inline-flex items-center gap-1.5 font-mono text-[11px] font-medium',
20
+ 'border rounded-[--radius-md] px-2 h-[22px] whitespace-nowrap',
21
+ 'transition-colors duration-[150ms]',
22
+ ],
23
+ {
24
+ variants: {
25
+ variant: {
26
+ default: 'bg-[--surface] border-[--border] text-[--text-3]',
27
+ brand: 'bg-[--brand-subtle] border-[--border-brand] text-[--text-brand]',
28
+ emerald: 'bg-[--emerald-subtle] border-[--border-brand] text-[--emerald]',
29
+ blue: 'bg-[--blue-subtle] border-[--border-blue] text-[--text-blue]',
30
+ success: 'bg-[--status-success-bg] border-[--border-brand] text-[--status-success]',
31
+ warning: 'bg-[--status-warning-bg] border-[--border-warning,var(--border)] text-[--status-warning]',
32
+ error: 'bg-[--status-error-bg] border-[--border-error] text-[--status-error]',
33
+ info: 'bg-[--status-info-bg] border-[--border-blue] text-[--status-info]',
34
+ outline: 'bg-transparent border-[--border-mid] text-[--text]',
35
+ solid: 'bg-[--brand] border-[--brand] text-black',
36
+ 'solid-blue':'bg-[--blue] border-[--blue] text-[--color-porcelain]',
37
+ porcelain: 'bg-[--color-porcelain] border-[--color-porcelain] text-black',
38
+ },
39
+ },
40
+ defaultVariants: { variant: 'default' },
41
+ }
42
+ );
43
+
44
+ export interface BadgeProps
45
+ extends React.HTMLAttributes<HTMLSpanElement>,
46
+ VariantProps<typeof badgeVariants> {
47
+ dot?: boolean;
48
+ }
49
+
50
+ const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
51
+ ({ className, variant, dot, children, ...props }, ref) => (
52
+ <span ref={ref} className={cn(badgeVariants({ variant }), className)} {...props}>
53
+ {dot && (
54
+ <span aria-hidden="true" className="inline-block w-1.5 h-1.5 rounded-full bg-current shrink-0" />
55
+ )}
56
+ {children}
57
+ </span>
58
+ )
59
+ );
60
+ Badge.displayName = 'Badge';
61
+
62
+ export { Badge, badgeVariants };
@@ -0,0 +1,125 @@
1
+ import * as React from 'react';
2
+ import { Slot } from '@radix-ui/react-slot';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+ import { cn } from '../../lib/utils';
5
+
6
+ /**
7
+ * Button — @rechta/ui v2.1.0
8
+ * Ruda 900 display font, updated to --c- token system.
9
+ * All WCAG AA contrast ratios maintained.
10
+ */
11
+ const buttonVariants = cva(
12
+ [
13
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap select-none cursor-pointer',
14
+ 'font-medium tracking-[-0.01em]',
15
+ 'border transition-all duration-[150ms] ease-out',
16
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[--c-brand-focus] focus-visible:ring-offset-2 focus-visible:ring-offset-[--c-bg]',
17
+ 'disabled:pointer-events-none disabled:opacity-40',
18
+ '[&_svg]:pointer-events-none [&_svg]:shrink-0',
19
+ ],
20
+ {
21
+ variants: {
22
+ variant: {
23
+ default: [
24
+ 'bg-[--c-brand] text-white border-[--c-brand]',
25
+ 'hover:bg-[--c-brand-hover] hover:border-[--c-brand-hover]',
26
+ 'active:bg-[--c-brand-active] active:scale-[0.98]',
27
+ ],
28
+ emerald: [
29
+ 'bg-[--c-emerald] text-white border-[--c-emerald]',
30
+ 'hover:bg-[--c-emerald-hover]',
31
+ 'active:scale-[0.98]',
32
+ ],
33
+ blue: [
34
+ 'bg-[--c-blue] text-white border-[--c-blue]',
35
+ 'hover:bg-[--c-blue-hover]',
36
+ 'active:bg-[--c-blue-active] active:scale-[0.98]',
37
+ ],
38
+ porcelain: [
39
+ 'bg-[--c-text] text-[--c-bg] border-[--c-text]',
40
+ 'hover:opacity-90',
41
+ 'active:scale-[0.98]',
42
+ ],
43
+ secondary: [
44
+ 'bg-[--c-bg-surface-hover] text-[--c-text] border-[--c-border]',
45
+ 'hover:bg-[--c-bg-elevated] hover:border-[--c-border-mid]',
46
+ 'active:scale-[0.98]',
47
+ ],
48
+ ghost: [
49
+ 'bg-transparent text-[--c-text-2] border-transparent',
50
+ 'hover:bg-[--c-bg-surface-hover] hover:text-[--c-text] hover:border-[--c-border]',
51
+ 'active:scale-[0.98]',
52
+ ],
53
+ outline: [
54
+ 'bg-transparent text-[--c-text] border-[--c-border]',
55
+ 'hover:bg-[--c-bg-surface-hover] hover:border-[--c-border-mid]',
56
+ 'active:scale-[0.98]',
57
+ ],
58
+ destructive: [
59
+ 'bg-transparent text-[--c-status-error] border-[--c-border-error]',
60
+ 'hover:bg-[rgba(251,113,133,0.08)]',
61
+ 'active:scale-[0.98]',
62
+ ],
63
+ link: [
64
+ 'bg-transparent text-[--c-text-accent] border-transparent',
65
+ 'underline-offset-4 hover:underline',
66
+ 'h-auto! p-0!',
67
+ ],
68
+ },
69
+ size: {
70
+ xs: 'h-7 px-3 text-xs rounded-[6px] gap-1.5 [&_svg]:size-3 font-[var(--font-sans)]',
71
+ sm: 'h-8 px-4 text-[13px] rounded-[8px] gap-1.5 [&_svg]:size-3.5 font-[var(--font-sans)]',
72
+ md: 'h-[36px] px-[14px] text-[13px] rounded-[8px] gap-2 [&_svg]:size-4 font-[var(--font-sans)]',
73
+ lg: 'h-11 px-6 text-sm rounded-[10px] gap-2 [&_svg]:size-5 font-[var(--font-sans)]',
74
+ xl: 'h-[52px] px-8 text-[15px] rounded-[12px] gap-2.5 [&_svg]:size-5 font-[var(--font-sans)] font-semibold',
75
+ icon: 'size-9 rounded-[8px] p-0 [&_svg]:size-4',
76
+ 'icon-sm':'size-7 rounded-[6px] p-0 [&_svg]:size-3.5',
77
+ 'icon-lg':'size-11 rounded-[10px] p-0 [&_svg]:size-5',
78
+ },
79
+ loading: { true: 'cursor-wait opacity-70' },
80
+ },
81
+ defaultVariants: { variant: 'default', size: 'md' },
82
+ }
83
+ );
84
+
85
+ export interface ButtonProps
86
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
87
+ VariantProps<typeof buttonVariants> {
88
+ asChild?: boolean;
89
+ loading?: boolean;
90
+ loadingText?: string;
91
+ leftIcon?: React.ReactNode;
92
+ rightIcon?: React.ReactNode;
93
+ }
94
+
95
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
96
+ ({ className, variant, size, loading, loadingText, asChild = false,
97
+ leftIcon, rightIcon, children, disabled, ...props }, ref) => {
98
+ const Comp = asChild ? Slot : 'button';
99
+ return (
100
+ <Comp
101
+ ref={ref}
102
+ className={cn(buttonVariants({ variant, size, loading, className }))}
103
+ disabled={disabled || loading === true}
104
+ aria-busy={loading === true}
105
+ {...props}
106
+ >
107
+ {loading ? (
108
+ <>
109
+ <span aria-hidden="true" className="size-4 border-2 border-current border-t-transparent rounded-full animate-spin shrink-0" />
110
+ {loadingText ?? children}
111
+ </>
112
+ ) : (
113
+ <>
114
+ {leftIcon && <span aria-hidden="true">{leftIcon}</span>}
115
+ {children}
116
+ {rightIcon && <span aria-hidden="true">{rightIcon}</span>}
117
+ </>
118
+ )}
119
+ </Comp>
120
+ );
121
+ }
122
+ );
123
+ Button.displayName = 'Button';
124
+
125
+ export { Button, buttonVariants };
@@ -0,0 +1,116 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { cn } from '../../lib/utils';
4
+
5
+ const inputVariants = cva(
6
+ [
7
+ 'w-full font-body text-text-primary placeholder:text-text-tertiary',
8
+ 'bg-surface border border-border rounded-lg',
9
+ 'transition-all duration-150',
10
+ 'focus:outline-none focus:ring-2 focus:ring-brand-500/50 focus:border-brand-500',
11
+ 'disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-bg-muted',
12
+ 'read-only:bg-bg-subtle',
13
+ ],
14
+ {
15
+ variants: {
16
+ size: {
17
+ sm: 'h-8 px-3 text-sm',
18
+ md: 'h-10 px-4 text-sm',
19
+ lg: 'h-12 px-4 text-base',
20
+ },
21
+ state: {
22
+ default: '',
23
+ error: 'border-red-500/50 focus:ring-red-500/30 focus:border-red-500',
24
+ success: 'border-brand-500/50 focus:ring-brand-500/30 focus:border-brand-500',
25
+ warning: 'border-amber-500/50 focus:ring-amber-500/30 focus:border-amber-500',
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ size: 'md',
30
+ state: 'default',
31
+ },
32
+ }
33
+ );
34
+
35
+ export interface InputProps
36
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'>,
37
+ VariantProps<typeof inputVariants> {
38
+ leftElement?: React.ReactNode;
39
+ rightElement?: React.ReactNode;
40
+ state?: 'default' | 'error' | 'success' | 'warning';
41
+ }
42
+
43
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
44
+ ({ className, size, state, leftElement, rightElement, type = 'text', ...props }, ref) => {
45
+ if (leftElement || rightElement) {
46
+ return (
47
+ <div className="relative flex items-center w-full">
48
+ {leftElement && (
49
+ <div className="absolute left-3 flex items-center pointer-events-none text-text-tertiary [&_svg]:size-4">
50
+ {leftElement}
51
+ </div>
52
+ )}
53
+ <input
54
+ ref={ref}
55
+ type={type}
56
+ className={cn(
57
+ inputVariants({ size, state, className }),
58
+ leftElement && 'pl-9',
59
+ rightElement && 'pr-9'
60
+ )}
61
+ {...props}
62
+ />
63
+ {rightElement && (
64
+ <div className="absolute right-3 flex items-center text-text-tertiary [&_svg]:size-4">
65
+ {rightElement}
66
+ </div>
67
+ )}
68
+ </div>
69
+ );
70
+ }
71
+
72
+ return (
73
+ <input
74
+ ref={ref}
75
+ type={type}
76
+ className={cn(inputVariants({ size, state, className }))}
77
+ {...props}
78
+ />
79
+ );
80
+ }
81
+ );
82
+ Input.displayName = 'Input';
83
+
84
+ // ─── Textarea ────────────────────────────────────────────────────────────────
85
+ export interface TextareaProps
86
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
87
+ state?: 'default' | 'error' | 'success' | 'warning';
88
+ resize?: 'none' | 'vertical' | 'horizontal' | 'both';
89
+ }
90
+
91
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
92
+ ({ className, state = 'default', resize = 'vertical', ...props }, ref) => {
93
+ const resizeClass = {
94
+ none: 'resize-none',
95
+ vertical: 'resize-y',
96
+ horizontal: 'resize-x',
97
+ both: 'resize',
98
+ }[resize];
99
+
100
+ return (
101
+ <textarea
102
+ ref={ref}
103
+ className={cn(
104
+ inputVariants({ size: 'md', state }),
105
+ 'h-auto min-h-[100px] py-3',
106
+ resizeClass,
107
+ className
108
+ )}
109
+ {...props}
110
+ />
111
+ );
112
+ }
113
+ );
114
+ Textarea.displayName = 'Textarea';
115
+
116
+ export { Input, Textarea, inputVariants };