add design system

This commit is contained in:
2026-02-26 02:12:02 +00:00
parent 79b4eb17e0
commit 09dffa626e
14 changed files with 207 additions and 72 deletions

128
design-system.md Normal file
View File

@@ -0,0 +1,128 @@
# Galdr Design System
## Brand direction
**Brand:** Galdr
**Meaning:** Norse incantation / spellcraft
**Positioning:** Professional customer-health intelligence with a dark, edgy, ritual-tech aesthetic.
This system blends:
- `26-industrial` for tactile product seriousness and mechanical confidence
- `19-minimal-dark` for premium nocturnal polish and readability
- `14-bold-typography` accents for controlled punk energy
## Experience principles
1. **Ritual over noise** — the UI should feel intentional and “cast,” never chaotic.
2. **Dark by default** — depth from layered charcoals, not pure black.
3. **Sharp confidence** — cards/buttons carry strong edges and subtle hard shadows.
4. **Professional legibility** — AA+ contrast and clean hierarchy at all breakpoints.
5. **Measured motion** — short, deliberate transitions (150250ms), no flashy gimmicks.
## Core design tokens
### Color tokens
- `--galdr-bg`: `#0b0b12`
- `--galdr-bg-elevated`: `#12121a`
- `--galdr-surface`: `#171724`
- `--galdr-surface-soft`: `#1f1f2e`
- `--galdr-border`: `#2d2d40`
- `--galdr-fg`: `#f2f2f8`
- `--galdr-fg-muted`: `#a8a8bc`
- `--galdr-accent`: `#8b5cf6` (arcane violet)
- `--galdr-accent-2`: `#22d3ee` (frost-cyan)
- `--galdr-danger`: `#f43f5e`
- `--galdr-success`: `#34d399`
### Typography
- **Display + UI:** `Space Grotesk`, `Inter`, `system-ui`, sans-serif
- **Technical labels:** `JetBrains Mono`, monospace
- Headings: weight `700`
- Body: weight `400500`
- Labels/chips: uppercase + tracking `0.08em`
### Shape and depth
- Radius: `10px`, `14px`, `18px`
- Border: 1px, high-contrast dark edge
- Shadow style:
- ambient: `0 10px 30px rgba(0,0,0,0.35)`
- accent glow: `0 0 0 1px rgba(139,92,246,0.35), 0 0 24px rgba(139,92,246,0.18)`
## Component language
### Buttons
- **Primary:** violet gradient fill, bright text, slight glow on hover
- **Secondary:** elevated dark surface with subtle border and hover brighten
- States:
- hover: `translateY(-1px)`
- active: `translateY(1px)` + reduced shadow
- focus-visible: 2px accent ring
### Cards / panels
- Elevated dark surfaces with a soft top highlight and hard-ish outer edge
- Optional “rune rail” accent (2px violet strip) for featured modules
### Inputs
- Dark recessed field
- Strong focus ring with accent and no outline suppression without replacement
### Status treatment
- healthy: emerald
- at-risk: amber/violet blend
- critical: rose
- Status should use icon + text, not color only.
## Layout and spacing
- Container max width: `1280px` (`max-w-7xl`)
- Section rhythm: `py-16` mobile, `py-24` desktop
- Grid spacing: `gap-4` to `gap-8`
- First paint pages to align first: Landing, Pricing, Auth, App shell
## Motion and interaction
- Default transition: `200ms ease`
- Elevation on hover should remain subtle (no dramatic scale)
- Respect `prefers-reduced-motion`
## Accessibility guardrails
- Minimum text contrast: 4.5:1
- Visible keyboard focus for all interactive controls
- Semantic elements over clickable `<div>` wrappers
- Touch targets >= 44px
## Implementation rollout
### Phase 1 (foundation)
1. Global tokens + typography in `web/src/index.css`
2. Global focus ring / motion-reduction defaults
3. Shared utility classes (`galdr-panel`, `galdr-card`, `galdr-button-*`)
### Phase 2 (brand surfaces)
1. Rebrand core UI strings to **Galdr**
2. Landing page + marketing sections refactor
3. Sidebar/app shell visual alignment
### Phase 3 (full convergence)
1. Auth + onboarding screens styling alignment
2. Pricing/SEO/legal pages finish
3. Chart color/token normalization and hardcoded color cleanup
## Acceptance criteria
- No new hardcoded one-off hex values in components where a token exists
- All primary CTAs use shared Galdr button patterns
- Landing, pricing, auth, and app shell all visually belong to one system
- Keyboard-only navigation has clear focus indicators on every route

View File

@@ -10,7 +10,9 @@ import AppLayout from "@/layouts/AppLayout";
const LandingPage = lazy(() => import("@/pages/LandingPage"));
const PricingPage = lazy(() => import("@/pages/PricingPage"));
const SeoHubPage = lazy(() => import("@/pages/seo/SeoHubPage"));
const SeoProgrammaticPage = lazy(() => import("@/pages/seo/SeoProgrammaticPage"));
const SeoProgrammaticPage = lazy(
() => import("@/pages/seo/SeoProgrammaticPage"),
);
const LoginPage = lazy(() => import("@/pages/auth/LoginPage"));
const RegisterPage = lazy(() => import("@/pages/auth/RegisterPage"));
const DashboardPage = lazy(() => import("@/pages/DashboardPage"));
@@ -42,7 +44,10 @@ function App() {
{/* Public marketing + auth routes */}
<Route path="/" element={<LandingPage />} />
<Route path="/pricing" element={<PricingPage />} />
<Route path="/templates" element={<SeoHubPage family="templates" />} />
<Route
path="/templates"
element={<SeoHubPage family="templates" />}
/>
<Route
path="/templates/:slug"
element={<SeoProgrammaticPage family="templates" />}
@@ -55,7 +60,10 @@ function App() {
path="/integrations/:slug"
element={<SeoProgrammaticPage family="integrations" />}
/>
<Route path="/for" element={<SeoHubPage family="personas" />} />
<Route
path="/for"
element={<SeoHubPage family="personas" />}
/>
<Route
path="/for/:slug"
element={<SeoProgrammaticPage family="personas" />}
@@ -84,7 +92,10 @@ function App() {
path="/examples/:slug"
element={<SeoProgrammaticPage family="examples" />}
/>
<Route path="/best" element={<SeoHubPage family="curation" />} />
<Route
path="/best"
element={<SeoHubPage family="curation" />}
/>
<Route
path="/best/:slug"
element={<SeoProgrammaticPage family="curation" />}

View File

@@ -181,9 +181,7 @@ export default function SubscriptionManager({
</section>
<section className="galdr-panel p-5">
<h4 className="text-sm font-semibold text-[var(--galdr-fg)]">
Usage
</h4>
<h4 className="text-sm font-semibold text-[var(--galdr-fg)]">Usage</h4>
<div className="mt-4 space-y-4">
{[
{

View File

@@ -41,10 +41,7 @@ const features = [
export default function FeaturesSection() {
return (
<section
id="features"
className="px-6 py-16 sm:px-10 lg:px-14 lg:py-24"
>
<section id="features" className="px-6 py-16 sm:px-10 lg:px-14 lg:py-24">
<div className="mx-auto max-w-7xl">
<div className="max-w-3xl">
<h2 className="text-3xl font-bold tracking-tight text-[var(--galdr-fg)] sm:text-4xl">

View File

@@ -115,17 +115,11 @@ export default function FooterSection() {
return (
<li key={link.label}>
{isExternal ? (
<a
href={link.href}
className="galdr-link"
>
<a href={link.href} className="galdr-link">
{link.label}
</a>
) : (
<Link
to={link.href}
className="galdr-link"
>
<Link to={link.href} className="galdr-link">
{link.label}
</Link>
)}

View File

@@ -69,10 +69,7 @@ export default function PricingSection({
}, []);
return (
<section
id="pricing"
className="px-6 py-16 sm:px-10 lg:px-14 lg:py-24"
>
<section id="pricing" className="px-6 py-16 sm:px-10 lg:px-14 lg:py-24">
<div className="mx-auto max-w-7xl">
{showStandaloneHeader && (
<div className="mb-8">

View File

@@ -61,10 +61,7 @@ export default function SocialProofSection() {
<div className="mt-10 grid grid-cols-1 gap-4 lg:grid-cols-3">
{metrics.map((metric) => (
<div
key={metric.label}
className="galdr-card p-6 text-center"
>
<div key={metric.label} className="galdr-card p-6 text-center">
<p className="text-3xl font-extrabold text-[var(--galdr-accent)]">
{metric.value}
</p>

View File

@@ -6,7 +6,9 @@ export default function NotFoundPage() {
<div className="galdr-shell flex min-h-screen items-center justify-center p-8">
<div className="galdr-card w-full max-w-xl p-8 text-center">
<FileQuestion className="mx-auto mb-6 h-16 w-16 text-[var(--galdr-fg-muted)]" />
<h1 className="text-3xl font-bold text-[var(--galdr-fg)]">Page not found</h1>
<h1 className="text-3xl font-bold text-[var(--galdr-fg)]">
Page not found
</h1>
<p className="mt-2 text-[var(--galdr-fg-muted)]">
The page you're looking for doesn't exist or has been moved.
</p>

View File

@@ -56,9 +56,7 @@ export default function SeoHubPage({ family }: SeoHubPageProps) {
<main className="mx-auto max-w-7xl">
<header className="galdr-card galdr-noise p-8">
<p className="galdr-kicker px-3 py-1">
{hub.label}
</p>
<p className="galdr-kicker px-3 py-1">{hub.label}</p>
<h1 className="mt-2 text-3xl font-extrabold tracking-tight sm:text-4xl">
{hub.title}
</h1>
@@ -72,9 +70,7 @@ export default function SeoHubPage({ family }: SeoHubPageProps) {
<span className="galdr-pill px-3 py-1">
Intent-driven templates
</span>
<span className="galdr-pill px-3 py-1">
Internal-link ready
</span>
<span className="galdr-pill px-3 py-1">Internal-link ready</span>
</div>
</header>

View File

@@ -115,7 +115,12 @@ function buildSections(page: SeoPage): {
3,
);
const evidenceArtifact = pickBySeed(
["renewal cohort outcomes", "risk-resolution lag", "expansion conversion", "retention trend deltas"],
[
"renewal cohort outcomes",
"risk-resolution lag",
"expansion conversion",
"retention trend deltas",
],
seed,
4,
);
@@ -201,7 +206,8 @@ function buildSections(page: SeoPage): {
"Ranking tools on popularity alone while ignoring migration and change-management overhead.";
break;
default:
familyOutcome = "Align teams on a single operating model for health scoring execution.";
familyOutcome =
"Align teams on a single operating model for health scoring execution.";
familyImplementation =
"Track one leading and one lagging indicator to validate intervention quality over time.";
familyPitfall =
@@ -212,12 +218,22 @@ function buildSections(page: SeoPage): {
return {
summary,
outcomes: [...baseOutcomes, familyOutcome],
implementation: [...baseImplementation, familyImplementation, `Keep ${evidenceArtifact} visible in your monthly review so the workflow stays outcome-driven.`],
pitfalls: [...basePitfalls, familyPitfall, `Ignoring ${context.hazard} controls turns scoring into noise and slows response quality.`],
implementation: [
...baseImplementation,
familyImplementation,
`Keep ${evidenceArtifact} visible in your monthly review so the workflow stays outcome-driven.`,
],
pitfalls: [
...basePitfalls,
familyPitfall,
`Ignoring ${context.hazard} controls turns scoring into noise and slows response quality.`,
],
};
}
export default function SeoProgrammaticPage({ family }: SeoProgrammaticPageProps) {
export default function SeoProgrammaticPage({
family,
}: SeoProgrammaticPageProps) {
const params = useParams<{ slug: string }>();
const slug = params.slug ?? "";
@@ -235,7 +251,8 @@ export default function SeoProgrammaticPage({ family }: SeoProgrammaticPageProps
/>
<h1 className="text-2xl font-bold">Resource not found</h1>
<p className="mt-2 text-sm text-[var(--galdr-fg-muted)]">
This page may have moved. Explore the full {hub.label.toLowerCase()} library instead.
This page may have moved. Explore the full {hub.label.toLowerCase()}{" "}
library instead.
</p>
<Link
to={hub.path}
@@ -303,10 +320,7 @@ export default function SeoProgrammaticPage({ family }: SeoProgrammaticPageProps
Home
</Link>
<span className="mx-2">/</span>
<Link
to={hub.path}
className="galdr-link"
>
<Link to={hub.path} className="galdr-link">
{hub.label}
</Link>
<span className="mx-2">/</span>
@@ -335,10 +349,7 @@ export default function SeoProgrammaticPage({ family }: SeoProgrammaticPageProps
<section className="mt-8 grid gap-4 md:grid-cols-3">
{sections.outcomes.map((outcome) => (
<article
key={outcome}
className="galdr-panel p-4 text-sm"
>
<article key={outcome} className="galdr-panel p-4 text-sm">
{outcome}
</article>
))}
@@ -367,7 +378,9 @@ export default function SeoProgrammaticPage({ family }: SeoProgrammaticPageProps
<section className="galdr-card mt-10 p-6">
<h2 className="text-xl font-semibold">Operational next step</h2>
<p className="mt-2 text-sm text-[var(--galdr-fg-muted)]">
Start with this workflow in a lightweight template, then connect live billing, CRM, and support signals in PulseScore to automate account prioritization.
Start with this workflow in a lightweight template, then connect
live billing, CRM, and support signals in PulseScore to automate
account prioritization.
</p>
<div className="mt-5 flex flex-wrap gap-3">
<Link

View File

@@ -100,15 +100,11 @@ function RuleForm({ onSave, onCancel, initial, saving }: RuleFormProps) {
});
}
const inputCls =
"galdr-input w-full px-3 py-2 text-sm";
const inputCls = "galdr-input w-full px-3 py-2 text-sm";
const labelCls = "block text-sm font-medium text-[var(--galdr-fg-muted)]";
return (
<form
onSubmit={handleSubmit}
className="galdr-panel space-y-4 p-4"
>
<form onSubmit={handleSubmit} className="galdr-panel space-y-4 p-4">
<div>
<label className={labelCls}>Name</label>
<input
@@ -317,10 +313,7 @@ export default function AlertsTab() {
{ label: "Failed", value: stats.failed ?? 0 },
{ label: "Pending", value: stats.pending ?? 0 },
].map((s) => (
<div
key={s.label}
className="galdr-panel p-4"
>
<div key={s.label} className="galdr-panel p-4">
<p className="text-xs font-medium text-[var(--galdr-fg-muted)]">
{s.label}
</p>
@@ -369,10 +362,7 @@ export default function AlertsTab() {
) : (
<div className="space-y-3">
{rules.map((rule) => (
<div
key={rule.id}
className="galdr-panel"
>
<div key={rule.id} className="galdr-panel">
<div className="flex items-center justify-between p-4">
<div className="flex-1">
<div className="flex items-center gap-2">

View File

@@ -53,9 +53,11 @@ export default function HubSpotCallbackPage() {
<div className="galdr-card p-6 text-center">
<div className="galdr-alert-danger p-6">
<h2 className="text-lg font-semibold text-[var(--galdr-fg)]">
Connection Failed
Connection Failed
</h2>
<p className="mt-2 text-sm text-[var(--galdr-fg-muted)]">{error}</p>
<p className="mt-2 text-sm text-[var(--galdr-fg-muted)]">
{error}
</p>
<button
onClick={() => navigate("/settings")}
className="galdr-button-primary mt-4 px-4 py-2 text-sm font-medium"
@@ -73,7 +75,9 @@ export default function HubSpotCallbackPage() {
<BaseLayout>
<div className="mx-auto max-w-md">
<div className="galdr-card p-6 text-center">
<p className="text-sm text-[var(--galdr-fg-muted)]">Connecting HubSpot...</p>
<p className="text-sm text-[var(--galdr-fg-muted)]">
Connecting HubSpot...
</p>
</div>
</div>
</BaseLayout>

View File

@@ -53,9 +53,11 @@ export default function IntercomCallbackPage() {
<div className="galdr-card p-6 text-center">
<div className="galdr-alert-danger p-6">
<h2 className="text-lg font-semibold text-[var(--galdr-fg)]">
Connection Failed
Connection Failed
</h2>
<p className="mt-2 text-sm text-[var(--galdr-fg-muted)]">{error}</p>
<p className="mt-2 text-sm text-[var(--galdr-fg-muted)]">
{error}
</p>
<button
onClick={() => navigate("/settings/integrations")}
className="galdr-button-primary mt-4 px-4 py-2 text-sm font-medium"
@@ -73,7 +75,9 @@ export default function IntercomCallbackPage() {
<BaseLayout>
<div className="mx-auto max-w-md">
<div className="galdr-card p-6 text-center">
<p className="text-sm text-[var(--galdr-fg-muted)]">Connecting Intercom...</p>
<p className="text-sm text-[var(--galdr-fg-muted)]">
Connecting Intercom...
</p>
</div>
</div>
</BaseLayout>

View File

@@ -53,9 +53,11 @@ export default function StripeCallbackPage() {
<div className="galdr-card p-6 text-center">
<div className="galdr-alert-danger p-6">
<h2 className="text-lg font-semibold text-[var(--galdr-fg)]">
Connection Failed
Connection Failed
</h2>
<p className="mt-2 text-sm text-[var(--galdr-fg-muted)]">{error}</p>
<p className="mt-2 text-sm text-[var(--galdr-fg-muted)]">
{error}
</p>
<button
onClick={() => navigate("/settings")}
className="galdr-button-primary mt-4 px-4 py-2 text-sm font-medium"
@@ -73,7 +75,9 @@ export default function StripeCallbackPage() {
<BaseLayout>
<div className="mx-auto max-w-md">
<div className="galdr-card p-6 text-center">
<p className="text-sm text-[var(--galdr-fg-muted)]">Connecting Stripe...</p>
<p className="text-sm text-[var(--galdr-fg-muted)]">
Connecting Stripe...
</p>
</div>
</div>
</BaseLayout>