@smicolon/fasla-ui 0.2.3 → 0.3.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 (57) hide show
  1. package/README.md +1 -0
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +91 -23
  4. package/dist/registry/index.js +31 -2
  5. package/dist/tokens/index.d.ts +75 -6
  6. package/dist/tokens/index.js +59 -20
  7. package/package.json +4 -1
  8. package/registry/blocks/app-shell/AppShell.tsx +1 -1
  9. package/registry/blocks/data-table/DataTable.test.tsx +61 -0
  10. package/registry/blocks/data-table/DataTable.tsx +53 -16
  11. package/registry/blocks/data-table/index.ts +1 -0
  12. package/registry/blocks/empty-state/EmptyState.test.tsx +12 -0
  13. package/registry/blocks/empty-state/EmptyState.tsx +22 -31
  14. package/registry/blocks/form-section/FormSection.tsx +1 -1
  15. package/registry/blocks/navbar/Navbar.test.tsx +12 -0
  16. package/registry/blocks/navbar/Navbar.tsx +8 -23
  17. package/registry/blocks/page-header/PageHeader.tsx +5 -2
  18. package/registry/blocks/sidebar/Sidebar.tsx +9 -16
  19. package/registry/blocks/stats-card/StatsCard.tsx +9 -27
  20. package/registry/effects/animated-gradient/animated-gradient.tsx +23 -8
  21. package/registry/effects/border-beam/border-beam.tsx +77 -53
  22. package/registry/effects/effects.test.tsx +266 -0
  23. package/registry/effects/glow-card/glow-card.tsx +68 -43
  24. package/registry/effects/shimmer-button/shimmer-button.tsx +62 -23
  25. package/registry/effects/spotlight/spotlight.tsx +73 -29
  26. package/registry/effects/text-reveal/index.ts +1 -0
  27. package/registry/effects/text-reveal/text-reveal.test.tsx +66 -0
  28. package/registry/effects/text-reveal/text-reveal.tsx +68 -21
  29. package/registry/effects/typewriter-text/typewriter-text.tsx +41 -22
  30. package/registry/text-props-undefined.test.tsx +73 -0
  31. package/registry/ui/avatar/avatar.test.tsx +316 -0
  32. package/registry/ui/avatar/avatar.tsx +235 -108
  33. package/registry/ui/avatar/index.ts +1 -1
  34. package/registry/ui/badge/badge.test.tsx +267 -32
  35. package/registry/ui/badge/badge.tsx +233 -21
  36. package/registry/ui/button/button.test.tsx +6 -0
  37. package/registry/ui/button/button.tsx +6 -23
  38. package/registry/ui/card/card.tsx +1 -1
  39. package/registry/ui/checkbox/checkbox.tsx +13 -26
  40. package/registry/ui/combobox/combobox.test.tsx +43 -0
  41. package/registry/ui/combobox/combobox.tsx +27 -55
  42. package/registry/ui/input/input.test.tsx +2 -2
  43. package/registry/ui/input/input.tsx +4 -4
  44. package/registry/ui/radio/index.ts +1 -0
  45. package/registry/ui/radio/radio.test.tsx +248 -0
  46. package/registry/ui/radio/radio.tsx +208 -0
  47. package/registry/ui/select/select.tsx +4 -14
  48. package/registry/ui/skeleton/skeleton.tsx +1 -1
  49. package/registry/ui/status-indicator/index.ts +6 -0
  50. package/registry/ui/status-indicator/status-indicator.test.tsx +148 -0
  51. package/registry/ui/status-indicator/status-indicator.tsx +87 -0
  52. package/registry/ui/switch/index.ts +1 -1
  53. package/registry/ui/switch/switch.test.tsx +323 -0
  54. package/registry/ui/switch/switch.tsx +254 -57
  55. package/registry/ui/textarea/textarea.tsx +1 -1
  56. package/registry.json +435 -78
  57. package/tailwind-preset.ts +143 -0
package/README.md CHANGED
@@ -48,6 +48,7 @@ Core building blocks with CVA variants for consistent styling:
48
48
  | **Tabs** | Tabbed navigation with animations |
49
49
  | **Badge** | Status indicators and labels |
50
50
  | **Avatar** | User avatars with fallback initials |
51
+ | **Status Indicator** | Presence dot with a language-aware accessible name |
51
52
  | **Card** | Flexible content containers |
52
53
  | **Skeleton** | Loading placeholders |
53
54
  | **Combobox** | Searchable select with autocomplete |
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
  },
@@ -140,6 +198,15 @@ var registry = [
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 };
@@ -7,7 +7,7 @@ var registry = [
7
7
  title: "Button",
8
8
  description: "A button component with multiple variants, sizes, and loading state.",
9
9
  sourcePath: "registry/ui/button",
10
- dependencies: ["@radix-ui/react-slot", "class-variance-authority"],
10
+ dependencies: ["@radix-ui/react-slot", "class-variance-authority", "lucide-react"],
11
11
  categories: ["primitives", "forms"]
12
12
  },
13
13
  {
@@ -31,8 +31,27 @@ var registry = [
31
31
  name: "badge",
32
32
  type: "ui",
33
33
  title: "Badge",
34
- description: "A badge component for status indicators with multiple variants.",
34
+ 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.",
35
35
  sourcePath: "registry/ui/badge",
36
+ dependencies: ["class-variance-authority", "lucide-react"],
37
+ categories: ["primitives", "data-display"]
38
+ },
39
+ {
40
+ name: "avatar",
41
+ type: "ui",
42
+ title: "Avatar",
43
+ 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.",
44
+ sourcePath: "registry/ui/avatar",
45
+ dependencies: ["class-variance-authority", "lucide-react"],
46
+ registryDependencies: ["status-indicator"],
47
+ categories: ["primitives", "data-display"]
48
+ },
49
+ {
50
+ name: "status-indicator",
51
+ type: "ui",
52
+ title: "Status Indicator",
53
+ 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.",
54
+ sourcePath: "registry/ui/status-indicator",
36
55
  dependencies: ["class-variance-authority"],
37
56
  categories: ["primitives", "data-display"]
38
57
  },
@@ -44,6 +63,15 @@ var registry = [
44
63
  sourcePath: "registry/ui/skeleton",
45
64
  categories: ["primitives", "feedback"]
46
65
  },
66
+ {
67
+ name: "radio",
68
+ type: "ui",
69
+ title: "Radio",
70
+ description: "An accessible radio for choosing one option from a set, with a plain and a bordered layout variant.",
71
+ sourcePath: "registry/ui/radio",
72
+ dependencies: ["class-variance-authority"],
73
+ categories: ["primitives", "forms"]
74
+ },
47
75
  // Blocks
48
76
  {
49
77
  name: "app-shell",
@@ -67,6 +95,7 @@ var registry = [
67
95
  title: "Empty State",
68
96
  description: "Empty state component with icon, title, description, and actions.",
69
97
  sourcePath: "registry/blocks/empty-state",
98
+ dependencies: ["lucide-react"],
70
99
  categories: ["feedback", "data-display"]
71
100
  },
72
101
  {
@@ -25,15 +25,39 @@ interface TokenSet {
25
25
  declare const baseTokens: TokenSet;
26
26
 
27
27
  /**
28
- * Tailwind mapping helper (scaffold).
29
- * In real projects, you typically map semantic CSS vars into Tailwind's theme.
28
+ * Tailwind mapping for the Fasla semantic colour tokens.
29
+ *
30
+ * These are the 41 `theme/*` variables of the Figma "☾ Mode" collection, plus
31
+ * the six `opacity/*` Soft tints. Figma
32
+ * is the source of truth; `design/tokens/mode.json` holds the committed
33
+ * snapshot, and both apps declare the same values in their own `globals.css`.
34
+ *
35
+ * Spread this into `theme.extend.colors` and define the matching CSS variables
36
+ * as full colours (hex or oklch) on `:root` and `.dark`.
30
37
  */
38
+ /**
39
+ * Resolve a semantic token to a colour Tailwind can apply an opacity modifier to.
40
+ *
41
+ * A bare `var(--x)` does NOT work: in Tailwind 3 the `/opacity` modifier is
42
+ * silently dropped when the colour is an unparseable string, so `bg-primary/10`
43
+ * compiles to no rule at all. Wrapping in `color-mix` with the `<alpha-value>`
44
+ * placeholder keeps both `bg-primary` and `bg-primary/10` working, while letting
45
+ * the CSS variables stay full colours rather than bare channel triplets.
46
+ *
47
+ * Needs Chrome 111+, Safari 16.2+, Firefox 113+.
48
+ */
49
+ declare const semanticColor: (name: string) => string;
31
50
  declare const tailwindSemanticColors: {
32
- border: string;
33
- input: string;
34
- ring: string;
35
51
  background: string;
36
52
  foreground: string;
53
+ card: {
54
+ DEFAULT: string;
55
+ foreground: string;
56
+ };
57
+ popover: {
58
+ DEFAULT: string;
59
+ foreground: string;
60
+ };
37
61
  primary: {
38
62
  DEFAULT: string;
39
63
  foreground: string;
@@ -46,10 +70,55 @@ declare const tailwindSemanticColors: {
46
70
  DEFAULT: string;
47
71
  foreground: string;
48
72
  };
73
+ accent: {
74
+ DEFAULT: string;
75
+ foreground: string;
76
+ };
49
77
  destructive: {
50
78
  DEFAULT: string;
51
79
  foreground: string;
52
80
  };
81
+ border: string;
82
+ input: string;
83
+ ring: string;
84
+ success: {
85
+ DEFAULT: string;
86
+ foreground: string;
87
+ };
88
+ warning: {
89
+ DEFAULT: string;
90
+ foreground: string;
91
+ };
92
+ info: {
93
+ DEFAULT: string;
94
+ foreground: string;
95
+ };
96
+ soft: {
97
+ primary: string;
98
+ secondary: string;
99
+ info: string;
100
+ success: string;
101
+ warning: string;
102
+ destructive: string;
103
+ };
104
+ "background-inverse": string;
105
+ "foreground-inverse": string;
106
+ "muted-foreground-inverse": string;
107
+ "chart-1": string;
108
+ "chart-2": string;
109
+ "chart-3": string;
110
+ "chart-4": string;
111
+ "chart-5": string;
112
+ sidebar: {
113
+ DEFAULT: string;
114
+ foreground: string;
115
+ primary: string;
116
+ "primary-foreground": string;
117
+ accent: string;
118
+ "accent-foreground": string;
119
+ border: string;
120
+ ring: string;
121
+ };
53
122
  };
54
123
 
55
- export { type Density, type TokenSet, baseTokens, tailwindSemanticColors };
124
+ export { type Density, type TokenSet, baseTokens, semanticColor, tailwindSemanticColors };
@@ -2,33 +2,72 @@
2
2
  var baseTokens = {
3
3
  radius: { sm: "0.5rem", md: "0.75rem", lg: "1rem" },
4
4
  spacing: { densityMultiplier: { compact: 0.9, comfortable: 1, spacious: 1.1 } },
5
- typography: { fontFamily: "Inter", baseFontSizePx: 16, lineHeight: 1.5 },
5
+ // Brand V2.5 §14 — Geist is the LTR face. Arabic is set in Cairo, which the
6
+ // Tailwind preset selects per direction; this scaffold carries only the base.
7
+ // 16/24 is the `Base` rung, identical in both Figma ramps.
8
+ typography: { fontFamily: "Geist", baseFontSizePx: 16, lineHeight: 1.5 },
6
9
  motion: { durationsMs: { fast: 120, base: 180, slow: 260 } }
7
10
  };
8
11
 
9
12
  // src/tokens/tailwind.ts
13
+ var semanticColor = (name) => `color-mix(in oklch, var(--${name}) calc(<alpha-value> * 100%), transparent)`;
14
+ var c = semanticColor;
10
15
  var tailwindSemanticColors = {
11
- border: "hsl(var(--border))",
12
- input: "hsl(var(--input))",
13
- ring: "hsl(var(--ring))",
14
- background: "hsl(var(--background))",
15
- foreground: "hsl(var(--foreground))",
16
- primary: {
17
- DEFAULT: "hsl(var(--primary))",
18
- foreground: "hsl(var(--primary-foreground))"
19
- },
20
- secondary: {
21
- DEFAULT: "hsl(var(--secondary))",
22
- foreground: "hsl(var(--secondary-foreground))"
16
+ // Surfaces
17
+ background: c("background"),
18
+ foreground: c("foreground"),
19
+ card: { DEFAULT: c("card"), foreground: c("card-foreground") },
20
+ popover: { DEFAULT: c("popover"), foreground: c("popover-foreground") },
21
+ // Actions. `primary` is Figma's strong neutral (#0a0a0a light, #fafafa dark),
22
+ // identical to `foreground` in both modes. It is NOT a brand accent — an app
23
+ // that wants its brand colour should define its own token for it.
24
+ primary: { DEFAULT: c("primary"), foreground: c("primary-foreground") },
25
+ secondary: { DEFAULT: c("secondary"), foreground: c("secondary-foreground") },
26
+ muted: { DEFAULT: c("muted"), foreground: c("muted-foreground") },
27
+ accent: { DEFAULT: c("accent"), foreground: c("accent-foreground") },
28
+ destructive: {
29
+ DEFAULT: c("destructive"),
30
+ foreground: c("destructive-foreground")
23
31
  },
24
- muted: {
25
- DEFAULT: "hsl(var(--muted))",
26
- foreground: "hsl(var(--muted-foreground))"
32
+ // Lines and focus
33
+ border: c("border"),
34
+ input: c("input"),
35
+ ring: c("ring"),
36
+ // Status
37
+ success: { DEFAULT: c("success"), foreground: c("success-foreground") },
38
+ warning: { DEFAULT: c("warning"), foreground: c("warning-foreground") },
39
+ info: { DEFAULT: c("info"), foreground: c("info-foreground") },
40
+ // Soft tints — Figma's `opacity/*` variables, already translucent. The Badge
41
+ // Soft type lays one over `card`, exactly as Figma stacks the two fills, so
42
+ // the chip reads the same on any surface. Only `primary` changes in Dark.
43
+ soft: {
44
+ primary: c("soft-primary"),
45
+ secondary: c("soft-secondary"),
46
+ info: c("soft-info"),
47
+ success: c("soft-success"),
48
+ warning: c("soft-warning"),
49
+ destructive: c("soft-destructive")
27
50
  },
28
- destructive: {
29
- DEFAULT: "hsl(var(--destructive))",
30
- foreground: "hsl(var(--destructive-foreground))"
51
+ // Inverse — a fixed contrast band, not a mode-reactive surface.
52
+ "background-inverse": c("background-inverse"),
53
+ "foreground-inverse": c("foreground-inverse"),
54
+ "muted-foreground-inverse": c("muted-foreground-inverse"),
55
+ // Charts
56
+ "chart-1": c("chart-1"),
57
+ "chart-2": c("chart-2"),
58
+ "chart-3": c("chart-3"),
59
+ "chart-4": c("chart-4"),
60
+ "chart-5": c("chart-5"),
61
+ sidebar: {
62
+ DEFAULT: c("sidebar"),
63
+ foreground: c("sidebar-foreground"),
64
+ primary: c("sidebar-primary"),
65
+ "primary-foreground": c("sidebar-primary-foreground"),
66
+ accent: c("sidebar-accent"),
67
+ "accent-foreground": c("sidebar-accent-foreground"),
68
+ border: c("sidebar-border"),
69
+ ring: c("sidebar-ring")
31
70
  }
32
71
  };
33
72
 
34
- export { baseTokens, tailwindSemanticColors };
73
+ export { baseTokens, semanticColor, tailwindSemanticColors };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smicolon/fasla-ui",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Professional UI components with animations and app building blocks",
5
5
  "license": "MIT",
6
6
  "author": "Smicolon",
@@ -38,6 +38,7 @@
38
38
  "types": "./dist/registry/index.d.ts",
39
39
  "import": "./dist/registry/index.js"
40
40
  },
41
+ "./tailwind-preset": "./tailwind-preset.ts",
41
42
  "./lib/utils": {
42
43
  "types": "./dist/lib/utils.d.ts",
43
44
  "import": "./dist/lib/utils.js"
@@ -45,6 +46,7 @@
45
46
  },
46
47
  "files": [
47
48
  "dist",
49
+ "tailwind-preset.ts",
48
50
  "registry",
49
51
  "registry.json"
50
52
  ],
@@ -74,6 +76,7 @@
74
76
  "eslint": "^8.57.0",
75
77
  "framer-motion": "^11.0.0",
76
78
  "jsdom": "^24.0.0",
79
+ "lucide-react": "^1.48.0",
77
80
  "react": "^18.2.0",
78
81
  "react-dom": "^18.2.0",
79
82
  "tsup": "^8.0.0",
@@ -42,7 +42,7 @@ export function AppShell({
42
42
  {sidebar && (
43
43
  <aside
44
44
  className={cn(
45
- "shrink-0 border-r bg-muted/30",
45
+ "shrink-0 border-e bg-muted/30",
46
46
  sidebarWidths[sidebarWidth],
47
47
  sidebarMobile ? "block" : "hidden md:block"
48
48
  )}
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { render, screen } from "@testing-library/react"
3
+ import { DataTable, Pagination } from "./DataTable"
4
+
5
+ describe("Pagination text", () => {
6
+ it("keeps its English defaults", () => {
7
+ render(<Pagination page={2} totalPages={5} onPageChange={() => {}} />)
8
+ expect(screen.getByRole("navigation", { name: "Pagination" })).toBeTruthy()
9
+ expect(screen.getByText("Page 2 of 5")).toBeTruthy()
10
+ expect(screen.getByRole("button", { name: "Go to previous page" })).toBeTruthy()
11
+ expect(screen.getByRole("button", { name: "Go to page 3" })).toBeTruthy()
12
+ })
13
+
14
+ it("uses the text it is given", () => {
15
+ render(
16
+ <Pagination
17
+ page={2}
18
+ totalPages={5}
19
+ onPageChange={() => {}}
20
+ labels={{
21
+ pagination: "التنقّل بين الصفحات",
22
+ status: (page, total) => `الصفحة ${page} من ${total}`,
23
+ previous: "السابق",
24
+ next: "التالي",
25
+ goToPrevious: "الانتقال إلى الصفحة السابقة",
26
+ goToNext: "الانتقال إلى الصفحة التالية",
27
+ goToPage: (page) => `الانتقال إلى الصفحة ${page}`,
28
+ }}
29
+ />
30
+ )
31
+ expect(screen.getByRole("navigation", { name: "التنقّل بين الصفحات" })).toBeTruthy()
32
+ expect(screen.getByText("الصفحة 2 من 5")).toBeTruthy()
33
+ expect(screen.getByRole("button", { name: "الانتقال إلى الصفحة السابقة" }).textContent).toBe("السابق")
34
+ expect(screen.getByRole("button", { name: "الانتقال إلى الصفحة 3" })).toBeTruthy()
35
+ })
36
+ })
37
+
38
+ describe("DataTable column align", () => {
39
+ const cellClass = (align?: "start" | "center" | "end" | "left" | "right") => {
40
+ const { container, unmount } = render(
41
+ <DataTable data={[{ id: 1 }]} columns={[{ id: "id", header: "ID", cell: (row) => row.id, align }]} getRowKey={(row) => row.id} />
42
+ )
43
+ const className = container.querySelector("td")!.className
44
+ unmount()
45
+ return className
46
+ }
47
+
48
+ it("defaults to the start of the reading direction", () => {
49
+ expect(cellClass()).toContain("text-start")
50
+ })
51
+
52
+ it("maps start and end to logical sides", () => {
53
+ expect(cellClass("start")).toContain("text-start")
54
+ expect(cellClass("end")).toContain("text-end")
55
+ })
56
+
57
+ it("keeps left and right physical", () => {
58
+ expect(cellClass("left")).toContain("text-left")
59
+ expect(cellClass("right")).toContain("text-right")
60
+ })
61
+ })