Folders and files
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Repository files navigation
title Knowbooks - experiment kits in a book, for humans of all ages. description Knowbooks are experiment kits in a book, for humans of all ages. baseUrl https://knowbooks.com/ permalink index.html openGraphImage og.jpeg favicon knowbooksLogo.svg buildHtml metaTags buildToFileParser description Write the raw body to one or more files during the first build pass. extends abstractBuildOneCommandParser cueFromId baseParser blobParser example buildToFile robots.txt robots-copy.txt User-agent: * Disallow: javascript buildOutput() { return this.subparticlesToString() } async buildOne() { const { root } = this const { fileSystem, folderPath, filename, path } = root const content = this.buildOutput() for (const name of this.outputFileNames) { await fileSystem.writeProduct(path.join(folderPath, name), content) root.log(`💾 Built ${name} from ${filename}`) } } buildToFile .gitignore *.html .* *.svg *.js *.zip buildZipParser description Zip the listed files or folders after preceding build commands. Requires the zip command. extends abstractBuildCommandParser cueFromId javascript async buildTwo() { const { folderPath, fileSystem, path } = this.root const [name, ...files] = this.getAtomsFrom(1) const zip = require("child_process").execFileSync("zip", ["-rq", "-", ...files], { cwd: folderPath, maxBuffer: 100 * 1024 * 1024 }) await fileSystem.writeProduct(path.join(folderPath, name), zip) } buildToFile knowbooksLogo.svg <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby="title desc"> <title id="title">Knowbooks</title> <desc id="desc">A circular ring supported by two hands.</desc> <g fill="#d9bc78"> <path fill-rule="evenodd" d="M128 14C68.45 14 20 62.45 20 122c0 18.12 4.5 35.2 12.42 50.2l12.37-7.15A93.28 93.28 0 0 1 34.3 122c0-51.67 42.03-93.7 93.7-93.7s93.7 42.03 93.7 93.7a93.28 93.28 0 0 1-10.49 43.05l12.37 7.15A107.42 107.42 0 0 0 236 122c0-59.55-48.45-108-108-108Z"/> <path d="M29.31 156.42c-4.34-7.23-3.66-13.77.07-15.84 3.38-1.87 7.33.31 10.53 4.4l22.82 25.16c1.12 1.24 2.85-.31 1.88-1.66l-19.1-26.67c-3.67-5.13-3.33-10.19.2-12.41 3.77-2.38 8.54.65 12.52 5.26l18.1 20.97c3.7 4.29 8.36 7.65 12.05 12.6 5.96 7.99 6.08 19.48 6.08 31.36v25.99c-24.1-2.86-43.02-12.51-53.85-26.48-7.38-9.52-8.76-23.53-11.3-42.68Z"/> <path d="M226.69 156.42c4.34-7.23 3.66-13.77-.07-15.84-3.38-1.87-7.33.31-10.53 4.4l-22.82 25.16c-1.12 1.24-2.85-.31-1.88-1.66l19.1-26.67c3.67-5.13 3.33-10.19-.2-12.41-3.77-2.38-8.54.65-12.52 5.26l-18.1 20.97c-3.7 4.29-8.36 7.65-12.05 12.6-5.96 7.99-6.08 19.48-6.08 31.36v25.99c24.1-2.86 43.02-12.51 53.85-26.48 7.38-9.52 8.76-23.53 11.3-42.68Z"/> </g> </svg> <header class="site-header"> <a class="brand" href="#top" aria-label="Knowbooks home"><img src="knowbooksLogo.svg" alt=""><span>KNOWBOOKS</span></a> <a class="faq-link" href="#faq">FAQ</a> <button class="cart-toggle" type="button" aria-expanded="false" aria-controls="cart">Cart <span class="cart-count">0</span></button> </header> <main id="top"> <section class="hero"> <h1>Knowbooks are books you <em>do</em>.</h1> <p class="intro">A Knowbook is an experiment kit in a book. Open one, do the experiments, and learn how things work.</p> <a class="down" href="#catalog">Build your shelf <span aria-hidden="true">↓</span></a> </section> <section class="catalog-section" id="catalog" aria-labelledby="catalog-title"> <div class="catalog-tools"><input id="catalog-search" type="search" placeholder="Search books…" aria-label="Search knowbooks" aria-controls="product-grid" title="Search book titles; separate words with spaces to match any." autocomplete="off"></div> <div class="edition-divider is-first" id="search-heading" hidden><h2>Search results</h2></div> <p class="catalog-empty" role="status" aria-live="polite"></p> <div class="product-grid" id="product-grid"></div> </section> ## Frequently asked questions id faq ? Who are Knowbooks for? Knowbooks are for curious people of all ages. Younger children can explore them with an adult, while teenagers and adults can work through experiments independently. ? What comes inside each Knowbook? Each Knowbook contains a curated collection of real objects plus experiment cards that show you what to do to learn a superpower. ? How is the current build quality? Our first Knowbooks are Trailblazer editions. Each one is hand-crafted on Oahu. They are complete and usable, but their objects, experiments, packaging, and instructions are improving as we learn from our first customers. ? Can I use a Knowbook more than once? Yes. Knowbooks are designed to last a lifetime and can be explored repeatedly, shared, and returned to the shelf. Most objects are reusable; some contain consumable/replaceable materials. ? How will I receive my KnowBook(s) (Hawai'i customers)? Oahu customers can have their KnowBooks delivered free of charge, or come pick them up. ? When will my Knowbook(s) ship (Mainland customers)? Orders begin assembly immediately. You'll receive tracking when your order is on its way. Delivery times will improve as we scale up. ? What's the return policy? Your satisfaction is our top priority. If we don't reach your expectations, get in touch and we'll coordinate a replacement and/or refund. </main> <footer class="site-footer"> <p class="email-invite">We don't have an email list, but we'd love to hear from you! Emails welcome: <a href="mailto:breck@knowbooks.com">breck@knowbooks.com</a></p> <nav class="footer-links" aria-label="Social links"><a href="https://x.com/breckyunits">X</a><a href="https://github.com/breck7/knowbooks.com">GitHub</a></nav> <div class="footer-links"><a href="knowbooks.zip" download>Download this website</a></div> </footer> <div class="scrim" hidden></div> <div class="book-preview" role="dialog" aria-modal="true" aria-label="Inside this Knowbook" hidden></div> <aside class="cart" id="cart" aria-labelledby="cart-title" aria-hidden="true"> <div class="cart-head"><div><p class="eyebrow">Your shelf</p><h2 id="cart-title">Cart</h2></div><button class="cart-close" type="button" aria-label="Close cart">×</button></div> <div class="cart-lines"></div> <div class="cart-empty"><p>Your cart is empty.</p><a href="#catalog">Find a Knowbook</a></div> <div class="cart-footer" hidden><p class="cart-total"><span>Total</span><strong>$0</strong></p><a class="paypal-button" href="https://www.paypal.com/cgi-bin/webscr" rel="noopener">Pay with PayPal</a><small>Shipping and applicable taxes are handled by PayPal.<br>Oahu customers can pay by cash.</small><details class="crypto"><summary>Pay with crypto</summary><p><b>BTC</b><span><output data-crypto="BTC"></output><code>3G9q4tRjkTSP8PP4eFVZuNLtXxKuERJcJr</code></span><b>ETH</b><span><output data-crypto="ETH"></output><code>0xaEe37CE75AcC6341abfa1c73c6Bb1B73963a6ccD</code></span><b>NEAR</b><span><output data-crypto="NEAR"></output><code>4345b29b9fb2ba13d2fb47e7946b1b135118381fb13e54c8a9042cd79542e89f</code></span><b>USDC <i>(Base)</i></b><span><output data-crypto="USDC"></output><code>0x3d3e69E8f208A782E09f018643fE9796e918e45e</code></span></p><small>Send the amount shown, then email your transaction ID, cart, and shipping address to <a href="mailto:breck@knowbooks.com">breck@knowbooks.com</a>.</small></details></div> </aside> <noscript>This shop needs JavaScript to manage its cart.</noscript> css :root { --paper: #f4efe5; --ink: #20211e; --muted: #706b61; --line: rgba(32, 33, 30, .18); --gold: #d9bc78; color-scheme: light; font-family: Georgia, "Times New Roman", serif; } * { box-sizing: border-box; } html { scroll-behavior: smooth; } body { margin: 0; color: var(--ink); background: var(--paper); } body.cart-open { overflow: hidden; } button, a { color: inherit; } button { font: inherit; } .site-header { position: absolute; z-index: 10; inset: 0 0 auto; display: flex; align-items: center; justify-content: space-between; padding: 24px clamp(20px, 5vw, 72px); } .brand { display: inline-flex; align-items: center; gap: 11px; text-decoration: none; font: 700 11px/1 Arial, sans-serif; letter-spacing: .2em; } .brand img { width: 30px; height: 30px; padding: 5px; border-radius: 50%; background: var(--ink); } .faq-link { margin-left: auto; margin-right: 20px; text-decoration: none; text-transform: uppercase; font: 700 10px/1 Arial, sans-serif; letter-spacing: .13em; } .faq-link:hover, .faq-link:focus-visible { text-decoration: underline; text-underline-offset: 4px; } .cart-toggle { border: 1px solid var(--ink); border-radius: 999px; padding: 9px 10px 9px 16px; background: transparent; cursor: pointer; text-transform: uppercase; font: 700 10px/1 Arial, sans-serif; letter-spacing: .13em; } .cart-count { display: inline-grid; place-items: center; min-width: 24px; height: 24px; margin-left: 7px; border-radius: 50%; color: var(--paper); background: var(--ink); } .hero { --ring-radius: min(24vw, 305px); --ring-size: min(48vw, 610px); --ring-overhang: 8vw; --ring-top: 15%; min-height: 92svh; display: flex; flex-direction: column; justify-content: center; align-items: flex-start; padding: 120px clamp(20px, 8vw, 124px) 70px; overflow: hidden; position: relative; background: radial-gradient(circle at calc(100% + var(--ring-overhang) - var(--ring-radius)) calc(var(--ring-top) + var(--ring-radius)), rgba(217,188,120,.35), transparent 20%), linear-gradient(145deg, #f7f3eb, #e9e0d0); } .hero::after { content: ""; position: absolute; right: calc(0px - var(--ring-overhang)); top: var(--ring-top); width: var(--ring-size); aspect-ratio: 1; background: url("knowbooksLogo.svg") center / contain no-repeat; opacity: .14; } .eyebrow { margin: 0 0 20px; text-transform: uppercase; font: 700 10px/1 Arial, sans-serif; letter-spacing: .2em; } h1 { position: relative; z-index: 1; max-width: 820px; margin: 0; font-size: clamp(58px, 9vw, 132px); font-weight: 400; line-height: .85; letter-spacing: -.055em; } .intro { position: relative; z-index: 1; max-width: 610px; margin: 36px 0; color: #49473f; font-size: clamp(18px, 2vw, 25px); line-height: 1.45; } .down { position: relative; z-index: 1; display: inline-flex; gap: 18px; padding-bottom: 7px; border-bottom: 1px solid; text-decoration: none; text-transform: uppercase; font: 700 10px/1 Arial, sans-serif; letter-spacing: .15em; } .catalog-section { padding: 100px clamp(20px, 5vw, 72px) 120px; } .section-heading { display: grid; grid-template-columns: 1fr 2fr; align-items: end; gap: 30px; margin-bottom: 60px; } .section-heading .eyebrow { align-self: start; padding-top: 11px; } .section-heading h2 { margin: 0; max-width: 720px; font-size: clamp(40px, 6vw, 78px); font-weight: 400; line-height: .95; letter-spacing: -.04em; } .catalog-tools { display: flex; justify-content: flex-end; margin: 0 clamp(20px, 3vw, 42px) 18px; } #catalog-search { width: 155px; max-width: 100%; padding: 6px 2px; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; background: transparent; color: var(--ink); font: 12px/1.4 Arial, sans-serif; } #catalog-search::placeholder { color: var(--muted); } #catalog-search:focus-visible { outline: 1px solid var(--muted); outline-offset: 4px; } .catalog-empty { margin: 0 clamp(20px, 3vw, 42px); color: var(--muted); font: 14px/1.5 Arial, sans-serif; } .catalog-empty:empty, .product[hidden], .edition-divider[hidden] { display: none; } .edition-divider.is-first { margin-top: 0; } .product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); } .edition-divider { grid-column: 1 / -1; display: flex; align-items: baseline; gap: 14px; margin: 72px clamp(20px, 3vw, 42px) 8px; } .edition-divider:first-child { margin-top: 0; } .edition-divider h2 { margin: 0; font-size: clamp(28px, 3vw, 42px); font-weight: 400; line-height: 1; letter-spacing: -.025em; } .edition-divider p { margin: 0; color: var(--muted); white-space: nowrap; text-transform: uppercase; font: 700 14px/1.2 Arial, sans-serif; letter-spacing: .06em; } .product { min-width: 0; padding: clamp(20px, 3vw, 42px); background: var(--paper); } .book-cover { position: relative; isolation: isolate; aspect-ratio: .72; display: flex; flex-direction: column; align-items: center; padding: 11% 9%; overflow: hidden; color: #f4dfb3; background: linear-gradient(135deg, var(--cover), color-mix(in srgb, var(--cover) 58%, #000)); border-radius: 2px 12px 12px 2px; box-shadow: 11px 16px 25px rgba(34,28,20,.2), inset 5px 0 8px rgba(0,0,0,.17), inset 0 0 0 1px rgba(255,255,255,.08); cursor: pointer; transition: transform 180ms ease; } .product:hover .book-cover { transform: translateY(-5px) rotate(-.5deg); } .book-cover::before { content: ""; position: absolute; z-index: -1; inset: 0; opacity: .22; background: repeating-linear-gradient(89deg, rgba(255,255,255,.18) 0 1px, transparent 1px 4px); } .book-cover h3 { width: 100%; margin: 5% 0 0; white-space: nowrap; text-align: center; text-transform: uppercase; font-size: clamp(19px, 2vw, 29px); font-weight: 400; letter-spacing: .11em; } .book-symbol { position: absolute; left: 50%; top: 50%; width: 44%; aspect-ratio: 1; transform: translate(-50%, -50%); pointer-events: none; } .book-symbol svg { display: block; width: 100%; height: 100%; } .book-cover a { text-decoration: none; } .book-brand { margin-top: auto; text-transform: uppercase; font: 700 7px/1 Arial, sans-serif; letter-spacing: .18em; } .book-preview { position: fixed; z-index: 30; inset: 0; background: rgba(23,23,20,.55); opacity: 0; visibility: hidden; transition: opacity 250ms, visibility 250ms; } .book-preview.is-open { opacity: 1; visibility: visible; } .preview-book { position: absolute; left: 50%; top: 50%; width: min(320px, 43vw); aspect-ratio: .72; translate: -50% -50%; transform: translate(var(--x), var(--y)) scale(var(--scale)); transition: transform 450ms cubic-bezier(.2,.8,.2,1); perspective: 1400px; } .book-preview.is-open .preview-book { transform: none; } .preview-pages { position: absolute; inset: 0; display: grid; place-items: center; padding: 16%; border-radius: 2px 12px 12px 2px; background: #fffaf0; box-shadow: 11px 16px 35px rgba(0,0,0,.28), inset 7px 0 12px rgba(90,70,35,.12); } .preview-pages p { margin: 0; font-size: clamp(13px, 2vw, 19px); line-height: 1.45; } .preview-close { position: absolute; z-index: 1; top: 8px; right: 8px; border: 0; background: transparent; cursor: pointer; font-size: 25px; } .preview-cover { position: absolute; inset: 0; transform-origin: left; transition: transform 650ms cubic-bezier(.2,.7,.2,1) 450ms; } .preview-cover h3, .preview-cover .book-symbol, .preview-cover .book-brand { transition: opacity 100ms 450ms; } .book-preview.is-open .preview-cover { transform: rotateY(-155deg); } .book-preview.is-open .preview-cover h3, .book-preview.is-open .preview-cover .book-symbol, .book-preview.is-open .preview-cover .book-brand { opacity: 0; } .product-info { display: flex; justify-content: space-between; gap: 16px; align-items: center; padding-top: 23px; } .product-meta p { margin: 4px 0 0; color: var(--muted); font: 12px/1.4 Arial, sans-serif; } .price { font-size: 19px; } .add-button { flex: 0 0 auto; width: 44px; height: 44px; border: 1px solid var(--ink); border-radius: 50%; background: transparent; cursor: pointer; font: 22px/1 Arial, sans-serif; transition: background 150ms, color 150ms; } .add-button:hover, .add-button.is-added { color: var(--paper); background: var(--ink); } main > .scrollSection { padding: 0 clamp(20px, 8vw, 124px); } main > .scrollSection h2.scrollParagraph { max-width: 720px; margin: 0 0 45px; font-size: clamp(40px, 6vw, 78px); font-weight: 400; line-height: .95; letter-spacing: -.04em; } main > .scrollSection:last-child { padding-bottom: 120px; } main > .scrollSection:last-child .scrollParagraph { border-bottom: 1px solid var(--line); } .scrollQuestion, .scrollQuestion + .scrollParagraph { max-width: 900px; margin-right: 0; margin-left: auto; } .scrollQuestion { margin-top: 0; margin-bottom: 0; padding: 24px 0 10px; border-top: 1px solid var(--line); font-size: clamp(19px, 2vw, 25px); font-weight: 400; } .scrollQuestion + .scrollParagraph { margin-top: 0; margin-bottom: 0; padding: 0 0 24px; color: var(--muted); font: 15px/1.6 Arial, sans-serif; } .scrim { position: fixed; z-index: 19; inset: 0; background: rgba(23,23,20,.36); backdrop-filter: blur(2px); } .cart { position: fixed; z-index: 20; inset: 0 0 0 auto; width: min(480px, 100%); padding: 30px; display: flex; flex-direction: column; background: #faf7f0; box-shadow: -18px 0 60px rgba(20,20,17,.18); transform: translateX(105%); visibility: hidden; transition: transform 240ms ease, visibility 240ms; } .cart.is-open { transform: none; visibility: visible; } .cart-head { display: flex; justify-content: space-between; align-items: flex-start; padding-bottom: 24px; border-bottom: 1px solid var(--line); } .cart-head .eyebrow { margin-bottom: 7px; } .cart-head h2 { margin: 0; font-size: 42px; font-weight: 400; } .cart-close { width: 42px; height: 42px; border: 0; background: transparent; cursor: pointer; font-size: 32px; font-weight: 300; } .cart-lines { overflow-y: auto; } .cart-line { display: grid; grid-template-columns: 54px 1fr auto; gap: 15px; align-items: center; padding: 18px 0; border-bottom: 1px solid var(--line); } .line-swatch { width: 54px; aspect-ratio: .72; border-radius: 1px 5px 5px 1px; background: var(--cover); box-shadow: inset 4px 0 rgba(0,0,0,.16); } .line-copy h3 { margin: 0 0 6px; font-size: 18px; font-weight: 400; } .line-copy p { margin: 0; color: var(--muted); font: 12px/1 Arial, sans-serif; } .quantity { display: flex; align-items: center; gap: 8px; } .quantity button { width: 27px; height: 27px; border: 1px solid var(--line); border-radius: 50%; background: transparent; cursor: pointer; } .quantity span { min-width: 12px; text-align: center; font: 12px/1 Arial, sans-serif; } .cart-empty { margin: auto; text-align: center; color: var(--muted); } .cart-empty a { color: var(--ink); text-underline-offset: 4px; } .cart-footer { margin-top: auto; padding-top: 24px; border-top: 1px solid var(--line); } .cart-total { display: flex; justify-content: space-between; margin: 0 0 20px; font: 16px/1 Arial, sans-serif; } .cart-total strong { font-size: 20px; } .paypal-button { display: block; padding: 16px; border-radius: 999px; color: white; background: #142c8e; text-align: center; text-decoration: none; font: 700 14px/1 Arial, sans-serif; } .paypal-button:hover { background: #0c236d; } .cart-footer small { display: block; margin-top: 12px; color: var(--muted); text-align: center; font: 10px/1.4 Arial, sans-serif; } .crypto { margin-top: 16px; font: 12px/1.5 Arial, sans-serif; } .crypto summary { cursor: pointer; text-align: center; text-decoration: underline; text-underline-offset: 3px; } .crypto p { display: grid; grid-template-columns: auto 1fr; gap: 8px 10px; } .crypto output, .crypto code { display: block; } .crypto code { overflow-wrap: anywhere; user-select: all; } .site-footer { display: flex; flex-direction: column; align-items: center; gap: 24px; padding: 8px 20px 32px; color: var(--muted); text-align: center; } .email-invite { max-width: 670px; margin: 0; font-size: clamp(17px, 2vw, 22px); line-height: 1.5; } .email-invite a { color: var(--ink); text-decoration-thickness: 1px; text-underline-offset: 4px; } .footer-links { display: flex; gap: 22px; text-transform: uppercase; font: 10px/1 Arial, sans-serif; letter-spacing: .12em; } .footer-links a { color: inherit; text-decoration: none; text-underline-offset: 3px; } .footer-links a:hover, .footer-links a:focus-visible { text-decoration: underline; } noscript { display: block; padding: 12px; color: white; background: #8a2d21; text-align: center; } @media (max-width: 850px) { .product-grid { grid-template-columns: repeat(2, 1fr); } .section-heading { grid-template-columns: 1fr; } } @media (max-width: 520px) { .hero { --ring-radius: 44vw; --ring-size: 88vw; --ring-overhang: 32vw; min-height: 82svh; } .product-grid { grid-template-columns: 1fr; } .cart { padding: 22px; } } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } *, *::before, *::after { transition-duration: .01ms !important; } } catalog.js script (() => { "use strict"; const coverColors = [ { coverColor: "#243f59" }, { coverColor: "#a8442e" }, { coverColor: "#375748" }, { coverColor: "#5b4154" }, { coverColor: "#26727a" } ]; const productCatalog = (Array.isArray(catalog) ? catalog : []).map((product, index) => ({ ...product, ...coverColors[index % coverColors.length] })); const products = new Map(productCatalog.map(product => [product.id, product])); const paypalEmail = "breck7@gmail.com"; const currency = "USD"; const money = new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 0 }); const shippingByStatus = { believer: "Ship within 90 days", trailblazer: "Ship within 30 days", frontier: "Ship within 6 months" }; const editionNameByStatus = { believer: "Believer", trailblazer: "Trailblazer", frontier: "Frontier" }; const statusOrder = ["trailblazer", "believer", "frontier"]; const crypto = { BTC: [78759.81, 8], ETH: [2474.64, 6], NEAR: [1.97, 2], USDC: [1, 2] }; // USD price, display decimals; updated August 31, 2026 const cart = new Map(); const grid = document.querySelector("#product-grid"); const preview = document.querySelector(".book-preview"); const drawer = document.querySelector("#cart"); const scrim = document.querySelector(".scrim"); const toggle = document.querySelector(".cart-toggle"); const count = document.querySelector(".cart-count"); const lines = document.querySelector(".cart-lines"); const empty = document.querySelector(".cart-empty"); const footer = document.querySelector(".cart-footer"); const total = document.querySelector(".cart-total strong"); const checkout = document.querySelector(".paypal-button"); const escapeHtml = value => String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); const editionDetails = status => { const name = editionNameByStatus[status]; const shipping = shippingByStatus[status]; if (!name || !shipping) throw new Error(`Unknown product status: ${status}`); return { name, shipping }; }; const productCard = product => { const { name, shipping } = editionDetails(product.status); return `<article class="product" id="${escapeHtml(product.id)}"> <div class="book-cover" style="--cover:${escapeHtml(product.coverColor)}"> <h3><a href="?knowbookId=${escapeHtml(product.id)}" onclick="return false">${escapeHtml(product.title)}</a></h3><span class="book-symbol" aria-hidden="true">${product.cover || ""}</span><span class="book-brand">Knowbooks</span> </div> <div class="product-info"><div class="product-meta"><span class="price">${money.format(product.priceUsd)}</span><p>Experiment kit in a book.<br><i>${escapeHtml(name)} edition<br>${escapeHtml(shipping)}</i></p></div><button class="add-button" type="button" data-add="${escapeHtml(product.id)}" aria-label="Add ${escapeHtml(product.title)} to cart">+</button></div> </article>`; }; const editionDivider = status => { const { name, shipping } = editionDetails(status); return `<div class="edition-divider" data-status="${escapeHtml(status)}"><h2>${escapeHtml(name)} editions</h2><p>${escapeHtml(shipping)}</p></div>`; }; const filterCatalog = () => { const query = document.querySelector("#catalog-search").value.trim(); const terms = query.toLowerCase().split(/\s+/).filter(Boolean); const url = new URL(location.href); query ? url.searchParams.set("q", query) : url.searchParams.delete("q"); if (url.href !== location.href) history.replaceState(null, "", url); document.querySelector("#search-heading").hidden = !query; const visibleStatuses = new Set(); grid.querySelectorAll(".product").forEach(card => { const title = products.get(card.id).title.toLowerCase(); card.hidden = terms.length > 0 && !terms.some(term => title.includes(term)); if (!card.hidden) visibleStatuses.add(products.get(card.id).status); }); const firstStatus = statusOrder.find(status => visibleStatuses.has(status)); grid.querySelectorAll(".edition-divider").forEach(divider => { divider.hidden = Boolean(query) || !visibleStatuses.has(divider.dataset.status); divider.classList.toggle("is-first", divider.dataset.status === firstStatus); }); document.querySelector(".catalog-empty").textContent = visibleStatuses.size ? "" : "No knowbooks match this search."; fitBookTitles(); }; let titleFitFrame; const fitBookTitles = () => { cancelAnimationFrame(titleFitFrame); titleFitFrame = requestAnimationFrame(() => { grid.querySelectorAll(".product:not([hidden]) .book-cover h3").forEach(title => { title.style.removeProperty("font-size"); if (title.scrollWidth > title.clientWidth) { const baseSize = Number.parseFloat(getComputedStyle(title).fontSize); title.style.fontSize = `${baseSize * title.clientWidth / title.scrollWidth}px`; } }); }); }; const paypalUrl = () => { const url = new URL("https://www.paypal.com/cgi-bin/webscr"); url.searchParams.set("cmd", "_cart"); url.searchParams.set("upload", "1"); url.searchParams.set("business", paypalEmail); url.searchParams.set("currency_code", currency); [...cart].forEach(([id, quantity], index) => { const product = products.get(id); const number = index + 1; url.searchParams.set(`item_name_${number}`, `${product.title} Knowbook`); url.searchParams.set(`item_number_${number}`, product.id); url.searchParams.set(`amount_${number}`, Number(product.priceUsd).toFixed(2)); url.searchParams.set(`quantity_${number}`, String(quantity)); }); return url.href; }; const save = () => { try { localStorage.setItem("knowbooks-cart", JSON.stringify([...cart])); } catch {} }; const refresh = () => { const quantity = [...cart.values()].reduce((sum, value) => sum + value, 0); const amount = [...cart].reduce((sum, [id, value]) => sum + products.get(id).priceUsd * value, 0); count.textContent = quantity; document.querySelectorAll("[data-add]").forEach(button => { const added = cart.has(button.dataset.add); button.classList.toggle("is-added", added); button.textContent = added ? "✓" : "+"; }); lines.innerHTML = [...cart].map(([id, value]) => { const product = products.get(id); return `<div class="cart-line"><span class="line-swatch" style="--cover:${escapeHtml(product.coverColor)}" aria-hidden="true"></span><div class="line-copy"><h3>${escapeHtml(product.title)}</h3><p>${money.format(product.priceUsd)} each</p></div><div class="quantity"><button type="button" data-less="${escapeHtml(id)}" aria-label="Remove one ${escapeHtml(product.title)}">−</button><span>${value}</span><button type="button" data-more="${escapeHtml(id)}" aria-label="Add one ${escapeHtml(product.title)}">+</button></div></div>`; }).join(""); empty.hidden = quantity > 0; footer.hidden = quantity === 0; total.textContent = money.format(amount); document.querySelectorAll("[data-crypto]").forEach(output => { const [usd, decimals] = crypto[output.dataset.crypto]; output.textContent = `${(amount / usd).toFixed(decimals)} ${output.dataset.crypto}`; }); checkout.href = quantity ? paypalUrl() : "https://www.paypal.com/cgi-bin/webscr"; save(); }; const openCart = () => { drawer.classList.add("is-open"); drawer.setAttribute("aria-hidden", "false"); toggle.setAttribute("aria-expanded", "true"); scrim.hidden = false; document.body.classList.add("cart-open"); drawer.querySelector(".cart-close").focus(); }; const closeCart = () => { drawer.classList.remove("is-open"); drawer.setAttribute("aria-hidden", "true"); toggle.setAttribute("aria-expanded", "false"); scrim.hidden = true; document.body.classList.remove("cart-open"); }; const openBook = cover => { const rect = cover.getBoundingClientRect(); preview.innerHTML = `<div class="preview-book"><div class="preview-pages"><button class="preview-close" aria-label="Close">×</button><p>Each Knowbook is currently built by hand on Oahu, and each version is currently still unique. Order yours to see what's inside.</p></div>${cover.outerHTML}</div>`; preview.querySelector(".book-cover").classList.add("preview-cover"); preview.hidden = false; const book = preview.firstElementChild; book.style.transition = "none"; book.style.setProperty("--x", `${rect.left + rect.width / 2 - innerWidth / 2}px`); book.style.setProperty("--y", `${rect.top + rect.height / 2 - innerHeight / 2}px`); book.style.setProperty("--scale", rect.width / book.offsetWidth); book.offsetWidth; book.style.removeProperty("transition"); requestAnimationFrame(() => preview.classList.add("is-open")); }; const closeBook = () => { if (preview.hidden) return; const url = new URL(location.href); url.searchParams.delete("knowbookId"); history.replaceState(null, "", url); preview.classList.remove("is-open"); setTimeout(() => { preview.hidden = true; }, 250); }; const change = (id, delta) => { const next = (cart.get(id) || 0) + delta; next > 0 ? cart.set(id, next) : cart.delete(id); refresh(); }; productCatalog.forEach(product => editionDetails(product.status)); grid.innerHTML = statusOrder.flatMap(status => { const stageProducts = productCatalog.filter(product => product.status === status); return stageProducts.length ? [editionDivider(status), ...stageProducts.map(productCard)] : []; }).join(""); const restoreSearch = () => { document.querySelector("#catalog-search").value = new URLSearchParams(location.search).get("q") || ""; filterCatalog(); }; restoreSearch(); window.addEventListener("popstate", restoreSearch); const linkedBook = document.getElementById(new URLSearchParams(location.search).get("knowbookId")); if (linkedBook && !linkedBook.hidden) { document.addEventListener("scrollend", () => openBook(linkedBook.querySelector(".book-cover")), { once: true }); linkedBook.scrollIntoView(); } fitBookTitles(); try { const saved = JSON.parse(localStorage.getItem("knowbooks-cart") || "[]"); saved.forEach(([id, quantity]) => { if (products.has(id) && Number.isInteger(quantity) && quantity > 0) cart.set(id, quantity); }); } catch {} grid.addEventListener("click", event => { const cover = event.target.closest(".book-cover"); const button = event.target.closest("[data-add]"); if (cover) { const url = new URL(location.href); url.searchParams.set("knowbookId", cover.closest(".product").id); history.replaceState(null, "", url); openBook(cover); } if (button) { change(button.dataset.add, 1); openCart(); } }); preview.addEventListener("click", event => { if (event.target === preview || event.target.closest(".preview-close")) closeBook(); }); lines.addEventListener("click", event => { const more = event.target.closest("[data-more]"); const less = event.target.closest("[data-less]"); if (more) change(more.dataset.more, 1); if (less) change(less.dataset.less, -1); }); toggle.addEventListener("click", openCart); drawer.querySelector(".cart-close").addEventListener("click", closeCart); empty.querySelector("a").addEventListener("click", closeCart); scrim.addEventListener("click", closeCart); document.addEventListener("keydown", event => { if (event.key === "Escape") { closeCart(); closeBook(); } }); document.querySelector("#catalog-search").addEventListener("input", filterCatalog); window.addEventListener("resize", fitBookTitles); refresh(); })(); catalog.scroll buildConcepts catalog.js buildZip knowbooks.zip index.html catalog.js knowbooksLogo.svg og.jpeg readme.scroll catalog.scroll covers.scroll