@stevederico/skateboard-ui 2.11.0 → 2.13.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,19 @@
1
1
  # CHANGELOG
2
2
 
3
+ 2.13.0
4
+
5
+ Fix DynamicIcon per-icon imports
6
+ Remove lucide-react from views
7
+ Switch views to Tabler icons
8
+
9
+ 2.12.0
10
+
11
+ Add skip-to-content link
12
+ Add selection styling brand color
13
+ Add content-auto utility class
14
+ Add button active press feedback
15
+ Add main landmark id
16
+
3
17
  2.11.0
4
18
 
5
19
  Fix DynamicIcon lazy loading
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { useTheme } from 'next-themes';
3
- import { Sun, Moon } from 'lucide-react';
3
+ import { IconSun, IconMoon } from '@tabler/icons-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
- ? <Sun size={iconSize} />
49
- : <Moon size={iconSize} />
48
+ ? <IconSun size={iconSize} />
49
+ : <IconMoon 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 { Sparkles, CircleCheck } from 'lucide-react';
19
+ import { IconSparkles, IconCircleCheck } from '@tabler/icons-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
- <CircleCheck className="size-5 text-primary shrink-0" />
107
+ <IconCircleCheck 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
- <Sparkles className="size-4" />
118
+ <IconSparkles className="size-4" />
119
119
  Upgrade to {product.title}
120
120
  </Button>
121
121
  </DrawerFooter>
@@ -14,64 +14,74 @@ function toPascalCase(str) {
14
14
  .join("");
15
15
  }
16
16
 
17
- let iconsModule = null;
18
- let iconsPromise = null;
17
+ /** Cache of resolved icon components keyed by name */
18
+ const iconCache = new Map();
19
+
20
+ /** Cache of in-flight import promises keyed by module name */
21
+ const importCache = new Map();
19
22
 
20
23
  /**
21
- * Lazily load the lucide-react module on first use.
22
- * Subsequent calls return the cached module immediately.
24
+ * Load a single Tabler icon by its PascalCase name (e.g. "IconLock").
25
+ * Each icon is imported individually (~1KB) instead of loading the entire
26
+ * icon library (~400KB). Results are cached for instant subsequent lookups.
23
27
  *
24
- * @returns {Promise<Object>} The lucide-react module
28
+ * @param {string} iconName - PascalCase icon name with "Icon" prefix
29
+ * @returns {Promise<React.ComponentType|null>} Icon component or null
25
30
  */
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;
31
+ function loadIcon(iconName) {
32
+ if (iconCache.has(iconName)) return Promise.resolve(iconCache.get(iconName));
33
+ if (importCache.has(iconName)) return importCache.get(iconName);
34
+
35
+ const promise = import(`@tabler/icons-react/dist/esm/icons/${iconName}.mjs`)
36
+ .then((mod) => {
37
+ const Icon = mod.default || mod[iconName] || null;
38
+ iconCache.set(iconName, Icon);
39
+ importCache.delete(iconName);
40
+ return Icon;
41
+ })
42
+ .catch(() => {
43
+ iconCache.set(iconName, null);
44
+ importCache.delete(iconName);
45
+ return null;
32
46
  });
33
- }
34
- return iconsPromise;
47
+
48
+ importCache.set(iconName, promise);
49
+ return promise;
35
50
  }
36
51
 
37
52
  /**
38
- * Resolve icon name to a Lucide component from the cached module.
53
+ * Resolve a kebab-case icon name to a Tabler PascalCase module name.
54
+ * e.g. "layout-dashboard" → "IconLayoutDashboard"
39
55
  *
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
56
+ * @param {string} name - Icon name in any case format
57
+ * @returns {string} Tabler PascalCase name with "Icon" prefix
43
58
  */
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];
53
- }
54
- return null;
59
+ function toTablerName(name) {
60
+ if (name.startsWith("Icon")) return name;
61
+ return "Icon" + toPascalCase(name);
55
62
  }
56
63
 
57
64
  /**
58
- * Check if a name string resolves to a valid Lucide icon.
59
- * Returns false until the icon module has been loaded.
65
+ * Check if a name string has been resolved to a valid icon.
66
+ * Returns false for unloaded or invalid icons.
60
67
  *
61
68
  * @param {string} name - Icon name to check
62
- * @returns {boolean} True if name resolves to an icon
69
+ * @returns {boolean} True if icon is cached and valid
63
70
  */
64
71
  export function canResolveIcon(name) {
65
- if (!iconsModule) return false;
66
- return resolveIcon(iconsModule, name) !== null;
72
+ const tablerName = toTablerName(name);
73
+ return iconCache.has(tablerName) && iconCache.get(tablerName) !== null;
67
74
  }
68
75
 
69
76
  /**
70
- * Render a Lucide icon by name string with lazy loading.
77
+ * Render a Tabler icon by name string with per-icon lazy loading.
71
78
  *
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).
79
+ * Each icon is imported individually from @tabler/icons-react (~1KB per icon)
80
+ * instead of loading the entire library. Resolved icons are cached in memory
81
+ * for instant rendering on subsequent uses.
82
+ *
83
+ * Accepts kebab-case ("layout-dashboard"), PascalCase ("LayoutDashboard"),
84
+ * or prefixed ("IconLayoutDashboard") names.
75
85
  *
76
86
  * @param {Object} props
77
87
  * @param {string} props.name - Icon name (e.g. "home", "arrow-right", "Settings")
@@ -96,18 +106,16 @@ const DynamicIcon = ({
96
106
  className,
97
107
  ...props
98
108
  }) => {
99
- const [Icon, setIcon] = useState(() => {
100
- if (iconsModule) return resolveIcon(iconsModule, name);
101
- return null;
102
- });
109
+ const tablerName = toTablerName(name);
110
+
111
+ const [Icon, setIcon] = useState(() => iconCache.get(tablerName) || null);
103
112
 
104
113
  useEffect(() => {
105
114
  if (Icon) return;
106
- loadIcons().then((mod) => {
107
- const resolved = resolveIcon(mod, name);
108
- setIcon(() => resolved);
115
+ loadIcon(tablerName).then((resolved) => {
116
+ if (resolved) setIcon(() => resolved);
109
117
  });
110
- }, [name, Icon]);
118
+ }, [tablerName, Icon]);
111
119
 
112
120
  if (!Icon) return null;
113
121
 
@@ -116,7 +124,7 @@ const DynamicIcon = ({
116
124
  size={size}
117
125
  color={color}
118
126
  strokeWidth={strokeWidth}
119
- className={cn(className)}
127
+ className={className}
120
128
  {...props}
121
129
  />
122
130
  );
package/layout/Layout.jsx CHANGED
@@ -40,6 +40,9 @@ export default function Layout({ children }) {
40
40
 
41
41
  return (
42
42
  <div className="min-h-screen flex flex-col pt-[env(safe-area-inset-top)] pb-[calc(5rem+env(safe-area-inset-bottom))] md:pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]">
43
+ <a href="#main" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-background focus:text-foreground focus:rounded-md focus:ring-2 focus:ring-ring">
44
+ Skip to content
45
+ </a>
43
46
  <SidebarProvider
44
47
  defaultOpen={!constants.sidebarCollapsed}
45
48
  style={{
@@ -47,7 +50,7 @@ export default function Layout({ children }) {
47
50
  '--header-height': '3.5rem',
48
51
  }}>
49
52
  {showSidebar && <Sidebar variant="inset" />}
50
- <SidebarInset className={`border border-border/50 ${constants.hideSidebarInsetRounding ? "md:peer-data-[variant=inset]:rounded-none" : ""}`}>
53
+ <SidebarInset id="main" className={`border border-border/50 ${constants.hideSidebarInsetRounding ? "md:peer-data-[variant=inset]:rounded-none" : ""}`}>
51
54
  <Outlet />
52
55
  </SidebarInset>
53
56
  </SidebarProvider>
@@ -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 { IconSettings } from '@tabler/icons-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
- <Settings size={20} strokeWidth={2} />
114
+ <IconSettings 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.11.0",
4
+ "version": "2.13.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  "./Sidebar": {
@@ -4,7 +4,7 @@ import { cva } from "class-variance-authority";
4
4
  import { cn } from "../lib/utils.js"
5
5
 
6
6
  const buttonVariants = cva(
7
- "cursor-pointer 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:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
7
+ "cursor-pointer 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:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none active:scale-[0.98]",
8
8
  {
9
9
  variants: {
10
10
  variant: {
package/styles.css CHANGED
@@ -209,6 +209,16 @@
209
209
  code, pre, kbd {
210
210
  font-family: 'Geist Mono', ui-monospace, monospace;
211
211
  }
212
+ ::selection {
213
+ background-color: color-mix(in oklch, var(--color-app) 20%, transparent);
214
+ }
215
+ }
216
+
217
+ @layer utilities {
218
+ .content-auto {
219
+ content-visibility: auto;
220
+ contain-intrinsic-size: auto 200px;
221
+ }
212
222
  }
213
223
 
214
224
  #root {
@@ -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 { Sun, Moon, Check } from 'lucide-react';
6
+ import { IconSun, IconMoon, IconCheck } from '@tabler/icons-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 ? <Sun size={18} /> : <Moon size={18} />}
89
+ {isDarkMode ? <IconSun size={18} /> : <IconMoon 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
- <Check size={16} className="text-primary shrink-0" />
149
+ <IconCheck 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
- <Check size={16} className="text-primary shrink-0" />
155
+ <IconCheck 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 { Sun, Moon } from 'lucide-react';
5
+ import { IconSun, IconMoon } from '@tabler/icons-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 ? <Sun size={20} /> : <Moon size={20} />}
65
+ {isDarkMode ? <IconSun size={20} /> : <IconMoon size={20} />}
66
66
  </Button>
67
67
  </Header>
68
68