mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-19 09:36:59 +03:00
update
This commit is contained in:
@@ -194,7 +194,6 @@ const roles = [
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<button class="picker-back" type="button" data-advance="prev">Back</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
@@ -267,8 +266,11 @@ const roles = [
|
||||
</div>
|
||||
<div class="ps-proof">
|
||||
<div class="ps-proof-item"><i></i><span></span></div>
|
||||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||||
<div class="ps-proof-item"><i></i><span></span></div>
|
||||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||||
<div class="ps-proof-item"><i></i><span></span></div>
|
||||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||||
<div class="ps-proof-item"><i></i><span></span></div>
|
||||
</div>
|
||||
<div class="ps-editorial">
|
||||
@@ -301,6 +303,7 @@ const roles = [
|
||||
<div class="ps-actions"><i></i><i></i></div>
|
||||
<div class="ps-proof">
|
||||
<div class="ps-proof-item"><i></i><span></span></div>
|
||||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||||
<div class="ps-proof-item"><i></i><span></span></div>
|
||||
</div>
|
||||
<div class="ps-editorial-copy">
|
||||
@@ -377,8 +380,11 @@ const roles = [
|
||||
</div>
|
||||
<div class="ps-proof">
|
||||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||||
</div>
|
||||
<div class="ps-editorial">
|
||||
@@ -411,6 +417,7 @@ const roles = [
|
||||
<div class="ps-actions"><i data-type-cta-primary></i><i data-type-cta-secondary></i></div>
|
||||
<div class="ps-proof">
|
||||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||||
<i class="ps-proof-divider" aria-hidden="true"></i>
|
||||
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
|
||||
</div>
|
||||
<div class="ps-editorial-copy">
|
||||
@@ -509,11 +516,20 @@ const roles = [
|
||||
data-custom-url={role}
|
||||
/>
|
||||
<p class="picker-modal-or">or</p>
|
||||
<label class="picker-modal-file">
|
||||
<input type="file" accept=".woff2,.woff,.ttf,.otf" data-custom-file={role} />
|
||||
<span class="picker-modal-file-button">Choose a file</span>
|
||||
<span class="picker-modal-file-name" data-custom-file-name={role}>No file chosen</span>
|
||||
</label>
|
||||
<div class="picker-modal-file-block">
|
||||
<div class="picker-modal-file">
|
||||
<input
|
||||
class="picker-modal-file-input"
|
||||
id={`custom-${role}-file`}
|
||||
type="file"
|
||||
accept=".woff2,.woff,.ttf,.otf,font/woff2,font/woff,font/ttf,font/otf,application/font-woff,application/font-woff2,application/x-font-ttf,application/x-font-otf"
|
||||
multiple
|
||||
data-custom-file={role}
|
||||
/>
|
||||
<label class="picker-modal-file-button" for={`custom-${role}-file`}>Choose font files</label>
|
||||
</div>
|
||||
<ul class="picker-modal-file-list" data-custom-file-list={role} hidden></ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
))}
|
||||
|
||||
|
||||
@@ -387,9 +387,77 @@ 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 FONT_SOURCE_SEP = '\n';
|
||||
const FONT_FILE_EXTENSIONS = new Set(['.woff2', '.woff', '.ttf', '.otf']);
|
||||
const customFilesByRole = { heading: [], body: [] };
|
||||
|
||||
function fontFileExtension(name) {
|
||||
const index = name.lastIndexOf('.');
|
||||
return index === -1 ? '' : name.slice(index).toLowerCase();
|
||||
}
|
||||
|
||||
function isFontFile(file) {
|
||||
return FONT_FILE_EXTENSIONS.has(fontFileExtension(file.name));
|
||||
}
|
||||
|
||||
function setChosenFontFiles(input, files) {
|
||||
const transfer = new DataTransfer();
|
||||
for (const file of files) transfer.items.add(file);
|
||||
input.files = transfer.files;
|
||||
}
|
||||
|
||||
function sameFontFile(a, b) {
|
||||
return a.name === b.name && a.size === b.size && a.lastModified === b.lastModified;
|
||||
}
|
||||
|
||||
function mergeFontFiles(existing, incoming) {
|
||||
const merged = [...existing];
|
||||
for (const file of incoming) {
|
||||
if (!merged.some((entry) => sameFontFile(entry, file))) merged.push(file);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function renderCustomFileList(role) {
|
||||
const list = fontModal.querySelector(`[data-custom-file-list="${role}"]`);
|
||||
const files = customFilesByRole[role];
|
||||
list.replaceChildren();
|
||||
list.hidden = files.length === 0;
|
||||
for (const file of files) {
|
||||
const item = document.createElement('li');
|
||||
item.className = 'picker-modal-file-item';
|
||||
const name = document.createElement('span');
|
||||
name.textContent = file.name;
|
||||
const remove = document.createElement('button');
|
||||
remove.type = 'button';
|
||||
remove.className = 'picker-modal-file-remove';
|
||||
remove.setAttribute('aria-label', `Remove ${file.name}`);
|
||||
remove.textContent = '×';
|
||||
remove.onclick = () => {
|
||||
customFilesByRole[role] = customFilesByRole[role].filter((entry) => !sameFontFile(entry, file));
|
||||
setChosenFontFiles(customFile(role), customFilesByRole[role]);
|
||||
renderCustomFileList(role);
|
||||
customStatus.textContent = '';
|
||||
};
|
||||
item.append(name, remove);
|
||||
list.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
function parseFontSources(source) {
|
||||
if (!source) return [];
|
||||
if (source.includes(FONT_SOURCE_SEP)) return source.split(FONT_SOURCE_SEP).filter(Boolean);
|
||||
return [source];
|
||||
}
|
||||
|
||||
document.querySelector('[data-font-custom-open]').onclick = () => {
|
||||
customStatus.textContent = '';
|
||||
for (const role of ['heading', 'body']) {
|
||||
customFilesByRole[role] = [];
|
||||
customFile(role).value = '';
|
||||
setChosenFontFiles(customFile(role), []);
|
||||
renderCustomFileList(role);
|
||||
}
|
||||
fontModal.showModal();
|
||||
};
|
||||
|
||||
@@ -397,22 +465,38 @@ document.querySelector('[data-font-custom-close]').onclick = () => fontModal.clo
|
||||
|
||||
for (const role of ['heading', 'body']) {
|
||||
customFile(role).onchange = ({ target }) => {
|
||||
const label = fontModal.querySelector(`[data-custom-file-name="${role}"]`);
|
||||
label.textContent = target.files[0]?.name || 'No file chosen';
|
||||
const incoming = [...target.files].filter(isFontFile);
|
||||
if (incoming.length !== target.files.length) {
|
||||
customStatus.textContent = 'Only .woff2, .woff, .ttf, and .otf files are accepted.';
|
||||
} else {
|
||||
customStatus.textContent = '';
|
||||
}
|
||||
customFilesByRole[role] = mergeFontFiles(customFilesByRole[role], incoming);
|
||||
setChosenFontFiles(target, customFilesByRole[role]);
|
||||
target.value = '';
|
||||
renderCustomFileList(role);
|
||||
};
|
||||
}
|
||||
|
||||
async function uploadFontFile(file) {
|
||||
const response = await fetch('/font-upload', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Font-Filename': file.name, 'Content-Type': 'application/octet-stream' },
|
||||
body: file,
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.error || 'Upload failed');
|
||||
return result.path;
|
||||
}
|
||||
|
||||
async function resolveCustomFace(role) {
|
||||
const file = customFile(role).files[0];
|
||||
if (file) {
|
||||
const response = await fetch('/font-upload', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Font-Filename': file.name, 'Content-Type': 'application/octet-stream' },
|
||||
body: file,
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.error || 'Upload failed');
|
||||
return { family: file.name.replace(/\.[^.]+$/, ''), source: result.path };
|
||||
const files = customFilesByRole[role].filter(isFontFile);
|
||||
if (files.length) {
|
||||
const paths = await Promise.all(files.map((file) => uploadFontFile(file)));
|
||||
return {
|
||||
family: files[0].name.replace(/\.[^.]+$/, ''),
|
||||
source: paths.join(FONT_SOURCE_SEP),
|
||||
};
|
||||
}
|
||||
const url = customUrl(role).value.trim();
|
||||
return url ? { family: url.split('/').pop().replace(/\.[^.]+$/, '') || 'Custom', source: url } : null;
|
||||
@@ -454,15 +538,17 @@ document.querySelector('[data-font-custom-save]').onclick = async () => {
|
||||
function loadCustomFace({ heading, body }) {
|
||||
for (const face of [heading, body]) {
|
||||
if (!face?.source) continue;
|
||||
if (/\.(css)(\?|$)/i.test(face.source) || !/\.(woff2?|ttf|otf)(\?|$)/i.test(face.source)) {
|
||||
if (/\.(css)(\?|$)/i.test(face.source) || (!face.source.includes(FONT_SOURCE_SEP) && !/\.(woff2?|ttf|otf)(\?|$)/i.test(face.source))) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = face.source;
|
||||
document.head.append(link);
|
||||
continue;
|
||||
}
|
||||
const src = face.source.startsWith('http') ? face.source : `/fonts/${face.source.split('/').pop()}`;
|
||||
document.fonts.add(new FontFace(face.family, `url(${src})`));
|
||||
for (const entry of parseFontSources(face.source)) {
|
||||
const src = entry.startsWith('http') ? entry : `/fonts/${entry.split('/').pop()}`;
|
||||
document.fonts.add(new FontFace(face.family, `url(${src})`));
|
||||
}
|
||||
document.fonts.load(`16px "${face.family}"`);
|
||||
}
|
||||
}
|
||||
|
||||
+132
-10
@@ -146,6 +146,10 @@ body.picker-page {
|
||||
padding-inline: 56px;
|
||||
}
|
||||
|
||||
.picker-screen[data-screen="01"] .picker-container {
|
||||
padding-inline: 75px;
|
||||
}
|
||||
|
||||
.picker-start {
|
||||
max-width: 620px;
|
||||
display: grid;
|
||||
@@ -1670,15 +1674,37 @@ body.picker-page {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.picker-strategy-preview:not(.picker-preview-type) .ps-proof-item:nth-child(2) i {
|
||||
.picker-strategy-preview:not(.picker-preview-type) .ps-proof {
|
||||
grid-template-columns:
|
||||
minmax(0, 1fr) auto
|
||||
minmax(0, 1fr) auto
|
||||
minmax(0, 1fr) auto
|
||||
minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.picker-strategy-preview:not(.picker-preview-type) .ps-proof-item + .ps-proof-item {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.picker-strategy-preview:not(.picker-preview-type) .ps-proof-divider {
|
||||
width: 1px;
|
||||
align-self: stretch;
|
||||
background-color: var(--pvs-rule);
|
||||
}
|
||||
|
||||
.picker-strategy-preview:not(.picker-preview-type) .ps-phone-body .ps-proof {
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.picker-strategy-preview:not(.picker-preview-type) .ps-proof-item:nth-of-type(2) i {
|
||||
background-color: var(--pvs-accent-b);
|
||||
}
|
||||
|
||||
.picker-strategy-preview:not(.picker-preview-type) .ps-proof-item:nth-child(3) i {
|
||||
.picker-strategy-preview:not(.picker-preview-type) .ps-proof-item:nth-of-type(3) i {
|
||||
background-color: var(--pvs-accent-c);
|
||||
}
|
||||
|
||||
.picker-strategy-preview:not(.picker-preview-type) .ps-proof-item:nth-child(4) i {
|
||||
.picker-strategy-preview:not(.picker-preview-type) .ps-proof-item:nth-of-type(4) i {
|
||||
background-color: var(--pvs-accent-d);
|
||||
}
|
||||
|
||||
@@ -2106,7 +2132,9 @@ body.picker-page {
|
||||
|
||||
/* The rail keeps the row height owned by the preview: the fieldset scrolls
|
||||
inside it instead of stretching the page when six tall cards stack up. The
|
||||
control row underneath is what tells you the list runs past its frame. */
|
||||
control row underneath is what tells you the list runs past its frame. On
|
||||
wide screens the rail stops growing at 570px so a tall preview does not
|
||||
turn six font cards into a page-length column. */
|
||||
.picker-type-rail {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
@@ -2115,6 +2143,12 @@ body.picker-page {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.picker-type-rail {
|
||||
max-height: 570px;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-type-options {
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
@@ -2205,7 +2239,7 @@ body.picker-page {
|
||||
|
||||
.picker-modal-head h2 {
|
||||
color: var(--ks-champagne);
|
||||
font-family: var(--ks-font-display);
|
||||
font-family: var(--ks-font);
|
||||
font-size: 1.7rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.1;
|
||||
@@ -2268,6 +2302,11 @@ body.picker-page {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.picker-modal-file-block {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.picker-modal-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2281,10 +2320,13 @@ body.picker-page {
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.picker-modal-file-button {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 14px;
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 2px;
|
||||
@@ -2304,15 +2346,59 @@ body.picker-page {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.picker-modal-file-name {
|
||||
.picker-modal-file-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.picker-modal-file-item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
background: var(--ks-lacquer-deep);
|
||||
border: 1px solid var(--ks-rule);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.picker-modal-file-item span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--ks-text-muted);
|
||||
color: var(--ks-text);
|
||||
font-size: 0.8rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.picker-modal-file-remove {
|
||||
flex: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: color 180ms var(--ks-ease), background-color 180ms var(--ks-ease);
|
||||
}
|
||||
|
||||
.picker-modal-file-remove:hover {
|
||||
color: var(--ks-champagne);
|
||||
background: color-mix(in oklab, var(--ks-patina) 10%, transparent);
|
||||
}
|
||||
|
||||
.picker-modal-file-remove:focus-visible {
|
||||
outline: 1px solid var(--ks-kinpaku);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.picker-modal-status:empty {
|
||||
display: none;
|
||||
}
|
||||
@@ -2323,9 +2409,11 @@ body.picker-page {
|
||||
}
|
||||
|
||||
.picker-modal-actions {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
/* Screen 04 mirrors screen 03's options panel. */
|
||||
@@ -2570,6 +2658,40 @@ body.picker-page {
|
||||
color: var(--pvs-title);
|
||||
}
|
||||
|
||||
.picker-preview-type .ps-proof {
|
||||
grid-template-columns:
|
||||
minmax(0, 1fr) auto
|
||||
minmax(0, 1fr) auto
|
||||
minmax(0, 1fr) auto
|
||||
minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.picker-preview-type .ps-proof-item + .ps-proof-item {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.picker-preview-type .ps-proof-divider {
|
||||
width: 1px;
|
||||
align-self: stretch;
|
||||
background-color: var(--pvs-rule);
|
||||
}
|
||||
|
||||
.picker-preview-type .ps-phone-body .ps-proof {
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.picker-preview-type .ps-proof-item:nth-of-type(2) i {
|
||||
background-color: var(--pvs-accent-b);
|
||||
}
|
||||
|
||||
.picker-preview-type .ps-proof-item:nth-of-type(3) i {
|
||||
background-color: var(--pvs-accent-c);
|
||||
}
|
||||
|
||||
.picker-preview-type .ps-proof-item:nth-of-type(4) i {
|
||||
background-color: var(--pvs-accent-d);
|
||||
}
|
||||
|
||||
.picker-preview-type .ps-proof-item span {
|
||||
color: var(--pvs-bars);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user