boko 0.2.0

Fast ebook conversion library for EPUB and Kindle formats
Documentation
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="Convert between EPUB and Kindle formats instantly in your browser. No upload, no server, 100% private.">
    <title>boko - Ebook Converter</title>
    <link rel="modulepreload" href="pkg/boko.js">
    <link rel="preload" href="pkg/boko_bg.wasm" as="fetch" crossorigin>
    <style>
*{margin:0;padding:0;box-sizing:border-box}:root{--primary:#2563eb;--primary-hover:#1d4ed8;--success:#10b981;--error:#ef4444;--bg:#f8fafc;--surface:#fff;--text:#1e293b;--text-muted:#64748b;--border:#e2e8f0;--radius:12px}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column}.container{max-width:480px;margin:0 auto;padding:2rem 1rem;flex:1;display:flex;flex-direction:column}header{text-align:center;margin-bottom:2rem}h1{font-size:2.5rem;font-weight:700;letter-spacing:-.02em}.subtitle{color:var(--text-muted);margin-top:.5rem}main{flex:1}.dropzone{background:var(--surface);border:2px dashed var(--border);border-radius:var(--radius);padding:3rem 2rem;text-align:center;cursor:pointer;transition:border-color .2s,background .2s}.dropzone:hover,.dropzone.dragover{border-color:var(--primary);background:#f0f7ff}.dropzone-content{pointer-events:none}.upload-icon{width:48px;height:48px;color:var(--text-muted);margin-bottom:1rem}.dropzone p{color:var(--text);margin-bottom:.5rem}.browse-link{color:var(--primary);text-decoration:underline;pointer-events:auto;cursor:pointer}.supported-formats{font-size:.875rem;color:var(--text-muted)!important}.file-info{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:1rem 1.25rem;display:flex;align-items:center;justify-content:space-between;margin-top:1rem}.file-details{display:flex;flex-direction:column;gap:.25rem}.file-name{font-weight:500;word-break:break-all}.file-size{font-size:.875rem;color:var(--text-muted)}.clear-btn{background:none;border:none;font-size:1.5rem;color:var(--text-muted);cursor:pointer;padding:.25rem;line-height:1}.clear-btn:hover{color:var(--error)}.conversion-options{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:1.25rem;margin-top:1rem;display:flex;align-items:center;gap:1rem}.conversion-options label{color:var(--text-muted);white-space:nowrap}.conversion-options select{flex:1;padding:.625rem 1rem;border:1px solid var(--border);border-radius:8px;font-size:1rem;background:var(--surface);cursor:pointer}.convert-btn{background:var(--primary);color:#fff;border:none;padding:.625rem 1.5rem;border-radius:8px;font-size:1rem;font-weight:500;cursor:pointer;transition:background .2s}.convert-btn:hover{background:var(--primary-hover)}.convert-btn:disabled{background:var(--border);cursor:not-allowed}.progress{margin-top:1.5rem;text-align:center}.progress-bar{background:var(--border);border-radius:4px;height:8px;overflow:hidden}.progress-fill{background:var(--primary);height:100%;width:0;transition:width .3s;animation:pulse 1.5s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.7}}.progress-text{margin-top:.75rem;color:var(--text-muted);font-size:.875rem}.result{background:var(--surface);border:1px solid var(--success);border-radius:var(--radius);padding:2rem;margin-top:1.5rem;text-align:center}.check-icon{width:48px;height:48px;color:var(--success);margin-bottom:1rem}.result p{margin-bottom:1rem;color:var(--success);font-weight:500}.download-btn{display:inline-block;background:var(--success);color:#fff;text-decoration:none;padding:.75rem 2rem;border-radius:8px;font-weight:500;transition:opacity .2s}.download-btn:hover{opacity:.9}.error{background:#fef2f2;border:1px solid var(--error);border-radius:var(--radius);padding:2rem;margin-top:1.5rem;text-align:center}.error-icon{width:48px;height:48px;color:var(--error);margin-bottom:1rem}.error p{color:var(--error)}footer{text-align:center;padding:2rem 0;color:var(--text-muted);font-size:.875rem}footer a{color:var(--primary);text-decoration:underline}.hidden{display:none!important}@media(max-width:480px){.conversion-options{flex-direction:column;align-items:stretch}.conversion-options label{text-align:center}}
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>boko</h1>
            <p class="subtitle">Convert ebooks in your browser</p>
        </header>

        <main>
            <div id="dropzone" class="dropzone">
                <input type="file" id="file-input" accept=".epub,.azw3,.mobi,.kfx" hidden>
                <div class="dropzone-content">
                    <svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
                        <polyline points="17 8 12 3 7 8"/>
                        <line x1="12" y1="3" x2="12" y2="15"/>
                    </svg>
                    <p>Drop your ebook here or <span class="browse-link">browse</span></p>
                    <p class="supported-formats">Supports EPUB, AZW3, MOBI, KFX</p>
                </div>
            </div>

            <div id="file-info" class="file-info hidden">
                <div class="file-details">
                    <span id="file-name" class="file-name"></span>
                    <span id="file-size" class="file-size"></span>
                </div>
                <button id="clear-file" class="clear-btn" title="Clear file">&times;</button>
            </div>

            <div id="conversion-options" class="conversion-options hidden">
                <label for="output-format">Convert to:</label>
                <select id="output-format">
                    <option value="epub">EPUB</option>
                    <option value="azw3">AZW3</option>
                </select>
                <button id="convert-btn" class="convert-btn">Convert</button>
            </div>

            <div id="progress" class="progress hidden">
                <div class="progress-bar">
                    <div id="progress-fill" class="progress-fill"></div>
                </div>
                <p id="progress-text" class="progress-text">Converting...</p>
            </div>

            <div id="result" class="result hidden">
                <svg class="check-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
                    <polyline points="22 4 12 14.01 9 11.01"/>
                </svg>
                <p>Conversion complete!</p>
                <a id="download-link" class="download-btn" download>Download</a>
            </div>

            <div id="error" class="error hidden">
                <svg class="error-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <circle cx="12" cy="12" r="10"/>
                    <line x1="15" y1="9" x2="9" y2="15"/>
                    <line x1="9" y1="9" x2="15" y2="15"/>
                </svg>
                <p id="error-message"></p>
            </div>
        </main>

        <footer>
            <p>Powered by <a href="https://github.com/zacharydenton/boko">boko</a> &middot; All conversions happen locally in your browser</p>
        </footer>
    </div>

    <script type="module" src="app.js"></script>
</body>
</html>