@stevederico/skateboard-ui 2.10.0 → 2.11.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 +12 -0
- package/core/DynamicIcon.jsx +98 -31
- package/fonts/GeistMonoVF.woff2 +0 -0
- package/fonts/GeistVF.woff2 +0 -0
- package/package.json +1 -1
- package/styles.css +16 -0
- package/views/LandingView.jsx +29 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
2.11.0
|
|
4
|
+
|
|
5
|
+
Fix DynamicIcon lazy loading
|
|
6
|
+
Remove barrel import lucide-react
|
|
7
|
+
Hoist LandingView static values
|
|
8
|
+
Add FeatureIcon emoji fallback
|
|
9
|
+
|
|
10
|
+
2.10.1
|
|
11
|
+
|
|
12
|
+
Add Geist font files self-hosted
|
|
13
|
+
Add @font-face declarations
|
|
14
|
+
|
|
3
15
|
2.10.0
|
|
4
16
|
|
|
5
17
|
Fix landing page feature icons
|
package/core/DynamicIcon.jsx
CHANGED
|
@@ -1,28 +1,11 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import * as LucideIcons from "lucide-react";
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
3
2
|
import { cn } from "../shadcn/lib/utils.js";
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
5
|
+
* Convert a kebab-case, snake_case, or space-separated string to PascalCase.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* via shadcn cn() utility.
|
|
11
|
-
*
|
|
12
|
-
* @param {Object} props
|
|
13
|
-
* @param {string} props.name - Icon name (e.g. "home", "arrow-right", "Settings")
|
|
14
|
-
* @param {number} [props.size=24] - Icon size in pixels
|
|
15
|
-
* @param {string} [props.color='currentColor'] - Icon stroke color
|
|
16
|
-
* @param {number} [props.strokeWidth=2] - Stroke width
|
|
17
|
-
* @param {string} [props.className] - Additional CSS classes
|
|
18
|
-
* @returns {JSX.Element|null} Rendered icon or null if name not found
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* import DynamicIcon from '@stevederico/skateboard-ui/DynamicIcon';
|
|
22
|
-
*
|
|
23
|
-
* <DynamicIcon name="home" size={24} />
|
|
24
|
-
* <DynamicIcon name="arrow-right" size={20} color="red" />
|
|
25
|
-
* <DynamicIcon name="settings" className="text-muted-foreground" />
|
|
7
|
+
* @param {string} str - Input string
|
|
8
|
+
* @returns {string} PascalCase version
|
|
26
9
|
*/
|
|
27
10
|
function toPascalCase(str) {
|
|
28
11
|
return str
|
|
@@ -31,28 +14,112 @@ function toPascalCase(str) {
|
|
|
31
14
|
.join("");
|
|
32
15
|
}
|
|
33
16
|
|
|
34
|
-
|
|
35
|
-
|
|
17
|
+
let iconsModule = null;
|
|
18
|
+
let iconsPromise = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Lazily load the lucide-react module on first use.
|
|
22
|
+
* Subsequent calls return the cached module immediately.
|
|
23
|
+
*
|
|
24
|
+
* @returns {Promise<Object>} The lucide-react module
|
|
25
|
+
*/
|
|
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;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return iconsPromise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve icon name to a Lucide component from the cached module.
|
|
39
|
+
*
|
|
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
|
|
43
|
+
*/
|
|
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
|
+
];
|
|
36
51
|
for (const candidate of candidates) {
|
|
37
|
-
if (
|
|
52
|
+
if (mod[candidate]) return mod[candidate];
|
|
38
53
|
}
|
|
39
54
|
return null;
|
|
40
55
|
}
|
|
41
56
|
|
|
42
|
-
const DynamicIcon = ({ name, size = 24, color = "currentColor", strokeWidth = 2, className, ...props }) => {
|
|
43
|
-
const Icon = resolveIcon(name);
|
|
44
|
-
if (!Icon) return null;
|
|
45
|
-
return <Icon size={size} color={color} strokeWidth={strokeWidth} className={cn(className)} {...props} />;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
57
|
/**
|
|
49
58
|
* Check if a name string resolves to a valid Lucide icon.
|
|
59
|
+
* Returns false until the icon module has been loaded.
|
|
50
60
|
*
|
|
51
61
|
* @param {string} name - Icon name to check
|
|
52
62
|
* @returns {boolean} True if name resolves to an icon
|
|
53
63
|
*/
|
|
54
64
|
export function canResolveIcon(name) {
|
|
55
|
-
|
|
65
|
+
if (!iconsModule) return false;
|
|
66
|
+
return resolveIcon(iconsModule, name) !== null;
|
|
56
67
|
}
|
|
57
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Render a Lucide icon by name string with lazy loading.
|
|
71
|
+
*
|
|
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).
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} props
|
|
77
|
+
* @param {string} props.name - Icon name (e.g. "home", "arrow-right", "Settings")
|
|
78
|
+
* @param {number} [props.size=24] - Icon size in pixels
|
|
79
|
+
* @param {string} [props.color='currentColor'] - Icon stroke color
|
|
80
|
+
* @param {number} [props.strokeWidth=2] - Stroke width
|
|
81
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
82
|
+
* @returns {JSX.Element|null} Rendered icon or null if loading/not found
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* import DynamicIcon from '@stevederico/skateboard-ui/DynamicIcon';
|
|
86
|
+
*
|
|
87
|
+
* <DynamicIcon name="home" size={24} />
|
|
88
|
+
* <DynamicIcon name="arrow-right" size={20} color="red" />
|
|
89
|
+
* <DynamicIcon name="settings" className="text-muted-foreground" />
|
|
90
|
+
*/
|
|
91
|
+
const DynamicIcon = ({
|
|
92
|
+
name,
|
|
93
|
+
size = 24,
|
|
94
|
+
color = "currentColor",
|
|
95
|
+
strokeWidth = 2,
|
|
96
|
+
className,
|
|
97
|
+
...props
|
|
98
|
+
}) => {
|
|
99
|
+
const [Icon, setIcon] = useState(() => {
|
|
100
|
+
if (iconsModule) return resolveIcon(iconsModule, name);
|
|
101
|
+
return null;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (Icon) return;
|
|
106
|
+
loadIcons().then((mod) => {
|
|
107
|
+
const resolved = resolveIcon(mod, name);
|
|
108
|
+
setIcon(() => resolved);
|
|
109
|
+
});
|
|
110
|
+
}, [name, Icon]);
|
|
111
|
+
|
|
112
|
+
if (!Icon) return null;
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<Icon
|
|
116
|
+
size={size}
|
|
117
|
+
color={color}
|
|
118
|
+
strokeWidth={strokeWidth}
|
|
119
|
+
className={cn(className)}
|
|
120
|
+
{...props}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
58
125
|
export default DynamicIcon;
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/styles.css
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
@font-face {
|
|
2
|
+
font-family: 'Geist';
|
|
3
|
+
src: url('./fonts/GeistVF.woff2') format('woff2');
|
|
4
|
+
font-weight: 100 900;
|
|
5
|
+
font-style: normal;
|
|
6
|
+
font-display: swap;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@font-face {
|
|
10
|
+
font-family: 'Geist Mono';
|
|
11
|
+
src: url('./fonts/GeistMonoVF.woff2') format('woff2');
|
|
12
|
+
font-weight: 100 900;
|
|
13
|
+
font-style: normal;
|
|
14
|
+
font-display: swap;
|
|
15
|
+
}
|
|
16
|
+
|
|
1
17
|
@import "tailwindcss";
|
|
2
18
|
|
|
3
19
|
@source '../';
|
package/views/LandingView.jsx
CHANGED
|
@@ -2,23 +2,42 @@ import React from 'react';
|
|
|
2
2
|
import { useNavigate } from 'react-router-dom';
|
|
3
3
|
import { useTheme } from 'next-themes';
|
|
4
4
|
import { getState } from "../core/Context.jsx";
|
|
5
|
-
import DynamicIcon
|
|
5
|
+
import DynamicIcon from '../core/DynamicIcon.jsx';
|
|
6
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';
|
|
10
10
|
import { Separator } from '../shadcn/ui/separator.jsx';
|
|
11
11
|
|
|
12
|
+
const CURRENT_YEAR = new Date().getFullYear();
|
|
13
|
+
|
|
14
|
+
const DEFAULT_NAV_LINKS = [
|
|
15
|
+
{ label: 'Features', href: '#features' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const DEFAULT_NAV_LINKS_SUFFIX = [
|
|
19
|
+
{ label: 'Terms', href: '/terms' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const PRICING_LINK = { label: 'Pricing', href: '#pricing' };
|
|
23
|
+
|
|
24
|
+
const PRIVACY_LINK = { label: 'Privacy', href: '/privacy' };
|
|
25
|
+
const TERMS_LINK = { label: 'Terms', href: '/terms' };
|
|
26
|
+
const EULA_LINK = { label: 'EULA', href: '/eula' };
|
|
27
|
+
|
|
12
28
|
/**
|
|
13
29
|
* Renders a feature icon from a name string or emoji.
|
|
14
|
-
*
|
|
30
|
+
* If the name looks like an icon identifier (ASCII letters, digits, hyphens),
|
|
31
|
+
* renders it via DynamicIcon with lazy loading. Otherwise treats it as
|
|
32
|
+
* raw text (e.g. an emoji).
|
|
15
33
|
*
|
|
16
34
|
* @param {Object} props
|
|
17
35
|
* @param {string} props.name - Icon name (kebab-case) or emoji string
|
|
18
36
|
* @returns {JSX.Element} Icon component or text span
|
|
19
37
|
*/
|
|
20
38
|
function FeatureIcon({ name }) {
|
|
21
|
-
|
|
39
|
+
const isIconName = /^[a-z][a-z0-9-]*$/i.test(name);
|
|
40
|
+
if (isIconName) return <DynamicIcon name={name} size={24} />;
|
|
22
41
|
return <span>{name}</span>;
|
|
23
42
|
}
|
|
24
43
|
|
|
@@ -57,9 +76,9 @@ export default function LandingView() {
|
|
|
57
76
|
|
|
58
77
|
<div className="hidden md:flex gap-6">
|
|
59
78
|
{(constants.navLinks || [
|
|
60
|
-
|
|
61
|
-
...(constants.stripeProducts?.length > 0 ? [
|
|
62
|
-
|
|
79
|
+
...DEFAULT_NAV_LINKS,
|
|
80
|
+
...(constants.stripeProducts?.length > 0 ? [PRICING_LINK] : []),
|
|
81
|
+
...DEFAULT_NAV_LINKS_SUFFIX,
|
|
63
82
|
]).map((link, index) => (
|
|
64
83
|
<a key={index} href={link.href} className="text-muted-foreground hover:text-foreground transition-colors font-semibold">{link.label}</a>
|
|
65
84
|
))}
|
|
@@ -171,14 +190,14 @@ export default function LandingView() {
|
|
|
171
190
|
<Separator className="mb-8" />
|
|
172
191
|
<div className="flex justify-center gap-8 mb-6">
|
|
173
192
|
{(constants.footerLinks || [
|
|
174
|
-
...(constants.privacyPolicy ? [
|
|
175
|
-
...(constants.termsOfService ? [
|
|
176
|
-
...(constants.EULA ? [
|
|
193
|
+
...(constants.privacyPolicy ? [PRIVACY_LINK] : []),
|
|
194
|
+
...(constants.termsOfService ? [TERMS_LINK] : []),
|
|
195
|
+
...(constants.EULA ? [EULA_LINK] : []),
|
|
177
196
|
]).map((link, index) => (
|
|
178
197
|
<a key={index} href={link.href} className="text-muted-foreground hover:text-foreground transition-colors font-semibold">{link.label}</a>
|
|
179
198
|
))}
|
|
180
199
|
</div>
|
|
181
|
-
<p className="text-center text-muted-foreground">© {
|
|
200
|
+
<p className="text-center text-muted-foreground">© {CURRENT_YEAR} {constants.companyName}. {constants.copyrightText || 'All rights reserved.'}</p>
|
|
182
201
|
</div>
|
|
183
202
|
</footer>
|
|
184
203
|
</div>
|