Santara Website Packager

Click “Build ZIP” to download santara-website.zip with all files & images.
Ready.

What’s inside

Preview index.html (trimmed)

    
`, "styles.css": `:root{ --bg:#0e0f14; --panel:#151823; --soft:#1b2030; --text:#e7e9f1; --muted:#9aa3b2; --accent:#76c7ff; --accent-2:#ffd36e; --good:#6ee7a8; --danger:#f27373; } *{box-sizing:border-box} html,body{height:100%} body{ margin:0; background: radial-gradient(1200px 800px at 70% -10%, #1a1f2e 0%, var(--bg) 60%), radial-gradient(800px 500px at -10% 110%, #121522 0%, var(--bg) 60%); color:var(--text); font: 16px/1.6 system-ui,-apple-system,Segoe UI,Roboto,Arial; } .header{position:sticky;top:0;z-index:5;backdrop-filter:blur(8px);background:linear-gradient(180deg, rgba(14,15,20,.9), rgba(14,15,20,.5));border-bottom:1px solid #222633} .wrap{max-width:1100px;margin:0 auto;padding:16px} .brand{display:flex;align-items:center;gap:14px} .logo{width:40px;height:40px;border-radius:50%; background: radial-gradient(12px 12px at 30% 30%, #fff 0%, #ffd36e 40%, transparent 41%), radial-gradient(30px 30px at 65% 70%, rgba(118,199,255,.7) 0%, transparent 60%); box-shadow:0 0 24px rgba(118,199,255,.35), inset 0 0 10px rgba(255,211,110,.5); } .nav{display:flex; gap:8px; flex-wrap:wrap} .nav button{background:var(--soft); color:var(--text); border:1px solid #2a2f3f; border-radius:10px; padding:8px 12px; cursor:pointer; font-weight:600} .nav button.active,.nav button:hover{border-color:var(--accent); box-shadow:0 0 0 2px rgba(118,199,255,.15)} main{padding:22px 0} .grid{display:grid; gap:20px; grid-template-columns: 280px 1fr} .card{background: linear-gradient(180deg, #121523, #0f111a); border:1px solid #252a3a; border-radius:14px; padding:16px; box-shadow:0 10px 30px rgba(0,0,0,.35)} .card h3{margin:0 0 8px 0} .muted{color:var(--muted)} .small{font-size:12px} #toc{max-height:68vh; overflow:auto; padding-right:6px} #search{display:flex; gap:6px; margin:10px 0} #search input,.field input,.field textarea{width:100%; background:#0d0f17; border:1px solid #252a3a; border-radius:10px; padding:10px; color:var(--text)} #search input::placeholder{color:#5c6680} .toc-item{padding:8px; border-radius:8px; cursor:pointer} .toc-item:hover{background:#171a26} .toc-item.active{background:#192033; border:1px solid #2d3650} #reader{min-height:400px} .chapter{margin-bottom:24px} .chapter h2{margin:8px 0 6px 0; font-size:18px} .chunk{padding:8px 10px; border-left:3px solid transparent; border-radius:8px} .chunk[data-hit="1"]{background:#171b28; border-left-color:var(--accent)} .controls{display:flex; gap:8px; flex-wrap:wrap; margin-bottom:10px} .btn{background:var(--soft); color:var(--text); border:1px solid #2b3144; border-radius:10px; padding:8px 12px; cursor:pointer} .btn:hover{border-color:var(--accent)} .badge{display:inline-block; padding:2px 8px; border-radius:999px; background:#192033; border:1px solid #2a3147; color:#c6cee1; font-size:12px} .gallery{display:grid; grid-template-columns: repeat(3, 1fr); gap:10px} .tile{aspect-ratio: 1; border-radius:14px; background:#0f1320; border:1px solid #252a3a; position:relative; overflow:hidden} .tile img{position:absolute; inset:0; width:100%; height:100%; object-fit:cover} .caption{position:absolute; left:8px; bottom:8px; font-size:12px; color:#c6cee1; background:rgba(0,0,0,.35); padding:2px 6px; border-radius:8px} .overlay{position:fixed; inset:0; background:rgba(0,0,0,.65); display:none; align-items:center; justify-content:center; z-index:9} .overlay .panel{width:min(900px, 92vw); max-height:85vh; overflow:auto; padding:18px; border-radius:14px; background:linear-gradient(180deg,#0f1320,#0c0f19); border:1px solid #2b3144} .panel header{position:sticky; top:0; padding:0 0 10px 0; background:transparent} .panel h2{margin:0 0 2px 0} .panel .close{float:right; background:#131828; border:1px solid #2b3144; color:#d6def0; border-radius:8px; padding:6px 10px; cursor:pointer} .panel .content{white-space:pre-wrap} dialog{border:none; background:#0f1320; color:var(--text); border:1px solid #2a3147; border-radius:14px; padding:0; width:min(760px,95vw)} dialog header{padding:12px 16px; border-bottom:1px solid #233; display:flex; justify-content:space-between; align-items:center} dialog .body{padding:12px 16px} dialog form .field{margin-bottom:10px} dialog::backdrop{background:rgba(0,0,0,.6)} footer{padding:30px 0; color:#8f98ac} .rules{columns:2; column-gap:18px} .rule{break-inside:avoid; background:#121525; border:1px solid #232a3e; border-radius:12px; padding:10px; margin:6px 0} `, "app.js": `"use strict"; /* Santara SPA logic (no external deps) */ // --- State & storage const STORAGE_KEY = "santara.books.v1"; const state = { db: null, current: 1, filter: "" }; function loadBooks(){ try{ const raw = localStorage.getItem(STORAGE_KEY); if(!raw) return structuredClone(defaultBooks()); const parsed = JSON.parse(raw); return Object.assign(structuredClone(defaultBooks()), parsed); }catch(e){ console.warn("Storage error, resetting", e); return structuredClone(defaultBooks()); } } function saveBooks(db){ localStorage.setItem(STORAGE_KEY, JSON.stringify(db)); } // --- Default DB function defaultBooks(){ return { 1:{id:1, title:"Santara: Book One", text: \`${bookOneText().trim()}\`}, 2:{id:2, title:"Santara: Book Two — (Add Title)", text:""}, 3:{id:3, title:"Santara: Book Three — (Add Title)", text:""}, 4:{id:4, title:"Santara: Book Four — (Add Title)", text:""}, 5:{id:5, title:"Santara: Book Five — Transforming – Self and World (Paste full text)", text:""}, 6:{id:6, title:"Santara: Book Six — The Great Integration (Paste full text)", text:""}, 7:{id:7, title:"Santara: Book Seven — The Eternal Continuum (Paste full text)", text:""} }; } // --- Bootstrap state.db = loadBooks(); (function routerFromHash(){ const params = new URLSearchParams(location.hash.slice(1)); const b = Number(params.get('book')||"1"); if(state.db[b]) state.current=b; const query = params.get('query'); if(query){ state.filter = query; document.addEventListener('DOMContentLoaded', ()=>{ const q=document.getElementById('q'); if(q) q.value=query; }); } })(); // --- Elements const navButtons = document.querySelectorAll('nav button'); navButtons.forEach(btn=>{ btn.addEventListener('click', ()=>{ navButtons.forEach(b=>b.classList.remove('active')); btn.classList.add('active'); const v = btn.dataset.view; document.querySelector('#library').style.display = v==='library'?'grid':'none'; document.querySelector('#galleryView').style.display = v==='gallery'?'block':'none'; document.querySelector('#rulesView').style.display = v==='rules'?'block':'none'; document.querySelector('#aboutView').style.display = v==='about'?'block':'none'; }); }); document.getElementById('go').addEventListener('click', ()=>{ state.filter = document.getElementById('q').value; renderBook(); }); document.getElementById('randomQuoteBtn').addEventListener('click', ()=>{ const book = state.db[state.current]; const chunks = collectChunks(book).filter(Boolean); if(!chunks.length){ alert("No text in this book yet."); return;} const pick = chunks[Math.floor(Math.random()*chunks.length)]; openOverlay(book.title, pick); }); document.getElementById('closeOverlay').addEventListener('click',()=>document.getElementById('overlay').style.display='none'); // Edit dialog const editDialog = document.getElementById('editDialog'); document.getElementById('editBtn').addEventListener('click', ()=>editDialog.showModal()); document.getElementById('closeEdit').addEventListener('click', ()=>editDialog.close()); document.getElementById('clearBook').addEventListener('click', ()=>{ const n = Number(document.getElementById('bookNumber').value); if(!n || !state.db[n]) return; state.db[n].text = ""; saveBooks(state.db); renderAll(); }); document.getElementById('editForm').addEventListener('submit', (e)=>{ e.preventDefault(); const n = Number(document.getElementById('bookNumber').value); const t = document.getElementById('bookTitleInput').value.trim(); const txt = document.getElementById('bookText').value; if(!n || !state.db[n]){ alert("Pick a book number 1–7"); return;} if(t) state.db[n].title = t; state.db[n].text = txt; saveBooks(state.db); editDialog.close(); renderAll(); }); // --- Rendering function parseChapters(text){ const lines = (text||"").split(/\\r?\\n/); const chapters=[]; let current=null; for(const raw of lines){ const line = raw.trim(); const m = /^Chapter\\s+(\\d+):\\s*(.+)$/.exec(line); if(m){ if(current) chapters.push(current); current = { num:Number(m[1]), title:m[2], chunks:[] }; continue; } if(!current || !line) continue; current.chunks.push(line); } if(current) chapters.push(current); return chapters; } function renderTOC(){ const toc = document.getElementById('toc'); toc.innerHTML=""; Object.values(state.db).sort((a,b)=>a.id-b.id).forEach(book=>{ const div = document.createElement('div'); div.className = 'toc-item' + (book.id===state.current?' active':''); div.textContent = \`\${book.id}. \${book.title}\`; div.addEventListener('click', ()=>{ state.current = book.id; history.replaceState(null,"", \`#book=\${book.id}\`); renderAll(); }); toc.appendChild(div); }); } function renderBook(){ const book = state.db[state.current]; document.getElementById('bookTitle').textContent = book.title || \`Book \${book.id}\`; document.getElementById('bookMeta').innerHTML = \`Book \${book.id}\`; const holder = document.getElementById('chapters'); holder.innerHTML=""; const chapters = parseChapters(book.text||""); if(!chapters.length){ holder.innerHTML='
No content yet. Click Edit / Import to paste the full text.
'; return;} const q = (state.filter||"").trim().toLowerCase(); chapters.forEach(ch=>{ const chDiv = document.createElement('div'); chDiv.className='chapter'; chDiv.innerHTML = \`

Chapter \${ch.num}: \${ch.title}

\`; ch.chunks.forEach(raw=>{ const m = /^(\\d+\\.\\d+)\\s+(.*)$/.exec(raw); const id = m? m[1] : ""; const body = m? m[2] : raw; const hit = q && raw.toLowerCase().includes(q); const p = document.createElement('div'); p.className='chunk'; if(hit) p.dataset.hit="1"; p.innerHTML = \`\${id? id+" " : ""}\${escapeHtml(body)}\`; p.tabIndex=0; p.addEventListener('click', ()=>openOverlay(\`Chapter \${ch.num} — \${ch.title}\`, \`\${id? id+" " : ""}\${body}\`)); chDiv.appendChild(p); }); holder.appendChild(chDiv); }); } function renderGallery(){ const items = [ {name:"Moon Window", src:"assets/moon.svg"}, {name:"Candle", src:"assets/candle.svg"}, {name:"Sanctum", src:"assets/sanctum.svg"}, {name:"Athena", src:"assets/glyph_A.svg"}, {name:"Kali", src:"assets/glyph_K.svg"}, {name:"Thor", src:"assets/glyph_T.svg"}, {name:"Anansi", src:"assets/glyph_N.svg"}, {name:"Saraswati", src:"assets/glyph_S.svg"}, {name:"Ma’at", src:"assets/glyph_M.svg"} ]; const g = document.getElementById('gallery'); g.innerHTML=""; for(const it of items){ const tile=document.createElement('div'); tile.className='tile'; const img=new Image(); img.src=it.src; img.alt=it.name; tile.appendChild(img); const cap=document.createElement('div'); cap.className='caption'; cap.textContent=it.name; tile.appendChild(cap); g.appendChild(tile); } } function renderRules(){ const rules = [ "Know Thyself: identify, understand, refine your nature.", "Choose accountability over perfection.", "Control the tongue; choose pause over punishment.", "Transmute shadow into service.", "Order your care: Self, Family, Friends, Community, Humanity.", "Practice: breath, presence, deliberate correction.", "We rise together—or not at all." ]; const list = document.getElementById('rulesList'); list.innerHTML=""; rules.forEach((text,i)=>{ const div=document.createElement('div'); div.className='rule'; div.innerHTML = \`Scroll \${i+1}. \${escapeHtml(text)}\`; list.appendChild(div); }); } function escapeHtml(s){ return (s||"").replace(/[&<>'"]/g, c=>({'&':'&','<':'<','>':'>',\"'\":''','\"':'"'}[c])); } function openOverlay(title, content){ document.getElementById('overlayTitle').textContent = title; document.getElementById('overlayContent').textContent = content; document.getElementById('overlay').style.display='flex'; } // --- Utilities function collectChunks(book){ return parseChapters(book.text||\"\").flatMap(ch=>ch.chunks); } // --- Initial render function renderAll(){ renderTOC(); renderBook(); renderGallery(); renderRules(); } document.addEventListener('DOMContentLoaded', renderAll); // --- Tests (non-destructive) (function tests(){ console.group(\"%cSantara Tests\",\"color:#76c7ff\"); try{ const db = state.db; console.assert(db[1].title.includes(\"Book One\"), \"Book One title missing\"); const chapters = parseChapters(db[1].text); console.assert(chapters.length>=7, \"Expected at least 7 chapters in Book One\"); console.assert(db[1].text.includes(\"Know Thyself\"), \"Expected 'Know Thyself' present\"); console.assert(typeof openOverlay === \"function\", \"openOverlay should be defined\"); console.log(\"%cAll basic tests passed.\",\"color:#6ee7a8\"); }catch(err){ console.error(\"Test failure:\", err); } console.groupEnd(); })(); // --- Embedded Book One text function bookOneText(){ return \` Santara: Book One Chapter 1: Know Thyself 1.1 To begin the path, one must first begin with themselves. Not with the world, not with the gods, but with the mirror. 1.2 Every soul is born with a nature—some soft, some wild, some dark, some shining. These natures are not fixed, but they shape our path until we claim them. 1.3 Ignorance is the first default. Hate is the absence of love. And the first ignorance we must overcome is the ignorance of ourselves. 1.4 Some are born wrathful, some manipulative, some gentle, some lost. No nature is cursed—but every nature requires mastery. 1.5 To identify your nature is the first step. To understand it is the second. To refine it is the third. Only then can you move forward. 1.6 Let no child grow without knowing their strengths and shadows. Let no parent nurture the shadow and starve the light. 1.7 You cannot control your world until you learn to control yourself. You cannot be your best self until you harness your nature for a positive path. 1.8 Observe what brings you pleasure and at what cost. If it comes from another's pain, it is Taker behavior. Let it go. 1.9 Together we can shed the pain into the void. Break it into pieces, breathe it out, let the cosmos take what no longer serves. 1.10 To see yourself is the greatest gift you can give the world. For in knowing your nature, you begin to shape your destiny. Chapter 2: The Divine Within 2.1 These natures are not fixed. They blend. They evolve. 2.2 You are your own God. Make your way, chart your path—the cosmos will guide it. 2.3 You are given the freedom, the knowledge, the power. Use it well. 2.4 Some things are inevitable—but your nature is not one of them. 2.5 Each nature has a place in humanity. It can build or break. It can elevate or destroy. 2.6 Every trait can be a gift. Every trait can become a curse. Wisdom is knowing when to lead, and when to let go. 2.7 Lucifer – The Devil. Passionate, clever, misunderstood. Born with fire, often feared, yet capable of brilliant solutions. His nature: Problem solver, boundary pusher, challenger of norms. Path: From rebellion to redemption. 2.8 Athena – The Wise. Strategic, calm, guiding. Born to lead through knowledge and logic. Her nature: The patient teacher. Path: From detachment to engaged wisdom. 2.9 Kali – The Fierce. Destroyer and mother. Born in fire, reborn in love. Her nature: Intensity. Path: From chaos to creative transformation. 2.10 Brigid – The Nurturer. Kind, empathic, healing. Her nature: The caregiver. Path: From martyrdom to self-preserving compassion. 2.11 Anansi – The Weaver. Communicator, planner, schemer. Her nature: The manipulator when unconscious, the diplomat when awake. Path: From cunning to counsel. 2.12 Thor – The Willful. Powerful, impulsive, protective. His nature: Wrathful protector. Path: From destruction to sacred defense. 2.13 Saraswati – The Artist. Dreamy, expressive, free. Her nature: The visionary. Path: From escapism to illumination. 2.14 Ma’at – The Just. Disciplined, fair, unwavering. Her nature: The righteous. Path: From rigidity to universal balance. 2.15 You are not confined to one. You may be many. But your current pulls you. Learn its flow, or be swept away. Chapter 3: The Mirror of Needs 3.1 Observe yourself. Observe your world. And if you are a parent—observe your children's world. 3.2 List your wants. List your needs. Look deeper. What godlike nature fuels them? 3.3 Some wants are born of wounds. Others are born of wisdom. Learn the difference. 3.4 A frivolous desire may, in balance, restore your joy. Do not judge your longings—investigate them. 3.5 What you desire reveals your alignment. Is it for growth? For escape? For ego? For service? 3.6 A new car may be vanity—or security. A vacation may be avoidance—or healing. The truth is in your why. 3.7 Teach your children to examine not just what they want—but who wants it within them. The hungry shadow? The wise self? 3.8 Lucifer may long to break a rule—but also to right a wrong. Kali may burn with rage—but also with justice. 3.9 In every need, a nature calls. In every nature, a tool waits. 3.10 If you observe well, you will act wisely. If you observe poorly, you will react blindly. 3.11 To know your needs is not weakness. It is mastery. For when you know what feeds you, you no longer steal crumbs from others. 3.12 The gods walk with those who examine their hunger. Chapter 4: The Fire Within 4.1 I once stabbed my older brother with a butter knife. He had messed up my hair. It was petty—but also profound. It taught me: anything can hurt. Everyone bleeds. 4.2 That fire, that wrath, lives in me. And yet—mindfulness, breath, stillness—they have calmed my fire. I know who I can be. I choose who I will be. 4.3 The Devil’s Night Celebration, October 30th. My family tradition. Let the devil out after dark—but within reason. We honored the shadow without becoming it. 4.4 You must not pretend you are all light. But you must not unleash your shadow unconsciously. 4.5 You cannot be your best self until you peel away or harness your nature for a positive. 4.6 Wrath becomes strength when tempered. Manipulation becomes guidance when owned. Victimhood becomes testimony when transcended. 4.7 To balance fire, one must breathe. To master impulse, one must pause. 4.8 Your legacy is not what you suppress—it is how you transmute. Chapter 5: Godlike Behavior 5.1 To live godlike is not to be perfect—it is to be accountable. 5.2 Correct the moment. Don’t wait for the apology. Don’t seek validation. Make the shift. 5.3 Control the tongue. So much destruction begins in the mouth. Wisdom is often silence. 5.4 Choose kindness over cleverness. Choose patience over pride. Choose pause over punishment. 5.5 Help others see themselves—not by shame, but by mirror. 5.6 A god corrects with love. A tyrant with force. Be a god. 5.7 Your child learns from what you live. Be the example. Let your correction come with consistency, not rage. 5.8 Tame your inner war. The world doesn’t need more soldiers—it needs healers with scars. 5.9 The path to godhood begins with one small choice: be better. Chapter 6: The Path Forward 6.1 The world we build starts with the foundation we walk upon: the self. Everything you are ripples outward. 6.2 You cannot become the steward of your world if you still fumble the keys to your own house. 6.3 Your emotions, your habits, your temper—these are yours to command. If not, they command you. 6.4 I was born with rage. I stabbed my brother with a butter knife over a bad haircut. Even a spoon can harm when wielded by pain. That taught me everything can hurt, and everyone bleeds. 6.5 But mindfulness tamed me. Breath, stillness, understanding—the devil in me calmed not by suppression but by discipline. I no longer lash, I listen. 6.6 You will not always get it right. But the divine within you keeps the score not by failure—but by effort, by honesty, by change. 6.7 Celebrate your fire, but contain it. Let Devil’s Night be a ritual—not a release into chaos. Let the devil out, but know when to usher him home. 6.8 Your nature will not vanish—it will evolve. Temperance is not weakness. It is the mark of a master. 6.9 You imprint the world with your vibrations—every word, every pause, every action. Leave behind a signature of growth. 6.10 The universe will not ask perfection. It asks participation. Elevation. Presence. 6.11 Be honest about your starting point. Be ruthless in your healing. Be relentless in your becoming. 6.12 The gods do not favor the untouched—they favor the scarred who walk forward anyway. Chapter 7: The Sacred Order 7.1 There is an order to life that, when honored, brings harmony. It is not rigid—but it is wise. The order is this: Self, Family, Friends, Community, Humanity. The first and last are equally sacred. 7.2 The Self is where the divine voice whispers loudest. If you do not love yourself, discipline yourself, and know yourself, you cannot lead or serve. 7.3 Family is the forge. It can harden or soften you. You must learn to forgive your family when possible, protect yourself when needed, and raise the next generation with consciousness. 7.4 Friends are the chosen mirror. They reflect your growth, your patterns, your joy. Choose wisely. Be the friend you wish you had. 7.5 Community is where your influence spreads. Your actions shape the environment. Be the healing presence, not the poison. 7.6 Humanity is the vast whole—strangers, cultures, beliefs beyond your own. You are responsible to it, just as it is responsible for you. 7.7 The Self and Humanity are one and the same at opposite ends of the scale. Your internal chaos echoes in the world. Your peace does too. 7.8 To uplift yourself is not selfish. It is the start. But to stop there is cowardice. What you learn, share. What you overcome, guide others through. 7.9 The gods within you did not arrive to sit in silence. They came to serve. To correct. To uplift. 7.10 We rise together—or not at all. \`; } `, "data/books.json": JSON.stringify({ "1": { "id": 1, "title": "Santara: Book One", "text": "Use the in-app editor or keep index/app.js embedded default.\n\n(You can replace this file later with your full corpus.)" }, "2": { "id": 2, "title": "Santara: Book Two — (Add Title)", "text": "" }, "3": { "id": 3, "title": "Santara: Book Three — (Add Title)", "text": "" }, "4": { "id": 4, "title": "Santara: Book Four — (Add Title)", "text": "" }, "5": { "id": 5, "title": "Santara: Book Five — Transforming – Self and World (Paste full text)", "text": "" }, "6": { "id": 6, "title": "Santara: Book Six — The Great Integration (Paste full text)", "text": "" }, "7": { "id": 7, "title": "Santara: Book Seven — The Eternal Continuum (Paste full text)", "text": "" } }, null, 2), // --- SVG assets "assets/moon.svg": ``, "assets/candle.svg": ``, "assets/sanctum.svg": ``, "assets/glyph_A.svg": glyph('A'), "assets/glyph_K.svg": glyph('K'), "assets/glyph_T.svg": glyph('T'), "assets/glyph_N.svg": glyph('N'), "assets/glyph_S.svg": glyph('S'), "assets/glyph_M.svg": glyph('M') }; // Simple glyph SVG generator function glyph(letter){ return `${letter}`; } // Show preview of index.html (first 80 lines) document.getElementById('preview').textContent = files["index.html"].split("\n").slice(0,80).join("\n"); /* ----------------------------------------------------------- 2) Minimal ZIP (STORE method) builder in-browser - No compression, CRC32 computed - Produces Blob → downloadable URL ----------------------------------------------------------- */ function crc32(buf){ // CRC32 for Uint8Array let c=~0>>>0; for(let i=0;i>>1) ^ (0xedb88320 & -(c & 1)); } } return (~c)>>>0; } function strToU8(str){ return new TextEncoder().encode(str); } function numToLE(n, bytes){ const a=new Uint8Array(bytes); for(let i=0;i>(8*i))&255; } return a; } function concat(chunks){ let total=0; for(const c of chunks) total += c.length; const out = new Uint8Array(total); let off=0; for(const c of chunks){ out.set(c, off); off+=c.length; } return out; } function buildZip(fileMap){ const localHeaders=[]; const centralHeaders=[]; const contents=[]; let offset=0; const encoder = new TextEncoder(); const LFH = 0x04034b50; // Local file header signature const CDH = 0x02014b50; // Central directory header signature const EOCD= 0x06054b50; // End of central directory const version = 20; // version needed to extract const flags = 0; // general purpose bit flag const method = 0; // 0 = store const time = 0; // leave 0 const date = 0; const entries = Object.entries(fileMap); for(const [path, content] of entries){ const nameU8 = encoder.encode(path); const dataU8 = (typeof content === "string") ? strToU8(content) : content; const crc = crc32(dataU8); const compSize = dataU8.length; const uncomp = dataU8.length; // Local File Header const lfh = concat([ numToLE(LFH,4), numToLE(version,2), numToLE(flags,2), numToLE(method,2), numToLE(time,2), numToLE(date,2), numToLE(crc,4), numToLE(compSize,4), numToLE(uncomp,4), numToLE(nameU8.length,2), numToLE(0,2), // extra len nameU8 ]); localHeaders.push(lfh); contents.push(dataU8); const localLen = lfh.length + dataU8.length; // Central Directory Header const cdh = concat([ numToLE(CDH,4), numToLE(0x0314,2), // version made by numToLE(version,2), // version needed numToLE(flags,2), numToLE(method,2), numToLE(time,2), numToLE(date,2), numToLE(crc,4), numToLE(compSize,4), numToLE(uncomp,4), numToLE(nameU8.length,2), numToLE(0,2), // extra numToLE(0,2), // comment numToLE(0,2), // disk start numToLE(0,2), // internal attrs numToLE(0,4), // external attrs numToLE(offset,4), nameU8 ]); centralHeaders.push(cdh); offset += localLen; } const centralDir = concat(centralHeaders); const localDir = concat([...localHeaders, ...contents]); const eocd = concat([ numToLE(EOCD,4), numToLE(0,2), // disk number numToLE(0,2), // disk start numToLE(entries.length,2), numToLE(entries.length,2), numToLE(centralDir.length,4), numToLE(localDir.length,4), numToLE(0,2) // no comment ]); return new Blob([localDir, centralDir, eocd], {type:"application/zip"}); } /* ----------------------------------------------------------- 3) Wire up Build button ----------------------------------------------------------- */ const buildBtn = document.getElementById('build'); const link = document.getElementById('download'); const status = document.getElementById('status'); buildBtn.addEventListener('click', ()=>{ status.textContent = "Building ZIP…"; try{ // Ensure folder structure (ZIP entries use path with /) const map = {}; for(const [k,v] of Object.entries(files)) map[k]=v; const blob = buildZip(map); const url = URL.createObjectURL(blob); link.href = url; link.style.display="inline-block"; status.innerHTML = 'ZIP ready → click Download.'; }catch(e){ console.error(e); status.innerHTML = 'Build failed. See console.'; } }); // Smoke test console.group("%cPackager Tests","color:#76c7ff"); try{ console.assert(typeof buildZip === "function","buildZip should exist"); const testZip = buildZip({"hello.txt": "world"}); console.assert(testZip instanceof Blob, "buildZip must return a Blob"); console.log("%cPackager OK","color:#6ee7a8"); }catch(e){ console.error("Packager test failed", e); } console.groupEnd();