@stevederico/skateboard-ui 2.13.0 → 2.15.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,16 @@
1
1
  # CHANGELOG
2
2
 
3
+ 2.15.0
4
+
5
+ Add authOverlay 401 retry logic
6
+ Add SHOW_AUTH_OVERLAY dispatch
7
+
8
+ 2.14.0
9
+
10
+ Switch icons to Lucide React
11
+ Update DynamicIcon Lucide loader
12
+ Remove Tabler icon imports
13
+
3
14
  2.13.0
4
15
 
5
16
  Fix DynamicIcon per-icon imports
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { useTheme } from 'next-themes';
3
- import { IconSun, IconMoon } from '@tabler/icons-react';
3
+ import { Sun, Moon } from 'lucide-react';
4
4
  import { Button } from '../shadcn/ui/button.jsx';
5
5
  import { cn } from '../shadcn/lib/utils.js';
6
6
 
@@ -45,8 +45,8 @@ export default function ThemeToggle({ className = "", iconSize = 16, variant = "
45
45
  {...props}
46
46
  >
47
47
  {isDarkMode
48
- ? <IconSun size={iconSize} />
49
- : <IconMoon size={iconSize} />
48
+ ? <Sun size={iconSize} />
49
+ : <Moon size={iconSize} />
50
50
  }
51
51
  </Button>
52
52
  );
@@ -16,7 +16,7 @@ import { Separator } from "../shadcn/ui/separator"
16
16
  import { Button } from "../shadcn/ui/button"
17
17
  import { getState } from "../core/Context.jsx";
18
18
  import { showCheckout } from '../core/Utilities.js';
19
- import { IconSparkles, IconCircleCheck } from '@tabler/icons-react';
19
+ import { Sparkles, CircleCheck } from 'lucide-react';
20
20
 
21
21
  /**
22
22
  * Premium upgrade drawer with pricing and checkout button.
@@ -104,7 +104,7 @@ const UpgradeSheet = forwardRef(function UpgradeSheet(props, ref) {
104
104
  <div className="w-full space-y-3">
105
105
  {product.features?.map((feature, index) => (
106
106
  <div key={index} className="flex items-center gap-3">
107
- <IconCircleCheck className="size-5 text-primary shrink-0" />
107
+ <CircleCheck className="size-5 text-primary shrink-0" />
108
108
  <span className="text-sm">{feature}</span>
109
109
  </div>
110
110
  ))}
@@ -115,7 +115,7 @@ const UpgradeSheet = forwardRef(function UpgradeSheet(props, ref) {
115
115
 
116
116
  <DrawerFooter>
117
117
  <Button className="w-full" size="lg" onClick={handleUpgrade}>
118
- <IconSparkles className="size-4" />
118
+ <Sparkles className="size-4" />
119
119
  Upgrade to {product.title}
120
120
  </Button>
121
121
  </DrawerFooter>
@@ -14,6 +14,19 @@ function toPascalCase(str) {
14
14
  .join("");
15
15
  }
16
16
 
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
+
17
30
  /** Cache of resolved icon components keyed by name */
18
31
  const iconCache = new Map();
19
32
 
@@ -21,44 +34,53 @@ const iconCache = new Map();
21
34
  const importCache = new Map();
22
35
 
23
36
  /**
24
- * Load a single Tabler icon by its PascalCase name (e.g. "IconLock").
37
+ * Load a single Lucide icon by its kebab-case file name (e.g. "arrow-right").
25
38
  * Each icon is imported individually (~1KB) instead of loading the entire
26
- * icon library (~400KB). Results are cached for instant subsequent lookups.
39
+ * icon library. Results are cached for instant subsequent lookups.
27
40
  *
28
- * @param {string} iconName - PascalCase icon name with "Icon" prefix
41
+ * @param {string} kebabName - kebab-case icon file name
42
+ * @param {string} cacheKey - PascalCase name used as cache key
29
43
  * @returns {Promise<React.ComponentType|null>} Icon component or null
30
44
  */
31
- function loadIcon(iconName) {
32
- if (iconCache.has(iconName)) return Promise.resolve(iconCache.get(iconName));
33
- if (importCache.has(iconName)) return importCache.get(iconName);
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);
34
48
 
35
- const promise = import(`@tabler/icons-react/dist/esm/icons/${iconName}.mjs`)
49
+ const promise = import(`/node_modules/lucide-react/dist/esm/icons/${kebabName}.js`)
36
50
  .then((mod) => {
37
- const Icon = mod.default || mod[iconName] || null;
38
- iconCache.set(iconName, Icon);
39
- importCache.delete(iconName);
51
+ const Icon = mod.default || null;
52
+ iconCache.set(cacheKey, Icon);
53
+ importCache.delete(cacheKey);
40
54
  return Icon;
41
55
  })
42
56
  .catch(() => {
43
- iconCache.set(iconName, null);
44
- importCache.delete(iconName);
57
+ iconCache.set(cacheKey, null);
58
+ importCache.delete(cacheKey);
45
59
  return null;
46
60
  });
47
61
 
48
- importCache.set(iconName, promise);
62
+ importCache.set(cacheKey, promise);
49
63
  return promise;
50
64
  }
51
65
 
52
66
  /**
53
- * Resolve a kebab-case icon name to a Tabler PascalCase module name.
54
- * e.g. "layout-dashboard" → "IconLayoutDashboard"
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" }
70
+ *
71
+ * Strips legacy "Icon" prefix from Tabler-style names for backwards compatibility.
55
72
  *
56
73
  * @param {string} name - Icon name in any case format
57
- * @returns {string} Tabler PascalCase name with "Icon" prefix
74
+ * @returns {{ pascal: string, kebab: string }} Resolved icon name pair
58
75
  */
59
- function toTablerName(name) {
60
- if (name.startsWith("Icon")) return name;
61
- return "Icon" + toPascalCase(name);
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);
80
+ }
81
+ const pascal = /[-_\s]/.test(stripped) ? toPascalCase(stripped) : stripped;
82
+ const kebab = toKebabCase(pascal);
83
+ return { pascal, kebab };
62
84
  }
63
85
 
64
86
  /**
@@ -69,19 +91,19 @@ function toTablerName(name) {
69
91
  * @returns {boolean} True if icon is cached and valid
70
92
  */
71
93
  export function canResolveIcon(name) {
72
- const tablerName = toTablerName(name);
73
- return iconCache.has(tablerName) && iconCache.get(tablerName) !== null;
94
+ const { pascal } = toIconName(name);
95
+ return iconCache.has(pascal) && iconCache.get(pascal) !== null;
74
96
  }
75
97
 
76
98
  /**
77
- * Render a Tabler icon by name string with per-icon lazy loading.
99
+ * Render a Lucide icon by name string with per-icon lazy loading.
78
100
  *
79
- * Each icon is imported individually from @tabler/icons-react (~1KB per icon)
101
+ * Each icon is imported individually from lucide-react (~1KB per icon)
80
102
  * instead of loading the entire library. Resolved icons are cached in memory
81
103
  * for instant rendering on subsequent uses.
82
104
  *
83
105
  * Accepts kebab-case ("layout-dashboard"), PascalCase ("LayoutDashboard"),
84
- * or prefixed ("IconLayoutDashboard") names.
106
+ * or legacy prefixed ("IconLayoutDashboard") names.
85
107
  *
86
108
  * @param {Object} props
87
109
  * @param {string} props.name - Icon name (e.g. "home", "arrow-right", "Settings")
@@ -106,16 +128,16 @@ const DynamicIcon = ({
106
128
  className,
107
129
  ...props
108
130
  }) => {
109
- const tablerName = toTablerName(name);
131
+ const { pascal, kebab } = toIconName(name);
110
132
 
111
- const [Icon, setIcon] = useState(() => iconCache.get(tablerName) || null);
133
+ const [Icon, setIcon] = useState(() => iconCache.get(pascal) || null);
112
134
 
113
135
  useEffect(() => {
114
136
  if (Icon) return;
115
- loadIcon(tablerName).then((resolved) => {
137
+ loadIcon(kebab, pascal).then((resolved) => {
116
138
  if (resolved) setIcon(() => resolved);
117
139
  });
118
- }, [tablerName, Icon]);
140
+ }, [pascal, kebab, Icon]);
119
141
 
120
142
  if (!Icon) return null;
121
143
 
package/core/Utilities.js CHANGED
@@ -723,11 +723,31 @@ export async function apiRequest(endpoint, options = {}) {
723
723
  if (timeoutId) clearTimeout(timeoutId);
724
724
  }
725
725
 
726
- // Handle 401 (redirect to signout, unless authOverlay mode)
726
+ // Handle 401 (redirect to signout, or show auth overlay and retry)
727
727
  if (response.status === 401) {
728
728
  if (getConstants().authOverlay !== true) {
729
729
  window.location.href = '/signout';
730
+ throw new Error('Unauthorized');
730
731
  }
732
+
733
+ // In authOverlay mode: show sign-in overlay, retry request after auth
734
+ const dispatch = getDispatch();
735
+ if (dispatch) {
736
+ return new Promise((resolve, reject) => {
737
+ dispatch({
738
+ type: 'SHOW_AUTH_OVERLAY',
739
+ payload: async () => {
740
+ try {
741
+ const result = await apiRequest(endpoint, options);
742
+ resolve(result);
743
+ } catch (err) {
744
+ reject(err);
745
+ }
746
+ }
747
+ });
748
+ });
749
+ }
750
+
731
751
  throw new Error('Unauthorized');
732
752
  }
733
753
 
@@ -15,7 +15,7 @@ import {
15
15
  SidebarRail,
16
16
  useSidebar,
17
17
  } from "../shadcn/ui/sidebar";
18
- import { IconSettings } from '@tabler/icons-react';
18
+ import { Settings } from 'lucide-react';
19
19
 
20
20
  /**
21
21
  * Desktop navigation sidebar using shadcn primitives.
@@ -111,7 +111,7 @@ export default function Sidebar({ variant = "inset", ...props }) {
111
111
  className="data-active:font-normal"
112
112
  onClick={() => navigate("/app/settings")}
113
113
  >
114
- <IconSettings size={20} strokeWidth={2} />
114
+ <Settings size={20} strokeWidth={2} />
115
115
  <span>Settings</span>
116
116
  </SidebarMenuButton>
117
117
  </SidebarMenuItem>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stevederico/skateboard-ui",
3
3
  "private": false,
4
- "version": "2.13.0",
4
+ "version": "2.15.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  "./Sidebar": {
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
3
3
  import { useTheme } from 'next-themes';
4
4
  import { getState } from "../core/Context.jsx";
5
5
  import DynamicIcon from '../core/DynamicIcon.jsx';
6
- import { IconSun, IconMoon, IconCheck } from '@tabler/icons-react';
6
+ import { Sun, Moon, Check } from 'lucide-react';
7
7
  import { Button } from '../shadcn/ui/button.jsx';
8
8
  import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../shadcn/ui/card.jsx';
9
9
  import { Badge } from '../shadcn/ui/badge.jsx';
@@ -86,7 +86,7 @@ export default function LandingView() {
86
86
 
87
87
  <div className="flex gap-3 items-center">
88
88
  <Button variant="outline" size="icon" onClick={() => setTheme(isDarkMode ? 'light' : 'dark')} aria-label="Toggle dark mode">
89
- {isDarkMode ? <IconSun size={18} /> : <IconMoon size={18} />}
89
+ {isDarkMode ? <Sun size={18} /> : <Moon size={18} />}
90
90
  </Button>
91
91
  <Button variant="default" onClick={() => navigate('/app')}>
92
92
  {constants.cta}
@@ -146,13 +146,13 @@ export default function LandingView() {
146
146
  <ul className="text-left space-y-4 mb-8">
147
147
  {(constants.stripeProducts[0]?.features || []).map((feature, index) => (
148
148
  <li key={index} className="flex items-center gap-2">
149
- <IconCheck size={16} className="text-primary shrink-0" />
149
+ <Check size={16} className="text-primary shrink-0" />
150
150
  {feature}
151
151
  </li>
152
152
  ))}
153
153
  {(constants.pricing?.extras || []).map((extra, index) => (
154
154
  <li key={`extra-${index}`} className="flex items-center gap-2">
155
- <IconCheck size={16} className="text-primary shrink-0" />
155
+ <Check size={16} className="text-primary shrink-0" />
156
156
  {extra}
157
157
  </li>
158
158
  ))}
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { useNavigate } from 'react-router-dom';
3
3
  import { getState } from '../core/Context.jsx';
4
4
  import { useTheme } from 'next-themes';
5
- import { IconSun, IconMoon } from '@tabler/icons-react';
5
+ import { Sun, Moon } from 'lucide-react';
6
6
  import Header from '../layout/Header.jsx';
7
7
  import { Avatar, AvatarFallback } from '../shadcn/ui/avatar.jsx';
8
8
  import { Badge } from '../shadcn/ui/badge.jsx';
@@ -62,7 +62,7 @@ export default function SettingsView() {
62
62
  <div className="flex-1">
63
63
  <Header title="Settings">
64
64
  <Button variant="ghost" size="icon" onClick={() => setTheme(isDarkMode ? 'light' : 'dark')} aria-label="Toggle dark mode">
65
- {isDarkMode ? <IconSun size={20} /> : <IconMoon size={20} />}
65
+ {isDarkMode ? <Sun size={20} /> : <Moon size={20} />}
66
66
  </Button>
67
67
  </Header>
68
68
 
@@ -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