shru-design-system 0.1.8 → 0.1.10

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/dist/index.d.mts CHANGED
@@ -170,8 +170,19 @@ declare const THEME_CATEGORY_ORDER: readonly ["color", "typography", "shape", "d
170
170
  /**
171
171
  * Register a custom theme dynamically
172
172
  * Allows users to add themes without modifying the base config
173
+ * Can be used for any category including custom
173
174
  */
174
175
  declare function registerTheme(category: string, themeId: string, metadata: ThemeMetadata): Record<string, ThemeCategory>;
176
+ /**
177
+ * Register a theme from a token file
178
+ * Helper function to automatically register a theme by loading its file
179
+ * Users can call this after creating a theme file
180
+ */
181
+ declare function registerThemeFromFile(category: string, themeId: string, filePath?: string): Promise<{
182
+ success: boolean;
183
+ themeId: string;
184
+ category: string;
185
+ }>;
175
186
  /**
176
187
  * Get merged theme categories (base + discovered)
177
188
  */
@@ -215,4 +226,4 @@ declare function getCurrentCSSVariables(): Record<string, string>;
215
226
  */
216
227
  declare function applyThemeSync(): void;
217
228
 
218
- export { Badge, Button, Checkbox, Label, Modal, ModalClose, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalOverlay, ModalPortal, ModalTitle, ModalTrigger, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, THEME_CATEGORY_ORDER, TextInput, Textarea, type ThemeMetadata$1 as ThemeMetadata, type ThemeSelection, ThemeToggle, type ThemeToggleProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, applyThemeSync, badgeVariants, buttonVariants, enableDebugMode, getCurrentCSSVariables, getTheme, getThemeCategories, getThemeFilePath, getThemesForCategory, registerTheme, useTheme, useThemeToggle };
229
+ export { Badge, Button, Checkbox, Label, Modal, ModalClose, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalOverlay, ModalPortal, ModalTitle, ModalTrigger, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, THEME_CATEGORY_ORDER, TextInput, Textarea, type ThemeMetadata$1 as ThemeMetadata, type ThemeSelection, ThemeToggle, type ThemeToggleProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, applyThemeSync, badgeVariants, buttonVariants, enableDebugMode, getCurrentCSSVariables, getTheme, getThemeCategories, getThemeFilePath, getThemesForCategory, registerTheme, registerThemeFromFile, useTheme, useThemeToggle };
package/dist/index.d.ts CHANGED
@@ -170,8 +170,19 @@ declare const THEME_CATEGORY_ORDER: readonly ["color", "typography", "shape", "d
170
170
  /**
171
171
  * Register a custom theme dynamically
172
172
  * Allows users to add themes without modifying the base config
173
+ * Can be used for any category including custom
173
174
  */
174
175
  declare function registerTheme(category: string, themeId: string, metadata: ThemeMetadata): Record<string, ThemeCategory>;
176
+ /**
177
+ * Register a theme from a token file
178
+ * Helper function to automatically register a theme by loading its file
179
+ * Users can call this after creating a theme file
180
+ */
181
+ declare function registerThemeFromFile(category: string, themeId: string, filePath?: string): Promise<{
182
+ success: boolean;
183
+ themeId: string;
184
+ category: string;
185
+ }>;
175
186
  /**
176
187
  * Get merged theme categories (base + discovered)
177
188
  */
@@ -215,4 +226,4 @@ declare function getCurrentCSSVariables(): Record<string, string>;
215
226
  */
216
227
  declare function applyThemeSync(): void;
217
228
 
218
- export { Badge, Button, Checkbox, Label, Modal, ModalClose, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalOverlay, ModalPortal, ModalTitle, ModalTrigger, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, THEME_CATEGORY_ORDER, TextInput, Textarea, type ThemeMetadata$1 as ThemeMetadata, type ThemeSelection, ThemeToggle, type ThemeToggleProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, applyThemeSync, badgeVariants, buttonVariants, enableDebugMode, getCurrentCSSVariables, getTheme, getThemeCategories, getThemeFilePath, getThemesForCategory, registerTheme, useTheme, useThemeToggle };
229
+ export { Badge, Button, Checkbox, Label, Modal, ModalClose, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalOverlay, ModalPortal, ModalTitle, ModalTrigger, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, THEME_CATEGORY_ORDER, TextInput, Textarea, type ThemeMetadata$1 as ThemeMetadata, type ThemeSelection, ThemeToggle, type ThemeToggleProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, applyThemeSync, badgeVariants, buttonVariants, enableDebugMode, getCurrentCSSVariables, getTheme, getThemeCategories, getThemeFilePath, getThemesForCategory, registerTheme, registerThemeFromFile, useTheme, useThemeToggle };
package/dist/index.js CHANGED
@@ -55,9 +55,9 @@ var buttonVariantsConfig = {
55
55
  link: "text-primary underline-offset-4 hover:underline"
56
56
  },
57
57
  size: {
58
- default: "h-9 px-4 py-2 has-[>svg]:px-3",
59
- sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
60
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
58
+ default: "h-9 px-4 py-2 has-[>svg]:px-3 gap-component-sm",
59
+ sm: "h-8 rounded-md gap-component-xs px-3 has-[>svg]:px-2.5",
60
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4 gap-component-md",
61
61
  icon: "size-9",
62
62
  "icon-sm": "size-8",
63
63
  "icon-lg": "size-10"
@@ -69,7 +69,7 @@ var buttonVariantsConfig = {
69
69
  }
70
70
  };
71
71
  var buttonVariants = classVarianceAuthority.cva(
72
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
72
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium font-sans transition-all duration-normal disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
73
73
  buttonVariantsConfig
74
74
  );
75
75
  var Button = React__namespace.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
@@ -99,7 +99,7 @@ var badgeVariantsConfig = {
99
99
  }
100
100
  };
101
101
  var badgeVariants = classVarianceAuthority.cva(
102
- "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
102
+ "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium font-sans w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-component-xs [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] duration-normal overflow-hidden",
103
103
  badgeVariantsConfig
104
104
  );
105
105
  var Badge = React__namespace.forwardRef(({ className, variant, asChild = false, ...props }, ref) => {
@@ -124,7 +124,7 @@ var TextInput = React__namespace.forwardRef(
124
124
  type,
125
125
  "data-slot": "text-input",
126
126
  className: cn(
127
- "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
127
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] duration-normal outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium font-sans disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
128
128
  "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
129
129
  "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
130
130
  className
@@ -158,7 +158,7 @@ var Textarea = React__namespace.forwardRef(
158
158
  ref,
159
159
  "data-slot": "textarea",
160
160
  className: cn(
161
- "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
161
+ "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] duration-normal outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm font-sans",
162
162
  className
163
163
  ),
164
164
  ...props
@@ -236,7 +236,7 @@ function ModalOverlay({
236
236
  {
237
237
  "data-slot": "modal-overlay",
238
238
  className: cn(
239
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
239
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 duration-normal",
240
240
  className
241
241
  ),
242
242
  ...props
@@ -256,7 +256,7 @@ function ModalContent({
256
256
  {
257
257
  "data-slot": "modal-content",
258
258
  className: cn(
259
- "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
259
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-component-md rounded-lg border p-component-lg shadow-lg duration-normal font-sans sm:max-w-lg",
260
260
  className
261
261
  ),
262
262
  ...props,
@@ -283,7 +283,7 @@ function ModalHeader({ className, ...props }) {
283
283
  "div",
284
284
  {
285
285
  "data-slot": "modal-header",
286
- className: cn("flex flex-col gap-2 text-center sm:text-left", className),
286
+ className: cn("flex flex-col gap-component-sm text-center sm:text-left font-sans", className),
287
287
  ...props
288
288
  }
289
289
  );
@@ -294,7 +294,7 @@ function ModalFooter({ className, ...props }) {
294
294
  {
295
295
  "data-slot": "modal-footer",
296
296
  className: cn(
297
- "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
297
+ "flex flex-col-reverse gap-component-sm sm:flex-row sm:justify-end",
298
298
  className
299
299
  ),
300
300
  ...props
@@ -653,7 +653,39 @@ async function discoverThemes() {
653
653
  const tokensBase = typeof window !== "undefined" && window.__THEME_TOKENS_BASE__ ? window.__THEME_TOKENS_BASE__ : "/tokens";
654
654
  const knownCategories = Object.keys(baseThemeCategories);
655
655
  for (const category of knownCategories) {
656
- const categoryPath = `${tokensBase}/themes/${category}`;
656
+ const existingThemes = discovered[category]?.themes || {};
657
+ const themeIds = Object.keys(existingThemes);
658
+ const commonThemeNames = ["ocean", "forest", "sunset", "midnight", "pastel", "vibrant", "muted", "high-contrast"];
659
+ for (const themeName of commonThemeNames) {
660
+ if (themeIds.includes(themeName)) continue;
661
+ const themePath = `${tokensBase}/themes/${category}/${themeName}.json`;
662
+ try {
663
+ const response = await fetch(themePath);
664
+ if (response.ok && response.headers.get("content-type")?.includes("application/json")) {
665
+ const themeData = await response.json();
666
+ registerTheme(category, themeName, {
667
+ name: themeData.name || themeName.charAt(0).toUpperCase() + themeName.slice(1),
668
+ file: `${category}/${themeName}.json`,
669
+ icon: themeData.icon || "\u{1F3A8}",
670
+ description: themeData.description || `Custom ${category} theme: ${themeName}`
671
+ });
672
+ if (!discovered[category]) {
673
+ discovered[category] = {
674
+ name: category.charAt(0).toUpperCase() + category.slice(1),
675
+ order: baseThemeCategories[category]?.order || 99,
676
+ themes: {}
677
+ };
678
+ }
679
+ discovered[category].themes[themeName] = {
680
+ name: themeData.name || themeName.charAt(0).toUpperCase() + themeName.slice(1),
681
+ file: `${category}/${themeName}.json`,
682
+ icon: themeData.icon || "\u{1F3A8}",
683
+ description: themeData.description || `Custom ${category} theme: ${themeName}`
684
+ };
685
+ }
686
+ } catch {
687
+ }
688
+ }
657
689
  }
658
690
  discoveredThemesCache = discovered;
659
691
  return discovered;
@@ -670,10 +702,14 @@ function registerTheme(category, themeId, metadata) {
670
702
  }
671
703
  const cache = discoveredThemesCache;
672
704
  if (!cache[category]) {
705
+ let order = 99;
706
+ if (THEME_CATEGORY_ORDER.includes(category)) {
707
+ const index = THEME_CATEGORY_ORDER.indexOf(category);
708
+ order = (index + 1) * 10;
709
+ }
673
710
  cache[category] = {
674
711
  name: category.charAt(0).toUpperCase() + category.slice(1),
675
- order: 99,
676
- // Custom categories get high order
712
+ order,
677
713
  themes: {}
678
714
  };
679
715
  }
@@ -685,6 +721,29 @@ function registerTheme(category, themeId, metadata) {
685
721
  };
686
722
  return cache;
687
723
  }
724
+ async function registerThemeFromFile(category, themeId, filePath) {
725
+ const tokensBase = typeof window !== "undefined" && window.__THEME_TOKENS_BASE__ ? window.__THEME_TOKENS_BASE__ : "/tokens";
726
+ const path = filePath || `${tokensBase}/themes/${category}/${themeId}.json`;
727
+ try {
728
+ const response = await fetch(path);
729
+ if (!response.ok) {
730
+ throw new Error(`Failed to load theme file: ${response.statusText}`);
731
+ }
732
+ const themeData = await response.json();
733
+ registerTheme(category, themeId, {
734
+ name: themeData.name || themeId.charAt(0).toUpperCase() + themeId.slice(1),
735
+ file: filePath || `${category}/${themeId}.json`,
736
+ icon: themeData.icon || "\u{1F3A8}",
737
+ description: themeData.description || `Custom ${category} theme: ${themeId}`
738
+ });
739
+ return { success: true, themeId, category };
740
+ } catch (error) {
741
+ if (typeof window !== "undefined" && window.__DESIGN_SYSTEM_DEBUG__) {
742
+ console.error(`Failed to register theme from ${path}:`, error);
743
+ }
744
+ throw error;
745
+ }
746
+ }
688
747
  async function getThemeCategories() {
689
748
  return await discoverThemes();
690
749
  }
@@ -863,6 +922,8 @@ function flattenToCSS(tokens, prefix = "", result = {}, isColorContext = false)
863
922
  const enteringColor = key === "color" && prefix === "";
864
923
  const enteringTypography = key === "typography" && prefix === "";
865
924
  const enteringShape = key === "shape" && prefix === "";
925
+ const enteringAnimation = key === "animation" && prefix === "";
926
+ const enteringDensity = key === "spacing" && prefix === "";
866
927
  const inColorContext = isColorContext || enteringColor;
867
928
  if (enteringColor) {
868
929
  flattenToCSS(value, "", result, true);
@@ -870,6 +931,10 @@ function flattenToCSS(tokens, prefix = "", result = {}, isColorContext = false)
870
931
  flattenToCSS(value, "", result, false);
871
932
  } else if (enteringShape) {
872
933
  flattenToCSS(value, "", result, false);
934
+ } else if (enteringAnimation) {
935
+ flattenToCSS(value, "", result, false);
936
+ } else if (enteringDensity) {
937
+ flattenToCSS(value, "spacing", result, false);
873
938
  } else if (inColorContext) {
874
939
  flattenToCSS(value, "", result, true);
875
940
  } else {
@@ -1585,6 +1650,8 @@ function flattenToCSSSync(tokens, prefix = "", result = {}, isColorContext = fal
1585
1650
  const enteringColor = key === "color" && prefix === "";
1586
1651
  const enteringTypography = key === "typography" && prefix === "";
1587
1652
  const enteringShape = key === "shape" && prefix === "";
1653
+ const enteringAnimation = key === "animation" && prefix === "";
1654
+ const enteringDensity = key === "spacing" && prefix === "";
1588
1655
  const inColorContext = isColorContext || enteringColor;
1589
1656
  if (enteringColor) {
1590
1657
  flattenToCSSSync(value, "", result, true);
@@ -1592,6 +1659,10 @@ function flattenToCSSSync(tokens, prefix = "", result = {}, isColorContext = fal
1592
1659
  flattenToCSSSync(value, "", result, false);
1593
1660
  } else if (enteringShape) {
1594
1661
  flattenToCSSSync(value, "", result, false);
1662
+ } else if (enteringAnimation) {
1663
+ flattenToCSSSync(value, "", result, false);
1664
+ } else if (enteringDensity) {
1665
+ flattenToCSSSync(value, "spacing", result, false);
1595
1666
  } else if (inColorContext) {
1596
1667
  flattenToCSSSync(value, "", result, true);
1597
1668
  } else {
@@ -1678,5 +1749,6 @@ exports.getThemeCategories = getThemeCategories;
1678
1749
  exports.getThemeFilePath = getThemeFilePath;
1679
1750
  exports.getThemesForCategory = getThemesForCategory;
1680
1751
  exports.registerTheme = registerTheme;
1752
+ exports.registerThemeFromFile = registerThemeFromFile;
1681
1753
  exports.useTheme = useTheme;
1682
1754
  exports.useThemeToggle = useThemeToggle;
package/dist/index.mjs CHANGED
@@ -28,9 +28,9 @@ var buttonVariantsConfig = {
28
28
  link: "text-primary underline-offset-4 hover:underline"
29
29
  },
30
30
  size: {
31
- default: "h-9 px-4 py-2 has-[>svg]:px-3",
32
- sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
33
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
31
+ default: "h-9 px-4 py-2 has-[>svg]:px-3 gap-component-sm",
32
+ sm: "h-8 rounded-md gap-component-xs px-3 has-[>svg]:px-2.5",
33
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4 gap-component-md",
34
34
  icon: "size-9",
35
35
  "icon-sm": "size-8",
36
36
  "icon-lg": "size-10"
@@ -42,7 +42,7 @@ var buttonVariantsConfig = {
42
42
  }
43
43
  };
44
44
  var buttonVariants = cva(
45
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
45
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium font-sans transition-all duration-normal disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
46
46
  buttonVariantsConfig
47
47
  );
48
48
  var Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
@@ -72,7 +72,7 @@ var badgeVariantsConfig = {
72
72
  }
73
73
  };
74
74
  var badgeVariants = cva(
75
- "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
75
+ "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium font-sans w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-component-xs [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] duration-normal overflow-hidden",
76
76
  badgeVariantsConfig
77
77
  );
78
78
  var Badge = React.forwardRef(({ className, variant, asChild = false, ...props }, ref) => {
@@ -97,7 +97,7 @@ var TextInput = React.forwardRef(
97
97
  type,
98
98
  "data-slot": "text-input",
99
99
  className: cn(
100
- "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
100
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] duration-normal outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium font-sans disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
101
101
  "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
102
102
  "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
103
103
  className
@@ -131,7 +131,7 @@ var Textarea = React.forwardRef(
131
131
  ref,
132
132
  "data-slot": "textarea",
133
133
  className: cn(
134
- "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
134
+ "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] duration-normal outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm font-sans",
135
135
  className
136
136
  ),
137
137
  ...props
@@ -209,7 +209,7 @@ function ModalOverlay({
209
209
  {
210
210
  "data-slot": "modal-overlay",
211
211
  className: cn(
212
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
212
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 duration-normal",
213
213
  className
214
214
  ),
215
215
  ...props
@@ -229,7 +229,7 @@ function ModalContent({
229
229
  {
230
230
  "data-slot": "modal-content",
231
231
  className: cn(
232
- "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
232
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-component-md rounded-lg border p-component-lg shadow-lg duration-normal font-sans sm:max-w-lg",
233
233
  className
234
234
  ),
235
235
  ...props,
@@ -256,7 +256,7 @@ function ModalHeader({ className, ...props }) {
256
256
  "div",
257
257
  {
258
258
  "data-slot": "modal-header",
259
- className: cn("flex flex-col gap-2 text-center sm:text-left", className),
259
+ className: cn("flex flex-col gap-component-sm text-center sm:text-left font-sans", className),
260
260
  ...props
261
261
  }
262
262
  );
@@ -267,7 +267,7 @@ function ModalFooter({ className, ...props }) {
267
267
  {
268
268
  "data-slot": "modal-footer",
269
269
  className: cn(
270
- "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
270
+ "flex flex-col-reverse gap-component-sm sm:flex-row sm:justify-end",
271
271
  className
272
272
  ),
273
273
  ...props
@@ -626,7 +626,39 @@ async function discoverThemes() {
626
626
  const tokensBase = typeof window !== "undefined" && window.__THEME_TOKENS_BASE__ ? window.__THEME_TOKENS_BASE__ : "/tokens";
627
627
  const knownCategories = Object.keys(baseThemeCategories);
628
628
  for (const category of knownCategories) {
629
- const categoryPath = `${tokensBase}/themes/${category}`;
629
+ const existingThemes = discovered[category]?.themes || {};
630
+ const themeIds = Object.keys(existingThemes);
631
+ const commonThemeNames = ["ocean", "forest", "sunset", "midnight", "pastel", "vibrant", "muted", "high-contrast"];
632
+ for (const themeName of commonThemeNames) {
633
+ if (themeIds.includes(themeName)) continue;
634
+ const themePath = `${tokensBase}/themes/${category}/${themeName}.json`;
635
+ try {
636
+ const response = await fetch(themePath);
637
+ if (response.ok && response.headers.get("content-type")?.includes("application/json")) {
638
+ const themeData = await response.json();
639
+ registerTheme(category, themeName, {
640
+ name: themeData.name || themeName.charAt(0).toUpperCase() + themeName.slice(1),
641
+ file: `${category}/${themeName}.json`,
642
+ icon: themeData.icon || "\u{1F3A8}",
643
+ description: themeData.description || `Custom ${category} theme: ${themeName}`
644
+ });
645
+ if (!discovered[category]) {
646
+ discovered[category] = {
647
+ name: category.charAt(0).toUpperCase() + category.slice(1),
648
+ order: baseThemeCategories[category]?.order || 99,
649
+ themes: {}
650
+ };
651
+ }
652
+ discovered[category].themes[themeName] = {
653
+ name: themeData.name || themeName.charAt(0).toUpperCase() + themeName.slice(1),
654
+ file: `${category}/${themeName}.json`,
655
+ icon: themeData.icon || "\u{1F3A8}",
656
+ description: themeData.description || `Custom ${category} theme: ${themeName}`
657
+ };
658
+ }
659
+ } catch {
660
+ }
661
+ }
630
662
  }
631
663
  discoveredThemesCache = discovered;
632
664
  return discovered;
@@ -643,10 +675,14 @@ function registerTheme(category, themeId, metadata) {
643
675
  }
644
676
  const cache = discoveredThemesCache;
645
677
  if (!cache[category]) {
678
+ let order = 99;
679
+ if (THEME_CATEGORY_ORDER.includes(category)) {
680
+ const index = THEME_CATEGORY_ORDER.indexOf(category);
681
+ order = (index + 1) * 10;
682
+ }
646
683
  cache[category] = {
647
684
  name: category.charAt(0).toUpperCase() + category.slice(1),
648
- order: 99,
649
- // Custom categories get high order
685
+ order,
650
686
  themes: {}
651
687
  };
652
688
  }
@@ -658,6 +694,29 @@ function registerTheme(category, themeId, metadata) {
658
694
  };
659
695
  return cache;
660
696
  }
697
+ async function registerThemeFromFile(category, themeId, filePath) {
698
+ const tokensBase = typeof window !== "undefined" && window.__THEME_TOKENS_BASE__ ? window.__THEME_TOKENS_BASE__ : "/tokens";
699
+ const path = filePath || `${tokensBase}/themes/${category}/${themeId}.json`;
700
+ try {
701
+ const response = await fetch(path);
702
+ if (!response.ok) {
703
+ throw new Error(`Failed to load theme file: ${response.statusText}`);
704
+ }
705
+ const themeData = await response.json();
706
+ registerTheme(category, themeId, {
707
+ name: themeData.name || themeId.charAt(0).toUpperCase() + themeId.slice(1),
708
+ file: filePath || `${category}/${themeId}.json`,
709
+ icon: themeData.icon || "\u{1F3A8}",
710
+ description: themeData.description || `Custom ${category} theme: ${themeId}`
711
+ });
712
+ return { success: true, themeId, category };
713
+ } catch (error) {
714
+ if (typeof window !== "undefined" && window.__DESIGN_SYSTEM_DEBUG__) {
715
+ console.error(`Failed to register theme from ${path}:`, error);
716
+ }
717
+ throw error;
718
+ }
719
+ }
661
720
  async function getThemeCategories() {
662
721
  return await discoverThemes();
663
722
  }
@@ -836,6 +895,8 @@ function flattenToCSS(tokens, prefix = "", result = {}, isColorContext = false)
836
895
  const enteringColor = key === "color" && prefix === "";
837
896
  const enteringTypography = key === "typography" && prefix === "";
838
897
  const enteringShape = key === "shape" && prefix === "";
898
+ const enteringAnimation = key === "animation" && prefix === "";
899
+ const enteringDensity = key === "spacing" && prefix === "";
839
900
  const inColorContext = isColorContext || enteringColor;
840
901
  if (enteringColor) {
841
902
  flattenToCSS(value, "", result, true);
@@ -843,6 +904,10 @@ function flattenToCSS(tokens, prefix = "", result = {}, isColorContext = false)
843
904
  flattenToCSS(value, "", result, false);
844
905
  } else if (enteringShape) {
845
906
  flattenToCSS(value, "", result, false);
907
+ } else if (enteringAnimation) {
908
+ flattenToCSS(value, "", result, false);
909
+ } else if (enteringDensity) {
910
+ flattenToCSS(value, "spacing", result, false);
846
911
  } else if (inColorContext) {
847
912
  flattenToCSS(value, "", result, true);
848
913
  } else {
@@ -1558,6 +1623,8 @@ function flattenToCSSSync(tokens, prefix = "", result = {}, isColorContext = fal
1558
1623
  const enteringColor = key === "color" && prefix === "";
1559
1624
  const enteringTypography = key === "typography" && prefix === "";
1560
1625
  const enteringShape = key === "shape" && prefix === "";
1626
+ const enteringAnimation = key === "animation" && prefix === "";
1627
+ const enteringDensity = key === "spacing" && prefix === "";
1561
1628
  const inColorContext = isColorContext || enteringColor;
1562
1629
  if (enteringColor) {
1563
1630
  flattenToCSSSync(value, "", result, true);
@@ -1565,6 +1632,10 @@ function flattenToCSSSync(tokens, prefix = "", result = {}, isColorContext = fal
1565
1632
  flattenToCSSSync(value, "", result, false);
1566
1633
  } else if (enteringShape) {
1567
1634
  flattenToCSSSync(value, "", result, false);
1635
+ } else if (enteringAnimation) {
1636
+ flattenToCSSSync(value, "", result, false);
1637
+ } else if (enteringDensity) {
1638
+ flattenToCSSSync(value, "spacing", result, false);
1568
1639
  } else if (inColorContext) {
1569
1640
  flattenToCSSSync(value, "", result, true);
1570
1641
  } else {
@@ -1608,4 +1679,4 @@ function mapToTailwindVarsSync(cssVars) {
1608
1679
  return mapped;
1609
1680
  }
1610
1681
 
1611
- export { Badge, Button, Checkbox, Label, Modal, ModalClose, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalOverlay, ModalPortal, ModalTitle, ModalTrigger, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, THEME_CATEGORY_ORDER, TextInput, Textarea, ThemeToggle, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, applyThemeSync, badgeVariants, buttonVariants, enableDebugMode, getCurrentCSSVariables, getTheme, getThemeCategories, getThemeFilePath, getThemesForCategory, registerTheme, useTheme, useThemeToggle };
1682
+ export { Badge, Button, Checkbox, Label, Modal, ModalClose, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalOverlay, ModalPortal, ModalTitle, ModalTrigger, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, THEME_CATEGORY_ORDER, TextInput, Textarea, ThemeToggle, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, applyThemeSync, badgeVariants, buttonVariants, enableDebugMode, getCurrentCSSVariables, getTheme, getThemeCategories, getThemeFilePath, getThemesForCategory, registerTheme, registerThemeFromFile, useTheme, useThemeToggle };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shru-design-system",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "A React component library with atoms and molecules built on Radix UI and Tailwind CSS",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -171,13 +171,31 @@
171
171
  for (var key in tokens) {
172
172
  var value = tokens[key];
173
173
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
174
- // Token files are already in correct structure (no nested typography/shape wrappers)
174
+ // Special handling: color, typography, shape, animation, and density objects flatten without their category prefix
175
175
  var enteringColor = key === 'color' && prefix === '';
176
+ var enteringTypography = key === 'typography' && prefix === '';
177
+ var enteringShape = key === 'shape' && prefix === '';
178
+ var enteringAnimation = key === 'animation' && prefix === '';
179
+ var enteringDensity = key === 'spacing' && prefix === ''; // Density uses spacing key
176
180
  var inColorContext = isColorContext || enteringColor;
177
181
 
178
182
  if (enteringColor) {
179
183
  // When entering color object, flatten without "color-" prefix
180
184
  flattenToCSS(value, '', result, true);
185
+ } else if (enteringTypography) {
186
+ // When entering typography object, flatten without "typography-" prefix
187
+ flattenToCSS(value, '', result, false);
188
+ } else if (enteringShape) {
189
+ // When entering shape object, flatten without "shape-" prefix
190
+ flattenToCSS(value, '', result, false);
191
+ } else if (enteringAnimation) {
192
+ // When entering animation object, flatten without "animation-" prefix
193
+ // animation.duration.fast → --duration-fast
194
+ flattenToCSS(value, '', result, false);
195
+ } else if (enteringDensity) {
196
+ // When entering density/spacing object at root, we need to preserve "spacing-" prefix
197
+ // spacing.component.md → --spacing-component-md
198
+ flattenToCSS(value, 'spacing', result, false);
181
199
  } else if (inColorContext) {
182
200
  // Already in color context, continue with empty prefix
183
201
  flattenToCSS(value, '', result, true);
@@ -217,11 +217,33 @@ function flattenToCSSSync(tokens, prefix = '', result = {}, isColorContext = fal
217
217
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
218
218
  // Token files are already in correct structure (no nested typography/shape wrappers)
219
219
  const enteringColor = key === 'color' && prefix === '';
220
+ const enteringTypography = key === 'typography' && prefix === '';
221
+ const enteringShape = key === 'shape' && prefix === '';
222
+ const enteringAnimation = key === 'animation' && prefix === '';
223
+ const enteringDensity = key === 'spacing' && prefix === ''; // Density uses spacing key
220
224
  const inColorContext = isColorContext || enteringColor;
221
225
  if (enteringColor) {
222
226
  // When entering color object, flatten without "color-" prefix
223
227
  flattenToCSSSync(value, '', result, true);
224
228
  }
229
+ else if (enteringTypography) {
230
+ // When entering typography object, flatten without "typography-" prefix
231
+ flattenToCSSSync(value, '', result, false);
232
+ }
233
+ else if (enteringShape) {
234
+ // When entering shape object, flatten without "shape-" prefix
235
+ flattenToCSSSync(value, '', result, false);
236
+ }
237
+ else if (enteringAnimation) {
238
+ // When entering animation object, flatten without "animation-" prefix
239
+ // animation.duration.fast → --duration-fast
240
+ flattenToCSSSync(value, '', result, false);
241
+ }
242
+ else if (enteringDensity) {
243
+ // When entering density/spacing object at root, we need to preserve "spacing-" prefix
244
+ // spacing.component.md → --spacing-component-md
245
+ flattenToCSSSync(value, 'spacing', result, false);
246
+ }
225
247
  else if (inColorContext) {
226
248
  // Already in color context, continue with empty prefix
227
249
  flattenToCSSSync(value, '', result, true);
package/scripts/init.js CHANGED
@@ -63,19 +63,159 @@ function installPackage(packageName, isDev = true) {
63
63
  function createTailwindConfig() {
64
64
  const configPath = path.join(process.cwd(), 'tailwind.config.js');
65
65
 
66
+ // Required configs that must be present
67
+ const requiredConfigs = {
68
+ fontFamily: 'fontFamily',
69
+ spacing: 'spacing',
70
+ gap: 'gap',
71
+ transitionDuration: 'transitionDuration'
72
+ };
73
+
66
74
  if (fs.existsSync(configPath)) {
67
- log('tailwind.config.js already exists. Updating it...', 'yellow');
75
+ log('tailwind.config.js already exists. Checking for required configs...', 'yellow');
68
76
  const existing = fs.readFileSync(configPath, 'utf8');
69
77
 
70
- // Check if our config is already there
71
- if (existing.includes(PACKAGE_NAME)) {
72
- log(`Configuration already includes ${PACKAGE_NAME} setup.`, 'green');
78
+ // Check if all required configs are present with proper values
79
+ const hasFontFamily = existing.includes('fontFamily') && existing.includes('--font-sans');
80
+ const hasSpacing = existing.includes('spacing:') && existing.includes('component-xs');
81
+ const hasGap = existing.includes('gap:') && existing.includes('component-xs');
82
+ const hasTransitionDuration = existing.includes('transitionDuration') && existing.includes('duration-fast');
83
+
84
+ if (hasFontFamily && hasSpacing && hasGap && hasTransitionDuration) {
85
+ log(`Configuration already includes all required ${PACKAGE_NAME} setup.`, 'green');
73
86
  return;
74
87
  }
75
88
 
76
- // Try to merge (basic approach - user might need to do this manually)
77
- log('Please manually merge the Tailwind config. See docs for details.', 'yellow');
78
- return;
89
+ // Track what's missing
90
+ const missingConfigs = [];
91
+ if (!hasFontFamily) missingConfigs.push('fontFamily');
92
+ if (!hasSpacing) missingConfigs.push('spacing');
93
+ if (!hasGap) missingConfigs.push('gap');
94
+ if (!hasTransitionDuration) missingConfigs.push('transitionDuration');
95
+
96
+ // If configs are missing, update the file
97
+ if (missingConfigs.length > 0) {
98
+ log(`Missing required configs: ${missingConfigs.join(', ')}. Updating...`, 'yellow');
99
+
100
+ // Read the existing config
101
+ let updated = existing;
102
+
103
+ // Add fontFamily if missing
104
+ if (!hasFontFamily) {
105
+ const fontFamilyConfig = ` // ⚠️ IF YOU UPDATE fontFamily CONFIG, ALSO UPDATE:
106
+ // 1. test/tailwind.config.js - fontFamily config (test app)
107
+ fontFamily: {
108
+ sans: ["var(--font-sans)", "system-ui", "sans-serif"],
109
+ body: ["var(--font-body)", "var(--font-sans)", "system-ui", "sans-serif"],
110
+ },`;
111
+
112
+ // Insert after borderRadius
113
+ if (existing.includes('borderRadius')) {
114
+ updated = updated.replace(
115
+ /(borderRadius: \{[\s\S]*?\},)/,
116
+ `$1\n${fontFamilyConfig}`
117
+ );
118
+ } else {
119
+ // Insert after colors
120
+ updated = updated.replace(
121
+ /(ring: "hsl\(var\(--ring\)\)",)/,
122
+ `$1\n },\n${fontFamilyConfig}`
123
+ );
124
+ }
125
+ }
126
+
127
+ // Add spacing if missing
128
+ if (!hasSpacing) {
129
+ const spacingConfig = ` // ⚠️ IF YOU UPDATE spacing CONFIG, ALSO UPDATE:
130
+ // 1. test/tailwind.config.js - spacing config (test app)
131
+ spacing: {
132
+ 'component-xs': "var(--spacing-component-xs, 0.25rem)",
133
+ 'component-sm': "var(--spacing-component-sm, 0.5rem)",
134
+ 'component-md': "var(--spacing-component-md, 1rem)",
135
+ 'component-lg': "var(--spacing-component-lg, 1.5rem)",
136
+ 'component-xl': "var(--spacing-component-xl, 2rem)",
137
+ },`;
138
+
139
+ // Insert after fontFamily or borderRadius
140
+ if (updated.includes('fontFamily')) {
141
+ updated = updated.replace(
142
+ /(fontFamily: \{[\s\S]*?\},)/,
143
+ `$1\n${spacingConfig}`
144
+ );
145
+ } else if (updated.includes('borderRadius')) {
146
+ updated = updated.replace(
147
+ /(borderRadius: \{[\s\S]*?\},)/,
148
+ `$1\n${spacingConfig}`
149
+ );
150
+ }
151
+ }
152
+
153
+ // Add gap if missing
154
+ if (!hasGap) {
155
+ const gapConfig = ` // ⚠️ IF YOU UPDATE gap CONFIG, ALSO UPDATE:
156
+ // 1. test/tailwind.config.js - gap config (test app)
157
+ gap: {
158
+ 'component-xs': "var(--spacing-component-xs, 0.25rem)",
159
+ 'component-sm': "var(--spacing-component-sm, 0.5rem)",
160
+ 'component-md': "var(--spacing-component-md, 1rem)",
161
+ 'component-lg': "var(--spacing-component-lg, 1.5rem)",
162
+ 'component-xl': "var(--spacing-component-xl, 2rem)",
163
+ },`;
164
+
165
+ // Insert after spacing
166
+ if (updated.includes('spacing:')) {
167
+ updated = updated.replace(
168
+ /(spacing: \{[\s\S]*?\},)/,
169
+ `$1\n${gapConfig}`
170
+ );
171
+ } else if (updated.includes('fontFamily')) {
172
+ updated = updated.replace(
173
+ /(fontFamily: \{[\s\S]*?\},)/,
174
+ `$1\n${gapConfig}`
175
+ );
176
+ }
177
+ }
178
+
179
+ // Add transitionDuration if missing
180
+ if (!hasTransitionDuration) {
181
+ const transitionConfig = ` // ⚠️ IF YOU UPDATE transitionDuration CONFIG, ALSO UPDATE:
182
+ // 1. test/tailwind.config.js - transitionDuration config (test app)
183
+ transitionDuration: {
184
+ 'fast': "var(--duration-fast, 150ms)",
185
+ 'normal': "var(--duration-normal, 300ms)",
186
+ 'slow': "var(--duration-slow, 500ms)",
187
+ },`;
188
+
189
+ // Insert after gap or spacing
190
+ if (updated.includes('gap:')) {
191
+ updated = updated.replace(
192
+ /(gap: \{[\s\S]*?\},)/,
193
+ `$1\n${transitionConfig}`
194
+ );
195
+ } else if (updated.includes('spacing:')) {
196
+ updated = updated.replace(
197
+ /(spacing: \{[\s\S]*?\},)/,
198
+ `$1\n${transitionConfig}`
199
+ );
200
+ } else if (updated.includes('fontFamily')) {
201
+ updated = updated.replace(
202
+ /(fontFamily: \{[\s\S]*?\},)/,
203
+ `$1\n${transitionConfig}`
204
+ );
205
+ }
206
+ }
207
+
208
+ // Write the updated config
209
+ fs.writeFileSync(configPath, updated);
210
+ log('Updated tailwind.config.js with missing configs', 'green');
211
+ return;
212
+ }
213
+
214
+ // If config exists but doesn't have our package name, warn user
215
+ if (!existing.includes(PACKAGE_NAME) && !existing.includes('shru-design-system')) {
216
+ log('Please manually merge the Tailwind config. See docs for details.', 'yellow');
217
+ return;
218
+ }
79
219
  }
80
220
 
81
221
  const config = `/** @type {import('tailwindcss').Config} */
@@ -119,15 +259,21 @@ export default {
119
259
  input: "hsl(var(--input))",
120
260
  ring: "hsl(var(--ring))",
121
261
  },
262
+ // ⚠️ IF YOU UPDATE borderRadius CONFIG, ALSO UPDATE:
263
+ // 1. test/tailwind.config.js - borderRadius config (test app)
122
264
  borderRadius: {
123
265
  lg: "var(--radius)",
124
266
  md: "calc(var(--radius) - 2px)",
125
267
  sm: "calc(var(--radius) - 4px)",
126
268
  },
269
+ // ⚠️ IF YOU UPDATE fontFamily CONFIG, ALSO UPDATE:
270
+ // 1. test/tailwind.config.js - fontFamily config (test app)
127
271
  fontFamily: {
128
272
  sans: ["var(--font-sans)", "system-ui", "sans-serif"],
129
273
  body: ["var(--font-body)", "var(--font-sans)", "system-ui", "sans-serif"],
130
274
  },
275
+ // ⚠️ IF YOU UPDATE spacing CONFIG, ALSO UPDATE:
276
+ // 1. test/tailwind.config.js - spacing config (test app)
131
277
  spacing: {
132
278
  'component-xs': "var(--spacing-component-xs, 0.25rem)",
133
279
  'component-sm': "var(--spacing-component-sm, 0.5rem)",
@@ -135,6 +281,13 @@ export default {
135
281
  'component-lg': "var(--spacing-component-lg, 1.5rem)",
136
282
  'component-xl': "var(--spacing-component-xl, 2rem)",
137
283
  },
284
+ gap: {
285
+ 'component-xs': "var(--spacing-component-xs, 0.25rem)",
286
+ 'component-sm': "var(--spacing-component-sm, 0.5rem)",
287
+ 'component-md': "var(--spacing-component-md, 1rem)",
288
+ 'component-lg': "var(--spacing-component-lg, 1.5rem)",
289
+ 'component-xl': "var(--spacing-component-xl, 2rem)",
290
+ },
138
291
  transitionDuration: {
139
292
  'fast': "var(--duration-fast, 150ms)",
140
293
  'normal': "var(--duration-normal, 300ms)",
@@ -222,6 +375,7 @@ function createCSSFile() {
222
375
  body {
223
376
  background-color: hsl(var(--background));
224
377
  color: hsl(var(--foreground));
378
+ font-family: var(--font-sans), system-ui, sans-serif;
225
379
  }
226
380
  }
227
381
  `;
@@ -264,6 +418,7 @@ function createCSSFile() {
264
418
  body {
265
419
  background-color: hsl(var(--background));
266
420
  color: hsl(var(--foreground));
421
+ font-family: var(--font-sans), system-ui, sans-serif;
267
422
  }
268
423
  }
269
424
  `;
@@ -290,8 +445,8 @@ function getLibraryTokensPath() {
290
445
  }
291
446
 
292
447
  /**
293
- * Recursively copy directory, preserving user files
294
- * Only copies/updates files that were created by the library
448
+ * Recursively copy directory, always overwriting library files
449
+ * Never preserves existing library files - always overwrites or creates new
295
450
  */
296
451
  function copyDirectory(src, dest) {
297
452
  if (!fs.existsSync(src)) {
@@ -314,13 +469,13 @@ function copyDirectory(src, dest) {
314
469
  const subCopied = copyDirectory(srcPath, destPath);
315
470
  if (subCopied) copiedCount += subCopied;
316
471
  } else if (entry.isFile() && entry.name.endsWith('.json')) {
317
- // For JSON files, check if it's a library file or user file
472
+ // For JSON files, always overwrite if source is a library file
318
473
  try {
319
474
  const srcContent = JSON.parse(fs.readFileSync(srcPath, 'utf8'));
320
475
  const isLibraryFile = srcContent._createdBy && srcContent._createdBy.includes(LIBRARY_NAME.split(' ')[0]);
321
476
 
322
477
  if (isLibraryFile) {
323
- // Always update library files
478
+ // Always overwrite library files (never preserve)
324
479
  fs.copyFileSync(srcPath, destPath);
325
480
  copiedCount++;
326
481
  } else if (!fs.existsSync(destPath)) {
@@ -328,10 +483,20 @@ function copyDirectory(src, dest) {
328
483
  fs.copyFileSync(srcPath, destPath);
329
484
  copiedCount++;
330
485
  }
331
- // If dest exists and is not a library file, preserve it (user's custom file)
486
+ // If source is not a library file, don't copy (preserve user's custom files)
332
487
  } catch (e) {
333
- // If JSON parsing fails, just copy it
334
- if (!fs.existsSync(destPath)) {
488
+ // If JSON parsing fails, check if it's a known library file path
489
+ // Known library files: base.json, palettes.json, and all files in themes/
490
+ const isKnownLibraryFile = entry.name === 'base.json' ||
491
+ entry.name === 'palettes.json' ||
492
+ srcPath.includes(path.join('tokens', 'themes'));
493
+
494
+ if (isKnownLibraryFile) {
495
+ // Always overwrite known library files
496
+ fs.copyFileSync(srcPath, destPath);
497
+ copiedCount++;
498
+ } else if (!fs.existsSync(destPath)) {
499
+ // New file that doesn't exist in dest
335
500
  fs.copyFileSync(srcPath, destPath);
336
501
  copiedCount++;
337
502
  }
@@ -446,39 +611,22 @@ function createTokenFiles() {
446
611
  tokenFilesToCheck.forEach(relativePath => {
447
612
  const destPath = path.join(tokensDir, relativePath);
448
613
 
449
- // If file exists, check if it needs migration
450
- if (fs.existsSync(destPath)) {
451
- try {
452
- const existing = JSON.parse(fs.readFileSync(destPath, 'utf8'));
453
- const migrated = migrateTokenStructure(existing);
454
-
455
- // Check if migration changed the structure
456
- if (JSON.stringify(existing) !== JSON.stringify(migrated)) {
457
- migrated._createdBy = LIBRARY_NAME;
458
- fs.writeFileSync(destPath, JSON.stringify(migrated, null, 2));
459
- log(`Migrated ${relativePath} to new structure`, 'yellow');
460
- }
461
- } catch (e) {
462
- // If file is corrupted, try to read from library
463
- const libraryData = readLibraryTokenFile(relativePath);
464
- if (libraryData) {
465
- libraryData._createdBy = LIBRARY_NAME;
466
- fs.writeFileSync(destPath, JSON.stringify(libraryData, null, 2));
467
- log(`Restored ${relativePath} from library`, 'green');
468
- }
614
+ // Always read from library and overwrite (never preserve existing files)
615
+ const libraryData = readLibraryTokenFile(relativePath);
616
+ if (libraryData) {
617
+ // Ensure directory exists
618
+ const destDir = path.dirname(destPath);
619
+ if (!fs.existsSync(destDir)) {
620
+ fs.mkdirSync(destDir, { recursive: true });
469
621
  }
470
- } else {
471
- // File doesn't exist, try to read from library
472
- const libraryData = readLibraryTokenFile(relativePath);
473
- if (libraryData) {
474
- // Ensure directory exists
475
- const destDir = path.dirname(destPath);
476
- if (!fs.existsSync(destDir)) {
477
- fs.mkdirSync(destDir, { recursive: true });
478
- }
479
-
480
- libraryData._createdBy = LIBRARY_NAME;
481
- fs.writeFileSync(destPath, JSON.stringify(libraryData, null, 2));
622
+
623
+ // Always overwrite with library version
624
+ libraryData._createdBy = LIBRARY_NAME;
625
+ fs.writeFileSync(destPath, JSON.stringify(libraryData, null, 2));
626
+
627
+ if (fs.existsSync(destPath)) {
628
+ log(`Updated ${relativePath} from library`, 'green');
629
+ } else {
482
630
  log(`Created ${relativePath} from library`, 'green');
483
631
  }
484
632
  }
@@ -231,11 +231,33 @@ function flattenToCSS(tokens, prefix = '', result = {}, isColorContext = false)
231
231
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
232
232
  // Token files are already in correct structure (no nested typography/shape wrappers)
233
233
  const enteringColor = key === 'color' && prefix === '';
234
+ const enteringTypography = key === 'typography' && prefix === '';
235
+ const enteringShape = key === 'shape' && prefix === '';
236
+ const enteringAnimation = key === 'animation' && prefix === '';
237
+ const enteringDensity = key === 'spacing' && prefix === ''; // Density uses spacing key
234
238
  const inColorContext = isColorContext || enteringColor;
235
239
  if (enteringColor) {
236
240
  // When entering color object, flatten without "color-" prefix
237
241
  flattenToCSS(value, '', result, true);
238
242
  }
243
+ else if (enteringTypography) {
244
+ // When entering typography object, flatten without "typography-" prefix
245
+ flattenToCSS(value, '', result, false);
246
+ }
247
+ else if (enteringShape) {
248
+ // When entering shape object, flatten without "shape-" prefix
249
+ flattenToCSS(value, '', result, false);
250
+ }
251
+ else if (enteringAnimation) {
252
+ // When entering animation object, flatten without "animation-" prefix
253
+ // animation.duration.fast → --duration-fast
254
+ flattenToCSS(value, '', result, false);
255
+ }
256
+ else if (enteringDensity) {
257
+ // When entering density/spacing object at root, we need to preserve "spacing-" prefix
258
+ // spacing.component.md → --spacing-component-md
259
+ flattenToCSS(value, 'spacing', result, false);
260
+ }
239
261
  else if (inColorContext) {
240
262
  // Already in color context, continue with empty prefix
241
263
  flattenToCSS(value, '', result, true);
@@ -42,6 +42,32 @@
42
42
  "800": "#991b1b",
43
43
  "900": "#7f1d1d",
44
44
  "950": "#450a0a"
45
+ },
46
+ "purple": {
47
+ "50": "#faf5ff",
48
+ "100": "#f3e8ff",
49
+ "200": "#e9d5ff",
50
+ "300": "#d8b4fe",
51
+ "400": "#c084fc",
52
+ "500": "#a855f7",
53
+ "600": "#9333ea",
54
+ "700": "#7e22ce",
55
+ "800": "#6b21a8",
56
+ "900": "#581c87",
57
+ "950": "#3b0764"
58
+ },
59
+ "pink": {
60
+ "50": "#fdf2f8",
61
+ "100": "#fce7f3",
62
+ "200": "#fbcfe8",
63
+ "300": "#f9a8d4",
64
+ "400": "#f472b6",
65
+ "500": "#ec4899",
66
+ "600": "#db2777",
67
+ "700": "#be185d",
68
+ "800": "#9f1239",
69
+ "900": "#831843",
70
+ "950": "#500724"
45
71
  }
46
72
  }
47
73
  }