This commit is contained in:
Abdul Wahab
2026-09-01 10:04:59 +05:00
parent 6cfeb7d374
commit 8b6bbaef3d
7 changed files with 439 additions and 5 deletions
+126 -2
View File
@@ -25,12 +25,14 @@ const shell = $('[data-dcx-shell]');
let seedContext = null;
let seedModes = null;
let seedPalettes = null;
let seedCues = null;
fetch('/cues.json')
.then((response) => (response.ok ? response.json() : null))
.then((data) => {
seedContext = data?.context || null;
seedModes = Array.isArray(data?.modes) ? data.modes : null;
seedPalettes = data?.palette || null;
seedCues = Array.isArray(data?.cues) ? data.cues : null;
})
.catch(() => {});
@@ -134,6 +136,7 @@ function takeSnapshot() {
return {
context: seedContext,
suggestedModes: seedModes,
cueSlugs: seedCues,
surfaces,
palette,
paletteSource: fieldValue('palette-source'),
@@ -226,6 +229,38 @@ const fromChat = (what, home) => empty(
`${what} in chat, before the browser questionnaire. ${home} is the durable copy.`,
);
/* Staged brand-asset files are served by the picker server before submit and
by the doc session after it: the picker process exits when the submit
response lands, and article images only load when a detail view opens,
which is always after that. docSession is assigned before any render that
can reach the live DOM (startDocSession re-renders the templates). */
const brandAssetSrc = (file) => (docSession
? `${docSession.base}/brand-assets/${encodeURIComponent(file)}?token=${encodeURIComponent(docSession.token)}`
: `/brand-assets/${encodeURIComponent(file)}`);
/* Cue and asset images load after their innerHTML render. The load pass
stamps the cue frame with the image's natural size, which is the space
the cues.json sample coordinates live in (the same division the picker's
own ring placement does); the error pass hides the broken entry so a
missing file never leaves a dead image in an article. Capture phase,
because load and error do not bubble. */
document.addEventListener('load', (event) => {
const image = event.target;
if (!(image instanceof HTMLImageElement) || !('dcxCueImg' in image.dataset)) return;
const frame = image.closest('.dcx-cue-frame');
if (!frame) return;
frame.style.setProperty('--cue-w', String(image.naturalWidth || 1));
frame.style.setProperty('--cue-h', String(image.naturalHeight || 1));
frame.dataset.loaded = 'yes';
}, true);
document.addEventListener('error', (event) => {
const image = event.target;
if (!(image instanceof HTMLImageElement)) return;
const casualty = image.closest('[data-dcx-hide-on-error]');
if (casualty) casualty.hidden = true;
}, true);
/* Readable ink for a fan panel, from the swatch's own luminance. */
function inkFor(hex) {
const [r, g, b] = [1, 3, 5].map((at) => parseInt(hex.slice(at, at + 2), 16) / 255);
@@ -500,8 +535,49 @@ function buildBrand(s, name) {
parts.push(block('Anti-reference', anti
+ note('Q5 of the seed interview. A hard constraint on every palette and pair that followed.')));
}
if (Array.isArray(s.context?.assets) && s.context.assets.length) {
parts.push(block('Assets provided', list(s.context.assets.map(escapeHtml))
/* Assets: an object entry carries a staged file under
.impeccable/design-interview/assets/ and renders as an image; a plain
string keeps the text line it always had. A logo is proofed on two
chips, the committed primary and the committed neutral, so a colored
and a quiet ground are judged at once; boards and references get a
wide frame. A file that fails to load hides its own entry (the
delegated error listener), never the article. */
const assets = Array.isArray(s.context?.assets) ? s.context.assets : [];
const isFileAsset = (entry) => Boolean(entry) && typeof entry === 'object'
&& typeof entry.file === 'string' && entry.file;
const fileAssets = assets.filter(isFileAsset);
const textAssets = assets.filter((entry) => !isFileAsset(entry));
const logos = fileAssets.filter((entry) => entry.kind === 'logo');
const boards = fileAssets.filter((entry) => entry.kind !== 'logo');
const assetCaption = (entry) => `
<figcaption class="dcx-asset-caption">
<code>${escapeHtml(entry.file)}</code>${entry.note ? `
<p>${escapeHtml(entry.note)}</p>` : ''}
</figcaption>`;
const chipHex = (roleName) => s.palette.find((entry) => entry.role === roleName)?.hex || '';
if (logos.length) {
parts.push(block('Marks', `<div class="dcx-marks">${logos.map((entry) => `
<figure class="dcx-mark" data-dcx-hide-on-error>
<div class="dcx-mark-pair">
<span class="dcx-mark-chip"${chipHex('Primary') ? ` style="--chip-ground:${chipHex('Primary')};"` : ''}><img src="${brandAssetSrc(entry.file)}" alt="${escapeHtml(entry.file)} on the primary color" /></span>
<span class="dcx-mark-chip"${chipHex('Neutral') ? ` style="--chip-ground:${chipHex('Neutral')};"` : ''}><img src="${brandAssetSrc(entry.file)}" alt="${escapeHtml(entry.file)} on the neutral color" /></span>
</div>
${assetCaption(entry)}
</figure>`).join('')}</div>`
+ note('Provided marks proofed on the committed primary and neutral grounds. The files are staged in <code>.impeccable/design-interview/assets/</code>.')));
}
if (boards.length) {
parts.push(block('Boards and references', `<div class="dcx-boards">${boards.map((entry) => `
<figure class="dcx-board" data-dcx-hide-on-error>
<span class="dcx-board-frame"><img src="${brandAssetSrc(entry.file)}" alt="${escapeHtml(entry.file)}" loading="lazy" /></span>
${assetCaption(entry)}
</figure>`).join('')}</div>`
+ note('Boards and reference images provided in chat, staged in <code>.impeccable/design-interview/assets/</code>.')));
}
if (textAssets.length) {
parts.push(block('Assets provided', list(textAssets.map((entry) => escapeHtml(
typeof entry === 'string' ? entry : (entry.note || entry.file || ''),
)))
+ note('Gathered before the interview; the questions were grounded in what they showed.')));
}
return parts.join('');
@@ -519,6 +595,50 @@ const ROLE_STORY = {
function buildColor(s, name) {
const interview = s.context?.interview || {};
const parts = [heading(4, 'Color', 'Palette, roles, per-surface strategy, copyable values.', name)];
/* The chosen cue: the image the palette was sampled from, its four sample
points marked at the cues.json coordinates in each role's dealt color,
and the rest of the generated set dimmed below. Skipped without ceremony
when the palette came from a seed deck or a custom pick rather than a
cue, or when the run had no cues at all. */
const cueSlugs = Array.isArray(s.cueSlugs) ? s.cueSlugs : [];
const chosenCue = cueSlugs.includes(s.paletteSource) ? s.paletteSource : '';
if (chosenCue && s.palette.length) {
const cuePalette = seedPalettes?.[chosenCue] || {};
const dots = ROLES.map((role) => {
const slot = cuePalette[role];
if (!slot || !Array.isArray(slot.at) || slot.at.length !== 2) return '';
const fill = String(slot.snapped || slot.hex || '');
return `<span class="dcx-cue-dot" data-role="${role}" style="--at-x:${Number(slot.at[0]) || 0}; --at-y:${Number(slot.at[1]) || 0};${fill ? ` --dot-fill:${escapeHtml(fill)};` : ''}"></span>`;
}).join('');
const roleRows = s.palette.map((entry) => `
<div class="dcx-cue-role">
<span class="dcx-cue-role-dot" style="--dot-fill:${entry.hex};"></span>
<span class="dcx-cue-role-name">${escapeHtml(entry.role)}</span>
<code>${entry.hex}</code>
<code>${escapeHtml(formatOklch(entry.hex))}</code>
</div>`).join('');
parts.push(block('The cue', `<div class="dcx-cue" data-dcx-hide-on-error>
<figure class="dcx-cue-frame">
<img data-dcx-cue-img src="/cues/${encodeURIComponent(chosenCue)}.png" alt="The chosen visual cue, ${escapeHtml(chosenCue)}" />
${dots}
</figure>
<div class="dcx-cue-card">
<span class="dcx-cue-tag">Chosen cue</span>
<h3 class="dcx-cue-name">${escapeHtml(chosenCue)}</h3>
${roleRows}
</div>
</div>`
+ note('The image the palette was sampled from, each role&rsquo;s sample point marked in its dealt color. The values beside it are the committed ones, which move when a role is edited after sampling.')));
const others = cueSlugs.filter((slug) => slug !== chosenCue);
if (others.length) {
parts.push(block('Also generated', `<div class="dcx-cue-strip">${others.map((slug) => `
<figure class="dcx-cue-thumb" data-dcx-hide-on-error>
<img src="/cues/${encodeURIComponent(slug)}.png" alt="" loading="lazy" />
<figcaption>${escapeHtml(slug)}</figcaption>
</figure>`).join('')}</div>`
+ note('The directions not taken, kept on disk in <code>.impeccable/visual-cues/</code>.')));
}
}
if (s.palette.length) {
const step = 100 / (s.palette.length + 1);
const fan = s.palette.map((entry, index) => `
@@ -1166,6 +1286,10 @@ const docLive = () => Boolean(docSession);
function startDocSession(doc) {
docSession = doc;
document.body.classList.add('dcx-live');
/* Rebuild the templates with this session's URLs: brand-asset images can
only load through the session, because the picker server exits right
after submit and article images fetch after that exit. */
refreshDocument();
schedulePoll(1500);
}
+199
View File
@@ -1654,6 +1654,13 @@ body.dcx-open { overflow: hidden; background: linear-gradient(180deg, var(--ks-l
background: transparent;
}
/* On screen 11 the field's rows stretch into the rail's fixed height; at the
sheet's natural height here they would collapse to the glyph's own box, so
the square proportion the slide showed is restated per cell. */
.dcx-proof--icons .picker-icon-cell {
aspect-ratio: 1;
}
/* ============================================================
Surface cards — the tile anatomy, kept.
============================================================ */
@@ -1709,6 +1716,198 @@ body.dcx-open { overflow: hidden; background: linear-gradient(180deg, var(--ks-l
color: var(--ks-text-faint);
}
/* ============================================================
The chosen cue and the brand-asset gallery.
.dcx-cue shows the image the palette was sampled from. The
sample dots arrive with source-image pixel coordinates
(cues.json palette[slug][role].at); the load listener stamps
the image's natural size on the frame, and the calc division
below keeps each dot glued to its pixel at any rendered width.
.dcx-cue-strip is the unpicked cues, dimmed, not interactive.
.dcx-marks proofs a provided logo on the committed primary and
neutral grounds; .dcx-boards frames boards and references wide.
============================================================ */
.dcx-cue {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
gap: clamp(16px, 2.5vw, 30px);
align-items: start;
}
@media (max-width: 760px) {
.dcx-cue { grid-template-columns: minmax(0, 1fr); }
}
.dcx-cue-frame {
position: relative;
margin: 0;
border: 1px solid var(--ks-rule);
background: var(--panel-bg);
}
.dcx-cue-frame img {
display: block;
width: 100%;
height: auto;
}
.dcx-cue-dot {
position: absolute;
left: calc(var(--at-x, 0) / var(--cue-w, 1024) * 100%);
top: calc(var(--at-y, 0) / var(--cue-h, 1024) * 100%);
width: 16px;
height: 16px;
transform: translate(-50%, -50%);
border-radius: 50%;
background: var(--dot-fill, var(--accent));
border: 2px solid var(--ks-champagne);
box-shadow: 0 1px 6px var(--shadow-color);
transition: opacity 0.3s var(--ks-ease);
}
.dcx-cue-frame:not([data-loaded="yes"]) .dcx-cue-dot { opacity: 0; }
.dcx-cue-card {
display: flex;
flex-direction: column;
gap: 10px;
padding: clamp(14px, 2vw, 22px);
border: 1px solid var(--ks-rule);
background: var(--panel-bg);
}
.dcx-cue-tag {
font-family: var(--ks-mono);
font-size: 0.62rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--ks-text-faint);
}
.dcx-cue-name {
margin: 0 0 4px;
font-family: var(--ks-font-display);
font-weight: 300;
font-size: 1.45rem;
color: var(--ks-text);
}
.dcx-cue-role {
display: grid;
grid-template-columns: 14px minmax(0, 1fr) auto auto;
gap: 10px;
align-items: center;
}
.dcx-cue-role-dot {
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--dot-fill, var(--accent));
border: 1px solid var(--ks-rule);
}
.dcx-cue-role-name {
color: var(--ks-text-muted);
font-size: 0.9rem;
}
.dcx-cue-role code {
font-family: var(--ks-mono);
font-size: 0.78rem;
color: var(--ks-text-faint);
white-space: nowrap;
}
.dcx-cue-strip {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 14px;
}
.dcx-cue-thumb {
margin: 0;
opacity: 0.6;
filter: saturate(0.7);
}
.dcx-cue-thumb img {
display: block;
width: 100%;
height: auto;
border: 1px solid var(--ks-rule);
}
.dcx-cue-thumb figcaption {
margin-top: 6px;
font-family: var(--ks-mono);
font-size: 0.68rem;
letter-spacing: 0.08em;
color: var(--ks-text-faint);
}
.dcx-marks,
.dcx-boards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
gap: clamp(16px, 2.5vw, 24px);
}
.dcx-mark { margin: 0; }
.dcx-mark-pair {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.dcx-mark-chip {
display: flex;
align-items: center;
justify-content: center;
min-height: 128px;
padding: 18px;
border: 1px solid var(--ks-rule);
background: var(--chip-ground, var(--ks-lacquer-raised));
}
.dcx-mark-chip img {
display: block;
max-width: 70%;
max-height: 88px;
}
.dcx-board { margin: 0; }
.dcx-board-frame {
display: block;
border: 1px solid var(--ks-rule);
background: var(--panel-bg);
}
.dcx-board-frame img {
display: block;
width: 100%;
height: auto;
}
.dcx-asset-caption { margin-top: 8px; }
.dcx-asset-caption code {
font-family: var(--ks-mono);
font-size: 0.78rem;
color: var(--ks-text-faint);
}
.dcx-asset-caption p {
margin: 4px 0 0;
max-width: 52ch;
color: var(--ks-text-muted);
font-size: 0.9rem;
line-height: 1.55;
}
/* ============================================================
Swatch board — each role at real size, both notations.
============================================================ */