mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-21 02:26:31 +03:00
update
This commit is contained in:
@@ -111,6 +111,21 @@ import { dcxAsset } from './assets.js';
|
||||
section.querySelectorAll(":scope .dcx-fan-note").forEach((note) => note.remove());
|
||||
};
|
||||
|
||||
/* A "default" chip marks a surface that kept its preset instead of being
|
||||
configured, and the sentence that said so was a provenance note, which the
|
||||
pass above removes. Restate it in a class this pass keeps, so a value the
|
||||
run never chose is never read as one it did. The "leading" chip shares the
|
||||
class and means something else, hence the text test. */
|
||||
const notePresetDefaults = (body) => {
|
||||
const chips = [...body.querySelectorAll(".dcx-default-mark")];
|
||||
if (!chips.some((chip) => chip.textContent.trim() === "default")) return;
|
||||
if (body.querySelector(":scope > .dcx-default-note")) return;
|
||||
const note = document.createElement("p");
|
||||
note.className = "dcx-default-note";
|
||||
note.textContent = "Entries marked default kept their surface's preset; this run never set them. Run /impeccable design-context edit to answer them.";
|
||||
body.appendChild(note);
|
||||
};
|
||||
|
||||
const compactColorArticle = (article) => {
|
||||
if (article.dataset.dcxColorCompacted === "true") return;
|
||||
|
||||
@@ -268,6 +283,7 @@ import { dcxAsset } from './assets.js';
|
||||
wrapPrincipleContent(section, category);
|
||||
placeProductPlatform(section, category);
|
||||
removeProvenanceNotes(section);
|
||||
notePresetDefaults(body);
|
||||
};
|
||||
|
||||
const enhanceArticle = (article, category) => {
|
||||
|
||||
@@ -315,7 +315,37 @@ import { dcxAsset } from './assets.js';
|
||||
if (lede) lede.textContent = "Identity, voice, references, taste boundaries, and available assets.";
|
||||
};
|
||||
|
||||
const componentCardMedia = () => dcxAsset("/assets/components/hanazono-ikebana-card.jpg");
|
||||
const FALLBACK_CARD_IMAGE = "/assets/components/hanazono-ikebana-card.jpg";
|
||||
|
||||
const escapeSpecimen = (value) => String(value).replace(/[&<>"]/g, (character) => (
|
||||
{ "&": "&", "<": "<", ">": ">", '"': """ }[character]
|
||||
));
|
||||
|
||||
/* The run's own product name, published by the data layer (design-context.js
|
||||
renderDocument). The specimen fields show a real value rather than a label,
|
||||
so a run with no name on record keeps a neutral stand-in. */
|
||||
const productName = () => {
|
||||
const name = typeof window.dcxProductName === "string" ? window.dcxProductName.trim() : "";
|
||||
return escapeSpecimen(name || "Untitled project");
|
||||
};
|
||||
|
||||
const productDomain = () => {
|
||||
const name = typeof window.dcxProductName === "string" ? window.dcxProductName : "";
|
||||
/* The strip leaves only [a-z0-9], so this needs no escaping of its own. */
|
||||
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
||||
return slug || "example";
|
||||
};
|
||||
|
||||
/* Cue-first card media: the data layer publishes the run's chosen cue on
|
||||
window.dcxCueImageSrc, and the vendored photo is the fallback, swapped in
|
||||
by the delegated error listener when the cue cannot load. A run whose
|
||||
palette named no cue gets the vendored photo directly. */
|
||||
const componentCardImage = (fallbackAlt, cueAlt) => {
|
||||
const cue = typeof window.dcxCueImageSrc === "string" ? window.dcxCueImageSrc : "";
|
||||
const fallbackSrc = dcxAsset(FALLBACK_CARD_IMAGE);
|
||||
if (!cue) return `src="${fallbackSrc}" alt="${fallbackAlt}"`;
|
||||
return `src="${cue}" alt="${cueAlt}" data-dcx-swap-src="${fallbackSrc}" data-dcx-swap-alt="${fallbackAlt}"`;
|
||||
};
|
||||
|
||||
const composeComponentsArticle = (article) => {
|
||||
article.querySelectorAll(":scope > .dcx-block[data-label]").forEach((block) => block.remove());
|
||||
@@ -414,7 +444,7 @@ import { dcxAsset } from './assets.js';
|
||||
</div>
|
||||
<div class="dcx-demo-field">
|
||||
<span class="dcx-demo-field-label">Project type</span>
|
||||
<span class="dcx-demo-input">Ceremony florals</span>
|
||||
<span class="dcx-demo-input">Quarterly report</span>
|
||||
<small>Filled</small>
|
||||
</div>
|
||||
<div class="dcx-demo-field is-invalid">
|
||||
@@ -432,15 +462,15 @@ import { dcxAsset } from './assets.js';
|
||||
<div class="dcx-component-demo-grid dcx-component-demo-grid--three dcx-component-demo-grid--baseline">
|
||||
<div class="dcx-demo-field dcx-demo-field--small">
|
||||
<span class="dcx-demo-field-label">Small</span>
|
||||
<span class="dcx-demo-input">Hana</span>
|
||||
<span class="dcx-demo-input">Nova</span>
|
||||
</div>
|
||||
<div class="dcx-demo-field">
|
||||
<span class="dcx-demo-field-label">Medium</span>
|
||||
<span class="dcx-demo-input">Hanazono Atelier</span>
|
||||
<span class="dcx-demo-input">${productName()}</span>
|
||||
</div>
|
||||
<div class="dcx-demo-field dcx-demo-field--large">
|
||||
<span class="dcx-demo-field-label">Large</span>
|
||||
<span class="dcx-demo-input">Private celebration</span>
|
||||
<span class="dcx-demo-input">Quarterly planning</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -454,7 +484,7 @@ import { dcxAsset } from './assets.js';
|
||||
<span class="dcx-demo-field-label">Search</span>
|
||||
<span class="dcx-demo-input dcx-demo-input--affix">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="6"></circle><path d="m16 16 4 4"></path></svg>
|
||||
Spring arrangements
|
||||
Recent documents
|
||||
</span>
|
||||
</div>
|
||||
<div class="dcx-demo-field">
|
||||
@@ -463,7 +493,7 @@ import { dcxAsset } from './assets.js';
|
||||
</div>
|
||||
<div class="dcx-demo-field">
|
||||
<span class="dcx-demo-field-label">Website</span>
|
||||
<span class="dcx-demo-input dcx-demo-input--affix dcx-demo-input--suffix">hanazono<span>.jp</span></span>
|
||||
<span class="dcx-demo-input dcx-demo-input--affix dcx-demo-input--suffix">${productDomain()}<span>.com</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -474,8 +504,8 @@ import { dcxAsset } from './assets.js';
|
||||
<div class="dcx-component-canvas dcx-component-canvas--fields">
|
||||
<div class="dcx-component-demo-grid dcx-component-demo-grid--single">
|
||||
<div class="dcx-demo-field">
|
||||
<span class="dcx-demo-field-label">Tell us about the setting</span>
|
||||
<span class="dcx-demo-input dcx-demo-input--textarea">A quiet evening ceremony with seasonal branches, soft candlelight, and room for the architecture to breathe.</span>
|
||||
<span class="dcx-demo-field-label">Tell us about the project</span>
|
||||
<span class="dcx-demo-input dcx-demo-input--textarea">A short brief describing the setting, the audience, and the constraints this piece of work has to hold.</span>
|
||||
<small>240 characters remaining</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -501,15 +531,15 @@ import { dcxAsset } from './assets.js';
|
||||
<article class="dcx-showcase-card">
|
||||
<div class="dcx-showcase-card-body">
|
||||
<span class="dcx-component-kind">Testimonial</span>
|
||||
<blockquote>“The room felt entirely transformed, but still like us.”</blockquote>
|
||||
<span class="dcx-showcase-card-meta">Mika & Ren</span>
|
||||
<blockquote>“It fits the way we already work, and it still feels like ours.”</blockquote>
|
||||
<span class="dcx-showcase-card-meta">A long-time customer</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="dcx-showcase-card">
|
||||
<div class="dcx-showcase-card-body">
|
||||
<span class="dcx-component-kind">Details</span>
|
||||
<h5>Autumn ceremony</h5>
|
||||
<dl><div><dt>Season</dt><dd>Late autumn</dd></div><div><dt>Setting</dt><dd>Kyoto townhouse</dd></div></dl>
|
||||
<h5>Project details</h5>
|
||||
<dl><div><dt>Timeline</dt><dd>Six weeks</dd></div><div><dt>Setting</dt><dd>Distributed team</dd></div></dl>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
@@ -521,18 +551,18 @@ import { dcxAsset } from './assets.js';
|
||||
<div class="dcx-component-canvas dcx-component-canvas--cards">
|
||||
<div class="dcx-component-demo-grid dcx-component-demo-grid--two">
|
||||
<article class="dcx-showcase-card dcx-showcase-card--media">
|
||||
<img src="${componentCardMedia()}" width="800" height="1200" alt="Purple flowers arranged in a black ceramic vase" loading="lazy" decoding="async">
|
||||
<img ${componentCardImage("Purple flowers arranged in a black ceramic vase", "The visual cue this palette was picked from")} width="800" height="1200" loading="lazy" decoding="async">
|
||||
<div class="dcx-showcase-card-body">
|
||||
<span class="dcx-component-kind">Garden study</span>
|
||||
<span class="dcx-component-kind">Case study</span>
|
||||
<h5>Line, pause, and negative space</h5>
|
||||
<p>A restrained floral study built around one deliberate gesture.</p>
|
||||
<p>A calm composition built around one deliberate gesture.</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="dcx-showcase-card dcx-showcase-card--media dcx-showcase-card--media-close">
|
||||
<img src="${componentCardMedia()}" width="800" height="1200" alt="Close crop of purple ikebana flowers" loading="lazy" decoding="async">
|
||||
<img ${componentCardImage("Close crop of purple ikebana flowers", "The chosen visual cue, cropped close")} width="800" height="1200" loading="lazy" decoding="async">
|
||||
<div class="dcx-showcase-card-body">
|
||||
<span class="dcx-component-kind">Material note</span>
|
||||
<h5>Dark ceramic, soft bloom</h5>
|
||||
<h5>Detail, closely cropped</h5>
|
||||
<p>The same image can lead with subject or material detail.</p>
|
||||
</div>
|
||||
</article>
|
||||
@@ -544,11 +574,11 @@ import { dcxAsset } from './assets.js';
|
||||
<figcaption><h4>Horizontal</h4></figcaption>
|
||||
<div class="dcx-component-canvas dcx-component-canvas--cards dcx-component-canvas--single-card">
|
||||
<article class="dcx-showcase-card dcx-showcase-card--horizontal">
|
||||
<img src="${componentCardMedia()}" width="800" height="1200" alt="Purple ikebana in a dark ceramic vase" loading="lazy" decoding="async">
|
||||
<img ${componentCardImage("Purple ikebana in a dark ceramic vase", "The chosen visual cue as a wide feature image")} width="800" height="1200" loading="lazy" decoding="async">
|
||||
<div class="dcx-showcase-card-body">
|
||||
<span class="dcx-component-kind">Featured story</span>
|
||||
<h5>A study in asymmetry</h5>
|
||||
<p>How a single stem, a dark vessel, and generous space hold the composition together.</p>
|
||||
<p>How a single focal point, a quiet ground, and generous space hold the composition together.</p>
|
||||
<span class="dcx-showcase-card-link">Explore the study <span aria-hidden="true">→</span></span>
|
||||
</div>
|
||||
</article>
|
||||
@@ -563,15 +593,15 @@ import { dcxAsset } from './assets.js';
|
||||
<div class="dcx-showcase-card-body">
|
||||
<span class="dcx-component-kind">Consultation</span>
|
||||
<h5>Plan the first conversation</h5>
|
||||
<p>Share the date and setting so the studio can prepare.</p>
|
||||
<p>Share the date and setting so the team can prepare.</p>
|
||||
<span class="dcx-demo-button dcx-demo-button--primary">Book now</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="dcx-showcase-card dcx-showcase-card--action">
|
||||
<div class="dcx-showcase-card-body">
|
||||
<span class="dcx-component-kind">Commission guide</span>
|
||||
<span class="dcx-component-kind">Process guide</span>
|
||||
<h5>Understand the process</h5>
|
||||
<p>Read the stages, timing, and what the studio needs from you.</p>
|
||||
<p>Read the stages, timing, and what the team needs from you.</p>
|
||||
<span class="dcx-demo-button dcx-demo-button--outline">View the guide</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -68,12 +68,16 @@ const contextReady = Promise.all([getJson('/context.json'), cuesReady])
|
||||
document mode, read by the parts of it that differ. */
|
||||
let docMode = false;
|
||||
|
||||
/* The submit flow renders before the server has exited but reveals after, and
|
||||
the store's copy of the cue is made during that submit: a URL first requested
|
||||
after the exit would find nothing serving it. The cue the questionnaire
|
||||
already displayed is in the browser's cache, so that run keeps reading it
|
||||
from the workspace, and only a document opened later reads the store copy. */
|
||||
const cueImageSrc = (slug) => (docMode ? '/cue.png' : `/cues/${encodeURIComponent(slug)}.png`);
|
||||
/* The chosen cue, wherever this page can still reach it. A live doc session
|
||||
outlives every server the page booted from, so it is the first choice, and
|
||||
the store's copy is made during submit before that session is forked. A
|
||||
document opened later without a session still has the picker server serving
|
||||
the store copy; a submit run before either exists reads the workspace image
|
||||
the questionnaire already displayed, which is in the browser's cache. */
|
||||
const cueImageSrc = (slug) => {
|
||||
if (docSession) return `${docSession.base}/cue.png?token=${encodeURIComponent(docSession.token)}`;
|
||||
return docMode ? '/cue.png' : `/cues/${encodeURIComponent(slug)}.png`;
|
||||
};
|
||||
|
||||
const seedHexFor = (source, role) => {
|
||||
const slot = seedPalettes?.[source]?.[role];
|
||||
@@ -88,7 +92,10 @@ const seedHexFor = (source, role) => {
|
||||
const ROLES = ['primary', 'secondary', 'tertiary', 'neutral'];
|
||||
const SURFACE_ORDER = ['persuade', 'operate', 'read', 'experience'];
|
||||
const SURFACE_LABELS = { persuade: 'Landing page', operate: 'Tool', read: 'Docs', experience: 'Portfolio' };
|
||||
const PER_SURFACE = ['color-strategy', 'boundary-style', 'corner-style', 'depth-style'];
|
||||
/* The five questions asked per surface, matching portability.mjs. The bare key
|
||||
is the leading surface's answer, which is what DESIGN.md records as the rule
|
||||
for the whole product. */
|
||||
const PER_SURFACE = ['color-strategy', 'boundary-style', 'corner-style', 'depth-style', 'motion-energy'];
|
||||
|
||||
const fieldValue = (name) => {
|
||||
const field = form.elements[name];
|
||||
@@ -290,6 +297,19 @@ document.addEventListener('load', (event) => {
|
||||
document.addEventListener('error', (event) => {
|
||||
const image = event.target;
|
||||
if (!(image instanceof HTMLImageElement)) return;
|
||||
/* A card that asked for the cue swaps to its vendored photo rather than
|
||||
hiding: the entry is real either way, only the imagery moved. One swap
|
||||
only, so a fallback that also fails lands at the hide rule below. */
|
||||
if (image.dataset.dcxSwapSrc) {
|
||||
const fallback = image.dataset.dcxSwapSrc;
|
||||
delete image.dataset.dcxSwapSrc;
|
||||
if (image.dataset.dcxSwapAlt) {
|
||||
image.alt = image.dataset.dcxSwapAlt;
|
||||
delete image.dataset.dcxSwapAlt;
|
||||
}
|
||||
image.src = fallback;
|
||||
return;
|
||||
}
|
||||
const casualty = image.closest('[data-dcx-hide-on-error]');
|
||||
if (casualty) casualty.hidden = true;
|
||||
}, true);
|
||||
@@ -323,6 +343,7 @@ function paintCommitted(node) {
|
||||
set('n-ink', contrastInk(colors.neutral));
|
||||
set('p-ink', contrastInk(colors.primary));
|
||||
set('t-ink', contrastInk(colors.tertiary));
|
||||
set('s-on-n', readableOn(colors.secondary, colors.neutral));
|
||||
set('p-on-n', readableOn(colors.primary, colors.neutral));
|
||||
set('t-on-n', readableOn(colors.tertiary, colors.neutral));
|
||||
set('t-on-p', readableOn(colors.tertiary, colors.primary));
|
||||
@@ -337,6 +358,7 @@ function proofHtml(source, { strategy = '', kind = 'board', marks = null } = {})
|
||||
if (!source) return '';
|
||||
const clone = source.cloneNode(true);
|
||||
clone.hidden = false;
|
||||
const sourceSurface = source.getAttribute('data-surface') || '';
|
||||
clone.removeAttribute('data-surface');
|
||||
for (const node of [clone, ...clone.querySelectorAll('[id]')]) node.removeAttribute('id');
|
||||
for (const node of $$('button, input', clone)) {
|
||||
@@ -356,6 +378,13 @@ function proofHtml(source, { strategy = '', kind = 'board', marks = null } = {})
|
||||
if (value) wrap.setAttribute(`data-dcx-${key}`, value);
|
||||
}
|
||||
}
|
||||
/* The surface the drawing came from, restated on the frame: the clone loses
|
||||
its own data-surface above, and the stylesheet's quiet-base rule has to
|
||||
tell a persuade board from a derived one. Marks that already named a
|
||||
surface win, since those carry the committed answer. */
|
||||
if (sourceSurface && !wrap.hasAttribute('data-dcx-surface')) {
|
||||
wrap.setAttribute('data-dcx-surface', sourceSurface);
|
||||
}
|
||||
paintCommitted(wrap);
|
||||
wrap.appendChild(clone);
|
||||
return wrap.outerHTML;
|
||||
@@ -964,6 +993,24 @@ function renderDocument() {
|
||||
description: MODE_DEFS[surface.mode] || surface.goal || '',
|
||||
}));
|
||||
const name = snapshot.context?.product?.name || '';
|
||||
/* Bridges for the document engine, whose modules read globals rather than
|
||||
importing this file. The cue URL is empty when the palette came from a
|
||||
seed deck or a custom pick, which keeps the vendored card photo; the
|
||||
product name fills the specimen fields that would otherwise show another
|
||||
studio's. */
|
||||
const dealtCues = Array.isArray(snapshot.cueSlugs) ? snapshot.cueSlugs : [];
|
||||
const chosenCue = snapshot.paletteSource && (docMode || dealtCues.includes(snapshot.paletteSource))
|
||||
? snapshot.paletteSource
|
||||
: '';
|
||||
window.dcxCueImageSrc = chosenCue ? cueImageSrc(chosenCue) : '';
|
||||
window.dcxProductName = name;
|
||||
/* The components inventory reads the committed palette through --pkc-* on
|
||||
<body>: its article is rebuilt on every remount, so the paint lives on the
|
||||
one node that survives. A run that never committed a full palette leaves
|
||||
the gate off and keeps the vendored demo colors. */
|
||||
const paletteLive = ROLES.every((role) => fieldValue(`palette-${role}`));
|
||||
document.body.classList.toggle('dcx-palette-live', paletteLive);
|
||||
if (paletteLive) paintCommitted(document.body);
|
||||
for (const [id, build] of Object.entries(BUILDERS)) {
|
||||
const template = document.getElementById(`dcx-detail-${id}`);
|
||||
template.innerHTML = `<article class="dcx-article">${build(snapshot, name)}</article>`;
|
||||
@@ -983,6 +1030,16 @@ function collectAnswers() {
|
||||
if (!(name in answers)) answers[name] = value;
|
||||
else answers[name] = Array.isArray(answers[name]) ? [...answers[name], value] : [answers[name], value];
|
||||
}
|
||||
/* The visible radio follows whichever surface tab was shown last, so the bare
|
||||
key can leave carrying a trailing surface's answer. The leading surface owns
|
||||
it: restate it from that surface's own field before this goes to disk. */
|
||||
const chosen = [].concat(answers['surface-modes'] || []);
|
||||
const leading = SURFACE_ORDER.filter((mode) => chosen.includes(mode));
|
||||
for (const key of PER_SURFACE) {
|
||||
if (!(key in answers)) continue;
|
||||
const owner = leading.find((mode) => answers[`${key}-${mode}`]);
|
||||
if (owner) answers[key] = answers[`${key}-${owner}`];
|
||||
}
|
||||
return answers;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user