@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 +14 -0
- package/components/ThemeToggle.jsx +3 -3
- package/components/UpgradeSheet.jsx +3 -3
- package/core/DynamicIcon.jsx +54 -46
- package/layout/Layout.jsx +4 -1
- package/layout/Sidebar.jsx +2 -2
- package/package.json +1 -1
- package/shadcn/ui/button.jsx +1 -1
- package/styles.css +10 -0
- package/views/LandingView.jsx +4 -4
- package/views/SettingsView.jsx +2 -2
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 {
|
|
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
|
-
? <
|
|
49
|
-
: <
|
|
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 {
|
|
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
|
-
<
|
|
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
|
-
<
|
|
118
|
+
<IconSparkles className="size-4" />
|
|
119
119
|
Upgrade to {product.title}
|
|
120
120
|
</Button>
|
|
121
121
|
</DrawerFooter>
|
package/core/DynamicIcon.jsx
CHANGED
|
@@ -14,64 +14,74 @@ function toPascalCase(str) {
|
|
|
14
14
|
.join("");
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
*
|
|
22
|
-
*
|
|
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
|
-
* @
|
|
28
|
+
* @param {string} iconName - PascalCase icon name with "Icon" prefix
|
|
29
|
+
* @returns {Promise<React.ComponentType|null>} Icon component or null
|
|
25
30
|
*/
|
|
26
|
-
function
|
|
27
|
-
if (
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
47
|
+
|
|
48
|
+
importCache.set(iconName, promise);
|
|
49
|
+
return promise;
|
|
35
50
|
}
|
|
36
51
|
|
|
37
52
|
/**
|
|
38
|
-
* Resolve icon name to a
|
|
53
|
+
* Resolve a kebab-case icon name to a Tabler PascalCase module name.
|
|
54
|
+
* e.g. "layout-dashboard" → "IconLayoutDashboard"
|
|
39
55
|
*
|
|
40
|
-
* @param {
|
|
41
|
-
* @
|
|
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
|
|
45
|
-
if (
|
|
46
|
-
|
|
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
|
|
59
|
-
* Returns false
|
|
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
|
|
69
|
+
* @returns {boolean} True if icon is cached and valid
|
|
63
70
|
*/
|
|
64
71
|
export function canResolveIcon(name) {
|
|
65
|
-
|
|
66
|
-
return
|
|
72
|
+
const tablerName = toTablerName(name);
|
|
73
|
+
return iconCache.has(tablerName) && iconCache.get(tablerName) !== null;
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
/**
|
|
70
|
-
* Render a
|
|
77
|
+
* Render a Tabler icon by name string with per-icon lazy loading.
|
|
71
78
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
setIcon(() => resolved);
|
|
115
|
+
loadIcon(tablerName).then((resolved) => {
|
|
116
|
+
if (resolved) setIcon(() => resolved);
|
|
109
117
|
});
|
|
110
|
-
}, [
|
|
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={
|
|
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>
|
package/layout/Sidebar.jsx
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
SidebarRail,
|
|
16
16
|
useSidebar,
|
|
17
17
|
} from "../shadcn/ui/sidebar";
|
|
18
|
-
import {
|
|
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
|
-
<
|
|
114
|
+
<IconSettings size={20} strokeWidth={2} />
|
|
115
115
|
<span>Settings</span>
|
|
116
116
|
</SidebarMenuButton>
|
|
117
117
|
</SidebarMenuItem>
|
package/package.json
CHANGED
package/shadcn/ui/button.jsx
CHANGED
|
@@ -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 {
|
package/views/LandingView.jsx
CHANGED
|
@@ -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 {
|
|
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 ? <
|
|
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
|
-
<
|
|
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
|
-
<
|
|
155
|
+
<IconCheck size={16} className="text-primary shrink-0" />
|
|
156
156
|
{extra}
|
|
157
157
|
</li>
|
|
158
158
|
))}
|
package/views/SettingsView.jsx
CHANGED
|
@@ -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 {
|
|
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 ? <
|
|
65
|
+
{isDarkMode ? <IconSun size={20} /> : <IconMoon size={20} />}
|
|
66
66
|
</Button>
|
|
67
67
|
</Header>
|
|
68
68
|
|