This commit is contained in:
Abdul Wahab
2026-09-01 10:05:00 +05:00
parent 7e9a873499
commit 33ea98ae3e
3 changed files with 359 additions and 409 deletions
+79 -62
View File
@@ -1099,73 +1099,90 @@ const questions = [
</div>
<div class="picker-hub-grid">
<div class="picker-hub-col">
<button class="picker-hub-card" type="button" data-hub-target="06" data-hub-category="material" aria-describedby="picker-hub-tip-06">
<span class="picker-hub-eyebrow">Material</span>
<span class="picker-hub-title">Motion
<svg class="picker-hub-arrow" viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" aria-hidden="true"><path d="M0 4h14M10 0l4 4-4 4"></path></svg>
<span class="picker-hub-mark" data-hub-mark hidden>Edited</span>
</span>
<span class="picker-hub-preview" data-hub-preview aria-hidden="true"></span>
<span class="picker-hub-tabs" data-hub-tabs hidden></span>
<span class="picker-hub-tip" role="tooltip" id="picker-hub-tip-06" data-hub-tip>
<span class="picker-hub-summary" data-hub-summary></span>
</span>
</button>
<button class="picker-hub-card" type="button" data-hub-target="08" data-hub-category="material" aria-describedby="picker-hub-tip-08">
<span class="picker-hub-eyebrow">Material</span>
<span class="picker-hub-title">Boundaries
<svg class="picker-hub-arrow" viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" aria-hidden="true"><path d="M0 4h14M10 0l4 4-4 4"></path></svg>
<span class="picker-hub-mark" data-hub-mark hidden>Edited</span>
</span>
<span class="picker-hub-preview" data-hub-preview aria-hidden="true"></span>
<span class="picker-hub-tabs" data-hub-tabs hidden></span>
<span class="picker-hub-tip" role="tooltip" id="picker-hub-tip-08" data-hub-tip>
<span class="picker-hub-summary" data-hub-summary></span>
</span>
<div class="picker-hub-card">
<button class="picker-hub-open" type="button" data-hub-target="06">
<span class="picker-hub-title">Motion</span>
<svg class="picker-hub-arrow" viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" aria-hidden="true"><path d="M0 4h14M10 0l4 4-4 4"></path></svg>
</button>
<span class="picker-hub-preview" data-hub-preview aria-hidden="true"></span>
<span class="picker-hub-note" data-hub-note></span>
<div class="picker-hub-slider" data-hub-slider>
<button class="picker-hub-step" type="button" data-hub-step="-1" aria-label="Previous surface">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m12.5 4.5-5 5.5 5 5.5"></path></svg>
</button>
<span class="picker-hub-slider-label" data-hub-slider-label></span>
<button class="picker-hub-step" type="button" data-hub-step="1" aria-label="Next surface">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m7.5 4.5 5 5.5-5 5.5"></path></svg>
</button>
</div>
</div>
<div class="picker-hub-col">
<button class="picker-hub-card" type="button" data-hub-target="11" data-hub-category="iconography" aria-describedby="picker-hub-tip-11">
<span class="picker-hub-eyebrow">Iconography</span>
<span class="picker-hub-title">Icon set
<svg class="picker-hub-arrow" viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" aria-hidden="true"><path d="M0 4h14M10 0l4 4-4 4"></path></svg>
<span class="picker-hub-mark" data-hub-mark hidden>Edited</span>
</span>
<span class="picker-hub-preview" data-hub-preview aria-hidden="true"></span>
<span class="picker-hub-tabs" data-hub-tabs hidden></span>
<span class="picker-hub-tip" role="tooltip" id="picker-hub-tip-11" data-hub-tip>
<span class="picker-hub-summary" data-hub-summary></span>
</span>
</button>
<button class="picker-hub-card" type="button" data-hub-target="09" data-hub-category="material" aria-describedby="picker-hub-tip-09">
<span class="picker-hub-eyebrow">Material</span>
<span class="picker-hub-title">Corners
<svg class="picker-hub-arrow" viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" aria-hidden="true"><path d="M0 4h14M10 0l4 4-4 4"></path></svg>
<span class="picker-hub-mark" data-hub-mark hidden>Edited</span>
</span>
<span class="picker-hub-preview" data-hub-preview aria-hidden="true"></span>
<span class="picker-hub-tabs" data-hub-tabs hidden></span>
<span class="picker-hub-tip" role="tooltip" id="picker-hub-tip-09" data-hub-tip>
<span class="picker-hub-summary" data-hub-summary></span>
</span>
<div class="picker-hub-card">
<button class="picker-hub-open" type="button" data-hub-target="11">
<span class="picker-hub-title">Icon set</span>
<svg class="picker-hub-arrow" viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" aria-hidden="true"><path d="M0 4h14M10 0l4 4-4 4"></path></svg>
</button>
<span class="picker-hub-preview" data-hub-preview aria-hidden="true"></span>
<span class="picker-hub-note" data-hub-note></span>
<div class="picker-hub-slider" data-hub-slider>
<button class="picker-hub-step" type="button" data-hub-step="-1" aria-label="Previous surface">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m12.5 4.5-5 5.5 5 5.5"></path></svg>
</button>
<span class="picker-hub-slider-label" data-hub-slider-label></span>
<button class="picker-hub-step" type="button" data-hub-step="1" aria-label="Next surface">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m7.5 4.5 5 5.5-5 5.5"></path></svg>
</button>
</div>
</div>
<div class="picker-hub-col">
<button class="picker-hub-card" type="button" data-hub-target="10" data-hub-category="material" aria-describedby="picker-hub-tip-10">
<span class="picker-hub-eyebrow">Material</span>
<span class="picker-hub-title">Depth
<svg class="picker-hub-arrow" viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" aria-hidden="true"><path d="M0 4h14M10 0l4 4-4 4"></path></svg>
<span class="picker-hub-mark" data-hub-mark hidden>Edited</span>
</span>
<span class="picker-hub-preview" data-hub-preview aria-hidden="true"></span>
<span class="picker-hub-tabs" data-hub-tabs hidden></span>
<span class="picker-hub-tip" role="tooltip" id="picker-hub-tip-10" data-hub-tip>
<span class="picker-hub-summary" data-hub-summary></span>
</span>
<div class="picker-hub-card">
<button class="picker-hub-open" type="button" data-hub-target="08">
<span class="picker-hub-title">Boundaries</span>
<svg class="picker-hub-arrow" viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" aria-hidden="true"><path d="M0 4h14M10 0l4 4-4 4"></path></svg>
</button>
<span class="picker-hub-preview" data-hub-preview aria-hidden="true"></span>
<span class="picker-hub-note" data-hub-note></span>
<div class="picker-hub-slider" data-hub-slider>
<button class="picker-hub-step" type="button" data-hub-step="-1" aria-label="Previous surface">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m12.5 4.5-5 5.5 5 5.5"></path></svg>
</button>
<span class="picker-hub-slider-label" data-hub-slider-label></span>
<button class="picker-hub-step" type="button" data-hub-step="1" aria-label="Next surface">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m7.5 4.5 5 5.5-5 5.5"></path></svg>
</button>
</div>
</div>
<div class="picker-hub-card">
<button class="picker-hub-open" type="button" data-hub-target="09">
<span class="picker-hub-title">Corners</span>
<svg class="picker-hub-arrow" viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" aria-hidden="true"><path d="M0 4h14M10 0l4 4-4 4"></path></svg>
</button>
<span class="picker-hub-preview" data-hub-preview aria-hidden="true"></span>
<span class="picker-hub-note" data-hub-note></span>
<div class="picker-hub-slider" data-hub-slider>
<button class="picker-hub-step" type="button" data-hub-step="-1" aria-label="Previous surface">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m12.5 4.5-5 5.5 5 5.5"></path></svg>
</button>
<span class="picker-hub-slider-label" data-hub-slider-label></span>
<button class="picker-hub-step" type="button" data-hub-step="1" aria-label="Next surface">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m7.5 4.5 5 5.5-5 5.5"></path></svg>
</button>
</div>
</div>
<div class="picker-hub-card">
<button class="picker-hub-open" type="button" data-hub-target="10">
<span class="picker-hub-title">Depth</span>
<svg class="picker-hub-arrow" viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" aria-hidden="true"><path d="M0 4h14M10 0l4 4-4 4"></path></svg>
</button>
<span class="picker-hub-preview" data-hub-preview aria-hidden="true"></span>
<span class="picker-hub-note" data-hub-note></span>
<div class="picker-hub-slider" data-hub-slider>
<button class="picker-hub-step" type="button" data-hub-step="-1" aria-label="Previous surface">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m12.5 4.5-5 5.5 5 5.5"></path></svg>
</button>
<span class="picker-hub-slider-label" data-hub-slider-label></span>
<button class="picker-hub-step" type="button" data-hub-step="1" aria-label="Next surface">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m7.5 4.5 5 5.5-5 5.5"></path></svg>
</button>
</div>
</div>
</div>
+108 -150
View File
@@ -5257,15 +5257,6 @@ const HUB_GROUPS = {
'10': 'depth-style',
'11': 'icon-pack',
};
/* The icon set renders no per-surface fields, so it has no
dataset.chosen. A change event on its group is the one signal
that a person picked rather than the markup default: programmatic
checks never fire it. */
const hubEdited = new Set();
document.addEventListener('change', ({ target }) => {
if (target?.name === 'icon-pack') hubEdited.add(target.name);
});
const hubSurfaceLabel = (mode) => modeInputs.find((input) => input.value === mode)?.dataset.surfaceLabel ?? mode;
/* The display title lives on the option row of the question's own
@@ -5291,10 +5282,8 @@ function hubSurfaceRows(group) {
}));
}
/* A summary row is a surface pill on the left and the value docked
right behind a patina dot, the same signal the surface tabs and the
option rows spend on a committed choice. The plain line survives
only for the skipped-card message. */
/* The only line the renderer still builds by hand: the skipped-card
message, which stands in for the note. */
function hubPlainLine(text) {
const line = document.createElement('span');
line.className = 'picker-hub-line';
@@ -5302,33 +5291,6 @@ function hubPlainLine(text) {
return line;
}
function hubRow(surfaceLabel, valueTitle, group, value) {
const entry = document.createElement('span');
entry.className = 'picker-hub-entry';
const pill = document.createElement('span');
pill.className = 'picker-hub-pill';
const label = document.createElement('span');
label.className = 'picker-hub-pill-label';
label.textContent = surfaceLabel;
const val = document.createElement('span');
val.className = 'picker-hub-value';
const dot = document.createElement('i');
dot.className = 'picker-hub-dot';
dot.setAttribute('aria-hidden', 'true');
val.append(dot, valueTitle);
pill.append(label, val);
entry.append(pill);
if (group !== 'icon-pack') {
const note = document.createElement('span');
note.className = 'picker-hub-note';
const why = value ? hubOptionDesc(group, value) : '';
note.textContent = why;
note.hidden = !why;
entry.append(note);
}
return entry;
}
/* The one-line reason the current answer is a sound default, read off
the option row's own description so the hub never restates copy.
dataset.copy is read first because applyAllows swaps textContent for
@@ -5424,18 +5386,37 @@ const hubQuestionBoard = (screen, mode) => {
return boards.find((board) => board.dataset.motionCell === `${mode}-${answer}`) ?? boards[0];
};
/* The preview and its tab strip for one card. The active tab carries
the patina dot and the surface's answer, so the choice reads without
hovering; the tooltip carries the summary rows. */
function renderHubPreview(cardNode, rows) {
/* CSS cannot divide one length by another to reach the unitless number
scale() needs, so that number is measured here: the clone's own desktop
pane, read unscaled through offsetWidth, against the frame it has to
sit in. The frame is one fixed shape for every card, so the pane is
fitted rather than stretched to it and centred in whichever direction
it falls short. The handset beside the pane lands outside the frame,
which is the crop. */
function fitHubPreview(cardNode) {
const previewSlot = cardNode.querySelector('[data-hub-preview]');
const tabsSlot = cardNode.querySelector('[data-hub-tabs]');
const screen = cardNode.dataset.hubTarget;
const group = HUB_GROUPS[screen];
if (!previewSlot || previewSlot.classList.contains('picker-hub-preview--icons')) return;
const proof = previewSlot.firstElementChild;
const pane = proof?.querySelector('.ps-desktop, .pv-desktop') ?? proof?.firstElementChild;
const width = previewSlot.clientWidth;
const height = previewSlot.clientHeight;
if (!width || !height || !pane?.offsetWidth || !pane.offsetHeight) return;
const scale = Math.min(width / pane.offsetWidth, height / pane.offsetHeight);
cardNode.style.setProperty('--pk-hub-scale', scale);
cardNode.style.setProperty('--pk-hub-x', `${(width - pane.offsetWidth * scale) / 2}px`);
cardNode.style.setProperty('--pk-hub-y', `${(height - pane.offsetHeight * scale) / 2}px`);
}
/* One card's miniature, for the surface the slider is parked on. */
function renderHubPreview(cardNode, screen, group, mode) {
const previewSlot = cardNode.querySelector('[data-hub-preview]');
previewSlot.classList.toggle('picker-hub-preview--icons', group === 'icon-pack');
if (group === 'icon-pack') {
tabsSlot.hidden = true;
tabsSlot.replaceChildren();
previewSlot.replaceChildren(hubProof(iconSheet));
/* The 16px row alone. A pack is chosen for interface work and that row
is the same glyphs at the size they will be read at, so it is the
specimen worth showing; the magnified field above it on screen 11 is
a reading this card has no room for and no need of. */
previewSlot.replaceChildren(hubProof(iconStrip));
/* The glyphs are fetched on screen 11's first arrival, which a run
that stops at the hub never makes; fetch them here and re-render
once. The empty marker is the failed state, which must not refetch
@@ -5445,14 +5426,9 @@ function renderHubPreview(cardNode, rows) {
}
return;
}
const modes = rows.map((row) => row.mode);
const mode = modes.includes(cardNode.dataset.hubSurface) ? cardNode.dataset.hubSurface : modes[0];
cardNode.dataset.hubSurface = mode ?? '';
const board = mode ? hubQuestionBoard(screen, mode) : null;
if (!board) {
previewSlot.replaceChildren();
tabsSlot.hidden = true;
tabsSlot.replaceChildren();
return;
}
if (group === 'motion-energy') {
@@ -5462,82 +5438,62 @@ function renderHubPreview(cardNode, rows) {
previewSlot.replaceChildren(proof);
/* The route stops are measured off the clone's own layout, exactly
what screen 06 does for the originals on arrival. */
requestAnimationFrame(() => plotMotionRoute(proof.firstChild));
requestAnimationFrame(() => {
plotMotionRoute(proof.firstChild);
fitHubPreview(cardNode);
});
} else {
previewSlot.replaceChildren(hubProof(board, hubPreviewMarks(mode)));
}
tabsSlot.hidden = rows.length === 0;
tabsSlot.replaceChildren(...rows.map((row) => {
const tab = document.createElement('span');
tab.className = 'picker-hub-tab';
tab.dataset.hubTab = row.mode;
const dot = document.createElement('i');
dot.className = 'picker-hub-tab-dot';
dot.setAttribute('aria-hidden', 'true');
tab.append(dot, hubSurfaceLabel(row.mode));
if (row.mode === mode) {
tab.dataset.on = '';
const value = document.createElement('span');
value.className = 'picker-hub-tab-value';
value.textContent = hubOptionTitle(group, row.value);
tab.append(value);
}
return tab;
}));
fitHubPreview(cardNode);
}
/* A card reads top to bottom: the question's name, a miniature of its own
board, the line saying what the current answer does, and the control
that walks the surfaces. One surface is shown at a time, so the note and
the label both speak for whichever one the slider is parked on. */
function renderHubCard(cardNode) {
const group = HUB_GROUPS[cardNode.dataset.hubTarget];
const opener = cardNode.querySelector('[data-hub-target]');
const screen = opener?.dataset.hubTarget;
const group = HUB_GROUPS[screen];
if (!group) return;
const summary = cardNode.querySelector('[data-hub-summary]');
const mark = cardNode.querySelector('[data-hub-mark]');
const previewSlot = cardNode.querySelector('[data-hub-preview]');
const tabsSlot = cardNode.querySelector('[data-hub-tabs]');
const target = document.querySelector(`.picker-screen[data-screen="${cardNode.dataset.hubTarget}"]`);
const note = cardNode.querySelector('[data-hub-note]');
const slider = cardNode.querySelector('[data-hub-slider]');
const label = cardNode.querySelector('[data-hub-slider-label]');
const target = document.querySelector(`.picker-screen[data-screen="${screen}"]`);
const skipped = Boolean(target?.hasAttribute('data-skip'));
cardNode.classList.toggle('is-skipped', skipped);
cardNode.disabled = skipped;
cardNode.setAttribute('aria-disabled', skipped ? 'true' : 'false');
opener.disabled = skipped;
opener.setAttribute('aria-disabled', skipped ? 'true' : 'false');
if (skipped) {
summary.replaceChildren();
previewSlot.replaceChildren(hubPlainLine('Not asked of these surfaces'));
tabsSlot.hidden = true;
tabsSlot.replaceChildren();
cardNode.classList.remove('is-edited');
mark.hidden = true;
previewSlot.replaceChildren();
note.replaceChildren(hubPlainLine('Not asked of these surfaces'));
note.hidden = false;
slider.hidden = true;
return;
}
const rows = hubSurfaceRows(group);
let edited;
let lines;
if (rows.length === 0) {
const checked = document.querySelector(`input[name="${group}"]:checked`);
edited = hubEdited.has(group);
lines = [hubRow(
'All surfaces',
checked ? hubOptionTitle(group, checked.value) : '',
group,
checked?.value,
)];
} else {
edited = rows.some((row) => row.chosen);
if (rows.length > 1 && rows.every((row) => row.value === rows[0].value)) {
lines = [hubRow('All surfaces', hubOptionTitle(group, rows[0].value), group, rows[0].value)];
} else if (rows.length === 1) {
lines = [hubRow(hubSurfaceLabel(rows[0].mode), hubOptionTitle(group, rows[0].value), group, rows[0].value)];
} else {
lines = rows.map((row) => hubRow(
hubSurfaceLabel(row.mode),
hubOptionTitle(group, row.value),
group,
row.value,
));
}
}
summary.replaceChildren(...lines);
cardNode.classList.toggle('is-edited', edited);
mark.hidden = !edited;
renderHubPreview(cardNode, rows);
const modes = rows.map((row) => row.mode);
const mode = modes.includes(cardNode.dataset.hubSurface) ? cardNode.dataset.hubSurface : modes[0];
cardNode.dataset.hubSurface = mode ?? '';
/* A flat question answers for the whole run, so its value comes off the
checked radio and it has no surfaces to walk. */
const value = mode
? rows.find((row) => row.mode === mode)?.value
: document.querySelector(`input[name="${group}"]:checked`)?.value;
const why = value ? hubOptionDesc(group, value) : '';
note.textContent = why;
note.hidden = !why;
const answer = document.createElement('span');
answer.className = 'picker-hub-slider-value';
answer.textContent = value ? hubOptionTitle(group, value) : '';
label.replaceChildren(...(mode ? [`${hubSurfaceLabel(mode)} · `, answer] : [answer]));
slider.hidden = false;
/* One surface is not a trip, so the chevrons stand down and the strip
stays as the answer's label. */
for (const step of slider.querySelectorAll('[data-hub-step]')) step.hidden = modes.length < 2;
renderHubPreview(cardNode, screen, group, mode);
}
function renderHub() {
@@ -5545,50 +5501,52 @@ function renderHub() {
for (const cardNode of hubScreen.querySelectorAll('.picker-hub-card')) renderHubCard(cardNode);
}
/* The tooltip floats above the card and flips below when the card sits
too near the viewport top to hold it; when neither side holds the
whole panel, the roomier side takes it. Measured when the pointer or
focus arrives, before the reveal transition starts; the tip is
opacity-hidden rather than display-hidden, so its height is real. */
function placeHubTip(cardNode) {
const tip = cardNode.querySelector('[data-hub-tip]');
if (!tip) return;
const rect = cardNode.getBoundingClientRect();
const need = tip.offsetHeight + 20;
const above = rect.top;
const below = window.innerHeight - rect.bottom;
cardNode.dataset.tipAt = (above >= need || above >= below) ? 'above' : 'below';
function stepHubSurface(cardNode, step) {
const opener = cardNode?.querySelector('[data-hub-target]');
const group = HUB_GROUPS[opener?.dataset.hubTarget];
if (!group) return;
const modes = hubSurfaceRows(group).map((row) => row.mode);
if (modes.length < 2) return;
const at = modes.indexOf(cardNode.dataset.hubSurface);
cardNode.dataset.hubSurface = modes[(at + step + modes.length) % modes.length];
renderHubCard(cardNode);
}
hubScreen?.addEventListener('pointerover', (event) => {
const cardNode = event.target.closest('.picker-hub-card');
if (cardNode) placeHubTip(cardNode);
});
hubScreen?.addEventListener('focusin', (event) => {
const cardNode = event.target.closest('.picker-hub-card');
if (cardNode) placeHubTip(cardNode);
});
/* Cards and the finish CTA jump by screen id. The inline nav script
owns goTo(); it listens for this event, so no swap logic is
duplicated here. A disabled card never reaches this handler. A mini
tab hit re-clones its own card instead and never travels: the card
click must stay the jump to the question. */
duplicated here. A step button is a sibling of the opener rather than
a child of it, so walking the surfaces never travels to the question
and needs nothing stopped. */
hubScreen?.addEventListener('click', (event) => {
const tab = event.target.closest('[data-hub-tab]');
if (tab) {
event.stopPropagation();
const cardNode = tab.closest('.picker-hub-card');
cardNode.dataset.hubSurface = tab.dataset.hubTab;
renderHubCard(cardNode);
const step = event.target.closest('[data-hub-step]');
if (step) {
stepHubSurface(step.closest('.picker-hub-card'), Number(step.dataset.hubStep));
return;
}
const cardNode = event.target.closest('[data-hub-target]');
if (!cardNode || cardNode.disabled) return;
const opener = event.target.closest('[data-hub-target]');
if (!opener || opener.disabled) return;
document.dispatchEvent(new CustomEvent('picker:goto', {
detail: { screen: cardNode.dataset.hubTarget },
detail: { screen: opener.dataset.hubTarget },
}));
});
/* Arrow keys walk the surfaces too: a control that reads as a slider owes
a keyboard the same trip its chevrons offer a pointer. */
hubScreen?.addEventListener('keydown', (event) => {
const step = { ArrowLeft: -1, ArrowRight: 1 }[event.key];
if (!step || !event.target.closest('[data-hub-slider]')) return;
event.preventDefault();
stepHubSurface(event.target.closest('.picker-hub-card'), step);
});
/* The grid flips to one full-width column at the narrow breakpoint, and a
scale measured against the three-column width would stay behind. */
if (hubScreen && window.ResizeObserver) {
new ResizeObserver(() => {
for (const cardNode of hubScreen.querySelectorAll('.picker-hub-card')) fitHubPreview(cardNode);
}).observe(hubScreen);
}
for (const input of modeInputs) {
input.addEventListener('change', () => {
syncModesNext();
+172 -197
View File
@@ -8120,12 +8120,14 @@ body.picker-page {
}
/* ============================================================
Screen 04b: the configure hub. Five near-uniform preview cards
flow a three-column grid, three over two; the column wrappers
are display: contents so the markup's masonry grouping costs
nothing. Sharp corners and hairline rules, the same vocabulary
as the option rows. Previews, tabs, and the tooltip's summary
rows are written by renderHub().
Screen 04b: the configure hub. Five preview cards flow a
three-column grid, three over two; the column wrappers are
display: contents so the markup's grouping costs nothing.
Every card reads top to bottom and centred: the question's
name, a miniature of its own board, the one line saying what
the current answer does, and the control that walks the
surfaces. Sharp corners and hairline rules, the same
vocabulary as the option rows.
============================================================ */
.picker-hub {
display: grid;
@@ -8139,29 +8141,43 @@ body.picker-page {
font-weight: 300;
}
/* Six tracks so a card can take two of them: three across the top, and
the remaining pair centred underneath by starting one track in. */
.picker-hub-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 14px;
align-items: stretch;
width: min(100%, 1080px);
}
.picker-hub-col {
display: contents;
.picker-hub-card {
grid-column: span 2;
}
.picker-hub-card:nth-child(4) {
grid-column: 2 / span 2;
}
/* Every card lays its four parts on the same four rows of the grid, so a
two-line note on one card and a one-line note on its neighbour still
leave both titles on one line, both previews on the next, and both
sliders on the last. Centring each card's own stack instead let the
taller stack start higher, which is what pushed Corners' title above
Depth's. */
.picker-hub-card {
position: relative;
grid-row: span 4;
display: grid;
gap: 16px;
grid-template-rows: subgrid;
align-items: center;
justify-items: center;
width: 100%;
padding: 24px 28px;
text-align: left;
padding: 22px 20px;
text-align: center;
background: var(--ks-lacquer-raised);
border: 1px solid var(--ks-rule);
border-radius: 0;
cursor: pointer;
transition:
background-color 180ms var(--ks-ease),
border-color 180ms var(--ks-ease);
@@ -8171,28 +8187,34 @@ body.picker-page {
border-color: color-mix(in oklab, var(--ks-patina) 45%, var(--ks-rule));
}
.picker-hub-card:focus-visible {
outline: 2px solid var(--ks-patina-deep);
outline-offset: -2px;
z-index: 1;
/* The title is the control, and its stretched pseudo-element carries
the whole card's click without wrapping it. A card that wrapped its
contents in a button could not hold the slider's own buttons, which
is why the surfaces used to be spans no keyboard could reach. */
.picker-hub-open {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 0;
border: 0;
background: none;
color: inherit;
font-family: var(--ks-font);
cursor: pointer;
}
.picker-hub-eyebrow {
.picker-hub-open::after {
content: "";
position: absolute;
top: 24px;
right: 28px;
color: var(--ks-text-faint);
font-size: var(--ks-type-eyebrow-size);
font-weight: 500;
letter-spacing: var(--ks-type-eyebrow-track);
text-transform: uppercase;
inset: 0;
}
.picker-hub-open:focus-visible {
outline: 2px solid var(--ks-patina-deep);
outline-offset: -2px;
}
.picker-hub-title {
display: flex;
align-items: center;
gap: 8px;
padding-right: 72px;
color: var(--ks-champagne);
font-size: var(--ks-type-title-size);
font-weight: var(--ks-type-title-weight);
@@ -8212,121 +8234,125 @@ body.picker-page {
transform: translateX(2px);
}
.picker-hub-mark {
margin-left: auto;
padding: 2px 8px;
border: 1px solid var(--ks-kinpaku-deep);
color: var(--ks-kinpaku);
font-size: var(--ks-type-eyebrow-size);
font-weight: 500;
letter-spacing: var(--ks-type-eyebrow-track);
text-transform: uppercase;
}
.picker-hub-summary {
display: grid;
gap: 14px;
color: var(--ks-text-muted);
font-size: 0.86rem;
/* Why the current answer is a sound default, read off the option
row's own description by renderHubCard(). */
/* Top of its row rather than centred in it: the row is as tall as the
longest note in the band, and a shorter one centred there would drop
its first line below its neighbour's. */
.picker-hub-note {
align-self: start;
max-width: 34ch;
color: var(--ks-text-faint);
font-size: 0.84rem;
line-height: 1.5;
}
.picker-hub-entry {
display: grid;
gap: 8px;
.picker-hub-note[hidden] {
display: none;
}
.picker-hub-line {
display: block;
}
/* Surface pill wraps the label and the committed value; patina dot
plus the text mix mirror the surface tabs' answered state. */
.picker-hub-pill {
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: fit-content;
max-width: 100%;
padding: 6px 9px;
color: var(--ks-text-faint);
font-size: 12.5px;
letter-spacing: 0.015em;
border: 1px solid var(--ks-rule);
border-radius: 2px;
}
.picker-hub-pill-label {
flex: 0 0 auto;
}
.picker-hub-value {
display: inline-flex;
align-items: center;
gap: 8px;
color: color-mix(in oklab, var(--ks-patina) 85%, var(--ks-champagne));
text-align: right;
}
.picker-hub-dot {
flex: 0 0 auto;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--ks-patina);
}
/* Why this answer is a sound default, read off the option row's own
description by renderHub(). One note per answer row. */
.picker-hub-note {
padding-inline: 2px;
color: var(--ks-text-faint);
font-size: 0.84rem;
line-height: 1.5;
}
/* The card body: a clone of the question's own artboard, written by
renderHubPreview(). The frame borrows design-context.css's
.dcx-proof--board hooks for the committed bodies, so no rendition
rule is restated here. Pointer events stay off: everything inside
the card except a mini tab is the jump to the question. */
/* The board is authored at 918px wide and mixes fixed pixels,
viewport clamps, and container queries keyed to absolute height, so
reflowing it into a card crushes it: the layout shrinks and the
drawn detail does not. Laid out at its natural width it resolves
exactly as it does on its own screen, and one transform shrinks the
finished picture. The frame crops to the desktop pane, so the
handset beside it falls outside. renderHubCard() measures the pane
and writes the scale and the ratio; the fallbacks below are the
four-surface geometry, so the frame is never wrong-shaped even for
the frame before the measurement lands. */
.picker-hub-preview {
position: relative;
display: block;
width: 100%;
/* A miniature, never a magnification: past the pane's own width the
frame would blow the board up bigger than the screen it came from
ever draws it. The cap only bites in the single-column collapse. */
max-width: 560px;
/* One shape for all five. The desktop panes differ by about a percent
between the artboard and the derived drawings, and measuring each
card's own pane made five frames of five heights. The pane is fitted
inside this box instead, so the cards line up. */
aspect-ratio: 1.34;
overflow: hidden;
background: var(--ks-lacquer);
border: 1px solid var(--ks-rule);
pointer-events: none;
}
.picker-hub-preview > .picker-hub-proof {
position: absolute;
top: 0;
left: 0;
width: var(--pk-hub-natural, 918px);
transform: translate(var(--pk-hub-x, 0px), var(--pk-hub-y, 0px)) scale(var(--pk-hub-scale, 0.32));
transform-origin: top left;
}
/* The icon sheet takes the same frame as the boards beside it, filled by
a grid of its own glyphs. They stay a hard 16px, which is the size a
pack is chosen to be read at and the reason this one preview is drawn
rather than scaled; the room between them is the frame's, not theirs. */
.picker-hub-preview--icons > .picker-hub-proof {
position: absolute;
inset: 0;
width: auto;
transform: none;
}
.picker-hub-preview--icons .picker-icon-strip-row {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
grid-auto-rows: minmax(0, 1fr);
place-items: center;
gap: 0;
height: 100%;
padding: 10px 8px;
color: var(--ks-text-muted);
}
.picker-hub-proof {
display: block;
}
/* The mini tab strip: the surface-tab vocabulary at card scale. Spans
rather than buttons because the card itself is one, and a button
cannot hold another. */
.picker-hub-tabs {
display: flex;
flex-wrap: wrap;
gap: 3px;
width: fit-content;
/* One surface at a time, the chevrons wrapped around the surface and
the answer it holds. The strip keeps the surface tabs' vocabulary,
and it sits above the stretched opener so its buttons take their
own clicks. */
.picker-hub-slider {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 100%;
/* A card with nothing to walk stands its chevrons down, and the strip
would shrink to the label and sit lower than the strips beside it.
The height is the one with chevrons in it. */
min-height: 34px;
padding: 3px;
background: var(--ks-lacquer-raised);
border: 1px solid var(--ks-rule);
border-radius: 2px;
}
.picker-hub-tabs[hidden] {
.picker-hub-slider[hidden] {
display: none;
}
.picker-hub-tab {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
.picker-hub-step {
display: grid;
place-items: center;
width: 26px;
height: 26px;
padding: 0;
color: var(--ks-text-muted);
font-size: 12.5px;
letter-spacing: 0.015em;
background: none;
border: 0;
border-radius: 2px;
cursor: pointer;
transition:
@@ -8334,99 +8360,43 @@ body.picker-page {
color 180ms var(--ks-ease);
}
.picker-hub-tab:hover {
.picker-hub-step:hover {
color: var(--ks-champagne);
background: color-mix(in oklab, var(--ks-patina) 9%, var(--ks-lacquer-raised));
background: color-mix(in oklab, var(--ks-patina) 12%, var(--ks-lacquer-raised));
}
.picker-hub-tab[data-on] {
color: var(--ks-champagne);
background: color-mix(in oklab, var(--ks-patina) 14%, var(--ks-lacquer-raised));
.picker-hub-step:focus-visible {
outline: 2px solid var(--ks-patina-deep);
outline-offset: -2px;
}
/* The same report the full-size strip's dot gives: filled on the tab
being previewed, hollow on the rest. */
.picker-hub-tab-dot {
flex: none;
width: 7px;
height: 7px;
border: 1px solid color-mix(in oklab, var(--ks-patina) 55%, transparent);
border-radius: 50%;
.picker-hub-step[hidden] {
display: none;
}
.picker-hub-tab[data-on] .picker-hub-tab-dot {
background: var(--ks-patina);
border-color: var(--ks-patina);
.picker-hub-step svg {
width: 13px;
height: 13px;
fill: none;
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: square;
}
.picker-hub-tab-value {
.picker-hub-slider-label {
padding-inline: 8px;
color: var(--ks-text-muted);
font-size: 12.5px;
letter-spacing: 0.015em;
}
.picker-hub-slider-value {
color: color-mix(in oklab, var(--ks-patina) 85%, var(--ks-champagne));
}
/* The summary rows' new home: a floating panel on the card's own
hover and focus, the contrast alert tip's pattern at card width.
Above by default; placeHubTip() flips it below when the card sits
too near the viewport top. The hovered card rises over its
neighbours so the panel is never clipped by the masonry. */
.picker-hub-card:is(:hover, :focus-within) {
z-index: 3;
}
.picker-hub-tip {
position: absolute;
left: -1px;
right: -1px;
bottom: calc(100% + 10px);
z-index: 12;
padding: 16px 18px;
display: grid;
gap: 14px;
opacity: 0;
background: var(--ks-lacquer-raised);
border: 1px solid var(--ks-rule);
border-radius: 2px;
pointer-events: none;
transform: translateY(3px);
transition:
opacity 180ms var(--ks-ease),
transform 180ms var(--ks-ease);
}
.picker-hub-tip::after {
content: "";
position: absolute;
left: 50%;
bottom: -5px;
width: 8px;
height: 8px;
background: var(--ks-lacquer-raised);
border-right: 1px solid var(--ks-rule);
border-bottom: 1px solid var(--ks-rule);
transform: translateX(-50%) rotate(45deg);
}
.picker-hub-card[data-tip-at="below"] .picker-hub-tip {
top: calc(100% + 10px);
bottom: auto;
transform: translateY(-3px);
}
.picker-hub-card[data-tip-at="below"] .picker-hub-tip::after {
top: -5px;
bottom: auto;
border: 0;
border-left: 1px solid var(--ks-rule);
border-top: 1px solid var(--ks-rule);
}
.picker-hub-card:not(.is-skipped):is(:hover, :focus-within) .picker-hub-tip {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.picker-hub-tip,
.picker-hub-tab {
.picker-hub-step,
.picker-hub-arrow {
transition: none;
}
}
@@ -8442,8 +8412,7 @@ body.picker-page {
}
.picker-hub-card.is-skipped .picker-hub-title,
.picker-hub-card.is-skipped .picker-hub-eyebrow,
.picker-hub-card.is-skipped .picker-hub-summary,
.picker-hub-card.is-skipped .picker-hub-note,
.picker-hub-card.is-skipped .picker-hub-arrow {
color: var(--ks-text-mute-deep);
}
@@ -8455,4 +8424,10 @@ body.picker-page {
.picker-hub-grid {
grid-template-columns: minmax(0, 1fr);
}
/* One track, so the two-track spans and the centring offset both go. */
.picker-hub-card,
.picker-hub-card:nth-child(4) {
grid-column: auto;
}
}