This commit is contained in:
Abdul Wahab
2026-09-01 10:09:03 +05:00
parent f775f9e585
commit 82b03772e4
11 changed files with 469 additions and 47 deletions
+16
View File
@@ -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) => {
+53 -23
View File
@@ -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) => (
{ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[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>&ldquo;The room felt entirely transformed, but still like us.&rdquo;</blockquote>
<span class="dcx-showcase-card-meta">Mika &amp; Ren</span>
<blockquote>&ldquo;It fits the way we already work, and it still feels like ours.&rdquo;</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">&#8594;</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>
+64 -7
View File
@@ -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;
}