add design system
This commit is contained in:
128
design-system.md
Normal file
128
design-system.md
Normal 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 (150–250ms), 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 `400–500`
|
||||
- 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
|
||||
@@ -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" />}
|
||||
|
||||
@@ -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">
|
||||
{[
|
||||
{
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user