feat: add local autosave history prototype
@@ -10,17 +10,20 @@ python -m http.server 4173 --bind 127.0.0.1
|
||||
|
||||
打开 `http://127.0.0.1:4173/prototype/`。完整状态入口、领域边界、评审清单和 Android 映射见 [`docs/prototype.md`](../docs/prototype.md)。
|
||||
|
||||
服务保持运行时,可用当前系统的 Chrome/Edge 重新生成八张截图并执行浏览器审计:
|
||||
服务保持运行时,可用当前系统的 Chrome/Edge 重新生成 18 张截图并执行浏览器审计:
|
||||
|
||||
```powershell
|
||||
node prototype\capture.mjs
|
||||
```
|
||||
|
||||
脚本使用 Node 22 自带的 WebSocket 直接连接 Chrome DevTools,不安装 npm 包;它会核对两个已知卦象、AI 同意门、控制台/外部请求、按钮名称和 48px 最小触控目标。任一门禁失败时退出码为非零。
|
||||
脚本使用 Node 22 自带的 WebSocket 直接连接 Chrome DevTools,不安装 npm 包;它会生成 18 个评审状态,并核对两个已知卦象、AI 同意门、首页层级、历史列表/空态、保存设置、自动保存/当次退出、解读关联、删除/清空流程、控制台/外部请求、按钮名称和 48px 最小触控目标。任一门禁失败时退出码为非零。
|
||||
|
||||
当前代码是 v0.3 高保真候选,已经同步 ADR-012 的默认本机自动保存策略;所有记录和设置仍只存在于浏览器内存,用于评审而非真实持久化。
|
||||
|
||||
约束:
|
||||
|
||||
- 不发起外部请求,不调用 AI;
|
||||
- 不生成随机卦,用户逐枚录入硬币;
|
||||
- 查询参数只用于稳定预览和截图;
|
||||
- 问卦簿使用内存演示数据,刷新会恢复夹具,不代表 Room 已实现;
|
||||
- 工作名“一问”与当前文字内容均为评审候选,不代表正式命名或内容授权完成。
|
||||
|
||||
@@ -3,6 +3,32 @@
|
||||
|
||||
const SAMPLE_QUESTION = "我是否要接受这次工作调整?怎样做能更稳妥?";
|
||||
const SAMPLE_LINES = [9, 8, 8, 8, 8, 8];
|
||||
const SAMPLE_HISTORY = [
|
||||
{
|
||||
id: "sample-1",
|
||||
date: "今天 · 21:10",
|
||||
lines: [9, 8, 8, 8, 8, 8],
|
||||
question: SAMPLE_QUESTION,
|
||||
explanationSource: "AI 生成",
|
||||
action: "写下三条已知事实,再约一次不超过 20 分钟的信息沟通。"
|
||||
},
|
||||
{
|
||||
id: "sample-2",
|
||||
date: "昨天 · 07:40",
|
||||
lines: [8, 8, 7, 8, 8, 8],
|
||||
question: null,
|
||||
explanationSource: "本地解读",
|
||||
action: null
|
||||
},
|
||||
{
|
||||
id: "sample-3",
|
||||
date: "7 月 30 日 · 22:15",
|
||||
lines: [7, 7, 8, 8, 7, 6],
|
||||
question: "这段合作应该怎样调整边界?",
|
||||
explanationSource: null,
|
||||
action: "先把需要确认的职责写成一页清单。"
|
||||
}
|
||||
];
|
||||
|
||||
const HEXAGRAM_NAMES = [
|
||||
"", "乾", "坤", "屯", "蒙", "需", "讼", "师", "比", "小畜", "履", "泰", "否",
|
||||
@@ -29,7 +55,8 @@
|
||||
lock: '<svg aria-hidden="true" viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8.5 10V7.5a3.5 3.5 0 017 0V10"/></svg>',
|
||||
arrow: '<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M5 12h14M14 7l5 5-5 5"/></svg>',
|
||||
chevron: '<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M9 6l6 6-6 6"/></svg>',
|
||||
shield: '<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 3l7 3v5c0 4.7-2.8 8-7 10-4.2-2-7-5.3-7-10V6l7-3z"/><path d="M9.2 12l1.8 1.8 3.9-4"/></svg>'
|
||||
shield: '<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 3l7 3v5c0 4.7-2.8 8-7 10-4.2-2-7-5.3-7-10V6l7-3z"/><path d="M9.2 12l1.8 1.8 3.9-4"/></svg>',
|
||||
book: '<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 5.5A2.5 2.5 0 016.5 3H11v16H6.5A2.5 2.5 0 004 21.5v-16zM20 5.5A2.5 2.5 0 0017.5 3H13v16h4.5a2.5 2.5 0 012.5 2.5v-16z"/></svg>'
|
||||
};
|
||||
|
||||
const screen = document.querySelector("#screen");
|
||||
@@ -45,9 +72,27 @@
|
||||
modal: null,
|
||||
consent: false,
|
||||
explanationSource: "ai",
|
||||
aiError: false
|
||||
aiError: false,
|
||||
historyRecords: [],
|
||||
selectedRecordId: null,
|
||||
autoSaveHistory: true,
|
||||
saveQuestion: true,
|
||||
saveExplanation: true,
|
||||
saveAction: true,
|
||||
saved: false,
|
||||
currentRecordId: "current-session",
|
||||
currentSessionOptedOut: false,
|
||||
currentSaveMode: null,
|
||||
explanationGenerated: false,
|
||||
questionOrigin: "welcome",
|
||||
settingsOrigin: "home",
|
||||
deleteReturnScreen: "history"
|
||||
};
|
||||
|
||||
function sampleHistory() {
|
||||
return SAMPLE_HISTORY.map((record) => ({ ...record, lines: [...record.lines] }));
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
@@ -103,6 +148,59 @@
|
||||
};
|
||||
}
|
||||
|
||||
function hasSavableQuestion() {
|
||||
return Boolean(state.question && !state.question.startsWith("未记录具体问题"));
|
||||
}
|
||||
|
||||
function generatedExplanationSource() {
|
||||
if (!state.explanationGenerated || !state.saveExplanation) return null;
|
||||
return state.explanationSource === "ai" ? "AI 生成" : "本地解读";
|
||||
}
|
||||
|
||||
function currentSessionRecord() {
|
||||
return state.historyRecords.find((record) => record.id === state.currentRecordId);
|
||||
}
|
||||
|
||||
function persistCurrentSession(mode = "manual") {
|
||||
if (state.lines.length !== 6) return;
|
||||
const data = resultData();
|
||||
const existing = currentSessionRecord();
|
||||
const record = {
|
||||
id: state.currentRecordId,
|
||||
date: "刚刚",
|
||||
lines: [...data.sourceLines],
|
||||
question: state.saveQuestion && hasSavableQuestion() ? state.question : null,
|
||||
explanationSource: generatedExplanationSource(),
|
||||
action: state.explanationGenerated && state.saveAction
|
||||
? "写下三条已知事实,再进行一次短沟通。"
|
||||
: null
|
||||
};
|
||||
|
||||
if (existing) Object.assign(existing, record);
|
||||
else state.historyRecords.unshift(record);
|
||||
state.saved = true;
|
||||
state.currentSessionOptedOut = false;
|
||||
state.currentSaveMode = mode;
|
||||
}
|
||||
|
||||
function attachExplanationToCurrentSession() {
|
||||
const record = currentSessionRecord();
|
||||
if (!record || !state.saved) return;
|
||||
if (state.saveExplanation) record.explanationSource = generatedExplanationSource();
|
||||
if (state.saveAction) {
|
||||
record.action = "写下三条已知事实,再进行一次短沟通。";
|
||||
}
|
||||
}
|
||||
|
||||
function prepareNewSession() {
|
||||
state.currentRecordId = `current-session-${Date.now()}-${state.historyRecords.length}`;
|
||||
state.saved = false;
|
||||
state.currentSessionOptedOut = false;
|
||||
state.currentSaveMode = null;
|
||||
state.explanationGenerated = false;
|
||||
state.aiError = false;
|
||||
}
|
||||
|
||||
function stepper(current, total) {
|
||||
const progress = Math.round((current / total) * 100);
|
||||
return `
|
||||
@@ -141,6 +239,259 @@
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function historyRecordData(record) {
|
||||
const changedLines = transformLines(record.lines);
|
||||
const movingIndexes = record.lines
|
||||
.map((value, index) => lineIsMoving(value) ? index : -1)
|
||||
.filter((index) => index >= 0);
|
||||
return {
|
||||
primary: lookupHexagram(record.lines),
|
||||
changed: lookupHexagram(changedLines),
|
||||
changedLines,
|
||||
movingIndexes
|
||||
};
|
||||
}
|
||||
|
||||
function recordRoute(record) {
|
||||
const data = historyRecordData(record);
|
||||
return data.movingIndexes.length
|
||||
? `${data.primary.name} <span>之</span> ${data.changed.name}`
|
||||
: data.primary.name;
|
||||
}
|
||||
|
||||
function recentRecordCard(record) {
|
||||
const data = historyRecordData(record);
|
||||
return `
|
||||
<button class="recent-card" type="button" data-action="open-history-detail" data-record-id="${record.id}">
|
||||
<span class="recent-card__top">
|
||||
<span>${record.date}</span>
|
||||
<span class="source-badge">最近一次</span>
|
||||
</span>
|
||||
<span class="recent-card__route">${recordRoute(record)}</span>
|
||||
<span class="recent-card__question">${escapeHtml(record.question ?? "未保存问题原文")}</span>
|
||||
<span class="recent-card__meta">
|
||||
<span>${data.movingIndexes.length ? `${data.movingIndexes.length} 个动爻` : "无动爻"}</span>
|
||||
<span>${record.explanationSource ?? "仅保存卦象"}</span>
|
||||
${icons.chevron}
|
||||
</span>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
function homeView() {
|
||||
const latest = state.historyRecords[0];
|
||||
return `
|
||||
<section class="page page--home" aria-labelledby="home-title">
|
||||
<header class="home-intro">
|
||||
<p class="eyebrow">慢一点,看清当下</p>
|
||||
<h1 id="home-title">此刻,有什么想安静看清?</h1>
|
||||
<p class="lede">你投币,应用记录;结果用于整理想法,不替你预测或决定。</p>
|
||||
<button class="button button--primary home-primary" type="button" data-action="begin">
|
||||
开始一问 ${icons.arrow}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section class="home-shelf" aria-labelledby="reflection-shelf-title">
|
||||
<div class="section-heading home-shelf__heading">
|
||||
<h2 id="reflection-shelf-title">你的观照</h2>
|
||||
<button class="text-link" type="button" data-action="open-history">查看问卦簿</button>
|
||||
</div>
|
||||
${latest ? recentRecordCard(latest) : `
|
||||
<button class="home-empty-card" type="button" data-action="open-history">
|
||||
<span class="home-empty-card__icon">${icons.book}</span>
|
||||
<span><strong>问卦簿尚无记录</strong><small>${state.autoSaveHistory ? "完成起卦后会默认保存在本机" : "自动保存已关闭,可在结果页保存本次"}</small></span>
|
||||
${icons.chevron}
|
||||
</button>`}
|
||||
<aside class="local-data-card" data-local-data-notice>
|
||||
<span class="local-data-card__icon">${icons.lock}</span>
|
||||
<div>
|
||||
<strong>起卦与历史默认保存在本机,不主动上传。</strong>
|
||||
<p>只有你选择 AI 解读时,本次所需内容才会发送。</p>
|
||||
<button class="text-link" type="button" data-action="open-settings">管理保存设置</button>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function historyCard(record) {
|
||||
const data = historyRecordData(record);
|
||||
return `
|
||||
<li>
|
||||
<button class="history-card" type="button" data-action="open-history-detail" data-record-id="${record.id}">
|
||||
<span class="history-card__top">
|
||||
<span>${record.date}</span>
|
||||
<span>${data.movingIndexes.length ? `${data.movingIndexes.length} 个动爻` : "无动爻"}</span>
|
||||
</span>
|
||||
<span class="history-card__route">${recordRoute(record)}</span>
|
||||
<span class="history-card__question">${escapeHtml(record.question ?? "未保存问题原文")}</span>
|
||||
<span class="history-card__footer">
|
||||
<span>${record.explanationSource ?? "仅保存卦象"}</span>
|
||||
${icons.chevron}
|
||||
</span>
|
||||
</button>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
function historyView() {
|
||||
const hasRecords = state.historyRecords.length > 0;
|
||||
return `
|
||||
<section class="page page--compact page--history" aria-labelledby="history-title">
|
||||
<p class="eyebrow">本机记录</p>
|
||||
<div class="history-title-row">
|
||||
<h1 id="history-title">问卦簿</h1>
|
||||
${hasRecords ? `<span class="source-badge">${state.historyRecords.length} 条</span>` : ""}
|
||||
</div>
|
||||
<p class="lede">回看当时看见了什么,而不是用旧结果替今天做决定。</p>
|
||||
|
||||
<div class="privacy-strip">
|
||||
${icons.lock}
|
||||
<span>记录只在本机,默认排除系统备份与设备迁移;你可随时删除。</span>
|
||||
</div>
|
||||
|
||||
${hasRecords ? `
|
||||
<ol class="history-list" data-history-list>
|
||||
${state.historyRecords.map(historyCard).join("")}
|
||||
</ol>` : `
|
||||
<div class="history-empty" data-history-empty>
|
||||
<span class="history-empty__mark" aria-hidden="true"><i></i><i></i></span>
|
||||
<h2>还没有保存的记录</h2>
|
||||
<p>${state.autoSaveHistory ? "完成一次起卦后,会默认把问题、卦象与后续解读保存在本机。" : "自动保存已经关闭;完成后仍可在结果页选择保存本次。"}</p>
|
||||
<button class="text-link" type="button" data-action="open-settings">管理保存设置</button>
|
||||
</div>`}
|
||||
|
||||
<div class="bottom-action">
|
||||
<button class="button button--primary" type="button" data-action="begin">
|
||||
${hasRecords ? "开始新的一问" : "开始第一次记录"} ${icons.arrow}
|
||||
</button>
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function settingRow({ action, title, description, checked, disabled = false, detail = false }) {
|
||||
return `
|
||||
<label class="setting-row${detail ? " setting-row--detail" : ""}${disabled ? " setting-row--disabled" : ""}">
|
||||
<span class="setting-row__copy"><strong>${title}</strong><small>${description}</small></span>
|
||||
<input
|
||||
class="switch-control"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
data-action="${action}"
|
||||
${checked ? "checked" : ""}
|
||||
${disabled ? "disabled" : ""}
|
||||
/>
|
||||
</label>`;
|
||||
}
|
||||
|
||||
function settingsView() {
|
||||
const detailsDisabled = !state.autoSaveHistory;
|
||||
return `
|
||||
<section class="page page--compact page--settings" aria-labelledby="settings-title">
|
||||
<p class="eyebrow">本机与隐私</p>
|
||||
<h1 id="settings-title">保存设置</h1>
|
||||
<p class="lede">默认留住完整的反思过程;你可以全局关闭,也可以只调整保存内容。</p>
|
||||
|
||||
<div class="settings-boundary">
|
||||
${icons.shield}
|
||||
<div>
|
||||
<strong>只保存在 App 私有存储</strong>
|
||||
<p>历史不会主动上传,并默认排除系统备份、设备迁移与云同步。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="settings-card" aria-label="历史保存选项">
|
||||
${settingRow({
|
||||
action: "auto-save-toggle",
|
||||
title: "自动保存完整记录",
|
||||
description: state.autoSaveHistory ? "已开启 · 完成起卦后立即保存到本机" : "已关闭 · 结果页仍可手动保存本次",
|
||||
checked: state.autoSaveHistory
|
||||
})}
|
||||
<div class="settings-card__details${detailsDisabled ? " settings-card__details--disabled" : ""}">
|
||||
${settingRow({
|
||||
action: "save-question-setting",
|
||||
title: "保存问题原文",
|
||||
description: "与你写下的卦象背景一同回顾",
|
||||
checked: state.saveQuestion,
|
||||
disabled: detailsDisabled,
|
||||
detail: true
|
||||
})}
|
||||
${settingRow({
|
||||
action: "save-explanation-setting",
|
||||
title: "保存解读全文",
|
||||
description: "本地或 AI 解读会加入同一次记录",
|
||||
checked: state.saveExplanation,
|
||||
disabled: detailsDisabled,
|
||||
detail: true
|
||||
})}
|
||||
${settingRow({
|
||||
action: "save-action-setting",
|
||||
title: "保存行动记录",
|
||||
description: "保留“可以试的一小步”与完成状态",
|
||||
checked: state.saveAction,
|
||||
disabled: detailsDisabled,
|
||||
detail: true
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="settings-ai-note">这些开关只控制本机历史,不会允许或触发 AI。每次 AI 解读仍需要你主动选择并确认发送范围。</p>
|
||||
|
||||
<section class="settings-danger" aria-labelledby="history-management-title">
|
||||
<div>
|
||||
<h2 id="history-management-title">管理本机记录</h2>
|
||||
<p>当前原型会话中有 ${state.historyRecords.length} 条记录。</p>
|
||||
</div>
|
||||
<button class="button button--danger-quiet" type="button" data-action="open-clear-all" ${state.historyRecords.length ? "" : "disabled"}>清空全部问卦记录</button>
|
||||
</section>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function historyDetailView() {
|
||||
const record = state.historyRecords.find((item) => item.id === state.selectedRecordId);
|
||||
if (!record) return historyView();
|
||||
const data = historyRecordData(record);
|
||||
const hasMoving = data.movingIndexes.length > 0;
|
||||
return `
|
||||
<article class="page page--compact page--history-detail" aria-labelledby="history-detail-title">
|
||||
<header class="history-detail-header">
|
||||
<p class="eyebrow">${record.date}</p>
|
||||
<h1 id="history-detail-title">${recordRoute(record)}</h1>
|
||||
<p>保存的起卦快照</p>
|
||||
</header>
|
||||
|
||||
<div class="result-hero history-result-hero">
|
||||
<div class="result-kicker"><span>coin-v1 · 本地快照</span><span>${hasMoving ? `${data.movingIndexes.length} 个动爻` : "无动爻"}</span></div>
|
||||
<div class="hexagram-pair${hasMoving ? "" : " hexagram-pair--single"}">
|
||||
${hexagramFigure(record.lines, data.primary, true)}
|
||||
${hasMoving ? `<div class="transform-arrow" aria-label="变化为">之</div>${hexagramFigure(data.changedLines, data.changed, false)}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="content-section" aria-labelledby="saved-question-title">
|
||||
<div class="section-heading"><h2 id="saved-question-title">当时所问</h2><span class="source-badge">${record.question ? "已保存" : "未保存"}</span></div>
|
||||
<div class="content-card"><p>${escapeHtml(record.question ?? "这条记录没有保存问题原文。卦象结果和方法版本仍可独立复核。")}</p></div>
|
||||
</section>
|
||||
|
||||
<section class="content-section" aria-labelledby="saved-content-title">
|
||||
<div class="section-heading"><h2 id="saved-content-title">保存内容</h2></div>
|
||||
<dl class="record-facts">
|
||||
<div><dt>起卦结果</dt><dd>${data.primary.name}${hasMoving ? `之${data.changed.name}` : ""} · ${data.movingIndexes.length ? data.movingIndexes.map((index) => lineName(record.lines[index], index)).join("、") : "无动爻"}</dd></div>
|
||||
<div><dt>解释来源</dt><dd>${record.explanationSource ?? "未保存解释"}</dd></div>
|
||||
<div><dt>版本</dt><dd>coin-v1 · content-demo</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
${record.action ? `
|
||||
<section class="content-section" aria-labelledby="saved-action-title">
|
||||
<div class="small-step"><div class="small-step__label" id="saved-action-title">当时记下的一小步</div><p>${escapeHtml(record.action)}</p></div>
|
||||
</section>` : ""}
|
||||
|
||||
<div class="detail-danger-zone">
|
||||
<button class="button button--danger-quiet" type="button" data-action="delete-record">删除这条记录</button>
|
||||
<p>删除前会再次确认;原型刷新后恢复演示数据。</p>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
function questionView() {
|
||||
return `
|
||||
<section class="page" aria-labelledby="question-title">
|
||||
@@ -158,7 +509,7 @@
|
||||
data-question-input
|
||||
>${escapeHtml(state.question)}</textarea>
|
||||
<div class="field-meta">
|
||||
<span>只保存在本次原型会话中</span>
|
||||
<span>${state.autoSaveHistory ? "完成起卦后默认保存在本机" : "自动保存已关闭,本次可稍后手动保存"}</span>
|
||||
<span data-char-count>${state.question.length}/120</span>
|
||||
</div>
|
||||
|
||||
@@ -307,6 +658,39 @@
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function currentSavePanel({ compact = false } = {}) {
|
||||
if (state.saved) {
|
||||
const explanationAttached = state.explanationGenerated && state.saveExplanation;
|
||||
const savedContext = state.saveQuestion && hasSavableQuestion() ? "问题、卦象" : "卦象";
|
||||
return `
|
||||
<div class="current-save-panel${compact ? " current-save-panel--compact" : ""}" data-current-save-status>
|
||||
<div class="current-save-panel__status">
|
||||
<span class="current-save-panel__icon">${icons.book}</span>
|
||||
<span>
|
||||
<strong>${explanationAttached ? "解读已加入本次记录" : state.currentSaveMode === "automatic" ? "已自动保存到本机" : "已保存到本机问卦簿"}</strong>
|
||||
<small>${explanationAttached ? `与${savedContext}归在同一次观照中` : `${savedContext}快照已进入问卦簿`}</small>
|
||||
</span>
|
||||
</div>
|
||||
<div class="current-save-panel__actions">
|
||||
<button class="text-link" type="button" data-action="open-history">查看问卦簿</button>
|
||||
<button class="text-link text-link--danger" type="button" data-action="remove-current-record">本次不保存</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="current-save-panel current-save-panel--off${compact ? " current-save-panel--compact" : ""}" data-current-save-status>
|
||||
<div class="current-save-panel__status">
|
||||
<span class="current-save-panel__icon">${icons.lock}</span>
|
||||
<span>
|
||||
<strong>本次未保存</strong>
|
||||
<small>${state.currentSessionOptedOut ? "你已选择不保留这次记录" : "自动保存已关闭;不会写入问卦簿"}</small>
|
||||
</span>
|
||||
</div>
|
||||
<button class="button button--secondary" type="button" data-action="save-current">保存本次</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function resultView() {
|
||||
const data = resultData();
|
||||
const copy = resultCopy(data);
|
||||
@@ -365,7 +749,10 @@
|
||||
</div>` : ""}
|
||||
|
||||
<hr class="divider" />
|
||||
<button class="text-link" type="button" data-action="restart">重新起一卦</button>
|
||||
<div class="result-secondary-actions">
|
||||
${currentSavePanel()}
|
||||
<button class="text-link" type="button" data-action="restart">重新起一卦</button>
|
||||
</div>
|
||||
|
||||
<div class="bottom-action">
|
||||
<button class="button button--primary" type="button" data-action="open-interpretation">
|
||||
@@ -430,11 +817,58 @@
|
||||
<p class="safety-note">这是一种文化反思与文字整理,不是预测、诊断或替代你的判断。涉及医疗、法律、财务或人身安全时,请寻求相应专业支持。</p>
|
||||
|
||||
<div class="bottom-action">
|
||||
<button class="button button--secondary" type="button" data-action="back-to-result">回到起卦结果</button>
|
||||
${currentSavePanel({ compact: true })}
|
||||
<button class="button button--quiet" type="button" data-action="back-to-result">回到起卦结果</button>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
function deleteRecordModal() {
|
||||
return `
|
||||
<div class="modal-backdrop" data-action="dismiss-modal">
|
||||
<section class="modal-sheet modal-sheet--dialog" role="alertdialog" aria-modal="true" aria-labelledby="delete-modal-title" aria-describedby="delete-modal-description" data-modal-sheet>
|
||||
<div class="modal-handle" aria-hidden="true"></div>
|
||||
<header class="modal-header">
|
||||
<div>
|
||||
<h2 id="delete-modal-title">删除这条记录?</h2>
|
||||
<p id="delete-modal-description">卦象、问题、已保存解读和行动记录会一并从本机删除。</p>
|
||||
</div>
|
||||
<button class="icon-button" type="button" data-action="close-modal" aria-label="关闭"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
||||
</header>
|
||||
<div class="notice notice--danger">
|
||||
${icons.shield}<p>原型使用演示数据;正式版删除不可恢复,因此必须在这里再次确认。</p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="button button--danger" type="button" data-action="confirm-delete">确认删除</button>
|
||||
<button class="button button--secondary" type="button" data-action="close-modal">取消</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function clearAllModal() {
|
||||
return `
|
||||
<div class="modal-backdrop" data-action="dismiss-modal">
|
||||
<section class="modal-sheet modal-sheet--dialog" role="alertdialog" aria-modal="true" aria-labelledby="clear-modal-title" aria-describedby="clear-modal-description" data-modal-sheet>
|
||||
<div class="modal-handle" aria-hidden="true"></div>
|
||||
<header class="modal-header">
|
||||
<div>
|
||||
<h2 id="clear-modal-title">清空全部记录?</h2>
|
||||
<p id="clear-modal-description">当前 ${state.historyRecords.length} 条记录的卦象、问题、解读和行动内容都会从本机删除。</p>
|
||||
</div>
|
||||
<button class="icon-button" type="button" data-action="close-modal" aria-label="关闭"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
||||
</header>
|
||||
<div class="notice notice--danger">
|
||||
${icons.shield}<p>这项操作不可恢复,但不会改变你的自动保存设置。</p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="button button--danger" type="button" data-action="confirm-clear-all">确认清空全部</button>
|
||||
<button class="button button--secondary" type="button" data-action="close-modal">取消</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function interpretationModal() {
|
||||
return `
|
||||
<div class="modal-backdrop" data-action="dismiss-modal">
|
||||
@@ -532,6 +966,8 @@
|
||||
if (state.modal === "interpretation") modalRoot.innerHTML = interpretationModal();
|
||||
else if (state.modal === "consent") modalRoot.innerHTML = consentModal();
|
||||
else if (state.modal === "method") modalRoot.innerHTML = methodModal();
|
||||
else if (state.modal === "delete") modalRoot.innerHTML = deleteRecordModal();
|
||||
else if (state.modal === "clear-all") modalRoot.innerHTML = clearAllModal();
|
||||
else modalRoot.innerHTML = "";
|
||||
|
||||
document.body.style.overflow = state.modal ? "hidden" : "";
|
||||
@@ -544,13 +980,17 @@
|
||||
|
||||
function render(options = {}) {
|
||||
if (state.screen === "welcome") screen.innerHTML = welcomeView();
|
||||
else if (state.screen === "home") screen.innerHTML = homeView();
|
||||
else if (state.screen === "history") screen.innerHTML = historyView();
|
||||
else if (state.screen === "history-detail") screen.innerHTML = historyDetailView();
|
||||
else if (state.screen === "settings") screen.innerHTML = settingsView();
|
||||
else if (state.screen === "question") screen.innerHTML = questionView();
|
||||
else if (state.screen === "casting") screen.innerHTML = castingView();
|
||||
else if (state.screen === "result") screen.innerHTML = resultView();
|
||||
else if (state.screen === "loading") screen.innerHTML = loadingView();
|
||||
else if (state.screen === "explanation") screen.innerHTML = explanationView();
|
||||
|
||||
backButton.hidden = state.screen === "welcome";
|
||||
backButton.hidden = state.screen === "welcome" || state.screen === "home";
|
||||
renderModal();
|
||||
|
||||
if (!options.preserveFocus) {
|
||||
@@ -577,7 +1017,10 @@
|
||||
closeModal();
|
||||
return;
|
||||
}
|
||||
if (state.screen === "question") state.screen = "welcome";
|
||||
if (state.screen === "question") state.screen = state.questionOrigin;
|
||||
else if (state.screen === "history") state.screen = "home";
|
||||
else if (state.screen === "history-detail") state.screen = "history";
|
||||
else if (state.screen === "settings") state.screen = state.settingsOrigin;
|
||||
else if (state.screen === "casting") state.screen = "question";
|
||||
else if (state.screen === "result") state.screen = "question";
|
||||
else if (state.screen === "loading" || state.screen === "explanation") state.screen = "result";
|
||||
@@ -591,6 +1034,8 @@
|
||||
render();
|
||||
window.setTimeout(() => {
|
||||
if (state.screen !== "loading") return;
|
||||
state.explanationGenerated = true;
|
||||
attachExplanationToCurrentSession();
|
||||
state.screen = "explanation";
|
||||
render();
|
||||
}, 900);
|
||||
@@ -605,10 +1050,21 @@
|
||||
});
|
||||
|
||||
document.addEventListener("change", (event) => {
|
||||
if (!event.target.matches("[data-action='consent-toggle']")) return;
|
||||
state.consent = event.target.checked;
|
||||
const confirm = document.querySelector("[data-action='confirm-ai']");
|
||||
if (confirm) confirm.disabled = !state.consent;
|
||||
if (event.target.matches("[data-action='consent-toggle']")) {
|
||||
state.consent = event.target.checked;
|
||||
const confirm = document.querySelector("[data-action='confirm-ai']");
|
||||
if (confirm) confirm.disabled = !state.consent;
|
||||
} else if (event.target.matches("[data-action='auto-save-toggle']")) {
|
||||
state.autoSaveHistory = event.target.checked;
|
||||
render({ preserveFocus: true });
|
||||
document.querySelector("[data-action='auto-save-toggle']")?.focus();
|
||||
} else if (event.target.matches("[data-action='save-question-setting']")) {
|
||||
state.saveQuestion = event.target.checked;
|
||||
} else if (event.target.matches("[data-action='save-explanation-setting']")) {
|
||||
state.saveExplanation = event.target.checked;
|
||||
} else if (event.target.matches("[data-action='save-action-setting']")) {
|
||||
state.saveAction = event.target.checked;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
@@ -620,9 +1076,22 @@
|
||||
|
||||
if (action === "back") goBack();
|
||||
else if (action === "begin") {
|
||||
state.questionOrigin = state.screen;
|
||||
state.screen = "question";
|
||||
render();
|
||||
} else if (action === "open-history") {
|
||||
state.screen = "history";
|
||||
render();
|
||||
} else if (action === "open-settings") {
|
||||
state.settingsOrigin = state.screen === "settings" ? "home" : state.screen;
|
||||
state.screen = "settings";
|
||||
render();
|
||||
} else if (action === "open-history-detail") {
|
||||
state.selectedRecordId = actionTarget.dataset.recordId;
|
||||
state.screen = "history-detail";
|
||||
render();
|
||||
} else if (action === "skip-question") {
|
||||
prepareNewSession();
|
||||
state.question = "未记录具体问题(仅作自我观察)";
|
||||
state.lines = [];
|
||||
state.coins = [null, null, null];
|
||||
@@ -630,6 +1099,7 @@
|
||||
render();
|
||||
} else if (action === "start-casting") {
|
||||
if (!state.question.trim()) return;
|
||||
prepareNewSession();
|
||||
state.lines = [];
|
||||
state.coins = [null, null, null];
|
||||
state.screen = "casting";
|
||||
@@ -645,6 +1115,7 @@
|
||||
state.lines.push(state.coins.reduce((total, value) => total + value, 0));
|
||||
state.coins = [null, null, null];
|
||||
if (state.lines.length === 6) {
|
||||
if (state.autoSaveHistory) persistCurrentSession("automatic");
|
||||
state.screen = "result";
|
||||
}
|
||||
render();
|
||||
@@ -663,6 +1134,8 @@
|
||||
} else if (action === "choose-local") {
|
||||
state.modal = null;
|
||||
state.explanationSource = "local";
|
||||
state.explanationGenerated = true;
|
||||
attachExplanationToCurrentSession();
|
||||
state.screen = "explanation";
|
||||
render();
|
||||
} else if (action === "confirm-ai") {
|
||||
@@ -673,12 +1146,56 @@
|
||||
state.modal = "consent";
|
||||
state.consent = false;
|
||||
renderModal();
|
||||
} else if (action === "save-current") {
|
||||
persistCurrentSession();
|
||||
render();
|
||||
showToast("本次记录已保存到本机");
|
||||
} else if (action === "remove-current-record") {
|
||||
if (!state.saved) return;
|
||||
state.selectedRecordId = state.currentRecordId;
|
||||
state.deleteReturnScreen = state.screen;
|
||||
state.modal = "delete";
|
||||
renderModal();
|
||||
} else if (action === "delete-record") {
|
||||
state.deleteReturnScreen = "history";
|
||||
state.modal = "delete";
|
||||
renderModal();
|
||||
} else if (action === "confirm-delete") {
|
||||
const deletedCurrentSession = state.selectedRecordId === state.currentRecordId;
|
||||
state.historyRecords = state.historyRecords.filter((record) => record.id !== state.selectedRecordId);
|
||||
state.selectedRecordId = null;
|
||||
state.modal = null;
|
||||
if (deletedCurrentSession) {
|
||||
state.saved = false;
|
||||
state.currentSessionOptedOut = true;
|
||||
state.currentSaveMode = null;
|
||||
}
|
||||
state.screen = state.deleteReturnScreen;
|
||||
render();
|
||||
showToast("记录已从本机删除");
|
||||
} else if (action === "open-clear-all") {
|
||||
if (!state.historyRecords.length) return;
|
||||
state.modal = "clear-all";
|
||||
renderModal();
|
||||
} else if (action === "confirm-clear-all") {
|
||||
const includedCurrentSession = Boolean(currentSessionRecord());
|
||||
state.historyRecords = [];
|
||||
state.selectedRecordId = null;
|
||||
state.modal = null;
|
||||
if (includedCurrentSession) {
|
||||
state.saved = false;
|
||||
state.currentSessionOptedOut = true;
|
||||
state.currentSaveMode = null;
|
||||
}
|
||||
render();
|
||||
showToast("已清空全部本机记录");
|
||||
} else if (action === "restart") {
|
||||
state.question = "";
|
||||
state.lines = [];
|
||||
state.coins = [null, null, null];
|
||||
state.aiError = false;
|
||||
state.screen = "welcome";
|
||||
prepareNewSession();
|
||||
state.questionOrigin = state.historyRecords.length ? "home" : "welcome";
|
||||
state.screen = "question";
|
||||
render();
|
||||
} else if (action === "back-to-result") {
|
||||
state.screen = "result";
|
||||
@@ -705,6 +1222,22 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (["home", "home-empty", "history", "history-empty", "history-detail", "delete", "settings", "settings-off", "clear-all"].includes(view)) {
|
||||
state.historyRecords = view === "home-empty" || view === "history-empty" ? [] : sampleHistory();
|
||||
state.screen = view.startsWith("home")
|
||||
? "home"
|
||||
: view === "history-detail" || view === "delete"
|
||||
? "history-detail"
|
||||
: view === "settings" || view === "settings-off" || view === "clear-all"
|
||||
? "settings"
|
||||
: "history";
|
||||
state.selectedRecordId = state.historyRecords[0]?.id ?? null;
|
||||
if (view === "settings-off") state.autoSaveHistory = false;
|
||||
if (view === "delete") state.modal = "delete";
|
||||
if (view === "clear-all") state.modal = "clear-all";
|
||||
return;
|
||||
}
|
||||
|
||||
state.question = SAMPLE_QUESTION;
|
||||
if (view === "casting") {
|
||||
state.screen = "casting";
|
||||
@@ -715,14 +1248,20 @@
|
||||
|
||||
state.lines = view === "static" ? [7, 7, 7, 7, 7, 7] : [...SAMPLE_LINES];
|
||||
state.screen = "result";
|
||||
if (view === "result-not-saved") state.autoSaveHistory = false;
|
||||
if (state.autoSaveHistory) persistCurrentSession("automatic");
|
||||
|
||||
if (view === "consent") state.modal = "consent";
|
||||
else if (view === "explanation") {
|
||||
state.screen = "explanation";
|
||||
state.explanationSource = "ai";
|
||||
state.explanationGenerated = true;
|
||||
attachExplanationToCurrentSession();
|
||||
} else if (view === "local") {
|
||||
state.screen = "explanation";
|
||||
state.explanationSource = "local";
|
||||
state.explanationGenerated = true;
|
||||
attachExplanationToCurrentSession();
|
||||
} else if (view === "error") {
|
||||
state.aiError = true;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,17 @@ const cases = [
|
||||
{ view: "static", file: "05-result-static.png", fullPage: true },
|
||||
{ view: "consent", file: "06-ai-consent.png", fullPage: false },
|
||||
{ view: "explanation", file: "07-ai-explanation.png", fullPage: true },
|
||||
{ view: "error", file: "08-ai-error.png", fullPage: true }
|
||||
{ view: "error", file: "08-ai-error.png", fullPage: true },
|
||||
{ view: "home", file: "09-home-returning.png", fullPage: true },
|
||||
{ view: "home-empty", file: "10-home-empty.png", fullPage: true },
|
||||
{ view: "history", file: "11-history-list.png", fullPage: true },
|
||||
{ view: "history-empty", file: "12-history-empty.png", fullPage: true },
|
||||
{ view: "history-detail", file: "13-history-detail.png", fullPage: true },
|
||||
{ view: "settings", file: "14-settings-default.png", fullPage: true },
|
||||
{ view: "delete", file: "15-delete-confirm.png", fullPage: false },
|
||||
{ view: "settings-off", file: "16-settings-off.png", fullPage: true },
|
||||
{ view: "result-not-saved", file: "17-result-not-saved.png", fullPage: true },
|
||||
{ view: "clear-all", file: "18-clear-all-confirm.png", fullPage: false }
|
||||
];
|
||||
|
||||
function findChrome() {
|
||||
@@ -206,6 +216,26 @@ async function auditView(client, view) {
|
||||
})()`);
|
||||
}
|
||||
|
||||
async function auditLayout(client, { name, width, height, view }) {
|
||||
await client.send("Emulation.setDeviceMetricsOverride", {
|
||||
width,
|
||||
height,
|
||||
deviceScaleFactor: 1,
|
||||
mobile: false,
|
||||
screenWidth: width,
|
||||
screenHeight: height
|
||||
});
|
||||
await navigate(client, view);
|
||||
return evaluate(client, `(() => ({
|
||||
name: ${JSON.stringify(name)},
|
||||
view: ${JSON.stringify(view)},
|
||||
viewport: { width: innerWidth, height: innerHeight },
|
||||
documentWidth: document.documentElement.scrollWidth,
|
||||
horizontalOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth,
|
||||
mainReachable: Boolean(document.querySelector('#screen'))
|
||||
}))()`);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const health = await fetch(baseUrl);
|
||||
if (!health.ok) {
|
||||
@@ -265,6 +295,18 @@ async function run() {
|
||||
const screenshots = [];
|
||||
for (const item of cases) screenshots.push(await capture(client, item));
|
||||
|
||||
await navigate(client, "casting");
|
||||
for (let remainingRound = 0; remainingRound < 4; remainingRound += 1) {
|
||||
for (let coinIndex = 0; coinIndex < 3; coinIndex += 1) {
|
||||
await evaluate(client, `document.querySelector('[data-action="toggle-coin"][data-index="${coinIndex}"]')?.click()`);
|
||||
}
|
||||
await evaluate(client, "document.querySelector('[data-action=\"confirm-line\"]')?.click()");
|
||||
}
|
||||
const completedCastingSave = await evaluate(client, `(() => ({
|
||||
resultVisible: Boolean(document.querySelector('#result-title')),
|
||||
status: document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, " ").trim()
|
||||
}))()`);
|
||||
|
||||
await navigate(client, "result");
|
||||
const movingResult = await evaluate(client, `(() => ({
|
||||
names: [...document.querySelectorAll(".hexagram-name")].map((node) => node.textContent.trim()),
|
||||
@@ -289,28 +331,158 @@ async function run() {
|
||||
await evaluate(client, "document.querySelector('[data-action=\"confirm-ai\"]')?.click()");
|
||||
await delay(1050);
|
||||
const explanationSource = await evaluate(client, "document.querySelector('.source-badge')?.textContent.trim()");
|
||||
const explanationSaveStatus = await evaluate(client, "document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, ' ').trim()");
|
||||
|
||||
await navigate(client, "home");
|
||||
const homeState = await evaluate(client, `(() => ({
|
||||
primaryActions: document.querySelectorAll('[data-action="begin"]').length,
|
||||
recentRoute: document.querySelector('.recent-card__route')?.textContent.replace(/\\s+/g, " ").trim(),
|
||||
historyEntry: document.querySelectorAll('[data-action="open-history"]').length,
|
||||
settingsEntry: document.querySelectorAll('[data-action="open-settings"]').length,
|
||||
localNotice: document.querySelector('[data-local-data-notice]')?.textContent.replace(/\\s+/g, " ").trim(),
|
||||
bottomNavigation: document.querySelectorAll('nav, [role="navigation"]').length
|
||||
}))()`);
|
||||
await evaluate(client, "document.querySelector('[data-action=\"begin\"]')?.click()");
|
||||
const homeForwardTitle = await evaluate(client, "document.querySelector('#question-title')?.textContent.trim()");
|
||||
await evaluate(client, "document.querySelector('[data-action=\"back\"]')?.click()");
|
||||
const homeBackTitle = await evaluate(client, "document.querySelector('#home-title')?.textContent.trim()");
|
||||
|
||||
await navigate(client, "history");
|
||||
const historyState = await evaluate(client, `(() => ({
|
||||
count: document.querySelectorAll('.history-card').length,
|
||||
firstRoute: document.querySelector('.history-card__route')?.textContent.replace(/\\s+/g, " ").trim()
|
||||
}))()`);
|
||||
|
||||
await navigate(client, "history-empty");
|
||||
const historyEmptyState = await evaluate(client, `(() => ({
|
||||
hasEmptyGuidance: Boolean(document.querySelector('[data-history-empty]')),
|
||||
hasPrimaryAction: Boolean(document.querySelector('[data-action="begin"]')),
|
||||
hasSettingsEntry: Boolean(document.querySelector('[data-action="open-settings"]'))
|
||||
}))()`);
|
||||
|
||||
await navigate(client, "settings");
|
||||
const settingsDefaults = await evaluate(client, `(() => ({
|
||||
autoSave: document.querySelector('[data-action="auto-save-toggle"]')?.checked,
|
||||
question: document.querySelector('[data-action="save-question-setting"]')?.checked,
|
||||
explanation: document.querySelector('[data-action="save-explanation-setting"]')?.checked,
|
||||
action: document.querySelector('[data-action="save-action-setting"]')?.checked,
|
||||
detailDisabled: [...document.querySelectorAll('.settings-card__details input')].some((input) => input.disabled),
|
||||
boundary: document.querySelector('.settings-boundary')?.textContent.replace(/\\s+/g, " ").trim()
|
||||
}))()`);
|
||||
await evaluate(client, "document.querySelector('[data-action=\"auto-save-toggle\"]')?.click()");
|
||||
await delay(100);
|
||||
const settingsDisabled = await evaluate(client, `(() => ({
|
||||
autoSave: document.querySelector('[data-action="auto-save-toggle"]')?.checked,
|
||||
detailsDisabled: [...document.querySelectorAll('.settings-card__details input')].every((input) => input.disabled),
|
||||
retainedValues: [...document.querySelectorAll('.settings-card__details input')].every((input) => input.checked)
|
||||
}))()`);
|
||||
|
||||
await navigate(client, "result");
|
||||
const autoSaveBefore = await evaluate(client, `(() => ({
|
||||
status: document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, " ").trim(),
|
||||
canOptOut: Boolean(document.querySelector('[data-action="remove-current-record"]'))
|
||||
}))()`);
|
||||
await evaluate(client, "document.querySelector('[data-action=\"remove-current-record\"]')?.click()");
|
||||
const optOutDialogVisible = await evaluate(client, "Boolean(document.querySelector('[role=\"alertdialog\"]'))");
|
||||
await evaluate(client, "document.querySelector('[data-action=\"confirm-delete\"]')?.click()");
|
||||
await delay(100);
|
||||
const afterOptOut = await evaluate(client, `(() => ({
|
||||
status: document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, " ").trim(),
|
||||
canSave: Boolean(document.querySelector('[data-action="save-current"]'))
|
||||
}))()`);
|
||||
await evaluate(client, "document.querySelector('[data-action=\"save-current\"]')?.click()");
|
||||
await delay(100);
|
||||
const afterManualSave = await evaluate(client, "document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, ' ').trim()");
|
||||
|
||||
await navigate(client, "result-not-saved");
|
||||
const autoSaveOffResult = await evaluate(client, `(() => ({
|
||||
status: document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, " ").trim(),
|
||||
canSave: Boolean(document.querySelector('[data-action="save-current"]'))
|
||||
}))()`);
|
||||
|
||||
await navigate(client, "clear-all");
|
||||
const clearBefore = await evaluate(client, `(() => ({
|
||||
dialogVisible: Boolean(document.querySelector('[role="alertdialog"]')),
|
||||
countText: document.querySelector('#clear-modal-description')?.textContent.trim()
|
||||
}))()`);
|
||||
await evaluate(client, "document.querySelector('[data-action=\"confirm-clear-all\"]')?.click()");
|
||||
await delay(100);
|
||||
const clearAfter = await evaluate(client, `(() => ({
|
||||
settingsVisible: document.querySelector('#settings-title')?.textContent.trim(),
|
||||
recordCount: document.querySelector('.settings-danger p')?.textContent.trim(),
|
||||
clearDisabled: document.querySelector('[data-action="open-clear-all"]')?.disabled,
|
||||
autoSaveRetained: document.querySelector('[data-action="auto-save-toggle"]')?.checked,
|
||||
modalClosed: !document.querySelector('[role="alertdialog"]')
|
||||
}))()`);
|
||||
|
||||
await navigate(client, "delete");
|
||||
const deleteBefore = await evaluate(client, `(() => ({
|
||||
dialogVisible: Boolean(document.querySelector('[role="alertdialog"]')),
|
||||
detailVisible: Boolean(document.querySelector('#history-detail-title'))
|
||||
}))()`);
|
||||
await evaluate(client, "document.querySelector('[data-action=\"confirm-delete\"]')?.click()");
|
||||
await delay(100);
|
||||
const deleteAfter = await evaluate(client, `(() => ({
|
||||
onHistory: document.querySelector('#history-title')?.textContent.trim(),
|
||||
historyCount: document.querySelectorAll('.history-card').length,
|
||||
modalClosed: !document.querySelector('[role="alertdialog"]')
|
||||
}))()`);
|
||||
|
||||
const accessibility = [];
|
||||
for (const item of cases) accessibility.push(await auditView(client, item.view));
|
||||
|
||||
const layoutAudits = [];
|
||||
for (const layout of [
|
||||
{ name: "small-phone", width: 375, height: 812, view: "home" },
|
||||
{ name: "large-phone", width: 430, height: 932, view: "history" },
|
||||
{ name: "large-phone-settings", width: 430, height: 932, view: "settings" },
|
||||
{ name: "phone-landscape", width: 792, height: 360, view: "history-detail" }
|
||||
]) {
|
||||
layoutAudits.push(await auditLayout(client, layout));
|
||||
}
|
||||
|
||||
await client.send("Emulation.setEmulatedMedia", {
|
||||
features: [{ name: "prefers-reduced-motion", value: "reduce" }]
|
||||
});
|
||||
await navigate(client, "home");
|
||||
const reducedMotion = await evaluate(client, `(() => {
|
||||
const page = document.querySelector('.page');
|
||||
const duration = Number.parseFloat(getComputedStyle(page).animationDuration) || 0;
|
||||
return { animationDurationSeconds: duration, reduced: duration <= 0.001 };
|
||||
})()`);
|
||||
await client.send("Emulation.setEmulatedMedia", { features: [] });
|
||||
|
||||
const report = {
|
||||
chrome,
|
||||
baseUrl,
|
||||
screenshots,
|
||||
completedCastingSave,
|
||||
movingResult,
|
||||
staticResult,
|
||||
consentGate: {
|
||||
disabledBeforeConsent: consentBefore,
|
||||
disabledAfterConsent: consentAfter,
|
||||
explanationSource
|
||||
explanationSource,
|
||||
explanationSaveStatus
|
||||
},
|
||||
homeState: { ...homeState, forwardTitle: homeForwardTitle, backTitle: homeBackTitle },
|
||||
historyState,
|
||||
historyEmptyState,
|
||||
settingsFlow: { defaults: settingsDefaults, disabled: settingsDisabled },
|
||||
autoSaveFlow: { before: autoSaveBefore, optOutDialogVisible, afterOptOut, afterManualSave, autoSaveOffResult },
|
||||
clearAllFlow: { before: clearBefore, after: clearAfter },
|
||||
deleteFlow: { before: deleteBefore, after: deleteAfter },
|
||||
consoleErrors,
|
||||
runtimeErrors,
|
||||
externalRequests: [...new Set(externalRequests)],
|
||||
accessibility
|
||||
accessibility,
|
||||
layoutAudits,
|
||||
reducedMotion
|
||||
};
|
||||
|
||||
const failed =
|
||||
completedCastingSave.resultVisible !== true ||
|
||||
!completedCastingSave.status?.includes("已自动保存到本机") ||
|
||||
movingResult.names.join(",") !== "复,坤" ||
|
||||
movingResult.numbers.join(",") !== "第 24 卦,第 2 卦" ||
|
||||
!movingResult.moving.includes("1 个动爻") ||
|
||||
@@ -319,12 +491,59 @@ async function run() {
|
||||
consentBefore !== true ||
|
||||
consentAfter !== false ||
|
||||
explanationSource !== "AI 生成" ||
|
||||
!explanationSaveStatus?.includes("解读已加入本次记录") ||
|
||||
homeState.primaryActions !== 1 ||
|
||||
homeState.recentRoute !== "复 之 坤" ||
|
||||
homeState.historyEntry < 1 ||
|
||||
homeState.settingsEntry !== 1 ||
|
||||
!homeState.localNotice?.includes("起卦与历史默认保存在本机,不主动上传") ||
|
||||
!homeState.localNotice?.includes("只有你选择 AI 解读时,本次所需内容才会发送") ||
|
||||
homeState.bottomNavigation !== 0 ||
|
||||
homeForwardTitle !== "你想看清什么?" ||
|
||||
homeBackTitle !== "此刻,有什么想安静看清?" ||
|
||||
historyState.count !== 3 ||
|
||||
historyState.firstRoute !== "复 之 坤" ||
|
||||
historyEmptyState.hasEmptyGuidance !== true ||
|
||||
historyEmptyState.hasPrimaryAction !== true ||
|
||||
historyEmptyState.hasSettingsEntry !== true ||
|
||||
settingsDefaults.autoSave !== true ||
|
||||
settingsDefaults.question !== true ||
|
||||
settingsDefaults.explanation !== true ||
|
||||
settingsDefaults.action !== true ||
|
||||
settingsDefaults.detailDisabled !== false ||
|
||||
!settingsDefaults.boundary?.includes("默认排除系统备份、设备迁移与云同步") ||
|
||||
settingsDisabled.autoSave !== false ||
|
||||
settingsDisabled.detailsDisabled !== true ||
|
||||
settingsDisabled.retainedValues !== true ||
|
||||
!autoSaveBefore.status?.includes("已自动保存到本机") ||
|
||||
autoSaveBefore.canOptOut !== true ||
|
||||
optOutDialogVisible !== true ||
|
||||
!afterOptOut.status?.includes("本次未保存") ||
|
||||
!afterOptOut.status?.includes("你已选择不保留这次记录") ||
|
||||
afterOptOut.canSave !== true ||
|
||||
!afterManualSave?.includes("已保存到本机问卦簿") ||
|
||||
!autoSaveOffResult.status?.includes("自动保存已关闭") ||
|
||||
autoSaveOffResult.canSave !== true ||
|
||||
clearBefore.dialogVisible !== true ||
|
||||
!clearBefore.countText?.includes("当前 3 条记录") ||
|
||||
clearAfter.settingsVisible !== "保存设置" ||
|
||||
!clearAfter.recordCount?.includes("0 条记录") ||
|
||||
clearAfter.clearDisabled !== true ||
|
||||
clearAfter.autoSaveRetained !== true ||
|
||||
clearAfter.modalClosed !== true ||
|
||||
deleteBefore.dialogVisible !== true ||
|
||||
deleteBefore.detailVisible !== true ||
|
||||
deleteAfter.onHistory !== "问卦簿" ||
|
||||
deleteAfter.historyCount !== 2 ||
|
||||
deleteAfter.modalClosed !== true ||
|
||||
consoleErrors.length > 0 ||
|
||||
runtimeErrors.length > 0 ||
|
||||
externalRequests.length > 0 ||
|
||||
accessibility.some((item) =>
|
||||
item.unnamedButtons > 0 || item.touchIssues.length > 0 || item.smallCheckboxLabels > 0
|
||||
);
|
||||
) ||
|
||||
layoutAudits.some((item) => item.horizontalOverflow || !item.mainReachable) ||
|
||||
reducedMotion.reduced !== true;
|
||||
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
if (failed) process.exitCode = 1;
|
||||
|
||||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 75 KiB |
@@ -202,6 +202,583 @@ a:focus-visible {
|
||||
padding-top: clamp(36px, 9vh, 72px);
|
||||
}
|
||||
|
||||
.page--home {
|
||||
padding-top: 28px;
|
||||
padding-bottom: calc(32px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.home-intro h1 {
|
||||
max-width: 10em;
|
||||
margin-bottom: 12px;
|
||||
font-size: clamp(31px, 9vw, 38px);
|
||||
}
|
||||
|
||||
.home-intro .lede {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.home-primary {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.home-shelf {
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--outline-soft);
|
||||
}
|
||||
|
||||
.home-shelf__heading {
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
|
||||
.home-shelf__heading h2 {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.home-shelf__heading .text-link {
|
||||
min-height: 48px;
|
||||
padding-right: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.recent-card,
|
||||
.home-empty-card,
|
||||
.history-card {
|
||||
width: 100%;
|
||||
color: var(--ink);
|
||||
background: rgb(255 253 247 / 76%);
|
||||
border: 1px solid var(--outline-soft);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: border-color 180ms ease, background-color 180ms ease, transform 180ms ease, box-shadow 180ms ease;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.recent-card:hover,
|
||||
.home-empty-card:hover,
|
||||
.history-card:hover {
|
||||
background: var(--surface);
|
||||
border-color: var(--outline);
|
||||
box-shadow: var(--shadow-soft);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.recent-card:active,
|
||||
.home-empty-card:active,
|
||||
.history-card:active {
|
||||
transform: scale(.995);
|
||||
}
|
||||
|
||||
.recent-card {
|
||||
display: grid;
|
||||
min-height: 154px;
|
||||
padding: 15px 16px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.recent-card__top,
|
||||
.recent-card__meta,
|
||||
.history-card__top,
|
||||
.history-card__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.recent-card__top,
|
||||
.history-card__top {
|
||||
color: var(--ink-faint);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.recent-card__route,
|
||||
.history-card__route {
|
||||
font-family: var(--serif);
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.recent-card__route span,
|
||||
.history-card__route span {
|
||||
margin: 0 4px;
|
||||
color: var(--outline);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recent-card__question,
|
||||
.history-card__question {
|
||||
overflow: hidden;
|
||||
color: var(--ink-soft);
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.recent-card__meta,
|
||||
.history-card__footer {
|
||||
color: var(--ink-faint);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.recent-card__meta svg,
|
||||
.history-card__footer svg,
|
||||
.home-empty-card > svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 18px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 1.7;
|
||||
}
|
||||
|
||||
.recent-card__meta span:nth-child(2) {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.home-empty-card {
|
||||
display: grid;
|
||||
min-height: 86px;
|
||||
padding: 14px;
|
||||
grid-template-columns: 44px 1fr 20px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.home-empty-card__icon {
|
||||
display: grid;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
color: var(--ochre);
|
||||
background: var(--surface-muted);
|
||||
border-radius: 50%;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.home-empty-card__icon svg,
|
||||
.button > svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 1.7;
|
||||
}
|
||||
|
||||
.home-empty-card strong,
|
||||
.home-empty-card small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.home-empty-card small {
|
||||
margin-top: 2px;
|
||||
color: var(--ink-faint);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.local-data-card {
|
||||
display: grid;
|
||||
margin-top: 14px;
|
||||
padding: 14px;
|
||||
grid-template-columns: 36px 1fr;
|
||||
align-items: start;
|
||||
gap: 11px;
|
||||
color: var(--ink-soft);
|
||||
background: #edf1e9;
|
||||
border: 1px solid #d0dacb;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.local-data-card__icon {
|
||||
display: grid;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: var(--success);
|
||||
background: rgb(255 255 255 / 64%);
|
||||
border-radius: 50%;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.local-data-card__icon svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 1.7;
|
||||
}
|
||||
|
||||
.local-data-card strong {
|
||||
display: block;
|
||||
color: #304c39;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.local-data-card p {
|
||||
margin: 3px 0 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.local-data-card .text-link {
|
||||
margin-bottom: -8px;
|
||||
color: #405e49;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.page--history {
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
.history-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.history-title-row h1 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.privacy-strip {
|
||||
display: flex;
|
||||
margin: 20px 0;
|
||||
padding: 12px 13px;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
color: var(--ink-soft);
|
||||
background: rgb(255 253 247 / 62%);
|
||||
border: 1px solid var(--outline-soft);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.privacy-strip svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 18px;
|
||||
fill: none;
|
||||
stroke: var(--ochre);
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 1.7;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: grid;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 12px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.history-card {
|
||||
display: grid;
|
||||
min-height: 132px;
|
||||
padding: 15px 16px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.history-card__route {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.history-card__footer {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.history-card__footer span {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.history-empty {
|
||||
display: grid;
|
||||
min-height: 330px;
|
||||
padding: 34px 20px;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
color: var(--ink-soft);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.history-empty__mark {
|
||||
display: grid;
|
||||
width: 92px;
|
||||
height: 92px;
|
||||
margin-bottom: 22px;
|
||||
place-content: center;
|
||||
gap: 11px;
|
||||
border: 1px solid var(--outline-soft);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.history-empty__mark i {
|
||||
display: block;
|
||||
width: 44px;
|
||||
height: 5px;
|
||||
background: var(--ink);
|
||||
}
|
||||
|
||||
.history-empty__mark i:last-child {
|
||||
background: linear-gradient(90deg, var(--ink) 0 40%, transparent 40% 60%, var(--ink) 60%);
|
||||
}
|
||||
|
||||
.history-empty h2 {
|
||||
margin-bottom: 8px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.history-empty p {
|
||||
max-width: 23em;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.page--settings {
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
.page--settings > h1 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.settings-boundary {
|
||||
display: grid;
|
||||
margin: 22px 0 16px;
|
||||
padding: 15px;
|
||||
grid-template-columns: 40px 1fr;
|
||||
align-items: start;
|
||||
gap: 12px;
|
||||
color: var(--ink-soft);
|
||||
background: #e9efe7;
|
||||
border: 1px solid #cfdbcc;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.settings-boundary > svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 5px auto 0;
|
||||
fill: none;
|
||||
stroke: var(--success);
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 1.7;
|
||||
}
|
||||
|
||||
.settings-boundary strong,
|
||||
.settings-boundary p {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-boundary p {
|
||||
margin: 3px 0 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
overflow: hidden;
|
||||
background: rgb(255 253 247 / 80%);
|
||||
border: 1px solid var(--outline-soft);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.settings-card__details {
|
||||
margin-left: 16px;
|
||||
border-left: 2px solid var(--cinnabar-soft);
|
||||
}
|
||||
|
||||
.settings-card__details--disabled {
|
||||
background: rgb(221 211 196 / 18%);
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
display: grid;
|
||||
min-height: 78px;
|
||||
padding: 13px 15px;
|
||||
grid-template-columns: 1fr 52px;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.setting-row + .setting-row,
|
||||
.settings-card__details .setting-row:first-child {
|
||||
border-top: 1px solid var(--outline-soft);
|
||||
}
|
||||
|
||||
.setting-row--detail {
|
||||
min-height: 72px;
|
||||
padding-left: 13px;
|
||||
}
|
||||
|
||||
.setting-row--disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.setting-row__copy strong,
|
||||
.setting-row__copy small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.setting-row__copy strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.setting-row__copy small {
|
||||
margin-top: 3px;
|
||||
color: var(--ink-faint);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.switch-control {
|
||||
position: relative;
|
||||
width: 52px;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
background: #cfc5b7;
|
||||
border: 1px solid #b8ad9e;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: background-color 180ms ease, border-color 180ms ease;
|
||||
}
|
||||
|
||||
.switch-control::after {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: var(--surface);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 6px rgb(47 36 25 / 18%);
|
||||
content: "";
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
|
||||
.switch-control:checked {
|
||||
background: var(--cinnabar);
|
||||
border-color: var(--cinnabar);
|
||||
}
|
||||
|
||||
.switch-control:checked::after {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.switch-control:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.settings-ai-note {
|
||||
margin: 14px 2px 26px;
|
||||
color: var(--ink-faint);
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.settings-danger {
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--outline-soft);
|
||||
}
|
||||
|
||||
.settings-danger h2 {
|
||||
margin-bottom: 4px;
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.settings-danger p {
|
||||
margin-bottom: 13px;
|
||||
color: var(--ink-faint);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.settings-danger .button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.history-detail-header h1 {
|
||||
max-width: none;
|
||||
margin-bottom: 4px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.history-detail-header h1 span {
|
||||
color: var(--outline);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.history-detail-header > p:last-child {
|
||||
color: var(--ink-faint);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.history-result-hero {
|
||||
margin-top: 20px;
|
||||
border: 1px solid var(--outline-soft);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.record-facts {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: rgb(255 253 247 / 70%);
|
||||
border: 1px solid var(--outline-soft);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.record-facts div {
|
||||
display: grid;
|
||||
padding: 13px 15px;
|
||||
grid-template-columns: 82px 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.record-facts div + div {
|
||||
border-top: 1px solid var(--outline-soft);
|
||||
}
|
||||
|
||||
.record-facts dt {
|
||||
color: var(--ink-faint);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.record-facts dd {
|
||||
margin: 0;
|
||||
color: var(--ink-soft);
|
||||
font-size: 13px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.detail-danger-zone {
|
||||
margin-top: 32px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--outline-soft);
|
||||
}
|
||||
|
||||
.detail-danger-zone .button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detail-danger-zone p {
|
||||
margin: 7px 0 0;
|
||||
color: var(--ink-faint);
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes page-in {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
@@ -417,6 +994,15 @@ h3 {
|
||||
border-color: rgb(155 52 45 / 28%);
|
||||
}
|
||||
|
||||
.button--danger {
|
||||
color: white;
|
||||
background: var(--error);
|
||||
}
|
||||
|
||||
.button--danger:hover {
|
||||
background: #812a25;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
color: #91877a;
|
||||
background: #ded6c9;
|
||||
@@ -453,6 +1039,11 @@ h3 {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.text-link--danger {
|
||||
color: var(--error);
|
||||
text-decoration-color: rgb(155 52 45 / 35%);
|
||||
}
|
||||
|
||||
.stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -995,6 +1586,99 @@ h3 {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.result-secondary-actions {
|
||||
display: grid;
|
||||
margin-top: 14px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.current-save-panel {
|
||||
display: grid;
|
||||
padding: 13px 14px 4px;
|
||||
gap: 6px;
|
||||
color: var(--success);
|
||||
background: #e7eee6;
|
||||
border: 1px solid #cbdacb;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.current-save-panel--off {
|
||||
padding-bottom: 13px;
|
||||
color: var(--ink-soft);
|
||||
background: rgb(255 253 247 / 72%);
|
||||
border-color: var(--outline-soft);
|
||||
}
|
||||
|
||||
.current-save-panel__status {
|
||||
display: grid;
|
||||
min-height: 46px;
|
||||
grid-template-columns: 36px 1fr;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.current-save-panel__icon {
|
||||
display: grid;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: rgb(255 255 255 / 62%);
|
||||
border-radius: 50%;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.current-save-panel__icon svg {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 1.7;
|
||||
}
|
||||
|
||||
.current-save-panel__status strong,
|
||||
.current-save-panel__status small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.current-save-panel__status strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.current-save-panel__status small {
|
||||
margin-top: 2px;
|
||||
color: #526858;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.current-save-panel--off .current-save-panel__status small {
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
|
||||
.current-save-panel__actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
border-top: 1px solid rgb(62 99 74 / 18%);
|
||||
}
|
||||
|
||||
.current-save-panel__actions .text-link {
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.current-save-panel__actions .text-link + .text-link {
|
||||
border-left: 1px solid rgb(62 99 74 / 18%);
|
||||
}
|
||||
|
||||
.current-save-panel--off > .button {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.current-save-panel--compact {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.interpretation-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -1138,6 +1822,10 @@ h3 {
|
||||
animation: sheet-in 240ms cubic-bezier(.2, .8, .2, 1) both;
|
||||
}
|
||||
|
||||
.modal-sheet--dialog {
|
||||
margin-bottom: max(18px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.modal-handle {
|
||||
width: 42px;
|
||||
height: 4px;
|
||||
@@ -1256,6 +1944,27 @@ h3 {
|
||||
accent-color: var(--cinnabar);
|
||||
}
|
||||
|
||||
.consent-check strong,
|
||||
.consent-check small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.consent-check small {
|
||||
margin-top: 2px;
|
||||
color: var(--ink-faint);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.notice--danger {
|
||||
color: #71322d;
|
||||
background: #fff3f0;
|
||||
border-color: #e4b9b3;
|
||||
}
|
||||
|
||||
.notice--danger svg {
|
||||
stroke: var(--error);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: grid;
|
||||
margin-top: 14px;
|
||||
|
||||