作品在列表里新建,落盘产物可在侧栏直接改。

顶栏挂上价目提示和直聊入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-14 08:29:57 +08:00
co-authored by Cursor
parent b59ccf976c
commit e7a4b6c954
4 changed files with 473 additions and 39 deletions
+326 -20
View File
@@ -928,6 +928,10 @@ function wireDictateProductMenu() {
tagEl?.getAttribute("title")?.trim() ||
tagEl?.textContent?.trim() ||
"产物";
if (action === "edit") {
beginDictateProductEdit(card);
return;
}
if (action === "peek") {
openContentPeek({
title,
@@ -1606,12 +1610,34 @@ export function renderSkillPicker(_view, _onPick) {
}
let activeRailTab = "books";
/** 用户手动展开侧栏后,在本会话保持展开,直到再点收起 */
let railUserExpanded = false;
const RAIL_EXPANDED_KEY = "wa-rail-user-expanded";
const COORD_COLLAPSED_KEY = "wa-coord-rail-collapsed";
function readBoolStore(key, fallback) {
try {
const v = localStorage.getItem(key);
if (v === "1") return true;
if (v === "0") return false;
} catch {
/* ignore */
}
return fallback;
}
function writeBoolStore(key, value) {
try {
localStorage.setItem(key, value ? "1" : "0");
} catch {
/* ignore */
}
}
/** 左侧栏:可收起时是否由用户展开(localStorage 记忆) */
let railUserExpanded = readBoolStore(RAIL_EXPANDED_KEY, false);
/** 创作右侧栏:history=此前对话;products=转述产物 */
let coordRailMode = "history";
/** 创作验收右侧栏:默认收起(对话);转述看产物时默认展开 */
let coordRailCollapsed = true;
/** 右侧栏是否收起(localStorage 记忆;默认收起) */
let coordRailCollapsed = readBoolStore(COORD_COLLAPSED_KEY, true);
function syncCoordRailChrome() {
const rail = document.getElementById("coord-rail");
@@ -1658,6 +1684,7 @@ function scrollCoordDrawerToLatest() {
function setCoordRailCollapsed(collapsed) {
const opening = coordRailCollapsed && !collapsed;
coordRailCollapsed = Boolean(collapsed);
writeBoolStore(COORD_COLLAPSED_KEY, coordRailCollapsed);
syncCoordRailChrome();
if (opening) {
requestAnimationFrame(() => scrollCoordDrawerToLatest());
@@ -1724,6 +1751,7 @@ function wireRailChrome() {
expand.dataset.wired = "1";
expand.addEventListener("click", () => {
railUserExpanded = true;
writeBoolStore(RAIL_EXPANDED_KEY, true);
syncRailCollapse();
});
}
@@ -1733,6 +1761,7 @@ function wireRailChrome() {
collapse.addEventListener("click", () => {
if (document.body.dataset.railCanCollapse !== "1") return;
railUserExpanded = false;
writeBoolStore(RAIL_EXPANDED_KEY, false);
syncRailCollapse();
});
}
@@ -1768,8 +1797,9 @@ function syncRailCollapse(view) {
export function resetRailChrome() {
activeRailTab = "books";
// 根目录必须展开左栏;记忆仍留在 localStorage,进作品后恢复
railUserExpanded = true;
coordRailCollapsed = true;
coordRailCollapsed = readBoolStore(COORD_COLLAPSED_KEY, true);
document.body.dataset.railCanCollapse = "0";
document.body.classList.remove(
"rail-collapsed",
@@ -1779,7 +1809,7 @@ export function resetRailChrome() {
const rail = document.getElementById("coord-rail");
if (rail) {
rail.hidden = true;
rail.dataset.collapsed = "1";
rail.dataset.collapsed = coordRailCollapsed ? "1" : "0";
}
setRailTab("books");
wireRailChrome();
@@ -1852,9 +1882,9 @@ function maybeSyncRail(view) {
}
const started = hasStartedCreation(view);
const wasCollapsible = document.body.dataset.railCanCollapse === "1";
// 刚进入创作:默认缩进;未开写:展开并停在作品
// 刚进入可收起:恢复浏览器记忆,无记忆则默认收起
if (started && !wasCollapsible) {
railUserExpanded = false;
railUserExpanded = readBoolStore(RAIL_EXPANDED_KEY, false);
}
if (!started) {
railUserExpanded = true;
@@ -2889,7 +2919,16 @@ function renderContextFragmentHtml(doc, opts = {}) {
}
}
const body = frag.正文 != null ? frag.正文 : frag.body;
let body = frag.正文 != null ? frag.正文 : frag.body;
if (
frag.体验锚定 != null &&
body &&
typeof body === "object" &&
!Array.isArray(body) &&
body.体验锚定 == null
) {
body = { ...body, 体验锚定: frag.体验锚定 };
}
if (typeof body === "string" && body.trim()) {
parts.push(
`<section class="artifact-block"><h4>正文</h4>${renderProseHtml(body)}</section>`,
@@ -3147,13 +3186,32 @@ function renderSpecialtyBodyHtml(body, skill) {
if (html) return html;
}
if (
body.既成机制 != null ||
body.支撑点 != null ||
(Array.isArray(body.依据的核心体验) && body.覆盖检验 != null) ||
skillName.includes("世界法则") ||
skillName.includes("实现机制")
) {
const html = renderMechanismBodyHtml(body);
if (html) return html;
}
const storySkill =
skillName.includes("故事推进") && !skillName.includes("叙事指南");
const aestheticsShape =
skillName.includes("美学") ||
body.美学纲领 != null ||
body.体验内核 != null ||
body.呈现要点 != null;
if (
!aestheticsShape &&
(storySkill ||
body.用户输入用法 != null ||
body.单轮篇幅 != null ||
(body.交互范式 != null && body.推进方式 != null))
) {
const html = renderStoryProgressionBodyHtml(body);
if (html) return html;
}
if (
body.风格与写法 != null ||
body.推进与决策 != null ||
@@ -3193,8 +3251,11 @@ function renderSpecialtyBodyHtml(body, skill) {
body = normalizeAestheticsBody(body);
const productKeys = [
{ key: "美学纲领", missing: "(未写)" },
{ key: "交互范式", missing: "(未写)" },
{ key: "体验锚定", missing: "(未写)" },
];
if (body.交互范式 != null && body.交互范式 !== "") {
productKeys.push({ key: "交互范式", missing: "(未写)" });
}
const evalKeys = [
{
key: body.背景与规则 == null && body.设定逻辑 != null ? "设定逻辑" : "背景与规则",
@@ -3337,7 +3398,60 @@ function renderReplyFormatBodyHtml(body) {
return `<div class="artifact-friendly artifact-reply-format">${parts.join("")}</div>`;
}
/** 叙事指南 · 一份全文结构化卡(不强调双投影裁剪) */
/** 故事推进与交互范式:左权限,右推进合同 */
function renderStoryProgressionBodyHtml(body) {
if (!body || typeof body !== "object" || Array.isArray(body)) return "";
const interaction = body.交互范式;
const hasInteraction = interaction != null && interaction !== "";
const order = [
"推进方式",
"用户输入用法",
"驱动力",
"成败与难度",
"变量钩子",
"选用偏好",
"单轮篇幅",
"扩写要求",
"续写要求",
"OOC处理",
"主角设定偏好",
];
const used = new Set(["交互范式", ...order]);
const right = [];
const pushBlock = (key, value) => {
right.push(`<section class="af-block af-block--extra" data-af-block="${esc(key)}">
<header class="af-block-head"><h3 class="af-block-title">${esc(key)}</h3></header>
<div class="af-block-body">${renderStructuredValueHtml(value, 0)}</div>
</section>`);
};
for (const key of order) {
if (body[key] == null || body[key] === "") continue;
pushBlock(key, body[key]);
}
for (const [key, value] of Object.entries(body)) {
if (used.has(key) || value == null || value === "") continue;
pushBlock(key, value);
}
const interactionHtml = hasInteraction
? renderAestheticsNodeHtml(interaction, "交互范式", 0)
: `<p class="ws-muted">(未写)</p>`;
return `<div class="af-split af-split--progression">
<div class="af-split-grid">
<div class="af-split-product" aria-label="交互范式">
<section class="af-block af-block--product" data-af-block="交互范式">
<header class="af-block-head">
<h3 class="af-block-title">交互范式${hasInteraction ? "" : `<span class="af-block-miss">缺</span>`}</h3>
</header>
<div class="af-block-body">${interactionHtml}</div>
</section>
</div>
<div class="af-split-progress" aria-label="推进合同">
${right.join("") || `<p class="ws-muted">(推进合同未写)</p>`}
</div>
</div>
</div>`;
}
function renderNarrativeBodyHtml(body) {
const order = [
"依据的体验",
@@ -3841,9 +3955,59 @@ function renderWorldBlueprintBodyHtml(body) {
return parts.length ? `<div class="skill-view skill-view-world-blueprint">${parts.join("")}</div>` : "";
}
/** 实现机制 · 专用正文卡 */
/** 世界法则 · 专用正文卡(旧稿字段 法则内容 / 支撑点 仍能显示) */
function renderMechanismBodyHtml(body) {
const parts = [];
const laws = Array.isArray(body.既成机制) ? body.既成机制 : [];
if (laws.length) {
parts.push(
skillSection(
"既成机制",
`<div class="skill-entity-grid">${laws
.map((item) => {
if (!item || typeof item !== "object") return "";
const law = item.法则 || item.法则内容 || "";
const chains = Array.isArray(item.连锁)
? item.连锁
: Array.isArray(item.因果影响)
? item.因果影响
: [];
const bounds = Array.isArray(item["代价与边界"])
? item["代价与边界"]
: Array.isArray(item["边界或例外"])
? item["边界或例外"]
: [];
const scale = item.标尺 ? String(item.标尺) : "";
return `<article class="skill-entity-card">
<header class="skill-entity-head"><div class="skill-entity-name">${esc(String(item.名称 || "?"))}</div></header>
${law ? `<p>${esc(String(law))}</p>` : ""}
${chains.length ? skillProseList(chains.map((line) => String(line))) : ""}
${bounds.length ? `<p class="ws-muted">${esc(bounds.map((line) => String(line)).join(";"))}</p>` : ""}
${scale ? `<p class="ws-muted">${esc(clampPreviewText(scale, 240))}</p>` : ""}
</article>`;
})
.join("")}</div>`,
),
);
}
const joint = Array.isArray(body.共同作用) ? body.共同作用 : [];
if (joint.length) {
parts.push(
skillSection(
"共同作用",
`<ul class="artifact-list">${joint
.map((item) => {
if (typeof item === "string") return `<li>${esc(item)}</li>`;
if (!item || typeof item !== "object") return "";
const who = Array.isArray(item.涉及机制) ? item.涉及机制.join(" · ") : "";
return `<li><strong>${esc(String(item.关系 || "关系"))}</strong>${who ? ` · ${esc(who)}` : ""}${
item.推演影响 ? `<div class="ws-muted">${esc(String(item.推演影响))}</div>` : ""
}</li>`;
})
.join("")}</ul>`,
),
);
}
if (Array.isArray(body.依据的核心体验) && body.依据的核心体验.length) {
parts.push(skillSection("依据的核心体验", skillProseList(body.依据的核心体验)));
}
@@ -3890,7 +4054,14 @@ function renderMechanismBodyHtml(body) {
if (body.覆盖检验 && typeof body.覆盖检验 === "object") {
parts.push(skillSection("覆盖检验", skillKvBlock(body.覆盖检验)));
}
const used = new Set(["依据的核心体验", "支撑点", "支撑点关系", "覆盖检验"]);
const used = new Set([
"依据的核心体验",
"支撑点",
"支撑点关系",
"覆盖检验",
"既成机制",
"共同作用",
]);
for (const [k, v] of Object.entries(body)) {
if (used.has(k) || v == null || v === "") continue;
parts.push(skillSection(k, renderStructuredValueHtml(v, 0)));
@@ -6089,7 +6260,7 @@ function renderDictateProductsList(view) {
if (!items.length) {
return `<div class="dictate-products-empty-block">
<p class="dictate-products-empty-title">尚无产物</p>
<p class="dictate-products-empty-desc">Agent 落盘后会出现在这里。点击行展开,右键可查看全文或复制。</p>
<p class="dictate-products-empty-desc">Agent 落盘后会出现在这里。点击行展开后可直接修改,右键也可查看全文或复制。</p>
</div>`;
}
return `<div class="dictate-products-list" role="list">${items
@@ -6105,7 +6276,7 @@ function renderDictateProductsList(view) {
.replace(/^设计\./, "")
.replace(/^用户\./, "")
.replace(/#/g, " · ");
return `<div class="dictate-product dictate-product--rail" role="listitem" data-dictate-product>
return `<div class="dictate-product dictate-product--rail" role="listitem" data-dictate-product data-product-tag="${esc(tag)}">
<button type="button" class="dictate-product-sum" aria-expanded="false">
<span class="dictate-product-sum-top">
<span class="dictate-product-tag" title="${esc(tag)}">${esc(shortTag || tag || "未命名")}</span>
@@ -6130,15 +6301,102 @@ function renderDictateProductsList(view) {
.join("")}</div>`;
}
/** 正在改的那一行在刷新时先留着,保存成功后再按这条 tag 重新展开 */
let openDictateProductTag = "";
let dictateProductSaveLock = false;
function hydrateDictateProductBody(card) {
if (!card) return;
if (!card || card.classList.contains("is-editing")) return;
const body = card.querySelector(".dictate-product-body");
if (!body || body.dataset.pending !== "1") return;
const content = readDictateProductContent(card);
body.innerHTML = renderStoredProductHtml(content);
body.innerHTML = `<div class="dictate-product-tools"><button type="button" class="btn-sm" data-dictate-edit>修改</button></div>${renderStoredProductHtml(content)}`;
delete body.dataset.pending;
}
function beginDictateProductEdit(card) {
if (!card) return;
if (card.classList.contains("is-editing")) {
card.querySelector("textarea")?.focus();
return;
}
const body = card.querySelector(".dictate-product-body");
if (!body) return;
const root = card.closest(".dictate-products-list") || card.parentElement;
root?.querySelectorAll("[data-dictate-product].is-open").forEach((other) => {
if (other !== card) setDictateProductOpen(other, false);
});
setDictateProductOpen(card, true);
const content = readDictateProductContent(card);
card.classList.add("is-editing");
openDictateProductTag = card.dataset.productTag || "";
body.hidden = false;
delete body.dataset.pending;
body.innerHTML = `<div class="dictate-product-edit">
<textarea class="dictate-product-ta" spellcheck="false" aria-label="产物全文"></textarea>
<div class="dictate-product-edit-actions">
<button type="button" class="btn-sm btn-primary" data-dictate-save>保存</button>
<button type="button" class="btn-sm" data-dictate-cancel>取消</button>
<span class="dictate-product-edit-status" aria-live="polite"></span>
</div>
</div>`;
const ta = body.querySelector("textarea");
if (ta) {
ta.value = content;
ta.focus();
ta.setSelectionRange(0, 0);
}
}
function cancelDictateProductEdit(card) {
if (!card) return;
card.classList.remove("is-editing");
const body = card.querySelector(".dictate-product-body");
if (body) {
body.dataset.pending = "1";
body.innerHTML = "";
}
hydrateDictateProductBody(card);
}
async function saveDictateProductEdit(card) {
const tag = card?.dataset.productTag?.trim() || "";
const sessionId =
document.getElementById("coord-drawer-body")?.dataset.sessionId || "";
const ta = card?.querySelector("textarea");
const status = card?.querySelector(".dictate-product-edit-status");
const saveBtn = card?.querySelector("[data-dictate-save]");
if (!tag || !sessionId || !ta) {
if (status) status.textContent = "无法保存";
return;
}
if (saveBtn) saveBtn.disabled = true;
if (status) status.textContent = "保存中";
dictateProductSaveLock = true;
try {
const res = await fetch(
`/api/sessions/${encodeURIComponent(sessionId)}/board`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tag, content: ta.value }),
},
);
const data = await res.json();
if (!res.ok) throw new Error(data.error || "写入失败");
card.classList.remove("is-editing");
openDictateProductTag = tag;
dictateProductSaveLock = false;
window.dispatchEvent(
new CustomEvent("wa:session-updated", { detail: data }),
);
} catch (err) {
dictateProductSaveLock = false;
if (saveBtn) saveBtn.disabled = false;
if (status) status.textContent = err instanceof Error ? err.message : "写入失败";
}
}
function setDictateProductOpen(card, open) {
if (!card) return;
const sum = card.querySelector(".dictate-product-sum");
@@ -6153,19 +6411,53 @@ function wireDictateProductAccordion(root) {
if (!root || root.dataset.dictateProductAccordionWired) return;
root.dataset.dictateProductAccordionWired = "1";
root.addEventListener("click", (e) => {
const editBtn = e.target.closest("[data-dictate-edit]");
if (editBtn && root.contains(editBtn)) {
beginDictateProductEdit(editBtn.closest("[data-dictate-product]"));
return;
}
const saveBtn = e.target.closest("[data-dictate-save]");
if (saveBtn && root.contains(saveBtn)) {
void saveDictateProductEdit(saveBtn.closest("[data-dictate-product]"));
return;
}
const cancelBtn = e.target.closest("[data-dictate-cancel]");
if (cancelBtn && root.contains(cancelBtn)) {
cancelDictateProductEdit(cancelBtn.closest("[data-dictate-product]"));
return;
}
const sum = e.target.closest(".dictate-product-sum");
if (!sum || !root.contains(sum)) return;
// 右键菜单用 pointer,这里只处理左键展开
if (e.button != null && e.button !== 0) return;
const card = sum.closest("[data-dictate-product]");
if (!card) return;
if (root.querySelector("[data-dictate-product].is-editing")) return;
e.preventDefault();
const willOpen = !card.classList.contains("is-open");
root.querySelectorAll("[data-dictate-product].is-open").forEach((other) => {
if (other !== card) setDictateProductOpen(other, false);
});
if (willOpen) openDictateProductTag = card.dataset.productTag || "";
else if (openDictateProductTag === (card.dataset.productTag || "")) {
openDictateProductTag = "";
}
setDictateProductOpen(card, willOpen);
});
root.addEventListener("keydown", (e) => {
const ta = e.target.closest?.(".dictate-product-ta");
if (!ta || !root.contains(ta)) return;
const card = ta.closest("[data-dictate-product]");
if (e.key === "Escape") {
e.preventDefault();
cancelDictateProductEdit(card);
return;
}
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
void saveDictateProductEdit(card);
}
});
}
function fillCoordRailProducts(view) {
@@ -6180,6 +6472,14 @@ function fillCoordRailProducts(view) {
}
coordRail.hidden = false;
if (drawerBody) {
drawerBody.dataset.sessionId = view.id || "";
if (
dictateProductSaveLock ||
drawerBody.querySelector("[data-dictate-product].is-editing")
) {
syncCoordRailChrome();
return;
}
drawerBody.innerHTML = renderDictateProductsList(view);
const cards = drawerBody.querySelectorAll(".dictate-product");
cards.forEach((el, i) => {
@@ -6187,6 +6487,13 @@ function fillCoordRailProducts(view) {
});
wireDictateProductContextMenu(drawerBody);
wireDictateProductAccordion(drawerBody);
if (openDictateProductTag) {
const reopen = [...cards].find(
(el) => el.dataset.productTag === openDictateProductTag,
);
if (reopen) setDictateProductOpen(reopen, true);
else openDictateProductTag = "";
}
}
if (drawerCount) {
drawerCount.textContent = String(items.length);
@@ -6552,7 +6859,7 @@ export function renderMessageFeed(view, loading, handlers = {}) {
document.body.dataset.designSurface = surface;
syncCoordRailChrome();
} else {
document.body.classList.remove("coord-rail-collapsed");
// 勿在此清掉收起 class:游玩/对话每次重绘都会走这里,清掉会把右侧栏弹开
delete document.body.dataset.designSurface;
}
@@ -6727,8 +7034,7 @@ export function renderMessageFeed(view, loading, handlers = {}) {
// —— 游玩 / 转述对话流 ——
if (isPlayView(view) && coordRail) {
document.body.classList.add("play-workspace");
coordRailCollapsed = false;
document.body.classList.remove("coord-rail-collapsed");
// 收起状态跟 localStorage / 用户点击,不在每次发消息重绘时强行展开
fillPlayAux(view, handlers, { busy: loading });
syncCoordRailChrome();
}
+16 -6
View File
@@ -2602,6 +2602,18 @@ function renderBookContents(book, list) {
}
}
function appendBookListAdd(list) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "book-list-add";
btn.id = "btn-book-list-add";
btn.title = "新建";
btn.setAttribute("aria-label", "新建作品");
btn.textContent = "+";
btn.addEventListener("click", () => openNewBookDialog());
list.appendChild(btn);
}
function renderBookList() {
const list = $("book-list");
if (!list) return;
@@ -2643,11 +2655,9 @@ function renderBookList() {
if (!modeBooks.length) {
const empty = document.createElement("p");
empty.className = "sidebar-empty";
empty.innerHTML = `此方式下暂无作品<br><button type="button" class="btn-sm" id="btn-new-inline">+ 新建</button>`;
empty.textContent = "此方式下暂无作品";
list.appendChild(empty);
$("btn-new-inline")?.addEventListener("click", () => {
openNewBookDialog({ presetMode: modeId === "recipe" ? "dictate" : modeId });
});
appendBookListAdd(list);
return;
}
@@ -2676,6 +2686,7 @@ function renderBookList() {
}),
);
}
appendBookListAdd(list);
if (enterPickerBookId && sidebarNav.level === "mode") {
const row = list.querySelector(`[data-book-id="${enterPickerBookId}"]`);
if (row) showEnterPicker(enterPickerBookId, row);
@@ -2791,7 +2802,7 @@ function setupBookBoxSelect() {
list.addEventListener("mousedown", (e) => {
if (sidebarNav.level !== "mode" || e.button !== 0) return;
if (e.target.closest("#book-action-menu")) return;
if (e.target.closest(".explorer-row")) return;
if (e.target.closest(".explorer-row, .book-list-add")) return;
dragging = true;
start = { x: e.clientX, y: e.clientY };
overlay.hidden = true;
@@ -4417,7 +4428,6 @@ $("lifecycle-toggle")?.addEventListener("click", (e) => {
if (stage === "play") enterPlayNow();
else if (stage === "design") void setLifecycle("design");
});
$("btn-new-book")?.addEventListener("click", () => openNewBookDialog());
$("btn-cancel-new")?.addEventListener("click", () => $("dialog-new-book").close());
$("book-enter-pop")?.addEventListener("click", (e) => {
e.stopPropagation();
+4 -1
View File
@@ -33,10 +33,12 @@
<body data-lifecycle="design" data-nav-level="root">
<div class="layout">
<div class="main-chrome" id="main-chrome">
<div id="price-watch" class="price-watch" hidden></div>
<header class="main-head" id="main-head">
<div class="main-head-left" aria-hidden="true"></div>
<nav class="main-app-nav" aria-label="应用">
<div id="persona-picker-host" class="persona-picker-host" hidden></div>
<a href="/chat.html">直聊</a>
<a href="/modules.html">技能</a>
<a href="/libraries.html">库</a>
<a href="/stats.html">Token</a>
@@ -110,7 +112,6 @@
</ol>
</nav>
<div class="panel-head-actions rail-head-actions">
<button type="button" class="btn-sm" id="btn-new-book" title="新建">+</button>
<span class="burst-badge" id="burst-badge" hidden></span>
<button type="button" class="btn-sm rail-collapse" id="btn-rail-collapse" title="收起侧栏" aria-label="收起侧栏">‹</button>
</div>
@@ -230,6 +231,7 @@
</menu>
<menu id="dictate-product-menu" class="context-menu" hidden>
<button type="button" class="context-menu-item" data-dictate-product-action="edit">修改</button>
<button type="button" class="context-menu-item" data-dictate-product-action="peek">查看完整内容</button>
<button type="button" class="context-menu-item" data-dictate-product-action="copy">复制全文</button>
</menu>
@@ -336,5 +338,6 @@
</dialog>
<script type="module" src="/app.js?v=20261007-view-raw"></script>
<script type="module" src="/price-watch.js"></script>
</body>
</html>
+127 -12
View File
@@ -237,7 +237,6 @@ body.rail-collapsed .rail-body { display: none; }
body:not([data-nav-level="root"]) .rail-tabs {
display: flex;
}
body:not([data-nav-level="root"]) .rail-head-actions #btn-new-book,
body[data-nav-level="root"] .burst-badge { display: none; }
.rail-tabs[hidden] { display: none !important; }
.rail-tab {
@@ -290,17 +289,6 @@ body[data-nav-level="root"] .burst-badge { display: none; }
flex-shrink: 0;
min-width: 0;
}
.rail-head-actions #btn-new-book {
width: 28px;
height: 28px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 1;
flex-shrink: 0;
}
.burst-badge {
max-width: 52px;
overflow: hidden;
@@ -2485,6 +2473,31 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
font-size: 12px; line-height: 1.35; position: relative;
min-width: 0;
}
.book-list-add {
display: flex;
align-items: center;
width: 100%;
min-height: 32px;
margin: 2px 0 0;
padding: 7px 8px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--text);
font-size: 18px;
font-weight: 500;
line-height: 1;
text-align: left;
cursor: pointer;
}
.book-list-add:hover {
background: var(--bg);
color: var(--accent);
}
.book-list-add:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.book-box-overlay {
position: fixed; z-index: 160; pointer-events: none;
border: 1px solid #3b82f6; background: rgba(59, 130, 246, 0.12);
@@ -3886,6 +3899,19 @@ body.design-workspace .af-mosaic {
}
.af-block-body {
min-width: 0;
overflow-wrap: anywhere;
}
.af-split--progression .af-split-grid {
grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.15fr);
}
.af-split-progress {
min-width: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.af-panel {
overflow-wrap: anywhere;
}
/* 美学卡:阅读层级 — 内核最大,范式次之,右栏更密更沉 */
@@ -7840,6 +7866,46 @@ menu.context-menu[hidden],
border-top: none;
cursor: auto;
}
.dictate-product--rail.is-editing .dictate-product-body {
max-height: min(70vh, 520px);
}
.dictate-product--rail .dictate-product-tools {
display: flex;
justify-content: flex-end;
margin: 0 0 8px;
}
.dictate-product--rail .dictate-product-edit {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
}
.dictate-product--rail textarea.dictate-product-ta {
width: 100%;
min-height: 180px;
max-height: min(52vh, 380px);
margin: 0;
padding: 8px;
box-sizing: border-box;
resize: vertical;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface, var(--bg));
color: var(--text);
font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
white-space: pre;
overflow: auto;
}
.dictate-product--rail .dictate-product-edit-actions {
display: flex;
align-items: center;
gap: 6px;
}
.dictate-product--rail .dictate-product-edit-status {
font-size: 11px;
color: var(--muted, var(--text-muted, inherit));
opacity: 0.8;
}
.dictate-product--rail .dictate-product-body[hidden] {
display: none !important;
}
@@ -7879,3 +7945,52 @@ body[data-coord-rail-mode="products"] .coord-rail-title {
letter-spacing: 0.02em;
color: var(--text);
}
.price-watch {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 8px 16px;
border-bottom: 1px solid var(--border);
background: var(--accent-soft);
color: var(--text);
font-size: 13px;
line-height: 1.45;
}
.price-watch[hidden] {
display: none;
}
.price-watch[data-state="changed"] {
background: color-mix(in srgb, var(--warn) 16%, var(--surface));
}
.price-watch[data-state="error"] {
background: color-mix(in srgb, var(--danger) 12%, var(--surface));
}
.price-watch p {
margin: 0;
}
.price-watch button {
flex-shrink: 0;
height: 28px;
padding: 0 10px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
color: var(--text);
cursor: pointer;
}
.price-watch button:hover {
border-color: var(--accent);
}
.price-watch button:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--accent-soft);
}