mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-20 01:56:37 +03:00
update
This commit is contained in:
@@ -292,7 +292,7 @@ const questions = [
|
||||
<legend>Surfaces</legend>
|
||||
|
||||
<label class="picker-mode-tile">
|
||||
<input type="checkbox" name="surface-modes" value="persuade" aria-label="Landing pages, marketing, campaigns, and pricing" checked />
|
||||
<input type="checkbox" name="surface-modes" value="persuade" aria-label="Landing pages, marketing, campaigns, and pricing" data-surface-label="Landing page" data-strategies="restrained committed full-palette drenched" data-strategy-default="committed" checked />
|
||||
<span class="picker-mode-preview picker-mode-preview--exact" aria-hidden="true">
|
||||
<span class="picker-preview">
|
||||
<span class="pv-desktop">
|
||||
@@ -332,7 +332,7 @@ const questions = [
|
||||
</label>
|
||||
|
||||
<label class="picker-mode-tile">
|
||||
<input type="checkbox" name="surface-modes" value="operate" aria-label="App UI, dashboards, admin, and settings" />
|
||||
<input type="checkbox" name="surface-modes" value="operate" aria-label="App UI, dashboards, admin, and settings" data-surface-label="App UI" data-strategies="restrained committed full-palette" data-strategy-default="restrained" />
|
||||
<span class="picker-mode-preview picker-mode-preview--exact" aria-hidden="true">
|
||||
<span class="picker-preview picker-preview--ops">
|
||||
<span class="pv-desktop">
|
||||
@@ -421,7 +421,7 @@ const questions = [
|
||||
</label>
|
||||
|
||||
<label class="picker-mode-tile">
|
||||
<input type="checkbox" name="surface-modes" value="read" aria-label="Docs, articles, guides, and changelogs" />
|
||||
<input type="checkbox" name="surface-modes" value="read" aria-label="Docs, articles, guides, and changelogs" data-surface-label="Docs" data-strategies="restrained committed full-palette" data-strategy-default="restrained" />
|
||||
<span class="picker-mode-preview picker-mode-preview--exact" aria-hidden="true">
|
||||
<span class="picker-preview picker-preview--docs">
|
||||
<span class="pv-desktop">
|
||||
@@ -476,7 +476,7 @@ const questions = [
|
||||
</label>
|
||||
|
||||
<label class="picker-mode-tile">
|
||||
<input type="checkbox" name="surface-modes" value="experience" aria-label="Portfolios, galleries, and showcases" />
|
||||
<input type="checkbox" name="surface-modes" value="experience" aria-label="Portfolios, galleries, and showcases" data-surface-label="Portfolio" data-strategies="restrained committed drenched" data-strategy-default="restrained" />
|
||||
<span class="picker-mode-preview picker-mode-preview--exact" aria-hidden="true">
|
||||
<span class="picker-preview picker-preview--gallery">
|
||||
<span class="pv-desktop">
|
||||
@@ -764,6 +764,14 @@ const questions = [
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
{/* Every chosen surface is colored separately and leaves its own
|
||||
answer, so each has a field to leave it in. They stay disabled
|
||||
until their tile is chosen, which is what keeps a surface
|
||||
nobody asked for out of the answers. */}
|
||||
{surfaces.map(({ value }) => (
|
||||
<input type="hidden" name={`color-strategy-${value}`} data-surface-strategy={value} disabled />
|
||||
))}
|
||||
|
||||
<div class="picker-bands picker-bands--compact" data-band-scope="strategy">
|
||||
{roles.map(([role, label]) => (
|
||||
<div class="picker-band-item" data-band-item={role}>
|
||||
@@ -787,7 +795,13 @@ const questions = [
|
||||
<p class="picker-band-status" data-reorder-status role="status" aria-live="polite"></p>
|
||||
</div>
|
||||
|
||||
<Artboard class="picker-strategy-preview" strategyHook />
|
||||
{/* The stage the chosen surfaces are drawn on. Its contents are
|
||||
lifted from the tiles on screen 01b, so it is filled by script;
|
||||
the ratio is the artboard's so the CTA sits where it sits on
|
||||
every other screen. */}
|
||||
<div class="picker-strategy-stage" data-strategy-stage>
|
||||
<div class="picker-surface-tabs" data-surface-tabs role="tablist" aria-label="Surface being colored" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="picker-actions-stack">
|
||||
|
||||
@@ -185,26 +185,30 @@ function renderPreview() {
|
||||
preview.style.setProperty('--pv-n-ink', contrastInk(state().colors.neutral));
|
||||
}
|
||||
|
||||
function syncCommittedPalette(target) {
|
||||
/* The prefix exists for the strategy stage, which needs the committed colors
|
||||
under names its own CSS never rewrites: the remap there reads these to know
|
||||
what was chosen, and reading the live --pv-* would read its own output. */
|
||||
function syncCommittedPalette(target, prefix = 'pv') {
|
||||
const committed = roleMap((role) => $(`[name="palette-${role}"]`).value);
|
||||
if (!target || Object.values(committed).some((hex) => !hex)) return;
|
||||
for (const role of ROLES) target.style.setProperty(`--pv-${role}`, committed[role]);
|
||||
const set = (name, value) => target.style.setProperty(`--${prefix}-${name}`, value);
|
||||
for (const role of ROLES) set(role, committed[role]);
|
||||
// One ink per fill a preview can paint a label on: the strategy decides
|
||||
// which of the three carries the button on any given artboard.
|
||||
target.style.setProperty('--pv-n-ink', contrastInk(committed.neutral));
|
||||
target.style.setProperty('--pv-p-ink', contrastInk(committed.primary));
|
||||
target.style.setProperty('--pv-t-ink', contrastInk(committed.tertiary));
|
||||
set('n-ink', contrastInk(committed.neutral));
|
||||
set('p-ink', contrastInk(committed.primary));
|
||||
set('t-ink', contrastInk(committed.tertiary));
|
||||
// And one reading version of each accent that the type artboards set words
|
||||
// in, per ground it can land on: the neutral page, or the primary once the
|
||||
// strategy drenches the page in it.
|
||||
target.style.setProperty('--pv-p-on-n', readableOn(committed.primary, committed.neutral));
|
||||
target.style.setProperty('--pv-t-on-n', readableOn(committed.tertiary, committed.neutral));
|
||||
target.style.setProperty('--pv-t-on-p', readableOn(committed.tertiary, committed.primary));
|
||||
set('p-on-n', readableOn(committed.primary, committed.neutral));
|
||||
set('t-on-n', readableOn(committed.tertiary, committed.neutral));
|
||||
set('t-on-p', readableOn(committed.tertiary, committed.primary));
|
||||
// Labels on filled buttons: hue is the role that paints the fill, ground is
|
||||
// that same fill — not the page neutral that produced the 1.63:1 regression.
|
||||
target.style.setProperty('--pv-p-on-p', readableOn(committed.primary, committed.primary));
|
||||
target.style.setProperty('--pv-t-on-t', readableOn(committed.tertiary, committed.tertiary));
|
||||
target.style.setProperty('--pv-p-on-i', readableOn(committed.primary, contrastInkHex(committed.primary)));
|
||||
// that same fill, not the page neutral that produced the 1.63:1 regression.
|
||||
set('p-on-p', readableOn(committed.primary, committed.primary));
|
||||
set('t-on-t', readableOn(committed.tertiary, committed.tertiary));
|
||||
set('p-on-i', readableOn(committed.primary, contrastInkHex(committed.primary)));
|
||||
}
|
||||
|
||||
/* Every field is checked only when it is present. The manifest merges over
|
||||
@@ -1311,6 +1315,7 @@ function recommitPalette() {
|
||||
if (!$('[name="palette-source"]').value) return;
|
||||
for (const role of ROLES) $(`[name="palette-${role}"]`).value = state().colors[role];
|
||||
paintStrategyBands();
|
||||
paintStage();
|
||||
for (const artboard of document.querySelectorAll('.picker-screen[data-active] [data-artboard]')) {
|
||||
syncCommittedPalette(artboard);
|
||||
}
|
||||
@@ -1536,6 +1541,7 @@ panel.onclick = async (e) => {
|
||||
// is captured for the transition as it is handed over, and a strip still
|
||||
// holding last run's colors is what would be captured.
|
||||
paintStrategyBands();
|
||||
paintStage();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1570,7 +1576,16 @@ document.addEventListener('picker:screenchange', (event) => {
|
||||
}
|
||||
// Coming back to the strategy screen from further along, where the palette may
|
||||
// have been reordered on the screen it was left on.
|
||||
if (event.detail.screen === '03') paintStrategyBands();
|
||||
if (event.detail.screen === '03') {
|
||||
paintStrategyBands();
|
||||
paintStage();
|
||||
} else {
|
||||
/* Everywhere else previews the leading surface's answer, so the radio the
|
||||
later screens read is parked there whenever 03 is off screen. Without
|
||||
this the run would carry whichever surface was last on the tab, and a
|
||||
strategy switched off for that surface would leave the answer empty. */
|
||||
showSurface(chosenSurfaces()[0]?.value);
|
||||
}
|
||||
// Arriving is the quietest moment there is, so the rail settles here even
|
||||
// if it is already in order: the chosen pair is the row you land on.
|
||||
if (event.detail.screen === '04') {
|
||||
@@ -1630,6 +1645,9 @@ const landingPreview = preview.cloneNode(true);
|
||||
let previewSource;
|
||||
|
||||
function syncModePreview() {
|
||||
// Screen 03 draws every chosen surface, not just the leading one, so it is
|
||||
// rebuilt from here: every path that changes the tiles already runs this.
|
||||
syncSurfaces();
|
||||
const chosen = modeInputs.findIndex((input) => input.checked);
|
||||
// A tile drawn in something other than this component keeps the landing page,
|
||||
// which is also the floor for the empty answer the continue button blocks.
|
||||
@@ -1646,6 +1664,163 @@ function syncModePreview() {
|
||||
renderPreview();
|
||||
}
|
||||
|
||||
/* Screen 03 colors the surfaces that were chosen rather than one fixed page,
|
||||
and it colors each of them separately: the answer that suits the marketing
|
||||
page rarely suits the tool it sells. Every chosen tile's drawing is mounted
|
||||
on the stage, one is shown, and a tab in the frame's corner carries between
|
||||
them when there is more than one to carry between. */
|
||||
const stage = document.querySelector('[data-strategy-stage]');
|
||||
const surfaceTabs = document.querySelector('[data-surface-tabs]');
|
||||
const strategyInputs = [...document.querySelectorAll('input[name="color-strategy"]')];
|
||||
const strategyRows = new Map(strategyInputs.map((input) => [input.value, input.closest('.picker-strategy-option')]));
|
||||
|
||||
/* Why a strategy is out belongs to the strategy, not to the pairing, so it is
|
||||
written once here rather than once per surface that rules it out. */
|
||||
const BLOCKED_BECAUSE = {
|
||||
drenched: 'Too loud for a page people work in or read at length.',
|
||||
'full-palette': 'Four colors on duty compete with the work on show.',
|
||||
};
|
||||
|
||||
const chosenSurfaces = () => modeInputs.filter((input) => input.checked);
|
||||
const surfaceInput = (value) => modeInputs.find((input) => input.value === value);
|
||||
const allowedFor = (value) => (surfaceInput(value)?.dataset.strategies ?? '').split(' ').filter(Boolean);
|
||||
const defaultFor = (value) => surfaceInput(value)?.dataset.strategyDefault ?? 'restrained';
|
||||
const strategyField = (value) => document.querySelector(`[data-surface-strategy="${value}"]`);
|
||||
const strategyTitle = (value) => strategyRows.get(value)?.querySelector('.picker-strategy-title').textContent ?? value;
|
||||
let activeSurface = null;
|
||||
|
||||
/* One drawing per chosen surface, painted with the committed palette. All of
|
||||
them stay mounted and one is shown, so a tab switch costs a hidden attribute
|
||||
rather than a rebuild and the frame never blinks. */
|
||||
function syncSurfaces() {
|
||||
if (!stage) return;
|
||||
const chosen = chosenSurfaces();
|
||||
for (const node of stage.querySelectorAll('[data-surface]')) node.remove();
|
||||
for (const input of chosen) {
|
||||
const source = modePreviews[modeInputs.indexOf(input)];
|
||||
if (!source) continue;
|
||||
const clone = source.cloneNode(true);
|
||||
// Decorative here as on the tile, but the marker sits on the tile's
|
||||
// wrapper rather than on the drawing, so it does not survive the lift.
|
||||
clone.setAttribute('aria-hidden', 'true');
|
||||
for (const node of [clone, ...clone.querySelectorAll('[id]')]) node.removeAttribute('id');
|
||||
clone.dataset.surface = input.value;
|
||||
stage.append(clone);
|
||||
}
|
||||
paintStage();
|
||||
|
||||
/* Every chosen surface leaves an answer whether or not it was ever opened,
|
||||
so the field is filled with the default the moment the tile is chosen and
|
||||
the tab reports it as unset until someone says otherwise. */
|
||||
for (const input of modeInputs) {
|
||||
const field = strategyField(input.value);
|
||||
if (!field) continue;
|
||||
field.disabled = !input.checked;
|
||||
if (!input.checked) {
|
||||
field.value = '';
|
||||
delete field.dataset.chosen;
|
||||
} else if (!field.value) {
|
||||
field.value = defaultFor(input.value);
|
||||
}
|
||||
}
|
||||
|
||||
buildTabs(chosen);
|
||||
showSurface(chosen.some((input) => input.value === activeSurface) ? activeSurface : chosen[0]?.value);
|
||||
}
|
||||
|
||||
/* Painted once on the frame rather than on each drawing inside it, so the
|
||||
strategy layer keeps a fixed reading of what was chosen and the drawings
|
||||
themselves carry no inline color for it to argue with. */
|
||||
function paintStage() {
|
||||
syncCommittedPalette(stage, 'pkc');
|
||||
}
|
||||
|
||||
/* One surface needs no tabs: the frame is already showing the only answer
|
||||
there is. The dot is the whole report on state, filled once the surface has
|
||||
been answered deliberately and hollow while it is still holding a default. */
|
||||
function buildTabs(chosen) {
|
||||
if (!surfaceTabs) return;
|
||||
surfaceTabs.hidden = chosen.length < 2;
|
||||
surfaceTabs.replaceChildren(...chosen.map((input) => {
|
||||
const tab = document.createElement('button');
|
||||
tab.type = 'button';
|
||||
tab.className = 'picker-surface-tab';
|
||||
tab.dataset.surfaceTab = input.value;
|
||||
tab.innerHTML = '<span class="picker-surface-dot"></span>';
|
||||
tab.append(input.dataset.surfaceLabel ?? input.value);
|
||||
tab.onclick = () => showSurface(input.value);
|
||||
return tab;
|
||||
}));
|
||||
markTabs();
|
||||
}
|
||||
|
||||
function markTabs() {
|
||||
for (const tab of surfaceTabs?.children ?? []) {
|
||||
const value = tab.dataset.surfaceTab;
|
||||
const field = strategyField(value);
|
||||
const set = Boolean(field?.dataset.chosen);
|
||||
const on = value === activeSurface;
|
||||
tab.dataset.set = set ? 'yes' : 'no';
|
||||
tab.setAttribute('aria-pressed', on ? 'true' : 'false');
|
||||
tab.tabIndex = on ? 0 : -1;
|
||||
tab.setAttribute('aria-label', set
|
||||
? `${tab.textContent}, colored ${strategyTitle(field.value).toLowerCase()}`
|
||||
: `${tab.textContent}, no color strategy chosen yet`);
|
||||
}
|
||||
}
|
||||
|
||||
function showSurface(value) {
|
||||
if (!value || !stage) return;
|
||||
activeSurface = value;
|
||||
for (const clone of stage.querySelectorAll('[data-surface]')) {
|
||||
clone.hidden = clone.dataset.surface !== value;
|
||||
}
|
||||
applyApplicability();
|
||||
const field = strategyField(value);
|
||||
const wanted = field?.value || defaultFor(value);
|
||||
const input = strategyInputs.find((radio) => radio.value === wanted);
|
||||
if (input) input.checked = true;
|
||||
markTabs();
|
||||
}
|
||||
|
||||
/* A strategy a surface cannot carry is left in place and turned off rather
|
||||
than removed: the list keeps its shape as you move between surfaces, and the
|
||||
row says why it is out instead of vanishing without a reason. */
|
||||
function applyApplicability() {
|
||||
const allowed = new Set(allowedFor(activeSurface));
|
||||
for (const [value, row] of strategyRows) {
|
||||
if (!row) continue;
|
||||
const ok = allowed.has(value);
|
||||
const desc = row.querySelector('.picker-strategy-desc');
|
||||
desc.dataset.copy ??= desc.textContent;
|
||||
desc.textContent = ok ? desc.dataset.copy : BLOCKED_BECAUSE[value] ?? desc.dataset.copy;
|
||||
row.classList.toggle('is-blocked', !ok);
|
||||
row.querySelector('input').disabled = !ok;
|
||||
}
|
||||
}
|
||||
|
||||
for (const input of strategyInputs) {
|
||||
input.addEventListener('change', () => {
|
||||
const field = strategyField(activeSurface);
|
||||
if (!input.checked || !field) return;
|
||||
field.value = input.value;
|
||||
field.dataset.chosen = 'yes';
|
||||
markTabs();
|
||||
});
|
||||
}
|
||||
|
||||
/* Arrow keys walk the group, which is the one thing a row of buttons owes a
|
||||
keyboard once only the current tab is in the tab order. */
|
||||
surfaceTabs?.addEventListener('keydown', (event) => {
|
||||
const step = { ArrowLeft: -1, ArrowRight: 1 }[event.key];
|
||||
if (!step) return;
|
||||
const tabs = [...surfaceTabs.children];
|
||||
const next = tabs[(tabs.findIndex((tab) => tab.dataset.surfaceTab === activeSurface) + step + tabs.length) % tabs.length];
|
||||
event.preventDefault();
|
||||
showSurface(next.dataset.surfaceTab);
|
||||
next.focus();
|
||||
});
|
||||
|
||||
for (const input of modeInputs) {
|
||||
input.addEventListener('change', () => {
|
||||
syncModesNext();
|
||||
|
||||
+198
-11
@@ -185,8 +185,13 @@ body.picker-page {
|
||||
view-transition-name: pk-band-neutral;
|
||||
}
|
||||
|
||||
/* Both screens now judge their answer on the same drawing, so the pair the
|
||||
transition carries is one component in two sizes: the frame grows out of the
|
||||
palette screen's rather than dissolving into a different page. The hidden
|
||||
surfaces are display: none and so are not rendered, which is what keeps the
|
||||
name unique while every chosen surface stays mounted. */
|
||||
.picker-screen[data-screen="02"] .picker-preview,
|
||||
.picker-screen[data-screen="03"] .picker-strategy-preview {
|
||||
.picker-screen[data-screen="03"] .picker-strategy-stage > .picker-preview {
|
||||
view-transition-name: pk-test-page;
|
||||
}
|
||||
|
||||
@@ -2565,15 +2570,23 @@ body.picker-page {
|
||||
only holds where there are four options to divide. */
|
||||
.picker-screen[data-screen="03"] .picker-strategy-grid > .picker-type-rail {
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
/* The artboard is width: 100% at 1.865/1, so its height is this column's own
|
||||
width over that ratio: the container's 1500 cap less its padding, less the
|
||||
rail and the gap. What the rail can spend without growing the row. */
|
||||
--pk-art: calc((min(1500px, 100vw) - 582px) / 1.865);
|
||||
/* And the band is what gives when that budget will not cover four legible
|
||||
rows: 368px is the four at their floor plus the gap and the panel's
|
||||
hairline. A shorter reading of the palette costs less than a squeezed
|
||||
sentence, so the band yields first and the rows yield last. */
|
||||
--pk-band: clamp(72px, calc(var(--pk-art) - 368px), 96px);
|
||||
/* The rail is measured against the viewport rather than against the frame
|
||||
beside it. The frame draws the chosen surface at that drawing's own ratio,
|
||||
which is wider than the artboard this column used to hold, so matching its
|
||||
height would spend the whole column on four rows of 78px.
|
||||
|
||||
It is also the tallest thing in its row now, which makes it the column
|
||||
that decides where the CTA lands. The shared budget is measured for a row
|
||||
an artboard sets and runs about 30px short of what this screen spends
|
||||
around its grid; at a 900px viewport that difference is the whole
|
||||
overhang, so this screen measures itself. Taken at 1600x900, the tightest
|
||||
of the common sizes. */
|
||||
--pk-fit: calc(100svh - 376px);
|
||||
/* 546px is the comfortable rail: four rows at 108, the band at 96, the gap,
|
||||
and the panel's hairline. Below that the band is the first thing to give,
|
||||
down to a floor where the palette is still readable, because a shorter
|
||||
reading of four colors costs less than a squeezed sentence. */
|
||||
--pk-band: clamp(72px, calc(var(--pk-fit) - 450px), 96px);
|
||||
}
|
||||
|
||||
/* And the rows come in under what the artboard beside them asks for, so the row
|
||||
@@ -2590,7 +2603,7 @@ body.picker-page {
|
||||
measures against the column budget alone, which on a short viewport asks
|
||||
for less than the floor and clips the fourth option to reach it. */
|
||||
max-height: none;
|
||||
grid-auto-rows: max(88px, min(108px, calc((var(--pk-art) - var(--pk-band) - 16px) / 4), calc((var(--pk-column) - 2px) / 4)));
|
||||
grid-auto-rows: max(88px, min(108px, calc((var(--pk-fit) - var(--pk-band) - 18px) / 4)));
|
||||
}
|
||||
|
||||
/* Equal rows, sized by the tallest one's copy rather than by how many there
|
||||
@@ -2773,6 +2786,180 @@ body.picker-page {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
The strategy stage. Screen 03 colors the surfaces chosen on screen 01b, so
|
||||
the frame holds one drawing per chosen surface and shows one at a time. The
|
||||
drawings are lifted whole from the tiles, which is why the frame takes their
|
||||
ratio rather than imposing the artboard's: two of the four are drawn in
|
||||
pixels and two scale off their own height, and stretching either to a taller
|
||||
frame breaks the drawing rather than filling it.
|
||||
|
||||
The rail beside it is the taller column, so the frame is centred against it
|
||||
and the tabs ride in the space above, where they cover none of the page they
|
||||
are switching between.
|
||||
============================================================ */
|
||||
.picker-strategy-stage {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto;
|
||||
align-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* One drawing is shown and the rest stay mounted, so switching costs an
|
||||
attribute rather than a rebuild. [hidden] is a display: none that the
|
||||
component's own display: grid outranks, so it is restated here. */
|
||||
.picker-preview[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The group only appears with a second surface to switch to; a single surface
|
||||
is already the only thing the frame can be showing. */
|
||||
.picker-surface-tabs {
|
||||
justify-self: end;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
background: var(--ks-lacquer-raised);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.picker-surface-tabs[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.picker-surface-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 5px 11px;
|
||||
color: var(--ks-text-muted);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
font-family: inherit;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 180ms var(--ks-ease),
|
||||
color 180ms var(--ks-ease);
|
||||
}
|
||||
|
||||
.picker-surface-tab:hover {
|
||||
color: var(--ks-champagne);
|
||||
background: color-mix(in oklab, var(--ks-patina) 9%, var(--ks-lacquer-raised));
|
||||
}
|
||||
|
||||
.picker-surface-tab[aria-pressed="true"] {
|
||||
color: var(--ks-champagne);
|
||||
background: color-mix(in oklab, var(--ks-patina) 14%, var(--ks-lacquer-raised));
|
||||
}
|
||||
|
||||
.picker-surface-tab:focus-visible {
|
||||
outline: 2px solid var(--ks-patina-deep);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* The whole report on state: filled once the surface has been answered
|
||||
deliberately, hollow while it is still carrying the default it was given. */
|
||||
.picker-surface-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border: 1px solid color-mix(in oklab, var(--ks-patina) 55%, transparent);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.picker-surface-tab[data-set="yes"] .picker-surface-dot {
|
||||
background: var(--ks-patina);
|
||||
border-color: var(--ks-patina);
|
||||
}
|
||||
|
||||
/* A strategy the surface in the frame cannot carry. The row keeps its place so
|
||||
the list does not reshuffle as you move between surfaces, and its sentence
|
||||
is replaced by the reason it is out. */
|
||||
.picker-strategy-option.is-blocked {
|
||||
opacity: 0.42;
|
||||
/* Deaf to the cursor as well as to the click, so a row the surface cannot
|
||||
carry cannot preview itself either. It also keeps the hover and checked
|
||||
branches below written the way the artboard's are: no blocked row can be
|
||||
the one being hovered, so neither branch has to ask. */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
The strategy, applied to the surface in the frame.
|
||||
|
||||
The drawings read the palette through role names, and every shade they mix
|
||||
is mixed from those same names, so a strategy is a remapping of the roles
|
||||
rather than a second set of rules per drawing. Redistributing four roles
|
||||
reaches all four surfaces at once, and a new surface arrives already
|
||||
answering to it.
|
||||
|
||||
The committed colors are painted onto the stage under --pkc-*, which gives
|
||||
each remap a fixed handle on what was chosen. Reading --pv-* instead would
|
||||
read whatever the remap had just written, which is how drenched would end
|
||||
up mixing its ground out of its own ink.
|
||||
============================================================ */
|
||||
.picker-strategy-stage > .picker-preview {
|
||||
--pv-primary: var(--pkc-primary, var(--ks-champagne));
|
||||
--pv-secondary: var(--pkc-secondary, var(--ks-patina));
|
||||
--pv-tertiary: var(--pkc-tertiary, var(--ks-kinpaku));
|
||||
--pv-neutral: var(--pkc-neutral, var(--ks-lacquer-raised));
|
||||
--pv-n-ink: var(--pkc-n-ink, var(--ks-champagne));
|
||||
--pv-p-ink: var(--pkc-p-ink, var(--ks-champagne));
|
||||
}
|
||||
|
||||
/* Restrained: the page is its neutral and the primary is the only color that
|
||||
arrives. The other two stand down into the ground rather than disappearing,
|
||||
so what they were holding is still legible as structure. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="color-strategy"][value="restrained"]) .picker-strategy-stage > .picker-preview,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="restrained"]:checked) .picker-strategy-stage > .picker-preview {
|
||||
--pv-secondary: color-mix(in oklab, var(--pkc-neutral) 78%, var(--pkc-n-ink));
|
||||
--pv-tertiary: color-mix(in oklab, var(--pkc-neutral) 86%, var(--pkc-n-ink));
|
||||
--pv-t-ink: var(--pkc-n-ink);
|
||||
--pv-t-on-t: var(--pkc-n-ink);
|
||||
--pv-t-on-n: color-mix(in oklab, var(--pkc-n-ink) 62%, var(--pkc-neutral));
|
||||
--pv-t-on-p: var(--pkc-p-ink);
|
||||
}
|
||||
|
||||
/* Committed: one color doing all the work the other two were doing, and a
|
||||
trace of it in the paper as well. Without the paper, a surface whose accents
|
||||
are small (a dashboard, a doc) separates from restrained by a few tinted
|
||||
pixels, which is not what "does real work across the page" describes. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="color-strategy"][value="committed"]) .picker-strategy-stage > .picker-preview,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="committed"]:checked) .picker-strategy-stage > .picker-preview {
|
||||
--pv-neutral: color-mix(in oklab, var(--pkc-neutral) 95%, var(--pkc-primary));
|
||||
--pv-secondary: var(--pkc-primary);
|
||||
--pv-tertiary: var(--pkc-primary);
|
||||
--pv-t-ink: var(--pkc-p-ink);
|
||||
--pv-t-on-t: var(--pkc-p-on-p);
|
||||
--pv-t-on-n: var(--pkc-p-on-n);
|
||||
--pv-t-on-p: var(--pkc-p-on-p);
|
||||
}
|
||||
|
||||
/* Full palette is the drawing as it was authored: four roles, each already
|
||||
holding a different part of the page. It needs no block of its own, which
|
||||
is the same reason the artboard's restrained does not have one. */
|
||||
|
||||
/* Drenched: the ground becomes the primary, and everything that was picked out
|
||||
in the primary is now picked out against it in ink. The shades the drawings
|
||||
mix from the neutral follow the ground without being told, which is what
|
||||
keeps the images and panels tinted rather than left as white holes. */
|
||||
#picker-form:has(.picker-strategy-option:hover input[name="color-strategy"][value="drenched"]) .picker-strategy-stage > .picker-preview,
|
||||
#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="drenched"]:checked) .picker-strategy-stage > .picker-preview {
|
||||
--pv-neutral: var(--pkc-primary);
|
||||
--pv-n-ink: var(--pkc-p-ink);
|
||||
--pv-primary: var(--pkc-p-ink);
|
||||
--pv-p-ink: var(--pkc-primary);
|
||||
--pv-p-on-n: var(--pkc-p-ink);
|
||||
--pv-p-on-p: var(--pkc-primary);
|
||||
--pv-secondary: var(--pkc-tertiary);
|
||||
--pv-tertiary: var(--pkc-tertiary);
|
||||
--pv-t-on-n: var(--pkc-t-on-p);
|
||||
}
|
||||
|
||||
/* Hovering an option previews its strategy before the click commits it.
|
||||
While any strategy option is hovered, the hovered branch drives the
|
||||
preview and the checked branch stands down; hovering Restrained needs
|
||||
|
||||
Reference in New Issue
Block a user