chore(publication): close pre-integration release blockers
@@ -130,9 +130,6 @@ 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'); }
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -13,7 +13,6 @@
|
||||
in email bodies — was wrapping random digits in <a href="tel:..."> with
|
||||
browser-default styling that didn't match the Odysseus theme. -->
|
||||
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
|
||||
<link rel="apple-touch-icon" href="/static/icons/icon-192.png">
|
||||
<script nonce="{{CSP_NONCE}}">
|
||||
window._odysseusLoadTime = Date.now();
|
||||
(function(){
|
||||
@@ -77,9 +76,10 @@
|
||||
}
|
||||
// Apply font early
|
||||
if (t && t.font) {
|
||||
var fm = {gohu:"'GohuFont', monospace",mono:"'Fira Code', monospace",sans:"system-ui, -apple-system, 'Segoe UI', sans-serif",serif:"Georgia, 'Times New Roman', serif",opendyslexic:"'OpenDyslexic', sans-serif"};
|
||||
if (fm[t.font]) { s.setProperty('--font-family', fm[t.font]); }
|
||||
else { s.setProperty('--font-family', "'" + t.font.replace(/'/g,'') + "', sans-serif"); }
|
||||
var font = /^(gohu|GohuFont)$/.test(t.font) ? "mono" : t.font;
|
||||
var fm = {mono:"'Fira Code', monospace",sans:"system-ui, -apple-system, 'Segoe UI', sans-serif",serif:"Georgia, 'Times New Roman', serif",opendyslexic:"'OpenDyslexic', sans-serif"};
|
||||
if (fm[font]) { s.setProperty('--font-family', fm[font]); }
|
||||
else { s.setProperty('--font-family', "'" + font.replace(/'/g,'') + "', sans-serif"); }
|
||||
}
|
||||
// Apply density class on html
|
||||
if (t && t.density && t.density !== 'comfortable') {
|
||||
@@ -656,7 +656,6 @@
|
||||
<div class="theme-fd-group">
|
||||
<label class="theme-fd-label">Font</label>
|
||||
<select id="theme-font-select" class="theme-fd-select" aria-label="Font">
|
||||
<option value="gohu">GohuFont</option>
|
||||
<option value="mono">Monospace</option>
|
||||
<option value="sans">Sans-serif</option>
|
||||
<option value="serif">Serif</option>
|
||||
|
||||
@@ -2083,15 +2083,15 @@ export function _expandModelRow(row, modelData) {
|
||||
const _HWFIT_ENGINE_GLYPHS = {
|
||||
'': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="4" y1="6" x2="20" y2="6"></line><line x1="4" y1="12" x2="20" y2="12"></line><line x1="4" y1="18" x2="20" y2="18"></line><circle cx="8" cy="6" r="2" fill="currentColor" stroke="none"></circle><circle cx="16" cy="12" r="2" fill="currentColor" stroke="none"></circle><circle cx="10" cy="18" r="2" fill="currentColor" stroke="none"></circle></svg>',
|
||||
vllm: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 4l7 16 7-16"></path><path d="M14 4l4 9 3-9"></path></svg>',
|
||||
sglang: '<span aria-hidden="true" style="display:block;width:14px;height:14px;background:currentColor;-webkit-mask:url(/static/icons/sglang-mark.png) center/contain no-repeat;mask:url(/static/icons/sglang-mark.png) center/contain no-repeat;"></span>',
|
||||
sglang: '',
|
||||
mlx: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 18V6l4 7 4-7v12"></path><path d="M16 6v12"></path><path d="M20 6v12"></path></svg>',
|
||||
llamacpp: '<svg width="14" height="14" viewBox="0 0 600 600" fill="none" aria-hidden="true"><path d="M600 392L504.249 558L504.137 557.929C487.252 584.069 458.193 600 426.864 600H120L240 392H600Z" fill="currentColor"></path><path d="M240 392H0L199.602 46.0254C216.032 17.5463 246.411 0 279.29 0H466.154L240 392Z" fill="currentColor"></path></svg>',
|
||||
ollama: '<span aria-hidden="true" style="display:block;width:14px;height:14px;background:currentColor;-webkit-mask:url(/static/icons/ollama-mark-crop.png) center/contain no-repeat;mask:url(/static/icons/ollama-mark-crop.png) center/contain no-repeat;"></span>',
|
||||
ollama: '',
|
||||
diffusers: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M5 19l2-2M17 7l2-2"></path></svg>',
|
||||
};
|
||||
|
||||
function _hwfitEngineGlyph(value) {
|
||||
return _HWFIT_ENGINE_GLYPHS[value] || _HWFIT_ENGINE_GLYPHS[''];
|
||||
return _HWFIT_ENGINE_GLYPHS[value] ?? _HWFIT_ENGINE_GLYPHS[''];
|
||||
}
|
||||
|
||||
const _HWFIT_USECASE_GLYPHS = {
|
||||
|
||||
@@ -1167,12 +1167,12 @@ async function _fetchDependencies() {
|
||||
// icon (the name alone is fine for librosa, hf_transfer, etc.).
|
||||
const _DEP_GLYPHS = {
|
||||
vllm: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 4l7 16 7-16"/><path d="M14 4l4 9 3-9"/></svg>',
|
||||
sglang: '<span aria-hidden="true" style="display:block;width:13px;height:13px;background:currentColor;-webkit-mask:url(/static/icons/sglang-mark.png) center/contain no-repeat;mask:url(/static/icons/sglang-mark.png) center/contain no-repeat;"></span>',
|
||||
sglang: '',
|
||||
mlx_lm: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 18V6l4 7 4-7v12"/><path d="M16 6v12"/><path d="M20 6v12"/></svg>',
|
||||
mflux: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>',
|
||||
boogu_image_mlx: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 17c2.5-4 4.5-4 7 0"/><circle cx="9" cy="9" r="1"/><circle cx="15" cy="9" r="1"/></svg>',
|
||||
llama_cpp: '<svg width="13" height="13" viewBox="0 0 600 600" fill="none" aria-hidden="true"><path d="M600 392L504.249 558L504.137 557.929C487.252 584.069 458.193 600 426.864 600H120L240 392H600Z" fill="currentColor"/><path d="M240 392H0L199.602 46.0254C216.032 17.5463 246.411 0 279.29 0H466.154L240 392Z" fill="currentColor"/></svg>',
|
||||
ollama: '<img src="/static/icons/ollama-mark-crop.png" alt="" aria-hidden="true" width="13" height="13" style="display:block;width:13px;height:13px;object-fit:contain;" />',
|
||||
ollama: '',
|
||||
diffusers: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M5 19l2-2M17 7l2-2"/></svg>',
|
||||
krea_diffusers: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 19V5"/><path d="M4 12h4"/><path d="M12 5l-7 7 7 7"/><path d="M14 19l3-14 3 14"/><path d="M15.3 13h3.4"/></svg>',
|
||||
sam_mask: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7c3-3 13-3 16 0"/><path d="M4 17c3 3 13 3 16 0"/><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3"/></svg>',
|
||||
|
||||
@@ -2210,11 +2210,11 @@ function _rerenderCachedModels() {
|
||||
// custom picker so the dropdown lists "[V] vLLM", "[⚡] SGLang", etc.
|
||||
const _BACKEND_GLYPHS = {
|
||||
vllm: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 4l7 16 7-16"/><path d="M14 4l4 9 3-9"/></svg>',
|
||||
sglang: '<span aria-hidden="true" style="display:block;width:14px;height:14px;background:currentColor;-webkit-mask:url(/static/icons/sglang-mark.png) center/contain no-repeat;mask:url(/static/icons/sglang-mark.png) center/contain no-repeat;"></span>',
|
||||
sglang: '',
|
||||
mlx: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 18V6l4 7 4-7v12"/><path d="M16 6v12"/><path d="M20 6v12"/></svg>',
|
||||
mlx_image: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="9" r="1.5"/><path d="M21 15l-5-5L5 20"/><path d="M17.5 4v4M15.5 6h4"/></svg>',
|
||||
llamacpp: '<svg width="14" height="14" viewBox="0 0 600 600" fill="none" aria-hidden="true"><path d="M600 392L504.249 558L504.137 557.929C487.252 584.069 458.193 600 426.864 600H120L240 392H600Z" fill="currentColor"/><path d="M240 392H0L199.602 46.0254C216.032 17.5463 246.411 0 279.29 0H466.154L240 392Z" fill="currentColor"/></svg>',
|
||||
ollama: '<span aria-hidden="true" style="display:block;width:14px;height:14px;background:currentColor;-webkit-mask:url(/static/icons/ollama-mark-crop.png) center/contain no-repeat;mask:url(/static/icons/ollama-mark-crop.png) center/contain no-repeat;"></span>',
|
||||
ollama: '',
|
||||
diffusers: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M5 19l2-2M17 7l2-2"/></svg>',
|
||||
};
|
||||
|
||||
@@ -2234,7 +2234,7 @@ function _rerenderCachedModels() {
|
||||
const opt = _backendSource?.querySelector(`option[value="${CSS.escape(v)}"]`);
|
||||
const label = opt ? opt.textContent : v;
|
||||
if (_backendBtnLabel) _backendBtnLabel.textContent = label;
|
||||
if (_backendBtnIconSlot) _backendBtnIconSlot.innerHTML = _BACKEND_GLYPHS[v] || _BACKEND_GLYPHS.vllm;
|
||||
if (_backendBtnIconSlot) _backendBtnIconSlot.innerHTML = _BACKEND_GLYPHS[v] ?? _BACKEND_GLYPHS.vllm;
|
||||
}
|
||||
|
||||
function _renderBackendMenu() {
|
||||
@@ -2242,7 +2242,7 @@ function _rerenderCachedModels() {
|
||||
const items = Array.from(_backendSource.options).map(o => ({ value: o.value, label: o.textContent }));
|
||||
_backendMenu.innerHTML = items.map(it => `
|
||||
<button type="button" role="option" class="hwfit-backend-item" data-value="${it.value}" style="all:unset;display:flex;align-items:center;gap:8px;width:100%;padding:6px 9px;border-radius:5px;font-size:12px;cursor:pointer;color:var(--fg);box-sizing:border-box;">
|
||||
<span class="hwfit-backend-item-icon" style="display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;color:var(--accent, var(--red));flex-shrink:0;">${_BACKEND_GLYPHS[it.value] || _BACKEND_GLYPHS.vllm}</span>
|
||||
<span class="hwfit-backend-item-icon" style="display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;color:var(--accent, var(--red));flex-shrink:0;">${_BACKEND_GLYPHS[it.value] ?? _BACKEND_GLYPHS.vllm}</span>
|
||||
<span class="hwfit-backend-item-label" style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${it.label}</span>
|
||||
</button>
|
||||
`).join('');
|
||||
|
||||
@@ -15060,20 +15060,6 @@ import { attachColorPicker } from './colorPicker.js?v=20260910eyedropper1';
|
||||
return _docxReady;
|
||||
}
|
||||
|
||||
let _html2pdfReady = null;
|
||||
function ensureHtml2Pdf() {
|
||||
if (_html2pdfReady) return _html2pdfReady;
|
||||
if (window.html2pdf) return (_html2pdfReady = Promise.resolve());
|
||||
_html2pdfReady = new Promise((resolve, reject) => {
|
||||
const s = document.createElement('script');
|
||||
s.src = '/static/lib/html2pdf.bundle.min.js';
|
||||
s.onload = resolve;
|
||||
s.onerror = () => reject(new Error('Failed to load PDF library'));
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
return _html2pdfReady;
|
||||
}
|
||||
|
||||
function _getExportBaseName() {
|
||||
const doc = docs.get(activeDocId);
|
||||
const title = (doc && doc.title) || 'document';
|
||||
@@ -15282,11 +15268,11 @@ import { attachColorPicker } from './colorPicker.js?v=20260910eyedropper1';
|
||||
// white paper preview; PDF is converted from its extracted text into an
|
||||
// editable DOCX in the browser.
|
||||
// PDF-backed documents already have a lossless filled-PDF export above.
|
||||
// Running the generic html2pdf path would rebuild the extracted text and
|
||||
// Running the generic browser print path would rebuild the extracted text and
|
||||
// destroy the original page layout, images, and form structure.
|
||||
if (!isForm) {
|
||||
if (!isForm && (_isDocxLang(lang) || typeof window.print === 'function')) {
|
||||
options.push({
|
||||
label: _isDocxLang(lang) ? 'Convert to PDF' : 'Print as PDF',
|
||||
label: _isDocxLang(lang) ? 'Convert to PDF' : 'Print / save PDF',
|
||||
fn: _isDocxLang(lang) ? () => convertOriginalDocument('pdf') : exportAsPdf,
|
||||
});
|
||||
}
|
||||
@@ -15449,15 +15435,13 @@ import { attachColorPicker } from './colorPicker.js?v=20260910eyedropper1';
|
||||
if (!activeDocId) return;
|
||||
const textarea = document.getElementById('doc-editor-textarea');
|
||||
if (!textarea) return;
|
||||
try {
|
||||
await ensureHtml2Pdf();
|
||||
} catch (e) {
|
||||
if (uiModule) uiModule.showError('Failed to load PDF library');
|
||||
if (typeof window.print !== 'function') {
|
||||
if (uiModule) uiModule.showError('Browser printing is unavailable.');
|
||||
return;
|
||||
}
|
||||
const lang = document.getElementById('doc-language-select')?.value || '';
|
||||
const text = textarea.value || '';
|
||||
// Render content as HTML for PDF
|
||||
// Render content as HTML for the browser print dialog.
|
||||
let html;
|
||||
if (_isRichTextLang(lang)) {
|
||||
html = text;
|
||||
@@ -15470,28 +15454,57 @@ import { attachColorPicker } from './colorPicker.js?v=20260910eyedropper1';
|
||||
html = '<pre style="white-space:pre-wrap;font-size:11px;font-family:monospace;color:#000;background:#fff;">' +
|
||||
text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') + '</pre>';
|
||||
}
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = 'padding:20px;font-family:sans-serif;font-size:12px;color:#000;background:#fff;line-height:1.6;';
|
||||
container.innerHTML = html;
|
||||
if (_isRichTextLang(lang)) {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = _richTextExportCss();
|
||||
container.prepend(style);
|
||||
// Use a sandboxed same-origin frame: rich text can render, but scripts,
|
||||
// event handlers and embedded plugins cannot execute in the print view.
|
||||
document.getElementById('doc-browser-print-frame')?.remove();
|
||||
const frame = document.createElement('iframe');
|
||||
frame.id = 'doc-browser-print-frame';
|
||||
frame.title = 'Document print view';
|
||||
frame.setAttribute('sandbox', 'allow-same-origin allow-modals');
|
||||
frame.style.cssText = 'position:fixed;left:-10000px;top:0;width:800px;height:600px;border:0;';
|
||||
document.body.appendChild(frame);
|
||||
try {
|
||||
const printDoc = frame.contentDocument;
|
||||
printDoc.open();
|
||||
printDoc.write('<!DOCTYPE html><html><head><meta charset="utf-8"></head><body></body></html>');
|
||||
printDoc.close();
|
||||
printDoc.title = _getExportBaseName();
|
||||
const container = printDoc.createElement('div');
|
||||
container.style.cssText = 'padding:20px;font-family:sans-serif;font-size:12px;color:#000;background:#fff;line-height:1.6;';
|
||||
container.innerHTML = html;
|
||||
if (_isRichTextLang(lang)) {
|
||||
const style = printDoc.createElement('style');
|
||||
style.textContent = _richTextExportCss();
|
||||
container.prepend(style);
|
||||
}
|
||||
// Typeset deferred math before copying it into the print document.
|
||||
await markdownModule.renderMath(container);
|
||||
const styles = [];
|
||||
document.querySelectorAll('link[rel="stylesheet"]').forEach(link => {
|
||||
if (!link.href.includes('/katex/')) return;
|
||||
const copy = printDoc.createElement('link');
|
||||
copy.rel = 'stylesheet';
|
||||
copy.href = link.href;
|
||||
styles.push(new Promise((resolve, reject) => {
|
||||
copy.onload = resolve;
|
||||
copy.onerror = () => reject(new Error('Math styles could not be loaded'));
|
||||
}));
|
||||
printDoc.head.appendChild(copy);
|
||||
});
|
||||
const printStyle = printDoc.createElement('style');
|
||||
printStyle.textContent = '@page { margin: 10mm; } body { margin: 0; } img { max-width: 100%; } pre { white-space: pre-wrap; overflow-wrap: anywhere; }';
|
||||
printDoc.head.appendChild(printStyle);
|
||||
printDoc.body.appendChild(printDoc.importNode(container, true));
|
||||
await Promise.all(styles);
|
||||
await printDoc.fonts.ready;
|
||||
await Promise.all(Array.from(printDoc.images, img => img.decode().catch(() => {})));
|
||||
frame.contentWindow.addEventListener('afterprint', () => frame.remove(), { once: true });
|
||||
frame.contentWindow.focus();
|
||||
frame.contentWindow.print();
|
||||
} catch (error) {
|
||||
frame.remove();
|
||||
if (uiModule) uiModule.showError('Could not open browser print: ' + (error.message || error));
|
||||
}
|
||||
// This container is detached, so the document-scoped flush mdToHtml
|
||||
// schedules never sees it. Typeset the deferred math before html2pdf
|
||||
// rasterises, or the PDF gets raw formula source. renderMath() returns
|
||||
// immediately, without loading KaTeX, when there is nothing pending.
|
||||
await markdownModule.renderMath(container);
|
||||
const baseName = _getExportBaseName();
|
||||
window.html2pdf().set({
|
||||
margin: 10,
|
||||
filename: baseName + '.pdf',
|
||||
image: { type: 'jpeg', quality: 0.95 },
|
||||
html2canvas: { scale: 2 },
|
||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
|
||||
}).from(container).save();
|
||||
if (uiModule) uiModule.showToast('Exporting PDF...');
|
||||
}
|
||||
|
||||
function _docxHexColor(value) {
|
||||
|
||||
@@ -40,13 +40,17 @@ const LS_KEY = 'odysseus-theme';
|
||||
const CUSTOM_THEMES_KEY = 'odysseus-custom-themes';
|
||||
|
||||
const FONT_MAP = {
|
||||
gohu: "'GohuFont', monospace",
|
||||
mono: "'Fira Code', monospace",
|
||||
sans: "system-ui, -apple-system, 'Segoe UI', sans-serif",
|
||||
serif: "Georgia, 'Times New Roman', serif",
|
||||
opendyslexic: "'OpenDyslexic', sans-serif",
|
||||
};
|
||||
const DEFAULT_FONT = 'gohu';
|
||||
const DEFAULT_FONT = 'mono';
|
||||
|
||||
// Compatibility for persisted themes that selected the omitted custom face.
|
||||
function normalizeFont(font) {
|
||||
return !font || font === 'gohu' || font === 'GohuFont' ? DEFAULT_FONT : font;
|
||||
}
|
||||
const DEFAULT_DENSITY = 'comfortable';
|
||||
const MAX_CUSTOM_THEMES = 8;
|
||||
|
||||
@@ -108,7 +112,11 @@ const THEME_DEFAULT_FROSTED = {
|
||||
|
||||
// ── Custom theme persistence ──
|
||||
function _loadCustomThemes() {
|
||||
return Storage.getJSON(CUSTOM_THEMES_KEY, {});
|
||||
const themes = Storage.getJSON(CUSTOM_THEMES_KEY, {});
|
||||
for (const theme of Object.values(themes)) {
|
||||
if (theme && theme.font) theme.font = normalizeFont(theme.font);
|
||||
}
|
||||
return themes;
|
||||
}
|
||||
function _saveCustomThemes(obj) {
|
||||
Storage.setJSON(CUSTOM_THEMES_KEY, obj);
|
||||
@@ -121,7 +129,7 @@ export function saveCustomTheme(name, colors, opts) {
|
||||
}
|
||||
const entry = { ...colors };
|
||||
if (opts) {
|
||||
if (opts.font) entry.font = opts.font;
|
||||
if (opts.font) entry.font = normalizeFont(opts.font);
|
||||
if (opts.density) entry.density = opts.density;
|
||||
if (opts.bgPattern) entry.bgPattern = opts.bgPattern;
|
||||
if (opts.bgEffectColor) entry.bgEffectColor = opts.bgEffectColor;
|
||||
@@ -400,7 +408,7 @@ function _injectFontFace(familyName, variants) {
|
||||
}
|
||||
|
||||
export function applyFontDensity(font, density) {
|
||||
const f = font || DEFAULT_FONT;
|
||||
const f = normalizeFont(font);
|
||||
const d = density || DEFAULT_DENSITY;
|
||||
let family = FONT_MAP[f];
|
||||
if (!family && _customFonts[f]) {
|
||||
@@ -506,13 +514,14 @@ export function getSaved() {
|
||||
obj.name = replacement[obj.name];
|
||||
obj.colors = { ...THEMES[obj.name] };
|
||||
}
|
||||
if (obj && obj.font) obj.font = normalizeFont(obj.font);
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function save(name, colors, opts) {
|
||||
const obj = { name, colors };
|
||||
if (opts) {
|
||||
if (opts.font && opts.font !== DEFAULT_FONT) obj.font = opts.font;
|
||||
if (normalizeFont(opts.font) !== DEFAULT_FONT) obj.font = normalizeFont(opts.font);
|
||||
if (opts.density && opts.density !== DEFAULT_DENSITY) obj.density = opts.density;
|
||||
if (opts.bgPattern && opts.bgPattern !== 'none') obj.bgPattern = opts.bgPattern;
|
||||
if (opts.bgEffectColor) obj.bgEffectColor = opts.bgEffectColor;
|
||||
@@ -757,7 +766,7 @@ export function initThemeUI() {
|
||||
sw.classList.add('active');
|
||||
syncPickers(colors);
|
||||
const ct = sw.dataset.custom ? customThemes[name] : null;
|
||||
const f = ct && ct.font ? ct.font : DEFAULT_FONT;
|
||||
const f = normalizeFont(ct && ct.font);
|
||||
const d = ct && ct.density ? ct.density : DEFAULT_DENSITY;
|
||||
const p = ct && ct.bgPattern ? ct.bgPattern : (THEME_DEFAULT_PATTERN[name] || 'none');
|
||||
const ec = ct && ct.bgEffectColor ? ct.bgEffectColor : (THEME_DEFAULT_EFFECT_COLOR[name] || '');
|
||||
@@ -1146,7 +1155,7 @@ export function initThemeUI() {
|
||||
syncResetButtons();
|
||||
|
||||
// Font, density, background pattern controls
|
||||
const _initFont = (saved && saved.font) || DEFAULT_FONT;
|
||||
const _initFont = normalizeFont(saved && saved.font);
|
||||
const _initDensity = (saved && saved.density) || DEFAULT_DENSITY;
|
||||
const _initPattern = (saved && saved.bgPattern) || (saved && THEME_DEFAULT_PATTERN[saved.name]) || 'none';
|
||||
const _initEffectColor = (saved && saved.bgEffectColor) || (saved && THEME_DEFAULT_EFFECT_COLOR[saved.name]) || '';
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<title>Odysseus — Login</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M16 4L16 22L6 22Z' fill='%23e06c75'/%3E%3Cpath d='M16 8L16 22L24 22Z' fill='%23e06c75' opacity='0.6'/%3E%3Cpath d='M4 24Q10 20 16 24Q22 28 28 24' stroke='%23e06c75' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E">
|
||||
<link rel="manifest" href="static/manifest.json">
|
||||
<link rel="apple-touch-icon" href="static/icons/icon-192.png">
|
||||
<script nonce="{{CSP_NONCE}}">
|
||||
(function(){
|
||||
function computeAppBasePath() {
|
||||
|
||||
@@ -7,10 +7,5 @@
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"background_color": "#282c34",
|
||||
"theme_color": "#282c34",
|
||||
"icons": [
|
||||
{ "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
|
||||
{ "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" },
|
||||
{ "src": "icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||
]
|
||||
"theme_color": "#282c34"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// - Other static assets (images/fonts/libs): cache-first with bg refresh.
|
||||
// - API / non-GET: never cached.
|
||||
// Bump CACHE_NAME whenever the precache list or SW logic changes.
|
||||
const CACHE_NAME = 'odysseus-v656-pr40-editor-precache';
|
||||
const CACHE_NAME = 'odysseus-v657-publication-plan-b';
|
||||
|
||||
// KaTeX resolves these from its own stylesheet, so caching the CSS without them
|
||||
// gives offline math fallback glyphs instead of proper typesetting.
|
||||
|
||||