@smicolon/fasla-ui 0.3.0 → 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.
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
@@ -83,9 +96,9 @@ Animated components for visual flair (powered by Framer Motion):
83
96
  ## Quick Start
84
97
 
85
98
  ```tsx
86
- import { Button } from "@smicolon/fasla-ui/registry/ui/button"
87
- import { Card, CardHeader, CardTitle, CardContent } from "@smicolon/fasla-ui/registry/ui/card"
88
- 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"
89
102
 
90
103
  export function MyComponent() {
91
104
  return (
@@ -106,26 +119,36 @@ export function MyComponent() {
106
119
 
107
120
  ```json
108
121
  {
109
- "react": "^18.0.0",
110
- "react-dom": "^18.0.0",
111
- "tailwindcss": "^3.4.0",
112
- "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
113
126
  }
114
127
  ```
115
128
 
116
- ## Tailwind Configuration
129
+ ## Theme
117
130
 
118
- 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.
119
133
 
120
- ```js
121
- module.exports = {
122
- content: [
123
- // ... your content paths
124
- "./node_modules/@smicolon/fasla-ui/**/*.{js,ts,jsx,tsx}",
125
- ],
126
- }
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
127
141
  ```
128
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
+
129
152
  ## Documentation
130
153
 
131
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.js CHANGED
@@ -194,7 +194,7 @@ var registry = [
194
194
  name: "skeleton",
195
195
  type: "ui",
196
196
  title: "Skeleton",
197
- 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.",
198
198
  sourcePath: "registry/ui/skeleton",
199
199
  categories: ["primitives", "feedback"]
200
200
  },
@@ -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 };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ var framerMotion = require('framer-motion');
4
+
5
+ // src/motion/presets.ts
6
+ function useMotionPresets() {
7
+ const prefersReduced = framerMotion.useReducedMotion();
8
+ const durations = {
9
+ fast: 0.12,
10
+ base: 0.18,
11
+ slow: 0.26
12
+ };
13
+ const easing = {
14
+ standard: [0.2, 0, 0, 1],
15
+ emphasized: [0.2, 0.8, 0.2, 1]
16
+ };
17
+ const safe = (preset) => {
18
+ if (!prefersReduced) return preset;
19
+ return {
20
+ initial: { opacity: 0 },
21
+ animate: { opacity: 1 },
22
+ exit: { opacity: 0 },
23
+ transition: { duration: durations.fast }
24
+ };
25
+ };
26
+ return {
27
+ fadeIn: safe({
28
+ initial: { opacity: 0 },
29
+ animate: { opacity: 1 },
30
+ exit: { opacity: 0 },
31
+ transition: { duration: durations.base, ease: easing.standard }
32
+ }),
33
+ slideUp: safe({
34
+ initial: { opacity: 0, y: 10 },
35
+ animate: { opacity: 1, y: 0 },
36
+ exit: { opacity: 0, y: 6 },
37
+ transition: { duration: durations.base, ease: easing.standard }
38
+ }),
39
+ modalScale: safe({
40
+ initial: { opacity: 0, scale: 0.98 },
41
+ animate: { opacity: 1, scale: 1 },
42
+ exit: { opacity: 0, scale: 0.98 },
43
+ transition: { duration: durations.base, ease: easing.emphasized }
44
+ }),
45
+ stagger: {
46
+ container: safe({
47
+ initial: { opacity: 0 },
48
+ animate: { opacity: 1 },
49
+ transition: { staggerChildren: 0.06, delayChildren: 0.02 }
50
+ }),
51
+ item: safe({
52
+ initial: { opacity: 0, y: 8 },
53
+ animate: { opacity: 1, y: 0 },
54
+ transition: { duration: durations.base, ease: easing.standard }
55
+ })
56
+ },
57
+ meta: { prefersReduced, durations, easing }
58
+ };
59
+ }
60
+
61
+ exports.useMotionPresets = useMotionPresets;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Motion presets: use these across the app to keep motion consistent.
3
+ * Rule: do not hardcode durations/easing elsewhere.
4
+ */
5
+ type MotionPreset = {
6
+ initial?: Record<string, unknown>;
7
+ animate?: Record<string, unknown>;
8
+ exit?: Record<string, unknown>;
9
+ transition?: Record<string, unknown>;
10
+ };
11
+ declare function useMotionPresets(): {
12
+ fadeIn: MotionPreset;
13
+ slideUp: MotionPreset;
14
+ modalScale: MotionPreset;
15
+ stagger: {
16
+ container: MotionPreset;
17
+ item: MotionPreset;
18
+ };
19
+ meta: {
20
+ prefersReduced: boolean | null;
21
+ durations: {
22
+ fast: number;
23
+ base: number;
24
+ slow: number;
25
+ };
26
+ easing: {
27
+ standard: number[];
28
+ emphasized: number[];
29
+ };
30
+ };
31
+ };
32
+
33
+ export { type MotionPreset, useMotionPresets };