Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5c0ab4e
fix: trim excess page bottom padding below footer (120px -> 40px)
lottabydesign Jun 13, 2026
eec4a9c
fix: derive footer 'Last updated' + copyright year at build time (no …
lottabydesign Jun 13, 2026
bec6297
fix: vertically center Copy button in the single-line Install box
lottabydesign Jun 13, 2026
79dd378
polish: copy buttons — drop lift/shadow, hover darkens text, 'Copied'…
lottabydesign Jun 13, 2026
86fd9a6
style: make --accent a dark neutral (kill blue accent text)
lottabydesign Jun 13, 2026
6386bbd
feat: add 'Prompt' tab to Install — copy-this-to-your-agent setup pro…
lottabydesign Jun 13, 2026
4a8b54c
Tighten hero→Install spacing (Section marginTop 72→32)
lottabydesign Jun 13, 2026
c053e4b
Wrap code box content instead of horizontal scroll
lottabydesign Jun 13, 2026
f064aef
Make Copy buttons text-only (strip box: border/bg/radius/padding)
lottabydesign Jun 13, 2026
0a0a489
Copy button: align right padding to 18px, match inactive-tab text sty…
lottabydesign Jun 13, 2026
1061de9
Remove grey fill on secondary action buttons (transparent + border)
lottabydesign Jun 13, 2026
efc3b58
Replace lift+shadow hover with subtle scale (Emil: flat UI, drastical…
lottabydesign Jun 13, 2026
2dbaaca
Fix missing press depression: order :active after :hover (LVHA cascade)
lottabydesign Jun 13, 2026
c10392c
Link footer 'Lota' to x.com/lottabydesign
lottabydesign Jun 13, 2026
d377325
Hover darkens links + tabs (instead of fading); move colors inline→cl…
lottabydesign Jun 13, 2026
1ba8e6d
Remove underline on byline name link (.link-plain), keep hover-darken
lottabydesign Jun 13, 2026
9893453
Darken byline hover to #1a1a1a (base text already dark, #333 was impe…
lottabydesign Jun 13, 2026
137d6dc
Hero rebuild, Section marginTop prop, drop unused Geist Mono, add log…
lottabydesign Jun 13, 2026
55a438e
Add CI workflow: library (typecheck/test/build) + site (typecheck/build)
lottabydesign Jun 13, 2026
cb0f684
Bump CI actions to v6 (Node 24 runtime; v4 deprecated June 2026)
lottabydesign Jun 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CI

# Run on pushes to main and on every pull request targeting main,
# so PRs are validated before merge.
on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
# The library lives at the repo root: typecheck, run the vitest suite, build.
library:
name: Library (typecheck · test · build)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
- run: npm run typecheck
- run: npm test
- run: npm run build

# The docs site lives in /site and consumes the published stampstack from npm,
# so it builds independently of the library job. No tests here — typecheck + build.
site:
name: Site (typecheck · build)
runs-on: ubuntu-latest
defaults:
run:
working-directory: site
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
cache: npm
cache-dependency-path: site/package-lock.json
- run: npm ci
- run: npm run typecheck
- run: npm run build
1 change: 1 addition & 0 deletions site/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
.next
next-env.d.ts
.vercel
7 changes: 0 additions & 7 deletions site/app/fonts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import localFont from 'next/font/local'
import { Geist_Mono } from 'next/font/google'

export const openRunde = localFont({
src: [
Expand All @@ -11,9 +10,3 @@ export const openRunde = localFont({
variable: '--font-open-runde',
display: 'swap',
})

export const geistMono = Geist_Mono({
subsets: ['latin'],
variable: '--font-geist-mono',
display: 'swap',
})
105 changes: 84 additions & 21 deletions site/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
--card: #ffffff;
--text: #333333;
--muted: #8a8a8a;
--border: #ececec;
--accent: #295df6;
--border: #E5E5E5;
--accent: #1a1a1a; /* dark neutral — no blue accent text */

/* The `, system-ui` fallback inside var() matters: if the next/font variable
isn't resolved yet, var() would otherwise invalidate font-family → serif. */
Expand All @@ -13,7 +13,7 @@
Cascadia first (modern, if installed), Consolas as the guaranteed catch-all. */
--font-mono: "SF Mono", SFMono-Regular, ui-monospace, "Cascadia Code", "Cascadia Mono", Consolas, "Courier New", monospace;

--maxw: 640px;
--maxw: 560px;
}

* { box-sizing: border-box; }
Expand All @@ -33,20 +33,26 @@ html, body {
overflow-x: clip;
}

/* A thin gradient hairline along the very top — the one playful accent. */
/* A soft, blurred rainbow wash across the very top — the one playful accent.
Figma treatment (node 1787:9551): a ~13px band heavily blurred (33px) and
dropped to 20% opacity, so the brand rainbow blooms into a pastel glow.
pointer-events:none so the bleed never blocks clicks on the header below. */
body::before {
content: '';
position: fixed;
top: 0; left: 0; right: 0;
height: 3px;
height: 14px;
background: linear-gradient(90deg, #295df6, #c6a0fd, #5cd500, #ff7a45, #ff3e8c, #00c9a7);
filter: blur(33px);
opacity: 0.2;
pointer-events: none;
z-index: 100;
}

main {
max-width: var(--maxw);
margin: 0 auto;
padding: 0 20px 120px;
padding: 0 20px 30px;
}

a { color: var(--accent); text-decoration: none; }
Expand All @@ -55,12 +61,23 @@ a:hover { text-decoration: underline; }
/* Button-style links (Figma nav buttons) — no underline / opacity fade. */
.btn:hover { text-decoration: none; opacity: 1; }

/* Quiet inline links (footer) — sit in the surrounding grey at rest, darken on
hover via the a:hover rule. Color lives here (not inline) so :hover can win. */
.link-quiet { color: inherit; text-decoration: underline; }

/* Byline link (the name) — same quiet grey + hover-darken, but never underlined.
The :hover rule overrides the base a:hover underline (equal specificity, later wins). */
.link-plain { color: inherit; }
/* Base text here (#484747) is already dark, so var(--text) barely reads as a
change. Darken to the brand's darkest neutral (#1a1a1a) for a visible hover. */
.link-plain:hover { color: var(--accent); text-decoration: none; }

/* The quiet section label — the haptics signature. */
.section-label {
font-size: 13px;
font-weight: 500;
font-weight: 600;
letter-spacing: -0.005em;
color: var(--muted);
color: #252525; /* match the Outro label (Footer.tsx) */
margin: 0 0 14px;
}

Expand All @@ -74,13 +91,17 @@ code, pre, .code-card pre, .code-card code {
position: relative;
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
border-radius: 15px;
overflow: hidden;
}
.code-card pre {
margin: 0;
padding: 16px 18px;
overflow-x: auto;
/* Reflow long lines inside the box instead of scrolling sideways:
pre-wrap keeps indentation + intentional breaks but wraps when too wide;
break-word lets a single long token (e.g. a path) break rather than overflow. */
white-space: pre-wrap;
overflow-wrap: break-word;
background: var(--card) !important;
font-family: var(--font-mono);
font-size: 13.5px;
Expand All @@ -96,21 +117,26 @@ code, pre, .code-card pre, .code-card code {
:root { --ease-out: cubic-bezier(0.23, 1, 0.32, 1); } /* strong ease-out */

/* Smooth state changes (color/bg swaps animate via the transition). */
.ss-tap { transition: transform 0.14s var(--ease-out), box-shadow 0.2s ease, color 0.18s ease, background-color 0.18s ease, border-color 0.18s ease; }
.ss-tab { transition: color 0.18s ease, opacity 0.18s ease, transform 0.14s var(--ease-out); }
.ss-tap { transition: transform 0.14s var(--ease-out), color 0.18s ease, background-color 0.18s ease, border-color 0.18s ease; }
.ss-tab { color: var(--muted); transition: color 0.18s ease, opacity 0.18s ease, transform 0.14s var(--ease-out); }
.ss-tab.is-active { color: var(--text); } /* selected tab — colored via class so :hover can win */
a { transition: opacity 0.16s ease, color 0.18s ease; }

/* Press feedback — every pointer type, motion-safe. */
@media (prefers-reduced-motion: no-preference) {
.ss-tap:active { transform: scale(0.97); box-shadow: none; }
.ss-tab:active { transform: scale(0.95); }
/* Hover — only true hover devices, so taps on touch don't trigger it.
MUST come before :active below — equal specificity means source order wins,
so :active needs to be last to override the hover scale during a press. */
@media (prefers-reduced-motion: no-preference) and (hover: hover) and (pointer: fine) {
.ss-tap:hover { transform: scale(1.02); }
/* Darken on hover (toward the main text color), not fade — matches the Copy button. */
.ss-tab:hover { color: var(--text); }
a:hover { color: var(--text); }
}

/* Hover — only true hover devices, so taps on touch don't trigger it. */
@media (prefers-reduced-motion: no-preference) and (hover: hover) and (pointer: fine) {
.ss-tap:hover { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); }
.ss-tab:hover { opacity: 0.6; }
a:hover { opacity: 0.6; }
/* Press feedback — every pointer type, motion-safe. Last so it wins over :hover
when the pointer is both hovering and pressing (the LVHA cascade order). */
@media (prefers-reduced-motion: no-preference) {
.ss-tap:active { transform: scale(0.97); }
.ss-tab:active { transform: scale(0.95); }
}

/* Scroll-reveal: fade + rise into view (one-shot via IntersectionObserver). */
Expand All @@ -133,3 +159,40 @@ a { transition: opacity 0.16s ease, color 0.18s ease; }
}
@keyframes ss-rise { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
@keyframes ss-fade { from { opacity: 0; } to { opacity: 1; } }

/* Copy buttons: no lift, no shadow. Hover darkens the text; the "Copied"
label eases in on click. */
.copy-btn {
color: var(--muted);
transition: color 0.18s ease-in;
}
@media (hover: hover) and (pointer: fine) {
.copy-btn:hover {
color: var(--text);
}
}
.copy-btn.is-copied {
color: var(--accent);
}
.copy-label {
display: inline-block;
}
@media (prefers-reduced-motion: no-preference) {
.copy-label {
animation: copy-in 0.2s ease-in;
}
}
@keyframes copy-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

/* Vertically-centered Copy button in single-line boxes (Install). */
.copy-center {
top: 50%;
transform: translateY(-50%);
}
4 changes: 2 additions & 2 deletions site/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Metadata } from 'next'
import './globals.css'
import 'stampstack/styles.css'
import { openRunde, geistMono } from './fonts'
import { openRunde } from './fonts'

export const metadata: Metadata = {
title: 'stampstack — a draggable 3D coverflow of postage-stamp cards',
Expand All @@ -11,7 +11,7 @@ export const metadata: Metadata = {

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${openRunde.variable} ${geistMono.variable}`}>
<html lang="en" className={openRunde.variable}>
<body>{children}</body>
</html>
)
Expand Down
4 changes: 3 additions & 1 deletion site/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Hero } from '@/components/Hero'
import { Section } from '@/components/Section'
import { Reveal } from '@/components/Reveal'
import { InstallTabs } from '@/components/InstallTabs'
import { ActionButtons } from '@/components/ActionButtons'
import { CodeBlock } from '@/components/CodeBlock'
import { Footer } from '@/components/Footer'

Expand All @@ -27,8 +28,9 @@ export default function Home() {
<Hero />

<Reveal>
<Section label="Install">
<Section label="Install" marginTop={32}>
<InstallTabs />
<ActionButtons />
</Section>
</Reveal>

Expand Down
42 changes: 42 additions & 0 deletions site/components/ActionButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { CSSProperties } from 'react'

// Button styles mirrored 1:1 from Figma (node 1704:513).
const btnBase: CSSProperties = {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 9,
height: 36,
padding: '8px 16px',
borderRadius: 11,
fontFamily: 'var(--font-ui)',
fontWeight: 500,
fontSize: 14,
letterSpacing: '-0.5px',
lineHeight: '20px',
whiteSpace: 'nowrap',
}
const primaryBtn: CSSProperties = { ...btnBase, background: '#171717', color: '#fafafa', border: '1px solid #171717' }
const secondaryBtn: CSSProperties = { ...btnBase, background: 'transparent', color: '#0a0a0a', border: '1px solid #e5e5e5' }

export function ActionButtons() {
return (
<div style={{ display: 'flex', gap: 12, justifyContent: 'flex-start', flexWrap: 'wrap', marginTop: 20 }}>
<a className="btn ss-tap" href="https://github.com/lottabydesign/stampstack" style={primaryBtn}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/icons/github.svg" alt="" width={14} height={13.5} />
Star on GitHub
</a>
<a className="btn ss-tap" href="https://github.com/lottabydesign/stampstack#readme" style={secondaryBtn}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/icons/book-open.svg" alt="" width={14} height={14} />
View docs
</a>
<a className="btn ss-tap" href="https://x.com/lottabydesign" style={secondaryBtn}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/icons/x.svg" alt="" width={12} height={12} />
Follow
</a>
</div>
)
}
29 changes: 18 additions & 11 deletions site/components/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
'use client'
import { useState } from 'react'

export function CopyButton({ text }: { text: string }) {
export function CopyButton({ text, center = false }: { text: string; center?: boolean }) {
const [copied, setCopied] = useState(false)
return (
<button
className="ss-tap"
className={`copy-btn${copied ? ' is-copied' : ''}${center ? ' copy-center' : ''}`}
onClick={async () => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 1400)
}}
style={{
position: 'absolute',
top: 10,
right: 10,
border: '1px solid var(--border)',
background: 'var(--bg)',
color: copied ? 'var(--accent)' : 'var(--muted)',
borderRadius: 7,
padding: '4px 9px',
fontSize: 12,
// Match the code's 18px horizontal padding so left/right inset reads even.
right: 18,
// `center` (single-line boxes like Install) vertically centers via the
// .copy-center class; otherwise pin to the top (multi-line code blocks).
...(center ? {} : { top: 14 }),
// Text-only: no box (border/background/radius/padding stripped).
border: 'none',
background: 'none',
padding: 0,
// Match the inactive Install tabs: 13px / weight 500 / muted (color via .copy-btn).
fontSize: 13,
fontWeight: 500,
fontFamily: 'var(--font-ui)',
cursor: 'pointer',
}}
aria-label="Copy to clipboard"
>
{copied ? 'Copied' : 'Copy'}
{/* keyed so the label remounts and the ease-in animation replays on toggle */}
<span key={copied ? 'copied' : 'copy'} className="copy-label">
{copied ? 'Copied' : 'Copy'}
</span>
</button>
)
}
Loading