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.
- package/.changeset/config.json +11 -0
- package/.github/workflows/release.yml +53 -0
- package/.github/workflows/storybook.yml +34 -0
- package/.storybook/main.ts +17 -0
- package/.storybook/preview.ts +35 -0
- package/CHANGELOG.md +65 -0
- package/CONTRIBUTING.md +106 -0
- package/README.md +206 -0
- package/package.json +30 -0
- package/packages/tokens/build.js +357 -0
- package/packages/tokens/package.json +44 -0
- package/packages/tokens/src/tokens.json +1538 -0
- package/packages/ui/.storybook/main.ts +17 -0
- package/packages/ui/.storybook/preview.tsx +37 -0
- package/packages/ui/package.json +109 -0
- package/packages/ui/postcss.config.js +6 -0
- package/packages/ui/src/components/atoms/Avatar.tsx +139 -0
- package/packages/ui/src/components/atoms/Badge.tsx +62 -0
- package/packages/ui/src/components/atoms/Button.tsx +125 -0
- package/packages/ui/src/components/atoms/Input.tsx +116 -0
- package/packages/ui/src/components/atoms/Misc.tsx +128 -0
- package/packages/ui/src/components/atoms/Toggle.tsx +191 -0
- package/packages/ui/src/components/atoms/Typography.tsx +178 -0
- package/packages/ui/src/components/atoms/index.ts +7 -0
- package/packages/ui/src/components/charts/Charts.tsx +380 -0
- package/packages/ui/src/components/charts/DataTable.tsx +222 -0
- package/packages/ui/src/components/charts/index.ts +19 -0
- package/packages/ui/src/components/molecules/Accordion.tsx +93 -0
- package/packages/ui/src/components/molecules/Card.tsx +100 -0
- package/packages/ui/src/components/molecules/PricingCard.tsx +196 -0
- package/packages/ui/src/components/molecules/TestimonialCard.tsx +85 -0
- package/packages/ui/src/components/molecules/Tooltip.tsx +71 -0
- package/packages/ui/src/components/molecules/index.ts +5 -0
- package/packages/ui/src/components/organisms/FeatureTabs.tsx +196 -0
- package/packages/ui/src/components/organisms/LogoMarquee.tsx +119 -0
- package/packages/ui/src/components/organisms/Navbar.tsx +194 -0
- package/packages/ui/src/components/organisms/index.ts +3 -0
- package/packages/ui/src/index.ts +15 -0
- package/packages/ui/src/lib/utils.ts +12 -0
- package/packages/ui/src/stories/atoms/Avatar.stories.tsx +49 -0
- package/packages/ui/src/stories/atoms/Badge.stories.tsx +68 -0
- package/packages/ui/src/stories/atoms/Button.stories.tsx +98 -0
- package/packages/ui/src/stories/atoms/Input.stories.tsx +66 -0
- package/packages/ui/src/stories/atoms/Toggle.stories.tsx +36 -0
- package/packages/ui/src/stories/molecules/Accordion.stories.tsx +47 -0
- package/packages/ui/src/stories/molecules/Card.stories.tsx +84 -0
- package/packages/ui/src/stories/molecules/PricingCard.stories.tsx +62 -0
- package/packages/ui/src/stories/molecules/TestimonialCard.stories.tsx +52 -0
- package/packages/ui/src/stories/molecules/Tooltip.stories.tsx +66 -0
- package/packages/ui/src/stories/organisms/LogoMarquee.stories.tsx +33 -0
- package/packages/ui/src/stories/organisms/Navbar.stories.tsx +37 -0
- package/packages/ui/src/styles/globals.css +220 -0
- package/packages/ui/tailwind.config.ts +68 -0
- package/packages/ui/tsconfig.json +23 -0
- package/packages/ui/tsup.config.ts +24 -0
- package/packages/ui/vite.config.ts +17 -0
- package/pnpm-workspace.yaml +2 -0
- 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,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 };
|