shru-design-system 0.1.8 → 0.1.9

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.9",
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
@@ -119,15 +119,21 @@ export default {
119
119
  input: "hsl(var(--input))",
120
120
  ring: "hsl(var(--ring))",
121
121
  },
122
+ // ⚠️ IF YOU UPDATE borderRadius CONFIG, ALSO UPDATE:
123
+ // 1. test/tailwind.config.js - borderRadius config (test app)
122
124
  borderRadius: {
123
125
  lg: "var(--radius)",
124
126
  md: "calc(var(--radius) - 2px)",
125
127
  sm: "calc(var(--radius) - 4px)",
126
128
  },
129
+ // ⚠️ IF YOU UPDATE fontFamily CONFIG, ALSO UPDATE:
130
+ // 1. test/tailwind.config.js - fontFamily config (test app)
127
131
  fontFamily: {
128
132
  sans: ["var(--font-sans)", "system-ui", "sans-serif"],
129
133
  body: ["var(--font-body)", "var(--font-sans)", "system-ui", "sans-serif"],
130
134
  },
135
+ // ⚠️ IF YOU UPDATE spacing CONFIG, ALSO UPDATE:
136
+ // 1. test/tailwind.config.js - spacing config (test app)
131
137
  spacing: {
132
138
  'component-xs': "var(--spacing-component-xs, 0.25rem)",
133
139
  'component-sm': "var(--spacing-component-sm, 0.5rem)",
@@ -135,6 +141,13 @@ export default {
135
141
  'component-lg': "var(--spacing-component-lg, 1.5rem)",
136
142
  'component-xl': "var(--spacing-component-xl, 2rem)",
137
143
  },
144
+ gap: {
145
+ 'component-xs': "var(--spacing-component-xs, 0.25rem)",
146
+ 'component-sm': "var(--spacing-component-sm, 0.5rem)",
147
+ 'component-md': "var(--spacing-component-md, 1rem)",
148
+ 'component-lg': "var(--spacing-component-lg, 1.5rem)",
149
+ 'component-xl': "var(--spacing-component-xl, 2rem)",
150
+ },
138
151
  transitionDuration: {
139
152
  'fast': "var(--duration-fast, 150ms)",
140
153
  'normal': "var(--duration-normal, 300ms)",
@@ -222,6 +235,7 @@ function createCSSFile() {
222
235
  body {
223
236
  background-color: hsl(var(--background));
224
237
  color: hsl(var(--foreground));
238
+ font-family: var(--font-sans), system-ui, sans-serif;
225
239
  }
226
240
  }
227
241
  `;
@@ -264,6 +278,7 @@ function createCSSFile() {
264
278
  body {
265
279
  background-color: hsl(var(--background));
266
280
  color: hsl(var(--foreground));
281
+ font-family: var(--font-sans), system-ui, sans-serif;
267
282
  }
268
283
  }
269
284
  `;
@@ -290,8 +305,8 @@ function getLibraryTokensPath() {
290
305
  }
291
306
 
292
307
  /**
293
- * Recursively copy directory, preserving user files
294
- * Only copies/updates files that were created by the library
308
+ * Recursively copy directory, always overwriting library files
309
+ * Never preserves existing library files - always overwrites or creates new
295
310
  */
296
311
  function copyDirectory(src, dest) {
297
312
  if (!fs.existsSync(src)) {
@@ -314,13 +329,13 @@ function copyDirectory(src, dest) {
314
329
  const subCopied = copyDirectory(srcPath, destPath);
315
330
  if (subCopied) copiedCount += subCopied;
316
331
  } else if (entry.isFile() && entry.name.endsWith('.json')) {
317
- // For JSON files, check if it's a library file or user file
332
+ // For JSON files, always overwrite if source is a library file
318
333
  try {
319
334
  const srcContent = JSON.parse(fs.readFileSync(srcPath, 'utf8'));
320
335
  const isLibraryFile = srcContent._createdBy && srcContent._createdBy.includes(LIBRARY_NAME.split(' ')[0]);
321
336
 
322
337
  if (isLibraryFile) {
323
- // Always update library files
338
+ // Always overwrite library files (never preserve)
324
339
  fs.copyFileSync(srcPath, destPath);
325
340
  copiedCount++;
326
341
  } else if (!fs.existsSync(destPath)) {
@@ -328,10 +343,20 @@ function copyDirectory(src, dest) {
328
343
  fs.copyFileSync(srcPath, destPath);
329
344
  copiedCount++;
330
345
  }
331
- // If dest exists and is not a library file, preserve it (user's custom file)
346
+ // If source is not a library file, don't copy (preserve user's custom files)
332
347
  } catch (e) {
333
- // If JSON parsing fails, just copy it
334
- if (!fs.existsSync(destPath)) {
348
+ // If JSON parsing fails, check if it's a known library file path
349
+ // Known library files: base.json, palettes.json, and all files in themes/
350
+ const isKnownLibraryFile = entry.name === 'base.json' ||
351
+ entry.name === 'palettes.json' ||
352
+ srcPath.includes(path.join('tokens', 'themes'));
353
+
354
+ if (isKnownLibraryFile) {
355
+ // Always overwrite known library files
356
+ fs.copyFileSync(srcPath, destPath);
357
+ copiedCount++;
358
+ } else if (!fs.existsSync(destPath)) {
359
+ // New file that doesn't exist in dest
335
360
  fs.copyFileSync(srcPath, destPath);
336
361
  copiedCount++;
337
362
  }
@@ -446,39 +471,22 @@ function createTokenFiles() {
446
471
  tokenFilesToCheck.forEach(relativePath => {
447
472
  const destPath = path.join(tokensDir, relativePath);
448
473
 
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
- }
474
+ // Always read from library and overwrite (never preserve existing files)
475
+ const libraryData = readLibraryTokenFile(relativePath);
476
+ if (libraryData) {
477
+ // Ensure directory exists
478
+ const destDir = path.dirname(destPath);
479
+ if (!fs.existsSync(destDir)) {
480
+ fs.mkdirSync(destDir, { recursive: true });
469
481
  }
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));
482
+
483
+ // Always overwrite with library version
484
+ libraryData._createdBy = LIBRARY_NAME;
485
+ fs.writeFileSync(destPath, JSON.stringify(libraryData, null, 2));
486
+
487
+ if (fs.existsSync(destPath)) {
488
+ log(`Updated ${relativePath} from library`, 'green');
489
+ } else {
482
490
  log(`Created ${relativePath} from library`, 'green');
483
491
  }
484
492
  }
@@ -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
  }