This commit is contained in:
Abdul Wahab
2026-09-01 10:02:39 +05:00
parent 5602cdfc7e
commit f3c26f1f42
3 changed files with 255 additions and 31 deletions
+22 -6
View File
@@ -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>
))}
+101 -15
View File
@@ -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
View File
@@ -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);
}