From 8fda203d28e7f76aafa05960fde96bca90050977 Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Tue, 13 Oct 2026 13:42:33 +0000 Subject: [PATCH] feat(ui): chat frontend, model picker, streaming, admin, a11y Preview/UI work: chat renderer, streaming/think-trace, model picker (instant-open/refresh-backoff), admin panel, a11y, gallery landing. Source: vendor/photocraft (ad1cff44) static files, 3-way merged onto upstream/dev (a8c147b2). Resolution policy in pr_work/D_conflicts.md. Co-Authored-By: Claude Opus 5.5 --- static/css/00-tokens.css | 19 +++ static/css/04-memory.css | 1 + static/css/12-gallery.css | 6 + static/js/a11y.js | 62 +++++++- static/js/calendar.js | 18 +-- static/js/chat.js | 70 ++++++++- static/js/codeRunner.js | 6 +- static/js/colorPicker.js | 15 ++ static/js/cookbook-hwfit.js | 6 +- static/js/cookbook.js | 4 +- static/js/cookbookServe.js | 13 +- static/js/document.js | 228 +++++++++++++++++------------- static/js/documentOutline.js | 1 + static/js/emailLibrary/index.js | 41 +++++- static/js/gallery.js | 104 +++++++++++++- static/js/models.js | 8 +- static/js/notes.js | 8 +- static/js/research/jobs.js | 5 +- static/js/sidebar-layout.js | 3 + static/js/signature.js | 2 +- static/js/slashCommands.js | 27 +++- static/js/streamingRenderer.js | 23 ++- static/js/tasks.js | 2 +- static/js/ui.js | 32 +++++ static/lib/html2pdf.bundle.min.js | 3 + static/lib/qrcode.min.js | 8 ++ 26 files changed, 571 insertions(+), 144 deletions(-) create mode 100644 static/lib/html2pdf.bundle.min.js create mode 100644 static/lib/qrcode.min.js diff --git a/static/css/00-tokens.css b/static/css/00-tokens.css index e502f7670..734303b2f 100644 --- a/static/css/00-tokens.css +++ b/static/css/00-tokens.css @@ -130,11 +130,30 @@ body { @font-face { font-family: 'Fira Code'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-Regular.woff2') format('woff2'); } @font-face { font-family: 'Fira Code'; font-weight: 600; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-SemiBold.woff2') format('woff2'); } +/* GohuFont is the default UI face across themes; keep the shipped font local + so the default is available before custom-font discovery completes. */ +@font-face { font-family: 'GohuFont'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/custom/GohuFont.ttf') format('truetype'); } /* Self-hosted OpenDyslexic — dyslexia-friendly accessibility font option (SIL OFL 1.1) */ @font-face { font-family: 'OpenDyslexic'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/OpenDyslexic-Regular.woff2') format('woff2'); } @font-face { font-family: 'OpenDyslexic'; font-weight: 700; font-style: normal; font-display: swap; src: url('/static/fonts/OpenDyslexic-Bold.woff2') format('woff2'); } +/* Self-hosted editor text fonts (SIL OFL 1.1). These are bundled so the text + tool renders distinct, expected faces on every device — phones (Android/iOS) + do not ship Arial/Verdana/Georgia/Times/Courier/Impact, which previously + collapsed every font choice onto the same system fallback. */ +@font-face { font-family: 'Inter'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/Inter-Regular.woff2') format('woff2'); } +@font-face { font-family: 'Inter'; font-weight: 500; font-style: normal; font-display: swap; src: url('/static/fonts/Inter-Medium.woff2') format('woff2'); } +@font-face { font-family: 'Inter'; font-weight: 600; font-style: normal; font-display: swap; src: url('/static/fonts/Inter-SemiBold.woff2') format('woff2'); } +@font-face { font-family: 'Roboto'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/Roboto-Regular.woff2') format('woff2'); } +@font-face { font-family: 'Roboto'; font-weight: 700; font-style: normal; font-display: swap; src: url('/static/fonts/Roboto-Bold.woff2') format('woff2'); } +@font-face { font-family: 'Playfair Display'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/PlayfairDisplay-Regular.woff2') format('woff2'); } +@font-face { font-family: 'Playfair Display'; font-weight: 700; font-style: normal; font-display: swap; src: url('/static/fonts/PlayfairDisplay-Bold.woff2') format('woff2'); } +@font-face { font-family: 'Lobster'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/Lobster-Regular.woff2') format('woff2'); } +@font-face { font-family: 'Bebas Neue'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/BebasNeue-Regular.woff2') format('woff2'); } +@font-face { font-family: 'Roboto Mono'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/RobotoMono-Regular.woff2') format('woff2'); } +@font-face { font-family: 'Roboto Mono'; font-weight: 700; font-style: normal; font-display: swap; src: url('/static/fonts/RobotoMono-Bold.woff2') format('woff2'); } + /* Code block baseline */ pre, code, .hljs { font-size: 11px; diff --git a/static/css/04-memory.css b/static/css/04-memory.css index 71b2537bd..67cb7ca16 100644 --- a/static/css/04-memory.css +++ b/static/css/04-memory.css @@ -704,6 +704,7 @@ textarea.memory-add-input { #serve-bulk-bar, #gallery-bulk-bar, #gallery-editor-drafts-bulk, +#notes-bulk-bar, #memory-modal .memory-bulk-bar { padding-right: 18px; } diff --git a/static/css/12-gallery.css b/static/css/12-gallery.css index 16f47e863..dad05ec52 100644 --- a/static/css/12-gallery.css +++ b/static/css/12-gallery.css @@ -90,6 +90,12 @@ .lib-tabs, .gallery-tabs, .preset-tabs { + /* A flex-item tab strip defaults to min-width:auto, so it grows to its + content width inside a narrow modal and the end tabs get clipped by the + modal's overflow instead of scrolling. min-width:0 lets the strip shrink + to the container so the overflow-x:auto below actually engages. */ + min-width: 0; + max-width: 100%; flex-wrap: nowrap !important; overflow-x: auto !important; overflow-y: hidden; diff --git a/static/js/a11y.js b/static/js/a11y.js index 814472d94..f44ada39d 100644 --- a/static/js/a11y.js +++ b/static/js/a11y.js @@ -18,7 +18,13 @@ 'use strict'; // Click-as-button rows we want reachable by keyboard. - var ROW_SELECTOR = ['#sidebar .list-item', '#user-bar-profile'].join(','); + var ROW_SELECTOR = [ + '#sidebar .list-item', + '#user-bar-profile', + '.sidebar-brand', + '.export-dropdown-item', + '.dropdown-item' + ].join(','); // Native interactive descendants. If a row contains one of these we must // NOT give the row role="button" — a button inside a button is invalid @@ -55,6 +61,31 @@ (root || document).querySelectorAll(ROW_SELECTOR).forEach(enhanceRow); } + // Settings form labels are written as + // with the control as a sibling/descendant, but are not associated via for/id, + // so screen readers cannot name the inputs. Associate each label with its + // control when the mapping is unambiguous (a single form control per row). + var settingsLabelSeq = 0; + function associateSettingsLabels(root) { + (root || document).querySelectorAll('.settings-label').forEach(function (label) { + if (label.hasAttribute('for')) return; + var control = null; + var sib = label.nextElementSibling; + if (sib && /^(INPUT|SELECT|TEXTAREA)$/.test(sib.tagName)) { + control = sib; + } else { + var row = label.closest('.settings-row') || label.parentElement; + if (row) { + var controls = row.querySelectorAll('input,select,textarea'); + if (controls.length === 1) control = controls[0]; + } + } + if (!control || control.closest('label')) return; + if (!control.id) control.id = 'a11y-settings-' + (++settingsLabelSeq); + if (!control.getAttribute('aria-label')) label.setAttribute('for', control.id); + }); + } + // ---- Modal dialogs ----------------------------------------------------- // Odysseus modals are plain