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:
205
projects/coulomb-pricing/ui/vendor/whynot-design/elements/form.js
vendored
Normal file
205
projects/coulomb-pricing/ui/vendor/whynot-design/elements/form.js
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
/* =============================================================
|
||||
* @whynot/design — form.js
|
||||
* ------------------------------------------------------------
|
||||
* <wn-input>, <wn-textarea>, <wn-select>,
|
||||
* <wn-search-input>, <wn-field-row>
|
||||
*
|
||||
* 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";
|
||||
|
||||
/* ---------- <wn-input> ---------- */
|
||||
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`
|
||||
<input class=${cls}
|
||||
part="input"
|
||||
name=${this.name ?? nothing}
|
||||
type=${this.type}
|
||||
.value=${this.value}
|
||||
placeholder=${this.placeholder ?? nothing}
|
||||
?required=${this.required}
|
||||
?disabled=${this.disabled}
|
||||
?readonly=${this.readonly}
|
||||
autocomplete=${this.autocomplete ?? nothing}
|
||||
@input=${this._onInput}>
|
||||
${this.error && this.errorText
|
||||
? html`<span class="wn-form-error">${this.errorText}</span>`
|
||||
: this.help
|
||||
? html`<span class="wn-form-help">${this.help}</span>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- <wn-textarea> ---------- */
|
||||
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`
|
||||
<textarea class=${cls}
|
||||
part="textarea"
|
||||
name=${this.name ?? nothing}
|
||||
rows=${this.rows}
|
||||
placeholder=${this.placeholder ?? nothing}
|
||||
?required=${this.required}
|
||||
?disabled=${this.disabled}
|
||||
@input=${this._onInput}
|
||||
.value=${this.value}></textarea>
|
||||
${this.error && this.errorText
|
||||
? html`<span class="wn-form-error">${this.errorText}</span>`
|
||||
: this.help
|
||||
? html`<span class="wn-form-help">${this.help}</span>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- <wn-select> ----------
|
||||
* Slot <option> elements; they're cloned into the inner <select>. */
|
||||
export class WnSelect extends WnBase {
|
||||
static properties = {
|
||||
name: { type: String, reflect: true },
|
||||
value: { type: String },
|
||||
required: { type: Boolean, reflect: true },
|
||||
disabled: { type: Boolean, reflect: true },
|
||||
error: { type: Boolean, reflect: true },
|
||||
help: { type: String },
|
||||
};
|
||||
_onSlotChange(e) {
|
||||
const slot = e.target;
|
||||
const select = this.shadowRoot?.querySelector("select.wn-select");
|
||||
if (!select) return;
|
||||
const options = slot.assignedElements({ flatten: true }).filter(el => el.tagName === "OPTION");
|
||||
select.innerHTML = "";
|
||||
for (const opt of options) select.appendChild(opt.cloneNode(true));
|
||||
if (this.value != null) select.value = this.value;
|
||||
}
|
||||
_onChange(e) {
|
||||
this.value = e.target.value;
|
||||
this.dispatchEvent(new CustomEvent("wn-change", { detail: { value: this.value }, bubbles: true, composed: true }));
|
||||
}
|
||||
render() {
|
||||
const cls = "wn-select" + (this.error ? " wn-select--error" : "");
|
||||
return html`
|
||||
<select class=${cls}
|
||||
part="select"
|
||||
name=${this.name ?? nothing}
|
||||
?required=${this.required}
|
||||
?disabled=${this.disabled}
|
||||
@change=${this._onChange}></select>
|
||||
<slot @slotchange=${this._onSlotChange} style="display:none"></slot>
|
||||
${this.help ? html`<span class="wn-form-help">${this.help}</span>` : nothing}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- <wn-search-input> ---------- */
|
||||
export class WnSearchInput extends WnBase {
|
||||
static properties = {
|
||||
placeholder: { type: String },
|
||||
kbd: { type: String },
|
||||
value: { type: String },
|
||||
name: { type: String, reflect: true },
|
||||
};
|
||||
constructor() { super(); this.placeholder = "Search…"; this.kbd = "⌘ K"; this.value = ""; }
|
||||
_onInput(e) {
|
||||
this.value = e.target.value;
|
||||
this.dispatchEvent(new CustomEvent("wn-input", { detail: { value: this.value }, bubbles: true, composed: true }));
|
||||
}
|
||||
render() {
|
||||
return html`
|
||||
<label class="wn-search" part="root">
|
||||
<wn-icon name="search" size="sm"></wn-icon>
|
||||
<input type="search"
|
||||
name=${this.name ?? nothing}
|
||||
.value=${this.value}
|
||||
placeholder=${this.placeholder}
|
||||
@input=${this._onInput}>
|
||||
${this.kbd ? html`<span class="wn-search__kbd">${this.kbd}</span>` : nothing}
|
||||
</label>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- <wn-field-row> ---------- */
|
||||
export class WnFieldRow extends WnBase {
|
||||
static properties = {
|
||||
label: { type: String },
|
||||
aside: { type: String },
|
||||
stacked: { type: Boolean, reflect: true },
|
||||
narrow: { type: Boolean, reflect: true },
|
||||
htmlFor: { type: String, attribute: "for" },
|
||||
};
|
||||
render() {
|
||||
const cls = ["wn-field-row",
|
||||
this.stacked ? "wn-field-row--stacked" : "",
|
||||
this.narrow ? "wn-field-row--narrow" : "",
|
||||
].filter(Boolean).join(" ");
|
||||
return html`
|
||||
<div class=${cls} part="root">
|
||||
<label class="wn-field-row__label" for=${this.htmlFor ?? nothing}>${this.label}</label>
|
||||
<div class="wn-field-row__value"><slot></slot></div>
|
||||
${this.aside
|
||||
? html`<div class="wn-field-row__aside">${this.aside}<slot name="aside"></slot></div>`
|
||||
: html`<div class="wn-field-row__aside"><slot name="aside"></slot></div>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export function defineForm() {
|
||||
if (!customElements.get("wn-input")) customElements.define("wn-input", WnInput);
|
||||
if (!customElements.get("wn-textarea")) customElements.define("wn-textarea", WnTextarea);
|
||||
if (!customElements.get("wn-select")) customElements.define("wn-select", WnSelect);
|
||||
if (!customElements.get("wn-search-input")) customElements.define("wn-search-input", WnSearchInput);
|
||||
if (!customElements.get("wn-field-row")) customElements.define("wn-field-row", WnFieldRow);
|
||||
}
|
||||
Reference in New Issue
Block a user