generated from coulomb/repo-seed
Integrate whynot-design into Economic Observatory UI
Vendor whynot-design Layer 1 (tokens, CSS) and Layer 2 (<wn-*> 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.
This commit is contained in:
206
projects/coulomb-pricing/ui/vendor/whynot-design/elements/chrome.js
vendored
Normal file
206
projects/coulomb-pricing/ui/vendor/whynot-design/elements/chrome.js
vendored
Normal file
@@ -0,0 +1,206 @@
|
||||
/* =============================================================
|
||||
* @whynot/design — chrome.js
|
||||
* ------------------------------------------------------------
|
||||
* <wn-top-nav>, <wn-sidebar> / <wn-sidebar-group> /
|
||||
* <wn-sidebar-item>, <wn-page-header>, <wn-pipeline>,
|
||||
* <wn-prototype-card>
|
||||
* ============================================================= */
|
||||
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { WnBase } from "./atoms.js";
|
||||
|
||||
/* ---------- <wn-top-nav> ---------- */
|
||||
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`
|
||||
<nav class="wn-topnav" part="nav">
|
||||
<div class="wn-topnav__brand">
|
||||
${this.logoSrc ? html`<img src=${this.logoSrc} alt="">` : nothing}
|
||||
<span>${this.brand}</span>
|
||||
${this.slug ? html`<span class="wn-topnav__brand-slug">/ ${this.slug}</span>` : nothing}
|
||||
</div>
|
||||
<div class="wn-topnav__links"><slot name="links"></slot></div>
|
||||
<div class="wn-topnav__right"><slot name="right"></slot></div>
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- <wn-sidebar> ---------- */
|
||||
export class WnSidebar extends WnBase {
|
||||
static properties = { activation: { type: String } };
|
||||
render() {
|
||||
return html`
|
||||
<aside class="wn-sidebar" part="sidebar">
|
||||
<slot></slot>
|
||||
${this.activation
|
||||
? html`<div class="wn-sidebar__footer">
|
||||
<div class="wn-sidebar__activation">
|
||||
<span class="wn-sidebar__activation-dot"></span>
|
||||
<span>${this.activation}</span>
|
||||
</div>
|
||||
</div>`
|
||||
: nothing}
|
||||
</aside>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export class WnSidebarGroup extends WnBase {
|
||||
static properties = { label: { type: String } };
|
||||
render() {
|
||||
return html`
|
||||
<div class="wn-sidebar__group" part="group">
|
||||
${this.label ? html`<wn-eyebrow class="wn-sidebar__group-label">${this.label}</wn-eyebrow>` : nothing}
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
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`<wn-icon name=${this.icon} size=${this.variant === "doc" ? "sm" : "md"}></wn-icon>` : nothing}
|
||||
<slot></slot>
|
||||
${this.count ? html`<span class="wn-sidebar__count">${this.count}</span>` : nothing}
|
||||
`;
|
||||
return this.href
|
||||
? html`<a class=${cls} href=${this.href} part="item">${inner}</a>`
|
||||
: html`<div class=${cls} part="item">${inner}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- <wn-page-header> ---------- */
|
||||
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`
|
||||
<header class="wn-page-header" part="root">
|
||||
${this.eyebrow ? html`<wn-eyebrow>${this.eyebrow}</wn-eyebrow>` : nothing}
|
||||
<slot name="eyebrow"></slot>
|
||||
<div class="wn-page-header__row">
|
||||
<h1 class="wn-page-header__title">${this.title}<slot name="title"></slot></h1>
|
||||
<div class="wn-page-header__actions" ?hidden=${!this.hasActions}>
|
||||
<slot name="actions" @slotchange=${this._onSlot}></slot>
|
||||
</div>
|
||||
</div>
|
||||
${this.lede ? html`<p class="wn-page-header__lede">${this.lede}</p>` : nothing}
|
||||
<slot name="lede"></slot>
|
||||
</header>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- <wn-pipeline> ---------- */
|
||||
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`
|
||||
<div class="wn-pipeline" part="root">
|
||||
${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`
|
||||
<div class=${cls}>
|
||||
<span class="wn-pipeline__num">${s.num}</span>
|
||||
<span class="wn-pipeline__name">${s.name}</span>
|
||||
${s.meta ? html`<span class="wn-pipeline__meta">${s.meta}</span>` : nothing}
|
||||
${i > 0 ? html`<span class="wn-pipeline__arrow">→</span>` : nothing}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- <wn-prototype-card> ---------- */
|
||||
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`
|
||||
<article class=${cls}
|
||||
role=${clickable ? "button" : nothing}
|
||||
tabindex=${clickable ? "0" : nothing}
|
||||
@click=${clickable ? this._onClick : nothing}
|
||||
part="card">
|
||||
<header class="wn-card__head">
|
||||
<wn-eyebrow>${this.cardId ? this.cardId + " · " : ""}Prototype</wn-eyebrow>
|
||||
<wn-stage-dot level=${this.signal}>${this.stageLabel || this.signal}</wn-stage-dot>
|
||||
</header>
|
||||
<h3 class="wn-card__title"><slot name="pitch"></slot></h3>
|
||||
<div class="wn-prototype-card__qrow">
|
||||
<span class="wn-prototype-card__qkey">Learning q.</span>
|
||||
<span class="wn-prototype-card__qval"><slot name="learning"></slot></span>
|
||||
<span class="wn-prototype-card__qkey">Smallest test</span>
|
||||
<span class="wn-prototype-card__qval"><slot name="test"></slot></span>
|
||||
</div>
|
||||
<footer class="wn-card__foot">
|
||||
<span><slot name="target"></slot></span>
|
||||
<span>${this.signal} signal</span>
|
||||
</footer>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user