From da3b7d66f04bfd4d0a44529a769df106eb5cc965 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 22 Jun 2026 03:09:44 +0200 Subject: [PATCH] Integrate whynot-design into Economic Observatory UI Vendor whynot-design Layer 1 (tokens, CSS) and Layer 2 ( components) via scripts/sync-whynot-design.sh with a pinned ref. Migrate the observatory shell to canonical web components, keep observatory-specific layout in styles.css, and add vendor integrity tests plus correct JS MIME types on the dev server. --- projects/coulomb-pricing/README.md | 7 +- .../coulomb-pricing/observatory/server.py | 13 +- .../scripts/sync-whynot-design.sh | 47 ++ .../coulomb-pricing/tests/test_ui_vendor.py | 62 ++ projects/coulomb-pricing/ui/app.js | 108 ++-- projects/coulomb-pricing/ui/index.html | 136 ++-- projects/coulomb-pricing/ui/styles.css | 395 ++++-------- .../vendor/whynot-design/.whynot-design-ref | 1 + .../vendor/whynot-design/colors_and_type.css | 273 ++++++++ .../ui/vendor/whynot-design/components.css | 590 +++++++++++++++++ .../vendor/whynot-design/elements/_styles.js | 604 ++++++++++++++++++ .../ui/vendor/whynot-design/elements/atoms.js | 164 +++++ .../vendor/whynot-design/elements/chrome.js | 206 ++++++ .../ui/vendor/whynot-design/elements/form.js | 205 ++++++ .../ui/vendor/whynot-design/elements/icons.js | 45 ++ .../vendor/whynot-design/elements/layout.js | 277 ++++++++ .../ui/vendor/whynot-design/index.js | 35 + .../vendor/whynot-design/tokens/colors.json | 22 + .../ui/vendor/whynot-design/tokens/index.json | 6 + .../vendor/whynot-design/tokens/spacing.json | 28 + .../ui/vendor/whynot-design/tokens/type.json | 33 + 21 files changed, 2903 insertions(+), 354 deletions(-) create mode 100755 projects/coulomb-pricing/scripts/sync-whynot-design.sh create mode 100644 projects/coulomb-pricing/tests/test_ui_vendor.py create mode 100644 projects/coulomb-pricing/ui/vendor/whynot-design/.whynot-design-ref create mode 100644 projects/coulomb-pricing/ui/vendor/whynot-design/colors_and_type.css create mode 100644 projects/coulomb-pricing/ui/vendor/whynot-design/components.css create mode 100644 projects/coulomb-pricing/ui/vendor/whynot-design/elements/_styles.js create mode 100644 projects/coulomb-pricing/ui/vendor/whynot-design/elements/atoms.js create mode 100644 projects/coulomb-pricing/ui/vendor/whynot-design/elements/chrome.js create mode 100644 projects/coulomb-pricing/ui/vendor/whynot-design/elements/form.js create mode 100644 projects/coulomb-pricing/ui/vendor/whynot-design/elements/icons.js create mode 100644 projects/coulomb-pricing/ui/vendor/whynot-design/elements/layout.js create mode 100644 projects/coulomb-pricing/ui/vendor/whynot-design/index.js create mode 100644 projects/coulomb-pricing/ui/vendor/whynot-design/tokens/colors.json create mode 100644 projects/coulomb-pricing/ui/vendor/whynot-design/tokens/index.json create mode 100644 projects/coulomb-pricing/ui/vendor/whynot-design/tokens/spacing.json create mode 100644 projects/coulomb-pricing/ui/vendor/whynot-design/tokens/type.json diff --git a/projects/coulomb-pricing/README.md b/projects/coulomb-pricing/README.md index d3a0185..2498439 100644 --- a/projects/coulomb-pricing/README.md +++ b/projects/coulomb-pricing/README.md @@ -35,12 +35,15 @@ cd projects/coulomb-pricing python3 -m pytest -q python3 -m observatory --period 2026-06 python3 -m observatory --period 2026-06 --output reports/economics-2026-06.md +./scripts/sync-whynot-design.sh # pull whynot-design tokens/CSS/components python3 -m observatory.server ``` Open **http://127.0.0.1:8765/** for the Economic Observatory UI (served from -`ui/`, data via `/api/dashboard`). Design reference: -https://claude.ai/design/p/fb2eef8c-c1fc-4c75-bff4-3782552e5511 +`ui/`, data via `/api/dashboard`). The UI consumes **whynot-design** (Layer 1 CSS ++ Layer 2 `` components) from `ui/vendor/whynot-design/` — re-run the sync +script after bumping the pinned ref in `.whynot-design-ref`. Claude atelier mock +(reference only): https://claude.ai/design/p/fb2eef8c-c1fc-4c75-bff4-3782552e5511 Manual ledgers will be replaced by Bubble (Sprint 2), Stripe (Sprint 3), and OpenRouter (Sprint 4) importers. \ No newline at end of file diff --git a/projects/coulomb-pricing/observatory/server.py b/projects/coulomb-pricing/observatory/server.py index fa6774b..5f0472b 100644 --- a/projects/coulomb-pricing/observatory/server.py +++ b/projects/coulomb-pricing/observatory/server.py @@ -11,6 +11,13 @@ from .api import build_dashboard_payload ROOT = Path(__file__).resolve().parent.parent UI_DIR = ROOT / "ui" +UI_CONTENT_TYPES = { + ".css": "text/css", + ".js": "application/javascript", + ".json": "application/json", + ".html": "text/html; charset=utf-8", +} + class ObservatoryHandler(BaseHTTPRequestHandler): data_dir: Path = ROOT / "data" @@ -38,8 +45,10 @@ class ObservatoryHandler(BaseHTTPRequestHandler): relative = parsed.path.removeprefix("/ui/") target = UI_DIR / relative if target.exists() and target.is_file(): - content_type = "text/css" if target.suffix == ".css" else "application/javascript" - return self._serve_file(target, f"{content_type}; charset=utf-8") + content_type = UI_CONTENT_TYPES.get(target.suffix, "application/octet-stream") + if "charset" not in content_type: + content_type = f"{content_type}; charset=utf-8" + return self._serve_file(target, content_type) self._send(404, b"Not found", "text/plain") diff --git a/projects/coulomb-pricing/scripts/sync-whynot-design.sh b/projects/coulomb-pricing/scripts/sync-whynot-design.sh new file mode 100755 index 0000000..714d955 --- /dev/null +++ b/projects/coulomb-pricing/scripts/sync-whynot-design.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Synchronises the vendored copy of whynot-design from a pinned upstream commit. +# Source: ~/whynot-design (worktree) or a clone from gitea. +# +# Usage: ./scripts/sync-whynot-design.sh [] +# Default: reads .whynot-design-ref from the vendor directory, else HEAD. +# +# Pulls Layer 1 (tokens + CSS) and Layer 2 (Lit web components) so the +# observatory UI picks up design-system changes on re-run. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VENDOR_DIR="$ROOT/ui/vendor/whynot-design" +REF_FILE="$VENDOR_DIR/.whynot-design-ref" +SRC_REPO="${WHYNOT_DESIGN_SRC:-$HOME/whynot-design}" + +REF="${1:-}" +if [[ -z "$REF" && -f "$REF_FILE" ]]; then + REF="$(cat "$REF_FILE")" +fi +if [[ -z "$REF" ]]; then + REF="HEAD" +fi + +if [[ ! -d "$SRC_REPO/.git" ]]; then + echo "Source not found: $SRC_REPO" >&2 + echo "Set WHYNOT_DESIGN_SRC or clone gitea:whynot/whynot-design there." >&2 + exit 1 +fi + +mkdir -p "$VENDOR_DIR/tokens" "$VENDOR_DIR/elements" + +git -C "$SRC_REPO" show "$REF:src/styles/colors_and_type.css" > "$VENDOR_DIR/colors_and_type.css" +git -C "$SRC_REPO" show "$REF:src/styles/components.css" > "$VENDOR_DIR/components.css" +git -C "$SRC_REPO" show "$REF:src/index.js" > "$VENDOR_DIR/index.js" + +for f in atoms.js chrome.js form.js icons.js layout.js _styles.js; do + git -C "$SRC_REPO" show "$REF:src/elements/$f" > "$VENDOR_DIR/elements/$f" +done + +for f in colors.json type.json spacing.json index.json; do + git -C "$SRC_REPO" show "$REF:tokens/$f" > "$VENDOR_DIR/tokens/$f" +done + +git -C "$SRC_REPO" rev-parse "$REF" > "$REF_FILE" + +echo "Vendor synced → $VENDOR_DIR (ref: $(cat "$REF_FILE"))" \ No newline at end of file diff --git a/projects/coulomb-pricing/tests/test_ui_vendor.py b/projects/coulomb-pricing/tests/test_ui_vendor.py new file mode 100644 index 0000000..b69466e --- /dev/null +++ b/projects/coulomb-pricing/tests/test_ui_vendor.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import importlib +import threading +from http.server import HTTPServer +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parent.parent +VENDOR = ROOT / "ui" / "vendor" / "whynot-design" + +REQUIRED_VENDOR_FILES = [ + VENDOR / ".whynot-design-ref", + VENDOR / "colors_and_type.css", + VENDOR / "components.css", + VENDOR / "index.js", + VENDOR / "elements" / "atoms.js", + VENDOR / "elements" / "chrome.js", + VENDOR / "elements" / "form.js", + VENDOR / "elements" / "layout.js", + VENDOR / "elements" / "icons.js", + VENDOR / "elements" / "_styles.js", + VENDOR / "tokens" / "colors.json", +] + + +def test_vendor_tree_is_complete() -> None: + missing = [path for path in REQUIRED_VENDOR_FILES if not path.exists()] + assert not missing, f"Missing vendored whynot-design files: {missing}" + + +def test_vendor_ref_is_pinned() -> None: + ref = (VENDOR / ".whynot-design-ref").read_text(encoding="utf-8").strip() + assert len(ref) == 40 + + +def test_server_serves_vendor_modules() -> None: + server_module = importlib.import_module("observatory.server") + handler = server_module.ObservatoryHandler + handler.data_dir = ROOT / "data" + + httpd = HTTPServer(("127.0.0.1", 0), handler) + port = httpd.server_address[1] + thread = threading.Thread(target=httpd.serve_forever, daemon=True) + thread.start() + + try: + import urllib.request + + index = urllib.request.urlopen(f"http://127.0.0.1:{port}/", timeout=2) + assert "wn-top-nav" in index.read().decode("utf-8") + + module = urllib.request.urlopen( + f"http://127.0.0.1:{port}/ui/vendor/whynot-design/index.js", + timeout=2, + ) + assert module.headers["Content-Type"].startswith("application/javascript") + assert "defineAtoms" in module.read().decode("utf-8") + finally: + httpd.shutdown() + thread.join(timeout=2) \ No newline at end of file diff --git a/projects/coulomb-pricing/ui/app.js b/projects/coulomb-pricing/ui/app.js index a917d5c..46d2583 100644 --- a/projects/coulomb-pricing/ui/app.js +++ b/projects/coulomb-pricing/ui/app.js @@ -9,10 +9,11 @@ async function loadDashboard(period) { function setBadge(el, status) { el.textContent = status; - el.classList.toggle("ok", status === "generating" || status === "neutral"); + el.active = status === "generating" || status === "neutral"; + el.draft = status === "burning"; } -function renderHero(data) { +function renderMetrics(data) { const { snapshot, liquidity } = data; const cards = [ { @@ -47,14 +48,14 @@ function renderHero(data) { }, ]; - document.getElementById("hero-grid").innerHTML = cards + document.getElementById("metric-grid").innerHTML = cards .map( (card) => ` -
-
${card.label}
-
${card.value}
-
${card.sub}
-
` + + ${card.label} +
${card.value}
+ ${card.sub} +
` ) .join(""); } @@ -64,9 +65,13 @@ function renderBudget(data) { const initial = Number(liquidity.initial_budget); const remaining = Number(liquidity.remaining_budget); const pct = Math.max(0, Math.min(100, (remaining / initial) * 100)); + const banner = document.getElementById("budget-banner"); + const caption = remaining >= 0 ? "Within allocated budget" : "Over allocated budget"; + document.getElementById("budget-fill").style.width = `${pct}%`; - document.getElementById("budget-caption").textContent = - remaining >= 0 ? "Within allocated budget" : "Over allocated budget"; + document.getElementById("budget-caption").textContent = caption; + banner.variant = remaining < initial * 0.25 ? "warn" : "info"; + document.getElementById("budget-stats").innerHTML = [ ["Initial budget", euro(initial)], ["Cumulative member payments (net)", euro(liquidity.cumulative_member_payments)], @@ -74,11 +79,10 @@ function renderBudget(data) { ["Cumulative processing", euro(liquidity.cumulative_payment_processing_cost)], ] .map( - ([k, v]) => ` -
-
${k}
-
${v}
-
` + ([label, value]) => ` + + ${value} + ` ) .join(""); } @@ -89,15 +93,15 @@ function renderChart(history) { .map((row) => { const value = Number(row.net_liquidity); const width = (Math.abs(value) / max) * 50; - const bar = - value < 0 - ? `
` - : `
`; + const barClass = value < 0 ? "obs-liquidity-row__bar--neg" : "obs-liquidity-row__bar--pos"; + const valueClass = value < 0 ? "obs-liquidity-row__value--neg" : ""; return ` -
-
${row.period}
-
${bar}
-
${euro(value)}
+
+
${row.period}
+
+
+
+
${euro(value)}
`; }) .join(""); @@ -107,18 +111,19 @@ function renderInfra(data) { const domains = data.infrastructure.domains?.domains ?? []; const servers = data.infrastructure.virtual_servers?.servers ?? []; const stripe = data.infrastructure.stripe?.membership ?? {}; + const payout = data.infrastructure.stripe?.payout_account ?? "payout"; const items = [ ...domains.map( (d) => - `
${d.name}${d.monthly_eur} EUR/mo · ${d.tld}
` + `${d.monthly_eur} EUR/mo · ${d.tld}` ), ...servers.map( (s) => - `
${s.name}${s.monthly_eur} EUR/mo · since ${s.started}
` + `${s.monthly_eur} EUR/mo · since ${s.started}` ), - `
Stripe · ${stripe.member_username ?? "member"}${stripe.gross_monthly_eur} gross · ${stripe.fee_monthly_eur} fee · ${stripe.net_payout_monthly_eur} net to ${data.infrastructure.stripe?.payout_account ?? "payout"}
`, + `${stripe.gross_monthly_eur} gross · ${stripe.fee_monthly_eur} fee · ${stripe.net_payout_monthly_eur} net to ${payout}`, ]; - document.getElementById("infra-stack").innerHTML = `
${items.join("")}
`; + document.getElementById("infra-stack").innerHTML = `
${items.join("")}
`; } function renderTables(data) { @@ -126,12 +131,12 @@ function renderTables(data) { .map( (row) => ` - ${row.period} - ${row.active_members} - ${euro(row.gross_revenue)} - ${euro(row.infrastructure_cost)} - ${euro(row.payment_processing_cost)} - ${euro(row.net_liquidity)} + ${row.period} + ${row.active_members} + ${euro(row.gross_revenue)} + ${euro(row.infrastructure_cost)} + ${euro(row.payment_processing_cost)} + ${euro(row.net_liquidity)} ` ) .join(""); @@ -140,10 +145,10 @@ function renderTables(data) { .map( (model) => ` - ${model.id} + ${model.id} ${model.name} - ${model.model_type} - ${model.status} + ${model.model_type} + ${model.status} ` ) .join(""); @@ -154,16 +159,35 @@ function populatePeriods(history, current) { select.innerHTML = [...history].reverse().map( (row) => `` ); - select.onchange = async () => { - const next = await loadDashboard(select.value); - render(next); - }; + select.value = current; + if (!select._obsBound) { + select.addEventListener("wn-change", async (event) => { + const next = await loadDashboard(event.detail.value); + render(next); + }); + select._obsBound = true; + } +} + +async function loadDesignRefLabel() { + try { + const response = await fetch("/ui/vendor/whynot-design/.whynot-design-ref"); + if (!response.ok) return; + const ref = (await response.text()).trim().slice(0, 7); + const label = document.getElementById("design-ref-label"); + if (label && ref) label.textContent = `whynot-design @ ${ref}`; + } catch { + // optional footer detail + } } function render(data) { document.getElementById("design-link").href = data.design_reference; + loadDesignRefLabel(); + const header = document.getElementById("page-header"); + header.lede = `Period ${data.period}. Ledger-backed view of infrastructure spend, member payments, and remaining budget. Customer cost-pass-through billing is not active.`; setBadge(document.getElementById("liquidity-badge"), data.snapshot.liquidity_status); - renderHero(data); + renderMetrics(data); renderBudget(data); renderChart(data.history); renderInfra(data); @@ -174,5 +198,5 @@ function render(data) { loadDashboard() .then(render) .catch((error) => { - document.body.innerHTML = `
${error}
`; + document.body.innerHTML = `
${error}
`; }); \ No newline at end of file diff --git a/projects/coulomb-pricing/ui/index.html b/projects/coulomb-pricing/ui/index.html index e05ba7e..5f4ed11 100644 --- a/projects/coulomb-pricing/ui/index.html +++ b/projects/coulomb-pricing/ui/index.html @@ -3,71 +3,95 @@ - Coulomb Economic Observatory + Coulomb · Economic Observatory + + + + -
-
-
-

Adaptive Pricing · Coulomb Social MVP

-

Economic Observatory

-
-
- - -
-
+ +
+ Period + +
+ +
-
+
+ -
-
-

Liquidity & Budget

-

Operator liquidity pool

+
+
+ Snapshot +
-
-
-
-
+
-
-
-
-

Monthly Net Liquidity

-

Member net payments minus infrastructure

+
+
+ Liquidity & budget +
+
+ +

+
+ +
+
+ +
+
+
+ Monthly net liquidity +
-
+

Member net payments minus infrastructure, by period.

+
-
-
-

Infrastructure Stack

-

Domains, hosting, and Stripe reference

+
+
+ Infrastructure stack +
+

Domains, hosting, and Stripe reference rates.

-
-
-

Monthly Ledger

-

Computed from expense and payment record tables

+
+
+ Monthly ledger +
-
- +

Computed from expense and payment record tables.

+
+
- - - - + + + + @@ -75,12 +99,13 @@ -
-
-

Pricing Model Registry

+
+
+ Pricing model registry +
-
-
Period MembersGrossInfrastructureProcessingNet liquidityGrossInfrastructureProcessingNet liquidity
+
+
@@ -94,14 +119,17 @@ -
-

- Design reference: +

+

+ Design: Claude design share - · Totals computed programmatically from ledgers · Customer cost-pass-through billing not active + · + whynot-design + · totals computed programmatically from ledgers

- + + \ No newline at end of file diff --git a/projects/coulomb-pricing/ui/styles.css b/projects/coulomb-pricing/ui/styles.css index 75b638d..32994ed 100644 --- a/projects/coulomb-pricing/ui/styles.css +++ b/projects/coulomb-pricing/ui/styles.css @@ -1,309 +1,196 @@ -:root { - color-scheme: dark; - --bg: #0b1020; - --panel: #121a2f; - --panel-border: #24304d; - --text: #e8edf8; - --muted: #93a0bf; - --accent: #5eead4; - --accent-soft: rgba(94, 234, 212, 0.12); - --warn: #fb7185; - --warn-soft: rgba(251, 113, 133, 0.14); - --ok: #86efac; - --ok-soft: rgba(134, 239, 172, 0.14); - --shadow: 0 18px 50px rgba(0, 0, 0, 0.28); - font-family: "Segoe UI", system-ui, sans-serif; -} - -* { - box-sizing: border-box; -} +/* Observatory layout — extends whynot-design (Layer 1). No gradients, no shadows on cards. */ body { - margin: 0; - background: - radial-gradient(circle at top left, rgba(94, 234, 212, 0.08), transparent 28%), - radial-gradient(circle at top right, rgba(96, 165, 250, 0.08), transparent 24%), - var(--bg); - color: var(--text); + background: var(--paper); } -.app { - max-width: 1200px; +.obs-main { + max-width: 1080px; margin: 0 auto; - padding: 28px 20px 48px; + padding: var(--sp-6) var(--sp-5) var(--sp-9); } -.topbar, -.panel, -.hero-card { - background: var(--panel); - border: 1px solid var(--panel-border); - border-radius: 18px; - box-shadow: var(--shadow); -} - -.topbar { - display: flex; - justify-content: space-between; - gap: 20px; - align-items: center; - padding: 22px 24px; - margin-bottom: 20px; -} - -.eyebrow { - margin: 0 0 6px; - color: var(--muted); - font-size: 0.82rem; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -h1, -h2 { - margin: 0; - font-weight: 650; -} - -.topbar-actions { - display: flex; - gap: 14px; - align-items: end; -} - -label { +.obs-period { display: grid; gap: 6px; - color: var(--muted); - font-size: 0.82rem; + min-width: 132px; } -select { - background: #0d1426; - color: var(--text); - border: 1px solid var(--panel-border); - border-radius: 10px; - padding: 10px 12px; - min-width: 140px; +wn-top-nav [slot="right"] { + display: flex; + align-items: end; + gap: var(--sp-3); } -.badge { - padding: 10px 14px; - border-radius: 999px; - font-size: 0.85rem; - font-weight: 600; - background: var(--warn-soft); - color: var(--warn); +.obs-section { + margin-bottom: var(--sp-7); } -.badge.ok { - background: var(--ok-soft); - color: var(--ok); +.obs-section__head { + display: flex; + align-items: center; + gap: var(--sp-3); + margin-bottom: var(--sp-4); } -.hero-grid { +.obs-section__rule { + flex: 1; + border-top: 1px solid var(--border-soft); +} + +.obs-section-note { + margin: 0 0 var(--sp-4); + max-width: 60ch; +} + +.obs-metric-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 14px; - margin-bottom: 20px; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--sp-3); } -.hero-card { - padding: 18px 18px 16px; +.obs-metric__value { + font: 500 28px/1.1 var(--ff-sans); + letter-spacing: -0.02em; + color: var(--fg-1); + font-variant-numeric: tabular-nums; } -.hero-card .label { - color: var(--muted); - font-size: 0.82rem; - margin-bottom: 8px; +.obs-budget-meter { + height: 8px; + border: 1px solid var(--border); + background: var(--paper-2); + margin-bottom: var(--sp-4); } -.hero-card .value { - font-size: 1.7rem; - font-weight: 700; - letter-spacing: -0.03em; -} - -.hero-card .sub { - margin-top: 8px; - color: var(--muted); - font-size: 0.85rem; -} - -.panel { - padding: 20px 22px; - margin-bottom: 20px; -} - -.panel-head { - margin-bottom: 16px; -} - -.panel-head p { - margin: 6px 0 0; - color: var(--muted); - font-size: 0.92rem; -} - -.split-grid { - display: grid; - grid-template-columns: 1.4fr 1fr; - gap: 20px; -} - -.budget-bar { - height: 14px; - border-radius: 999px; - background: #0d1426; - overflow: hidden; - margin-bottom: 14px; -} - -.budget-fill { +.obs-budget-meter__fill { height: 100%; width: 0; - background: linear-gradient(90deg, var(--accent), #60a5fa); - transition: width 0.35s ease; + background: var(--ink); + transition: width 0.25s ease; } -.budget-stats { +.obs-field-sheet { + border: 1px solid var(--border); + border-radius: var(--r-2); + background: var(--paper); + padding: 0 var(--sp-4); +} + +.obs-split { display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 12px; + grid-template-columns: 1.35fr 1fr; + gap: var(--sp-6); + margin-bottom: var(--sp-2); } -.stat { - padding: 12px 14px; - border-radius: 12px; - background: rgba(255, 255, 255, 0.02); - border: 1px solid rgba(255, 255, 255, 0.04); +.obs-liquidity-list { + border: 1px solid var(--border); + border-radius: var(--r-2); + background: var(--paper); + padding: 0 var(--sp-4); } -.stat .k { - color: var(--muted); - font-size: 0.8rem; -} - -.stat .v { - margin-top: 6px; - font-size: 1.1rem; - font-weight: 650; -} - -.chart { +.obs-liquidity-row { display: grid; - gap: 10px; -} - -.chart-row { - display: grid; - grid-template-columns: 72px 1fr 72px; - gap: 10px; + grid-template-columns: 72px 1fr 88px; + gap: var(--sp-3); align-items: center; + padding: var(--sp-3) 0; + border-bottom: 1px solid var(--border-soft); } -.chart-label, -.chart-value { - font-size: 0.82rem; - color: var(--muted); +.obs-liquidity-row:last-child { + border-bottom: 0; } -.chart-value { - text-align: right; +.obs-liquidity-row__period { + font: 400 12px var(--ff-mono); + color: var(--fg-3); } -.chart-bar-wrap { - height: 28px; - background: #0d1426; - border-radius: 8px; - overflow: hidden; - position: relative; -} - -.chart-bar { - position: absolute; - top: 0; - bottom: 0; - border-radius: 8px; -} - -.chart-bar.neg { - left: 50%; - background: linear-gradient(90deg, transparent, var(--warn)); -} - -.chart-bar.pos { - right: 50%; - background: linear-gradient(270deg, transparent, var(--ok)); -} - -.infra-list { - display: grid; - gap: 10px; -} - -.infra-item { - padding: 12px 14px; - border-radius: 12px; - background: rgba(255, 255, 255, 0.02); - border: 1px solid rgba(255, 255, 255, 0.04); -} - -.infra-item strong { - display: block; - margin-bottom: 4px; -} - -.infra-item span { - color: var(--muted); - font-size: 0.88rem; -} - -.table-wrap { - overflow-x: auto; -} - -table { - width: 100%; - border-collapse: collapse; -} - -th, -td { - padding: 11px 10px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - text-align: left; - font-size: 0.92rem; -} - -th { - color: var(--muted); - font-weight: 600; -} - -td.num, -th.num { +.obs-liquidity-row__value { + font: 500 12px var(--ff-mono); + color: var(--fg-1); text-align: right; font-variant-numeric: tabular-nums; } -.footer { - color: var(--muted); - font-size: 0.86rem; +.obs-liquidity-row__value--neg { + color: var(--fg-2); } -.footer a { - color: var(--accent); +.obs-liquidity-row__bar-wrap { + height: 10px; + border: 1px solid var(--border-soft); + background: var(--paper-2); + position: relative; +} + +.obs-liquidity-row__bar { + position: absolute; + top: 0; + bottom: 0; + background: var(--ink); +} + +.obs-liquidity-row__bar--neg { + right: 50%; + background: var(--ink-4); +} + +.obs-liquidity-row__bar--pos { + left: 50%; + background: var(--ink); +} + +.obs-infra-list { + border: 1px solid var(--border); + border-radius: var(--r-2); + background: var(--paper); + padding: 0 var(--sp-4); +} + +.obs-table-wrap { + border: 1px solid var(--border); + border-radius: var(--r-2); + overflow-x: auto; + background: var(--paper); +} + +.obs-table { + width: 100%; + margin: 0; +} + +.obs-num { + text-align: right; + font-variant-numeric: tabular-nums; +} + +.obs-footer { + padding-top: var(--sp-5); + border-top: 1px solid var(--border); +} + +.obs-footer a { + color: var(--fg-1); + text-decoration: underline; + text-underline-offset: 2px; +} + +wn-banner { + display: block; + margin-bottom: var(--sp-4); } @media (max-width: 900px) { - .split-grid { + .obs-split { grid-template-columns: 1fr; } - .topbar { - flex-direction: column; - align-items: flex-start; + wn-top-nav [slot="right"] { + width: 100%; + justify-content: space-between; } } \ No newline at end of file diff --git a/projects/coulomb-pricing/ui/vendor/whynot-design/.whynot-design-ref b/projects/coulomb-pricing/ui/vendor/whynot-design/.whynot-design-ref new file mode 100644 index 0000000..f638832 --- /dev/null +++ b/projects/coulomb-pricing/ui/vendor/whynot-design/.whynot-design-ref @@ -0,0 +1 @@ +9b9f3728937ca308966de9c62accdb00c8cf5b0e diff --git a/projects/coulomb-pricing/ui/vendor/whynot-design/colors_and_type.css b/projects/coulomb-pricing/ui/vendor/whynot-design/colors_and_type.css new file mode 100644 index 0000000..d64a8ff --- /dev/null +++ b/projects/coulomb-pricing/ui/vendor/whynot-design/colors_and_type.css @@ -0,0 +1,273 @@ +/* ============================================================ + WhyNot Design System — Colors & Type + ------------------------------------------------------------ + Neutral, mostly black/white. Color is used SPARINGLY — only + one warm accent (annotation yellow) borrowed from the LEGO + brick in the logo. The system favours light grey wireframe + artefacts over heavy fills. + ============================================================ */ + +/* ---------- Webfonts (Google Fonts, see /fonts for offline) ---------- */ +@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600;700&family=IBM+Plex+Serif:ital,wght@0,400;0,500;1,400&display=swap"); + +:root { + /* ---------- Base palette: neutrals ---------- */ + --ink: #0A0A0A; /* near-black, the only "fill" most of the time */ + --ink-2: #1F1F1F; + --ink-3: #5C5C5C; + --ink-4: #8A8A8A; + --ink-5: #B5B5B3; /* placeholder text, wireframe labels */ + --line: #E5E5E2; /* default 1px wireframe rule */ + --line-strong: #C9C9C5; /* dividers between sections */ + --line-soft: #F0F0EC; /* hairline within a card */ + --paper: #FFFFFF; /* canvas */ + --paper-2: #FAFAF7; /* sheet, dim canvas */ + --paper-3: #F4F4EF; /* recessed surface, code block bg */ + + /* ---------- Foreground / background semantic ---------- */ + --fg-1: var(--ink); + --fg-2: var(--ink-3); + --fg-3: var(--ink-4); + --fg-mute: var(--ink-5); + --fg-on-dark: #FAFAF7; + + --bg-1: var(--paper); + --bg-2: var(--paper-2); + --bg-3: var(--paper-3); + --bg-invert: var(--ink); + + --border: var(--line); + --border-strong: var(--line-strong); + --border-soft: var(--line-soft); + + /* ---------- The single accent: annotation yellow ---------- */ + /* Lifted from the LEGO brick. Used as highlighter, "draft" + stamp, signal-marker. Never as a button fill. */ + --hi: #FFE14A; + --hi-2: #FFD400; + --hi-ink: #1A1500; /* text on yellow */ + + /* ---------- Status (for prototype lifecycle, signal strength) ---------- */ + /* Kept deliberately desaturated so they read as labels, not UI. */ + --status-raw: #B5B5B3; /* S0 — no signal */ + --status-weak: #8A8A8A; /* S1 — weak signal */ + --status-medium: #5C5C5C; /* S2 — medium signal */ + --status-strong: #0A0A0A; /* S3 — strong signal */ + --status-commercial: #FFD400; /* S4 — commercial */ + + /* ---------- Type families ---------- */ + --ff-sans: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif; + --ff-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace; + --ff-serif: "IBM Plex Serif", "Iowan Old Style", Georgia, serif; + + /* ---------- Type scale (modular, ~1.2) ---------- */ + --fs-xs: 11px; + --fs-sm: 13px; + --fs-base: 15px; + --fs-md: 17px; + --fs-lg: 20px; + --fs-xl: 24px; + --fs-2xl: 32px; + --fs-3xl: 44px; + --fs-4xl: 64px; + --fs-5xl: 96px; + + --lh-tight: 1.05; + --lh-snug: 1.25; + --lh-base: 1.5; + --lh-loose: 1.7; + + --tr-tight: -0.02em; + --tr-snug: -0.01em; + --tr-base: 0em; + --tr-mono: 0.02em; + --tr-label: 0.08em; /* uppercase eyebrow labels */ + + /* ---------- Spacing (4px base) ---------- */ + --sp-1: 4px; + --sp-2: 8px; + --sp-3: 12px; + --sp-4: 16px; + --sp-5: 24px; + --sp-6: 32px; + --sp-7: 48px; + --sp-8: 64px; + --sp-9: 96px; + --sp-10: 128px; + + /* ---------- Radii — small, mostly square ---------- */ + --r-0: 0px; + --r-1: 2px; + --r-2: 4px; + --r-3: 8px; + --r-pill: 999px; + + /* ---------- Elevation — almost none. This is a wireframe system. ---------- */ + --shadow-0: none; + --shadow-1: 0 1px 0 var(--line); + --shadow-2: 0 1px 0 var(--line-strong); + --shadow-3: 0 4px 12px -6px rgba(10,10,10,0.10); +} + +/* ============================================================ + Semantic element styles + ============================================================ */ + +html { + font-family: var(--ff-sans); + font-size: var(--fs-base); + line-height: var(--lh-base); + color: var(--fg-1); + background: var(--bg-1); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +body { + margin: 0; + font-feature-settings: "ss01", "cv11"; + text-wrap: pretty; +} + +/* ---------- Headings ---------- */ +h1, .h1 { + font: 600 var(--fs-3xl)/var(--lh-tight) var(--ff-sans); + letter-spacing: var(--tr-tight); + margin: 0 0 var(--sp-5); + color: var(--fg-1); +} +h2, .h2 { + font: 500 var(--fs-2xl)/var(--lh-snug) var(--ff-sans); + letter-spacing: var(--tr-snug); + margin: 0 0 var(--sp-4); +} +h3, .h3 { + font: 500 var(--fs-xl)/var(--lh-snug) var(--ff-sans); + letter-spacing: var(--tr-snug); + margin: 0 0 var(--sp-3); +} +h4, .h4 { + font: 500 var(--fs-lg)/var(--lh-snug) var(--ff-sans); + margin: 0 0 var(--sp-2); +} +h5, .h5 { + font: 500 var(--fs-md)/var(--lh-snug) var(--ff-sans); + margin: 0 0 var(--sp-2); +} + +/* ---------- Display (for hero / title slides) ---------- */ +.display-1 { + font: 300 var(--fs-5xl)/0.95 var(--ff-sans); + letter-spacing: -0.035em; + color: var(--fg-1); +} +.display-2 { + font: 400 var(--fs-4xl)/1.0 var(--ff-sans); + letter-spacing: var(--tr-tight); +} + +/* ---------- Body ---------- */ +p { + margin: 0 0 var(--sp-4); + line-height: var(--lh-base); + color: var(--fg-1); +} +.lead { + font-size: var(--fs-md); + line-height: 1.55; + color: var(--fg-2); +} +small, .small { + font-size: var(--fs-sm); + color: var(--fg-2); +} + +/* ---------- Eyebrow / uppercase labels (very common in this system) ---------- */ +.eyebrow, +.label { + font: 500 var(--fs-xs)/1.2 var(--ff-mono); + letter-spacing: var(--tr-label); + text-transform: uppercase; + color: var(--fg-3); +} + +/* ---------- Code / mono ---------- */ +code, kbd, samp, pre, .mono { + font-family: var(--ff-mono); + font-size: 0.92em; + letter-spacing: var(--tr-mono); +} +code { + background: var(--bg-3); + padding: 1px 6px; + border-radius: var(--r-1); + color: var(--ink-2); +} +pre { + background: var(--bg-3); + border: 1px solid var(--border); + padding: var(--sp-4); + overflow-x: auto; + border-radius: var(--r-2); + font-size: var(--fs-sm); + line-height: var(--lh-snug); +} +pre code { background: none; padding: 0; } + +/* ---------- Editorial serif moments ---------- */ +.serif { font-family: var(--ff-serif); } +.serif-quote { + font: 400 italic var(--fs-xl)/1.4 var(--ff-serif); + color: var(--fg-2); +} + +/* ---------- Links ---------- */ +a { + color: var(--fg-1); + text-decoration: underline; + text-decoration-color: var(--border-strong); + text-underline-offset: 3px; + text-decoration-thickness: 1px; + transition: text-decoration-color 120ms ease, color 120ms ease; +} +a:hover { + text-decoration-color: var(--fg-1); +} + +/* ---------- HR ---------- */ +hr { + border: 0; + border-top: 1px solid var(--border); + margin: var(--sp-5) 0; +} + +/* ---------- Highlighter (the one place yellow appears in body copy) ---------- */ +mark, .mark { + background: var(--hi); + color: var(--hi-ink); + padding: 0 2px; +} + +/* ---------- Tables (used in templates) ---------- */ +table { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-sm); +} +th, td { + text-align: left; + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border); +} +th { + font-weight: 500; + color: var(--fg-2); + font-family: var(--ff-mono); + font-size: var(--fs-xs); + letter-spacing: var(--tr-label); + text-transform: uppercase; +} + +/* ---------- Selection ---------- */ +::selection { background: var(--hi); color: var(--hi-ink); } diff --git a/projects/coulomb-pricing/ui/vendor/whynot-design/components.css b/projects/coulomb-pricing/ui/vendor/whynot-design/components.css new file mode 100644 index 0000000..6fe2814 --- /dev/null +++ b/projects/coulomb-pricing/ui/vendor/whynot-design/components.css @@ -0,0 +1,590 @@ +/* ============================================================ + WhyNot Design System — Component Styles + ------------------------------------------------------------ + Utility classes that the Lit web components render to. These + are also consumable directly from any HTML (no JS required) + for the "Layer 1 only" use case — see MultiFrameworkSupport.md. + ============================================================ */ + +/* ====== Custom-element display defaults ====== + * For shadow-DOM components, the wn-* host has display: inline by default. + * Set sensible defaults so layout works without the consumer specifying them. + */ +wn-eyebrow, wn-tag, wn-stage-dot, wn-phase-dot, wn-stamp, wn-icon, +wn-search-input, wn-button { display: inline-block; } + +wn-card, wn-modal, wn-top-nav, wn-sidebar, wn-page-header, +wn-pipeline, wn-prototype-card, wn-field-row, wn-breadcrumb, +wn-table, wn-banner, wn-empty-state, +wn-input, wn-textarea, wn-select { display: block; } + +wn-toast-region { display: block; } +wn-toast { display: block; } + +wn-sidebar-group, wn-sidebar-item { display: block; } +wn-table-row, wn-table-cell { display: contents; } + +/* host hidden state — needed because shadow-DOM components don't inherit + * `[hidden]` semantics in light DOM. Lit's host attribute reflection + * handles attributes, but `hidden` on the host itself should still work. */ +[hidden] { display: none !important; } + +/* ====== Buttons ====== */ +.wn-btn { + font: 500 13px var(--ff-sans); + letter-spacing: -0.005em; + padding: 9px 16px; + border-radius: var(--r-2); + border: 1px solid var(--border); + background: var(--paper); + color: var(--ink); + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; + white-space: nowrap; + transition: background 120ms ease, border-color 120ms ease, color 120ms ease; + text-decoration: none; + line-height: 1.2; +} +.wn-btn:hover { border-color: var(--ink); } +.wn-btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; } +.wn-btn:active { background: var(--bg-3); } +.wn-btn[disabled], .wn-btn.is-disabled { + color: var(--ink-5); border-color: var(--border); cursor: not-allowed; background: var(--paper); +} + +.wn-btn--primary { background: var(--ink); color: var(--paper); border-color: var(--ink); } +.wn-btn--primary:hover { background: var(--ink-2); border-color: var(--ink-2); } +.wn-btn--primary:active { background: var(--ink); } +.wn-btn--primary[disabled], .wn-btn--primary.is-disabled { + background: var(--ink-5); border-color: var(--ink-5); color: var(--paper); +} + +.wn-btn--ghost { background: transparent; border-color: transparent; padding: 7px 10px; } +.wn-btn--ghost:hover { background: var(--bg-3); border-color: transparent; } + +.wn-btn--danger { background: var(--paper); color: var(--ink); border-color: var(--ink); } + +.wn-btn--sm { padding: 5px 10px; font-size: 12px; } +.wn-btn--lg { padding: 12px 20px; font-size: 14px; } + +.wn-btn__icon { width: 14px; height: 14px; flex: none; } +.wn-btn--lg .wn-btn__icon { width: 16px; height: 16px; } + +/* ====== Eyebrows & labels ====== */ +.wn-eyebrow { + font: 500 11px/1.2 var(--ff-mono); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-3); + display: inline-block; +} +.wn-eyebrow--strong { color: var(--fg-1); } + +/* ====== Tags ====== */ +.wn-tag { + font: 500 10px/1 var(--ff-mono); + letter-spacing: 0.1em; + text-transform: uppercase; + padding: 5px 10px; + border-radius: var(--r-pill); + border: 1px solid var(--border); + color: var(--fg-2); + background: var(--paper); + display: inline-block; + white-space: nowrap; +} +.wn-tag--active { background: var(--ink); color: var(--paper); border-color: var(--ink); } +.wn-tag--draft { background: var(--hi); color: var(--hi-ink); border-color: transparent; } + +/* ====== Stage / Phase dots ====== */ +.wn-dot { + display: inline-flex; + align-items: center; + gap: 6px; + font: 500 10px/1 var(--ff-mono); + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--fg-2); +} +.wn-dot__bullet { width: 8px; height: 8px; border-radius: 999px; background: var(--ink); flex: none; } + +/* signal levels (S0–S4) */ +.wn-stage-dot--s0 .wn-dot__bullet { background: var(--status-raw); } +.wn-stage-dot--s1 .wn-dot__bullet { background: var(--status-weak); } +.wn-stage-dot--s2 .wn-dot__bullet { background: var(--status-medium); } +.wn-stage-dot--s3 .wn-dot__bullet { background: var(--status-strong); } +.wn-stage-dot--s4 .wn-dot__bullet { background: var(--status-commercial); } + +/* phase states (todo / active / done / warn) — numbered phases, distinct from signal */ +.wn-phase-dot__bullet { + width: 18px; height: 18px; border-radius: 999px; + border: 1px solid var(--border-strong); + background: var(--paper); + display: inline-flex; align-items: center; justify-content: center; + font: 500 10px/1 var(--ff-mono); color: var(--fg-3); + flex: none; +} +.wn-phase-dot--todo .wn-phase-dot__bullet { border-color: var(--border-strong); color: var(--fg-3); background: var(--paper); } +.wn-phase-dot--active .wn-phase-dot__bullet { border-color: var(--ink); color: var(--ink); background: var(--paper); box-shadow: 0 0 0 3px rgba(10,10,10,0.06); } +.wn-phase-dot--done .wn-phase-dot__bullet { border-color: var(--ink); color: var(--paper); background: var(--ink); } +.wn-phase-dot--warn .wn-phase-dot__bullet { border-color: var(--hi-2); color: var(--hi-ink); background: var(--hi); } + +/* ====== Stamp ====== */ +.wn-stamp { + display: inline-block; + background: var(--hi); + color: var(--hi-ink); + padding: 5px 10px 3px; + font: 500 10px/1 var(--ff-mono); + letter-spacing: 0.12em; + text-transform: uppercase; + transform: rotate(-1.5deg); +} + +/* ====== Icon ====== */ +.wn-icon { stroke-width: 1.5; stroke: currentColor; fill: none; display: inline-block; vertical-align: middle; } +.wn-icon--sm { width: 14px; height: 14px; } +.wn-icon--md { width: 16px; height: 16px; } +.wn-icon--lg { width: 20px; height: 20px; } +.wn-icon--xl { width: 24px; height: 24px; } + +/* ====== Card ====== */ +.wn-card { + background: var(--paper); + border: 1px solid var(--border); + border-radius: var(--r-2); + padding: var(--sp-5); + display: flex; + flex-direction: column; + gap: var(--sp-3); + position: relative; +} +.wn-card--inset { background: var(--paper-2); border-color: var(--border); } +.wn-card--recessed { background: var(--paper-3); } +.wn-card--lg { padding: var(--sp-6); border-radius: var(--r-3); } +.wn-card--sm { padding: var(--sp-4); gap: var(--sp-2); } +.wn-card--clickable { cursor: pointer; transition: border-color 120ms ease; } +.wn-card--clickable:hover { border-color: var(--ink); } +.wn-card--clickable:hover::before { + content: ""; position: absolute; left: -1px; top: -1px; bottom: -1px; + width: 2px; background: var(--ink); border-radius: 2px 0 0 2px; +} +.wn-card__head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-3); } +.wn-card__title { font: 500 17px/1.35 var(--ff-sans); margin: 4px 0 8px; color: var(--fg-1); } +.wn-card__foot { + display: flex; justify-content: space-between; gap: var(--sp-3); + padding-top: var(--sp-3); margin-top: 4px; + border-top: 1px solid var(--border-soft); + font: 500 11px var(--ff-mono); letter-spacing: 0.06em; text-transform: uppercase; + color: var(--fg-3); +} + +/* ====== Field row (label + value, 3-col grid) ====== */ +.wn-field-row { + display: grid; + grid-template-columns: 200px 1fr auto; + gap: var(--sp-4) var(--sp-5); + padding: var(--sp-3) 0; + border-bottom: 1px solid var(--border-soft); + align-items: baseline; +} +.wn-field-row:last-child { border-bottom: 0; } +.wn-field-row__label { + font: 500 11px/1.5 var(--ff-mono); + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--fg-3); +} +.wn-field-row__value { font: 400 15px/1.55 var(--ff-sans); color: var(--fg-1); } +.wn-field-row__aside { font: 400 12px var(--ff-mono); color: var(--fg-3); text-align: right; } +.wn-field-row--stacked { grid-template-columns: 1fr; gap: 6px; } +.wn-field-row--narrow { grid-template-columns: 120px 1fr; } + +/* ====== Form inputs ====== */ +.wn-form-label { + font: 500 11px/1 var(--ff-mono); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-3); + display: block; + margin-bottom: 6px; +} +.wn-input, .wn-textarea, .wn-select { + font: 400 14px var(--ff-sans); + padding: 10px 12px; + border: 1px solid var(--border); + background: var(--paper); + border-radius: var(--r-1); + color: var(--fg-1); + outline: none; + width: 100%; + transition: border-color 120ms ease; + box-sizing: border-box; +} +.wn-input:hover, .wn-textarea:hover, .wn-select:hover { border-color: var(--border-strong); } +.wn-input:focus, .wn-textarea:focus, .wn-select:focus { border-color: var(--ink); } +.wn-input::placeholder, .wn-textarea::placeholder { color: var(--ink-5); } +.wn-input[disabled], .wn-textarea[disabled], .wn-select[disabled] { + background: var(--paper-2); color: var(--fg-3); cursor: not-allowed; +} +.wn-textarea { resize: vertical; min-height: 96px; font-family: var(--ff-sans); } +.wn-select { + appearance: none; -webkit-appearance: none; + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position: right 12px center; + padding-right: 32px; +} + +.wn-input--error, .wn-textarea--error, .wn-select--error { + border-color: var(--ink); border-bottom-width: 2px; +} +.wn-form-help { font: 400 11px var(--ff-mono); color: var(--fg-3); margin-top: 6px; display: block; } +.wn-form-error { font: 400 11px var(--ff-mono); color: var(--ink); margin-top: 6px; display: block; } + +/* Search input — extracted from TopNav, also usable standalone */ +.wn-search { + display: inline-flex; + align-items: center; + gap: 10px; + border: 1px solid var(--border); + padding: 6px 10px; + border-radius: var(--r-1); + background: var(--paper); + color: var(--fg-3); + font: 400 12px var(--ff-mono); + min-width: 200px; + transition: border-color 120ms ease; +} +.wn-search:focus-within { border-color: var(--ink); } +.wn-search input { + border: 0; outline: 0; background: none; flex: 1; + font: inherit; color: var(--fg-1); padding: 0; +} +.wn-search input::placeholder { color: var(--ink-5); } +.wn-search__kbd { + padding: 1px 5px; + border: 1px solid var(--border); + border-radius: 2px; + font-size: 10px; + color: var(--fg-3); +} + +/* ====== Breadcrumb ====== */ +.wn-breadcrumb { + display: flex; flex-wrap: wrap; align-items: center; + gap: 6px; + font: 400 12px/1.5 var(--ff-mono); + color: var(--fg-3); + margin-bottom: var(--sp-4); +} +.wn-breadcrumb a { + color: var(--fg-2); + text-decoration: none; + padding: 2px 0; + border-bottom: 1px solid transparent; + transition: border-color 120ms ease, color 120ms ease; +} +.wn-breadcrumb a:hover { color: var(--fg-1); border-bottom-color: var(--border-strong); } +.wn-breadcrumb__sep { color: var(--ink-5); user-select: none; } +.wn-breadcrumb__current { color: var(--fg-1); } + +/* ====== Modal / Dialog ====== */ +.wn-modal__backdrop { + position: fixed; inset: 0; + background: rgba(10, 10, 10, 0.40); + display: flex; align-items: center; justify-content: center; + z-index: 100; + padding: var(--sp-5); +} +.wn-modal__panel { + background: var(--paper); + border-radius: var(--r-3); + box-shadow: var(--shadow-3); + max-width: 560px; width: 100%; + max-height: calc(100vh - 64px); + display: flex; flex-direction: column; + overflow: hidden; +} +.wn-modal__head { + padding: var(--sp-5) var(--sp-6) var(--sp-4); + border-bottom: 1px solid var(--border); + display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4); +} +.wn-modal__title { font: 500 20px/1.25 var(--ff-sans); margin: 0; color: var(--fg-1); } +.wn-modal__close { + background: none; border: 0; cursor: pointer; padding: 4px; + color: var(--fg-3); border-radius: var(--r-1); + transition: color 120ms ease; +} +.wn-modal__close:hover { color: var(--fg-1); } +.wn-modal__body { + padding: var(--sp-5) var(--sp-6); + overflow-y: auto; + flex: 1; + font: 400 15px/1.6 var(--ff-sans); + color: var(--fg-1); +} +.wn-modal__foot { + padding: var(--sp-4) var(--sp-6) var(--sp-5); + border-top: 1px solid var(--border); + display: flex; justify-content: flex-end; gap: var(--sp-2); +} + +/* ====== Table ====== + * Note: shadow-DOM-rendered rows can't be children of a real
ID
(the + * HTML table model rejects unknown elements between
and ). The + * component therefore renders a CSS-grid imitation. For real + *
markup (Django QuerySet rendering, etc.) use these classes + * directly on
//
elements — see also the .wn-table--native + * variant below. + */ + +/* CSS-grid imitation (default ) */ +.wn-table { + width: 100%; + font-size: var(--fs-sm); + display: flex; + flex-direction: column; +} +.wn-table__thead { border-bottom: 1px solid var(--border); } +.wn-table__tbody { display: flex; flex-direction: column; } +.wn-table__tr { + display: grid; + gap: var(--sp-4); + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border-soft); + align-items: baseline; +} +.wn-table__tr:last-child { border-bottom: 0; } +.wn-table__tr--head { border-bottom: 0; padding: var(--sp-3) var(--sp-4); } +.wn-table__th { + font: 500 11px/1.2 var(--ff-mono); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-3); +} +.wn-table__td { + color: var(--fg-1); + line-height: 1.5; + font-size: var(--fs-sm); +} +.wn-table--compact .wn-table__tr { padding: var(--sp-2) var(--sp-3); } +.wn-table__cell--mono { font-family: var(--ff-mono); color: var(--fg-2); font-size: 12px; } +.wn-table__cell--meta { color: var(--fg-3); font: 400 12px var(--ff-mono); } +.wn-table__cell--right { text-align: right; } + +/* Native variant — for Django QuerySet rendering etc. */ +.wn-table--native { + border-collapse: collapse; + display: table; +} +.wn-table--native thead th { + text-align: left; + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border); + font: 500 11px/1.2 var(--ff-mono); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-3); +} +.wn-table--native tbody td { + padding: var(--sp-4); + border-bottom: 1px solid var(--border-soft); + vertical-align: top; + color: var(--fg-1); + font-size: var(--fs-sm); + line-height: 1.5; +} +.wn-table--native tbody tr:hover { background: var(--paper-2); } +.wn-table--native tbody tr:last-child td { border-bottom: 0; } + +/* ====== Banner / Toast (success / info / warn) ====== */ +.wn-banner { + display: flex; + align-items: flex-start; + gap: var(--sp-3); + padding: var(--sp-3) var(--sp-4); + border: 1px solid var(--border); + background: var(--paper); + border-radius: var(--r-2); + font: 400 14px/1.5 var(--ff-sans); + color: var(--fg-1); + position: relative; +} +.wn-banner__icon { color: var(--fg-2); flex: none; padding-top: 2px; } +.wn-banner__body { flex: 1; } +.wn-banner__title { + font: 500 11px/1.2 var(--ff-mono); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-3); + margin: 0 0 4px; +} +.wn-banner__dismiss { + background: none; border: 0; cursor: pointer; + color: var(--fg-3); padding: 4px; +} +.wn-banner__dismiss:hover { color: var(--fg-1); } +.wn-banner--success { border-left: 2px solid var(--ink); } +.wn-banner--warn { border-left: 2px solid var(--hi-2); background: #FFFCEB; } +.wn-banner--error { border-left: 2px solid var(--ink); background: var(--paper); } +.wn-banner--info { border-left: 2px solid var(--border-strong); } + +.wn-toast-region { + position: fixed; + bottom: var(--sp-5); right: var(--sp-5); + display: flex; flex-direction: column; gap: var(--sp-2); + z-index: 200; + max-width: 380px; +} +.wn-toast { box-shadow: var(--shadow-3); } + +/* ====== Empty state ====== */ +.wn-empty { + border: 1px dashed var(--border-strong); + border-radius: var(--r-2); + padding: var(--sp-7); + display: flex; flex-direction: column; align-items: center; + gap: var(--sp-2); + text-align: center; + color: var(--fg-3); +} +.wn-empty__icon { color: var(--fg-3); margin-bottom: var(--sp-2); } +.wn-empty__title { font: 500 14px var(--ff-sans); color: var(--fg-2); margin: 0; } +.wn-empty__body { font: 400 13px/1.5 var(--ff-sans); color: var(--fg-3); max-width: 40ch; margin: 0; } +.wn-empty__cta { margin-top: var(--sp-2); } + +/* ====== Top navigation ====== */ +.wn-topnav { + height: 56px; + background: rgba(255, 255, 255, 0.92); + border-bottom: 1px solid var(--border); + display: flex; align-items: center; + gap: var(--sp-6); + padding: 0 var(--sp-5); + position: sticky; top: 0; z-index: 10; +} +.wn-topnav__brand { display: flex; align-items: center; gap: 10px; font: 500 14px var(--ff-sans); } +.wn-topnav__brand img { width: 22px; height: 22px; } +.wn-topnav__brand-slug { font-family: var(--ff-mono); font-size: 12px; color: var(--fg-3); letter-spacing: 0.04em; } +.wn-topnav__links { display: flex; gap: 22px; } +.wn-topnav__link { + font: 500 13px var(--ff-sans); + color: var(--fg-2); + text-decoration: none; + padding: 6px 0; + border-bottom: 1px solid transparent; + transition: color 120ms ease, border-color 120ms ease; +} +.wn-topnav__link:hover { color: var(--fg-1); } +.wn-topnav__link--active { color: var(--fg-1); border-bottom-color: var(--ink); } +.wn-topnav__right { margin-left: auto; display: flex; align-items: center; gap: var(--sp-3); } + +/* ====== Sidebar ====== */ +.wn-sidebar { + width: 240px; + flex: none; + background: var(--paper-2); + border-right: 1px solid var(--border); + padding: var(--sp-5) var(--sp-4); + display: flex; flex-direction: column; gap: var(--sp-5); + height: calc(100vh - 56px); + position: sticky; top: 56px; + overflow-y: auto; +} +.wn-sidebar__group { display: flex; flex-direction: column; gap: 8px; } +.wn-sidebar__group-label { padding-left: 12px; } +.wn-sidebar__item { + display: flex; align-items: center; gap: 10px; + padding: 8px 12px; + border-radius: 4px; + color: var(--fg-2); + font: 500 13px var(--ff-sans); + cursor: pointer; text-decoration: none; + transition: background 120ms ease, color 120ms ease; +} +.wn-sidebar__item:hover { color: var(--fg-1); } +.wn-sidebar__item--active { + color: var(--fg-1); background: var(--paper); + box-shadow: 0 0 0 1px var(--border) inset; +} +.wn-sidebar__item--doc { font-family: var(--ff-mono); font-size: 12px; } +.wn-sidebar__count { margin-left: auto; font: 400 11px var(--ff-mono); color: var(--fg-3); } +.wn-sidebar__footer { margin-top: auto; padding-top: var(--sp-3); border-top: 1px solid var(--border); } +.wn-sidebar__activation { + display: flex; align-items: center; gap: 8px; padding: 6px 12px; + font: 500 11px var(--ff-mono); letter-spacing: 0.06em; text-transform: uppercase; + color: var(--fg-2); +} +.wn-sidebar__activation-dot { width: 6px; height: 6px; border-radius: 999px; background: var(--hi-2); } + +/* ====== Page header ====== */ +.wn-page-header { + margin-bottom: var(--sp-6); + display: flex; flex-direction: column; gap: 8px; +} +.wn-page-header__row { display: flex; align-items: flex-end; gap: var(--sp-5); } +.wn-page-header__title { + font: 500 32px/1.15 var(--ff-sans); + letter-spacing: -0.015em; + margin: 0; flex: 1; color: var(--fg-1); +} +.wn-page-header__actions { display: flex; gap: 8px; flex-wrap: wrap; } +.wn-page-header__lede { + font: 400 16px/1.55 var(--ff-sans); + color: var(--fg-2); + margin: 0; + max-width: 60ch; +} + +/* ====== Pipeline ====== */ +.wn-pipeline { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 0; + position: relative; + margin: 0 0 var(--sp-6); +} +.wn-pipeline__stage { + padding: 10px 12px 14px; + border-top: 2px solid var(--border); + display: flex; flex-direction: column; gap: 4px; + position: relative; +} +.wn-pipeline__stage--done { border-top-color: var(--ink); } +.wn-pipeline__stage--active { border-top-color: var(--hi-2); } +.wn-pipeline__num { + font: 500 10px/1 var(--ff-mono); letter-spacing: 0.1em; text-transform: uppercase; + color: var(--fg-3); +} +.wn-pipeline__stage--done .wn-pipeline__num, +.wn-pipeline__stage--active .wn-pipeline__num { color: var(--fg-1); } +.wn-pipeline__name { font: 500 14px/1.25 var(--ff-sans); color: var(--fg-1); } +.wn-pipeline__stage--pending .wn-pipeline__name { color: var(--fg-3); } +.wn-pipeline__meta { font: 400 11px/1.35 var(--ff-mono); color: var(--fg-3); } +.wn-pipeline__arrow { + position: absolute; top: -8px; right: -7px; + font: 400 14px var(--ff-mono); color: var(--ink-5); +} +.wn-pipeline__stage--done .wn-pipeline__arrow, +.wn-pipeline__stage--active .wn-pipeline__arrow { color: var(--ink); } + +/* ====== Prototype card (combined card variant) ====== */ +.wn-prototype-card { /* extends .wn-card */ } +.wn-prototype-card__qrow { + display: grid; grid-template-columns: 110px 1fr; gap: 6px 12px; + font-size: 13px; color: var(--fg-1); +} +.wn-prototype-card__qkey { + font: 500 11px/1.5 var(--ff-mono); + letter-spacing: 0.06em; text-transform: uppercase; + color: var(--fg-3); +} +.wn-prototype-card__qval { line-height: 1.45; } + +/* ====== Layout helpers ====== */ +.wn-main { padding: 40px 48px 80px; max-width: 1180px; } +.wn-app { display: grid; grid-template-columns: 240px 1fr; min-height: 100vh; } diff --git a/projects/coulomb-pricing/ui/vendor/whynot-design/elements/_styles.js b/projects/coulomb-pricing/ui/vendor/whynot-design/elements/_styles.js new file mode 100644 index 0000000..b0f9dd5 --- /dev/null +++ b/projects/coulomb-pricing/ui/vendor/whynot-design/elements/_styles.js @@ -0,0 +1,604 @@ +/* Auto-generated from src/styles/components.css by scripts/sync-shared-styles.mjs. + * Do NOT edit by hand. Edit components.css and re-run the script. + */ + +export const SHARED_CSS = String.raw`/* ============================================================ + WhyNot Design System — Component Styles + ------------------------------------------------------------ + Utility classes that the Lit web components render to. These + are also consumable directly from any HTML (no JS required) + for the "Layer 1 only" use case — see MultiFrameworkSupport.md. + ============================================================ */ + +/* ====== Custom-element display defaults ====== + * For shadow-DOM components, the wn-* host has display: inline by default. + * Set sensible defaults so layout works without the consumer specifying them. + */ +wn-eyebrow, wn-tag, wn-stage-dot, wn-phase-dot, wn-stamp, wn-icon, +wn-search-input, wn-button { display: inline-block; } + +wn-card, wn-modal, wn-top-nav, wn-sidebar, wn-page-header, +wn-pipeline, wn-prototype-card, wn-field-row, wn-breadcrumb, +wn-table, wn-banner, wn-empty-state, +wn-input, wn-textarea, wn-select { display: block; } + +wn-toast-region { display: block; } +wn-toast { display: block; } + +wn-sidebar-group, wn-sidebar-item { display: block; } +wn-table-row, wn-table-cell { display: contents; } + +/* host hidden state — needed because shadow-DOM components don't inherit + * \`[hidden]\` semantics in light DOM. Lit's host attribute reflection + * handles attributes, but \`hidden\` on the host itself should still work. */ +[hidden] { display: none !important; } + +/* ====== Buttons ====== */ +.wn-btn { + font: 500 13px var(--ff-sans); + letter-spacing: -0.005em; + padding: 9px 16px; + border-radius: var(--r-2); + border: 1px solid var(--border); + background: var(--paper); + color: var(--ink); + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; + white-space: nowrap; + transition: background 120ms ease, border-color 120ms ease, color 120ms ease; + text-decoration: none; + line-height: 1.2; +} +.wn-btn:hover { border-color: var(--ink); } +.wn-btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; } +.wn-btn:active { background: var(--bg-3); } +.wn-btn[disabled], .wn-btn.is-disabled { + color: var(--ink-5); border-color: var(--border); cursor: not-allowed; background: var(--paper); +} + +.wn-btn--primary { background: var(--ink); color: var(--paper); border-color: var(--ink); } +.wn-btn--primary:hover { background: var(--ink-2); border-color: var(--ink-2); } +.wn-btn--primary:active { background: var(--ink); } +.wn-btn--primary[disabled], .wn-btn--primary.is-disabled { + background: var(--ink-5); border-color: var(--ink-5); color: var(--paper); +} + +.wn-btn--ghost { background: transparent; border-color: transparent; padding: 7px 10px; } +.wn-btn--ghost:hover { background: var(--bg-3); border-color: transparent; } + +.wn-btn--danger { background: var(--paper); color: var(--ink); border-color: var(--ink); } + +.wn-btn--sm { padding: 5px 10px; font-size: 12px; } +.wn-btn--lg { padding: 12px 20px; font-size: 14px; } + +.wn-btn__icon { width: 14px; height: 14px; flex: none; } +.wn-btn--lg .wn-btn__icon { width: 16px; height: 16px; } + +/* ====== Eyebrows & labels ====== */ +.wn-eyebrow { + font: 500 11px/1.2 var(--ff-mono); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-3); + display: inline-block; +} +.wn-eyebrow--strong { color: var(--fg-1); } + +/* ====== Tags ====== */ +.wn-tag { + font: 500 10px/1 var(--ff-mono); + letter-spacing: 0.1em; + text-transform: uppercase; + padding: 5px 10px; + border-radius: var(--r-pill); + border: 1px solid var(--border); + color: var(--fg-2); + background: var(--paper); + display: inline-block; + white-space: nowrap; +} +.wn-tag--active { background: var(--ink); color: var(--paper); border-color: var(--ink); } +.wn-tag--draft { background: var(--hi); color: var(--hi-ink); border-color: transparent; } + +/* ====== Stage / Phase dots ====== */ +.wn-dot { + display: inline-flex; + align-items: center; + gap: 6px; + font: 500 10px/1 var(--ff-mono); + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--fg-2); +} +.wn-dot__bullet { width: 8px; height: 8px; border-radius: 999px; background: var(--ink); flex: none; } + +/* signal levels (S0–S4) */ +.wn-stage-dot--s0 .wn-dot__bullet { background: var(--status-raw); } +.wn-stage-dot--s1 .wn-dot__bullet { background: var(--status-weak); } +.wn-stage-dot--s2 .wn-dot__bullet { background: var(--status-medium); } +.wn-stage-dot--s3 .wn-dot__bullet { background: var(--status-strong); } +.wn-stage-dot--s4 .wn-dot__bullet { background: var(--status-commercial); } + +/* phase states (todo / active / done / warn) — numbered phases, distinct from signal */ +.wn-phase-dot__bullet { + width: 18px; height: 18px; border-radius: 999px; + border: 1px solid var(--border-strong); + background: var(--paper); + display: inline-flex; align-items: center; justify-content: center; + font: 500 10px/1 var(--ff-mono); color: var(--fg-3); + flex: none; +} +.wn-phase-dot--todo .wn-phase-dot__bullet { border-color: var(--border-strong); color: var(--fg-3); background: var(--paper); } +.wn-phase-dot--active .wn-phase-dot__bullet { border-color: var(--ink); color: var(--ink); background: var(--paper); box-shadow: 0 0 0 3px rgba(10,10,10,0.06); } +.wn-phase-dot--done .wn-phase-dot__bullet { border-color: var(--ink); color: var(--paper); background: var(--ink); } +.wn-phase-dot--warn .wn-phase-dot__bullet { border-color: var(--hi-2); color: var(--hi-ink); background: var(--hi); } + +/* ====== Stamp ====== */ +.wn-stamp { + display: inline-block; + background: var(--hi); + color: var(--hi-ink); + padding: 5px 10px 3px; + font: 500 10px/1 var(--ff-mono); + letter-spacing: 0.12em; + text-transform: uppercase; + transform: rotate(-1.5deg); +} + +/* ====== Icon ====== */ +.wn-icon { stroke-width: 1.5; stroke: currentColor; fill: none; display: inline-block; vertical-align: middle; } +.wn-icon--sm { width: 14px; height: 14px; } +.wn-icon--md { width: 16px; height: 16px; } +.wn-icon--lg { width: 20px; height: 20px; } +.wn-icon--xl { width: 24px; height: 24px; } + +/* ====== Card ====== */ +.wn-card { + background: var(--paper); + border: 1px solid var(--border); + border-radius: var(--r-2); + padding: var(--sp-5); + display: flex; + flex-direction: column; + gap: var(--sp-3); + position: relative; +} +.wn-card--inset { background: var(--paper-2); border-color: var(--border); } +.wn-card--recessed { background: var(--paper-3); } +.wn-card--lg { padding: var(--sp-6); border-radius: var(--r-3); } +.wn-card--sm { padding: var(--sp-4); gap: var(--sp-2); } +.wn-card--clickable { cursor: pointer; transition: border-color 120ms ease; } +.wn-card--clickable:hover { border-color: var(--ink); } +.wn-card--clickable:hover::before { + content: ""; position: absolute; left: -1px; top: -1px; bottom: -1px; + width: 2px; background: var(--ink); border-radius: 2px 0 0 2px; +} +.wn-card__head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-3); } +.wn-card__title { font: 500 17px/1.35 var(--ff-sans); margin: 4px 0 8px; color: var(--fg-1); } +.wn-card__foot { + display: flex; justify-content: space-between; gap: var(--sp-3); + padding-top: var(--sp-3); margin-top: 4px; + border-top: 1px solid var(--border-soft); + font: 500 11px var(--ff-mono); letter-spacing: 0.06em; text-transform: uppercase; + color: var(--fg-3); +} + +/* ====== Field row (label + value, 3-col grid) ====== */ +.wn-field-row { + display: grid; + grid-template-columns: 200px 1fr auto; + gap: var(--sp-4) var(--sp-5); + padding: var(--sp-3) 0; + border-bottom: 1px solid var(--border-soft); + align-items: baseline; +} +.wn-field-row:last-child { border-bottom: 0; } +.wn-field-row__label { + font: 500 11px/1.5 var(--ff-mono); + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--fg-3); +} +.wn-field-row__value { font: 400 15px/1.55 var(--ff-sans); color: var(--fg-1); } +.wn-field-row__aside { font: 400 12px var(--ff-mono); color: var(--fg-3); text-align: right; } +.wn-field-row--stacked { grid-template-columns: 1fr; gap: 6px; } +.wn-field-row--narrow { grid-template-columns: 120px 1fr; } + +/* ====== Form inputs ====== */ +.wn-form-label { + font: 500 11px/1 var(--ff-mono); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-3); + display: block; + margin-bottom: 6px; +} +.wn-input, .wn-textarea, .wn-select { + font: 400 14px var(--ff-sans); + padding: 10px 12px; + border: 1px solid var(--border); + background: var(--paper); + border-radius: var(--r-1); + color: var(--fg-1); + outline: none; + width: 100%; + transition: border-color 120ms ease; + box-sizing: border-box; +} +.wn-input:hover, .wn-textarea:hover, .wn-select:hover { border-color: var(--border-strong); } +.wn-input:focus, .wn-textarea:focus, .wn-select:focus { border-color: var(--ink); } +.wn-input::placeholder, .wn-textarea::placeholder { color: var(--ink-5); } +.wn-input[disabled], .wn-textarea[disabled], .wn-select[disabled] { + background: var(--paper-2); color: var(--fg-3); cursor: not-allowed; +} +.wn-textarea { resize: vertical; min-height: 96px; font-family: var(--ff-sans); } +.wn-select { + appearance: none; -webkit-appearance: none; + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position: right 12px center; + padding-right: 32px; +} + +.wn-input--error, .wn-textarea--error, .wn-select--error { + border-color: var(--ink); border-bottom-width: 2px; +} +.wn-form-help { font: 400 11px var(--ff-mono); color: var(--fg-3); margin-top: 6px; display: block; } +.wn-form-error { font: 400 11px var(--ff-mono); color: var(--ink); margin-top: 6px; display: block; } + +/* Search input — extracted from TopNav, also usable standalone */ +.wn-search { + display: inline-flex; + align-items: center; + gap: 10px; + border: 1px solid var(--border); + padding: 6px 10px; + border-radius: var(--r-1); + background: var(--paper); + color: var(--fg-3); + font: 400 12px var(--ff-mono); + min-width: 200px; + transition: border-color 120ms ease; +} +.wn-search:focus-within { border-color: var(--ink); } +.wn-search input { + border: 0; outline: 0; background: none; flex: 1; + font: inherit; color: var(--fg-1); padding: 0; +} +.wn-search input::placeholder { color: var(--ink-5); } +.wn-search__kbd { + padding: 1px 5px; + border: 1px solid var(--border); + border-radius: 2px; + font-size: 10px; + color: var(--fg-3); +} + +/* ====== Breadcrumb ====== */ +.wn-breadcrumb { + display: flex; flex-wrap: wrap; align-items: center; + gap: 6px; + font: 400 12px/1.5 var(--ff-mono); + color: var(--fg-3); + margin-bottom: var(--sp-4); +} +.wn-breadcrumb a { + color: var(--fg-2); + text-decoration: none; + padding: 2px 0; + border-bottom: 1px solid transparent; + transition: border-color 120ms ease, color 120ms ease; +} +.wn-breadcrumb a:hover { color: var(--fg-1); border-bottom-color: var(--border-strong); } +.wn-breadcrumb__sep { color: var(--ink-5); user-select: none; } +.wn-breadcrumb__current { color: var(--fg-1); } + +/* ====== Modal / Dialog ====== */ +.wn-modal__backdrop { + position: fixed; inset: 0; + background: rgba(10, 10, 10, 0.40); + display: flex; align-items: center; justify-content: center; + z-index: 100; + padding: var(--sp-5); +} +.wn-modal__panel { + background: var(--paper); + border-radius: var(--r-3); + box-shadow: var(--shadow-3); + max-width: 560px; width: 100%; + max-height: calc(100vh - 64px); + display: flex; flex-direction: column; + overflow: hidden; +} +.wn-modal__head { + padding: var(--sp-5) var(--sp-6) var(--sp-4); + border-bottom: 1px solid var(--border); + display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4); +} +.wn-modal__title { font: 500 20px/1.25 var(--ff-sans); margin: 0; color: var(--fg-1); } +.wn-modal__close { + background: none; border: 0; cursor: pointer; padding: 4px; + color: var(--fg-3); border-radius: var(--r-1); + transition: color 120ms ease; +} +.wn-modal__close:hover { color: var(--fg-1); } +.wn-modal__body { + padding: var(--sp-5) var(--sp-6); + overflow-y: auto; + flex: 1; + font: 400 15px/1.6 var(--ff-sans); + color: var(--fg-1); +} +.wn-modal__foot { + padding: var(--sp-4) var(--sp-6) var(--sp-5); + border-top: 1px solid var(--border); + display: flex; justify-content: flex-end; gap: var(--sp-2); +} + +/* ====== Table ====== + * Note: shadow-DOM-rendered rows can't be children of a real
(the + * HTML table model rejects unknown elements between
and ). The + * component therefore renders a CSS-grid imitation. For real + *
markup (Django QuerySet rendering, etc.) use these classes + * directly on
//
elements — see also the .wn-table--native + * variant below. + */ + +/* CSS-grid imitation (default ) */ +.wn-table { + width: 100%; + font-size: var(--fs-sm); + display: flex; + flex-direction: column; +} +.wn-table__thead { border-bottom: 1px solid var(--border); } +.wn-table__tbody { display: flex; flex-direction: column; } +.wn-table__tr { + display: grid; + gap: var(--sp-4); + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border-soft); + align-items: baseline; +} +.wn-table__tr:last-child { border-bottom: 0; } +.wn-table__tr--head { border-bottom: 0; padding: var(--sp-3) var(--sp-4); } +.wn-table__th { + font: 500 11px/1.2 var(--ff-mono); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-3); +} +.wn-table__td { + color: var(--fg-1); + line-height: 1.5; + font-size: var(--fs-sm); +} +.wn-table--compact .wn-table__tr { padding: var(--sp-2) var(--sp-3); } +.wn-table__cell--mono { font-family: var(--ff-mono); color: var(--fg-2); font-size: 12px; } +.wn-table__cell--meta { color: var(--fg-3); font: 400 12px var(--ff-mono); } +.wn-table__cell--right { text-align: right; } + +/* Native variant — for Django QuerySet rendering etc. */ +.wn-table--native { + border-collapse: collapse; + display: table; +} +.wn-table--native thead th { + text-align: left; + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border); + font: 500 11px/1.2 var(--ff-mono); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-3); +} +.wn-table--native tbody td { + padding: var(--sp-4); + border-bottom: 1px solid var(--border-soft); + vertical-align: top; + color: var(--fg-1); + font-size: var(--fs-sm); + line-height: 1.5; +} +.wn-table--native tbody tr:hover { background: var(--paper-2); } +.wn-table--native tbody tr:last-child td { border-bottom: 0; } + +/* ====== Banner / Toast (success / info / warn) ====== */ +.wn-banner { + display: flex; + align-items: flex-start; + gap: var(--sp-3); + padding: var(--sp-3) var(--sp-4); + border: 1px solid var(--border); + background: var(--paper); + border-radius: var(--r-2); + font: 400 14px/1.5 var(--ff-sans); + color: var(--fg-1); + position: relative; +} +.wn-banner__icon { color: var(--fg-2); flex: none; padding-top: 2px; } +.wn-banner__body { flex: 1; } +.wn-banner__title { + font: 500 11px/1.2 var(--ff-mono); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-3); + margin: 0 0 4px; +} +.wn-banner__dismiss { + background: none; border: 0; cursor: pointer; + color: var(--fg-3); padding: 4px; +} +.wn-banner__dismiss:hover { color: var(--fg-1); } +.wn-banner--success { border-left: 2px solid var(--ink); } +.wn-banner--warn { border-left: 2px solid var(--hi-2); background: #FFFCEB; } +.wn-banner--error { border-left: 2px solid var(--ink); background: var(--paper); } +.wn-banner--info { border-left: 2px solid var(--border-strong); } + +.wn-toast-region { + position: fixed; + bottom: var(--sp-5); right: var(--sp-5); + display: flex; flex-direction: column; gap: var(--sp-2); + z-index: 200; + max-width: 380px; +} +.wn-toast { box-shadow: var(--shadow-3); } + +/* ====== Empty state ====== */ +.wn-empty { + border: 1px dashed var(--border-strong); + border-radius: var(--r-2); + padding: var(--sp-7); + display: flex; flex-direction: column; align-items: center; + gap: var(--sp-2); + text-align: center; + color: var(--fg-3); +} +.wn-empty__icon { color: var(--fg-3); margin-bottom: var(--sp-2); } +.wn-empty__title { font: 500 14px var(--ff-sans); color: var(--fg-2); margin: 0; } +.wn-empty__body { font: 400 13px/1.5 var(--ff-sans); color: var(--fg-3); max-width: 40ch; margin: 0; } +.wn-empty__cta { margin-top: var(--sp-2); } + +/* ====== Top navigation ====== */ +.wn-topnav { + height: 56px; + background: rgba(255, 255, 255, 0.92); + border-bottom: 1px solid var(--border); + display: flex; align-items: center; + gap: var(--sp-6); + padding: 0 var(--sp-5); + position: sticky; top: 0; z-index: 10; +} +.wn-topnav__brand { display: flex; align-items: center; gap: 10px; font: 500 14px var(--ff-sans); } +.wn-topnav__brand img { width: 22px; height: 22px; } +.wn-topnav__brand-slug { font-family: var(--ff-mono); font-size: 12px; color: var(--fg-3); letter-spacing: 0.04em; } +.wn-topnav__links { display: flex; gap: 22px; } +.wn-topnav__link { + font: 500 13px var(--ff-sans); + color: var(--fg-2); + text-decoration: none; + padding: 6px 0; + border-bottom: 1px solid transparent; + transition: color 120ms ease, border-color 120ms ease; +} +.wn-topnav__link:hover { color: var(--fg-1); } +.wn-topnav__link--active { color: var(--fg-1); border-bottom-color: var(--ink); } +.wn-topnav__right { margin-left: auto; display: flex; align-items: center; gap: var(--sp-3); } + +/* ====== Sidebar ====== */ +.wn-sidebar { + width: 240px; + flex: none; + background: var(--paper-2); + border-right: 1px solid var(--border); + padding: var(--sp-5) var(--sp-4); + display: flex; flex-direction: column; gap: var(--sp-5); + height: calc(100vh - 56px); + position: sticky; top: 56px; + overflow-y: auto; +} +.wn-sidebar__group { display: flex; flex-direction: column; gap: 8px; } +.wn-sidebar__group-label { padding-left: 12px; } +.wn-sidebar__item { + display: flex; align-items: center; gap: 10px; + padding: 8px 12px; + border-radius: 4px; + color: var(--fg-2); + font: 500 13px var(--ff-sans); + cursor: pointer; text-decoration: none; + transition: background 120ms ease, color 120ms ease; +} +.wn-sidebar__item:hover { color: var(--fg-1); } +.wn-sidebar__item--active { + color: var(--fg-1); background: var(--paper); + box-shadow: 0 0 0 1px var(--border) inset; +} +.wn-sidebar__item--doc { font-family: var(--ff-mono); font-size: 12px; } +.wn-sidebar__count { margin-left: auto; font: 400 11px var(--ff-mono); color: var(--fg-3); } +.wn-sidebar__footer { margin-top: auto; padding-top: var(--sp-3); border-top: 1px solid var(--border); } +.wn-sidebar__activation { + display: flex; align-items: center; gap: 8px; padding: 6px 12px; + font: 500 11px var(--ff-mono); letter-spacing: 0.06em; text-transform: uppercase; + color: var(--fg-2); +} +.wn-sidebar__activation-dot { width: 6px; height: 6px; border-radius: 999px; background: var(--hi-2); } + +/* ====== Page header ====== */ +.wn-page-header { + margin-bottom: var(--sp-6); + display: flex; flex-direction: column; gap: 8px; +} +.wn-page-header__row { display: flex; align-items: flex-end; gap: var(--sp-5); } +.wn-page-header__title { + font: 500 32px/1.15 var(--ff-sans); + letter-spacing: -0.015em; + margin: 0; flex: 1; color: var(--fg-1); +} +.wn-page-header__actions { display: flex; gap: 8px; flex-wrap: wrap; } +.wn-page-header__lede { + font: 400 16px/1.55 var(--ff-sans); + color: var(--fg-2); + margin: 0; + max-width: 60ch; +} + +/* ====== Pipeline ====== */ +.wn-pipeline { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 0; + position: relative; + margin: 0 0 var(--sp-6); +} +.wn-pipeline__stage { + padding: 10px 12px 14px; + border-top: 2px solid var(--border); + display: flex; flex-direction: column; gap: 4px; + position: relative; +} +.wn-pipeline__stage--done { border-top-color: var(--ink); } +.wn-pipeline__stage--active { border-top-color: var(--hi-2); } +.wn-pipeline__num { + font: 500 10px/1 var(--ff-mono); letter-spacing: 0.1em; text-transform: uppercase; + color: var(--fg-3); +} +.wn-pipeline__stage--done .wn-pipeline__num, +.wn-pipeline__stage--active .wn-pipeline__num { color: var(--fg-1); } +.wn-pipeline__name { font: 500 14px/1.25 var(--ff-sans); color: var(--fg-1); } +.wn-pipeline__stage--pending .wn-pipeline__name { color: var(--fg-3); } +.wn-pipeline__meta { font: 400 11px/1.35 var(--ff-mono); color: var(--fg-3); } +.wn-pipeline__arrow { + position: absolute; top: -8px; right: -7px; + font: 400 14px var(--ff-mono); color: var(--ink-5); +} +.wn-pipeline__stage--done .wn-pipeline__arrow, +.wn-pipeline__stage--active .wn-pipeline__arrow { color: var(--ink); } + +/* ====== Prototype card (combined card variant) ====== */ +.wn-prototype-card { /* extends .wn-card */ } +.wn-prototype-card__qrow { + display: grid; grid-template-columns: 110px 1fr; gap: 6px 12px; + font-size: 13px; color: var(--fg-1); +} +.wn-prototype-card__qkey { + font: 500 11px/1.5 var(--ff-mono); + letter-spacing: 0.06em; text-transform: uppercase; + color: var(--fg-3); +} +.wn-prototype-card__qval { line-height: 1.45; } + +/* ====== Layout helpers ====== */ +.wn-main { padding: 40px 48px 80px; max-width: 1180px; } +.wn-app { display: grid; grid-template-columns: 240px 1fr; min-height: 100vh; } +`; + +let _sheet = null; +export function getSharedSheet() { + if (!_sheet) { + _sheet = new CSSStyleSheet(); + _sheet.replaceSync(SHARED_CSS); + } + return _sheet; +} diff --git a/projects/coulomb-pricing/ui/vendor/whynot-design/elements/atoms.js b/projects/coulomb-pricing/ui/vendor/whynot-design/elements/atoms.js new file mode 100644 index 0000000..7663bd3 --- /dev/null +++ b/projects/coulomb-pricing/ui/vendor/whynot-design/elements/atoms.js @@ -0,0 +1,164 @@ +/* ============================================================= + * @whynot/design — atoms.js + * ------------------------------------------------------------ + * , , , , + * , , + * + * Shadow-DOM components. Each adopts the shared component + * stylesheet so utility classes inside the shadow root work. + * Token CSS variables cascade through shadow boundaries + * because they're inherited properties. + * ============================================================= */ + +import { LitElement, html, nothing } from "lit"; +import { getSharedSheet } from "./_styles.js"; +import { ICON_PATHS } from "./icons.js"; + +class WnBase extends LitElement { + static styles = []; + connectedCallback() { + super.connectedCallback(); + // Adopt the shared sheet on first connect, after super() has built the shadow root. + const root = this.shadowRoot; + if (root && !root.adoptedStyleSheets.includes(getSharedSheet())) { + root.adoptedStyleSheets = [...root.adoptedStyleSheets, getSharedSheet()]; + } + } +} + +/* ---------- ---------- */ +export class WnButton extends WnBase { + static properties = { + variant: { type: String, reflect: true }, + size: { type: String, reflect: true }, + icon: { type: String }, + iconEnd: { type: String, attribute: "icon-end" }, + type: { type: String }, + disabled: { type: Boolean, reflect: true }, + href: { type: String }, + }; + constructor() { + super(); + this.variant = "secondary"; + this.size = "md"; + this.type = "button"; + this.disabled = false; + } + render() { + const cls = [ + "wn-btn", + this.variant && this.variant !== "secondary" ? `wn-btn--${this.variant}` : "", + this.size === "sm" ? "wn-btn--sm" : this.size === "lg" ? "wn-btn--lg" : "", + ].filter(Boolean).join(" "); + const iconStart = this.icon + ? html`` + : nothing; + const iconEnd = this.iconEnd + ? html`` + : nothing; + if (this.href) { + return html`${iconStart}${iconEnd}`; + } + return html``; + } +} + +/* ---------- ---------- */ +export class WnTag extends WnBase { + static properties = { + active: { type: Boolean, reflect: true }, + draft: { type: Boolean, reflect: true }, + }; + render() { + const cls = ["wn-tag", + this.active ? "wn-tag--active" : "", + this.draft ? "wn-tag--draft" : "", + ].filter(Boolean).join(" "); + return html``; + } +} + +/* ---------- ---------- */ +export class WnEyebrow extends WnBase { + static properties = { strong: { type: Boolean, reflect: true } }; + render() { + const cls = "wn-eyebrow" + (this.strong ? " wn-eyebrow--strong" : ""); + return html``; + } +} + +/* ---------- ---------- */ +export class WnStamp extends WnBase { + render() { return html``; } +} + +/* ---------- ---------- */ +export class WnStageDot extends WnBase { + static properties = { + level: { type: String, reflect: true }, + label: { type: String }, + }; + constructor() { super(); this.level = "S2"; } + render() { + const lvl = String(this.level || "S2").toLowerCase(); + const cls = `wn-dot wn-stage-dot wn-stage-dot--${lvl}`; + return html` + + + ${this.label || this.level} + `; + } +} + +/* ---------- ---------- */ +export class WnPhaseDot extends WnBase { + static properties = { + state: { type: String, reflect: true }, + num: { type: String, reflect: true }, + }; + constructor() { super(); this.state = "todo"; this.num = ""; } + render() { + const cls = `wn-phase-dot wn-phase-dot--${this.state}`; + const glyph = this.state === "done" ? "✓" : this.num; + return html` + + ${glyph} + + `; + } +} + +/* ---------- ---------- */ +export class WnIcon extends WnBase { + static properties = { + name: { type: String, reflect: true }, + size: { type: String, reflect: true }, + }; + constructor() { super(); this.size = "md"; } + render() { + const path = ICON_PATHS[this.name]; + const cls = `wn-icon wn-icon--${this.size || "md"}`; + if (!path) { + return html``; + } + return html``; + } +} + +export function defineAtoms() { + if (!customElements.get("wn-button")) customElements.define("wn-button", WnButton); + if (!customElements.get("wn-tag")) customElements.define("wn-tag", WnTag); + if (!customElements.get("wn-eyebrow")) customElements.define("wn-eyebrow", WnEyebrow); + if (!customElements.get("wn-stamp")) customElements.define("wn-stamp", WnStamp); + if (!customElements.get("wn-stage-dot")) customElements.define("wn-stage-dot", WnStageDot); + if (!customElements.get("wn-phase-dot")) customElements.define("wn-phase-dot", WnPhaseDot); + if (!customElements.get("wn-icon")) customElements.define("wn-icon", WnIcon); +} + +export { WnBase }; diff --git a/projects/coulomb-pricing/ui/vendor/whynot-design/elements/chrome.js b/projects/coulomb-pricing/ui/vendor/whynot-design/elements/chrome.js new file mode 100644 index 0000000..c5d992b --- /dev/null +++ b/projects/coulomb-pricing/ui/vendor/whynot-design/elements/chrome.js @@ -0,0 +1,206 @@ +/* ============================================================= + * @whynot/design — chrome.js + * ------------------------------------------------------------ + * , / / + * , , , + * + * ============================================================= */ + +import { LitElement, html, nothing } from "lit"; +import { WnBase } from "./atoms.js"; + +/* ---------- ---------- */ +export class WnTopNav extends WnBase { + static properties = { + logoSrc: { type: String, attribute: "logo-src" }, + brand: { type: String }, + slug: { type: String }, + }; + constructor() { super(); this.brand = "whynot"; this.slug = "control"; } + render() { + return html` + + `; + } +} + +/* ---------- ---------- */ +export class WnSidebar extends WnBase { + static properties = { activation: { type: String } }; + render() { + return html` + + `; + } +} + +export class WnSidebarGroup extends WnBase { + static properties = { label: { type: String } }; + render() { + return html` +
+ ${this.label ? html`${this.label}` : nothing} + +
+ `; + } +} + +export class WnSidebarItem extends WnBase { + static properties = { + href: { type: String }, + icon: { type: String }, + active: { type: Boolean, reflect: true }, + count: { type: String }, + variant: { type: String, reflect: true }, + }; + render() { + const cls = ["wn-sidebar__item", + this.active ? "wn-sidebar__item--active" : "", + this.variant === "doc" ? "wn-sidebar__item--doc" : "", + ].filter(Boolean).join(" "); + const inner = html` + ${this.icon ? html`` : nothing} + + ${this.count ? html`${this.count}` : nothing} + `; + return this.href + ? html`${inner}` + : html`
${inner}
`; + } +} + +/* ---------- ---------- */ +export class WnPageHeader extends WnBase { + static properties = { + eyebrow: { type: String }, + title: { type: String }, + lede: { type: String }, + hasActions: { state: true }, + }; + constructor() { super(); this.hasActions = false; } + _onSlot() { this.hasActions = !!this.querySelector('[slot="actions"]'); } + render() { + return html` +
+ ${this.eyebrow ? html`${this.eyebrow}` : nothing} + +
+

${this.title}

+
+ +
+
+ ${this.lede ? html`

${this.lede}

` : nothing} + +
+ `; + } +} + +/* ---------- ---------- */ +export class WnPipeline extends WnBase { + static properties = { + stages: { type: Array }, + activeIdx: { type: Number, attribute: "active-idx" }, + }; + constructor() { + super(); + this.stages = [ + { num: "Stage 0", name: "Raw idea", meta: "inbox/" }, + { num: "Stage 1", name: "Triage", meta: "" }, + { num: "Stage 2", name: "Prototype card", meta: "prototypes/" }, + { num: "Stage 3", name: "Experiment", meta: "" }, + { num: "Stage 4", name: "Signal review", meta: "" }, + ]; + this.activeIdx = 0; + } + render() { + return html` +
+ ${this.stages.map((s, i) => { + const state = i < this.activeIdx ? "done" : i === this.activeIdx ? "active" : "pending"; + const cls = `wn-pipeline__stage wn-pipeline__stage--${state}`; + return html` +
+ ${s.num} + ${s.name} + ${s.meta ? html`${s.meta}` : nothing} + ${i > 0 ? html`` : nothing} +
+ `; + })} +
+ `; + } +} + +/* ---------- ---------- */ +export class WnPrototypeCard extends WnBase { + static properties = { + cardId: { type: String, attribute: "card-id", reflect: true }, + signal: { type: String, reflect: true }, + stageLabel: { type: String, attribute: "stage-label" }, + href: { type: String }, + }; + constructor() { super(); this.signal = "S1"; } + _onClick() { + if (this.href) window.location.href = this.href; + this.dispatchEvent(new CustomEvent("wn-open", { detail: { id: this.cardId }, bubbles: true, composed: true })); + } + render() { + const clickable = !!this.href; + const cls = "wn-card wn-prototype-card" + (clickable ? " wn-card--clickable" : ""); + return html` +
+
+ ${this.cardId ? this.cardId + " · " : ""}Prototype + ${this.stageLabel || this.signal} +
+

+
+ Learning q. + + Smallest test + +
+
+ + ${this.signal} signal +
+
+ `; + } +} + +export function defineChrome() { + if (!customElements.get("wn-top-nav")) customElements.define("wn-top-nav", WnTopNav); + if (!customElements.get("wn-sidebar")) customElements.define("wn-sidebar", WnSidebar); + if (!customElements.get("wn-sidebar-group")) customElements.define("wn-sidebar-group", WnSidebarGroup); + if (!customElements.get("wn-sidebar-item")) customElements.define("wn-sidebar-item", WnSidebarItem); + if (!customElements.get("wn-page-header")) customElements.define("wn-page-header", WnPageHeader); + if (!customElements.get("wn-pipeline")) customElements.define("wn-pipeline", WnPipeline); + if (!customElements.get("wn-prototype-card")) customElements.define("wn-prototype-card", WnPrototypeCard); +} diff --git a/projects/coulomb-pricing/ui/vendor/whynot-design/elements/form.js b/projects/coulomb-pricing/ui/vendor/whynot-design/elements/form.js new file mode 100644 index 0000000..d24b714 --- /dev/null +++ b/projects/coulomb-pricing/ui/vendor/whynot-design/elements/form.js @@ -0,0 +1,205 @@ +/* ============================================================= + * @whynot/design — form.js + * ------------------------------------------------------------ + * , , , + * , + * + * Each wraps a real native element. Form participation works + * because the native input is part of the light DOM via the + * `name` attribute being copied through; for richer integration + * use ElementInternals (deferred — see CHANGELOG). + * ============================================================= */ + +import { LitElement, html, nothing } from "lit"; +import { WnBase } from "./atoms.js"; + +/* ---------- ---------- */ +export class WnInput extends WnBase { + static properties = { + name: { type: String, reflect: true }, + type: { type: String, reflect: true }, + value: { type: String }, + placeholder: { type: String }, + required: { type: Boolean, reflect: true }, + disabled: { type: Boolean, reflect: true }, + readonly: { type: Boolean, reflect: true }, + autocomplete:{ type: String }, + error: { type: Boolean, reflect: true }, + help: { type: String }, + errorText: { type: String, attribute: "error-text" }, + }; + constructor() { + super(); + this.type = "text"; + this.value = ""; + this.required = false; + this.disabled = false; + this.readonly = false; + this.error = false; + } + _onInput(e) { + this.value = e.target.value; + this.dispatchEvent(new CustomEvent("wn-input", { detail: { value: this.value }, bubbles: true, composed: true })); + } + render() { + const cls = "wn-input" + (this.error ? " wn-input--error" : ""); + return html` + + ${this.error && this.errorText + ? html`${this.errorText}` + : this.help + ? html`${this.help}` + : nothing} + `; + } +} + +/* ---------- ---------- */ +export class WnTextarea extends WnBase { + static properties = { + name: { type: String, reflect: true }, + value: { type: String }, + placeholder: { type: String }, + rows: { type: Number }, + required: { type: Boolean, reflect: true }, + disabled: { type: Boolean, reflect: true }, + error: { type: Boolean, reflect: true }, + help: { type: String }, + errorText: { type: String, attribute: "error-text" }, + }; + constructor() { super(); this.value = ""; this.rows = 4; } + _onInput(e) { + this.value = e.target.value; + this.dispatchEvent(new CustomEvent("wn-input", { detail: { value: this.value }, bubbles: true, composed: true })); + } + render() { + const cls = "wn-textarea" + (this.error ? " wn-textarea--error" : ""); + return html` + + ${this.error && this.errorText + ? html`${this.errorText}` + : this.help + ? html`${this.help}` + : nothing} + `; + } +} + +/* ---------- ---------- + * Slot
/ with slotted rows — + * the table model requires the row to be a child of
. So these + * components use CSS grid + flexbox to imitate a table visually. For real + *
+ Django QuerySet rendering, write raw
+ * markup directly using utility classes. + */ +export class WnTable extends WnBase { + static properties = { + columns: { type: Array }, + compact: { type: Boolean, reflect: true }, + }; + constructor() { super(); this.columns = []; } + render() { + const cols = this.columns || []; + const cls = "wn-table" + (this.compact ? " wn-table--compact" : ""); + return html` +
+ ${cols.length + ? html`
+
(typeof c === "object" && c.width) ? `${c.width}px` : "1fr").join(" ")}`}> + ${cols.map(c => html`
${typeof c === "string" ? c : c.label}
`)} +
+
` + : nothing} +
(typeof c === "object" && c.width) ? `${c.width}px` : "1fr").join(" ")}` + : nothing}> + +
+
+ `; + } +} + +export class WnTableRow extends WnBase { + render() { + return html`
+ +
`; + } +} + +export class WnTableCell extends WnBase { + static properties = { variant: { type: String, reflect: true } }; + render() { + const cls = "wn-table__td" + (this.variant ? ` wn-table__cell--${this.variant}` : ""); + return html`
`; + } +} + +/* ---------- ---------- */ +export class WnBanner extends WnBase { + static properties = { + variant: { type: String, reflect: true }, + title: { type: String }, + icon: { type: String }, + dismissible: { type: Boolean, reflect: true }, + }; + constructor() { super(); this.variant = "info"; } + _dismiss() { + this.dispatchEvent(new CustomEvent("wn-dismiss", { bubbles: true, composed: true })); + this.remove(); + } + render() { + const iconName = this.icon || ({ + info: "circle-info", success: "circle-check", + warn: "circle-alert", error: "circle-alert", + }[this.variant]); + const cls = `wn-banner wn-banner--${this.variant}`; + return html` +
+ ${iconName ? html`` : nothing} +
+ ${this.title ? html`

${this.title}

` : nothing} + +
+ ${this.dismissible + ? html`` + : nothing} +
+ `; + } +} + +/* ---------- / ---------- */ +export class WnToast extends WnBanner { + constructor() { super(); this.dismissible = true; } + render() { + const base = super.render(); + return html`
${base}
`; + } +} +export class WnToastRegion extends WnBase { + render() { + return html`
+ +
`; + } +} + +/* ---------- ---------- */ +export class WnEmptyState extends WnBase { + static properties = { + icon: { type: String }, + title: { type: String }, + hasCta: { state: true }, + }; + constructor() { super(); this.hasCta = false; } + _onSlot() { this.hasCta = !!this.querySelector('[slot="cta"]'); } + render() { + return html` +
+ ${this.icon ? html`` : nothing} + ${this.title ? html`

${this.title}

` : nothing} +

+
+ +
+
+ `; + } +} + +/* ---------- ---------- */ +export class WnBreadcrumb extends WnBase { + _onSlot(e) { + const slot = e.target; + const items = slot.assignedElements({ flatten: true }); + // Build the rendered tree: each item + a separator after it. + const wrapper = this.shadowRoot?.querySelector('.wn-breadcrumb__list'); + if (!wrapper) return; + wrapper.querySelectorAll('.wn-breadcrumb__sep').forEach(s => s.remove()); + items.forEach((el, i) => { + el.classList.toggle("wn-breadcrumb__current", i === items.length - 1); + if (i > 0) { + const sep = document.createElement("span"); + sep.className = "wn-breadcrumb__sep"; + sep.setAttribute("aria-hidden", "true"); + sep.textContent = "/"; + // Use light-DOM-relative insertion: items are still in light DOM, + // so DOM-order separators between them belong in light DOM too. + el.parentNode.insertBefore(sep, el); + } + }); + } + render() { + return html` + + `; + } +} + +export function defineLayout() { + if (!customElements.get("wn-card")) customElements.define("wn-card", WnCard); + if (!customElements.get("wn-modal")) customElements.define("wn-modal", WnModal); + if (!customElements.get("wn-table")) customElements.define("wn-table", WnTable); + if (!customElements.get("wn-table-row")) customElements.define("wn-table-row", WnTableRow); + if (!customElements.get("wn-table-cell")) customElements.define("wn-table-cell", WnTableCell); + if (!customElements.get("wn-banner")) customElements.define("wn-banner", WnBanner); + if (!customElements.get("wn-toast")) customElements.define("wn-toast", WnToast); + if (!customElements.get("wn-toast-region")) customElements.define("wn-toast-region", WnToastRegion); + if (!customElements.get("wn-empty-state")) customElements.define("wn-empty-state", WnEmptyState); + if (!customElements.get("wn-breadcrumb")) customElements.define("wn-breadcrumb", WnBreadcrumb); +} diff --git a/projects/coulomb-pricing/ui/vendor/whynot-design/index.js b/projects/coulomb-pricing/ui/vendor/whynot-design/index.js new file mode 100644 index 0000000..d567d18 --- /dev/null +++ b/projects/coulomb-pricing/ui/vendor/whynot-design/index.js @@ -0,0 +1,35 @@ +/* ============================================================= + * @whynot/design — entry point + * ------------------------------------------------------------ + * Side-effect import that registers every custom element. + * + * import "@whynot/design"; + * + * If you only need a subset, import the per-group files instead: + * + * import "@whynot/design/atoms"; + * import "@whynot/design/form"; + * import "@whynot/design/layout"; + * import "@whynot/design/chrome"; + * + * CSS is imported separately: + * + * import "@whynot/design/styles/colors_and_type.css"; + * import "@whynot/design/styles/components.css"; + * ============================================================= */ + +import { defineAtoms } from "./elements/atoms.js"; +import { defineForm } from "./elements/form.js"; +import { defineLayout } from "./elements/layout.js"; +import { defineChrome } from "./elements/chrome.js"; + +defineAtoms(); +defineForm(); +defineLayout(); +defineChrome(); + +// Re-export classes for consumers that want to extend or reference them. +export * from "./elements/atoms.js"; +export * from "./elements/form.js"; +export * from "./elements/layout.js"; +export * from "./elements/chrome.js"; diff --git a/projects/coulomb-pricing/ui/vendor/whynot-design/tokens/colors.json b/projects/coulomb-pricing/ui/vendor/whynot-design/tokens/colors.json new file mode 100644 index 0000000..00f0590 --- /dev/null +++ b/projects/coulomb-pricing/ui/vendor/whynot-design/tokens/colors.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://design-tokens.github.io/community-group/format/", + "ink": { "value": "#0A0A0A", "type": "color", "comment": "Near-black. The only fill most of the time." }, + "ink-2": { "value": "#1F1F1F", "type": "color" }, + "ink-3": { "value": "#5C5C5C", "type": "color" }, + "ink-4": { "value": "#8A8A8A", "type": "color" }, + "ink-5": { "value": "#B5B5B3", "type": "color", "comment": "Placeholder text, wireframe labels." }, + "line": { "value": "#E5E5E2", "type": "color", "comment": "Default 1px wireframe rule." }, + "line-strong": { "value": "#C9C9C5", "type": "color" }, + "line-soft": { "value": "#F0F0EC", "type": "color" }, + "paper": { "value": "#FFFFFF", "type": "color" }, + "paper-2": { "value": "#FAFAF7", "type": "color" }, + "paper-3": { "value": "#F4F4EF", "type": "color" }, + "hi": { "value": "#FFE14A", "type": "color", "comment": "Annotation yellow. Highlighter only, never a button fill." }, + "hi-2": { "value": "#FFD400", "type": "color" }, + "hi-ink": { "value": "#1A1500", "type": "color", "comment": "Text on yellow." }, + "status-raw": { "value": "#B5B5B3", "type": "color", "comment": "S0 — no signal" }, + "status-weak": { "value": "#8A8A8A", "type": "color", "comment": "S1 — weak signal" }, + "status-medium": { "value": "#5C5C5C", "type": "color", "comment": "S2 — medium signal" }, + "status-strong": { "value": "#0A0A0A", "type": "color", "comment": "S3 — strong signal" }, + "status-commercial": { "value": "#FFD400", "type": "color", "comment": "S4 — commercial" } +} diff --git a/projects/coulomb-pricing/ui/vendor/whynot-design/tokens/index.json b/projects/coulomb-pricing/ui/vendor/whynot-design/tokens/index.json new file mode 100644 index 0000000..974065d --- /dev/null +++ b/projects/coulomb-pricing/ui/vendor/whynot-design/tokens/index.json @@ -0,0 +1,6 @@ +{ + "comment": "Manifest pointing at the three token files. Source-of-truth for any future Style Dictionary build.", + "colors": "./colors.json", + "type": "./type.json", + "spacing": "./spacing.json" +} diff --git a/projects/coulomb-pricing/ui/vendor/whynot-design/tokens/spacing.json b/projects/coulomb-pricing/ui/vendor/whynot-design/tokens/spacing.json new file mode 100644 index 0000000..0e6e0e0 --- /dev/null +++ b/projects/coulomb-pricing/ui/vendor/whynot-design/tokens/spacing.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://design-tokens.github.io/community-group/format/", + "spacing": { + "1": { "value": "4px", "type": "dimension" }, + "2": { "value": "8px", "type": "dimension" }, + "3": { "value": "12px", "type": "dimension" }, + "4": { "value": "16px", "type": "dimension" }, + "5": { "value": "24px", "type": "dimension" }, + "6": { "value": "32px", "type": "dimension" }, + "7": { "value": "48px", "type": "dimension" }, + "8": { "value": "64px", "type": "dimension" }, + "9": { "value": "96px", "type": "dimension" }, + "10": { "value": "128px", "type": "dimension" } + }, + "radius": { + "0": { "value": "0px", "type": "dimension" }, + "1": { "value": "2px", "type": "dimension" }, + "2": { "value": "4px", "type": "dimension" }, + "3": { "value": "8px", "type": "dimension" }, + "pill": { "value": "999px", "type": "dimension" } + }, + "shadow": { + "0": { "value": "none", "type": "shadow" }, + "1": { "value": "0 1px 0 #E5E5E2", "type": "shadow" }, + "2": { "value": "0 1px 0 #C9C9C5", "type": "shadow" }, + "3": { "value": "0 4px 12px -6px rgba(10,10,10,0.10)", "type": "shadow", "comment": "Floating elements only." } + } +} diff --git a/projects/coulomb-pricing/ui/vendor/whynot-design/tokens/type.json b/projects/coulomb-pricing/ui/vendor/whynot-design/tokens/type.json new file mode 100644 index 0000000..2022120 --- /dev/null +++ b/projects/coulomb-pricing/ui/vendor/whynot-design/tokens/type.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://design-tokens.github.io/community-group/format/", + "family": { + "sans": { "value": "\"IBM Plex Sans\", ui-sans-serif, system-ui, sans-serif", "type": "fontFamily" }, + "mono": { "value": "\"IBM Plex Mono\", ui-monospace, \"SF Mono\", Menlo, monospace", "type": "fontFamily" }, + "serif": { "value": "\"IBM Plex Serif\", \"Iowan Old Style\", Georgia, serif", "type": "fontFamily" } + }, + "size": { + "xs": { "value": "11px", "type": "dimension" }, + "sm": { "value": "13px", "type": "dimension" }, + "base": { "value": "15px", "type": "dimension" }, + "md": { "value": "17px", "type": "dimension" }, + "lg": { "value": "20px", "type": "dimension" }, + "xl": { "value": "24px", "type": "dimension" }, + "2xl": { "value": "32px", "type": "dimension" }, + "3xl": { "value": "44px", "type": "dimension" }, + "4xl": { "value": "64px", "type": "dimension" }, + "5xl": { "value": "96px", "type": "dimension" } + }, + "lineHeight": { + "tight": { "value": 1.05, "type": "number" }, + "snug": { "value": 1.25, "type": "number" }, + "base": { "value": 1.5, "type": "number" }, + "loose": { "value": 1.7, "type": "number" } + }, + "tracking": { + "tight": { "value": "-0.02em", "type": "dimension" }, + "snug": { "value": "-0.01em", "type": "dimension" }, + "base": { "value": "0em", "type": "dimension" }, + "mono": { "value": "0.02em", "type": "dimension" }, + "label": { "value": "0.08em", "type": "dimension", "comment": "Uppercase eyebrow labels." } + } +}