@smicolon/fasla-ui 0.2.4 → 0.4.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 (72) hide show
  1. package/README.md +44 -20
  2. package/dist/index.cjs +319 -0
  3. package/dist/index.d.cts +5 -0
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +92 -24
  6. package/dist/lib/utils.cjs +11 -0
  7. package/dist/lib/utils.d.cts +9 -0
  8. package/dist/motion/index.cjs +61 -0
  9. package/dist/motion/index.d.cts +33 -0
  10. package/dist/registry/index.cjs +179 -0
  11. package/dist/registry/index.d.cts +17 -0
  12. package/dist/registry/index.js +32 -3
  13. package/dist/tokens/index.cjs +77 -0
  14. package/dist/tokens/index.d.cts +124 -0
  15. package/dist/tokens/index.d.ts +75 -6
  16. package/dist/tokens/index.js +59 -20
  17. package/package.json +17 -7
  18. package/registry/blocks/app-shell/AppShell.tsx +1 -1
  19. package/registry/blocks/data-table/DataTable.test.tsx +61 -0
  20. package/registry/blocks/data-table/DataTable.tsx +53 -16
  21. package/registry/blocks/data-table/index.ts +1 -0
  22. package/registry/blocks/empty-state/EmptyState.test.tsx +23 -0
  23. package/registry/blocks/empty-state/EmptyState.tsx +32 -33
  24. package/registry/blocks/form-section/FormSection.tsx +1 -1
  25. package/registry/blocks/navbar/Navbar.test.tsx +12 -0
  26. package/registry/blocks/navbar/Navbar.tsx +8 -23
  27. package/registry/blocks/page-header/PageHeader.tsx +5 -2
  28. package/registry/blocks/sidebar/Sidebar.test.tsx +39 -0
  29. package/registry/blocks/sidebar/Sidebar.tsx +36 -26
  30. package/registry/blocks/stats-card/StatsCard.tsx +12 -29
  31. package/registry/effects/animated-gradient/animated-gradient.tsx +19 -8
  32. package/registry/effects/border-beam/border-beam.tsx +77 -53
  33. package/registry/effects/effects.test.tsx +266 -0
  34. package/registry/effects/glow-card/glow-card.tsx +68 -43
  35. package/registry/effects/shimmer-button/shimmer-button.tsx +62 -23
  36. package/registry/effects/spotlight/spotlight.tsx +73 -29
  37. package/registry/effects/text-reveal/index.ts +1 -0
  38. package/registry/effects/text-reveal/text-reveal.test.tsx +66 -0
  39. package/registry/effects/text-reveal/text-reveal.tsx +70 -21
  40. package/registry/effects/typewriter-text/typewriter-text.tsx +39 -20
  41. package/registry/next14-tsconfig.test.ts +43 -0
  42. package/registry/text-props-undefined.test.tsx +73 -0
  43. package/registry/ui/avatar/avatar.test.tsx +316 -0
  44. package/registry/ui/avatar/avatar.tsx +235 -108
  45. package/registry/ui/avatar/index.ts +1 -1
  46. package/registry/ui/badge/badge.test.tsx +267 -32
  47. package/registry/ui/badge/badge.tsx +233 -21
  48. package/registry/ui/button/button.test.tsx +6 -0
  49. package/registry/ui/button/button.tsx +6 -23
  50. package/registry/ui/card/card.tsx +1 -1
  51. package/registry/ui/checkbox/checkbox.test.tsx +23 -0
  52. package/registry/ui/checkbox/checkbox.tsx +16 -27
  53. package/registry/ui/combobox/combobox.test.tsx +72 -0
  54. package/registry/ui/combobox/combobox.tsx +34 -59
  55. package/registry/ui/input/input.test.tsx +3 -3
  56. package/registry/ui/input/input.tsx +5 -5
  57. package/registry/ui/radio/index.ts +1 -0
  58. package/registry/ui/radio/radio.test.tsx +248 -0
  59. package/registry/ui/radio/radio.tsx +208 -0
  60. package/registry/ui/select/select.tsx +4 -14
  61. package/registry/ui/skeleton/index.ts +1 -0
  62. package/registry/ui/skeleton/skeleton.test.tsx +113 -29
  63. package/registry/ui/skeleton/skeleton.tsx +101 -41
  64. package/registry/ui/status-indicator/index.ts +6 -0
  65. package/registry/ui/status-indicator/status-indicator.test.tsx +148 -0
  66. package/registry/ui/status-indicator/status-indicator.tsx +87 -0
  67. package/registry/ui/switch/index.ts +1 -1
  68. package/registry/ui/switch/switch.test.tsx +323 -0
  69. package/registry/ui/switch/switch.tsx +254 -57
  70. package/registry/ui/textarea/textarea.tsx +1 -1
  71. package/registry.json +79 -16
  72. package/tailwind-preset.ts +143 -0
package/README.md CHANGED
@@ -19,12 +19,25 @@ A professional React UI component library built on **shadcn/ui** primitives. Pro
19
19
 
20
20
  ## Installation
21
21
 
22
+ Components are copied into your project with the CLI, as with shadcn/ui:
23
+
22
24
  ```bash
23
- # Using the CLI (recommended)
24
- npx @smicolon/cli init
25
- npx @smicolon/cli add button
25
+ npx @smicolon/cli init # once per project: writes components.json and lib/utils.ts,
26
+ # and installs clsx and tailwind-merge, which lib/utils.ts imports
27
+ npx @smicolon/cli add button card shimmer-button
28
+ ```
29
+
30
+ `add` ends by printing the install command for the packages the components
31
+ need, in your project's package manager. For `button card shimmer-button` with npm it is:
32
+
33
+ ```bash
34
+ npm install @radix-ui/react-slot class-variance-authority lucide-react framer-motion
35
+ ```
36
+
37
+ This package holds the design tokens, the Tailwind preset and the motion
38
+ presets, not the components. Install it when you want those:
26
39
 
27
- # Or install the package directly
40
+ ```bash
28
41
  npm install @smicolon/fasla-ui
29
42
  # or
30
43
  yarn add @smicolon/fasla-ui
@@ -48,6 +61,7 @@ Core building blocks with CVA variants for consistent styling:
48
61
  | **Tabs** | Tabbed navigation with animations |
49
62
  | **Badge** | Status indicators and labels |
50
63
  | **Avatar** | User avatars with fallback initials |
64
+ | **Status Indicator** | Presence dot with a language-aware accessible name |
51
65
  | **Card** | Flexible content containers |
52
66
  | **Skeleton** | Loading placeholders |
53
67
  | **Combobox** | Searchable select with autocomplete |
@@ -82,9 +96,9 @@ Animated components for visual flair (powered by Framer Motion):
82
96
  ## Quick Start
83
97
 
84
98
  ```tsx
85
- import { Button } from "@smicolon/fasla-ui/registry/ui/button"
86
- import { Card, CardHeader, CardTitle, CardContent } from "@smicolon/fasla-ui/registry/ui/card"
87
- import { ShimmerButton } from "@smicolon/fasla-ui/registry/effects/shimmer-button"
99
+ import { Button } from "@/components/ui/button"
100
+ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
101
+ import { ShimmerButton } from "@/components/ui/shimmer-button"
88
102
 
89
103
  export function MyComponent() {
90
104
  return (
@@ -105,26 +119,36 @@ export function MyComponent() {
105
119
 
106
120
  ```json
107
121
  {
108
- "react": "^18.0.0",
109
- "react-dom": "^18.0.0",
110
- "tailwindcss": "^3.4.0",
111
- "framer-motion": "^11.0.0" // optional, required for effects
122
+ "react": "^18.0.0 || ^19.0.0",
123
+ "react-dom": "^18.0.0 || ^19.0.0",
124
+ "tailwindcss": "^3.4.0 || ^4.1.4",
125
+ "framer-motion": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0" // optional, required for effects
112
126
  }
113
127
  ```
114
128
 
115
- ## Tailwind Configuration
129
+ ## Theme
116
130
 
117
- Add fasla-ui to your `tailwind.config.js`:
131
+ Components take every colour from your theme. `init` asks which of two layers
132
+ you want; both include the base layer.
118
133
 
119
- ```js
120
- module.exports = {
121
- content: [
122
- // ... your content paths
123
- "./node_modules/@smicolon/fasla-ui/**/*.{js,ts,jsx,tsx}",
124
- ],
125
- }
134
+ ```bash
135
+ # Starting from scratch: use Fasla's colours (palette, radius, Geist).
136
+ # On Next.js 14, leave out the font-geist.json URL: next/font there has no Geist.
137
+ npx shadcn@latest add https://ui.smicolon.com/r/theme.json https://ui.smicolon.com/r/font-geist.json
138
+
139
+ # I have a brand: keep my colours (base layer only)
140
+ npx shadcn@latest add https://ui.smicolon.com/r/theme-base.json
126
141
  ```
127
142
 
143
+ The base layer adds only what shadcn doesn't have: the success, warning and
144
+ info tokens and the soft tints, the type scale with Arabic line heights, the
145
+ Arabic setting, and the Cairo font. It never changes a colour you already
146
+ have. Arabic text uses `--font-arabic`; point it at another font to swap Cairo.
147
+
148
+ Fasla's base components replace shadcn's components of the same name in
149
+ `components/ui`; `add` asks before replacing a file that isn't Fasla's. Blocks
150
+ install to `components/blocks`.
151
+
128
152
  ## Documentation
129
153
 
130
154
  Visit [ui.smicolon.com](https://ui.smicolon.com) for full documentation, examples, and API reference.
package/dist/index.cjs ADDED
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+
3
+ var clsx = require('clsx');
4
+ var tailwindMerge = require('tailwind-merge');
5
+ var framerMotion = require('framer-motion');
6
+
7
+ // src/lib/utils.ts
8
+ function cn(...inputs) {
9
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
10
+ }
11
+
12
+ // src/tokens/model.ts
13
+ var baseTokens = {
14
+ radius: { sm: "0.5rem", md: "0.75rem", lg: "1rem" },
15
+ spacing: { densityMultiplier: { compact: 0.9, comfortable: 1, spacious: 1.1 } },
16
+ // Brand V2.5 §14 — Geist is the LTR face. Arabic is set in Cairo, which the
17
+ // Tailwind preset selects per direction; this scaffold carries only the base.
18
+ // 16/24 is the `Base` rung, identical in both Figma ramps.
19
+ typography: { fontFamily: "Geist", baseFontSizePx: 16, lineHeight: 1.5 },
20
+ motion: { durationsMs: { fast: 120, base: 180, slow: 260 } }
21
+ };
22
+
23
+ // src/tokens/tailwind.ts
24
+ var semanticColor = (name) => `color-mix(in oklch, var(--${name}) calc(<alpha-value> * 100%), transparent)`;
25
+ var c = semanticColor;
26
+ var tailwindSemanticColors = {
27
+ // Surfaces
28
+ background: c("background"),
29
+ foreground: c("foreground"),
30
+ card: { DEFAULT: c("card"), foreground: c("card-foreground") },
31
+ popover: { DEFAULT: c("popover"), foreground: c("popover-foreground") },
32
+ // Actions. `primary` is Figma's strong neutral (#0a0a0a light, #fafafa dark),
33
+ // identical to `foreground` in both modes. It is NOT a brand accent — an app
34
+ // that wants its brand colour should define its own token for it.
35
+ primary: { DEFAULT: c("primary"), foreground: c("primary-foreground") },
36
+ secondary: { DEFAULT: c("secondary"), foreground: c("secondary-foreground") },
37
+ muted: { DEFAULT: c("muted"), foreground: c("muted-foreground") },
38
+ accent: { DEFAULT: c("accent"), foreground: c("accent-foreground") },
39
+ destructive: {
40
+ DEFAULT: c("destructive"),
41
+ foreground: c("destructive-foreground")
42
+ },
43
+ // Lines and focus
44
+ border: c("border"),
45
+ input: c("input"),
46
+ ring: c("ring"),
47
+ // Status
48
+ success: { DEFAULT: c("success"), foreground: c("success-foreground") },
49
+ warning: { DEFAULT: c("warning"), foreground: c("warning-foreground") },
50
+ info: { DEFAULT: c("info"), foreground: c("info-foreground") },
51
+ // Soft tints — Figma's `opacity/*` variables, already translucent. The Badge
52
+ // Soft type lays one over `card`, exactly as Figma stacks the two fills, so
53
+ // the chip reads the same on any surface. Only `primary` changes in Dark.
54
+ soft: {
55
+ primary: c("soft-primary"),
56
+ secondary: c("soft-secondary"),
57
+ info: c("soft-info"),
58
+ success: c("soft-success"),
59
+ warning: c("soft-warning"),
60
+ destructive: c("soft-destructive")
61
+ },
62
+ // Inverse — a fixed contrast band, not a mode-reactive surface.
63
+ "background-inverse": c("background-inverse"),
64
+ "foreground-inverse": c("foreground-inverse"),
65
+ "muted-foreground-inverse": c("muted-foreground-inverse"),
66
+ // Charts
67
+ "chart-1": c("chart-1"),
68
+ "chart-2": c("chart-2"),
69
+ "chart-3": c("chart-3"),
70
+ "chart-4": c("chart-4"),
71
+ "chart-5": c("chart-5"),
72
+ sidebar: {
73
+ DEFAULT: c("sidebar"),
74
+ foreground: c("sidebar-foreground"),
75
+ primary: c("sidebar-primary"),
76
+ "primary-foreground": c("sidebar-primary-foreground"),
77
+ accent: c("sidebar-accent"),
78
+ "accent-foreground": c("sidebar-accent-foreground"),
79
+ border: c("sidebar-border"),
80
+ ring: c("sidebar-ring")
81
+ }
82
+ };
83
+ function useMotionPresets() {
84
+ const prefersReduced = framerMotion.useReducedMotion();
85
+ const durations = {
86
+ fast: 0.12,
87
+ base: 0.18,
88
+ slow: 0.26
89
+ };
90
+ const easing = {
91
+ standard: [0.2, 0, 0, 1],
92
+ emphasized: [0.2, 0.8, 0.2, 1]
93
+ };
94
+ const safe = (preset) => {
95
+ if (!prefersReduced) return preset;
96
+ return {
97
+ initial: { opacity: 0 },
98
+ animate: { opacity: 1 },
99
+ exit: { opacity: 0 },
100
+ transition: { duration: durations.fast }
101
+ };
102
+ };
103
+ return {
104
+ fadeIn: safe({
105
+ initial: { opacity: 0 },
106
+ animate: { opacity: 1 },
107
+ exit: { opacity: 0 },
108
+ transition: { duration: durations.base, ease: easing.standard }
109
+ }),
110
+ slideUp: safe({
111
+ initial: { opacity: 0, y: 10 },
112
+ animate: { opacity: 1, y: 0 },
113
+ exit: { opacity: 0, y: 6 },
114
+ transition: { duration: durations.base, ease: easing.standard }
115
+ }),
116
+ modalScale: safe({
117
+ initial: { opacity: 0, scale: 0.98 },
118
+ animate: { opacity: 1, scale: 1 },
119
+ exit: { opacity: 0, scale: 0.98 },
120
+ transition: { duration: durations.base, ease: easing.emphasized }
121
+ }),
122
+ stagger: {
123
+ container: safe({
124
+ initial: { opacity: 0 },
125
+ animate: { opacity: 1 },
126
+ transition: { staggerChildren: 0.06, delayChildren: 0.02 }
127
+ }),
128
+ item: safe({
129
+ initial: { opacity: 0, y: 8 },
130
+ animate: { opacity: 1, y: 0 },
131
+ transition: { duration: durations.base, ease: easing.standard }
132
+ })
133
+ },
134
+ meta: { prefersReduced, durations, easing }
135
+ };
136
+ }
137
+
138
+ // src/registry/index.ts
139
+ var registry = [
140
+ // UI Primitives
141
+ {
142
+ name: "button",
143
+ type: "ui",
144
+ title: "Button",
145
+ description: "A button component with multiple variants, sizes, and loading state.",
146
+ sourcePath: "registry/ui/button",
147
+ dependencies: ["@radix-ui/react-slot", "class-variance-authority", "lucide-react"],
148
+ categories: ["primitives", "forms"]
149
+ },
150
+ {
151
+ name: "input",
152
+ type: "ui",
153
+ title: "Input",
154
+ description: "An input component with variants for validation states and icon support.",
155
+ sourcePath: "registry/ui/input",
156
+ dependencies: ["class-variance-authority"],
157
+ categories: ["primitives", "forms"]
158
+ },
159
+ {
160
+ name: "card",
161
+ type: "ui",
162
+ title: "Card",
163
+ description: "A card component with header, content, and footer sections.",
164
+ sourcePath: "registry/ui/card",
165
+ categories: ["primitives", "layout"]
166
+ },
167
+ {
168
+ name: "badge",
169
+ type: "ui",
170
+ title: "Badge",
171
+ description: "A compact inline label for status, category or metadata: solid, soft and outline types in six semantic tones, with an optional icon, avatar and close button.",
172
+ sourcePath: "registry/ui/badge",
173
+ dependencies: ["class-variance-authority", "lucide-react"],
174
+ categories: ["primitives", "data-display"]
175
+ },
176
+ {
177
+ name: "avatar",
178
+ type: "ui",
179
+ title: "Avatar",
180
+ description: "A user's photo, initials or icon, in three sizes and two radii, with an optional border and presence dot. Falls back from photo to initials to icon on its own.",
181
+ sourcePath: "registry/ui/avatar",
182
+ dependencies: ["class-variance-authority", "lucide-react"],
183
+ registryDependencies: ["status-indicator"],
184
+ categories: ["primitives", "data-display"]
185
+ },
186
+ {
187
+ name: "status-indicator",
188
+ type: "ui",
189
+ title: "Status Indicator",
190
+ description: "A presence dot \u2014 online, away, busy or offline \u2014 with an accessible name in the page's language, so colour never carries the status alone.",
191
+ sourcePath: "registry/ui/status-indicator",
192
+ dependencies: ["class-variance-authority"],
193
+ categories: ["primitives", "data-display"]
194
+ },
195
+ {
196
+ name: "skeleton",
197
+ type: "ui",
198
+ title: "Skeleton",
199
+ description: "Loading placeholders that replace shadcn's Skeleton: a plain block plus text, avatar, list item and card shapes that stay still under reduced motion.",
200
+ sourcePath: "registry/ui/skeleton",
201
+ categories: ["primitives", "feedback"]
202
+ },
203
+ {
204
+ name: "radio",
205
+ type: "ui",
206
+ title: "Radio",
207
+ description: "An accessible radio for choosing one option from a set, with a plain and a bordered layout variant.",
208
+ sourcePath: "registry/ui/radio",
209
+ dependencies: ["class-variance-authority"],
210
+ categories: ["primitives", "forms"]
211
+ },
212
+ // Blocks
213
+ {
214
+ name: "app-shell",
215
+ type: "block",
216
+ title: "App Shell",
217
+ description: "Application shell with sidebar, header, and content area.",
218
+ sourcePath: "registry/blocks/app-shell",
219
+ categories: ["layout", "application"]
220
+ },
221
+ {
222
+ name: "page-header",
223
+ type: "block",
224
+ title: "Page Header",
225
+ description: "Page header with title, breadcrumb, description, and actions.",
226
+ sourcePath: "registry/blocks/page-header",
227
+ categories: ["layout", "navigation"]
228
+ },
229
+ {
230
+ name: "empty-state",
231
+ type: "block",
232
+ title: "Empty State",
233
+ description: "Empty state component with icon, title, description, and actions.",
234
+ sourcePath: "registry/blocks/empty-state",
235
+ dependencies: ["lucide-react"],
236
+ categories: ["feedback", "data-display"]
237
+ },
238
+ {
239
+ name: "form-section",
240
+ type: "block",
241
+ title: "Form Section",
242
+ description: "Form section with title, description, fields, and actions.",
243
+ sourcePath: "registry/blocks/form-section",
244
+ categories: ["forms", "layout"]
245
+ },
246
+ {
247
+ name: "data-table",
248
+ type: "block",
249
+ title: "Data Table",
250
+ description: "Data table with sorting, pagination, and custom cell rendering.",
251
+ sourcePath: "registry/blocks/data-table",
252
+ categories: ["data-display", "tables"]
253
+ },
254
+ // Effects
255
+ {
256
+ name: "shimmer-button",
257
+ type: "effect",
258
+ title: "Shimmer Button",
259
+ description: "A button with animated shimmer effect.",
260
+ sourcePath: "registry/effects/shimmer-button",
261
+ dependencies: ["framer-motion"],
262
+ categories: ["effects", "buttons"]
263
+ },
264
+ {
265
+ name: "animated-gradient",
266
+ type: "effect",
267
+ title: "Animated Gradient",
268
+ description: "Animated gradient background and text effects.",
269
+ sourcePath: "registry/effects/animated-gradient",
270
+ dependencies: ["framer-motion"],
271
+ categories: ["effects", "backgrounds"]
272
+ },
273
+ {
274
+ name: "text-reveal",
275
+ type: "effect",
276
+ title: "Text Reveal",
277
+ description: "Character and word reveal animations.",
278
+ sourcePath: "registry/effects/text-reveal",
279
+ dependencies: ["framer-motion"],
280
+ categories: ["effects", "text"]
281
+ },
282
+ {
283
+ name: "border-beam",
284
+ type: "effect",
285
+ title: "Border Beam",
286
+ description: "Animated border beam and glowing border effects.",
287
+ sourcePath: "registry/effects/border-beam",
288
+ dependencies: ["framer-motion"],
289
+ categories: ["effects", "borders"]
290
+ },
291
+ {
292
+ name: "spotlight",
293
+ type: "effect",
294
+ title: "Spotlight",
295
+ description: "Cursor-following spotlight effect.",
296
+ sourcePath: "registry/effects/spotlight",
297
+ dependencies: ["framer-motion"],
298
+ categories: ["effects", "interactive"]
299
+ }
300
+ ];
301
+ function getRegistryItem(name) {
302
+ return registry.find((item) => item.name === name);
303
+ }
304
+ function getRegistryItemsByType(type) {
305
+ return registry.filter((item) => item.type === type);
306
+ }
307
+ function getRegistryItemsByCategory(category) {
308
+ return registry.filter((item) => item.categories?.includes(category));
309
+ }
310
+
311
+ exports.baseTokens = baseTokens;
312
+ exports.cn = cn;
313
+ exports.getRegistryItem = getRegistryItem;
314
+ exports.getRegistryItemsByCategory = getRegistryItemsByCategory;
315
+ exports.getRegistryItemsByType = getRegistryItemsByType;
316
+ exports.registry = registry;
317
+ exports.semanticColor = semanticColor;
318
+ exports.tailwindSemanticColors = tailwindSemanticColors;
319
+ exports.useMotionPresets = useMotionPresets;
@@ -0,0 +1,5 @@
1
+ export { cn } from './lib/utils.cjs';
2
+ export { Density, TokenSet, baseTokens, semanticColor, tailwindSemanticColors } from './tokens/index.cjs';
3
+ export { MotionPreset, useMotionPresets } from './motion/index.cjs';
4
+ export { RegistryItem, RegistryItemType, getRegistryItem, getRegistryItemsByCategory, getRegistryItemsByType, registry } from './registry/index.cjs';
5
+ import 'clsx';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { cn } from './lib/utils.js';
2
- export { Density, TokenSet, baseTokens, tailwindSemanticColors } from './tokens/index.js';
2
+ export { Density, TokenSet, baseTokens, semanticColor, tailwindSemanticColors } from './tokens/index.js';
3
3
  export { MotionPreset, useMotionPresets } from './motion/index.js';
4
4
  export { RegistryItem, RegistryItemType, getRegistryItem, getRegistryItemsByCategory, getRegistryItemsByType, registry } from './registry/index.js';
5
5
  import 'clsx';
package/dist/index.js CHANGED
@@ -11,32 +11,71 @@ function cn(...inputs) {
11
11
  var baseTokens = {
12
12
  radius: { sm: "0.5rem", md: "0.75rem", lg: "1rem" },
13
13
  spacing: { densityMultiplier: { compact: 0.9, comfortable: 1, spacious: 1.1 } },
14
- typography: { fontFamily: "Inter", baseFontSizePx: 16, lineHeight: 1.5 },
14
+ // Brand V2.5 §14 — Geist is the LTR face. Arabic is set in Cairo, which the
15
+ // Tailwind preset selects per direction; this scaffold carries only the base.
16
+ // 16/24 is the `Base` rung, identical in both Figma ramps.
17
+ typography: { fontFamily: "Geist", baseFontSizePx: 16, lineHeight: 1.5 },
15
18
  motion: { durationsMs: { fast: 120, base: 180, slow: 260 } }
16
19
  };
17
20
 
18
21
  // src/tokens/tailwind.ts
22
+ var semanticColor = (name) => `color-mix(in oklch, var(--${name}) calc(<alpha-value> * 100%), transparent)`;
23
+ var c = semanticColor;
19
24
  var tailwindSemanticColors = {
20
- border: "hsl(var(--border))",
21
- input: "hsl(var(--input))",
22
- ring: "hsl(var(--ring))",
23
- background: "hsl(var(--background))",
24
- foreground: "hsl(var(--foreground))",
25
- primary: {
26
- DEFAULT: "hsl(var(--primary))",
27
- foreground: "hsl(var(--primary-foreground))"
28
- },
29
- secondary: {
30
- DEFAULT: "hsl(var(--secondary))",
31
- foreground: "hsl(var(--secondary-foreground))"
32
- },
33
- muted: {
34
- DEFAULT: "hsl(var(--muted))",
35
- foreground: "hsl(var(--muted-foreground))"
36
- },
25
+ // Surfaces
26
+ background: c("background"),
27
+ foreground: c("foreground"),
28
+ card: { DEFAULT: c("card"), foreground: c("card-foreground") },
29
+ popover: { DEFAULT: c("popover"), foreground: c("popover-foreground") },
30
+ // Actions. `primary` is Figma's strong neutral (#0a0a0a light, #fafafa dark),
31
+ // identical to `foreground` in both modes. It is NOT a brand accent — an app
32
+ // that wants its brand colour should define its own token for it.
33
+ primary: { DEFAULT: c("primary"), foreground: c("primary-foreground") },
34
+ secondary: { DEFAULT: c("secondary"), foreground: c("secondary-foreground") },
35
+ muted: { DEFAULT: c("muted"), foreground: c("muted-foreground") },
36
+ accent: { DEFAULT: c("accent"), foreground: c("accent-foreground") },
37
37
  destructive: {
38
- DEFAULT: "hsl(var(--destructive))",
39
- foreground: "hsl(var(--destructive-foreground))"
38
+ DEFAULT: c("destructive"),
39
+ foreground: c("destructive-foreground")
40
+ },
41
+ // Lines and focus
42
+ border: c("border"),
43
+ input: c("input"),
44
+ ring: c("ring"),
45
+ // Status
46
+ success: { DEFAULT: c("success"), foreground: c("success-foreground") },
47
+ warning: { DEFAULT: c("warning"), foreground: c("warning-foreground") },
48
+ info: { DEFAULT: c("info"), foreground: c("info-foreground") },
49
+ // Soft tints — Figma's `opacity/*` variables, already translucent. The Badge
50
+ // Soft type lays one over `card`, exactly as Figma stacks the two fills, so
51
+ // the chip reads the same on any surface. Only `primary` changes in Dark.
52
+ soft: {
53
+ primary: c("soft-primary"),
54
+ secondary: c("soft-secondary"),
55
+ info: c("soft-info"),
56
+ success: c("soft-success"),
57
+ warning: c("soft-warning"),
58
+ destructive: c("soft-destructive")
59
+ },
60
+ // Inverse — a fixed contrast band, not a mode-reactive surface.
61
+ "background-inverse": c("background-inverse"),
62
+ "foreground-inverse": c("foreground-inverse"),
63
+ "muted-foreground-inverse": c("muted-foreground-inverse"),
64
+ // Charts
65
+ "chart-1": c("chart-1"),
66
+ "chart-2": c("chart-2"),
67
+ "chart-3": c("chart-3"),
68
+ "chart-4": c("chart-4"),
69
+ "chart-5": c("chart-5"),
70
+ sidebar: {
71
+ DEFAULT: c("sidebar"),
72
+ foreground: c("sidebar-foreground"),
73
+ primary: c("sidebar-primary"),
74
+ "primary-foreground": c("sidebar-primary-foreground"),
75
+ accent: c("sidebar-accent"),
76
+ "accent-foreground": c("sidebar-accent-foreground"),
77
+ border: c("sidebar-border"),
78
+ ring: c("sidebar-ring")
40
79
  }
41
80
  };
42
81
  function useMotionPresets() {
@@ -103,7 +142,7 @@ var registry = [
103
142
  title: "Button",
104
143
  description: "A button component with multiple variants, sizes, and loading state.",
105
144
  sourcePath: "registry/ui/button",
106
- dependencies: ["@radix-ui/react-slot", "class-variance-authority"],
145
+ dependencies: ["@radix-ui/react-slot", "class-variance-authority", "lucide-react"],
107
146
  categories: ["primitives", "forms"]
108
147
  },
109
148
  {
@@ -127,8 +166,27 @@ var registry = [
127
166
  name: "badge",
128
167
  type: "ui",
129
168
  title: "Badge",
130
- description: "A badge component for status indicators with multiple variants.",
169
+ description: "A compact inline label for status, category or metadata: solid, soft and outline types in six semantic tones, with an optional icon, avatar and close button.",
131
170
  sourcePath: "registry/ui/badge",
171
+ dependencies: ["class-variance-authority", "lucide-react"],
172
+ categories: ["primitives", "data-display"]
173
+ },
174
+ {
175
+ name: "avatar",
176
+ type: "ui",
177
+ title: "Avatar",
178
+ description: "A user's photo, initials or icon, in three sizes and two radii, with an optional border and presence dot. Falls back from photo to initials to icon on its own.",
179
+ sourcePath: "registry/ui/avatar",
180
+ dependencies: ["class-variance-authority", "lucide-react"],
181
+ registryDependencies: ["status-indicator"],
182
+ categories: ["primitives", "data-display"]
183
+ },
184
+ {
185
+ name: "status-indicator",
186
+ type: "ui",
187
+ title: "Status Indicator",
188
+ description: "A presence dot \u2014 online, away, busy or offline \u2014 with an accessible name in the page's language, so colour never carries the status alone.",
189
+ sourcePath: "registry/ui/status-indicator",
132
190
  dependencies: ["class-variance-authority"],
133
191
  categories: ["primitives", "data-display"]
134
192
  },
@@ -136,10 +194,19 @@ var registry = [
136
194
  name: "skeleton",
137
195
  type: "ui",
138
196
  title: "Skeleton",
139
- description: "Loading skeleton placeholders with pre-built variants for text, avatars, and cards.",
197
+ description: "Loading placeholders that replace shadcn's Skeleton: a plain block plus text, avatar, list item and card shapes that stay still under reduced motion.",
140
198
  sourcePath: "registry/ui/skeleton",
141
199
  categories: ["primitives", "feedback"]
142
200
  },
201
+ {
202
+ name: "radio",
203
+ type: "ui",
204
+ title: "Radio",
205
+ description: "An accessible radio for choosing one option from a set, with a plain and a bordered layout variant.",
206
+ sourcePath: "registry/ui/radio",
207
+ dependencies: ["class-variance-authority"],
208
+ categories: ["primitives", "forms"]
209
+ },
143
210
  // Blocks
144
211
  {
145
212
  name: "app-shell",
@@ -163,6 +230,7 @@ var registry = [
163
230
  title: "Empty State",
164
231
  description: "Empty state component with icon, title, description, and actions.",
165
232
  sourcePath: "registry/blocks/empty-state",
233
+ dependencies: ["lucide-react"],
166
234
  categories: ["feedback", "data-display"]
167
235
  },
168
236
  {
@@ -238,4 +306,4 @@ function getRegistryItemsByCategory(category) {
238
306
  return registry.filter((item) => item.categories?.includes(category));
239
307
  }
240
308
 
241
- export { baseTokens, cn, getRegistryItem, getRegistryItemsByCategory, getRegistryItemsByType, registry, tailwindSemanticColors, useMotionPresets };
309
+ export { baseTokens, cn, getRegistryItem, getRegistryItemsByCategory, getRegistryItemsByType, registry, semanticColor, tailwindSemanticColors, useMotionPresets };
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ var clsx = require('clsx');
4
+ var tailwindMerge = require('tailwind-merge');
5
+
6
+ // src/lib/utils.ts
7
+ function cn(...inputs) {
8
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
9
+ }
10
+
11
+ exports.cn = cn;
@@ -0,0 +1,9 @@
1
+ import { ClassValue } from 'clsx';
2
+
3
+ /**
4
+ * Merge Tailwind CSS classes with clsx and tailwind-merge.
5
+ * This ensures proper class merging and deduplication.
6
+ */
7
+ declare function cn(...inputs: ClassValue[]): string;
8
+
9
+ export { cn };