@fabio.caffarello/react-design-system 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/index.cjs +29 -4
  2. package/dist/index.js +1866 -716
  3. package/dist/ui/atoms/Button/Button.d.ts +28 -5
  4. package/dist/ui/atoms/Button/Button.stories.d.ts +11 -3
  5. package/dist/ui/atoms/Checkbox/Checkbox.d.ts +24 -0
  6. package/dist/ui/atoms/Checkbox/Checkbox.stories.d.ts +10 -0
  7. package/dist/ui/atoms/Checkbox/Checkbox.test.d.ts +1 -0
  8. package/dist/ui/atoms/Collapsible/Collapsible.d.ts +29 -0
  9. package/dist/ui/atoms/Collapsible/Collapsible.stories.d.ts +9 -0
  10. package/dist/ui/atoms/Collapsible/Collapsible.test.d.ts +1 -0
  11. package/dist/ui/atoms/Input/Input.d.ts +28 -4
  12. package/dist/ui/atoms/Input/Input.stories.d.ts +8 -3
  13. package/dist/ui/atoms/Radio/Radio.d.ts +26 -0
  14. package/dist/ui/atoms/Radio/Radio.stories.d.ts +10 -0
  15. package/dist/ui/atoms/Radio/Radio.test.d.ts +1 -0
  16. package/dist/ui/atoms/SidebarItem/SidebarItem.d.ts +3 -1
  17. package/dist/ui/atoms/SidebarItem/SidebarItem.stories.d.ts +3 -0
  18. package/dist/ui/atoms/index.d.ts +7 -0
  19. package/dist/ui/hooks/useCollapsible.d.ts +27 -0
  20. package/dist/ui/index.d.ts +13 -0
  21. package/dist/ui/molecules/InputWithLabel/InputWithLabel.d.ts +4 -2
  22. package/dist/ui/molecules/SidebarGroup/SidebarGroup.d.ts +8 -1
  23. package/dist/ui/molecules/SidebarGroup/SidebarGroup.stories.d.ts +11 -0
  24. package/dist/ui/molecules/SidebarGroup/SidebarGroup.test.d.ts +1 -0
  25. package/dist/ui/providers/ThemeProvider.d.ts +34 -0
  26. package/dist/ui/tokens/breakpoints.d.ts +36 -0
  27. package/dist/ui/tokens/colors.d.ts +89 -0
  28. package/dist/ui/tokens/sidebar.d.ts +48 -0
  29. package/dist/ui/tokens/spacing.d.ts +53 -0
  30. package/dist/ui/tokens/themes/dark.d.ts +38 -0
  31. package/dist/ui/tokens/themes/light.d.ts +38 -0
  32. package/dist/ui/tokens/tokens.factory.d.ts +57 -0
  33. package/dist/ui/tokens/typography.d.ts +90 -0
  34. package/package.json +3 -2
  35. package/src/ui/atoms/Button/Button.stories.tsx +77 -7
  36. package/src/ui/atoms/Button/Button.tsx +176 -28
  37. package/src/ui/atoms/Checkbox/Checkbox.stories.tsx +61 -0
  38. package/src/ui/atoms/Checkbox/Checkbox.test.tsx +32 -0
  39. package/src/ui/atoms/Checkbox/Checkbox.tsx +103 -0
  40. package/src/ui/atoms/Collapsible/Collapsible.tsx +2 -2
  41. package/src/ui/atoms/Input/Input.stories.tsx +67 -6
  42. package/src/ui/atoms/Input/Input.tsx +117 -14
  43. package/src/ui/atoms/Radio/Radio.stories.tsx +72 -0
  44. package/src/ui/atoms/Radio/Radio.test.tsx +32 -0
  45. package/src/ui/atoms/Radio/Radio.tsx +104 -0
  46. package/src/ui/atoms/index.ts +7 -0
  47. package/src/ui/index.ts +14 -0
  48. package/src/ui/molecules/InputWithLabel/InputWithLabel.tsx +5 -4
  49. package/src/ui/molecules/SidebarGroup/SidebarGroup.tsx +30 -38
  50. package/src/ui/providers/ThemeProvider.tsx +105 -0
  51. package/src/ui/tokens/breakpoints.ts +71 -0
  52. package/src/ui/tokens/colors.ts +250 -0
  53. package/src/ui/tokens/sidebar.ts +9 -3
  54. package/src/ui/tokens/spacing.ts +127 -0
  55. package/src/ui/tokens/themes/dark.ts +18 -0
  56. package/src/ui/tokens/themes/light.ts +18 -0
  57. package/src/ui/tokens/tokens.factory.ts +117 -0
  58. package/src/ui/tokens/typography.ts +191 -0
@@ -1,12 +1,14 @@
1
1
  'use client';
2
2
 
3
3
  import type { HTMLAttributes, ReactNode } from "react";
4
+ import { ChevronRight } from "lucide-react";
4
5
  import { Text } from "../../atoms";
5
6
  import Collapsible from "../../atoms/Collapsible/Collapsible";
6
7
  import { SIDEBAR_TOKENS } from "../../tokens/sidebar";
7
8
 
8
9
  export interface SidebarGroupProps extends HTMLAttributes<HTMLDivElement> {
9
10
  title?: string;
11
+ titleIcon?: ReactNode; // Optional icon for the title
10
12
  children: ReactNode;
11
13
  collapsible?: boolean;
12
14
  defaultCollapsed?: boolean;
@@ -32,6 +34,7 @@ export interface SidebarGroupProps extends HTMLAttributes<HTMLDivElement> {
32
34
  */
33
35
  export default function SidebarGroup({
34
36
  title,
37
+ titleIcon,
35
38
  children,
36
39
  collapsible = false,
37
40
  defaultCollapsed = false,
@@ -45,38 +48,13 @@ export default function SidebarGroup({
45
48
  const baseClasses = ["space-y-1"];
46
49
  const classes = [...baseClasses, className].filter(Boolean).join(" ");
47
50
 
48
- // Chevron icon component
51
+ // Chevron icon component using lucide-react
49
52
  const ChevronIcon = ({ isOpen }: { isOpen: boolean }) => (
50
- <svg
51
- className={`h-4 w-4 transition-transform duration-200 ${
53
+ <ChevronRight
54
+ className={`${SIDEBAR_TOKENS.chevron.size} ${SIDEBAR_TOKENS.chevron.color} transition-transform duration-200 ${
52
55
  isOpen ? 'rotate-90' : ''
53
56
  }`}
54
- fill="none"
55
- viewBox="0 0 24 24"
56
- stroke="currentColor"
57
- >
58
- <path
59
- strokeLinecap="round"
60
- strokeLinejoin="round"
61
- strokeWidth={2}
62
- d="M9 5l7 7-7 7"
63
- />
64
- </svg>
65
- );
66
-
67
- // Title content with optional chevron
68
- const titleContent = (
69
- <>
70
- <Text
71
- as="h3"
72
- className={`${SIDEBAR_TOKENS.text.xs} font-semibold ${SIDEBAR_TOKENS.colors.groupTitle} uppercase tracking-wider`}
73
- >
74
- {title}
75
- </Text>
76
- {collapsible && showChevron && (
77
- <ChevronIcon isOpen={collapsed !== undefined ? !collapsed : !defaultCollapsed} />
78
- )}
79
- </>
57
+ />
80
58
  );
81
59
 
82
60
  // If collapsible and has title, use Collapsible component
@@ -88,15 +66,24 @@ export default function SidebarGroup({
88
66
  onOpenChange={(open) => onCollapseChange?.(!open)}
89
67
  storageKey={storageKey}
90
68
  trigger={
91
- <div className={`${SIDEBAR_TOKENS.spacing.groupTitlePadding} flex items-center justify-between w-full`}>
92
- <Text
93
- as="h3"
94
- className={`${SIDEBAR_TOKENS.text.xs} font-semibold ${SIDEBAR_TOKENS.colors.groupTitle} uppercase tracking-wider`}
95
- >
96
- {title}
97
- </Text>
69
+ <div className={`${SIDEBAR_TOKENS.spacing.groupTitlePadding} flex items-center justify-between w-full hover:bg-gray-50 rounded-md transition-colors cursor-pointer`}>
70
+ <div className="flex items-center gap-2">
71
+ {titleIcon && (
72
+ <span className={`${SIDEBAR_TOKENS.icon.md} ${SIDEBAR_TOKENS.colors.groupTitle}`}>
73
+ {titleIcon}
74
+ </span>
75
+ )}
76
+ <Text
77
+ as="h3"
78
+ className={`${SIDEBAR_TOKENS.text.xs} font-semibold ${SIDEBAR_TOKENS.colors.groupTitle} uppercase tracking-wider`}
79
+ >
80
+ {title}
81
+ </Text>
82
+ </div>
98
83
  {showChevron && (
99
- <ChevronIcon isOpen={collapsed !== undefined ? !collapsed : !defaultCollapsed} />
84
+ <span className="ml-2">
85
+ <ChevronIcon isOpen={collapsed !== undefined ? !collapsed : !defaultCollapsed} />
86
+ </span>
100
87
  )}
101
88
  </div>
102
89
  }
@@ -112,7 +99,12 @@ export default function SidebarGroup({
112
99
  return (
113
100
  <div className={classes} {...props}>
114
101
  {title && (
115
- <div className={SIDEBAR_TOKENS.spacing.groupTitlePadding}>
102
+ <div className={`${SIDEBAR_TOKENS.spacing.groupTitlePadding} flex items-center gap-2`}>
103
+ {titleIcon && (
104
+ <span className={`${SIDEBAR_TOKENS.icon.md} ${SIDEBAR_TOKENS.colors.groupTitle}`}>
105
+ {titleIcon}
106
+ </span>
107
+ )}
116
108
  <Text
117
109
  as="h3"
118
110
  className={`${SIDEBAR_TOKENS.text.xs} font-semibold ${SIDEBAR_TOKENS.colors.groupTitle} uppercase tracking-wider`}
@@ -0,0 +1,105 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
4
+ import { COLOR_TOKENS_LIGHT, COLOR_TOKENS_DARK, type ColorRole, type SemanticColor } from '../tokens/colors';
5
+ import type { ThemeMode } from '../tokens/tokens.factory';
6
+
7
+ export interface ThemeContextValue {
8
+ theme: ThemeMode;
9
+ toggleTheme: () => void;
10
+ setTheme: (theme: ThemeMode) => void;
11
+ colors: Record<ColorRole, SemanticColor>;
12
+ isDark: boolean;
13
+ }
14
+
15
+ const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
16
+
17
+ export interface ThemeProviderProps {
18
+ children: ReactNode;
19
+ defaultTheme?: ThemeMode;
20
+ storageKey?: string;
21
+ }
22
+
23
+ /**
24
+ * ThemeProvider Component
25
+ *
26
+ * Provides theme context to the application.
27
+ * Uses Strategy Pattern for different theme strategies (light, dark).
28
+ * Uses Observer Pattern to notify components about theme changes.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * <ThemeProvider defaultTheme="light">
33
+ * <App />
34
+ * </ThemeProvider>
35
+ * ```
36
+ */
37
+ export function ThemeProvider({
38
+ children,
39
+ defaultTheme = 'light',
40
+ storageKey = 'theme',
41
+ }: ThemeProviderProps) {
42
+ const [theme, setThemeState] = useState<ThemeMode>(() => {
43
+ if (typeof window === 'undefined') {
44
+ return defaultTheme;
45
+ }
46
+
47
+ try {
48
+ const stored = localStorage.getItem(storageKey);
49
+ if (stored === 'light' || stored === 'dark') {
50
+ return stored;
51
+ }
52
+ } catch (error) {
53
+ console.warn('Failed to read theme from localStorage:', error);
54
+ }
55
+
56
+ return defaultTheme;
57
+ });
58
+
59
+ useEffect(() => {
60
+ try {
61
+ localStorage.setItem(storageKey, theme);
62
+ } catch (error) {
63
+ console.warn('Failed to save theme to localStorage:', error);
64
+ }
65
+
66
+ // Apply theme class to document root
67
+ document.documentElement.classList.remove('light', 'dark');
68
+ document.documentElement.classList.add(theme);
69
+ }, [theme, storageKey]);
70
+
71
+ const toggleTheme = () => {
72
+ setThemeState((prev) => (prev === 'light' ? 'dark' : 'light'));
73
+ };
74
+
75
+ const setTheme = (newTheme: ThemeMode) => {
76
+ setThemeState(newTheme);
77
+ };
78
+
79
+ const colors = theme === 'light' ? COLOR_TOKENS_LIGHT : COLOR_TOKENS_DARK;
80
+
81
+ const value: ThemeContextValue = {
82
+ theme,
83
+ toggleTheme,
84
+ setTheme,
85
+ colors,
86
+ isDark: theme === 'dark',
87
+ };
88
+
89
+ return (
90
+ <ThemeContext.Provider value={value}>
91
+ {children}
92
+ </ThemeContext.Provider>
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Hook to use theme context
98
+ */
99
+ export function useTheme(): ThemeContextValue {
100
+ const context = useContext(ThemeContext);
101
+ if (context === undefined) {
102
+ throw new Error('useTheme must be used within a ThemeProvider');
103
+ }
104
+ return context;
105
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Breakpoint Tokens
3
+ *
4
+ * Responsive breakpoints for consistent media queries.
5
+ * Uses Factory Pattern for type-safe breakpoint creation.
6
+ */
7
+
8
+ export type BreakpointName = 'sm' | 'md' | 'lg' | 'xl' | '2xl';
9
+
10
+ export interface BreakpointToken {
11
+ name: BreakpointName;
12
+ minWidth: number;
13
+ px: string;
14
+ rem: string;
15
+ tailwind: string;
16
+ }
17
+
18
+ /**
19
+ * Breakpoint Token Factory
20
+ * Creates breakpoint tokens with consistent values
21
+ */
22
+ export class BreakpointTokenFactory {
23
+ /**
24
+ * Create breakpoint token
25
+ */
26
+ static create(name: BreakpointName): BreakpointToken {
27
+ const breakpointMap: Record<BreakpointName, { minWidth: number; tailwind: string }> = {
28
+ sm: { minWidth: 640, tailwind: 'sm' },
29
+ md: { minWidth: 768, tailwind: 'md' },
30
+ lg: { minWidth: 1024, tailwind: 'lg' },
31
+ xl: { minWidth: 1280, tailwind: 'xl' },
32
+ '2xl': { minWidth: 1536, tailwind: '2xl' },
33
+ };
34
+
35
+ const config = breakpointMap[name];
36
+ return {
37
+ name,
38
+ minWidth: config.minWidth,
39
+ px: `${config.minWidth}px`,
40
+ rem: `${config.minWidth / 16}rem`,
41
+ tailwind: config.tailwind,
42
+ };
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Pre-defined breakpoint tokens
48
+ */
49
+ export const BREAKPOINT_TOKENS: Record<BreakpointName, BreakpointToken> = {
50
+ sm: BreakpointTokenFactory.create('sm'),
51
+ md: BreakpointTokenFactory.create('md'),
52
+ lg: BreakpointTokenFactory.create('lg'),
53
+ xl: BreakpointTokenFactory.create('xl'),
54
+ '2xl': BreakpointTokenFactory.create('2xl'),
55
+ } as const;
56
+
57
+ /**
58
+ * Helper function to get breakpoint token
59
+ */
60
+ export function getBreakpoint(name: BreakpointName): BreakpointToken {
61
+ return BREAKPOINT_TOKENS[name];
62
+ }
63
+
64
+ /**
65
+ * Helper function to create media query string
66
+ */
67
+ export function getMediaQuery(name: BreakpointName, direction: 'min' | 'max' = 'min'): string {
68
+ const breakpoint = BREAKPOINT_TOKENS[name];
69
+ const operator = direction === 'min' ? 'min-width' : 'max-width';
70
+ return `@media (${operator}: ${breakpoint.px})`;
71
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Color Tokens
3
+ *
4
+ * Semantic color system for consistent theming.
5
+ * Uses Strategy Pattern for different color strategies (light, dark, custom).
6
+ */
7
+
8
+ export type ColorRole =
9
+ | 'primary'
10
+ | 'secondary'
11
+ | 'success'
12
+ | 'warning'
13
+ | 'error'
14
+ | 'info'
15
+ | 'neutral';
16
+
17
+ export type ColorShade = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
18
+
19
+ export interface ColorToken {
20
+ hex: string;
21
+ rgb: string;
22
+ tailwind: string;
23
+ }
24
+
25
+ export interface SemanticColor {
26
+ light: ColorToken;
27
+ DEFAULT: ColorToken;
28
+ dark: ColorToken;
29
+ contrast: ColorToken;
30
+ }
31
+
32
+ export interface ColorPalette {
33
+ [key: number]: ColorToken;
34
+ }
35
+
36
+ /**
37
+ * Color Strategy Interface
38
+ * Strategy Pattern for different color generation strategies
39
+ */
40
+ export interface ColorStrategy {
41
+ generatePrimary(): SemanticColor;
42
+ generateSecondary(): SemanticColor;
43
+ generateSuccess(): SemanticColor;
44
+ generateWarning(): SemanticColor;
45
+ generateError(): SemanticColor;
46
+ generateInfo(): SemanticColor;
47
+ generateNeutral(): SemanticColor;
48
+ }
49
+
50
+ /**
51
+ * Light Theme Color Strategy
52
+ */
53
+ export class LightColorStrategy implements ColorStrategy {
54
+ generatePrimary(): SemanticColor {
55
+ return {
56
+ light: { hex: '#818cf8', rgb: '129, 140, 248', tailwind: 'indigo-400' },
57
+ DEFAULT: { hex: '#6366f1', rgb: '99, 102, 241', tailwind: 'indigo-500' },
58
+ dark: { hex: '#4f46e5', rgb: '79, 70, 229', tailwind: 'indigo-600' },
59
+ contrast: { hex: '#ffffff', rgb: '255, 255, 255', tailwind: 'white' },
60
+ };
61
+ }
62
+
63
+ generateSecondary(): SemanticColor {
64
+ return {
65
+ light: { hex: '#a78bfa', rgb: '167, 139, 250', tailwind: 'violet-400' },
66
+ DEFAULT: { hex: '#8b5cf6', rgb: '139, 92, 246', tailwind: 'violet-500' },
67
+ dark: { hex: '#7c3aed', rgb: '124, 58, 237', tailwind: 'violet-600' },
68
+ contrast: { hex: '#ffffff', rgb: '255, 255, 255', tailwind: 'white' },
69
+ };
70
+ }
71
+
72
+ generateSuccess(): SemanticColor {
73
+ return {
74
+ light: { hex: '#86efac', rgb: '134, 239, 172', tailwind: 'green-300' },
75
+ DEFAULT: { hex: '#22c55e', rgb: '34, 197, 94', tailwind: 'green-500' },
76
+ dark: { hex: '#16a34a', rgb: '22, 163, 74', tailwind: 'green-600' },
77
+ contrast: { hex: '#ffffff', rgb: '255, 255, 255', tailwind: 'white' },
78
+ };
79
+ }
80
+
81
+ generateWarning(): SemanticColor {
82
+ return {
83
+ light: { hex: '#fde047', rgb: '253, 224, 71', tailwind: 'yellow-300' },
84
+ DEFAULT: { hex: '#eab308', rgb: '234, 179, 8', tailwind: 'yellow-500' },
85
+ dark: { hex: '#ca8a04', rgb: '202, 138, 4', tailwind: 'yellow-600' },
86
+ contrast: { hex: '#000000', rgb: '0, 0, 0', tailwind: 'black' },
87
+ };
88
+ }
89
+
90
+ generateError(): SemanticColor {
91
+ return {
92
+ light: { hex: '#fca5a5', rgb: '252, 165, 165', tailwind: 'red-300' },
93
+ DEFAULT: { hex: '#ef4444', rgb: '239, 68, 68', tailwind: 'red-500' },
94
+ dark: { hex: '#dc2626', rgb: '220, 38, 38', tailwind: 'red-600' },
95
+ contrast: { hex: '#ffffff', rgb: '255, 255, 255', tailwind: 'white' },
96
+ };
97
+ }
98
+
99
+ generateInfo(): SemanticColor {
100
+ return {
101
+ light: { hex: '#93c5fd', rgb: '147, 197, 253', tailwind: 'blue-300' },
102
+ DEFAULT: { hex: '#3b82f6', rgb: '59, 130, 246', tailwind: 'blue-500' },
103
+ dark: { hex: '#2563eb', rgb: '37, 99, 235', tailwind: 'blue-600' },
104
+ contrast: { hex: '#ffffff', rgb: '255, 255, 255', tailwind: 'white' },
105
+ };
106
+ }
107
+
108
+ generateNeutral(): SemanticColor {
109
+ return {
110
+ light: { hex: '#f3f4f6', rgb: '243, 244, 246', tailwind: 'gray-100' },
111
+ DEFAULT: { hex: '#6b7280', rgb: '107, 114, 128', tailwind: 'gray-500' },
112
+ dark: { hex: '#374151', rgb: '55, 65, 81', tailwind: 'gray-700' },
113
+ contrast: { hex: '#ffffff', rgb: '255, 255, 255', tailwind: 'white' },
114
+ };
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Dark Theme Color Strategy
120
+ */
121
+ export class DarkColorStrategy implements ColorStrategy {
122
+ generatePrimary(): SemanticColor {
123
+ return {
124
+ light: { hex: '#6366f1', rgb: '99, 102, 241', tailwind: 'indigo-500' },
125
+ DEFAULT: { hex: '#818cf8', rgb: '129, 140, 248', tailwind: 'indigo-400' },
126
+ dark: { hex: '#a5b4fc', rgb: '165, 180, 252', tailwind: 'indigo-300' },
127
+ contrast: { hex: '#ffffff', rgb: '255, 255, 255', tailwind: 'white' },
128
+ };
129
+ }
130
+
131
+ generateSecondary(): SemanticColor {
132
+ return {
133
+ light: { hex: '#8b5cf6', rgb: '139, 92, 246', tailwind: 'violet-500' },
134
+ DEFAULT: { hex: '#a78bfa', rgb: '167, 139, 250', tailwind: 'violet-400' },
135
+ dark: { hex: '#c4b5fd', rgb: '196, 181, 253', tailwind: 'violet-300' },
136
+ contrast: { hex: '#ffffff', rgb: '255, 255, 255', tailwind: 'white' },
137
+ };
138
+ }
139
+
140
+ generateSuccess(): SemanticColor {
141
+ return {
142
+ light: { hex: '#22c55e', rgb: '34, 197, 94', tailwind: 'green-500' },
143
+ DEFAULT: { hex: '#4ade80', rgb: '74, 222, 128', tailwind: 'green-400' },
144
+ dark: { hex: '#86efac', rgb: '134, 239, 172', tailwind: 'green-300' },
145
+ contrast: { hex: '#000000', rgb: '0, 0, 0', tailwind: 'black' },
146
+ };
147
+ }
148
+
149
+ generateWarning(): SemanticColor {
150
+ return {
151
+ light: { hex: '#eab308', rgb: '234, 179, 8', tailwind: 'yellow-500' },
152
+ DEFAULT: { hex: '#facc15', rgb: '250, 204, 21', tailwind: 'yellow-400' },
153
+ dark: { hex: '#fde047', rgb: '253, 224, 71', tailwind: 'yellow-300' },
154
+ contrast: { hex: '#000000', rgb: '0, 0, 0', tailwind: 'black' },
155
+ };
156
+ }
157
+
158
+ generateError(): SemanticColor {
159
+ return {
160
+ light: { hex: '#ef4444', rgb: '239, 68, 68', tailwind: 'red-500' },
161
+ DEFAULT: { hex: '#f87171', rgb: '248, 113, 113', tailwind: 'red-400' },
162
+ dark: { hex: '#fca5a5', rgb: '252, 165, 165', tailwind: 'red-300' },
163
+ contrast: { hex: '#ffffff', rgb: '255, 255, 255', tailwind: 'white' },
164
+ };
165
+ }
166
+
167
+ generateInfo(): SemanticColor {
168
+ return {
169
+ light: { hex: '#3b82f6', rgb: '59, 130, 246', tailwind: 'blue-500' },
170
+ DEFAULT: { hex: '#60a5fa', rgb: '96, 165, 250', tailwind: 'blue-400' },
171
+ dark: { hex: '#93c5fd', rgb: '147, 197, 253', tailwind: 'blue-300' },
172
+ contrast: { hex: '#ffffff', rgb: '255, 255, 255', tailwind: 'white' },
173
+ };
174
+ }
175
+
176
+ generateNeutral(): SemanticColor {
177
+ return {
178
+ light: { hex: '#374151', rgb: '55, 65, 81', tailwind: 'gray-700' },
179
+ DEFAULT: { hex: '#9ca3af', rgb: '156, 163, 175', tailwind: 'gray-400' },
180
+ dark: { hex: '#d1d5db', rgb: '209, 213, 219', tailwind: 'gray-300' },
181
+ contrast: { hex: '#ffffff', rgb: '255, 255, 255', tailwind: 'white' },
182
+ };
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Color Token Factory
188
+ * Uses Strategy Pattern to generate colors based on theme
189
+ */
190
+ export class ColorTokenFactory {
191
+ private strategy: ColorStrategy;
192
+
193
+ constructor(strategy: ColorStrategy) {
194
+ this.strategy = strategy;
195
+ }
196
+
197
+ /**
198
+ * Set color strategy
199
+ */
200
+ setStrategy(strategy: ColorStrategy): void {
201
+ this.strategy = strategy;
202
+ }
203
+
204
+ /**
205
+ * Generate semantic color palette
206
+ */
207
+ generatePalette(): Record<ColorRole, SemanticColor> {
208
+ return {
209
+ primary: this.strategy.generatePrimary(),
210
+ secondary: this.strategy.generateSecondary(),
211
+ success: this.strategy.generateSuccess(),
212
+ warning: this.strategy.generateWarning(),
213
+ error: this.strategy.generateError(),
214
+ info: this.strategy.generateInfo(),
215
+ neutral: this.strategy.generateNeutral(),
216
+ };
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Light theme colors (default)
222
+ */
223
+ const lightFactory = new ColorTokenFactory(new LightColorStrategy());
224
+ export const COLOR_TOKENS_LIGHT = lightFactory.generatePalette();
225
+
226
+ /**
227
+ * Dark theme colors
228
+ */
229
+ const darkFactory = new ColorTokenFactory(new DarkColorStrategy());
230
+ export const COLOR_TOKENS_DARK = darkFactory.generatePalette();
231
+
232
+ /**
233
+ * Default color tokens (light theme)
234
+ */
235
+ export const COLOR_TOKENS = COLOR_TOKENS_LIGHT;
236
+
237
+ /**
238
+ * Helper function to get color token
239
+ */
240
+ export function getColor(role: ColorRole, shade: 'light' | 'DEFAULT' | 'dark' = 'DEFAULT'): ColorToken {
241
+ return COLOR_TOKENS[role][shade];
242
+ }
243
+
244
+ /**
245
+ * Helper function to get color as Tailwind class
246
+ */
247
+ export function getColorClass(role: ColorRole, shade: 'light' | 'DEFAULT' | 'dark' = 'DEFAULT', type: 'text' | 'bg' | 'border' = 'text'): string {
248
+ const token = COLOR_TOKENS[role][shade];
249
+ return `${type}-${token.tailwind}`;
250
+ }
@@ -25,9 +25,9 @@ export const SIDEBAR_TOKENS = {
25
25
  spacing: {
26
26
  itemPaddingX: 'px-4', // 16px horizontal padding for items
27
27
  itemPaddingY: 'py-2', // 8px vertical padding for items
28
- nestedIndent: 'pl-8', // 32px for nested items (level 1)
29
- nestedIndentLevel2: 'pl-12', // 48px for nested items (level 2)
30
- nestedIndentLevel3: 'pl-16', // 64px for nested items (level 3)
28
+ nestedIndent: 'pl-6', // 24px for nested items (level 1)
29
+ nestedIndentLevel2: 'pl-10', // 40px for nested items (level 2)
30
+ nestedIndentLevel3: 'pl-14', // 56px for nested items (level 3)
31
31
  groupTitlePadding: 'px-4 py-2', // Padding for group titles
32
32
  iconMargin: 'mr-3', // 12px margin between icon and text
33
33
  },
@@ -45,6 +45,12 @@ export const SIDEBAR_TOKENS = {
45
45
  },
46
46
  groupTitle: 'text-gray-500',
47
47
  },
48
+
49
+ // Chevron (for collapsible groups)
50
+ chevron: {
51
+ size: 'h-3 w-3', // 12px (reduced from h-4 w-4)
52
+ color: 'text-gray-400', // Subtle gray color
53
+ },
48
54
  } as const;
49
55
 
50
56
  /**
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Spacing Tokens
3
+ *
4
+ * Centralized spacing scale based on 4px base unit.
5
+ * Uses Factory Pattern for type-safe token creation.
6
+ */
7
+
8
+ export type SpacingScale =
9
+ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 64 | 80 | 96;
10
+
11
+ export interface SpacingToken {
12
+ value: number;
13
+ rem: string;
14
+ px: string;
15
+ tailwind: string;
16
+ }
17
+
18
+ /**
19
+ * Spacing Token Factory
20
+ * Creates spacing tokens with consistent naming and values
21
+ */
22
+ export class SpacingTokenFactory {
23
+ private static readonly BASE_UNIT = 4; // 4px base
24
+
25
+ /**
26
+ * Create a spacing token from scale value
27
+ */
28
+ static create(scale: SpacingScale): SpacingToken {
29
+ const px = scale * this.BASE_UNIT;
30
+ const rem = px / 16; // 16px = 1rem
31
+
32
+ return {
33
+ value: px,
34
+ rem: `${rem}rem`,
35
+ px: `${px}px`,
36
+ tailwind: this.getTailwindClass(scale),
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Get Tailwind class for spacing value
42
+ */
43
+ private static getTailwindClass(scale: SpacingScale): string {
44
+ const tailwindMap: Record<SpacingScale, string> = {
45
+ 0: '0',
46
+ 1: '1', // 4px
47
+ 2: '2', // 8px
48
+ 3: '3', // 12px
49
+ 4: '4', // 16px
50
+ 5: '5', // 20px
51
+ 6: '6', // 24px
52
+ 8: '8', // 32px
53
+ 10: '10', // 40px
54
+ 12: '12', // 48px
55
+ 16: '16', // 64px
56
+ 20: '20', // 80px
57
+ 24: '24', // 96px
58
+ 32: '32', // 128px
59
+ 40: '40', // 160px
60
+ 48: '48', // 192px
61
+ 64: '64', // 256px
62
+ 80: '80', // 320px
63
+ 96: '96', // 384px
64
+ };
65
+
66
+ return tailwindMap[scale] || String(scale);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Pre-defined spacing tokens
72
+ */
73
+ export const SPACING_TOKENS = {
74
+ // Micro spacing (0-12px)
75
+ none: SpacingTokenFactory.create(0),
76
+ xs: SpacingTokenFactory.create(1), // 4px
77
+ sm: SpacingTokenFactory.create(2), // 8px
78
+ md: SpacingTokenFactory.create(3), // 12px
79
+
80
+ // Standard spacing (16-32px)
81
+ base: SpacingTokenFactory.create(4), // 16px
82
+ lg: SpacingTokenFactory.create(6), // 24px
83
+ xl: SpacingTokenFactory.create(8), // 32px
84
+
85
+ // Large spacing (40-64px)
86
+ '2xl': SpacingTokenFactory.create(10), // 40px
87
+ '3xl': SpacingTokenFactory.create(12), // 48px
88
+ '4xl': SpacingTokenFactory.create(16), // 64px
89
+
90
+ // Extra large spacing (80px+)
91
+ '5xl': SpacingTokenFactory.create(20), // 80px
92
+ '6xl': SpacingTokenFactory.create(24), // 96px
93
+ } as const;
94
+
95
+ /**
96
+ * Helper function to get spacing value
97
+ */
98
+ export function getSpacing(scale: keyof typeof SPACING_TOKENS): SpacingToken {
99
+ return SPACING_TOKENS[scale];
100
+ }
101
+
102
+ /**
103
+ * Helper function to get spacing as Tailwind class
104
+ */
105
+ export function getSpacingClass(scale: keyof typeof SPACING_TOKENS, direction: 'p' | 'm' | 'px' | 'mx' | 'py' | 'my' | 'pt' | 'mt' | 'pr' | 'mr' | 'pb' | 'mb' | 'pl' | 'ml' = 'p'): string {
106
+ const token = SPACING_TOKENS[scale];
107
+ const value = token.tailwind;
108
+
109
+ const prefixMap: Record<typeof direction, string> = {
110
+ p: 'p',
111
+ m: 'm',
112
+ px: 'px',
113
+ mx: 'mx',
114
+ py: 'py',
115
+ my: 'my',
116
+ pt: 'pt',
117
+ mt: 'mt',
118
+ pr: 'pr',
119
+ mr: 'mr',
120
+ pb: 'pb',
121
+ mb: 'mb',
122
+ pl: 'pl',
123
+ ml: 'ml',
124
+ };
125
+
126
+ return `${prefixMap[direction]}-${value}`;
127
+ }