@stevederico/skateboard-ui 2.12.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # CHANGELOG
2
2
 
3
+ 2.14.0
4
+
5
+ Switch icons to Lucide React
6
+ Update DynamicIcon Lucide loader
7
+ Remove Tabler icon imports
8
+
9
+ 2.13.0
10
+
11
+ Fix DynamicIcon per-icon imports
12
+ Remove lucide-react from views
13
+ Switch views to Tabler icons
14
+
3
15
  2.12.0
4
16
 
5
17
  Add skip-to-content link
@@ -14,64 +14,96 @@ function toPascalCase(str) {
14
14
  .join("");
15
15
  }
16
16
 
17
- let iconsModule = null;
18
- let iconsPromise = null;
17
+ /**
18
+ * Convert a PascalCase or camelCase string to kebab-case.
19
+ *
20
+ * @param {string} str - Input string
21
+ * @returns {string} kebab-case version
22
+ */
23
+ function toKebabCase(str) {
24
+ return str
25
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
26
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
27
+ .toLowerCase();
28
+ }
29
+
30
+ /** Cache of resolved icon components keyed by name */
31
+ const iconCache = new Map();
32
+
33
+ /** Cache of in-flight import promises keyed by module name */
34
+ const importCache = new Map();
19
35
 
20
36
  /**
21
- * Lazily load the lucide-react module on first use.
22
- * Subsequent calls return the cached module immediately.
37
+ * Load a single Lucide icon by its kebab-case file name (e.g. "arrow-right").
38
+ * Each icon is imported individually (~1KB) instead of loading the entire
39
+ * icon library. Results are cached for instant subsequent lookups.
23
40
  *
24
- * @returns {Promise<Object>} The lucide-react module
41
+ * @param {string} kebabName - kebab-case icon file name
42
+ * @param {string} cacheKey - PascalCase name used as cache key
43
+ * @returns {Promise<React.ComponentType|null>} Icon component or null
25
44
  */
26
- function loadIcons() {
27
- if (iconsModule) return Promise.resolve(iconsModule);
28
- if (!iconsPromise) {
29
- iconsPromise = import("lucide-react").then((mod) => {
30
- iconsModule = mod;
31
- return mod;
45
+ function loadIcon(kebabName, cacheKey) {
46
+ if (iconCache.has(cacheKey)) return Promise.resolve(iconCache.get(cacheKey));
47
+ if (importCache.has(cacheKey)) return importCache.get(cacheKey);
48
+
49
+ const promise = import(`/node_modules/lucide-react/dist/esm/icons/${kebabName}.js`)
50
+ .then((mod) => {
51
+ const Icon = mod.default || null;
52
+ iconCache.set(cacheKey, Icon);
53
+ importCache.delete(cacheKey);
54
+ return Icon;
55
+ })
56
+ .catch(() => {
57
+ iconCache.set(cacheKey, null);
58
+ importCache.delete(cacheKey);
59
+ return null;
32
60
  });
33
- }
34
- return iconsPromise;
61
+
62
+ importCache.set(cacheKey, promise);
63
+ return promise;
35
64
  }
36
65
 
37
66
  /**
38
- * Resolve icon name to a Lucide component from the cached module.
67
+ * Resolve an icon name in any format to a PascalCase name and kebab-case file path.
68
+ * e.g. "layout-dashboard" → { pascal: "LayoutDashboard", kebab: "layout-dashboard" }
69
+ * "LayoutDashboard" → { pascal: "LayoutDashboard", kebab: "layout-dashboard" }
39
70
  *
40
- * @param {Object} mod - The lucide-react module
41
- * @param {string} name - Icon name (kebab-case, snake_case, or PascalCase)
42
- * @returns {React.ComponentType|null} Icon component or null
71
+ * Strips legacy "Icon" prefix from Tabler-style names for backwards compatibility.
72
+ *
73
+ * @param {string} name - Icon name in any case format
74
+ * @returns {{ pascal: string, kebab: string }} Resolved icon name pair
43
75
  */
44
- function resolveIcon(mod, name) {
45
- if (!mod) return null;
46
- const candidates = [
47
- name,
48
- toPascalCase(name),
49
- name.charAt(0).toUpperCase() + name.slice(1),
50
- ];
51
- for (const candidate of candidates) {
52
- if (mod[candidate]) return mod[candidate];
76
+ function toIconName(name) {
77
+ let stripped = name;
78
+ if (stripped.startsWith("Icon") && stripped.length > 4 && stripped[4] === stripped[4].toUpperCase()) {
79
+ stripped = stripped.slice(4);
53
80
  }
54
- return null;
81
+ const pascal = /[-_\s]/.test(stripped) ? toPascalCase(stripped) : stripped;
82
+ const kebab = toKebabCase(pascal);
83
+ return { pascal, kebab };
55
84
  }
56
85
 
57
86
  /**
58
- * Check if a name string resolves to a valid Lucide icon.
59
- * Returns false until the icon module has been loaded.
87
+ * Check if a name string has been resolved to a valid icon.
88
+ * Returns false for unloaded or invalid icons.
60
89
  *
61
90
  * @param {string} name - Icon name to check
62
- * @returns {boolean} True if name resolves to an icon
91
+ * @returns {boolean} True if icon is cached and valid
63
92
  */
64
93
  export function canResolveIcon(name) {
65
- if (!iconsModule) return false;
66
- return resolveIcon(iconsModule, name) !== null;
94
+ const { pascal } = toIconName(name);
95
+ return iconCache.has(pascal) && iconCache.get(pascal) !== null;
67
96
  }
68
97
 
69
98
  /**
70
- * Render a Lucide icon by name string with lazy loading.
99
+ * Render a Lucide icon by name string with per-icon lazy loading.
100
+ *
101
+ * Each icon is imported individually from lucide-react (~1KB per icon)
102
+ * instead of loading the entire library. Resolved icons are cached in memory
103
+ * for instant rendering on subsequent uses.
71
104
  *
72
- * Loads lucide-react on first render via dynamic import, then caches the
73
- * module for all subsequent uses. Shows nothing while loading (typically
74
- * under 50ms from cache).
105
+ * Accepts kebab-case ("layout-dashboard"), PascalCase ("LayoutDashboard"),
106
+ * or legacy prefixed ("IconLayoutDashboard") names.
75
107
  *
76
108
  * @param {Object} props
77
109
  * @param {string} props.name - Icon name (e.g. "home", "arrow-right", "Settings")
@@ -96,18 +128,16 @@ const DynamicIcon = ({
96
128
  className,
97
129
  ...props
98
130
  }) => {
99
- const [Icon, setIcon] = useState(() => {
100
- if (iconsModule) return resolveIcon(iconsModule, name);
101
- return null;
102
- });
131
+ const { pascal, kebab } = toIconName(name);
132
+
133
+ const [Icon, setIcon] = useState(() => iconCache.get(pascal) || null);
103
134
 
104
135
  useEffect(() => {
105
136
  if (Icon) return;
106
- loadIcons().then((mod) => {
107
- const resolved = resolveIcon(mod, name);
108
- setIcon(() => resolved);
137
+ loadIcon(kebab, pascal).then((resolved) => {
138
+ if (resolved) setIcon(() => resolved);
109
139
  });
110
- }, [name, Icon]);
140
+ }, [pascal, kebab, Icon]);
111
141
 
112
142
  if (!Icon) return null;
113
143
 
@@ -116,7 +146,7 @@ const DynamicIcon = ({
116
146
  size={size}
117
147
  color={color}
118
148
  strokeWidth={strokeWidth}
119
- className={cn(className)}
149
+ className={className}
120
150
  {...props}
121
151
  />
122
152
  );
@@ -15,7 +15,7 @@ import {
15
15
  SidebarRail,
16
16
  useSidebar,
17
17
  } from "../shadcn/ui/sidebar";
18
- import { Settings } from "lucide-react";
18
+ import { Settings } from 'lucide-react';
19
19
 
20
20
  /**
21
21
  * Desktop navigation sidebar using shadcn primitives.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stevederico/skateboard-ui",
3
3
  "private": false,
4
- "version": "2.12.0",
4
+ "version": "2.14.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  "./Sidebar": {
@@ -1,16 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(for file in App.jsx AppSidebar.jsx Context.jsx Layout.jsx TabBar.jsx TextView.jsx SettingsView.jsx SignInView.jsx LandingView.jsx UpgradeSheet.jsx PaymentView.jsx SignUpView.jsx)",
5
- "Bash(do)",
6
- "Bash(if [ -f \"$file\" ])",
7
- "Bash(then)",
8
- "Bash(node --check:*)",
9
- "Bash(echo:*)",
10
- "Bash(fi)",
11
- "Bash(done)",
12
- "Bash(git commit:*)",
13
- "Bash(git push:*)"
14
- ]
15
- }
16
- }
Binary file
Binary file
Binary file
Binary file