Files

103 lines
9.3 KiB
JavaScript

(() => {
'use strict';
const B = window.Brand;
const $ = id => document.getElementById(id);
const modes = ['light','dim','dark'];
const modeName = mode => mode[0].toUpperCase() + mode.slice(1);
const defaults = {palette:'harbor',font:'dm',mark:'mosaic',mode:matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'};
let state = {...defaults};
let storageAvailable = true;
try {
const saved = JSON.parse(localStorage.getItem('mosaic-brand-preview-v1') || '{}');
for (const [key, items] of Object.entries({palette:B.palettes.map(p=>p.id),font:B.fonts.map(f=>f.id),mark:B.logos.map(l=>l.id),mode:modes})) {
if (items.includes(saved?.[key])) state[key]=saved[key];
}
$('review-notes').value = localStorage.getItem('mosaic-brand-notes-v1') || '';
} catch { storageAvailable = false; }
function announce(text) {$('announcement').textContent=text;}
function persist() {
try {localStorage.setItem('mosaic-brand-preview-v1',JSON.stringify(state));}
catch {storageAvailable=false;announce('Browser storage is unavailable. Download your review notes before closing.');}
}
for(const [id, data] of [['palette',B.palettes],['font',B.fonts],['mark',B.logos]]) {
$(id).innerHTML=data.map(x=>`<option value="${x.id}">${x.name}</option>`).join('');
$(id).addEventListener('change',event=>{state[id]=event.target.value;update();});
}
document.querySelectorAll('[name=mode]').forEach(input=>input.addEventListener('change',()=>{state.mode=input.value;update();}));
$('logo-grid').innerHTML=B.logos.map((l,i)=>`<article class="study-card" data-mark-card="${l.id}">
<div class="card-kicker"><span>${String(i+1).padStart(2,'0')} / ${l.family}</span><span class="selection-label" data-mark-label="${l.id}"></span></div>
<div class="logo-display">${B.logo(l.id)}</div><h3>${l.name}</h3><p>${l.note}</p>
<div class="size-strip" aria-label="${l.name} monochrome size samples">${[16,24,32].map(size=>`<span>${B.logo(l.id,size)}${size} px</span>`).join('')}<span>${B.logo(l.id,24).replace('class="logo"','class="logo reverse"')}Reverse</span></div>
<p class="tradeoff">${l.tradeoff}</p><button type="button" class="choose" data-choose-mark="${l.id}" aria-pressed="false">Use ${l.name}</button></article>`).join('');
$('font-grid').innerHTML=B.fonts.map(f=>`<article class="study-card" data-font-card="${f.id}"><div class="card-kicker"><span>${f.detail}</span><span class="selection-label" data-font-label="${f.id}"></span></div>
<div style='font-family:${f.family}'><h3 class="font-title">${f.name}</h3><div class="type-specimen"><p class="type-heading">A place for your next idea.</p><p>Plan a project, write a chapter, or ask an agent to help. Start with what you need.</p><p class="numerals">0123456789<br>Il1 O0 · 12:48 · 1,024</p></div></div><p>${f.note}</p><button type="button" class="choose" data-choose-font="${f.id}" aria-pressed="false">Use ${f.name}</button></article>`).join('');
$('palette-grid').innerHTML=B.palettes.map(p=>{
const t=B.tokens(p,'light');
return `<article class="palette-card" data-palette-card="${p.id}"><h3>${p.name}<span data-palette-label="${p.id}"></span></h3><div class="palette-swatches" aria-hidden="true">${['action','accent','raised','canvas'].map(role=>`<i style="background:${t[role]}"></i>`).join('')}</div><p class="theory">${p.theory} · ${p.hue}° / ${p.accent}°</p><p>${p.note}</p><p class="palette-use">${p.use}</p><div class="palette-modes" aria-label="${p.name} appearance">${modes.map(mode=>{const t=B.tokens(p,mode);return `<button type="button" data-palette="${p.id}" data-mode="${mode}" aria-label="Preview ${p.name} in ${modeName(mode)}" aria-pressed="false" style="--mini-surface:${t.surface};--mini-text:${t.text};--mini-border:${t.border};--mini-raised:${t.raised}">${modeName(mode)}</button>`;}).join('')}</div></article>`;
}).join('');
$('icon-grid').innerHTML=[['home','Home'],['conversation','Conversations'],['projects','Projects'],['agents','Agents'],['people','People'],['board','Board'],['extensions','Extensions'],['search','Search']].map(([id,label])=>`<div class="icon-specimen">${B.icon(id)}<span>${label}</span></div>`).join('');
document.querySelectorAll('[data-icon]').forEach(el=>el.innerHTML=B.icon(el.dataset.icon));
document.querySelectorAll('[data-choose-mark]').forEach(button=>button.addEventListener('click',()=>{state.mark=button.dataset.chooseMark;update();}));
document.querySelectorAll('[data-choose-font]').forEach(button=>button.addEventListener('click',()=>{state.font=button.dataset.chooseFont;update();}));
document.querySelectorAll('[data-palette][data-mode]').forEach(button=>button.addEventListener('click',()=>{state.palette=button.dataset.palette;state.mode=button.dataset.mode;update();}));
function update(shouldAnnounce=true) {
const p=B.palettes.find(p=>p.id===state.palette), f=B.fonts.find(f=>f.id===state.font), l=B.logos.find(l=>l.id===state.mark);
const t=B.tokens(p,state.mode);
for(const [role,value] of Object.entries(t)) document.documentElement.style.setProperty('--'+role,value);
document.documentElement.style.setProperty('--font',f.family);
document.documentElement.style.colorScheme=state.mode==='light'?'light':'dark';
document.documentElement.dataset.mode=state.mode;
document.documentElement.dataset.palette=state.palette;
for(const id of ['palette','font','mark']) $(id).value=state[id];
document.querySelectorAll('[name=mode]').forEach(input=>input.checked=input.value===state.mode);
document.querySelectorAll('[data-logo]').forEach(el=>el.innerHTML=B.logo(state.mark));
$('current-palette').textContent=p.name;
$('current-mode').textContent=modeName(state.mode);
for(const [kind,data] of [['mark',B.logos],['font',B.fonts],['palette',B.palettes]]) {
data.forEach(item=>{
const chosen=item.id===state[kind];
document.querySelector(`[data-${kind}-card="${item.id}"]`).classList.toggle('is-selected',chosen);
document.querySelector(`[data-${kind}-label="${item.id}"]`).textContent=chosen?'Selected':'';
document.querySelector(`[data-choose-${kind}="${item.id}"]`)?.setAttribute('aria-pressed',String(chosen));
});
}
document.querySelectorAll('[data-palette][data-mode]').forEach(button=>button.setAttribute('aria-pressed',String(button.dataset.palette===state.palette && button.dataset.mode===state.mode)));
$('token-title').textContent=`${p.name} / ${modeName(state.mode)} tokens`;
$('tokens').innerHTML=Object.entries(t).map(([role,value])=>`<div><dt><i style="background:${value}" aria-hidden="true"></i>${role.replace('onAction','On action')}</dt><dd>${value.toUpperCase()}</dd></div>`).join('');
const summary=`${l.name} · ${f.name} · ${p.name} · ${modeName(state.mode)}`;
$('selection-summary').textContent=`Current preview: ${summary}.`;
persist();
if(shouldAnnounce) announce(`Preview changed to ${summary}.`);
}
const messages={ready:['conversation','Ready. Try a local success preview.'],loading:['dim','Loading specimen. No request is running. Choose another state to continue.'],empty:['plus','No projects in this example. Your next step would be to create one.'],error:['error','Could not save this example. Your text is unchanged. Use Preview success to try again.'],success:['check','Success preview. No project was created or saved to a server.']};
function sample() {
const state=$('sample-state').value, [icon,message]=messages[state];
$('sample-feedback').dataset.state=state;
$('sample-feedback').innerHTML=B.icon(icon)+`<span>${message}</span>`;
$('sample-action').disabled=state==='loading';
$('sample-title').setAttribute('aria-describedby','sample-help sample-feedback');
}
$('sample-state').addEventListener('change',sample);
$('sample-action').addEventListener('click',()=>{$('sample-state').value='success';sample();});
$('reset').addEventListener('click',()=>{state={...defaults};update();announce('Preview reset. Your review notes and example project name are unchanged.');});
function download(name,type,content) {
const url=URL.createObjectURL(new Blob([content],{type}));
const a=document.createElement('a');a.href=url;a.download=name;a.click();
// Browser downloads consume the URL before the next task; retain briefly for portability.
setTimeout(()=>URL.revokeObjectURL(url),1000);
announce(`${name} prepared for download. Check your browser downloads.`);
}
$('export').addEventListener('click',()=>{
const p=B.palettes.find(p=>p.id===state.palette);
download(`mosaic-${p.id}-tokens.json`,'application/json',JSON.stringify({prototype:true,palette:p.name,theory:p.theory,rationale:p.note,modes:Object.fromEntries(modes.map(mode=>[mode,B.tokens(p,mode)]))},null,2));
});
$('review-notes').addEventListener('input',()=>{
try{localStorage.setItem('mosaic-brand-notes-v1',$('review-notes').value);}catch{storageAvailable=false;announce('Notes cannot be stored in this browser. Download them before closing.');}
});
$('download-review').addEventListener('click',()=>{
download('mosaic-brand-review.txt','text/plain',`Mosaic Stack branding review\n${$('selection-summary').textContent}\n\n${$('review-notes').value||'No written notes yet.'}\n\nThese selections are review preferences, not an automatic approval.\n`);
});
update(false);sample();
if(!storageAvailable) announce('Browser storage is unavailable. The preview still works. Download notes to keep them.');
})();