Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion .github/workflows/tag-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
309 changes: 282 additions & 27 deletions assets/account.html

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions assets/authors-nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions assets/authors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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}` } : {},
});
Expand Down
21 changes: 21 additions & 0 deletions assets/fastui/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 23 additions & 0 deletions assets/fastui/NOTICE.md
Original file line number Diff line number Diff line change
@@ -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`.
75 changes: 75 additions & 0 deletions assets/fastui/color-mode.js
Original file line number Diff line number Diff line change
@@ -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 });
})();
61 changes: 61 additions & 0 deletions assets/fastui/controls.css
Original file line number Diff line number Diff line change
@@ -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; }
79 changes: 79 additions & 0 deletions assets/fastui/index.js
Original file line number Diff line number Diff line change
@@ -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
};
21 changes: 21 additions & 0 deletions assets/fastui/provenance.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Loading
Loading