From d96058727bc6b4b1c4e9e86ad073514bb90bf278 Mon Sep 17 00:00:00 2001 From: Nathan Eckenrode Date: Sun, 17 May 2026 17:49:35 -0400 Subject: [PATCH 1/2] feat(design-systems): add DragonPunk Noir + DragonPunk Solar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new design systems based on Nathan's existing DPN.md / DPS.md style guides (canonical at ~/Documents/DPN.md and ~/Documents/DPS.md). They codify a single design language with a dark face (Noir) and a light face (Solar), originally authored for AI image generation and now extended into UI tokens via the typographic-roles addition from 2026-04-27. DragonPunk Noir (DPN) — dark-by-default: - Canvas: Void Black #0A0A0F (warmest possible black, never #000) - Ink: Dragon Gold #FFB800 — load-bearing read color at ~12:1 on void - Accent + Focus glow: Neon Cyan #00F5FF — single token (~110:1 on void lets it serve both readable-accent and focus-glow roles) - Neon family: Magenta #FF2D95, Purple #9D00FF, Toxic Green #39FF14 - Warm spike: Ember #FF6B35 (warn), Crimson #8B0000 (deep danger), Bronze #CD7F32 (stale/disabled) - Plan 9 / Acme / NeXTSTEP lineage: square corners, no shadows, no blur, state-change motion only DragonPunk Solar (DPS) — light-face twin: - Canvas: Solar White #FFF8E1 (cream warmed toward gold, never sterile) - Ink: Bole #3D1208 — Renaissance gilders' red-clay underground at ~13:1 - Readable accent: Rubric #B82F08 — manuscript-red, ~5.5:1 - Focus glow only: Plasma Cyan #00D4FF (cyan-on-cream collapses to ~1.7:1, so DPS splits the role into Rubric + Cyan rather than collapsing into one token like DPN does) - Dark-mode override flips DPS to DPN's full token set (same brand, light face flips to dark face — not duplicated values per Lens B) Both files satisfy the 9-section schema (Visual Theme / Color / Typography / Spacing / Layout / Components / Motion / Voice / Anti-patterns), include real CSS in :root blocks, use the [data-theme="dark"] override pattern, and target prefers-reduced-motion to specific selectors rather than global *. Substantive anti-patterns named (no rounded corners, no drop shadows, no ambient idle animation, no centered prose, no cyan-as-text on cream, etc.). Prior art names real specifics: Plan 9, Acme, NeXTSTEP, Blade Runner 2049, Renaissance manuscript rubrication, the Mixxx LateNight skins, dpn-void.css / dps-solar.css. Category: Bold & Expressive for both. Co-Authored-By: Claude Opus 4.7 (1M context) --- design-systems/dragonpunk-noir/DESIGN.md | 380 ++++++++++++++++++++ design-systems/dragonpunk-solar/DESIGN.md | 400 ++++++++++++++++++++++ 2 files changed, 780 insertions(+) create mode 100644 design-systems/dragonpunk-noir/DESIGN.md create mode 100644 design-systems/dragonpunk-solar/DESIGN.md diff --git a/design-systems/dragonpunk-noir/DESIGN.md b/design-systems/dragonpunk-noir/DESIGN.md new file mode 100644 index 0000000000..89614c841f --- /dev/null +++ b/design-systems/dragonpunk-noir/DESIGN.md @@ -0,0 +1,380 @@ +# Design System Inspired by DragonPunk Noir + +> Category: Bold & Expressive +> Noir cinematography meets cyberpunk neon and high-fantasy mythos — the dark face of the Dragonpunk world. + +## 1. Visual Theme & Atmosphere + +DragonPunk Noir (DPN) is film noir wearing fiber-optic veins. The default surface is **void-black** — not a pure `#000` but `#0A0A0F`, the warmest possible black, the color of a wet stone street at 3 AM. Against that void, the entire experience is a **70/20/10 budget**: 70% deep tones do the structural work, 20% neon does the signaling, 10% warm fantasy-accent breaks the cold. Nothing here is saturated by default; saturation is *event*. + +The atmospheric law is **operational dashboard, not arcade marquee**. Think reactor control room: a calm dark canvas where 95% of pixels do not move, do not glow, do not shimmer — and a small minority of pixels carry meaning by virtue of their flare. Cyan glow under a focus ring is data. Gold ink in a heading is fact. Magenta pulse on a warning chip is *now*. The same surface that looks meditative in idle state lights up *only* when something has changed. + +Typographically, DPN is **Plan 9 / Acme / NeXTSTEP** in its bones — fixed-width where data lives, generous line-height, square corners, no rounded box-drawing, no shadows, no blur, no animations except where state is changing. The DragonPunk part shows up in the chrome: dragon-gold ink and neon-cyan accent that say "this is a world where ancient magic compiles to fiber-optic spell-code." + +**Key Characteristics:** +- Void canvas (`#0A0A0F`) — warmest possible black, never `#000` +- Dragon-gold ink (`#FFB800`) as the load-bearing reading color (~12:1 contrast on void) +- Cyan-on-void (`#00F5FF`) collapses readable-accent and focus-glow into one token (~110:1 contrast headroom) +- Square corners exclusively — no border-radius anywhere except where a circle is the *point* (avatars, dots) +- No drop shadows. No blur. No glass. No skeumorphism. Plan 9 / Acme lineage +- Neon flares treated as instrument-panel LEDs: present, signal-carrying, never decorative +- Motion is minimal and reserved for state transitions; no ambient animation, no parallax +- The aesthetic implies a *lived-in* world with 10,500 years of layered history — not a tech demo + +## 2. Color + +The palette is organized by **function-budget**: void family does structure, neon family does signal, warm family does the fantasy-accent break. Every token is wired into `:root` and `[data-theme="dark"]` overrides. + +### Primary — Noir Foundation + +- **Void Black** (`#0A0A0F`): Primary canvas. Not pure black; carries a barely-perceptible blue undertone that reads as "stone at night" rather than "OLED off." Default page background, default container background. +- **Smoke Grey** (`#2D2D3A`): Elevated surfaces, panel separators, fog/midtone shadows. The "raised" layer above void. +- **Bone White** (`#E8E4DC`): Highlights, fog particles, dragon-bone, eyes-in-darkness. Reserved for moments demanding maximum lightness against void. + +### Neon — Cyberpunk Injection + +- **Neon Cyan** (`#00F5FF`): The single load-bearing accent. Links, focus rings, active states, hover decoration. On void, ~110:1 contrast — high enough that one color serves both *readable-accent* and *focus-glow* roles. Use generously where status matters; never as ambient decoration. +- **Magenta Pulse** (`#FF2D95`): Alert / now / urgent state. Reserved for transient signal — never for surface fill. +- **Electric Purple** (`#9D00FF`): Magic / arcane / high-tier system elements. Distinct from magenta by being deeper and more saturated; reads as "spell" where magenta reads as "alarm." +- **Toxic Green** (`#39FF14`): Data streams, system internals, "the machine is working" indicators. Terminal-flavored; reads as raw protocol. + +### Warm — Fantasy Accent + +- **Dragon Gold** (`#FFB800`): **Typographic ink role.** This is the load-bearing reading color, the typographic pencil. Body text, headings — gold-on-void clears ~12:1 contrast. Also: treasure, flame, power, dragon eyes in non-typographic use. +- **Ember Orange** (`#FF6B35`): Warning role. The warm spike at the limit — context-depth indicators (cyan→ember gradient in ChatHUD), error states, "approaching threshold." +- **Blood Crimson** (`#8B0000`): Danger / violence / old magic. Deeper than ember; reads as "consequence" where ember reads as "warn." Reserved for destructive-action confirmations. +- **Ancient Bronze** (`#CD7F32`): Aged metal, artifacts, history. Used for inactive/disabled states of warm accents — what gold turns into when the data is stale. + +### CSS tokens + +```css +:root { + /* Surface */ + --color-canvas: #0A0A0F; + --color-surface: #2D2D3A; + --color-surface-high: #3D3D4A; + --color-on-surface: #E8E4DC; + + /* Typographic roles */ + --color-ink: #FFB800; /* Dragon Gold — load-bearing read color */ + --color-ink-muted: #CD7F32; /* Ancient Bronze — disabled / stale */ + --color-accent: #00F5FF; /* Neon Cyan — readable accent + focus glow */ + --color-accent-hover: #5CFAFF; /* Cyan lightened ~20% on hover */ + + /* Semantic signal */ + --color-warning: #FF6B35; /* Ember Orange */ + --color-danger: #FF2D95; /* Magenta Pulse — transient now state */ + --color-deep-danger: #8B0000; /* Blood Crimson — destructive confirm */ + --color-magic: #9D00FF; /* Electric Purple — arcane / high-tier */ + --color-data: #39FF14; /* Toxic Green — protocol-flavored */ + + /* Bone overlay */ + --color-bone: #E8E4DC; +} + +[data-theme="dark"] { + /* DPN is dark-by-default; tokens above already are the dark theme. + Override block kept present per Lens A; values intentionally identical. */ + --color-canvas: #0A0A0F; + --color-surface: #2D2D3A; + --color-on-surface: #E8E4DC; + --color-ink: #FFB800; + --color-accent: #00F5FF; +} +``` + +## 3. Typography + +A **three-family stack** with deliberate role separation: serif-free, mono-forward, with optional display weight for headings carrying the DragonPunk world signal. + +- **Body / UI**: `Inter`, system-ui fallback. Reads quietly against void; no swashes, no character. Doing the structural work. +- **Mono**: `JetBrains Mono` (preferred), `IBM Plex Mono` fallback, `monospace` final. Where data lives. Code blocks, terminal panes, table values, technical metadata. +- **Display** (optional): `Major Mono Display` for hero numerals and section labels that carry signal-character. Reserved. + +Type scale uses a 1.2 ratio (minor third), tightly cadenced so that information density feels dashboard-correct rather than magazine-roomy. + +```css +:root { + --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, "SF Mono", monospace; + --font-display: "Major Mono Display", "JetBrains Mono", monospace; + + /* Scale — 1.2 ratio */ + --font-size-display: 2.488rem; /* 39.8px */ + --font-size-h1: 2.074rem; /* 33.2px */ + --font-size-h2: 1.728rem; /* 27.6px */ + --font-size-h3: 1.44rem; /* 23px */ + --font-size-h4: 1.2rem; /* 19px */ + --font-size-body: 1rem; /* 16px */ + --font-size-caption: 0.833rem; /* 13.3px */ + --font-size-micro: 0.694rem; /* 11px */ + + /* Weights */ + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-bold: 600; + + /* Leading */ + --leading-tight: 1.15; + --leading-normal: 1.45; + --leading-loose: 1.65; + + /* Tracking */ + --tracking-tight: -0.01em; + --tracking-normal: 0; + --tracking-wide: 0.05em; + --tracking-caps: 0.1em; /* small-caps + uppercase labels */ +} +``` + +All body text uses `var(--color-ink)` (Dragon Gold). All links use `var(--color-accent)` (Neon Cyan). All headings use `var(--color-ink)` but at heavier weight + tighter leading. Uppercase labels (status chips, section anchors, dashboard headers) use `tracking-caps` to earn the dashboard-instrument signal. + +## 4. Spacing + +A **4-pixel base unit**, with steps at 4 / 8 / 12 / 16 / 24 / 32 / 48 / 64. Tight at the small end (dashboard data density), generous at the large end (breathing between sections). No half-pixel values; no irregular steps. + +```css +:root { + --space-0: 0; + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-6: 24px; + --space-8: 32px; + --space-12: 48px; + --space-16: 64px; +} +``` + +Component-internal padding tends toward `--space-2` / `--space-3`. Component-to-component spacing tends toward `--space-4` / `--space-6`. Section breaks use `--space-12`. + +## 5. Layout & Composition + +The default container is **flush-edge with no max-width** — the canvas wants to fill the screen the way a reactor console does. When narrower reading is needed (long-form prose, articles), use a max-width of `72ch` and align left, never centered. **Centered prose is decorative; left-aligned prose is operational.** + +Grid is on a 12-column system at the page level, but components prefer **flex with explicit gap** over grid for internal composition. Avoid `grid-template-areas` for anything except actual map-shaped layouts (HUD, dashboard). + +Vertical rhythm follows the spacing scale: section heads sit `--space-12` above content, paragraphs sit `--space-4` apart, list items sit `--space-2` apart. No margin-collapse fuckery — every box owns its own padding. + +```css +.container { + max-width: none; + padding-inline: var(--space-6); +} +.container-prose { + max-width: 72ch; + text-align: left; +} +.grid-12 { + display: grid; + grid-template-columns: repeat(12, 1fr); + gap: var(--space-4); +} +``` + +## 6. Components + +### Card / Panel + +```css +.dpn-panel { + background: var(--color-surface); + color: var(--color-ink); + border: 1px solid var(--color-surface-high); + border-radius: 0; /* square — no exceptions */ + padding: var(--space-4) var(--space-6); + box-shadow: none; /* no shadows ever */ + font-family: var(--font-sans); +} + +.dpn-panel--data { + font-family: var(--font-mono); + font-size: var(--font-size-caption); + line-height: var(--leading-normal); +} +``` + +### Button + +```css +.dpn-btn { + background: transparent; + color: var(--color-ink); + border: 1px solid var(--color-ink); + border-radius: 0; + padding: var(--space-2) var(--space-4); + font-family: var(--font-sans); + font-size: var(--font-size-body); + font-weight: var(--font-weight-medium); + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + cursor: pointer; + transition: color 80ms linear, border-color 80ms linear, background 80ms linear; +} +.dpn-btn:hover { + background: var(--color-ink); + color: var(--color-canvas); +} +.dpn-btn:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} +.dpn-btn--primary { + background: var(--color-ink); + color: var(--color-canvas); +} +.dpn-btn--danger { + border-color: var(--color-deep-danger); + color: var(--color-deep-danger); +} +``` + +### Status chip / LED + +```css +.dpn-chip { + display: inline-flex; + align-items: center; + gap: var(--space-1); + background: transparent; + color: var(--color-ink); + border: 1px solid currentColor; + padding: 2px var(--space-2); + font-family: var(--font-mono); + font-size: var(--font-size-micro); + font-weight: var(--font-weight-medium); + text-transform: uppercase; + letter-spacing: var(--tracking-caps); +} +.dpn-chip--ok { color: var(--color-accent); } +.dpn-chip--warn { color: var(--color-warning); } +.dpn-chip--alert { color: var(--color-danger); } +.dpn-chip--magic { color: var(--color-magic); } +.dpn-chip--data { color: var(--color-data); } +``` + +### Link + +```css +.dpn-link { + color: var(--color-accent); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 2px; + transition: color 80ms linear; +} +.dpn-link:hover { + color: var(--color-accent-hover); +} +``` + +### Input + +```css +.dpn-input { + background: var(--color-canvas); + color: var(--color-ink); + border: 1px solid var(--color-surface-high); + border-radius: 0; + padding: var(--space-2) var(--space-3); + font-family: var(--font-mono); + font-size: var(--font-size-body); + caret-color: var(--color-accent); +} +.dpn-input:focus-visible { + outline: 0; + border-color: var(--color-accent); +} +``` + +### Heading + caption + +```css +.dpn-h1 { + font-family: var(--font-sans); + font-size: var(--font-size-h1); + font-weight: var(--font-weight-bold); + line-height: var(--leading-tight); + letter-spacing: var(--tracking-tight); + color: var(--color-ink); + margin-block-end: var(--space-4); +} +.dpn-caption { + font-family: var(--font-mono); + font-size: var(--font-size-caption); + color: var(--color-ink-muted); + text-transform: uppercase; + letter-spacing: var(--tracking-caps); +} +``` + +## 7. Motion & Interaction + +Motion is **state-change-only**. No ambient animation, no parallax, no infinite loops, no "delight" particles. When a state changes, the transition is 80–120ms linear or ease-out — quick enough to feel direct, slow enough to be perceived as motion rather than a jump cut. + +Allowed motion: +- Color/opacity transitions on hover, focus, active (80–120ms) +- Mount / unmount fades on modal-shape elements (120–160ms) +- One-shot pulse on state-change chips (a single `keyframes` cycle, never `infinite`) + +Disallowed motion: parallax, marquee scrolls, particle effects, ambient idle animation, hover bounces, micro-wiggles. + +```css +:root { + --motion-fast: 80ms; + --motion-normal: 120ms; + --motion-slow: 160ms; + --motion-ease: cubic-bezier(0.2, 0, 0.4, 1); +} + +@keyframes dpn-pulse { + 0% { opacity: 1; } + 50% { opacity: 0.4; } + 100% { opacity: 1; } +} + +.dpn-chip--alert { + animation: dpn-pulse var(--motion-slow) ease-in-out; + animation-iteration-count: 1; +} + +@media (prefers-reduced-motion: reduce) { + .dpn-chip--alert, + .dpn-btn { + animation: none; + transition: none; + } +} +``` + +## 8. Voice & Brand + +DPN's voice in copy is **terse, technical, slightly noir-poetic**. Status messages read like a ship's log; error messages read like a detective taking notes. Uppercase labels for status chips. Sentence case for everything else. Never exclamation marks; the visual neon flare carries emphasis already. + +Tone calibration: +- Status: `READY · WAITING · ACTIVE · IDLE` — uppercase, terse, single word +- Errors: "Authentication declined." — past-tense factual, no "Oops!" +- Empty state: "Nothing here yet." — flat, not encouraging +- Confirmation: "Saved." — single word, no exclamation + +Iconography is line-only, never filled. 1.5px stroke, square caps, square corners. Reserved palette: ink color or accent color, nothing custom. + +The brand pulls visual identity from the **Orbis world** — a 10,500-year-old setting where colony-ship descendants live alongside dragons and use spell-code for fiber-optic runes. The aesthetic does not announce this; it just *implies* a layered, lived-in world that's older than the user and indifferent to user-flattery. + +## 9. Anti-patterns + +- **Rounded corners > 0px on rectangles.** Square always. The single allowed roundness is `border-radius: 50%` for avatars, status dots, and circular elements that are circular because they're circles. Never `8px`, never `4px`, never `2px`. +- **Drop shadows.** Use border or surface elevation (different background) for separation. No `box-shadow`. Period. +- **Gradients on surfaces.** No background gradients on cards, buttons, page chrome. The only allowed gradient is a *signal* — a 1px focus-ring stroke fading from cyan→ember in a depth-indicator. Functional, not decorative. +- **Saturation on canvas.** The canvas is `--color-canvas`; nothing else. Don't tint with cyan, don't paint with subtle gradients, don't add ambient color washes. Canvas stays canvas. +- **Animation on idle.** No CSS `animation: ... infinite`. No "breathing" buttons. No spinning anything that isn't actively loading. State change *only*. +- **Centered prose.** Long-form text never gets `text-align: center`. Headlines occasionally; bodies never. +- **Cute or whimsical microcopy.** "Oops!", "Yay!", emojis-as-decoration in error messages — none of it. The world is too old for that voice. +- **Multiple neons in a single chip.** A chip is one color. Cyan or ember or magenta, not a gradient between them. The exception is the cyan→ember context-depth indicator which is itself a single semantic role. +- **Sans-serif numeric data.** Numbers in tables, code, terminals always render in `--font-mono`. Never `--font-sans` for any column that contains digits primarily. +- **Hover styles on touch-only components.** Nothing should require hover to be discoverable; hover is *decoration on already-visible affordance*. + +--- + +**Prior art / influences:** Plan 9 from Bell Labs, Acme editor, NeXTSTEP system fonts and chrome, Blade Runner 2049 set design, ChatHUD's terminal UI conventions, the Mixxx LateNight-DPN skin, the dpn-void.css scheme, the Dragonpunk Network brand surface for the public-facing site. diff --git a/design-systems/dragonpunk-solar/DESIGN.md b/design-systems/dragonpunk-solar/DESIGN.md new file mode 100644 index 0000000000..2ed6f65b48 --- /dev/null +++ b/design-systems/dragonpunk-solar/DESIGN.md @@ -0,0 +1,400 @@ +# Design System Inspired by DragonPunk Solar + +> Category: Bold & Expressive +> Sun-bleached solarpunk meets cyberpunk neon and high-fantasy mythos — the daylight twin of DragonPunk Noir. + +## 1. Visual Theme & Atmosphere + +DragonPunk Solar (DPS) is solarpunk wearing fiber-optic veins. Where DPN is `3 AM wet stone street`, DPS is `noon on a rooftop garden in summer`. The canvas is **Solar White** (`#FFF8E1`) — not a sterile sheet-of-paper white but a cream warmed by sun-glare, the lightness of dragon-scale catching light. Against that canvas, the same **70/20/10 budget** that runs DPN flips: 70% bright tones do the structural work, 20% cool accents do the signaling, 10% deep shadows break the blaze. + +The atmospheric law remains **operational dashboard, not arcade marquee** — same reactor-control-room calm, just daylit. 95% of pixels do not glow; the small flaring minority carries meaning. The difference from DPN: where DPN can use a single cyan token for both readable accent and focus-glow (cyan-on-void clears ~110:1 contrast), DPS *must* split the role — on cream, cyan contrast collapses to ~1.7:1. DPS solves this with **Bole** and **Rubric** drawn from Renaissance gilding tradition: bole is the deep red-clay ground gilders laid beneath gold leaf to make it glow; rubric is the manuscript-red used for headings and important wayfinding in flowing body copy. + +DPS is **light without being innocent**. The shadows aren't gone — they're cast hard by direct sunlight, defined and deliberate. Heat haze sits at the edges. Solar arrays bloom on gothic spires. Plan 9 / Acme / NeXTSTEP lineage holds: square corners, no shadows, no blur, no animations except where state is changing. + +**Key Characteristics:** +- Solar canvas (`#FFF8E1`) — cream warmed by sun-glare, never sterile white +- Bole (`#3D1208`) as load-bearing reading color — deep red-warm pencil, ~13:1 contrast on Solar White +- Rubric (`#B82F08`) for readable accent (links, hashtags, wayfinding) — ~5.5:1 contrast +- Plasma Cyan (`#00D4FF`) reserved for focus-glow only (cool spike in a warm register) +- Square corners exclusively — same DPN rule +- No drop shadows. No blur. No skeumorphism. Plan 9 / Acme lineage +- Cool flares treated as instrument-panel LEDs against a daylit field +- The same operational dashboard discipline as DPN, with a light surface and warm ink + +## 2. Color + +The palette mirrors DPN's three-band structure (canvas / accent / warm) but rotated for cream-surface contrast math. Bole replaces gold as ink; Rubric replaces cyan-as-readable-accent; Plasma Cyan retreats to glow-only. + +### Primary — Solar Foundation + +- **Solar White** (`#FFF8E1`): Primary canvas. A cream warmed toward gold, never neutral. Reads as "sun-baked paper" or "dragon-scale catching light." +- **Dune Gold** (`#D4A017`): Elevated surfaces, panel separators, midtone warmth. The "raised" layer above the canvas. +- **Horizon Azure** (`#4A90E2`): Far-distance accents, the cool spike at the edge of vision — clear skies, distant haze. Used sparingly as a secondary cool counterpoint. + +### Typographic Inks — Renaissance Gilding Tradition + +- **Bole** (`#3D1208`): **Load-bearing reading color.** Body text, headings. Deep red-warm pencil doing the contrast work against Solar White (~13:1). The gilder's underground that makes gold leaf glow. +- **Rubric** (`#B82F08`): **Readable accent.** Links, hashtags, wayfinding distinction in flowing text. Manuscript-red. ~5.5:1 contrast — comfortable for reading, distinct from body without shouting. +- **Plasma Cyan** (`#00D4FF`): **Glow only.** Focus rings, active states, hover decoration. The cool spike in DPS's warm register — mirrors gold's role as the warm spike in DPN's cool register. Never used as readable text on cream (the contrast doesn't support it). + +### Cool — Cyberpunk Injection + +- **Solar Magenta** (`#FF1493`): Alert / now / urgent state. Reserved for transient signal — never for surface fill. +- **Electric Violet** (`#A020F0`): Magic / arcane / high-tier system elements. Same role as DPN's Electric Purple, just one notch hotter for the daylit context. +- **Luminescent Green** (`#00FF7F`): Data streams, system internals, growth/positive-state indicators. The "the machine is working" green. + +### Warm — Fantasy Spike & Semantic Signal + +- **Solar Crimson** (`#FF4500`): Warning / threshold / hot-spike. Active danger state, error states, "approaching limit." +- **Blaze Orange** (`#FF8C00`): Active processing, in-flight, "this is happening now." Less severe than crimson. +- **Dragon Gold** (`#FFD700`): Highlight / treasure / power-state. Used sparingly as ornament on the cream — too much gold-on-cream becomes invisible. +- **Sunlit Bronze** (`#DAA520`): Disabled / stale / aged-metal state. What rubric turns into when the data is no longer fresh. + +### CSS tokens + +```css +:root { + /* Surface */ + --color-canvas: #FFF8E1; + --color-surface: #FFFFFF; + --color-surface-high: #FAF4D8; + --color-on-surface: #3D1208; + + /* Typographic roles */ + --color-ink: #3D1208; /* Bole — load-bearing read color */ + --color-ink-muted: #DAA520; /* Sunlit Bronze — disabled / stale */ + --color-accent: #B82F08; /* Rubric — readable accent */ + --color-accent-hover: #FF4500; /* Solar Crimson — link hover */ + --color-glow: #00D4FF; /* Plasma Cyan — focus rings only */ + + /* Semantic signal */ + --color-warning: #FF4500; /* Solar Crimson */ + --color-active: #FF8C00; /* Blaze Orange — in-flight */ + --color-danger: #FF1493; /* Solar Magenta — transient now state */ + --color-magic: #A020F0; /* Electric Violet — arcane / high-tier */ + --color-data: #00FF7F; /* Luminescent Green — protocol-flavored */ + + /* Surface tones */ + --color-dune: #D4A017; + --color-azure: #4A90E2; + --color-gold-ornament: #FFD700; +} + +[data-theme="dark"] { + /* DPS is the LIGHT face of Dragonpunk. The dark mirror is DPN. + This override block flips DPS to its DPN counterpart for users who + prefer dark; tokens shift roles per the cross-system mapping + documented in DPN.md § Typographic Roles. */ + --color-canvas: #0A0A0F; + --color-surface: #2D2D3A; + --color-surface-high: #3D3D4A; + --color-on-surface: #E8E4DC; + --color-ink: #FFB800; /* Dragon Gold */ + --color-ink-muted: #CD7F32; + --color-accent: #00F5FF; /* Neon Cyan does both roles in dark */ + --color-accent-hover: #5CFAFF; + --color-glow: #00F5FF; + --color-warning: #FF6B35; + --color-active: #FF8C00; + --color-danger: #FF2D95; + --color-magic: #9D00FF; + --color-data: #39FF14; +} +``` + +## 3. Typography + +The same three-family stack as DPN — `Inter` for UI, `JetBrains Mono` for data, `Major Mono Display` for display headings. The DragonPunk identity sits in the role-color mapping, not in custom typefaces. + +```css +:root { + --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, "SF Mono", monospace; + --font-display: "Major Mono Display", "JetBrains Mono", monospace; + + /* Scale — 1.2 ratio (matches DPN) */ + --font-size-display: 2.488rem; + --font-size-h1: 2.074rem; + --font-size-h2: 1.728rem; + --font-size-h3: 1.44rem; + --font-size-h4: 1.2rem; + --font-size-body: 1rem; + --font-size-caption: 0.833rem; + --font-size-micro: 0.694rem; + + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-bold: 600; + + --leading-tight: 1.15; + --leading-normal: 1.45; + --leading-loose: 1.65; + + --tracking-tight: -0.01em; + --tracking-normal: 0; + --tracking-wide: 0.05em; + --tracking-caps: 0.1em; +} +``` + +Body text uses `var(--color-ink)` (Bole). Links use `var(--color-accent)` (Rubric) with `var(--color-accent-hover)` (Solar Crimson) on hover. Focus glow uses `var(--color-glow)` (Plasma Cyan). Uppercase labels (status chips, dashboard headers) earn `tracking-caps` and read as "instrument-panel labels in a sunny control room." + +**Critical contrast rule:** Plasma Cyan (`#00D4FF`) on Solar White has ~1.7:1 contrast — failing WCAG AA for any text use. DPS *never* renders text in Plasma Cyan on canvas. Cyan only appears as 2px+ outlines (focus rings, active-state borders) where it functions as edge-detection rather than text. + +## 4. Spacing + +Same 4-pixel base unit as DPN. Same scale at 4 / 8 / 12 / 16 / 24 / 32 / 48 / 64. The visual register changes; the dimensional grammar holds. + +```css +:root { + --space-0: 0; + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-6: 24px; + --space-8: 32px; + --space-12: 48px; + --space-16: 64px; +} +``` + +## 5. Layout & Composition + +Same composition rules as DPN: flush-edge containers by default, prose pinned at `72ch` left-aligned, 12-column grid at page level, flex with explicit gap for component-internal layout, no margin-collapse. + +The only DPS-specific layout note: **sun-bleached negative space**. Cream canvas tolerates more whitespace than void does — DPN's tight dashboard density gives way slightly in DPS to a more spacious, sun-warmed cadence. Sections breathe at `--space-12`; consider `--space-16` for hero-level breaks. Cards sit on `--color-surface` (white) against the cream canvas, gaining presence by being *brighter* than their background rather than darker. + +```css +.container { + max-width: none; + padding-inline: var(--space-6); +} +.container-prose { + max-width: 72ch; + text-align: left; +} +.grid-12 { + display: grid; + grid-template-columns: repeat(12, 1fr); + gap: var(--space-4); +} +``` + +## 6. Components + +### Card / Panel + +```css +.dps-panel { + background: var(--color-surface); /* pure white, brighter than cream canvas */ + color: var(--color-ink); + border: 1px solid var(--color-surface-high); /* faint warm border */ + border-radius: 0; + padding: var(--space-4) var(--space-6); + box-shadow: none; + font-family: var(--font-sans); +} + +.dps-panel--data { + font-family: var(--font-mono); + font-size: var(--font-size-caption); + line-height: var(--leading-normal); + background: var(--color-surface-high); /* subtler than pure white for data panes */ +} +``` + +### Button + +```css +.dps-btn { + background: transparent; + color: var(--color-ink); + border: 1px solid var(--color-ink); + border-radius: 0; + padding: var(--space-2) var(--space-4); + font-family: var(--font-sans); + font-size: var(--font-size-body); + font-weight: var(--font-weight-medium); + text-transform: uppercase; + letter-spacing: var(--tracking-caps); + cursor: pointer; + transition: color 80ms linear, border-color 80ms linear, background 80ms linear; +} +.dps-btn:hover { + background: var(--color-ink); + color: var(--color-canvas); +} +.dps-btn:focus-visible { + outline: 2px solid var(--color-glow); /* Plasma Cyan ring on cream */ + outline-offset: 2px; +} +.dps-btn--primary { + background: var(--color-ink); + color: var(--color-canvas); +} +.dps-btn--accent { + border-color: var(--color-accent); /* Rubric */ + color: var(--color-accent); +} +.dps-btn--accent:hover { + background: var(--color-accent); + color: var(--color-canvas); +} +.dps-btn--danger { + border-color: var(--color-warning); + color: var(--color-warning); +} +``` + +### Status chip / LED + +```css +.dps-chip { + display: inline-flex; + align-items: center; + gap: var(--space-1); + background: transparent; + color: var(--color-ink); + border: 1px solid currentColor; + padding: 2px var(--space-2); + font-family: var(--font-mono); + font-size: var(--font-size-micro); + font-weight: var(--font-weight-medium); + text-transform: uppercase; + letter-spacing: var(--tracking-caps); +} +.dps-chip--ok { color: var(--color-data); } /* Luminescent Green */ +.dps-chip--active { color: var(--color-active); } /* Blaze Orange */ +.dps-chip--warn { color: var(--color-warning); } /* Solar Crimson */ +.dps-chip--alert { color: var(--color-danger); } /* Solar Magenta */ +.dps-chip--magic { color: var(--color-magic); } /* Electric Violet */ +``` + +### Link + +```css +.dps-link { + color: var(--color-accent); /* Rubric — readable on cream */ + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 2px; + transition: color 80ms linear; +} +.dps-link:hover { + color: var(--color-accent-hover); /* Solar Crimson — hot spike on hover */ +} +``` + +### Input + +```css +.dps-input { + background: var(--color-surface); /* pure white field on cream canvas */ + color: var(--color-ink); + border: 1px solid var(--color-surface-high); + border-radius: 0; + padding: var(--space-2) var(--space-3); + font-family: var(--font-mono); + font-size: var(--font-size-body); + caret-color: var(--color-glow); /* Plasma Cyan caret */ +} +.dps-input:focus-visible { + outline: 0; + border-color: var(--color-glow); /* Cyan border on focus */ + box-shadow: 0 0 0 1px var(--color-glow); /* one-pixel inset cyan glow */ +} +``` + +### Heading + caption + +```css +.dps-h1 { + font-family: var(--font-sans); + font-size: var(--font-size-h1); + font-weight: var(--font-weight-bold); + line-height: var(--leading-tight); + letter-spacing: var(--tracking-tight); + color: var(--color-ink); /* Bole */ + margin-block-end: var(--space-4); +} +.dps-h2-accent { + /* Wayfinding heading — Rubric for important-but-secondary breaks */ + font-family: var(--font-sans); + font-size: var(--font-size-h2); + font-weight: var(--font-weight-bold); + color: var(--color-accent); /* Rubric */ +} +.dps-caption { + font-family: var(--font-mono); + font-size: var(--font-size-caption); + color: var(--color-ink-muted); + text-transform: uppercase; + letter-spacing: var(--tracking-caps); +} +``` + +## 7. Motion & Interaction + +Identical motion discipline to DPN: **state-change only**. No ambient animation, no parallax, no infinite loops. Transitions 80–120ms. One-shot pulses on alert chips, never `animation-iteration-count: infinite`. + +```css +:root { + --motion-fast: 80ms; + --motion-normal: 120ms; + --motion-slow: 160ms; + --motion-ease: cubic-bezier(0.2, 0, 0.4, 1); +} + +@keyframes dps-pulse { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + +.dps-chip--alert { + animation: dps-pulse var(--motion-slow) ease-in-out; + animation-iteration-count: 1; +} + +@media (prefers-reduced-motion: reduce) { + .dps-chip--alert, + .dps-btn { + animation: none; + transition: none; + } +} +``` + +## 8. Voice & Brand + +DPS's voice is **same factual register as DPN, but daylit** — same terse, technical, no-exclamation-marks discipline, with copy that implies open horizons rather than wet streets. Status chips stay uppercase mono; body copy stays sentence case; error messages stay past-tense factual. + +Tone calibration: +- Status: `READY · ACTIVE · IDLE · SYNCING` — identical vocabulary to DPN +- Errors: "Authentication declined." — past-tense factual +- Empty state: "Nothing scheduled." — flat, neutral +- Confirmation: "Saved." — single word + +Iconography matches DPN: line-only, 1.5px stroke, square caps, square corners. Reserved palette is ink color (Bole) or accent color (Rubric) — never custom hues. + +The brand lineage is **Orbis under sun** — the same 10,500-year-old setting, but rooftop gardens, solar-sailed spires, dragons basking on heat-baked stone instead of slinking through wet alleys. The aesthetic does not announce the worldbuilding; it implies an old, layered, lived-in civilization that's *uplit* rather than backlit. + +## 9. Anti-patterns + +- **Sterile white** (`#FFF` as canvas). The canvas is warm cream `#FFF8E1`; nothing else. Pure white is reserved for elevated surface (`var(--color-surface)`) where a brighter card sits on the cream. +- **Cyan as text** on cream. Plasma Cyan fails contrast for body or link text. Cyan is exclusively a 2px+ outline / focus-ring / caret color. +- **Gold-ornament on cream as primary signal.** Dragon Gold (`#FFD700`) on Solar White is decorative; never use it as text or as the primary signal color. Bole or Rubric carry signal. +- **Rounded corners > 0px on rectangles.** Same DPN rule. Square always. The only allowed roundness is `border-radius: 50%` for circles. +- **Drop shadows.** Use border + brighter-surface elevation. No `box-shadow` except the 1px cyan focus-glow on inputs (a *signal*, not a depth illusion). +- **Background gradients on surfaces.** No gradient cards, no gradient buttons, no ambient wash on the canvas. The only allowed gradient is a functional cyan→crimson context-depth signal (mirroring DPN's cyan→ember). +- **Ambient idle animation.** No `animation: ... infinite`. No "breathing" UI. State change only. +- **Light/dark theme as identical palettes.** DPS's dark-mode override is *DPN's tokens* — Bole becomes Dragon Gold, Rubric becomes Neon Cyan, Plasma Cyan stays cyan. Same brand, different palette family, not duplicated values. +- **Centered prose.** Same DPN rule — long-form text is left-aligned. Centered = decorative. +- **Cute or whimsical microcopy.** Solar daylight doesn't soften the voice; the world is just as old in DPS as in DPN. No "Oops!", no emoji-as-decoration in errors. +- **Sans-serif numeric data.** Numbers in tables, code, terminals always render in `--font-mono`. Same rule as DPN — the data-density discipline crosses themes. +- **Hover-only affordances.** Anything interactive must be discoverable without hover; hover is decoration on already-visible affordance. + +--- + +**Prior art / influences:** Plan 9 from Bell Labs, Acme editor, NeXTSTEP system fonts and chrome, Renaissance manuscript rubrication (bole and rubric as named ink roles), solarpunk illustration tradition, the Mixxx LateNight-DPS skin, the `dps-solar.css` light-toggle for DPN, the cross-system role-mapping documented in DPN.md § Typographic Roles, and the Dragonpunk Network public-facing brand surface. From ffec29d5d51876aaeba385c9d65afda6a91a5973 Mon Sep 17 00:00:00 2001 From: Nathan Eckenrode Date: Tue, 19 May 2026 08:15:22 -0400 Subject: [PATCH 2/2] fix(tools-pack): focus existing AppImage instead of silent EADDRINUSE on second launch startPackedLinuxApp had no already-running detection. Every CMD+SPACE "Open Design" launch after the first one tried to bind the same desktop IPC socket at /tmp/open-design/ipc//desktop.sock, hit EADDRINUSE, and exited silently. Observed as the launcher "doing nothing" on the second click. This adds detectRunningLinuxApp() (mirrors stopPackedLinuxApp's marker + stamp + IPC liveness validation) and focusLinuxWindow() (swaymsg / i3-msg / hyprctl by env, wmctrl as universal fallback), so the fast path focuses an existing window instead of spawning a duplicate. - LinuxStartSource widened to include "already-running" - LinuxStartResult gains optional focusMethod - 5 new tests for focusAttemptsForLinuxWindow (66 total, all pass) - pnpm guard / typecheck / --filter @open-design/tools-pack test all green --- tools/pack/src/linux.ts | 152 ++++++++++++++++++++++++++++++++- tools/pack/tests/linux.test.ts | 48 +++++++++++ 2 files changed, 199 insertions(+), 1 deletion(-) diff --git a/tools/pack/src/linux.ts b/tools/pack/src/linux.ts index f2ebfa78cc..fc0f276871 100644 --- a/tools/pack/src/linux.ts +++ b/tools/pack/src/linux.ts @@ -628,11 +628,15 @@ export async function installPackedLinuxApp(config: ToolPackConfig): Promise { + const { marker } = await readDesktopRootIdentityMarker(config); + if (marker == null) return null; + + const snapshots = await listProcessSnapshots(); + if (!snapshots.some((s) => s.pid === marker.pid)) return null; + + const expectedIpc = resolveAppIpcPath({ + app: APP_KEYS.DESKTOP, + contract: OPEN_DESIGN_SIDECAR_CONTRACT, + namespace: config.namespace, + }); + const stampOk = + marker.stamp.app === APP_KEYS.DESKTOP && + marker.stamp.mode === SIDECAR_MODES.RUNTIME && + marker.stamp.namespace === config.namespace && + marker.stamp.ipc === expectedIpc && + (marker.stamp.source === SIDECAR_SOURCES.TOOLS_PACK || + marker.stamp.source === SIDECAR_SOURCES.PACKAGED); + if (!stampOk) return null; + if (marker.namespaceRoot !== config.roots.runtime.namespaceRoot) return null; + + const candidateAppImagePath = + (await pathExists(paths.installAppImagePath)) + ? paths.installAppImagePath + : await findBuiltAppImage(paths); + if (candidateAppImagePath == null) return null; + + const exePath = await readProcessExe(marker.pid); + const env = await readProcessEnv(marker.pid); + const cmdOk = matchesAppImageProcess( + { pid: marker.pid, executable: exePath, env }, + candidateAppImagePath, + ); + if (!cmdOk) return null; + + // Liveness probe: the marker may point at a wedged process whose IPC + // server isn't accepting connections (e.g. crashed mid-startup before + // sockets bound, but parent still alive). If STATUS doesn't answer + // quickly, treat as zombie and fall through to the spawn path -- the + // existing logic there rms the marker and waits for a fresh one. + try { + await requestJsonIpc( + marker.stamp.ipc, + { type: SIDECAR_MESSAGES.STATUS }, + { timeoutMs: 1000 }, + ); + } catch { + return null; + } + + return { appImagePath: candidateAppImagePath, marker, pid: marker.pid }; +} + +export type LinuxFocusAttempt = { + args: string[]; + command: string; + method: "hyprctl" | "i3-msg" | "swaymsg" | "wmctrl"; +}; + +// Window-manager-specific focus commands, ordered by specificity. The +// pid-targeted variants (sway/i3/hyprland) are preferred because they +// disambiguate when multiple Open Design windows exist; wmctrl by class is +// the universal fallback when no WM-specific socket is present. +export function focusAttemptsForLinuxWindow( + pid: number, + env: NodeJS.ProcessEnv, +): LinuxFocusAttempt[] { + const attempts: LinuxFocusAttempt[] = []; + if (env.SWAYSOCK) { + attempts.push({ + args: ["-t", "command", `[pid="${pid}"] focus`], + command: "swaymsg", + method: "swaymsg", + }); + } + if (env.I3SOCK) { + attempts.push({ + args: [`[pid="${pid}"] focus`], + command: "i3-msg", + method: "i3-msg", + }); + } + if (env.HYPRLAND_INSTANCE_SIGNATURE) { + attempts.push({ + args: ["dispatch", "focuswindow", `pid:${pid}`], + command: "hyprctl", + method: "hyprctl", + }); + } + attempts.push({ + args: ["-x", "-a", "Open Design"], + command: "wmctrl", + method: "wmctrl", + }); + return attempts; +} + +async function focusLinuxWindow(pid: number): Promise { + for (const attempt of focusAttemptsForLinuxWindow(pid, process.env)) { + try { + await execFileAsync(attempt.command, attempt.args, { timeout: 1500 }); + return attempt.method; + } catch { + // Try the next attempt -- command missing, WM not running, etc. + } + } + return null; +} + export async function startPackedLinuxApp(config: ToolPackConfig): Promise { const paths = resolveLinuxPaths(config); + + // Fast path: if a live instance is already running for this namespace, + // focus its window and return rather than spawning a duplicate. A second + // spawn would race the existing AppImage for the desktop IPC socket + // (/tmp/open-design/ipc//desktop.sock), hit EADDRINUSE, and exit 1 -- + // observed as the launcher "doing nothing" on the second click. Falls + // through to the spawn path when the marker is missing, stale, or IPC + // doesn't answer STATUS. + const existing = await detectRunningLinuxApp(config, paths); + if (existing != null) { + const focusMethod = await focusLinuxWindow(existing.pid).catch(() => null); + return { + appImagePath: existing.appImagePath, + executablePath: existing.appImagePath, + focusMethod, + logPath: desktopLogPath(config), + namespace: config.namespace, + pid: existing.pid, + source: "already-running", + status: await fetchDesktopStatus(config), + }; + } + const installed = await pathExists(paths.installAppImagePath); const built = !installed ? await findBuiltAppImage(paths) : null; const appImagePath = installed ? paths.installAppImagePath : built; diff --git a/tools/pack/tests/linux.test.ts b/tools/pack/tests/linux.test.ts index ab19d26f01..cec558ae52 100644 --- a/tools/pack/tests/linux.test.ts +++ b/tools/pack/tests/linux.test.ts @@ -8,6 +8,7 @@ import { describe, expect, it } from "vitest"; import type { ToolPackConfig } from "../src/config.js"; import { buildDockerArgs, + focusAttemptsForLinuxWindow, matchesAppImageProcess, renderDesktopTemplate, sanitizeNamespace, @@ -288,3 +289,50 @@ describe("matchesAppImageProcess", () => { expect(ok).toBe(false); }); }); + +describe("focusAttemptsForLinuxWindow", () => { + it("uses swaymsg with pid selector when SWAYSOCK is set", () => { + const attempts = focusAttemptsForLinuxWindow(4180627, { SWAYSOCK: "/run/user/1000/sway-ipc.sock" }); + expect(attempts[0]).toEqual({ + args: ["-t", "command", '[pid="4180627"] focus'], + command: "swaymsg", + method: "swaymsg", + }); + }); + + it("uses i3-msg with pid selector when I3SOCK is set", () => { + const attempts = focusAttemptsForLinuxWindow(42, { I3SOCK: "/run/user/1000/i3-ipc.sock" }); + expect(attempts[0]).toEqual({ + args: ['[pid="42"] focus'], + command: "i3-msg", + method: "i3-msg", + }); + }); + + it("uses hyprctl when HYPRLAND_INSTANCE_SIGNATURE is set", () => { + const attempts = focusAttemptsForLinuxWindow(42, { HYPRLAND_INSTANCE_SIGNATURE: "abc" }); + expect(attempts[0]).toEqual({ + args: ["dispatch", "focuswindow", "pid:42"], + command: "hyprctl", + method: "hyprctl", + }); + }); + + it("always includes wmctrl as the universal fallback", () => { + const swayAttempts = focusAttemptsForLinuxWindow(1, { SWAYSOCK: "/x" }); + const headlessAttempts = focusAttemptsForLinuxWindow(1, {}); + expect(swayAttempts.at(-1)?.method).toBe("wmctrl"); + expect(headlessAttempts).toEqual([ + { args: ["-x", "-a", "Open Design"], command: "wmctrl", method: "wmctrl" }, + ]); + }); + + it("orders WM-specific attempts before the universal wmctrl fallback", () => { + const attempts = focusAttemptsForLinuxWindow(7, { + HYPRLAND_INSTANCE_SIGNATURE: "abc", + I3SOCK: "/run/i3", + SWAYSOCK: "/run/sway", + }); + expect(attempts.map((a) => a.method)).toEqual(["swaymsg", "i3-msg", "hyprctl", "wmctrl"]); + }); +});