@nimblegiant/stilts 0.2.0-alpha.3 → 0.2.0-alpha.4
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/README.md +734 -107
- package/dist/index.cjs +125 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -5
- package/dist/index.d.ts +56 -5
- package/dist/index.js +124 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/layout/MainNav.tsx +39 -18
- package/src/components/patterns/ContentPage.tsx +15 -6
- package/src/components/patterns/ListCard.tsx +134 -0
- package/src/components/patterns/index.ts +26 -20
- package/src/components/ui/AuthorMeta.tsx +44 -0
- package/src/components/ui/index.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nimblegiant/stilts",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.4",
|
|
4
4
|
"description": "Nimble Giant's whimsical yet disciplined design system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -82,4 +82,4 @@
|
|
|
82
82
|
"access": "public",
|
|
83
83
|
"registry": "https://registry.npmjs.org"
|
|
84
84
|
}
|
|
85
|
-
}
|
|
85
|
+
}
|
|
@@ -4,13 +4,22 @@ import { Sun, Moon, Menu, X, ArrowRight } from "lucide-react";
|
|
|
4
4
|
interface MainNavProps {
|
|
5
5
|
currentPath: string;
|
|
6
6
|
isPortfolioPage?: boolean;
|
|
7
|
+
navLinks?: { href: string; label: string }[];
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export default function MainNav({
|
|
10
|
+
export default function MainNav({
|
|
11
|
+
currentPath,
|
|
12
|
+
isPortfolioPage = false,
|
|
13
|
+
navLinks = [],
|
|
14
|
+
}: MainNavProps) {
|
|
10
15
|
const [theme, setTheme] = useState<"light" | "dark">(() => {
|
|
11
16
|
// Initialize theme from document during first render (client-side only)
|
|
12
17
|
if (typeof document !== "undefined") {
|
|
13
|
-
return (
|
|
18
|
+
return (
|
|
19
|
+
(document.documentElement.getAttribute("data-theme") as
|
|
20
|
+
| "light"
|
|
21
|
+
| "dark") || "light"
|
|
22
|
+
);
|
|
14
23
|
}
|
|
15
24
|
return "light";
|
|
16
25
|
});
|
|
@@ -64,14 +73,6 @@ export default function MainNav({ currentPath, isPortfolioPage = false }: MainNa
|
|
|
64
73
|
localStorage.setItem("theme", newTheme);
|
|
65
74
|
}, [theme]);
|
|
66
75
|
|
|
67
|
-
const navLinks = [
|
|
68
|
-
{ href: "/", label: "HOME" },
|
|
69
|
-
{ href: "/about", label: "ABOUT" },
|
|
70
|
-
{ href: "/services", label: "SERVICES" },
|
|
71
|
-
{ href: "/products", label: "PRODUCTS" },
|
|
72
|
-
{ href: "/contact", label: "CONTACT" },
|
|
73
|
-
];
|
|
74
|
-
|
|
75
76
|
const isActive = (href: string) => {
|
|
76
77
|
if (href === "/") return currentPath === "/";
|
|
77
78
|
return currentPath.startsWith(href);
|
|
@@ -91,7 +92,9 @@ export default function MainNav({ currentPath, isPortfolioPage = false }: MainNa
|
|
|
91
92
|
role="navigation"
|
|
92
93
|
aria-label="Main navigation"
|
|
93
94
|
className={`fixed top-0 right-0 left-0 z-[var(--z-nav)] border-b border-[var(--color-border)]/0 transition-all duration-300 ease-out ${
|
|
94
|
-
isScrolled
|
|
95
|
+
isScrolled
|
|
96
|
+
? "bg-[var(--color-surface-overlay)]/0 backdrop-blur-sm"
|
|
97
|
+
: "bg-transparent"
|
|
95
98
|
} `}
|
|
96
99
|
>
|
|
97
100
|
<div className="container-xl container flex h-20 items-center justify-between sm:p-6 lg:p-14">
|
|
@@ -103,7 +106,11 @@ export default function MainNav({ currentPath, isPortfolioPage = false }: MainNa
|
|
|
103
106
|
>
|
|
104
107
|
{/* Light mode logo (dark logo on light bg) */}
|
|
105
108
|
<img
|
|
106
|
-
src={
|
|
109
|
+
src={
|
|
110
|
+
showLightText
|
|
111
|
+
? "/images/logo-light.svg"
|
|
112
|
+
: "/images/logo-dark.svg"
|
|
113
|
+
}
|
|
107
114
|
alt=""
|
|
108
115
|
aria-hidden="true"
|
|
109
116
|
className="h-10 w-auto"
|
|
@@ -174,7 +181,9 @@ export default function MainNav({ currentPath, isPortfolioPage = false }: MainNa
|
|
|
174
181
|
? "text-white/70 hover:text-white"
|
|
175
182
|
: "text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)]"
|
|
176
183
|
} `}
|
|
177
|
-
aria-label={
|
|
184
|
+
aria-label={
|
|
185
|
+
isMenuOpen ? "Close navigation menu" : "Open navigation menu"
|
|
186
|
+
}
|
|
178
187
|
aria-expanded={isMenuOpen}
|
|
179
188
|
aria-controls="mobile-menu"
|
|
180
189
|
>
|
|
@@ -194,7 +203,9 @@ export default function MainNav({ currentPath, isPortfolioPage = false }: MainNa
|
|
|
194
203
|
aria-modal="true"
|
|
195
204
|
aria-label="Navigation menu"
|
|
196
205
|
className={`fixed inset-0 z-[calc(var(--z-nav)-1)] transition-all duration-300 ease-out md:hidden ${
|
|
197
|
-
isMenuOpen
|
|
206
|
+
isMenuOpen
|
|
207
|
+
? "pointer-events-auto opacity-100"
|
|
208
|
+
: "pointer-events-none opacity-0"
|
|
198
209
|
} `}
|
|
199
210
|
>
|
|
200
211
|
{/* Backdrop */}
|
|
@@ -216,9 +227,15 @@ export default function MainNav({ currentPath, isPortfolioPage = false }: MainNa
|
|
|
216
227
|
<li
|
|
217
228
|
key={link.href}
|
|
218
229
|
className={`transition-all duration-300 ${
|
|
219
|
-
isMenuOpen
|
|
230
|
+
isMenuOpen
|
|
231
|
+
? "translate-x-0 opacity-100"
|
|
232
|
+
: "translate-x-8 opacity-0"
|
|
220
233
|
} `}
|
|
221
|
-
style={{
|
|
234
|
+
style={{
|
|
235
|
+
transitionDelay: isMenuOpen
|
|
236
|
+
? `${index * 50 + 100}ms`
|
|
237
|
+
: "0ms",
|
|
238
|
+
}}
|
|
222
239
|
>
|
|
223
240
|
<a
|
|
224
241
|
href={link.href}
|
|
@@ -233,7 +250,9 @@ export default function MainNav({ currentPath, isPortfolioPage = false }: MainNa
|
|
|
233
250
|
{link.label}
|
|
234
251
|
<ArrowRight
|
|
235
252
|
className={`h-4 w-4 transition-all duration-200 ${
|
|
236
|
-
isActive(link.href)
|
|
253
|
+
isActive(link.href)
|
|
254
|
+
? "opacity-50"
|
|
255
|
+
: "opacity-0 group-hover:opacity-30"
|
|
237
256
|
} `}
|
|
238
257
|
aria-hidden="true"
|
|
239
258
|
/>
|
|
@@ -246,7 +265,9 @@ export default function MainNav({ currentPath, isPortfolioPage = false }: MainNa
|
|
|
246
265
|
{/* Footer section with theme toggle */}
|
|
247
266
|
<div
|
|
248
267
|
className={`border-t border-[var(--color-border)] pt-6 transition-all duration-300 ${
|
|
249
|
-
isMenuOpen
|
|
268
|
+
isMenuOpen
|
|
269
|
+
? "translate-y-0 opacity-100"
|
|
270
|
+
: "translate-y-4 opacity-0"
|
|
250
271
|
} `}
|
|
251
272
|
style={{ transitionDelay: isMenuOpen ? "300ms" : "0ms" }}
|
|
252
273
|
>
|
|
@@ -10,26 +10,35 @@ interface ContentPageProps {
|
|
|
10
10
|
children: React.ReactNode;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export default function ContentPage({
|
|
13
|
+
export default function ContentPage({
|
|
14
|
+
title,
|
|
15
|
+
subtitle,
|
|
16
|
+
lastUpdated,
|
|
17
|
+
children,
|
|
18
|
+
}: ContentPageProps) {
|
|
14
19
|
return (
|
|
15
20
|
<>
|
|
16
21
|
<section className="pt-32 pb-12">
|
|
17
|
-
<div className="container-
|
|
22
|
+
<div className="container-md container">
|
|
18
23
|
<h1 className="mb-4 text-4xl font-bold text-[var(--color-text-primary)] md:text-5xl">
|
|
19
24
|
{title}
|
|
20
25
|
</h1>
|
|
21
26
|
{subtitle && (
|
|
22
|
-
<h2 className="mb-2 text-xl text-[var(--color-text-secondary)]">
|
|
27
|
+
<h2 className="mb-2 text-xl text-[var(--color-text-secondary)]">
|
|
28
|
+
{subtitle}
|
|
29
|
+
</h2>
|
|
23
30
|
)}
|
|
24
31
|
{lastUpdated && (
|
|
25
|
-
<p className="text-[var(--color-text-muted)]">
|
|
32
|
+
<p className="text-[var(--color-text-muted)]">
|
|
33
|
+
Last updated: {lastUpdated}
|
|
34
|
+
</p>
|
|
26
35
|
)}
|
|
27
36
|
</div>
|
|
28
37
|
</section>
|
|
29
38
|
|
|
30
39
|
<section className="py-12">
|
|
31
|
-
<div className="container-
|
|
32
|
-
<div className="prose
|
|
40
|
+
<div className="container-md container">
|
|
41
|
+
<div className="prose max-w-full">{children}</div>
|
|
33
42
|
</div>
|
|
34
43
|
</section>
|
|
35
44
|
</>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { AuthorMeta, Author } from "../ui/AuthorMeta";
|
|
3
|
+
|
|
4
|
+
export interface ListCardProps {
|
|
5
|
+
href: string;
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
tagline?: string;
|
|
9
|
+
thumbnail?: string;
|
|
10
|
+
tags?: string[];
|
|
11
|
+
meta?: {
|
|
12
|
+
date?: string;
|
|
13
|
+
author?: Author;
|
|
14
|
+
};
|
|
15
|
+
imageAlt?: string;
|
|
16
|
+
animationDelay?: number;
|
|
17
|
+
specialImageComponent?: boolean;
|
|
18
|
+
imageBgClass?: string;
|
|
19
|
+
imageDark?: string;
|
|
20
|
+
imageLight?: string;
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
footerSlot?: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ListCard Component
|
|
27
|
+
* Displays a card with optional image, title, description, meta, and tags
|
|
28
|
+
* Used for products, blog posts, and other list items
|
|
29
|
+
*/
|
|
30
|
+
export function ListCard({
|
|
31
|
+
href,
|
|
32
|
+
title,
|
|
33
|
+
description,
|
|
34
|
+
tagline,
|
|
35
|
+
thumbnail,
|
|
36
|
+
tags,
|
|
37
|
+
meta,
|
|
38
|
+
imageAlt = "Card image",
|
|
39
|
+
animationDelay,
|
|
40
|
+
specialImageComponent = false,
|
|
41
|
+
imageBgClass = "bg-[var(--color-surface-muted)]",
|
|
42
|
+
children,
|
|
43
|
+
imageDark,
|
|
44
|
+
imageLight,
|
|
45
|
+
footerSlot,
|
|
46
|
+
}: ListCardProps) {
|
|
47
|
+
const animationStyle = animationDelay
|
|
48
|
+
? { animationDelay: `${animationDelay}s` }
|
|
49
|
+
: undefined;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<a
|
|
53
|
+
href={href}
|
|
54
|
+
className="hero-animate group flex flex-col overflow-hidden rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)] opacity-0 shadow-sm transition-all duration-300 hover:border-[var(--color-primary)] hover:shadow-lg"
|
|
55
|
+
style={animationStyle}
|
|
56
|
+
>
|
|
57
|
+
{/* Image/Thumbnail */}
|
|
58
|
+
{thumbnail && !specialImageComponent && (
|
|
59
|
+
<div
|
|
60
|
+
className={`relative aspect-video w-full overflow-hidden rounded-t-lg ${imageBgClass}`}
|
|
61
|
+
>
|
|
62
|
+
<img
|
|
63
|
+
src={thumbnail}
|
|
64
|
+
alt={imageAlt}
|
|
65
|
+
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
{/* Special image component slot */}
|
|
71
|
+
<div className="relative flex aspect-video w-full items-center justify-center overflow-hidden rounded-t-lg bg-gradient-to-br from-[var(--color-dot-magenta)] via-[var(--color-dot-purple)] to-[var(--color-dot-turquoise)] p-12">
|
|
72
|
+
<img
|
|
73
|
+
src={imageDark}
|
|
74
|
+
alt={title}
|
|
75
|
+
className="dark-mode-hidden h-full w-full object-contain transition-transform duration-300 group-hover:scale-105"
|
|
76
|
+
/>
|
|
77
|
+
<img
|
|
78
|
+
src={imageLight}
|
|
79
|
+
alt={title}
|
|
80
|
+
className="dark-mode-visible h-full w-full object-contain transition-transform duration-300 group-hover:scale-105"
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Content */}
|
|
85
|
+
<div className="flex flex-1 flex-col p-6">
|
|
86
|
+
{/* Meta (optional - for date, author) */}
|
|
87
|
+
{meta && (
|
|
88
|
+
<div className="mb-2 flex items-center gap-2 text-xs text-[var(--color-text-muted)]">
|
|
89
|
+
{meta.author && <AuthorMeta author={meta.author} />}
|
|
90
|
+
{meta.date && meta.author && <span>•</span>}
|
|
91
|
+
{meta.date && <span>{meta.date}</span>}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
{/* Title */}
|
|
96
|
+
<h3 className="font-heading mb-2 text-xl font-semibold text-[var(--color-text-primary)]">
|
|
97
|
+
{title}
|
|
98
|
+
</h3>
|
|
99
|
+
{tagline && (
|
|
100
|
+
<p className="mb-3 text-sm font-medium text-[var(--color-primary)]">
|
|
101
|
+
{tagline}
|
|
102
|
+
</p>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{/* Description */}
|
|
106
|
+
<p className="mb-4 flex-1 text-sm leading-relaxed text-[var(--color-text-secondary)]">
|
|
107
|
+
{description}
|
|
108
|
+
</p>
|
|
109
|
+
|
|
110
|
+
{/* Tags */}
|
|
111
|
+
{tags && tags.length > 0 && (
|
|
112
|
+
<div className="mt-auto flex flex-wrap gap-2 pt-4">
|
|
113
|
+
{tags.map((tag) => (
|
|
114
|
+
<span
|
|
115
|
+
key={tag}
|
|
116
|
+
className="rounded-full border border-[var(--color-border)] bg-[var(--color-surface-muted)] px-3 py-1 text-xs font-medium text-[var(--color-text-secondary)]"
|
|
117
|
+
>
|
|
118
|
+
{tag}
|
|
119
|
+
</span>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
{/* Custom content slot */}
|
|
125
|
+
{children}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Footer slot */}
|
|
129
|
+
{footerSlot}
|
|
130
|
+
</a>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default ListCard;
|
|
@@ -2,23 +2,29 @@
|
|
|
2
2
|
* Pattern Components
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export { default as PageHero } from
|
|
6
|
-
export { default as VideoHero } from
|
|
7
|
-
export { default as TeamGrid } from
|
|
8
|
-
export { default as ServiceSection } from
|
|
9
|
-
export { default as ProcessSteps } from
|
|
10
|
-
export { default as StatBar } from
|
|
11
|
-
export { default as IndustryGrid } from
|
|
12
|
-
export { default as ContentPage } from
|
|
13
|
-
export { TerminalWindow, TerminalChrome } from
|
|
14
|
-
export type {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export {
|
|
20
|
-
export type {
|
|
21
|
-
export {
|
|
22
|
-
export type {
|
|
23
|
-
export {
|
|
24
|
-
export type {
|
|
5
|
+
export { default as PageHero } from "./PageHero";
|
|
6
|
+
export { default as VideoHero } from "./VideoHero";
|
|
7
|
+
export { default as TeamGrid } from "./TeamGrid";
|
|
8
|
+
export { default as ServiceSection } from "./ServiceSection";
|
|
9
|
+
export { default as ProcessSteps } from "./ProcessSteps";
|
|
10
|
+
export { default as StatBar } from "./StatBar";
|
|
11
|
+
export { default as IndustryGrid } from "./IndustryGrid";
|
|
12
|
+
export { default as ContentPage } from "./ContentPage";
|
|
13
|
+
export { TerminalWindow, TerminalChrome } from "./TerminalWindow";
|
|
14
|
+
export type {
|
|
15
|
+
TerminalLine,
|
|
16
|
+
TerminalWindowProps,
|
|
17
|
+
TerminalChromeProps,
|
|
18
|
+
} from "./TerminalWindow";
|
|
19
|
+
export { default as TerminalHero } from "./TerminalHero";
|
|
20
|
+
export type { TerminalHeroProps } from "./TerminalHero";
|
|
21
|
+
export { CTAButton } from "./CTAButton";
|
|
22
|
+
export type { CTAButtonProps } from "./CTAButton";
|
|
23
|
+
export { ProductCard } from "./ProductCard";
|
|
24
|
+
export type { ProductCardProps } from "./ProductCard";
|
|
25
|
+
export { CodeBlock } from "./CodeBlock";
|
|
26
|
+
export type { CodeBlockProps } from "./CodeBlock";
|
|
27
|
+
export { FeatureShowcase } from "./FeatureShowcase";
|
|
28
|
+
export type { FeatureShowcaseProps, Feature } from "./FeatureShowcase";
|
|
29
|
+
export { ListCard } from "./ListCard";
|
|
30
|
+
export type { ListCardProps } from "./ListCard";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface Author {
|
|
2
|
+
name: string;
|
|
3
|
+
url?: string;
|
|
4
|
+
avatar?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface AuthorMetaProps {
|
|
8
|
+
author: Author;
|
|
9
|
+
linkable?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* AuthorMeta Component
|
|
14
|
+
* Displays author information with optional avatar
|
|
15
|
+
*/
|
|
16
|
+
export function AuthorMeta({ author, linkable = false }: AuthorMetaProps) {
|
|
17
|
+
const content = (
|
|
18
|
+
<span className="flex items-center gap-1">
|
|
19
|
+
{author.avatar && (
|
|
20
|
+
<img
|
|
21
|
+
src={author.avatar}
|
|
22
|
+
alt={author.name}
|
|
23
|
+
className="h-4 w-4 rounded-full object-cover"
|
|
24
|
+
/>
|
|
25
|
+
)}
|
|
26
|
+
<span>{author.name}</span>
|
|
27
|
+
</span>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (linkable && author.url) {
|
|
31
|
+
return (
|
|
32
|
+
<a
|
|
33
|
+
href={author.url}
|
|
34
|
+
className="flex items-center gap-1 transition-colors hover:text-[var(--color-primary)]"
|
|
35
|
+
>
|
|
36
|
+
{content}
|
|
37
|
+
</a>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return content;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default AuthorMeta;
|