mirror of
https://github.com/layer-systems/website.git
synced 2026-09-14 15:26:22 +02:00
Redesign visual identity: layer-stack design system + styleguide
Reworks the site around a signature "layer stack" motif — the relay is literally one layer between a client and the wider Nostr network, so the brand mark is that stack rather than a logo standing in for it. - New color/type/spacing tokens (src/index.css, tailwind.config.ts): cool "Paper" light surface, deep "Ink" dark surface, relay-amber accent, navy/cyan structural colors; JetBrains Mono as the display face for headlines and data, Inter for body copy (both self-hosted via fontsource to satisfy the existing font-src 'self' CSP). - New brand components: LayerStack (hero signature), LayerDivider, Wordmark, SiteHeader/SiteFooter, shared across the landing page, legal pages, and the new styleguide. - Reworked the landing page hero, feature grid, and connection steps with benefit-first, plain-voice copy. - Added docs/DESIGN_STYLEGUIDE.md and a live /styleguide route documenting the system. - Fixed structural gaps: /messages had no route, and Explore/Messages had no sidebar navigation entry despite being real pages; both now sit in the same app shell as Dashboard. Closes #8 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FvaqCkYtksTvsEKGzjGcde
This commit is contained in:
@@ -116,6 +116,8 @@ The project includes a **`docs/`** directory containing specialized documentatio
|
||||
|
||||
- **`docs/NOSTR_DIRECT_MESSAGES.md`**: Read when implementing direct messaging features, building chat interfaces, or working with encrypted peer-to-peer communication (NIP-04 and NIP-17).
|
||||
|
||||
- **`docs/DESIGN_STYLEGUIDE.md`**: Read before making any visual change — color, type, spacing, or component styling. Covers the design tokens in `src/index.css`, the "layer stack" signature motif, and the rationale behind them. The live version is the `/styleguide` route (`src/pages/Styleguide.tsx`).
|
||||
|
||||
## System Prompt Management
|
||||
|
||||
The AI assistant's behavior and knowledge is defined by the AGENTS.md file, which serves as the system prompt. To modify the assistant's instructions or add new project-specific guidelines:
|
||||
|
||||
127
docs/DESIGN_STYLEGUIDE.md
Normal file
127
docs/DESIGN_STYLEGUIDE.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# LAYER.systems Design Styleguide
|
||||
|
||||
Read this before making visual changes anywhere in the app. The live version of
|
||||
everything below is at [`/styleguide`](../src/pages/Styleguide.tsx).
|
||||
|
||||
## The idea
|
||||
|
||||
LAYER.systems is a public Nostr relay: one layer in the path between a
|
||||
person's client and the rest of the network. The brand doesn't use a logo to
|
||||
*represent* that — the signature visual **is** the stack (`Client → Relay →
|
||||
Network`, see `src/components/brand/LayerStack.tsx`), lit up at the relay
|
||||
layer, because that's literally what this product is.
|
||||
|
||||
Everything else in the system follows from taking that stack seriously:
|
||||
layered, hairline-bordered surfaces instead of soft drop shadows; a
|
||||
monospace face used for headlines and data, not tucked away as a caption
|
||||
font; and a dark "Ink" hero that reads like an oscilloscope panel, not a
|
||||
marketing gradient.
|
||||
|
||||
## Color
|
||||
|
||||
Named tokens live in `src/index.css` as HSL CSS variables, consumed through
|
||||
Tailwind (`tailwind.config.ts`) and shadcn/ui components. Don't hardcode hex
|
||||
values in components — reference the token (`bg-primary`, `text-brand-cyan`,
|
||||
`hsl(var(--brand-navy))`) so light/dark mode and future palette tweaks stay
|
||||
centralized.
|
||||
|
||||
| Name | Hex | Token | Usage |
|
||||
|---|---|---|---|
|
||||
| Paper | `#F5F6F5` | `--background` (light) | Default light-mode surface — cool, not cream |
|
||||
| Ink | `#0E1116` | `--brand-ink`, `--background` (dark) | Hero panel + dark-mode base surface |
|
||||
| Relay Amber | `#F5A623` | `--primary` | The signature signal color: primary actions, the "live" layer, focus rings |
|
||||
| Deep Signal Navy | `#1C2B45` | `--brand-navy` | Structural color for layer bands and dark-mode accents |
|
||||
| Circuit Cyan | `#3FC1CE` | `--brand-cyan`, `--chart-2` | Secondary data color — charts, hover accents |
|
||||
| Slate Ink | `#2B3140` | `--foreground` (light) | Body text on light surfaces |
|
||||
|
||||
Both light and dark modes share the same amber accent so the brand reads
|
||||
consistently either way — only the surface (paper vs. ink) flips.
|
||||
|
||||
**Why not the obvious defaults:** we deliberately avoided a warm-cream +
|
||||
serif look (doesn't fit infrastructure) and a pure near-black + single
|
||||
neon accent (too generic for what should feel like real hardware — a relay
|
||||
lamp, not a startup gradient). The dark hero pairs a warm accent (amber)
|
||||
with a cool structural color (navy) and a cool data color (cyan), which is
|
||||
richer than the single-accent cliché, and only the *hero* is dark — the app
|
||||
itself is light-first.
|
||||
|
||||
## Type
|
||||
|
||||
Two faces, doing different jobs:
|
||||
|
||||
- **Display / data — JetBrains Mono** (`font-display` / `font-mono`,
|
||||
`@fontsource-variable/jetbrains-mono`). Used for headlines, the eyebrow
|
||||
labels (`.eyebrow` utility class), stat numbers, relay URLs, hex ids, and
|
||||
timestamps. This is a wire-protocol product — a monospace headline is a
|
||||
deliberate choice that matches the subject, not a caption font promoted
|
||||
by accident.
|
||||
- **Body — Inter** (`font-sans`, `@fontsource-variable/inter`). Everything a
|
||||
person reads at length: paragraphs, descriptions, terms & privacy copy.
|
||||
|
||||
Both are self-hosted via `@fontsource-variable/*` and imported once in
|
||||
`src/main.tsx` — the CSP (`font-src 'self'`) blocks third-party font CDNs,
|
||||
so don't add a Google Fonts `<link>`.
|
||||
|
||||
Eyebrow labels use the `.eyebrow` utility (`src/index.css`): mono, uppercase,
|
||||
`0.14em` tracking, small size. Use them to label a section's role
|
||||
("Public Nostr relay", "Getting connected"), not as decoration.
|
||||
|
||||
## Spacing & radius
|
||||
|
||||
Standard Tailwind 4px scale (`--spacing: 0.25rem`). `--radius` is `0.5rem`
|
||||
site-wide — soft enough to feel modern, restrained enough to stay
|
||||
technical. Don't mix radius values ad hoc; use the `rounded-lg` /
|
||||
`rounded-md` / `rounded-sm` scale derived from the token.
|
||||
|
||||
## Signature components
|
||||
|
||||
- **`LayerStack`** (`src/components/brand/LayerStack.tsx`) — the hero mark.
|
||||
Three cascading bands (Client / Relay / Network), the relay band lit
|
||||
amber and gently pulsing. Respects `prefers-reduced-motion` via Tailwind's
|
||||
`motion-safe:` variant — the settle-in animation and pulse simply don't
|
||||
run when reduced motion is requested; the stack still renders in its
|
||||
final state.
|
||||
- **`LayerDivider`** — a quiet three-line echo of the stack for breaking up
|
||||
sections without repeating the hero verbatim.
|
||||
- **`.layer-card`** utility class — the default card treatment across the
|
||||
marketing site: two hairline borders offset by 6px instead of a drop
|
||||
shadow, widening slightly on hover. Use plain `border + shadow-sm` cards
|
||||
instead inside dense data views (dashboard, explorer) where the offset
|
||||
border would add noise against a grid of cards.
|
||||
|
||||
## Components
|
||||
|
||||
Built on shadcn/ui (`src/components/ui/*`) — Button, Card, Badge, Input,
|
||||
Alert, Sidebar, etc. — restyled entirely through the token layer, not by
|
||||
forking the components. See `/styleguide` for live specimens of every
|
||||
variant. When adding new UI, prefer an existing shadcn primitive over a
|
||||
bespoke one.
|
||||
|
||||
## Motion
|
||||
|
||||
- Page-load: the `LayerStack` bands settle in with a short staggered
|
||||
animation (`animate-layer-settle`); the relay band pulses continuously
|
||||
but subtly (`animate-signal-pulse`).
|
||||
- Hover: `.layer-card` widens its offset border; buttons and links use the
|
||||
existing shadcn transition tokens. No scroll-triggered effects — this is
|
||||
a technical, read-once page, not a scrollytelling site.
|
||||
- Everything animated is wrapped in Tailwind's `motion-safe:` variant so
|
||||
`prefers-reduced-motion: reduce` gets the static end state.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Color pairs (background/foreground, primary/primary-foreground, etc.)
|
||||
are chosen for contrast in both themes — check new token pairs with a
|
||||
contrast checker before shipping.
|
||||
- Interactive elements rely on shadcn's built-in focus rings (`--ring`,
|
||||
amber) — don't remove `focus-visible` outlines.
|
||||
- The `LayerStack` diagram carries a text `aria-label` describing the same
|
||||
information conveyed visually (client → this relay → the network).
|
||||
|
||||
## Voice
|
||||
|
||||
Plain, direct, written from the person's side of the screen: "add the
|
||||
relay," not "leverage relay connectivity." Say what things do, not why
|
||||
they're impressive. Errors and empty states say what happened and what to
|
||||
do next — see `NotFound.tsx` for the tone ("There's no layer at
|
||||
`/whatever`") instead of a generic apology.
|
||||
30
package-lock.json
generated
30
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.2.6",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||
"@getalby/sdk": "^5.1.1",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@nostrify/nostrify": "^0.48.2",
|
||||
@@ -955,6 +956,15 @@
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/jetbrains-mono": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz",
|
||||
"integrity": "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@getalby/lightning-tools": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@getalby/lightning-tools/-/lightning-tools-5.2.0.tgz",
|
||||
@@ -3260,7 +3270,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3274,7 +3283,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3288,7 +3296,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3302,7 +3309,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3316,7 +3322,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3330,7 +3335,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3344,7 +3348,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3358,7 +3361,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3372,7 +3374,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3386,7 +3387,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3400,7 +3400,6 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3414,7 +3413,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3428,7 +3426,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3442,7 +3439,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3456,7 +3452,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3470,7 +3465,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3484,7 +3478,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3498,7 +3491,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3512,7 +3504,6 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3526,7 +3517,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.2.6",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||
"@getalby/sdk": "^5.1.1",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@nostrify/nostrify": "^0.48.2",
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Explore } from "./pages/Explore";
|
||||
import { Dashboard } from "./pages/Dashboard";
|
||||
import { DashboardEvents } from "./pages/DashboardEvents";
|
||||
import { DashboardExport } from "./pages/DashboardExport";
|
||||
import Messages from "./pages/Messages";
|
||||
import { Styleguide } from "./pages/Styleguide";
|
||||
import { NIP19Page } from "./pages/NIP19Page";
|
||||
import { Terms } from "./pages/Terms";
|
||||
import { Privacy } from "./pages/Privacy";
|
||||
@@ -21,6 +23,8 @@ export function AppRouter() {
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/dashboard/events" element={<DashboardEvents />} />
|
||||
<Route path="/dashboard/export" element={<DashboardExport />} />
|
||||
<Route path="/messages" element={<Messages />} />
|
||||
<Route path="/styleguide" element={<Styleguide />} />
|
||||
<Route path="/terms" element={<Terms />} />
|
||||
<Route path="/privacy" element={<Privacy />} />
|
||||
{/* NIP-19 route for npub1, note1, naddr1, nevent1, nprofile1 */}
|
||||
|
||||
16
src/components/brand/LayerDivider.tsx
Normal file
16
src/components/brand/LayerDivider.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* A quiet echo of the layer-stack device used to break up sections —
|
||||
* three hairlines of shrinking width, same cascade logic as the hero mark,
|
||||
* without repeating it verbatim.
|
||||
*/
|
||||
export function LayerDivider({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className={cn('flex flex-col items-center gap-1.5', className)} aria-hidden="true">
|
||||
<div className="h-px w-16 bg-border" />
|
||||
<div className="h-px w-10 bg-border" />
|
||||
<div className="h-px w-4 bg-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
93
src/components/brand/LayerStack.tsx
Normal file
93
src/components/brand/LayerStack.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Laptop, Radio, Globe2 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface Layer {
|
||||
eyebrow: string;
|
||||
name: string;
|
||||
detail: string;
|
||||
icon: typeof Laptop;
|
||||
emphasis?: boolean;
|
||||
}
|
||||
|
||||
const layers: Layer[] = [
|
||||
{
|
||||
eyebrow: '01 · You',
|
||||
name: 'Client',
|
||||
detail: 'Damus, Amethyst, Snort, or any Nostr app',
|
||||
icon: Laptop,
|
||||
},
|
||||
{
|
||||
eyebrow: '02 · This relay',
|
||||
name: 'LAYER.systems',
|
||||
detail: 'Receives, stores, and serves signed events',
|
||||
icon: Radio,
|
||||
emphasis: true,
|
||||
},
|
||||
{
|
||||
eyebrow: '03 · Everyone else',
|
||||
name: 'Nostr network',
|
||||
detail: 'Every other relay and client your notes reach',
|
||||
icon: Globe2,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* The site's signature device: LAYER.systems is literally one layer in a
|
||||
* relay stack, so the brand mark is that stack, not a logo standing in
|
||||
* for it. Each band cascades down and to the right — data moving outward
|
||||
* from the reader's client, through this relay, into the wider network.
|
||||
*/
|
||||
export function LayerStack({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className={cn('w-full max-w-md', className)} role="img" aria-label="Diagram: your client connects through the LAYER.systems relay to reach the wider Nostr network">
|
||||
{layers.map((layer, i) => (
|
||||
<div
|
||||
key={layer.name}
|
||||
className={cn(
|
||||
'motion-safe:animate-layer-settle',
|
||||
i > 0 && '-mt-px'
|
||||
)}
|
||||
style={{
|
||||
marginLeft: `${i * 28}px`,
|
||||
marginRight: `${(layers.length - 1 - i) * 28}px`,
|
||||
animationDelay: `${i * 140}ms`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-4 rounded-lg border px-5 py-4 backdrop-blur-sm transition-transform',
|
||||
layer.emphasis
|
||||
? 'border-brand-amber/50 bg-brand-amber/10 shadow-lg shadow-brand-amber/10 motion-safe:animate-signal-pulse'
|
||||
: 'border-white/10 bg-white/[0.03]'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-10 w-10 shrink-0 items-center justify-center rounded-md border',
|
||||
layer.emphasis
|
||||
? 'border-brand-amber/40 bg-brand-amber/15 text-brand-amber'
|
||||
: 'border-white/10 bg-white/5 text-white/60'
|
||||
)}
|
||||
>
|
||||
<layer.icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p
|
||||
className={cn(
|
||||
'eyebrow',
|
||||
layer.emphasis ? 'text-brand-amber' : 'text-white/40'
|
||||
)}
|
||||
>
|
||||
{layer.eyebrow}
|
||||
</p>
|
||||
<p className={cn('font-display text-base font-semibold', layer.emphasis ? 'text-white' : 'text-white/85')}>
|
||||
{layer.name}
|
||||
</p>
|
||||
<p className="truncate text-sm text-white/45">{layer.detail}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
src/components/brand/SiteFooter.tsx
Normal file
36
src/components/brand/SiteFooter.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Wordmark } from '@/components/brand/Wordmark';
|
||||
|
||||
const links = [
|
||||
{ to: '/styleguide', label: 'Styleguide' },
|
||||
{ to: '/terms', label: 'Terms' },
|
||||
{ to: '/privacy', label: 'Privacy' },
|
||||
];
|
||||
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<footer className="border-t border-border bg-muted/30">
|
||||
<div className="mx-auto flex max-w-6xl flex-col items-center gap-4 px-6 py-8 sm:flex-row sm:justify-between">
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<Wordmark className="text-sm" />
|
||||
<span aria-hidden="true">·</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-primary motion-safe:animate-signal-pulse" />
|
||||
relay online
|
||||
</span>
|
||||
</div>
|
||||
<nav className="flex items-center gap-6 text-sm">
|
||||
{links.map((link) => (
|
||||
<Link
|
||||
key={link.to}
|
||||
to={link.to}
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
30
src/components/brand/SiteHeader.tsx
Normal file
30
src/components/brand/SiteHeader.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { LoginArea } from '@/components/auth/LoginArea';
|
||||
import { Wordmark } from '@/components/brand/Wordmark';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface SiteHeaderProps {
|
||||
/** Transparent, for placement over a dark hero. */
|
||||
transparent?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SiteHeader({ transparent = false, className }: SiteHeaderProps) {
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
transparent
|
||||
? 'absolute top-0 left-0 right-0 z-50'
|
||||
: 'sticky top-0 z-50 border-b border-border bg-background/90 backdrop-blur supports-[backdrop-filter]:bg-background/70',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-5">
|
||||
<Link to="/" className="transition-opacity hover:opacity-80">
|
||||
<Wordmark inverted={transparent} className="text-lg" />
|
||||
</Link>
|
||||
<LoginArea className="max-w-60" />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
26
src/components/brand/Wordmark.tsx
Normal file
26
src/components/brand/Wordmark.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface WordmarkProps {
|
||||
className?: string;
|
||||
/** Renders on a dark surface (hero, footer) instead of the default app surface. */
|
||||
inverted?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The LAYER.systems wordmark, set in the display mono face. ".systems" is
|
||||
* de-emphasized so "LAYER" reads first — the word the brand is built on.
|
||||
*/
|
||||
export function Wordmark({ className, inverted = false }: WordmarkProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'font-display font-bold tracking-tight',
|
||||
inverted ? 'text-white' : 'text-foreground',
|
||||
className
|
||||
)}
|
||||
>
|
||||
LAYER
|
||||
<span className={inverted ? 'text-white/45' : 'text-muted-foreground'}>.systems</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Home, LayoutDashboard, FileText, Download } from 'lucide-react';
|
||||
import { Home, LayoutDashboard, FileText, Download, Compass, MessageCircle } from 'lucide-react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -7,11 +7,13 @@ import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from '@/components/ui/sidebar';
|
||||
import { LoginArea } from '@/components/auth/LoginArea';
|
||||
import { Wordmark } from '@/components/brand/Wordmark';
|
||||
|
||||
const navigationItems = [
|
||||
{
|
||||
@@ -19,6 +21,11 @@ const navigationItems = [
|
||||
url: '/',
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
title: 'Explore',
|
||||
url: '/explore',
|
||||
icon: Compass,
|
||||
},
|
||||
{
|
||||
title: 'Dashboard',
|
||||
url: '/dashboard',
|
||||
@@ -34,6 +41,11 @@ const navigationItems = [
|
||||
url: '/dashboard/export',
|
||||
icon: Download,
|
||||
},
|
||||
{
|
||||
title: 'Messages',
|
||||
url: '/messages',
|
||||
icon: MessageCircle,
|
||||
},
|
||||
];
|
||||
|
||||
export function AppSidebar() {
|
||||
@@ -41,9 +53,14 @@ export function AppSidebar() {
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
<SidebarHeader>
|
||||
<Link to="/" className="flex items-center px-2 py-1 transition-opacity hover:opacity-80">
|
||||
<Wordmark className="text-base" />
|
||||
</Link>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
||||
<SidebarGroupLabel className="eyebrow">Navigation</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{navigationItems.map((item) => {
|
||||
|
||||
248
src/index.css
248
src/index.css
@@ -2,113 +2,135 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/*
|
||||
* LAYER.systems design tokens.
|
||||
* See docs/DESIGN_STYLEGUIDE.md for the full rationale — palette names,
|
||||
* type scale, spacing, and the "layer stack" signature motif.
|
||||
*/
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 14.9020%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 14.9020%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 14.9020%;
|
||||
--primary: 37.6923 92.1260% 50.1961%;
|
||||
--primary-foreground: 0 0% 0%;
|
||||
--secondary: 220.0000 14.2857% 95.8824%;
|
||||
--secondary-foreground: 215 13.7931% 34.1176%;
|
||||
--muted: 210 20.0000% 98.0392%;
|
||||
/* Paper — cool, slightly green-gray light surface (not cream) */
|
||||
--background: 120 5.2632% 96.2745%;
|
||||
--foreground: 222.8571 19.6262% 20.9804%;
|
||||
--card: 60 9.0909% 97.8431%;
|
||||
--card-foreground: 222.8571 19.6262% 20.9804%;
|
||||
--popover: 60 9.0909% 97.8431%;
|
||||
--popover-foreground: 222.8571 19.6262% 20.9804%;
|
||||
/* Relay Amber — the signature signal color */
|
||||
--primary: 37.4286 91.3043% 54.9020%;
|
||||
--primary-foreground: 217.5000 22.2222% 7.0588%;
|
||||
--secondary: 220 23.0769% 92.3529%;
|
||||
--secondary-foreground: 222.8571 19.6262% 20.9804%;
|
||||
--muted: 216 16.1290% 93.9216%;
|
||||
--muted-foreground: 220 8.9362% 46.0784%;
|
||||
--accent: 48.0000 100.0000% 96.0784%;
|
||||
--accent-foreground: 22.7273 82.5000% 31.3725%;
|
||||
--destructive: 0 84.2365% 60.1961%;
|
||||
/* Circuit Cyan tint, used for hover/accent surfaces */
|
||||
--accent: 183.3333 47.3684% 92.5490%;
|
||||
--accent-foreground: 187.1429 70.0000% 23.5294%;
|
||||
--destructive: 358.0892 75.1196% 59.0196%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 220 13.0435% 90.9804%;
|
||||
--input: 220 13.0435% 90.9804%;
|
||||
--ring: 37.6923 92.1260% 50.1961%;
|
||||
--chart-1: 37.6923 92.1260% 50.1961%;
|
||||
--chart-2: 32.1327 94.6188% 43.7255%;
|
||||
--chart-3: 25.9649 90.4762% 37.0588%;
|
||||
--chart-4: 22.7273 82.5000% 31.3725%;
|
||||
--chart-5: 21.7143 77.7778% 26.4706%;
|
||||
--sidebar: 210 20.0000% 98.0392%;
|
||||
--sidebar-foreground: 0 0% 14.9020%;
|
||||
--sidebar-primary: 37.6923 92.1260% 50.1961%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 48.0000 100.0000% 96.0784%;
|
||||
--sidebar-accent-foreground: 22.7273 82.5000% 31.3725%;
|
||||
--sidebar-border: 220 13.0435% 90.9804%;
|
||||
--sidebar-ring: 37.6923 92.1260% 50.1961%;
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-serif: Source Serif 4, serif;
|
||||
--font-mono: JetBrains Mono, monospace;
|
||||
--radius: 0.375rem;
|
||||
--border: 213.3333 15.2542% 88.4314%;
|
||||
--input: 213.3333 15.2542% 88.4314%;
|
||||
--ring: 37.4286 91.3043% 54.9020%;
|
||||
--chart-1: 37.4286 91.3043% 54.9020%;
|
||||
--chart-2: 185.4545 59.3361% 52.7451%;
|
||||
--chart-3: 219.0909 41.7722% 30.9804%;
|
||||
--chart-4: 36.7839 84.6809% 46.0784%;
|
||||
--chart-5: 185.6934 61.9910% 43.3333%;
|
||||
--sidebar: 216 18.5185% 94.7059%;
|
||||
--sidebar-foreground: 222.8571 19.6262% 20.9804%;
|
||||
--sidebar-primary: 37.4286 91.3043% 54.9020%;
|
||||
--sidebar-primary-foreground: 217.5000 22.2222% 7.0588%;
|
||||
--sidebar-accent: 183.3333 47.3684% 92.5490%;
|
||||
--sidebar-accent-foreground: 187.1429 70.0000% 23.5294%;
|
||||
--sidebar-border: 213.3333 15.2542% 88.4314%;
|
||||
--sidebar-ring: 37.4286 91.3043% 54.9020%;
|
||||
|
||||
/* Brand-specific values used outside the shadcn token set
|
||||
(the layer-stack device, marketing surfaces) */
|
||||
--brand-ink: 217.5000 22.2222% 7.0588%;
|
||||
--brand-ink-raised: 216.9231 23.6364% 10.7843%;
|
||||
--brand-navy: 218.0488 42.2680% 19.0196%;
|
||||
--brand-navy-light: 219.0909 41.7722% 30.9804%;
|
||||
--brand-cyan: 185.4545 59.3361% 52.7451%;
|
||||
--brand-amber: 37.4286 91.3043% 54.9020%;
|
||||
|
||||
--font-sans: 'Inter Variable', Inter, sans-serif;
|
||||
--font-display: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, monospace;
|
||||
--font-mono: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 4px;
|
||||
--shadow-blur: 8px;
|
||||
--shadow-spread: -1px;
|
||||
--shadow-opacity: 0.1;
|
||||
--shadow-color: hsl(0 0% 0%);
|
||||
--shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
|
||||
--shadow-color: hsl(217.5 22% 7% / 1);
|
||||
--shadow-2xs: 0px 2px 4px -1px hsl(217.5 22% 7% / 0.04);
|
||||
--shadow-xs: 0px 2px 4px -1px hsl(217.5 22% 7% / 0.06);
|
||||
--shadow-sm: 0px 4px 8px -2px hsl(217.5 22% 7% / 0.08), 0px 1px 2px -2px hsl(217.5 22% 7% / 0.06);
|
||||
--shadow: 0px 4px 8px -2px hsl(217.5 22% 7% / 0.08), 0px 1px 2px -2px hsl(217.5 22% 7% / 0.06);
|
||||
--shadow-md: 0px 6px 12px -2px hsl(217.5 22% 7% / 0.10), 0px 2px 4px -2px hsl(217.5 22% 7% / 0.08);
|
||||
--shadow-lg: 0px 10px 20px -4px hsl(217.5 22% 7% / 0.12), 0px 4px 6px -2px hsl(217.5 22% 7% / 0.08);
|
||||
--shadow-xl: 0px 16px 28px -6px hsl(217.5 22% 7% / 0.16), 0px 8px 10px -4px hsl(217.5 22% 7% / 0.10);
|
||||
--shadow-2xl: 0px 24px 40px -8px hsl(217.5 22% 7% / 0.24);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 9.0196%;
|
||||
--foreground: 0 0% 89.8039%;
|
||||
--card: 0 0% 14.9020%;
|
||||
--card-foreground: 0 0% 89.8039%;
|
||||
--popover: 0 0% 14.9020%;
|
||||
--popover-foreground: 0 0% 89.8039%;
|
||||
--primary: 37.6923 92.1260% 50.1961%;
|
||||
--primary-foreground: 0 0% 0%;
|
||||
--secondary: 0 0% 14.9020%;
|
||||
--secondary-foreground: 0 0% 89.8039%;
|
||||
--muted: 0 0% 12.1569%;
|
||||
--muted-foreground: 0 0% 63.9216%;
|
||||
--accent: 22.7273 82.5000% 31.3725%;
|
||||
--accent-foreground: 48 96.6387% 76.6667%;
|
||||
--destructive: 0 84.2365% 60.1961%;
|
||||
/* Ink — deep blue-graphite, not pure black */
|
||||
--background: 217.5000 22.2222% 7.0588%;
|
||||
--foreground: 215 17.6471% 86.6667%;
|
||||
--card: 216.9231 23.6364% 10.7843%;
|
||||
--card-foreground: 215 17.6471% 86.6667%;
|
||||
--popover: 216.9231 23.6364% 10.7843%;
|
||||
--popover-foreground: 215 17.6471% 86.6667%;
|
||||
--primary: 37.4286 91.3043% 54.9020%;
|
||||
--primary-foreground: 217.5000 22.2222% 7.0588%;
|
||||
--secondary: 216 22.3881% 13.1373%;
|
||||
--secondary-foreground: 215 17.6471% 86.6667%;
|
||||
--muted: 216.9231 22.0339% 11.5686%;
|
||||
--muted-foreground: 218.4000 11.9617% 59.0196%;
|
||||
--accent: 219.0909 41.7722% 30.9804%;
|
||||
--accent-foreground: 185.1064 58.0247% 84.1176%;
|
||||
--destructive: 358.0892 75.1196% 59.0196%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 0 0% 25.0980%;
|
||||
--input: 0 0% 25.0980%;
|
||||
--ring: 37.6923 92.1260% 50.1961%;
|
||||
--chart-1: 43.2558 96.4126% 56.2745%;
|
||||
--chart-2: 32.1327 94.6188% 43.7255%;
|
||||
--chart-3: 22.7273 82.5000% 31.3725%;
|
||||
--chart-4: 25.9649 90.4762% 37.0588%;
|
||||
--chart-5: 22.7273 82.5000% 31.3725%;
|
||||
--sidebar: 0 0% 5.8824%;
|
||||
--sidebar-foreground: 0 0% 89.8039%;
|
||||
--sidebar-primary: 37.6923 92.1260% 50.1961%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 22.7273 82.5000% 31.3725%;
|
||||
--sidebar-accent-foreground: 48 96.6387% 76.6667%;
|
||||
--sidebar-border: 0 0% 25.0980%;
|
||||
--sidebar-ring: 37.6923 92.1260% 50.1961%;
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-serif: Source Serif 4, serif;
|
||||
--font-mono: JetBrains Mono, monospace;
|
||||
--radius: 0.375rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 4px;
|
||||
--shadow-blur: 8px;
|
||||
--shadow-spread: -1px;
|
||||
--shadow-opacity: 0.1;
|
||||
--shadow-color: hsl(0 0% 0%);
|
||||
--shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
|
||||
--border: 217.8947 21.3483% 17.4510%;
|
||||
--input: 217.8947 21.3483% 17.4510%;
|
||||
--ring: 37.4286 91.3043% 54.9020%;
|
||||
--chart-1: 37.4286 91.3043% 54.9020%;
|
||||
--chart-2: 185.4545 59.3361% 52.7451%;
|
||||
--chart-3: 219.7674 40.9524% 41.1765%;
|
||||
--chart-4: 36.7839 84.6809% 46.0784%;
|
||||
--chart-5: 185.6934 61.9910% 43.3333%;
|
||||
--sidebar: 217.5000 26.6667% 5.8824%;
|
||||
--sidebar-foreground: 215 17.6471% 86.6667%;
|
||||
--sidebar-primary: 37.4286 91.3043% 54.9020%;
|
||||
--sidebar-primary-foreground: 217.5000 22.2222% 7.0588%;
|
||||
--sidebar-accent: 219.0909 41.7722% 30.9804%;
|
||||
--sidebar-accent-foreground: 185.1064 58.0247% 84.1176%;
|
||||
--sidebar-border: 217.8947 21.3483% 17.4510%;
|
||||
--sidebar-ring: 37.4286 91.3043% 54.9020%;
|
||||
|
||||
--brand-ink: 217.5000 22.2222% 7.0588%;
|
||||
--brand-ink-raised: 216.9231 23.6364% 10.7843%;
|
||||
--brand-navy: 218.0488 42.2680% 19.0196%;
|
||||
--brand-navy-light: 219.7674 40.9524% 41.1765%;
|
||||
--brand-cyan: 185.4545 59.3361% 52.7451%;
|
||||
--brand-amber: 37.4286 91.3043% 54.9020%;
|
||||
|
||||
--font-sans: 'Inter Variable', Inter, sans-serif;
|
||||
--font-display: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, monospace;
|
||||
--font-mono: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-color: hsl(217.5 22% 7% / 1);
|
||||
--shadow-2xs: 0px 2px 4px -1px hsl(217.5 22% 3% / 0.20);
|
||||
--shadow-xs: 0px 2px 4px -1px hsl(217.5 22% 3% / 0.24);
|
||||
--shadow-sm: 0px 4px 8px -2px hsl(217.5 22% 3% / 0.32), 0px 1px 2px -2px hsl(217.5 22% 3% / 0.24);
|
||||
--shadow: 0px 4px 8px -2px hsl(217.5 22% 3% / 0.32), 0px 1px 2px -2px hsl(217.5 22% 3% / 0.24);
|
||||
--shadow-md: 0px 6px 12px -2px hsl(217.5 22% 3% / 0.36), 0px 2px 4px -2px hsl(217.5 22% 3% / 0.28);
|
||||
--shadow-lg: 0px 10px 20px -4px hsl(217.5 22% 3% / 0.40), 0px 4px 6px -2px hsl(217.5 22% 3% / 0.28);
|
||||
--shadow-xl: 0px 16px 28px -6px hsl(217.5 22% 3% / 0.48), 0px 8px 10px -4px hsl(217.5 22% 3% / 0.32);
|
||||
--shadow-2xl: 0px 24px 40px -8px hsl(217.5 22% 3% / 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,5 +141,47 @@
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: 'cv11', 'ss01';
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: hsl(var(--brand-amber) / 0.28);
|
||||
}
|
||||
}
|
||||
|
||||
/* Signature "layer stack" treatment: two offset hairline borders instead of
|
||||
a soft drop shadow, evoking stacked strata / relay layers. Used on cards
|
||||
that should read as part of the brand rather than generic chrome. */
|
||||
@layer components {
|
||||
.layer-card {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.layer-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
border: 1px solid hsl(var(--border));
|
||||
transform: translate(6px, 6px);
|
||||
z-index: -1;
|
||||
transition: transform 200ms ease, opacity 200ms ease;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.layer-card:hover::before {
|
||||
transform: translate(9px, 9px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,10 @@ import { createRoot } from 'react-dom/client';
|
||||
// Import polyfills first
|
||||
import './lib/polyfills.ts';
|
||||
|
||||
// Brand fonts: Inter for body copy, JetBrains Mono as the display/data face
|
||||
import '@fontsource-variable/inter';
|
||||
import '@fontsource-variable/jetbrains-mono';
|
||||
|
||||
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
@@ -26,7 +26,7 @@ export function Dashboard() {
|
||||
<main className="flex-1 min-w-0">
|
||||
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b bg-background px-4 lg:h-[60px] lg:px-6">
|
||||
<SidebarTrigger />
|
||||
<h1 className="text-lg font-semibold md:text-xl truncate">Dashboard</h1>
|
||||
<h1 className="font-display text-lg font-semibold md:text-xl truncate">Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-6 p-4 md:p-6 lg:p-8 overflow-x-hidden">
|
||||
|
||||
@@ -16,7 +16,7 @@ export function DashboardEvents() {
|
||||
<main className="flex-1 min-w-0">
|
||||
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b bg-background px-4 lg:h-[60px] lg:px-6">
|
||||
<SidebarTrigger />
|
||||
<h1 className="text-lg font-semibold md:text-xl truncate">My Events</h1>
|
||||
<h1 className="font-display text-lg font-semibold md:text-xl truncate">My Events</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-6 p-4 md:p-6 lg:p-8 overflow-x-hidden">
|
||||
|
||||
@@ -94,7 +94,7 @@ export function DashboardExport() {
|
||||
<main className="flex-1 min-w-0">
|
||||
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b bg-background px-4 lg:h-[60px] lg:px-6">
|
||||
<SidebarTrigger />
|
||||
<h1 className="text-lg font-semibold md:text-xl truncate">Export Following List</h1>
|
||||
<h1 className="font-display text-lg font-semibold md:text-xl truncate">Export Following List</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-6 p-4 md:p-6 lg:p-8 overflow-x-hidden">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import type { NostrEvent, NostrMetadata } from '@nostrify/nostrify';
|
||||
import { useExploreEvents } from '@/hooks/useExploreEvents';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
@@ -9,6 +10,8 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { NoteContent } from '@/components/NoteContent';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
|
||||
import { AppSidebar } from '@/components/navigation/AppSidebar';
|
||||
|
||||
function TextNoteCard({ event }: { event: NostrEvent }) {
|
||||
const author = useAuthor(event.pubkey);
|
||||
@@ -160,18 +163,25 @@ export function Explore() {
|
||||
const [activeTab, setActiveTab] = useState('notes');
|
||||
const { data, isLoading, isError } = useExploreEvents();
|
||||
|
||||
useSeoMeta({
|
||||
title: 'Explore — LAYER.systems',
|
||||
description: 'The latest text notes and profiles passing through the LAYER.systems relay.',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-background via-background to-muted/20">
|
||||
<div className="container max-w-4xl mx-auto px-4 py-8 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-4xl sm:text-5xl font-bold bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
|
||||
Explore
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm sm:text-base">
|
||||
Discover the latest text notes and profiles from the Nostr
|
||||
</p>
|
||||
</div>
|
||||
<SidebarProvider>
|
||||
<div className="flex min-h-screen w-full overflow-x-hidden">
|
||||
<AppSidebar />
|
||||
<main className="flex-1 min-w-0">
|
||||
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b bg-background px-4 lg:h-[60px] lg:px-6">
|
||||
<SidebarTrigger />
|
||||
<h1 className="font-display text-lg font-semibold md:text-xl truncate">Explore</h1>
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto px-4 py-8 space-y-6">
|
||||
<p className="text-muted-foreground text-sm sm:text-base">
|
||||
The latest text notes and profiles passing through this relay.
|
||||
</p>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
@@ -264,7 +274,9 @@ export function Explore() {
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,240 +1,181 @@
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { CheckCircle2, Copy, Server, Gift, Users, Globe } from 'lucide-react';
|
||||
import { CheckCircle2, Copy, Gift, Users, Globe, ShieldCheck } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { LoginArea } from '@/components/auth/LoginArea';
|
||||
import { SiteHeader } from '@/components/brand/SiteHeader';
|
||||
import { SiteFooter } from '@/components/brand/SiteFooter';
|
||||
import { LayerStack } from '@/components/brand/LayerStack';
|
||||
import { LayerDivider } from '@/components/brand/LayerDivider';
|
||||
|
||||
const relayUrl = 'wss://relay.layer.systems';
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: Gift,
|
||||
title: 'No cost, no catch',
|
||||
description: 'Publishing and reading are free. No paywalled feeds, no premium relay tier.',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: 'Run for the community',
|
||||
description: 'Kept online by people who use Nostr daily, not a company monetizing your notes.',
|
||||
},
|
||||
{
|
||||
icon: Globe,
|
||||
title: 'Speaks to every client',
|
||||
description: 'Standard NIP-01 relay behavior — works with Damus, Amethyst, Snort, and the rest.',
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: 'Yours to inspect',
|
||||
description: 'Your events, your keys. Browse everything you’ve published from your own dashboard.',
|
||||
},
|
||||
];
|
||||
|
||||
const steps = [
|
||||
{
|
||||
step: '01',
|
||||
title: 'Choose your client',
|
||||
body: 'Pick a Nostr client — Damus, Amethyst, Snort, or any other app that speaks the protocol.',
|
||||
},
|
||||
{
|
||||
step: '02',
|
||||
title: 'Add the relay',
|
||||
body: 'In your client’s relay settings, add the URL below to your read/write relay list.',
|
||||
},
|
||||
{
|
||||
step: '03',
|
||||
title: 'Post through it',
|
||||
body: 'That’s it. Notes you publish now pass through LAYER.systems on their way to the network.',
|
||||
},
|
||||
];
|
||||
|
||||
const Index = () => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const relayUrl = 'wss://relay.layer.systems';
|
||||
|
||||
useSeoMeta({
|
||||
title: 'LAYER.systems - Public Nostr Relay',
|
||||
description: 'A fast, reliable, and open Nostr relay serving the decentralized social network.',
|
||||
title: 'LAYER.systems — Public Nostr Relay',
|
||||
description: 'A free, community-run Nostr relay. Add wss://relay.layer.systems to any client to connect to the decentralized social network.',
|
||||
});
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(relayUrl);
|
||||
setCopied(true);
|
||||
toast({
|
||||
title: 'Copied!',
|
||||
description: 'Relay URL copied to clipboard',
|
||||
});
|
||||
toast({ title: 'Copied', description: 'Relay URL copied to clipboard' });
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
toast({
|
||||
title: 'Failed to copy',
|
||||
description: 'Please copy the URL manually',
|
||||
title: 'Couldn’t copy automatically',
|
||||
description: 'Select and copy the URL manually.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: Gift,
|
||||
title: 'Free',
|
||||
description: 'No cost to use - accessible for everyone',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: 'Community driven',
|
||||
description: 'Built and maintained by the Nostr community',
|
||||
},
|
||||
{
|
||||
icon: Globe,
|
||||
title: 'Global Network',
|
||||
description: 'Part of the decentralized Nostr ecosystem',
|
||||
},
|
||||
{
|
||||
icon: Server,
|
||||
title: 'Open Access',
|
||||
description: 'Free to use for all Nostr clients',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-background via-background to-muted/20">
|
||||
{/* Header */}
|
||||
<header className="absolute top-0 left-0 right-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex justify-end">
|
||||
<LoginArea className="max-w-60" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Hero — the layer stack, lit on dark ink */}
|
||||
<div className="relative overflow-hidden bg-brand-ink">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-40"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'radial-gradient(600px circle at 15% 20%, hsl(var(--brand-amber) / 0.12), transparent 60%), radial-gradient(500px circle at 85% 60%, hsl(var(--brand-cyan) / 0.10), transparent 60%)',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<SiteHeader transparent />
|
||||
|
||||
{/* Hero Section */}
|
||||
<div className="relative overflow-hidden">
|
||||
{/* Animated Background Elements */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-1/4 -left-20 w-96 h-96 bg-primary/5 rounded-full blur-3xl animate-pulse" />
|
||||
<div className="absolute bottom-1/4 -right-20 w-96 h-96 bg-primary/5 rounded-full blur-3xl animate-pulse delay-1000" />
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-16 sm:pt-32 sm:pb-24">
|
||||
{/* Status Badge */}
|
||||
{/* <div className="flex justify-center mb-8">
|
||||
<Badge variant="outline" className="px-4 py-2 text-sm font-medium gap-2 border-primary/20 bg-primary/5">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
Relay Online
|
||||
</Badge>
|
||||
</div> */}
|
||||
|
||||
{/* Main Heading */}
|
||||
<div className="text-center space-y-6 mb-12">
|
||||
<h1 className="text-5xl sm:text-7xl font-bold tracking-tight">
|
||||
<span className="bg-gradient-to-r from-primary via-primary to-primary/60 bg-clip-text text-transparent">
|
||||
LAYER.systems
|
||||
</span>
|
||||
<div className="relative mx-auto grid max-w-6xl gap-16 px-6 pb-24 pt-28 sm:pb-32 sm:pt-36 lg:grid-cols-[1.1fr_0.9fr] lg:items-center">
|
||||
<div className="motion-safe:animate-fade-up">
|
||||
<p className="eyebrow text-brand-amber">Public Nostr relay</p>
|
||||
<h1 className="mt-4 text-balance font-display text-4xl font-bold leading-[1.05] tracking-tight text-white sm:text-5xl lg:text-6xl">
|
||||
One relay in the stack between you and the network.
|
||||
</h1>
|
||||
<p className="text-xl sm:text-2xl text-muted-foreground max-w-2xl mx-auto font-light">
|
||||
Your gateway to the decentralized social network
|
||||
<p className="mt-6 max-w-lg text-lg text-white/60">
|
||||
LAYER.systems stores and forwards the events your client sends it — free, open,
|
||||
and built for the people actually using Nostr.
|
||||
</p>
|
||||
<p className="text-base sm:text-lg text-muted-foreground/80 max-w-xl mx-auto">
|
||||
A fast, reliable, and open Nostr relay connecting you to the future of social media
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Relay URL Card */}
|
||||
<div className="max-w-2xl mx-auto mb-16">
|
||||
<Card className="border-2 border-primary/20 shadow-2xl shadow-primary/5 backdrop-blur-sm bg-card/95">
|
||||
<CardContent className="p-8">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="sm:flex-1 min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-2">Relay URL</p>
|
||||
<code className="text-lg sm:text-xl font-mono text-primary break-all">
|
||||
{relayUrl}
|
||||
</code>
|
||||
</div>
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={copyToClipboard}
|
||||
className="w-full sm:w-auto sm:shrink-0 gap-2 hover:scale-105 transition-transform"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckCircle2 className="w-5 h-5" />
|
||||
Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-5 h-5" />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Add this URL to your Nostr client to connect to LAYER.systems
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-center mb-12">Why Choose LAYER.systems?</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{features.map((feature, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className="group hover:border-primary/40 transition-all duration-300 hover:shadow-lg hover:shadow-primary/5 hover:-translate-y-1"
|
||||
>
|
||||
<CardContent className="p-6 space-y-4">
|
||||
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
|
||||
<feature.icon className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">{feature.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{feature.description}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
<div className="mt-10 max-w-lg rounded-lg border border-white/10 bg-white/[0.04] p-5 backdrop-blur-sm">
|
||||
<p className="eyebrow text-white/40">Relay URL</p>
|
||||
<div className="mt-2 flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<code className="min-w-0 flex-1 truncate font-display text-base text-brand-amber sm:text-lg">
|
||||
{relayUrl}
|
||||
</code>
|
||||
<Button onClick={copyToClipboard} className="shrink-0 gap-2">
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckCircle2 className="h-4 w-4" /> Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4" /> Copy
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center lg:justify-end">
|
||||
<LayerStack />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* How to Connect Section */}
|
||||
<div className="border-t border-border/40">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-24">
|
||||
<h2 className="text-3xl font-bold text-center mb-12">Getting Started</h2>
|
||||
<div className="space-y-8">
|
||||
<Card>
|
||||
<CardContent className="p-8">
|
||||
<div className="space-y-6">
|
||||
<div className="flex gap-4">
|
||||
<div className="shrink-0 w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-semibold">
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Choose Your Client</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Pick a Nostr client like Damus, Amethyst, Snort, or any other compatible application
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="shrink-0 w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-semibold">
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Add the Relay</h3>
|
||||
<p className="text-muted-foreground">
|
||||
In your client settings, add <code className="px-2 py-1 bg-muted rounded text-sm font-mono">{relayUrl}</code> to your relay list
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="shrink-0 w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-semibold">
|
||||
3
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Start Connecting</h3>
|
||||
<p className="text-muted-foreground">
|
||||
You're all set! Start posting, following, and connecting with the Nostr network
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{/* Why this relay */}
|
||||
<section className="mx-auto max-w-6xl px-6 py-20 sm:py-28">
|
||||
<div className="mx-auto max-w-xl text-center">
|
||||
<p className="eyebrow text-primary">Why this relay</p>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight sm:text-4xl">
|
||||
Infrastructure, not a product to sell you
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-14 grid grid-cols-1 gap-x-8 gap-y-12 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{features.map((feature) => (
|
||||
<div key={feature.title} className="layer-card rounded-lg border border-border bg-card p-6">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-md bg-primary/10">
|
||||
<feature.icon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<h3 className="mt-4 font-display text-base font-semibold">{feature.title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{feature.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="flex justify-center py-4">
|
||||
<LayerDivider />
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t border-border/40 bg-muted/20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-center gap-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
© {new Date().getFullYear()} LAYER.systems. Powered by Nostr.
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Server className="w-4 h-4" />
|
||||
<span>Open and free for all</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<a href="/terms" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
Terms
|
||||
</a>
|
||||
<a href="/privacy" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
Privacy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/* How to connect — a genuine sequence, numbered accordingly */}
|
||||
<section className="border-t border-border bg-muted/20">
|
||||
<div className="mx-auto max-w-3xl px-6 py-20 sm:py-28">
|
||||
<div className="mx-auto max-w-xl text-center">
|
||||
<p className="eyebrow text-primary">Getting connected</p>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight sm:text-4xl">Three steps, in order</h2>
|
||||
</div>
|
||||
|
||||
<ol className="mt-14 space-y-10">
|
||||
{steps.map((item) => (
|
||||
<li key={item.step} className="flex gap-5">
|
||||
<span className="font-display text-sm font-semibold text-primary/70">{item.step}</span>
|
||||
<div>
|
||||
<h3 className="font-display text-base font-semibold">{item.title}</h3>
|
||||
<p className="mt-1.5 text-muted-foreground">{item.body}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
<SiteFooter />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { DMMessagingInterface } from '@/components/dm/DMMessagingInterface';
|
||||
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
|
||||
import { AppSidebar } from '@/components/navigation/AppSidebar';
|
||||
|
||||
const Messages = () => {
|
||||
useSeoMeta({
|
||||
title: 'Messages',
|
||||
description: 'Private encrypted messaging on Nostr',
|
||||
title: 'Messages — LAYER.systems',
|
||||
description: 'Private, end-to-end encrypted messaging on Nostr.',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="container mx-auto p-4 h-screen flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-2xl font-semibold">Messages</h1>
|
||||
</div>
|
||||
<SidebarProvider>
|
||||
<div className="flex h-screen w-full overflow-hidden">
|
||||
<AppSidebar />
|
||||
<main className="flex-1 min-w-0 flex flex-col">
|
||||
<div className="shrink-0 flex h-14 items-center gap-4 border-b bg-background px-4 lg:h-[60px] lg:px-6">
|
||||
<SidebarTrigger />
|
||||
<h1 className="font-display text-lg font-semibold md:text-xl truncate">Messages</h1>
|
||||
</div>
|
||||
|
||||
<DMMessagingInterface className="flex-1" />
|
||||
<div className="flex-1 min-h-0 p-4">
|
||||
<DMMessagingInterface className="h-full" />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { useSeoMeta } from "@unhead/react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useLocation, Link } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Wordmark } from "@/components/brand/Wordmark";
|
||||
|
||||
const NotFound = () => {
|
||||
const location = useLocation();
|
||||
|
||||
useSeoMeta({
|
||||
title: "404 - Page Not Found",
|
||||
description: "The page you are looking for could not be found. Return to the home page to continue browsing.",
|
||||
title: "404 — LAYER.systems",
|
||||
description: "This page doesn't exist. Return to the home page to keep browsing.",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -18,13 +20,19 @@ const NotFound = () => {
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-gray-100">404</h1>
|
||||
<p className="text-xl text-gray-600 dark:text-gray-400 mb-4">Oops! Page not found</p>
|
||||
<a href="/" className="text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline">
|
||||
Return to Home
|
||||
</a>
|
||||
<div className="flex min-h-screen items-center justify-center bg-background px-6">
|
||||
<div className="max-w-sm text-center">
|
||||
<p className="eyebrow text-primary">No signal</p>
|
||||
<p className="mt-3 font-display text-6xl font-bold tracking-tight">404</p>
|
||||
<p className="mt-3 text-muted-foreground">
|
||||
There's no layer at <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-sm">{location.pathname}</code>.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-col items-center gap-6">
|
||||
<Button asChild>
|
||||
<Link to="/">Back to LAYER.systems</Link>
|
||||
</Button>
|
||||
<Wordmark className="text-sm opacity-60" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Shield } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { SiteHeader } from '@/components/brand/SiteHeader';
|
||||
import { SiteFooter } from '@/components/brand/SiteFooter';
|
||||
|
||||
export function Privacy() {
|
||||
useSeoMeta({
|
||||
title: 'Privacy Policy - LAYER.systems',
|
||||
description: 'Privacy Policy for LAYER.systems Nostr relay',
|
||||
title: 'Privacy Policy — LAYER.systems',
|
||||
description: 'Privacy Policy for the LAYER.systems Nostr relay.',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-background via-background to-muted/20">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<Link to="/" className="inline-flex items-center gap-2 text-xl font-bold hover:opacity-80 transition-opacity">
|
||||
<Shield className="h-6 w-6" />
|
||||
<span>LAYER.systems</span>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
<div className="min-h-screen bg-background">
|
||||
<SiteHeader />
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="container mx-auto px-4 py-12 max-w-4xl">
|
||||
@@ -29,6 +22,7 @@ export function Privacy() {
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-4">
|
||||
<Shield className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<p className="eyebrow text-primary">Legal</p>
|
||||
<h1 className="text-4xl font-bold tracking-tight">Privacy Policy</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Last updated: December 27, 2025
|
||||
@@ -258,19 +252,9 @@ export function Privacy() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Footer Navigation */}
|
||||
<div className="mt-12 pt-8 border-t text-center space-y-4">
|
||||
<div className="flex justify-center gap-6 text-sm">
|
||||
<Link to="/" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
Home
|
||||
</Link>
|
||||
<Link to="/terms" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
Terms of Service
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<SiteFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
215
src/pages/Styleguide.tsx
Normal file
215
src/pages/Styleguide.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { SiteHeader } from '@/components/brand/SiteHeader';
|
||||
import { SiteFooter } from '@/components/brand/SiteFooter';
|
||||
import { LayerStack } from '@/components/brand/LayerStack';
|
||||
import { LayerDivider } from '@/components/brand/LayerDivider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface Swatch {
|
||||
name: string;
|
||||
token: string;
|
||||
hex: string;
|
||||
usage: string;
|
||||
dark?: boolean;
|
||||
}
|
||||
|
||||
const colors: Swatch[] = [
|
||||
{ name: 'Paper', token: 'background', hex: '#F5F6F5', usage: 'Default light-mode surface' },
|
||||
{ name: 'Ink', token: 'brand-ink', hex: '#0E1116', usage: 'Hero panel + dark-mode surface', dark: true },
|
||||
{ name: 'Relay Amber', token: 'primary', hex: '#F5A623', usage: 'Signature accent, primary actions, the "signal"' },
|
||||
{ name: 'Deep Signal Navy', token: 'brand-navy', hex: '#1C2B45', usage: 'Structural color — layer bands, dark accents', dark: true },
|
||||
{ name: 'Circuit Cyan', token: 'brand-cyan', hex: '#3FC1CE', usage: 'Secondary data color for charts + hover accents' },
|
||||
{ name: 'Slate Ink', token: 'foreground', hex: '#2B3140', usage: 'Body text on light surfaces' },
|
||||
];
|
||||
|
||||
const spacingScale = [1, 2, 3, 4, 6, 8, 12, 16, 24];
|
||||
|
||||
function Section({
|
||||
eyebrow,
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<section className={cn('py-14 sm:py-16', className)}>
|
||||
<p className="eyebrow text-primary">{eyebrow}</p>
|
||||
<h2 className="mt-2 text-2xl font-bold tracking-tight sm:text-3xl">{title}</h2>
|
||||
<div className="mt-8">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function Styleguide() {
|
||||
useSeoMeta({
|
||||
title: 'Styleguide — LAYER.systems',
|
||||
description: 'The LAYER.systems design system: color, type, spacing, and components.',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<SiteHeader />
|
||||
|
||||
<main className="mx-auto max-w-5xl px-6">
|
||||
<div className="border-b border-border py-16 sm:py-20">
|
||||
<p className="eyebrow text-primary">Design system</p>
|
||||
<h1 className="mt-3 text-4xl font-bold tracking-tight sm:text-5xl">Styleguide</h1>
|
||||
<p className="mt-4 max-w-2xl text-lg text-muted-foreground">
|
||||
The visual language behind LAYER.systems: a relay is one layer between a client and
|
||||
the network, so the whole system is built around that stack — literally.
|
||||
The full rationale lives in{' '}
|
||||
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-sm">docs/DESIGN_STYLEGUIDE.md</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Signature */}
|
||||
<Section eyebrow="Signature" title="The layer stack">
|
||||
<p className="max-w-2xl text-muted-foreground">
|
||||
Every note travels Client → Relay → Network. Instead of a logo standing in for that,
|
||||
the brand mark is the stack itself — this relay lit up as the middle layer.
|
||||
</p>
|
||||
<div className="mt-8 rounded-xl border border-border bg-brand-ink p-10">
|
||||
<LayerStack className="mx-auto" />
|
||||
</div>
|
||||
<div className="mt-10 flex items-center gap-6">
|
||||
<LayerDivider />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
A quiet echo of the stack, used to break up sections without repeating the hero mark.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="h-px bg-border" />
|
||||
|
||||
{/* Color */}
|
||||
<Section eyebrow="Color" title="Palette">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{colors.map((c) => (
|
||||
<div key={c.name} className="overflow-hidden rounded-lg border border-border">
|
||||
<div
|
||||
className="h-20"
|
||||
style={{ backgroundColor: `hsl(var(--${c.token}))` }}
|
||||
/>
|
||||
<div className="space-y-1 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="font-display text-sm font-semibold">{c.name}</p>
|
||||
<span className="font-mono text-xs text-muted-foreground">{c.hex}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{c.usage}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="h-px bg-border" />
|
||||
|
||||
{/* Type */}
|
||||
<Section eyebrow="Type" title="Display, body, data">
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<p className="eyebrow text-muted-foreground">Display — JetBrains Mono, for headlines & data</p>
|
||||
<p className="mt-2 font-display text-5xl font-bold tracking-tight">Signal in, signal out.</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="eyebrow text-muted-foreground">Body — Inter, for everything you read</p>
|
||||
<p className="mt-2 max-w-xl text-lg text-muted-foreground">
|
||||
Body copy stays in a humanist sans so long-form reading — terms, docs, dashboard
|
||||
explanations — never feels like staring at a terminal.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="eyebrow text-muted-foreground">Mono in use — labels & identifiers</p>
|
||||
<p className="mt-2 font-mono text-sm text-foreground">
|
||||
wss://relay.layer.systems · kind:1 · npub1q4w0…f8x2
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="h-px bg-border" />
|
||||
|
||||
{/* Spacing */}
|
||||
<Section eyebrow="Spacing" title="4px base scale">
|
||||
<div className="space-y-2">
|
||||
{spacingScale.map((n) => (
|
||||
<div key={n} className="flex items-center gap-4">
|
||||
<span className="w-10 shrink-0 font-mono text-xs text-muted-foreground">{n * 4}px</span>
|
||||
<div className="h-3 rounded-sm bg-primary/70" style={{ width: `${n * 4}px` }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="h-px bg-border" />
|
||||
|
||||
{/* Components */}
|
||||
<Section eyebrow="Components" title="Buttons">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button>Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section eyebrow="Components" title="Badges">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Badge>Default</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
<Badge variant="destructive">Destructive</Badge>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section eyebrow="Components" title="Cards">
|
||||
<div className="grid gap-8 pb-4 sm:grid-cols-2">
|
||||
<div className="layer-card rounded-lg border border-border bg-card p-6">
|
||||
<p className="font-display text-sm font-semibold">Layer card</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Two offset hairline borders instead of a soft shadow — the site's default card
|
||||
treatment, echoing the stack motif.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-card p-6 shadow-sm">
|
||||
<p className="font-display text-sm font-semibold">Plain card</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Used inside dense data views (dashboard, explorer) where the layered border would
|
||||
add visual noise.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section eyebrow="Components" title="Form & feedback">
|
||||
<div className="max-w-md space-y-4">
|
||||
<Input placeholder="npub1…" />
|
||||
<Alert>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle>Heads up</AlertTitle>
|
||||
<AlertDescription>
|
||||
Alerts explain what happened and what to do next — never vague, never apologetic.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</Section>
|
||||
</main>
|
||||
|
||||
<SiteFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Styleguide;
|
||||
@@ -2,24 +2,18 @@ import { useSeoMeta } from '@unhead/react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { ScrollText } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { SiteHeader } from '@/components/brand/SiteHeader';
|
||||
import { SiteFooter } from '@/components/brand/SiteFooter';
|
||||
|
||||
export function Terms() {
|
||||
useSeoMeta({
|
||||
title: 'Terms of Service - LAYER.systems',
|
||||
description: 'Terms of Service for LAYER.systems Nostr relay',
|
||||
title: 'Terms of Service — LAYER.systems',
|
||||
description: 'Terms of Service for the LAYER.systems Nostr relay.',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-background via-background to-muted/20">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<Link to="/" className="inline-flex items-center gap-2 text-xl font-bold hover:opacity-80 transition-opacity">
|
||||
<ScrollText className="h-6 w-6" />
|
||||
<span>LAYER.systems</span>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
<div className="min-h-screen bg-background">
|
||||
<SiteHeader />
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="container mx-auto px-4 py-12 max-w-4xl">
|
||||
@@ -29,6 +23,7 @@ export function Terms() {
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-4">
|
||||
<ScrollText className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<p className="eyebrow text-primary">Legal</p>
|
||||
<h1 className="text-4xl font-bold tracking-tight">Terms of Service</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Last updated: December 27, 2025
|
||||
@@ -194,19 +189,9 @@ export function Terms() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Footer Navigation */}
|
||||
<div className="mt-12 pt-8 border-t text-center space-y-4">
|
||||
<div className="flex justify-center gap-6 text-sm">
|
||||
<Link to="/" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
Home
|
||||
</Link>
|
||||
<Link to="/privacy" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<SiteFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ export default {
|
||||
}
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['var(--font-sans)'],
|
||||
display: ['var(--font-display)'],
|
||||
mono: ['var(--font-mono)'],
|
||||
},
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
@@ -53,6 +58,14 @@ export default {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
},
|
||||
brand: {
|
||||
ink: 'hsl(var(--brand-ink))',
|
||||
'ink-raised': 'hsl(var(--brand-ink-raised))',
|
||||
navy: 'hsl(var(--brand-navy))',
|
||||
'navy-light': 'hsl(var(--brand-navy-light))',
|
||||
cyan: 'hsl(var(--brand-cyan))',
|
||||
amber: 'hsl(var(--brand-amber))'
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: 'hsl(var(--sidebar))',
|
||||
foreground: 'hsl(var(--sidebar-foreground))',
|
||||
@@ -85,11 +98,42 @@ export default {
|
||||
to: {
|
||||
height: '0'
|
||||
}
|
||||
},
|
||||
'layer-settle': {
|
||||
from: {
|
||||
transform: 'translateY(var(--settle-offset, -14px))',
|
||||
opacity: '0'
|
||||
},
|
||||
to: {
|
||||
transform: 'translateY(0)',
|
||||
opacity: '1'
|
||||
}
|
||||
},
|
||||
'signal-pulse': {
|
||||
'0%, 100%': {
|
||||
opacity: '0.5'
|
||||
},
|
||||
'50%': {
|
||||
opacity: '1'
|
||||
}
|
||||
},
|
||||
'fade-up': {
|
||||
from: {
|
||||
transform: 'translateY(10px)',
|
||||
opacity: '0'
|
||||
},
|
||||
to: {
|
||||
transform: 'translateY(0)',
|
||||
opacity: '1'
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
'layer-settle': 'layer-settle 0.7s cubic-bezier(0.16, 1, 0.3, 1) both',
|
||||
'signal-pulse': 'signal-pulse 2.4s ease-in-out infinite',
|
||||
'fade-up': 'fade-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) both'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user