mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 15:16:35 +03:00
update
This commit is contained in:
@@ -407,7 +407,7 @@ const scaleRows = [['H1', 6], ['H2', 5], ['H3', 4], ['H4', 3], ['H5', 2], ['H6',
|
||||
<div class="ps-editorial">
|
||||
<div class="ps-editorial-copy">
|
||||
<strong data-type-section-title></strong>
|
||||
<span><i data-type-section-body></i><i data-type-section-body></i></span>
|
||||
<p class="pt-section-body" data-type-section-body></p>
|
||||
<em data-type-section-link></em>
|
||||
</div>
|
||||
<div class="ps-gallery">
|
||||
@@ -439,7 +439,7 @@ const scaleRows = [['H1', 6], ['H2', 5], ['H3', 4], ['H4', 3], ['H5', 2], ['H6',
|
||||
</div>
|
||||
<div class="ps-editorial-copy">
|
||||
<strong data-type-section-title></strong>
|
||||
<span><i data-type-section-body></i><i data-type-section-body></i></span>
|
||||
<p class="pt-section-body" data-type-section-body></p>
|
||||
</div>
|
||||
<div class="ps-gallery">
|
||||
<div class="ps-gallery-item"><i></i><span><b data-type-gallery-title></b><b data-type-gallery-meta></b></span></div>
|
||||
@@ -633,9 +633,17 @@ const scaleRows = [['H1', 6], ['H2', 5], ['H3', 4], ['H4', 3], ['H5', 2], ['H6',
|
||||
|
||||
<p class="picker-modal-status" data-custom-status aria-live="polite"></p>
|
||||
|
||||
<p class="picker-modal-hint" id="picker-font-modal-hint" data-custom-hint aria-live="polite"></p>
|
||||
|
||||
<div class="picker-modal-actions">
|
||||
<button class="picker-back" type="button" data-font-custom-close>Cancel</button>
|
||||
<button class="ks-button ks-button-primary" type="button" data-font-custom-save>Use these fonts</button>
|
||||
<button
|
||||
class="ks-button ks-button-secondary"
|
||||
type="button"
|
||||
data-font-custom-save
|
||||
aria-describedby="picker-font-modal-hint"
|
||||
disabled
|
||||
>Use these fonts</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -31,7 +31,7 @@ let fontManifest;
|
||||
is judged on texture, and real prose pulls the eye into reading it. */
|
||||
const LOREM = {
|
||||
sentence: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.',
|
||||
lines: ['Ut enim ad minim veniam, quis nostrud exercitation', 'ullamco laboris nisi ut aliquip ex ea commodo.'],
|
||||
paragraph: 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo.',
|
||||
};
|
||||
|
||||
/* The desktop artboard sets three cards and the phone two, so a fourth would
|
||||
@@ -328,8 +328,7 @@ function syncFontPair(pair) {
|
||||
document.querySelectorAll('[data-type-section-title]').forEach((node) => {
|
||||
node.textContent = preview.sectionTitle;
|
||||
});
|
||||
fillIndexed(desktop, '[data-type-section-body]', LOREM.lines);
|
||||
fillIndexed(phoneBody, '[data-type-section-body]', LOREM.lines);
|
||||
for (const node of document.querySelectorAll('[data-type-section-body]')) node.textContent = LOREM.paragraph;
|
||||
document.querySelectorAll('[data-type-section-link]').forEach((node) => {
|
||||
node.textContent = preview.sectionLink;
|
||||
});
|
||||
@@ -346,7 +345,13 @@ function syncFontPair(pair) {
|
||||
document.querySelector('[name="font-body-source"]').value = pair.body.source || '';
|
||||
}
|
||||
|
||||
function addPairCard(pair, { checked = false, prepend = false } = {}) {
|
||||
/* Where each pair belongs when it is not the chosen one. The rail renders a
|
||||
rotation of this list rather than the list itself, so hoisting the answer
|
||||
to the top never loses the order the rest were dealt in. An uploaded pair
|
||||
joins at the front: it is the one the user made. */
|
||||
const pairOrder = [];
|
||||
|
||||
function addPairCard(pair, { checked = false, first = false } = {}) {
|
||||
const node = pairTemplate.content.firstElementChild.cloneNode(true);
|
||||
const input = node.querySelector('input');
|
||||
input.value = pair.id;
|
||||
@@ -362,23 +367,32 @@ function addPairCard(pair, { checked = false, prepend = false } = {}) {
|
||||
node.querySelector('[data-pair-heading]').textContent = pair.heading.family;
|
||||
node.querySelector('[data-pair-body]').textContent = pair.body.family;
|
||||
node.querySelector('[data-pair-why]').textContent = pair.why;
|
||||
if (prepend) fontOptions.prepend(node);
|
||||
else fontOptions.append(node);
|
||||
if (first) pairOrder.unshift(node);
|
||||
else pairOrder.push(node);
|
||||
fontOptions.append(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
function removePairCard(node) {
|
||||
const index = pairOrder.indexOf(node);
|
||||
if (index !== -1) pairOrder.splice(index, 1);
|
||||
node.remove();
|
||||
}
|
||||
|
||||
function renderFontPairs(manifest, fallback) {
|
||||
fontManifest = normalizeFontManifest(manifest);
|
||||
fontOptions.toggleAttribute('data-fallback', fallback);
|
||||
manifest.pairs.forEach((pair, index) => addPairCard(pair, { checked: index === 0 }));
|
||||
loadFontStylesheet(manifest.pairs);
|
||||
syncFontPair(manifest.pairs[0]);
|
||||
applyHoist();
|
||||
}
|
||||
|
||||
fontOptions.onchange = ({ target }) => {
|
||||
if (!target.matches('input[name="font-pair"]')) return;
|
||||
const pair = fontManifest.pairs.find(({ id }) => id === target.value);
|
||||
if (pair) syncFontPair(pair);
|
||||
requestHoist();
|
||||
};
|
||||
|
||||
/* Scroll by whole rows so an option never ends up half in frame, and disable
|
||||
@@ -408,6 +422,103 @@ function wireListScroll(list) {
|
||||
const syncScrollButtons = wireListScroll(fontOptions);
|
||||
wireListScroll(scaleOptions);
|
||||
|
||||
/* The chosen pair takes the top of the rail, so the answer is the first thing
|
||||
the list shows and the rest keep their dealt order underneath it.
|
||||
|
||||
The move is not made at the moment of choosing, and that is the whole
|
||||
design. Reordering under a live cursor drags the row the user just clicked
|
||||
out from under the pointer and parks a different pair where the next click
|
||||
is already aimed. Reordering on a radio group's arrow keys is worse: every
|
||||
press both moves focus and commits, so a list that re-sorts per press
|
||||
re-sorts between presses and the group cannot be crossed at all.
|
||||
|
||||
So the rail reorders only while nobody is working it. A selection sets the
|
||||
request; the pointer leaving, or focus leaving, spends it. Arriving on the
|
||||
screen spends it too, which is the backstop if a settle is ever missed. */
|
||||
const typeRail = fontOptions.closest('.picker-type-rail');
|
||||
const RAIL_NAV_KEYS = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', ' ']);
|
||||
let hoistPending = false;
|
||||
let pointerInRail = false;
|
||||
let keyboardInRail = false;
|
||||
let hoistFlash;
|
||||
|
||||
/* :hover covers the one case the pointer events miss: the cursor already
|
||||
resting where the rail appears, which fires no enter of its own. */
|
||||
const railBusy = () => pointerInRail
|
||||
|| typeRail.matches(':hover')
|
||||
|| (keyboardInRail && typeRail.contains(document.activeElement));
|
||||
|
||||
/* Nothing is under the cursor when the rows move, so the move itself needs no
|
||||
transition to be readable. What it needs is somewhere for the eye to land
|
||||
after it: the row that just took the top comes up bright and settles to its
|
||||
resting checked state. Color only, so the acknowledgement cannot disturb
|
||||
the list it is pointing at. */
|
||||
function flashHoisted(node) {
|
||||
clearTimeout(hoistFlash);
|
||||
for (const other of pairOrder) other.removeAttribute('data-hoisted');
|
||||
// Reading a layout property between the two writes is what restarts the
|
||||
// animation on a row that is still marked from the previous hoist.
|
||||
void node.offsetWidth;
|
||||
node.dataset.hoisted = '';
|
||||
hoistFlash = setTimeout(() => node.removeAttribute('data-hoisted'), 700);
|
||||
}
|
||||
|
||||
function applyHoist({ force = false } = {}) {
|
||||
hoistPending = false;
|
||||
const chosen = fontOptions.querySelector('input[name="font-pair"]:checked')?.closest('.picker-type-option');
|
||||
if (!chosen) return;
|
||||
const wanted = [chosen, ...pairOrder.filter((node) => node !== chosen)];
|
||||
const shown = [...fontOptions.querySelectorAll('.picker-type-option')];
|
||||
const moved = wanted.some((node, index) => node !== shown[index]);
|
||||
if (moved) {
|
||||
// Rows move as real nodes so tab order, reading order, and what is on
|
||||
// screen stay one order. Re-parenting can drop focus on the way, which
|
||||
// would strand a keyboard user outside the group they were just in.
|
||||
const focused = document.activeElement;
|
||||
for (const node of wanted) fontOptions.append(node);
|
||||
if (fontOptions.contains(focused) && document.activeElement !== focused) {
|
||||
focused.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
if (!moved && !force) return;
|
||||
fontOptions.scrollTo({ top: 0 });
|
||||
syncScrollButtons();
|
||||
if (moved) flashHoisted(chosen);
|
||||
}
|
||||
|
||||
function requestHoist() {
|
||||
hoistPending = true;
|
||||
if (!railBusy()) applyHoist();
|
||||
}
|
||||
|
||||
function settleHoist() {
|
||||
if (hoistPending && !railBusy()) applyHoist();
|
||||
}
|
||||
|
||||
typeRail.addEventListener('pointerenter', () => {
|
||||
pointerInRail = true;
|
||||
});
|
||||
|
||||
// A frame of slack so :hover has resolved before the guard reads it.
|
||||
typeRail.addEventListener('pointerleave', () => {
|
||||
pointerInRail = false;
|
||||
requestAnimationFrame(settleHoist);
|
||||
});
|
||||
|
||||
typeRail.addEventListener('pointerdown', () => {
|
||||
keyboardInRail = false;
|
||||
});
|
||||
|
||||
typeRail.addEventListener('keydown', ({ key }) => {
|
||||
if (RAIL_NAV_KEYS.has(key)) keyboardInRail = true;
|
||||
});
|
||||
|
||||
typeRail.addEventListener('focusout', ({ relatedTarget }) => {
|
||||
if (typeRail.contains(relatedTarget)) return;
|
||||
keyboardInRail = false;
|
||||
settleHoist();
|
||||
});
|
||||
|
||||
/* Type scale.
|
||||
|
||||
The numbers are the real ones: step n is 16px * ratio^n, and a Golden Ratio
|
||||
@@ -476,6 +587,8 @@ const fontModal = document.querySelector('[data-font-modal]');
|
||||
const customStatus = fontModal.querySelector('[data-custom-status]');
|
||||
const customFile = (role) => fontModal.querySelector(`[data-custom-file="${role}"]`);
|
||||
const customUrl = (role) => fontModal.querySelector(`[data-custom-url="${role}"]`);
|
||||
const customSave = fontModal.querySelector('[data-font-custom-save]');
|
||||
const customHint = fontModal.querySelector('[data-custom-hint]');
|
||||
const FONT_SOURCE_SEP = '\n';
|
||||
const FONT_FILE_EXTENSIONS = new Set(['.woff2', '.woff', '.ttf', '.otf']);
|
||||
const customFilesByRole = { heading: [], body: [] };
|
||||
@@ -526,6 +639,7 @@ function renderCustomFileList(role) {
|
||||
customFilesByRole[role] = customFilesByRole[role].filter((entry) => !sameFontFile(entry, file));
|
||||
setChosenFontFiles(customFile(role), customFilesByRole[role]);
|
||||
renderCustomFileList(role);
|
||||
syncCustomSave();
|
||||
customStatus.textContent = '';
|
||||
};
|
||||
item.append(name, remove);
|
||||
@@ -533,6 +647,31 @@ function renderCustomFileList(role) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Either kind of source satisfies a role: a URL or at least one file. */
|
||||
const roleHasSource = (role) => customFilesByRole[role].length > 0 || customUrl(role).value.trim() !== '';
|
||||
|
||||
/* Both roles are required. A heading-only sheet used to commit and let the
|
||||
body fall back to whichever pair happened to be selected, which is a
|
||||
substitution the user never asked for and never saw.
|
||||
|
||||
The hint carries the reason. A commit that is dead with no explanation
|
||||
leaves the user clicking an inert control to find out why, so the footer
|
||||
names the role that is still open and the button is never the only
|
||||
feedback. It reads as instruction rather than correction: nothing here is
|
||||
wrong yet, the sheet is just unfinished. */
|
||||
function syncCustomSave() {
|
||||
const missing = ['heading', 'body'].filter((role) => !roleHasSource(role));
|
||||
customSave.disabled = missing.length > 0;
|
||||
if (missing.length === 2) {
|
||||
customHint.textContent = 'Heading and body each need a URL or a file.';
|
||||
} else if (missing.length === 1) {
|
||||
const role = missing[0] === 'heading' ? 'Heading' : 'Body';
|
||||
customHint.textContent = `${role} still needs a URL or a file.`;
|
||||
} else {
|
||||
customHint.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
function parseFontSources(source) {
|
||||
if (!source) return [];
|
||||
if (source.includes(FONT_SOURCE_SEP)) return source.split(FONT_SOURCE_SEP).filter(Boolean);
|
||||
@@ -545,8 +684,13 @@ document.querySelector('[data-font-custom-open]').onclick = () => {
|
||||
customFilesByRole[role] = [];
|
||||
customFile(role).value = '';
|
||||
setChosenFontFiles(customFile(role), []);
|
||||
// The URL goes with the files. Clearing half the sheet leaves a field
|
||||
// holding an address whose companion upload is already gone, and the
|
||||
// commit would then read as available for work the user did last time.
|
||||
customUrl(role).value = '';
|
||||
renderCustomFileList(role);
|
||||
}
|
||||
syncCustomSave();
|
||||
fontModal.showModal();
|
||||
};
|
||||
|
||||
@@ -564,7 +708,9 @@ for (const role of ['heading', 'body']) {
|
||||
setChosenFontFiles(target, customFilesByRole[role]);
|
||||
target.value = '';
|
||||
renderCustomFileList(role);
|
||||
syncCustomSave();
|
||||
};
|
||||
customUrl(role).oninput = syncCustomSave;
|
||||
}
|
||||
|
||||
async function uploadFontFile(file) {
|
||||
@@ -591,7 +737,7 @@ async function resolveCustomFace(role) {
|
||||
return url ? { family: url.split('/').pop().replace(/\.[^.]+$/, '') || 'Custom', source: url } : null;
|
||||
}
|
||||
|
||||
document.querySelector('[data-font-custom-save]').onclick = async () => {
|
||||
customSave.onclick = async () => {
|
||||
customStatus.textContent = 'Saving…';
|
||||
let heading;
|
||||
let body;
|
||||
@@ -601,24 +747,30 @@ document.querySelector('[data-font-custom-save]').onclick = async () => {
|
||||
customStatus.textContent = error.message;
|
||||
return;
|
||||
}
|
||||
if (!heading && !body) {
|
||||
customStatus.textContent = 'Add a URL or a file for at least one role.';
|
||||
// The button is already gated on both roles; this is the same rule stated
|
||||
// where the pair is built, so no path can assemble a half pair.
|
||||
if (!heading || !body) {
|
||||
customStatus.textContent = 'Add a URL or a file for both the heading and the body.';
|
||||
return;
|
||||
}
|
||||
const current = fontManifest.pairs.find(({ id }) => id === fontOptions.querySelector('input:checked')?.value);
|
||||
const pair = {
|
||||
id: 'custom',
|
||||
name: 'Custom',
|
||||
heading: heading || current.heading,
|
||||
body: body || current.body,
|
||||
heading,
|
||||
body,
|
||||
why: 'Your own faces',
|
||||
};
|
||||
fontManifest.pairs = [pair, ...fontManifest.pairs.filter(({ id }) => id !== 'custom')];
|
||||
addPairCard(pair, { prepend: true });
|
||||
// A second upload replaces the first rather than stacking a second row of
|
||||
// the same id, which the rail would then have to order against itself.
|
||||
for (const node of [...pairOrder]) {
|
||||
if (node.querySelector('input').value === 'custom') removePairCard(node);
|
||||
}
|
||||
addPairCard(pair, { checked: true, first: true });
|
||||
loadCustomFace(pair);
|
||||
fontOptions.querySelector('input[value="custom"]').checked = true;
|
||||
syncFontPair(pair);
|
||||
fontOptions.scrollTo({ top: 0 });
|
||||
// The dialog holds the pointer and focus, so the rail is free to reorder.
|
||||
applyHoist({ force: true });
|
||||
fontModal.close();
|
||||
};
|
||||
|
||||
@@ -884,6 +1036,12 @@ document.addEventListener('picker:screenchange', (event) => {
|
||||
// picker's own theme, not a third artboard painted in the user's palette.
|
||||
const target = { '03': strategyPreview, '04': typePreview }[event.detail.screen];
|
||||
if (target) syncCommittedPalette(target);
|
||||
// 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') {
|
||||
keyboardInRail = false;
|
||||
applyHoist({ force: true });
|
||||
}
|
||||
// A hidden sheet measures zero, so the fit can only be resolved on arrival.
|
||||
// The scroll waits a frame: the screen change focuses the first control after
|
||||
// this event, and that scrolls the list back to the top.
|
||||
|
||||
+103
-15
@@ -60,6 +60,13 @@ body.picker-page {
|
||||
.picker-page .ks-button.ks-button-primary {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
/* The paint is gated on :not([disabled]) so it never competes with the disabled
|
||||
state below. Without the gate this selector is (0,3,0) and the kit's
|
||||
`.ks-button[disabled]` is (0,2,0), so a disabled button kept the full gold
|
||||
fill and read as clickable. */
|
||||
.picker-page .ks-button.ks-button-primary:not([disabled]) {
|
||||
color: var(--ks-dark-ink);
|
||||
background: var(--ks-kinpaku);
|
||||
border-color: var(--ks-kinpaku);
|
||||
@@ -77,18 +84,18 @@ body.picker-page {
|
||||
transition: opacity 180ms var(--ks-ease);
|
||||
}
|
||||
|
||||
.picker-page .ks-button.ks-button-primary:hover {
|
||||
.picker-page .ks-button.ks-button-primary:not([disabled]):hover {
|
||||
color: var(--ks-dark-ink);
|
||||
background: var(--ks-kinpaku-vivid);
|
||||
border-color: var(--ks-kinpaku-vivid);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.picker-page .ks-button.ks-button-primary:hover::before {
|
||||
.picker-page .ks-button.ks-button-primary:not([disabled]):hover::before {
|
||||
opacity: 0.26;
|
||||
}
|
||||
|
||||
.picker-page .ks-button.ks-button-primary:active {
|
||||
.picker-page .ks-button.ks-button-primary:not([disabled]):active {
|
||||
background: var(--ks-kinpaku-rich);
|
||||
border-color: var(--ks-kinpaku-rich);
|
||||
transform: translateY(0);
|
||||
@@ -101,6 +108,22 @@ body.picker-page {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* A disabled button answering the pointer is the other half of the same bug:
|
||||
the kit's `.ks-button.ks-button-secondary:hover` is (0,3,0) and outranks its
|
||||
own `.ks-button[disabled]` at (0,2,0), so the tint and the lift still fired
|
||||
on a button that cannot be pressed. Restating the state here puts it at
|
||||
(0,3,0) at rest and (0,4,0) under the pointer, which clears every variant
|
||||
rule in the kit without an !important. */
|
||||
.picker-page .ks-button[disabled],
|
||||
.picker-page .ks-button[disabled]:hover,
|
||||
.picker-page .ks-button[disabled]:active {
|
||||
color: var(--ks-text-mute-deep);
|
||||
background: transparent;
|
||||
border-color: var(--ks-rule);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.picker-shell {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
@@ -2122,7 +2145,6 @@ body.picker-page {
|
||||
.ps-proof-item i,
|
||||
.ps-proof-item span,
|
||||
.ps-editorial-copy strong,
|
||||
.ps-editorial-copy span i,
|
||||
.ps-editorial-copy > em,
|
||||
.ps-gallery-item > i,
|
||||
.ps-gallery-item b,
|
||||
@@ -2130,7 +2152,8 @@ body.picker-page {
|
||||
.ps-footer-mark,
|
||||
.ps-phone-footer i,
|
||||
.pt-headline,
|
||||
.pt-body
|
||||
.pt-body,
|
||||
.pt-section-body
|
||||
) {
|
||||
transition:
|
||||
background-color 280ms var(--ks-ease),
|
||||
@@ -2217,6 +2240,34 @@ body.picker-page {
|
||||
scrollbar-color: color-mix(in oklab, var(--ks-patina-deep) 35%, transparent) transparent;
|
||||
}
|
||||
|
||||
/* The rail keeps the chosen pair in the first row, and it makes that move only
|
||||
once the pointer and focus have both left the list, so no row is ever pulled
|
||||
out from under a cursor and arrow keys can cross the group without it
|
||||
re-sorting between presses. Waiting costs the user the sight of the travel,
|
||||
so the row that has taken the top says so once: its wash and its spine come
|
||||
up bright and settle to the resting checked state. Both keyframes animate
|
||||
color and nothing else, so the acknowledgement cannot move the list it is
|
||||
drawing the eye to. */
|
||||
@keyframes picker-pair-hoisted {
|
||||
from {
|
||||
background-color: color-mix(in oklab, var(--ks-patina) 32%, var(--ks-lacquer-raised));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes picker-pair-hoisted-spine {
|
||||
from {
|
||||
background-color: var(--ks-patina-pale);
|
||||
}
|
||||
}
|
||||
|
||||
.picker-type-option[data-hoisted] {
|
||||
animation: picker-pair-hoisted 640ms var(--ks-ease);
|
||||
}
|
||||
|
||||
.picker-type-option[data-hoisted]::before {
|
||||
animation: picker-pair-hoisted-spine 640ms var(--ks-ease);
|
||||
}
|
||||
|
||||
.picker-type-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2670,6 +2721,20 @@ body.picker-page {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.picker-modal-hint:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* What the sheet is still waiting for, in the voice of the field labels above
|
||||
it rather than an error. Nothing is wrong yet: the form is unfinished, so
|
||||
this is muted caption ink and carries no vermilion and no icon. It sits in
|
||||
the footer because that is where the button it explains is. */
|
||||
.picker-modal-hint {
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* Cancel then commit, both hard against the right padding edge. The dialog is a
|
||||
fixed 520px behind a 1200px width gate and the pair needs 247px of it, so
|
||||
wrapping can only ever fire on a stray long label; nowrap keeps the two
|
||||
@@ -2994,6 +3059,25 @@ body.picker-page {
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
/* The section paragraph is one element, so its leading is the face's own and
|
||||
runs unbroken from the first line to the last. It was two stacked bars in
|
||||
the wireframe, and two elements set two lines with the container's gap
|
||||
between them, which read as a pair of unrelated lines rather than a
|
||||
paragraph. Micro size over a near-full-column measure wants more leading
|
||||
than the single-line labels around it, hence the step past their 1.4.
|
||||
|
||||
No measure cap: the artboard is a fixed-proportion frame with a ceiling of
|
||||
its own, so the copy column stops growing before the line does. Measured
|
||||
67 to 75 characters on the desktop board and 38 to 51 on the handset,
|
||||
across every width from 1280 up. */
|
||||
.pt-section-body {
|
||||
margin: 0;
|
||||
color: var(--pvs-copy);
|
||||
font-family: var(--pt-body);
|
||||
font-size: var(--pt-micro);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Phone artboard type: the same ladder re-based to handset scale, held above
|
||||
the legibility floor rather than scaled straight down. */
|
||||
.picker-preview-type .ps-phone {
|
||||
@@ -3013,11 +3097,16 @@ body.picker-page {
|
||||
|
||||
Overflow stays visible: a bar had to be kept inside its box, but a line of
|
||||
text hangs its descenders below the baseline, and the trimmed box ends at
|
||||
the baseline. Hiding the overflow would cut the tail off every g and p. */
|
||||
the baseline. Hiding the overflow would cut the tail off every g and p.
|
||||
|
||||
The nowrap below is the default for a one-line label, and the proof and
|
||||
card labels opt back out of it further down. Every selector in this list
|
||||
is one class and one type, so those two rules win on source order. Adding
|
||||
a more specific one here (a descendant pair, say) silently raises the whole
|
||||
:is() above them and puts the clipping back. */
|
||||
.picker-preview-type :is(
|
||||
.ps-nav-bars i,
|
||||
.ps-proof-item span,
|
||||
.ps-editorial-copy span i,
|
||||
.ps-editorial-copy > em,
|
||||
.ps-gallery-item b,
|
||||
.ps-footer-links i,
|
||||
@@ -3214,12 +3303,6 @@ body.picker-page {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.picker-preview-type .ps-editorial-copy span i {
|
||||
overflow: visible;
|
||||
color: var(--pvs-copy);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.picker-preview-type .ps-editorial-copy > em {
|
||||
width: auto;
|
||||
height: auto;
|
||||
@@ -3331,7 +3414,6 @@ body.picker-page {
|
||||
.ps-proof-item i,
|
||||
.ps-proof-item span,
|
||||
.ps-editorial-copy strong,
|
||||
.ps-editorial-copy span i,
|
||||
.ps-editorial-copy > em,
|
||||
.ps-gallery-item > i,
|
||||
.ps-gallery-item b,
|
||||
@@ -3339,7 +3421,8 @@ body.picker-page {
|
||||
.ps-footer-mark,
|
||||
.ps-phone-footer i,
|
||||
.pt-headline,
|
||||
.pt-body
|
||||
.pt-body,
|
||||
.pt-section-body
|
||||
) {
|
||||
transition: none;
|
||||
}
|
||||
@@ -3347,6 +3430,11 @@ body.picker-page {
|
||||
.picker-card[data-exit] {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.picker-type-option[data-hoisted],
|
||||
.picker-type-option[data-hoisted]::before {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-width-gate {
|
||||
|
||||
Reference in New Issue
Block a user