export const APP_VERSION="2.3.0"; export const DEFAULTS=Object.freeze({theme:"dark",accent:"green",intro:true,motion:true,compact:false,grid:false,clock:true,newTab:true,remember:false,focusMinutes:25,breakMinutes:5,accentHex:"",iconSet:"1",backgroundHex:"",panelHex:"",textHex:"",borderHex:"",scrollbarHex:"",radius:6,textScale:100,density:"normal",scrollbarSize:8,customCss:""}); export function settingsFrom(raw={}) { const output={...DEFAULTS}; if (!raw || typeof raw!=="object") return output; if (["dark","light","system"].includes(raw.theme)) output.theme=raw.theme; if (["green","purple","blue","orange"].includes(raw.accent)) output.accent=raw.accent; for (const key of ["intro","motion","compact","grid","clock","newTab","remember"]) if(typeof raw[key]==="boolean")output[key]=raw[key]; for(const [key,max] of [["focusMinutes",180],["breakMinutes",60]])if(Number.isInteger(raw[key])&&raw[key]>=1&&raw[key]<=max)output[key]=raw[key]; if(typeof raw.accentHex==="string"&&/^#[0-9a-f]{6}$/i.test(raw.accentHex))output.accentHex=raw.accentHex; if(["1","2","outline"].includes(raw.iconSet))output.iconSet=raw.iconSet; for(const key of ["backgroundHex","panelHex","textHex","borderHex","scrollbarHex"])if(typeof raw[key]==="string"&&/^#[0-9a-f]{6}$/i.test(raw[key]))output[key]=raw[key]; for(const [key,min,max] of [["radius",0,20],["textScale",85,125],["scrollbarSize",4,16]])if(Number.isInteger(raw[key])&&raw[key]>=min&&raw[key]<=max)output[key]=raw[key]; if(["compact","normal","roomy"].includes(raw.density))output.density=raw.density; if(typeof raw.customCss==="string"&&raw.customCss.length<=12000&&!/url\s*\(|@import|@font-face|image-set\s*\(/i.test(raw.customCss))output.customCss=raw.customCss; return output; } export function validShortcut(value) { if(!value||typeof value!=="object"||typeof value.name!=="string"||!value.name.trim()||value.name.length>48)throw new Error("Give your shortcut a name (up to 48 characters)."); if(typeof value.url!=="string"||value.url.length>2048)throw new Error("Enter a valid website address."); let url;try{url=new URL(value.url);}catch{throw new Error("Use a full website address, starting with https://.");} if(url.protocol!=="https:"||url.username||url.password)throw new Error("Use an HTTPS address without embedded login details."); if(!["apps","games","utilities"].includes(value.category))throw new Error("Choose a valid category."); return {id:typeof value.id==="string"&&/^custom-[a-zA-Z0-9-]{1,80}$/.test(value.id)?value.id:"custom-"+crypto.randomUUID(),name:value.name.trim(),url:url.href,category:value.category,description:typeof value.description==="string"?value.description.slice(0,100):"",kind:"external",icon:"link",color:"#94c7b3",bg:"#1f342d",custom:true}; } // A small arithmetic parser: no eval(), Function(), or executable expressions. export function calculate(source) { if(typeof source!=="string"||source.length>300)throw new Error("That expression is too long."); const text=source.replaceAll("×","*").replaceAll("÷","/").replace(/\s/g,""); if(!text||/[^0-9.+\-*/%()]/.test(text))throw new Error("Use numbers, parentheses, and + − × ÷ %."); let pos=0; function atom(){ if(text[pos]==="+"){pos++;return atom();} if(text[pos]==="-"){pos++;return -atom();} if(text[pos]==="("){pos++;const value=sum();if(text[pos++]!==")")throw new Error("Check your parentheses.");return value;} const match=/^(?:\d+(?:\.\d*)?|\.\d+)/.exec(text.slice(pos));if(!match)throw new Error("Check your expression."); pos+=match[0].length;return Number(match[0]); } function product(){let value=atom();while(["*","/","%"].includes(text[pos])){const op=text[pos++],right=atom();if((op==="/"||op==="%")&&right===0)throw new Error("Cannot divide by zero.");value=op==="*"?value*right:op==="/"?value/right:value%right;}return value;} function sum(){let value=product();while(text[pos]==="+"||text[pos]==="-"){const op=text[pos++],right=product();value=op==="+"?value+right:value-right;}return value;} const result=sum();if(pos!==text.length||!Number.isFinite(result))throw new Error("Check your expression."); return Number(result.toPrecision(12)).toString(); } export function relayTarget(value){ let url;try{url=new URL(value);}catch{throw new Error("Enter a full HTTPS address.");} if(url.protocol!=="https:"||url.username||url.password||url.port)throw new Error("Use HTTPS without login details or a custom port."); return url.href; } export function htmlGame(value){ if(!value||typeof value.html!=="string"||value.html.length>1024*1024||typeof value.name!=="string")throw new Error("Choose a single HTML game under 1 MB."); return {id:typeof value.id==="string"&&/^html-[a-zA-Z0-9-]+$/.test(value.id)?value.id:"html-"+crypto.randomUUID(),name:value.name.slice(0,48)||"HTML game",html:value.html,kind:"html",category:"games",description:"Imported HTML game.",icon:"game",color:"#fff",bg:"#354b38",custom:true}; } // Choose the higher contrast ink for any valid custom button color. export function accentInk(hex){ if(!/^#[0-9a-f]{6}$/i.test(hex))return "#101010"; const rgb=hex.slice(1).match(/../g).map(value=>parseInt(value,16)/255).map(value=>value<=.04045?value/12.92:((value+.055)/1.055)**2.4); const luminance=.2126*rgb[0]+.7152*rgb[1]+.0722*rgb[2]; return (luminance+.05)/.05>=1.05/(luminance+.05)?"#080808":"#ffffff"; } export function viewItems(items,{section="all",filter="all",query="",sort="default",favorites=[],recent={}}={}){ const saved=new Set(favorites),term=query.trim().toLowerCase(); const result=items.filter(item=>(section==="all"||section==="favorites"&&saved.has(item.id)||item.category===section)&&(filter==="all"||item.category===filter)&&(!term||(item.name+" "+item.description+" "+item.category).toLowerCase().includes(term))); if(sort==="name")result.sort((a,b)=>a.name.localeCompare(b.name)); if(sort==="recent")result.sort((a,b)=>(recent[b.id]||0)-(recent[a.id]||0)); return result; }