@stevederico/skateboard-ui 2.13.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 +6 -0
- package/components/ThemeToggle.jsx +3 -3
- package/components/UpgradeSheet.jsx +3 -3
- package/core/DynamicIcon.jsx +50 -28
- package/layout/Sidebar.jsx +2 -2
- package/package.json +1 -1
- package/views/LandingView.jsx +4 -4
- package/views/SettingsView.jsx +2 -2
- package/.claude/settings.local.json +0 -16
- package/.playwright-mcp/dark-mode-borders.png +0 -0
- package/.playwright-mcp/dark-mode-inputs.png +0 -0
- package/.playwright-mcp/dark-sidebar.png +0 -0
- package/.playwright-mcp/features-centered-v2.png +0 -0
- package/.playwright-mcp/features-centered-v3.png +0 -0
- package/.playwright-mcp/features-centered.png +0 -0
- package/.playwright-mcp/features-check.png +0 -0
- package/.playwright-mcp/landing-dark.png +0 -0
- package/.playwright-mcp/light-mode-borders.png +0 -0
- package/.playwright-mcp/sidebar-dark-mode.png +0 -0
- package/.playwright-mcp/sidebar-light-mode.png +0 -0
- package/.playwright-mcp/signin-dark.png +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useTheme } from 'next-themes';
|
|
3
|
-
import {
|
|
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
|
-
? <
|
|
49
|
-
: <
|
|
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 {
|
|
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
|
-
<
|
|
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
|
-
<
|
|
118
|
+
<Sparkles className="size-4" />
|
|
119
119
|
Upgrade to {product.title}
|
|
120
120
|
</Button>
|
|
121
121
|
</DrawerFooter>
|
package/core/DynamicIcon.jsx
CHANGED
|
@@ -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
|
|
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
|
|
39
|
+
* icon library. Results are cached for instant subsequent lookups.
|
|
27
40
|
*
|
|
28
|
-
* @param {string}
|
|
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(
|
|
32
|
-
if (iconCache.has(
|
|
33
|
-
if (importCache.has(
|
|
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(
|
|
49
|
+
const promise = import(`/node_modules/lucide-react/dist/esm/icons/${kebabName}.js`)
|
|
36
50
|
.then((mod) => {
|
|
37
|
-
const Icon = mod.default ||
|
|
38
|
-
iconCache.set(
|
|
39
|
-
importCache.delete(
|
|
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(
|
|
44
|
-
importCache.delete(
|
|
57
|
+
iconCache.set(cacheKey, null);
|
|
58
|
+
importCache.delete(cacheKey);
|
|
45
59
|
return null;
|
|
46
60
|
});
|
|
47
61
|
|
|
48
|
-
importCache.set(
|
|
62
|
+
importCache.set(cacheKey, promise);
|
|
49
63
|
return promise;
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
/**
|
|
53
|
-
* Resolve
|
|
54
|
-
* e.g. "layout-dashboard" → "
|
|
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}
|
|
74
|
+
* @returns {{ pascal: string, kebab: string }} Resolved icon name pair
|
|
58
75
|
*/
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
73
|
-
return iconCache.has(
|
|
94
|
+
const { pascal } = toIconName(name);
|
|
95
|
+
return iconCache.has(pascal) && iconCache.get(pascal) !== null;
|
|
74
96
|
}
|
|
75
97
|
|
|
76
98
|
/**
|
|
77
|
-
* Render a
|
|
99
|
+
* Render a Lucide icon by name string with per-icon lazy loading.
|
|
78
100
|
*
|
|
79
|
-
* Each icon is imported individually from
|
|
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
|
|
131
|
+
const { pascal, kebab } = toIconName(name);
|
|
110
132
|
|
|
111
|
-
const [Icon, setIcon] = useState(() => iconCache.get(
|
|
133
|
+
const [Icon, setIcon] = useState(() => iconCache.get(pascal) || null);
|
|
112
134
|
|
|
113
135
|
useEffect(() => {
|
|
114
136
|
if (Icon) return;
|
|
115
|
-
loadIcon(
|
|
137
|
+
loadIcon(kebab, pascal).then((resolved) => {
|
|
116
138
|
if (resolved) setIcon(() => resolved);
|
|
117
139
|
});
|
|
118
|
-
}, [
|
|
140
|
+
}, [pascal, kebab, Icon]);
|
|
119
141
|
|
|
120
142
|
if (!Icon) return null;
|
|
121
143
|
|
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 { 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
|
-
<
|
|
114
|
+
<Settings size={20} strokeWidth={2} />
|
|
115
115
|
<span>Settings</span>
|
|
116
116
|
</SidebarMenuButton>
|
|
117
117
|
</SidebarMenuItem>
|
package/package.json
CHANGED
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 { 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 ? <
|
|
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
|
-
<
|
|
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
|
-
<
|
|
155
|
+
<Check 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 { 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 ? <
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|