diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..59913c2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# Preserve byte-addressed assets; recognize CRLF while retaining whitespace checks. +assets/fastui/** -text whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol +# The generated bootstrap records this adapter's source-byte digest. +scripts/theme-init.js -text whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol +# Preserve the complete generated account page used by delivery byte audits. +assets/account.html -text whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol diff --git a/.github/workflows/tag-ci.yml b/.github/workflows/tag-ci.yml index 00dd3a6..2141e1c 100644 --- a/.github/workflows/tag-ci.yml +++ b/.github/workflows/tag-ci.yml @@ -49,7 +49,7 @@ jobs: deploy: needs: validate - if: startsWith(github.ref, 'refs/tags/v') + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') uses: FastR-D/FastDeploy/.github/workflows/deploy.yml@9a2c51dcd63f47a0e6c8f6bb07bd22ea589abbd2 with: app: fastnews diff --git a/assets/account.html b/assets/account.html index 00fa556..694e5c3 100644 --- a/assets/account.html +++ b/assets/account.html @@ -1,27 +1,282 @@ -FastNews · 账号 - -
← 返回报告

登录 FastNews

使用本项目账号登录。邀请注册和账号恢复凭证由实例管理员提供。

-
- - -
- +FastNews · 账号 + + + + +
← 返回报告

登录 FastNews

使用本项目账号登录。邀请注册和账号恢复凭证由实例管理员提供。

+
+ + +
+ diff --git a/assets/authors-nav.js b/assets/authors-nav.js index 2a40789..ddd7724 100644 --- a/assets/authors-nav.js +++ b/assets/authors-nav.js @@ -39,7 +39,7 @@ function applySidebar(collapsed) { document.documentElement.classList.toggle("sidebar-collapsed", collapsed); - window.localStorage.setItem("fastnews.sidebarCollapsed", collapsed ? "1" : "0"); + try { window.localStorage.setItem("fastnews.sidebarCollapsed", collapsed ? "1" : "0"); } catch { /* optional preference storage */ } if (sidebarToggle) { sidebarToggle.title = collapsed ? "打开总览栏" : "隐藏总览栏"; sidebarToggle.setAttribute("aria-label", sidebarToggle.title); @@ -59,7 +59,7 @@ applySidebar(document.documentElement.classList.contains("sidebar-collapsed")); } - fetch("/api/content/me", { credentials: "include" }) + fetch(window.fastNewsApi("/api/content/me"), { credentials: "include" }) .then((response) => (response.ok ? response.json() : null)) .then((payload) => { if (!payload) return; @@ -77,7 +77,7 @@ }) .catch(() => {}); - fetch("/api/content/inbox", { credentials: "include" }) + fetch(window.fastNewsApi("/api/content/inbox"), { credentials: "include" }) .then((response) => (response.ok ? response.json() : null)) .then((payload) => { if (!payload) return; diff --git a/assets/authors.js b/assets/authors.js index 510ef20..8343025 100644 --- a/assets/authors.js +++ b/assets/authors.js @@ -101,7 +101,7 @@ async function persistRemote() { if (!researchSession) return; try { - await fetch("/api/content/authors", { + await fetch(window.fastNewsApi("/api/content/authors"), { method: "PUT", credentials: "include", headers: { @@ -145,14 +145,14 @@ } async function fetchCookieSession() { - const response = await fetch("/api/content/me", { credentials: "include" }); + const response = await fetch(window.fastNewsApi("/api/content/me"), { credentials: "include" }); const payload = await response.json().catch(() => ({})); if (!response.ok || !(payload.keyId || payload.userId)) return null; return sessionFromPayload("", payload); } async function fetchRemoteAuthors() { - const response = await fetch("/api/content/authors", { + const response = await fetch(window.fastNewsApi("/api/content/authors"), { credentials: "include", headers: researchSession.session ? { Authorization: `Bearer ${researchSession.session}` } : {}, }); diff --git a/assets/fastui/LICENSE b/assets/fastui/LICENSE new file mode 100644 index 0000000..47c4d66 --- /dev/null +++ b/assets/fastui/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 FastR-D contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/assets/fastui/NOTICE.md b/assets/fastui/NOTICE.md new file mode 100644 index 0000000..59d1ef6 --- /dev/null +++ b/assets/fastui/NOTICE.md @@ -0,0 +1,23 @@ +# Source and publication scope + +The owner has authorized publication of the generic UI portions under MIT. +Vue semantic primitives and v-model adapters derive from the common +`primitives.ts` (203 lines) in FastPPT and FastFigure. Modal's public props and +the focus-lifecycle purpose derive from their common `use-dialog-focus.ts` +(58 lines) and Modal component. FastUI delegates focus trapping, inertness, +nesting and focus restoration to the browser's native modal dialog rather +than maintaining the original hand-written trap. Select retains the shared +value/options/change contract and delegates interaction to the native select. + +The palette follows FastWrite's semantic colors and the approved FastTask +workbench design. Only generic UI code and semantic design values are included. +No private artwork, icons, logos, fonts, application data, product-specific +workflows or brand assets are distributed. Upstream product repositories +retain ownership of their application-specific code and assets. + +Source reference revisions: FastPPT `f6bf21f13445c2507719928fa68550552a8fcab2` +(`packages/components/src/`) and FastFigure +`13dcc4179fd0073da76f7626264af8605d5e8c70` (`src/components/ui/`). +At both revisions, `primitives.ts` has Git blob +`c7123af5f8316bc0486bce5c99cfbb2e9ab3dc73` and `use-dialog-focus.ts` has +Git blob `3ffbb6c6c7bd5e3c96f2b8c05c5e008dcdfc4cab`. diff --git a/assets/fastui/color-mode.js b/assets/fastui/color-mode.js new file mode 100644 index 0000000..02a627b --- /dev/null +++ b/assets/fastui/color-mode.js @@ -0,0 +1,75 @@ +"use strict"; +(() => { + // packages/foundation/src/index.ts + var parse = (value) => value === "light" || value === "dark" ? value : "system"; + function initColorMode({ storageKey: storageKey2, target = document.documentElement }) { + const win = target.ownerDocument.defaultView; + const media = win.matchMedia("(prefers-color-scheme: dark)"); + let mode = "system"; + try { + mode = parse(win.localStorage.getItem(storageKey2)); + } catch { + } + let disposed = false; + const listeners = /* @__PURE__ */ new Set(); + const resolved = () => mode === "system" ? media.matches ? "dark" : "light" : mode; + const apply = () => { + target.dataset.fastuiTheme = resolved(); + listeners.forEach((listener) => listener({ mode, resolved: resolved() })); + }; + const systemChange = () => { + if (mode === "system") apply(); + }; + const storageChange = (event) => { + let storage; + try { + storage = win.localStorage; + } catch { + return; + } + if (event.storageArea !== storage || event.key !== storageKey2 && event.key !== null) return; + mode = parse(event.newValue); + apply(); + }; + media.addEventListener("change", systemChange); + win.addEventListener("storage", storageChange); + apply(); + return { + get mode() { + return mode; + }, + get resolved() { + return resolved(); + }, + setMode(value) { + if (disposed) return; + mode = parse(value); + try { + win.localStorage.setItem(storageKey2, mode); + } catch { + } + apply(); + }, + subscribe(listener) { + if (disposed) return () => { + }; + listeners.add(listener); + listener({ mode, resolved: resolved() }); + return () => { + listeners.delete(listener); + }; + }, + dispose() { + disposed = true; + media.removeEventListener("change", systemChange); + win.removeEventListener("storage", storageChange); + listeners.clear(); + } + }; + } + + // packages/foundation/src/bootstrap.ts + var storageKey = document.currentScript.dataset.storageKey; + if (!storageKey) throw new Error("FastUI color-mode.js requires data-storage-key on its script element."); + initColorMode({ storageKey }); +})(); diff --git a/assets/fastui/controls.css b/assets/fastui/controls.css new file mode 100644 index 0000000..fd85a23 --- /dev/null +++ b/assets/fastui/controls.css @@ -0,0 +1,61 @@ +/* Every appearance rule is explicitly opted into by a fastui class. No reset. */ +.fastui-button, .fastui-input, .fastui-textarea, .fastui-select { + box-sizing: border-box; + font-family: var(--fastui-font); + font-size: 13px; + line-height: 1.5; + min-height: var(--fastui-height-md); + padding: 7px 10px; + border: 1px solid var(--fastui-border-strong); + border-radius: var(--fastui-radius-control); + background: var(--fastui-surface); + color: var(--fastui-text); + max-width: 100%; + min-width: 0; + color-scheme: var(--fastui-color-scheme); +} +.fastui-button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer; font-weight: 500; text-decoration: none; } +.fastui-button--sm, .fastui-input--sm, .fastui-select--sm { min-height: var(--fastui-height-sm); padding: 4px 8px; font-size: 12px; } +.fastui-button--xs, .fastui-input--xs, .fastui-select--xs { min-height: var(--fastui-height-xs); padding: 1px 6px; font-size: 12px; } +.fastui-button:hover:not(:disabled) { background: var(--fastui-selected); } +.fastui-button--primary { background: var(--fastui-primary); border-color: var(--fastui-primary); color: var(--fastui-on-primary); } +.fastui-button--primary:hover:not(:disabled) { background: var(--fastui-primary-hover); } +.fastui-button--danger { background: var(--fastui-danger-fill); border-color: var(--fastui-danger-fill); color: var(--fastui-on-primary); } +.fastui-button--danger:hover:not(:disabled) { filter: brightness(.9); background: var(--fastui-danger-fill); } +.fastui-button--quiet, .fastui-button--icon, .fastui-button--navigation { background: transparent; border-color: transparent; } +.fastui-button--icon { aspect-ratio: 1; padding: 5px; } +.fastui-button--navigation { border-radius: 0; } +.fastui-button[aria-pressed=true], .fastui-button[aria-selected=true] { box-shadow: inset 0 -2px var(--fastui-focus); } +.fastui-button:disabled, .fastui-input:disabled, .fastui-textarea:disabled, .fastui-select:disabled { opacity: .6; cursor: not-allowed; } +.fastui-button:focus-visible, .fastui-input:focus-visible, .fastui-textarea:focus-visible, .fastui-select:focus-visible, .fastui-link:focus-visible, .fastui-disclosure-trigger:focus-visible { outline: 2px solid var(--fastui-focus); outline-offset: 2px; } +.fastui-input::placeholder, .fastui-textarea::placeholder { color: var(--fastui-muted); } +.fastui-input[aria-invalid=true], .fastui-textarea[aria-invalid=true] { border-color: var(--fastui-danger); } +.fastui-input[type=checkbox], .fastui-input[type=radio], .fastui-input[type=range] { min-height: auto; padding: 0; accent-color: var(--fastui-primary); } +.fastui-textarea { resize: vertical; } +.fastui-field { display: grid; gap: 6px; color: var(--fastui-secondary); font: 13px/1.5 var(--fastui-font); } +.fastui-fieldset { border: 1px solid var(--fastui-border); border-radius: var(--fastui-radius-panel); min-width: 0; padding: 16px; } +.fastui-legend { color: var(--fastui-secondary); padding: 0 4px; } +.fastui-heading { color: var(--fastui-text); font-weight: 600; } +.fastui-link { color: var(--fastui-focus); text-underline-offset: 3px; } +.fastui-progress { accent-color: var(--fastui-primary); } +.fastui-table { border-collapse: collapse; text-align: left; } +.fastui-table :where(th, td) { padding: 12px; border-bottom: 1px solid var(--fastui-border); } +.fastui-table th { background: var(--fastui-raised); font-weight: 600; } +.fastui-disclosure-trigger { padding: 5px 8px; cursor: pointer; } +.fastui-dialog { box-sizing: border-box; width: min(620px, calc(100% - 32px)); max-height: calc(100dvh - 32px); overflow: auto; padding: 0; border: 1px solid var(--fastui-border); border-radius: var(--fastui-radius-panel); background: var(--fastui-surface); color: var(--fastui-text); font: 14px/1.5 var(--fastui-font); box-shadow: 0 16px 48px #0004; } +.fastui-dialog--lg { width: min(880px, calc(100% - 32px)); } +.fastui-dialog--xl { width: min(1100px, calc(100% - 32px)); } +.fastui-dialog::backdrop { background: var(--fastui-overlay); } +.fastui-dialog > :where(header, footer), .fastui-modal-body { padding: 20px 24px; } +.fastui-dialog > header { border-bottom: 1px solid var(--fastui-border); } +.fastui-dialog > footer { display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid var(--fastui-border); } +.fastui-modal-body { display: grid; gap: 16px; } +.fastui-alert, .fastui-badge, .fastui-empty, .fastui-loading { font-family: var(--fastui-font); color: var(--fastui-text); } +.fastui-alert { border: 1px solid var(--fastui-border); border-left: 3px solid var(--fastui-tone, var(--fastui-focus)); border-radius: var(--fastui-radius-control); padding: 12px 16px; background: var(--fastui-surface); } +.fastui-badge { display: inline-block; border: 1px solid currentColor; color: var(--fastui-tone, var(--fastui-focus)); border-radius: 4px; padding: 1px 6px; font-size: 12px; } +.fastui-tone-success { --fastui-tone: var(--fastui-success); } +.fastui-tone-warning { --fastui-tone: var(--fastui-warning); } +.fastui-tone-danger { --fastui-tone: var(--fastui-danger); } +.fastui-empty { padding: 24px; text-align: center; } +.fastui-empty > h2 { font-size: 16px; } +.fastui-loading { display: inline-flex; align-items: center; gap: 8px; } diff --git a/assets/fastui/index.js b/assets/fastui/index.js new file mode 100644 index 0000000..e1ae2b4 --- /dev/null +++ b/assets/fastui/index.js @@ -0,0 +1,79 @@ +// packages/foundation/src/index.ts +var parse = (value) => value === "light" || value === "dark" ? value : "system"; +function initColorMode({ storageKey, target = document.documentElement }) { + const win = target.ownerDocument.defaultView; + const media = win.matchMedia("(prefers-color-scheme: dark)"); + let mode = "system"; + try { + mode = parse(win.localStorage.getItem(storageKey)); + } catch { + } + let disposed = false; + const listeners = /* @__PURE__ */ new Set(); + const resolved = () => mode === "system" ? media.matches ? "dark" : "light" : mode; + const apply = () => { + target.dataset.fastuiTheme = resolved(); + listeners.forEach((listener) => listener({ mode, resolved: resolved() })); + }; + const systemChange = () => { + if (mode === "system") apply(); + }; + const storageChange = (event) => { + let storage; + try { + storage = win.localStorage; + } catch { + return; + } + if (event.storageArea !== storage || event.key !== storageKey && event.key !== null) return; + mode = parse(event.newValue); + apply(); + }; + media.addEventListener("change", systemChange); + win.addEventListener("storage", storageChange); + apply(); + return { + get mode() { + return mode; + }, + get resolved() { + return resolved(); + }, + setMode(value) { + if (disposed) return; + mode = parse(value); + try { + win.localStorage.setItem(storageKey, mode); + } catch { + } + apply(); + }, + subscribe(listener) { + if (disposed) return () => { + }; + listeners.add(listener); + listener({ mode, resolved: resolved() }); + return () => { + listeners.delete(listener); + }; + }, + dispose() { + disposed = true; + media.removeEventListener("change", systemChange); + win.removeEventListener("storage", storageChange); + listeners.clear(); + } + }; +} +function syncDialog(element, open) { + if (open && !element.open) { + const descendants = Array.from(element.querySelectorAll("dialog:modal")); + for (const child of [...descendants].reverse()) child.close(); + element.showModal(); + for (const child of descendants) child.showModal(); + } else if (!open && element.open) element.close(); +} +export { + initColorMode, + syncDialog +}; diff --git a/assets/fastui/provenance.json b/assets/fastui/provenance.json new file mode 100644 index 0000000..9662720 --- /dev/null +++ b/assets/fastui/provenance.json @@ -0,0 +1,21 @@ +{ + "package": "@fastrd/ui-foundation", + "version": "0.1.2", + "url": "https://registry.npmjs.org/@fastrd/ui-foundation/-/ui-foundation-0.1.2.tgz", + "sha256": "c3dabbad16980242f66db07619b161889d0ee2492daaa4aaddee1583bc7bcd0b", + "files": { + "tokens.css": "fadf2cf99aa54150ff65776032c4f61b007ebe47a0051723f81afac0b397de38", + "controls.css": "f34fa4d48fbae9dcbf22fc7fcb1718e2b1a5cb33c36bc287805fd49b61cecbc4", + "index.js": "5085eb867b23e1531bd512bd55f1ba87da551f74bc2008e38a45ab748940c201", + "color-mode.js": "596e123cf27bf280dec4cabafe98dfbcf780fbef546763c8c27a0c28e6c52f99" + }, + "generated_by": "python scripts/supply_fastui.py", + "generated": { + "theme-init.js": { + "sha256": "b060ada12d03ff1940c0deceff35a07bf6312874fba1d90a49063e7ded71a5d7", + "foundation_source": "index.js", + "adapter": "scripts/theme-init.js", + "adapter_sha256": "978165ab783b8b5f127fa1c73fe2ca2e2fe02e617210465b1ef5c36ef13ef53f" + } + } +} diff --git a/assets/fastui/theme-init.js b/assets/fastui/theme-init.js new file mode 100644 index 0000000..70c20d5 --- /dev/null +++ b/assets/fastui/theme-init.js @@ -0,0 +1,93 @@ +// Generated from @fastrd/ui-foundation 0.1.2; do not edit. +(() => { +// packages/foundation/src/index.ts +var parse = (value) => value === "light" || value === "dark" ? value : "system"; +function initColorMode({ storageKey, target = document.documentElement }) { + const win = target.ownerDocument.defaultView; + const media = win.matchMedia("(prefers-color-scheme: dark)"); + let mode = "system"; + try { + mode = parse(win.localStorage.getItem(storageKey)); + } catch { + } + let disposed = false; + const listeners = /* @__PURE__ */ new Set(); + const resolved = () => mode === "system" ? media.matches ? "dark" : "light" : mode; + const apply = () => { + target.dataset.fastuiTheme = resolved(); + listeners.forEach((listener) => listener({ mode, resolved: resolved() })); + }; + const systemChange = () => { + if (mode === "system") apply(); + }; + const storageChange = (event) => { + let storage; + try { + storage = win.localStorage; + } catch { + return; + } + if (event.storageArea !== storage || event.key !== storageKey && event.key !== null) return; + mode = parse(event.newValue); + apply(); + }; + media.addEventListener("change", systemChange); + win.addEventListener("storage", storageChange); + apply(); + return { + get mode() { + return mode; + }, + get resolved() { + return resolved(); + }, + setMode(value) { + if (disposed) return; + mode = parse(value); + try { + win.localStorage.setItem(storageKey, mode); + } catch { + } + apply(); + }, + subscribe(listener) { + if (disposed) return () => { + }; + listeners.add(listener); + listener({ mode, resolved: resolved() }); + return () => { + listeners.delete(listener); + }; + }, + dispose() { + disposed = true; + media.removeEventListener("change", systemChange); + win.removeEventListener("storage", storageChange); + listeners.clear(); + } + }; +} +function syncDialog(element, open) { + if (open && !element.open) { + const descendants = Array.from(element.querySelectorAll("dialog:modal")); + for (const child of [...descendants].reverse()) child.close(); + element.showModal(); + for (const child of descendants) child.showModal(); + } else if (!open && element.open) element.close(); +} + +// This adapter is bundled after the verified foundation ESM by supply_fastui.py. +// The resulting classic script runs before gate.js and before page content. +try { + if (localStorage.getItem('fastresearch-theme') === null) { + const old = localStorage.getItem('fastnews.theme'); + if (old === 'light' || old === 'dark') localStorage.setItem('fastresearch-theme', old); + } +} catch { /* Preference storage is optional. */ } +const colorMode = initColorMode({ storageKey: 'fastresearch-theme' }); +colorMode.subscribe(({ resolved }) => { + document.documentElement.classList.toggle('dark', resolved === 'dark'); +}); +window.fastNewsColorMode = colorMode; + +})(); diff --git a/assets/fastui/tokens.css b/assets/fastui/tokens.css new file mode 100644 index 0000000..7008b75 --- /dev/null +++ b/assets/fastui/tokens.css @@ -0,0 +1,98 @@ +/* Generated from tokens.json. */ +:root { + --fastui-font: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans CJK SC', sans-serif; + --fastui-font-size: 14px; + --fastui-radius-control: 6px; + --fastui-radius-panel: 8px; + --fastui-space-1: 4px; + --fastui-space-2: 8px; + --fastui-space-3: 12px; + --fastui-space-4: 16px; + --fastui-space-6: 24px; + --fastui-space-8: 32px; + --fastui-height-xs: 24px; + --fastui-height-sm: 30px; + --fastui-height-md: 38px; + --fastui-on-primary: #ffffff; + --fastui-on-primary-rgb: 255, 255, 255; +} +:root, [data-fastui-theme="light"] { + --fastui-page: #f7f8fa; + --fastui-page-rgb: 247, 248, 250; + --fastui-workspace: #eef0f3; + --fastui-workspace-rgb: 238, 240, 243; + --fastui-surface: #ffffff; + --fastui-surface-rgb: 255, 255, 255; + --fastui-raised: #eef0f3; + --fastui-raised-rgb: 238, 240, 243; + --fastui-text: #151c28; + --fastui-text-rgb: 21, 28, 40; + --fastui-secondary: #4b5565; + --fastui-secondary-rgb: 75, 85, 101; + --fastui-muted: #687384; + --fastui-muted-rgb: 104, 115, 132; + --fastui-border: #dfe3e8; + --fastui-border-rgb: 223, 227, 232; + --fastui-border-strong: #a7b0bd; + --fastui-border-strong-rgb: 167, 176, 189; + --fastui-primary: #155fb8; + --fastui-primary-rgb: 21, 95, 184; + --fastui-primary-hover: #104a91; + --fastui-primary-hover-rgb: 16, 74, 145; + --fastui-focus: #155fb8; + --fastui-focus-rgb: 21, 95, 184; + --fastui-selected: #e5effb; + --fastui-selected-rgb: 229, 239, 251; + --fastui-disabled: #e8ebef; + --fastui-disabled-rgb: 232, 235, 239; + --fastui-success: #16834a; + --fastui-success-rgb: 22, 131, 74; + --fastui-warning: #8c5b09; + --fastui-warning-rgb: 140, 91, 9; + --fastui-danger: #c93636; + --fastui-danger-rgb: 201, 54, 54; + --fastui-danger-fill: #c93636; + --fastui-danger-fill-rgb: 201, 54, 54; + --fastui-overlay: #151c2880; + --fastui-color-scheme: light; +} +[data-fastui-theme="dark"] { + --fastui-page: #111820; + --fastui-page-rgb: 17, 24, 32; + --fastui-workspace: #111820; + --fastui-workspace-rgb: 17, 24, 32; + --fastui-surface: #171e27; + --fastui-surface-rgb: 23, 30, 39; + --fastui-raised: #273342; + --fastui-raised-rgb: 39, 51, 66; + --fastui-text: #f4f7fa; + --fastui-text-rgb: 244, 247, 250; + --fastui-secondary: #c5cfdb; + --fastui-secondary-rgb: 197, 207, 219; + --fastui-muted: #9aa8b9; + --fastui-muted-rgb: 154, 168, 185; + --fastui-border: #354252; + --fastui-border-rgb: 53, 66, 82; + --fastui-border-strong: #65758a; + --fastui-border-strong-rgb: 101, 117, 138; + --fastui-primary: #0e639c; + --fastui-primary-rgb: 14, 99, 156; + --fastui-primary-hover: #1177bb; + --fastui-primary-hover-rgb: 17, 119, 187; + --fastui-focus: #5ba3ea; + --fastui-focus-rgb: 91, 163, 234; + --fastui-selected: #094771; + --fastui-selected-rgb: 9, 71, 113; + --fastui-disabled: #273342; + --fastui-disabled-rgb: 39, 51, 66; + --fastui-success: #6acc96; + --fastui-success-rgb: 106, 204, 150; + --fastui-warning: #e6ba6a; + --fastui-warning-rgb: 230, 186, 106; + --fastui-danger: #ff848c; + --fastui-danger-rgb: 255, 132, 140; + --fastui-danger-fill: #b03030; + --fastui-danger-fill-rgb: 176, 48, 48; + --fastui-overlay: #00000099; + --fastui-color-scheme: dark; +} \ No newline at end of file diff --git a/assets/gate.js b/assets/gate.js index b2bcd0f..8489ce3 100644 --- a/assets/gate.js +++ b/assets/gate.js @@ -39,7 +39,7 @@ bounce(); return; } - fetch("/api/content/me", { credentials: "include" }) + fetch(window.fastNewsApi("/api/content/me"), { credentials: "include" }) .then((response) => (response.ok ? response.json() : Promise.reject())) .then((payload) => { if (!payload || !(payload.keyId || payload.userId)) throw new Error("no session"); diff --git a/assets/impression.js b/assets/impression.js index d7db95b..3ae8c03 100644 --- a/assets/impression.js +++ b/assets/impression.js @@ -61,7 +61,7 @@ async function loadImpression() { try { - const response = await fetch("/api/content/impression", { credentials: "include" }); + const response = await fetch(window.fastNewsApi("/api/content/impression"), { credentials: "include" }); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(payload.error || "load failed"); const impression = payload.impression || {}; @@ -78,8 +78,8 @@ submit.disabled = true; setStatus("正在保存…", "pending"); try { - const profile = await fetch("/api/content/me", {credentials:"include"}).then(r => r.json()); - const response = await fetch("/api/content/impression", { + const profile = await fetch(window.fastNewsApi("/api/content/me"), {credentials:"include"}).then(r => r.json()); + const response = await fetch(window.fastNewsApi("/api/content/impression"), { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json", ...(profile.csrf ? {"X-CSRF-Token":profile.csrf} : {}) }, diff --git a/assets/inbox.js b/assets/inbox.js index a4e771e..688d32e 100644 --- a/assets/inbox.js +++ b/assets/inbox.js @@ -76,8 +76,8 @@ } async function markRead(ids) { try { - const profile = await fetch("/api/content/me", {credentials:"include"}).then(r => r.json()); - const response = await fetch("/api/content/inbox", { + const profile = await fetch(window.fastNewsApi("/api/content/me"), {credentials:"include"}).then(r => r.json()); + const response = await fetch(window.fastNewsApi("/api/content/inbox"), { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json", ...(profile.csrf ? {"X-CSRF-Token":profile.csrf} : {}) }, diff --git a/assets/report.css b/assets/report.css new file mode 100644 index 0000000..e349a6a --- /dev/null +++ b/assets/report.css @@ -0,0 +1,43 @@ +/* Publications own their paper palette. No application theme, storage or OS + preference can change the document body, either on screen or on paper. */ +html.fastnews-report { + color-scheme: light; + --app-bg: #ffffff; + --muted-bg: #eef0f3; + --surface: #ffffff; + --surface-muted: #f7f8fa; + --soft: #f7f8fa; + --border: #dfe3e8; + --input-border: #687384; + --primary: #155fb8; + --primary-deep: #104a91; + --primary-soft: #eef0f3; + --accent: #8c5b09; + --accent-soft: #fff8e6; + --accent-border: #dfe3e8; + --accent-text: #8c5b09; + --text: #151c28; + --muted: #4b5565; + --subtle: #687384; +} +.fastnews-report body { + background: #ffffff; + color: #151c28; + font-family: Inter, "Noto Sans SC", "Microsoft YaHei", system-ui, sans-serif; +} +.fastnews-report :is(.surface, .paper, .article, .sidebar, .topbar) { + background: #ffffff; + border-color: #dfe3e8; + box-shadow: none; +} +.fastnews-report :is(.paper-summary, .article-intro, .article-summary) { color: #151c28; } +.fastnews-report :is(.paper-title a, .article-title a) { color: #155fb8; } +.fastnews-report .toc-list a:hover { background: #eef0f3; } +.fastnews-report :is(.brand-mark, .header-icon) { border-color: #dfe3e8; } +.fastnews-report .badge { background: #fff8e6; border-color: #dfe3e8; color: #8c5b09; } +@media print { + html.fastnews-report { color-scheme: light; } + .fastnews-report body { background: #ffffff; color: #151c28; } + .fastnews-report :is(.sidebar, .topbar, .mobile-toc, .paper-actions, .fastread-open) { display: none; } + .fastnews-report :is(.paper-summary, .article-intro, .article-summary) { color: #151c28; } +} diff --git a/assets/theme.js b/assets/theme.js new file mode 100644 index 0000000..1be2f5f --- /dev/null +++ b/assets/theme.js @@ -0,0 +1,14 @@ +// Startup has already resolved the first paint. No second media/storage owner. +const colorMode = window.fastNewsColorMode; +const button = document.getElementById('themeToggle'); +function applyTheme() { + const dark = colorMode.resolved === 'dark'; + if (!button) return; + button.querySelector('.theme-sun').hidden = dark; + button.querySelector('.theme-moon').hidden = !dark; + button.title = dark ? '切换浅色模式' : '切换深色模式'; + button.setAttribute('aria-label', button.title); +} +applyTheme(); +colorMode.subscribe(applyTheme); +button?.addEventListener('click', () => colorMode.setMode(colorMode.resolved === 'dark' ? 'light' : 'dark')); diff --git a/assets/workbench.css b/assets/workbench.css new file mode 100644 index 0000000..d1440ac --- /dev/null +++ b/assets/workbench.css @@ -0,0 +1,22 @@ +/* Host semantic slots; shared package is the only editable palette. */ +html[data-fastui-theme] { + --app-bg: var(--fastui-page); --muted-bg: var(--fastui-workspace); + --surface: var(--fastui-surface); --soft: var(--fastui-workspace); + --surface-muted: var(--fastui-workspace); + --border: var(--fastui-border); --input-border: var(--fastui-border-strong); + --primary: var(--fastui-primary); --primary-deep: var(--fastui-primary-hover); + --primary-soft: var(--fastui-selected); --primary-border: var(--fastui-border-strong); + --text: var(--fastui-text); --muted: var(--fastui-secondary); --subtle: var(--fastui-muted); + --danger: var(--fastui-danger); --danger-soft: var(--fastui-danger-fill); --focus: var(--fastui-focus); + --author-panel: var(--fastui-surface); --author-muted: var(--fastui-secondary); + --author-line: var(--fastui-border); --author-chip: var(--fastui-selected); --author-error: var(--fastui-danger); +} +html[data-fastui-theme] .button-primary { + background: var(--fastui-primary); color: var(--fastui-on-primary); border-color: var(--fastui-primary); box-shadow: none; +} +html[data-fastui-theme] .button-primary:hover { background: var(--fastui-primary-hover); } +html[data-fastui-theme] .surface { border-radius: 8px; box-shadow: none; } +html[data-fastui-theme] body { background: var(--fastui-page); color: var(--fastui-text); } +html[data-fastui-theme] :is(.brand-mark, .sidebar-logo) { background: var(--fastui-primary); color: var(--fastui-on-primary); } +html[data-fastui-theme] :is(button, a, input, textarea, select):focus-visible { outline: 2px solid var(--fastui-focus); outline-offset: 2px; } +html[data-fastui-theme] #fastnews-account-link { background: var(--fastui-surface) !important; color: var(--fastui-focus) !important; border-color: var(--fastui-border) !important; } diff --git a/docs/FASTUI.md b/docs/FASTUI.md new file mode 100644 index 0000000..136fdde --- /dev/null +++ b/docs/FASTUI.md @@ -0,0 +1,22 @@ +# FastUI 0.1.2 + +构建时执行,生成资产与页面一同部署: + +```sh +python scripts/supply_fastui.py +python generate_homepage.py +``` + +离线构建可将第一条改为 `python scripts/supply_fastui.py --artifact /path/to/ui-foundation-0.1.2.tgz`。两种入口校验同一个固定 SHA-256;`assets/fastui/provenance.json` 记录版本、官方 npm tarball URL、包摘要和逐文件摘要。该目录是生成物,升级时从公开 registry metadata 确认实际 tarball URL 并下载核准版本和摘要,再改供应脚本并重新构建;不编辑供应的 CSS/JS。运行时只请求本站资产,无 npm/CDN。 + +主页模板生成根页面和 top-conf、secnews、field-briefing、summary-brief、inbox、impression 七个入口。head 从 `asset_prefix` 同步加载经典脚本 `fastui/theme-init.js`,先迁移旧 key 再由唯一 foundation controller 初始化首屏;`gate.js` 放行内容前颜色已解析。`assets/theme.js` 为 defer 脚本,只连接同一 controller 的按钮/图标,不再初始化或监听系统/storage。 + +沿用 `fastresearch-theme`,缺失时一次性读取旧 `fastnews.theme`;保留 `light/dark/system`,不双写,不由 pageshow 维护第二套主题。供应器将已校验的 foundation `index.js` 去掉固定 ESM export list,再与 `scripts/theme-init.js` 适配代码封装成经典入口;源文件与生成文件摘要都进入 provenance。不会手工 vendor 或修改 foundation 实现。 + +会议/资讯报告是固定浅色出版文档:`assets/report.css` 单独拥有纸张、正文和打印颜色;screen chrome 使用同一蓝灰出版配色,报告不加载站点 workbench CSS、foundation 颜色 bootstrap,也不读取读者的 storage/OS 主题。模板已删除旧的深色 media/绿色 console 色板,输出内嵌出版 CSS;主页生成器为既有19份归档移除上一轮主题注入并内嵌相同出版边界,保留正文与布局。打印为白底深字,禁用 JS 仍成立。 + +本变更没有新增归档对外部 UI 包的运行依赖;历史 FastRead CSS/JS 链接仍存在,因此不将整个下载 HTML 宣称为完全自包含。正文和报告数据不会进入 FastUI 包。 + +账号页源模板为 `prompt/account.html`,供应脚本生成 `assets/account.html` 并内嵌颜色资产;private 模式认证前无需请求被登录保护的 `/assets` 文件。修改账号界面应改源模板后重新执行供应脚本。 + +主题/打印回归:设置 `PYTHONPATH=.` 后执行 `python tests/theme_print_browser.py /path/to/chromium /path/to/evidence`(需要 Playwright、WeasyPrint、PyMuPDF 和平台 Pango)。覆盖延迟应用脚本首屏、同一 controller 的按钮、OS/保存偏好相反组合、模板/真实归档 screen/print、Git HEAD 对照、全部归档打印及真正 WeasyPrint PDF/像素/文本。Windows 可通过 `WEASYPRINT_DLL_DIRECTORIES` 指向已有 Pango/GTK DLL。 diff --git a/docs/LOCAL-ACCOUNTS.md b/docs/LOCAL-ACCOUNTS.md index 7a94782..2422fdd 100644 --- a/docs/LOCAL-ACCOUNTS.md +++ b/docs/LOCAL-ACCOUNTS.md @@ -4,7 +4,9 @@ FastNews 的本地登录、邀请注册和恢复独立于 FastCAS。FastCAS 只 ## 本地运行 -使用 `uv sync` 安装本地账号依赖,启用 FastCAS 时使用 `uv sync --extra fastcas` 安装锁定的 PyPI FastCAS SDK 0.1.2。服务器数据目录由 `FASTNEWS_DATA_DIR` 指定,默认 `.fastnews-data`;SQLite 保存稳定用户 UUID、密码哈希、摘要会话、一次性凭证和个人内容。新目录权限 0700,数据库 0600。 +使用 `uv sync --locked` 安装本地账号及默认 dev 依赖;运行部署可用 `uv sync --locked --no-dev`。启用 FastCAS 时使用 `uv sync --locked --extra fastcas`,从 PyPI 安装固定发行版 `fastcas-sdk==0.1.2`(导入名 `fastcas`),完整 uv.lock 记录制品摘要,不需要相邻 SDK checkout。关闭 CAS 的部署不需要此 extra。服务器数据目录由 `FASTNEWS_DATA_DIR` 指定,默认 `.fastnews-data`;SQLite 保存稳定用户 UUID、密码哈希、摘要会话、一次性凭证和个人内容。新目录权限 0700,数据库 0600。 + +浏览器/打印回归环境用 `uv sync --locked --extra fastcas --group browser`,额外安装 Playwright 与 PyMuPDF;它们仅在 browser 依赖组,不属于运行时 dependencies。测试使用已有 Chromium 时直接传其路径;需要 Playwright 管理的浏览器时另执行 `uv run --locked --group browser python -m playwright install chromium`。WeasyPrint 的平台 Pango/GTK 运行库仍需由宿主提供;安装 Python 依赖不等于这些系统库已安装。主题/打印场景入口为 `PYTHONPATH=. uv run --locked --group browser python tests/theme_print_browser.py <证据目录>`;该场景详见 `FASTUI.md`,本次依赖迁移不修改测试脚本。 管理员命令: diff --git a/fastui_report.py b/fastui_report.py new file mode 100644 index 0000000..9ed1a24 --- /dev/null +++ b/fastui_report.py @@ -0,0 +1,37 @@ +"""Publication styling is independent from application controls and color mode.""" +from pathlib import Path +import re + +ROOT = Path(__file__).resolve().parent +START = '' +END = '' + + +def supply_archived_reports(): + """Keep archived body/layout, replace the former site-theme injection. + + The publication CSS is embedded so this change adds no external runtime + dependency to downloaded archives. Existing FastRead links are preserved. + """ + style = (ROOT / 'assets/report.css').read_text(encoding='utf-8') + for directory in (Path('top-conf/data/report'), Path('secnews/data/report')): + for path in sorted(directory.glob('*.html')): + content = path.read_text(encoding='utf-8') + if '' not in content: + continue + content = re.sub(re.escape(START) + r'.*?' + re.escape(END) + r'\s*', '', content, flags=re.S) + content = re.sub(r']*href="[^"]*(?:fastui/(?:tokens|controls)\.css|workbench\.css)"[^>]*>\s*', '', content) + content = re.sub(r']*src="[^"]*fastui/color-mode\.js"[^>]*>\s*', '', content) + # These inventoried archives have a plain root. + if not re.search(r']*class="[^"]*\bfastnews-report\b', content): + content = re.sub(r'(]*)(>)', r'\1 class="fastnews-report"\2', content, count=1) + block = START + '\n\n' + END + '\n' + updated = content.replace('', block + '', 1) + if updated != path.read_text(encoding='utf-8'): + path.write_text(updated, encoding='utf-8') + + +def report_assets(): + # Report templates intentionally receive no foundation tokens or bootstrap: + # print/PDF must not depend on JS or the reader's application preference. + return {'report_css': (ROOT / 'assets/report.css').read_text(encoding='utf-8')} diff --git a/field-briefing/index.html b/field-briefing/index.html index 8747c2b..592d62c 100644 --- a/field-briefing/index.html +++ b/field-briefing/index.html @@ -10,37 +10,37 @@ + + + + @@ -311,6 +522,7 @@ +
  • 安全资讯3
  • 关注作者0
  • - +
    @@ -365,7 +577,7 @@ -
    更新于 2026-09-25 12:03 UTC
    +
    更新于 2026-11-10 17:38 UTC
    - +
    @@ -365,7 +577,7 @@ -
    更新于 2026-09-25 12:03 UTC
    +
    更新于 2026-11-10 17:38 UTC
    - +
    @@ -365,7 +577,7 @@ -
    更新于 2026-09-25 12:03 UTC
    +
    更新于 2026-11-10 17:38 UTC
    - +
    @@ -365,7 +577,7 @@ -
    更新于 2026-09-25 12:03 UTC
    +
    更新于 2026-11-10 17:38 UTC
    - +
    @@ -365,7 +577,7 @@ -
    更新于 2026-09-25 12:03 UTC
    +
    更新于 2026-11-10 17:38 UTC
    - +
    @@ -365,7 +577,7 @@ -
    更新于 2026-09-25 12:03 UTC
    +
    更新于 2026-11-10 17:38 UTC
    - +
    @@ -365,7 +577,7 @@ -
    更新于 2026-09-25 12:03 UTC
    +
    更新于 2026-11-10 17:38 UTC