add official Chrome feature demos
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { escapeHtml, logTo, setText } from "../shared.js";
|
||||
|
||||
const features = [
|
||||
{
|
||||
name: "View Transitions API",
|
||||
available: () => "startViewTransition" in document,
|
||||
advice: "可作为体验增强;不支持时直接更新 DOM。",
|
||||
},
|
||||
{
|
||||
name: "Popover API",
|
||||
available: () => "popover" in HTMLElement.prototype,
|
||||
advice: "适合替代一部分自研浮层;保留 dialog/menu fallback。",
|
||||
},
|
||||
{
|
||||
name: "CSS Container Queries",
|
||||
available: () => CSS.supports("container-type", "inline-size"),
|
||||
advice: "适合组件级响应式;老环境使用传统 media query。",
|
||||
},
|
||||
{
|
||||
name: "Prompt API",
|
||||
available: () => "LanguageModel" in window,
|
||||
advice: "实验/受环境限制;必须做能力检测和 mock 或云端 fallback。",
|
||||
},
|
||||
{
|
||||
name: "WebMCP",
|
||||
available: () => "modelContext" in navigator,
|
||||
advice: "实验方向;适合演示 Agentic Web 的工具契约。",
|
||||
},
|
||||
];
|
||||
|
||||
const table = document.querySelector("#feature-table");
|
||||
const counts = { ready: 0, enhanced: 0, blocked: 0 };
|
||||
|
||||
table.innerHTML = features
|
||||
.map((feature) => {
|
||||
const ok = feature.available();
|
||||
const type = ok ? "ready" : feature.name.includes("API") ? "enhanced" : "blocked";
|
||||
counts[type] += 1;
|
||||
logTo("#log", "baseline.feature", {
|
||||
feature: feature.name,
|
||||
available: ok,
|
||||
advice: feature.advice,
|
||||
});
|
||||
return `<tr>
|
||||
<td><strong>${escapeHtml(feature.name)}</strong></td>
|
||||
<td><span class="pill ${ok ? "green" : "amber"}">${ok ? "available" : "not detected"}</span></td>
|
||||
<td>${escapeHtml(feature.advice)}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
setText("#ready-count", String(counts.ready));
|
||||
setText("#enhanced-count", String(counts.enhanced));
|
||||
setText("#blocked-count", String(counts.blocked));
|
||||
@@ -0,0 +1,70 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Baseline Demo</title>
|
||||
<link rel="stylesheet" href="../shared.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<aside class="rail">
|
||||
<p class="eyebrow">Web Platform</p>
|
||||
<h1>Baseline</h1>
|
||||
<p class="lead">演示如何把“能不能用某个 Web API”变成特性状态、运行检测和 fallback 策略。</p>
|
||||
<nav class="nav" aria-label="Demo navigation">
|
||||
<a href="../index.html"><span class="num">0</span><span>返回总览</span></a>
|
||||
<a href="../webmcp/index.html"><span class="num">1</span><span>WebMCP</span></a>
|
||||
<a href="../prompt-api/index.html"><span class="num">2</span><span>Prompt API</span></a>
|
||||
<a href="../devtools-agents/index.html"><span class="num">3</span><span>DevTools Agents</span></a>
|
||||
<a href="../modern-web-guidance/index.html"><span class="num">4</span><span>Modern Web Guidance</span></a>
|
||||
<a class="active" href="./index.html"><span class="num">5</span><span>Baseline</span></a>
|
||||
<a href="../view-transitions/index.html"><span class="num">6</span><span>View Transitions</span></a>
|
||||
</nav>
|
||||
<div class="note">本地矩阵用于现场讲述;生产判断仍应回到官方 Baseline 数据和项目目标浏览器。</div>
|
||||
</aside>
|
||||
|
||||
<section class="stage">
|
||||
<div class="hero">
|
||||
<section class="statement">
|
||||
<p class="eyebrow">Can I ship it?</p>
|
||||
<h2>从“Chrome 支持了”升级到“目标用户能稳定用”。</h2>
|
||||
<p>
|
||||
Baseline 的价值不是替代 feature detection,而是把团队讨论从版本号转成特性成熟度和采用策略。
|
||||
</p>
|
||||
</section>
|
||||
<aside class="metrics">
|
||||
<div class="metric"><strong id="ready-count">0</strong><span>当前环境可直接运行的能力</span></div>
|
||||
<div class="metric"><strong id="enhanced-count">0</strong><span>适合渐进增强的能力</span></div>
|
||||
<div class="metric"><strong id="blocked-count">0</strong><span>需要 fallback 或实验开关</span></div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<section class="two-col">
|
||||
<div class="panel">
|
||||
<p class="eyebrow">Feature matrix</p>
|
||||
<h2>本地特性检测</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>能力</th>
|
||||
<th>运行检测</th>
|
||||
<th>采用建议</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="feature-table"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<aside class="console">
|
||||
<p class="eyebrow">Decision log</p>
|
||||
<h2>判断依据</h2>
|
||||
<pre id="log">正在检测...</pre>
|
||||
</aside>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script type="module" src="./baseline.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
console.warn("Demo warning: this page intentionally includes diagnosable issues.");
|
||||
|
||||
const longTaskButton = document.querySelector("#long-task");
|
||||
const taskResult = document.querySelector("#task-result");
|
||||
|
||||
longTaskButton.addEventListener("click", () => {
|
||||
const start = performance.now();
|
||||
while (performance.now() - start < 650) {
|
||||
Math.sqrt(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
}
|
||||
const duration = Math.round(performance.now() - start);
|
||||
taskResult.textContent = `已触发约 ${duration}ms 的主线程长任务。`;
|
||||
console.error("Demo error: long task was triggered for DevTools MCP inspection.");
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DevTools for Agents Demo Target</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<section class="hero">
|
||||
<p class="eyebrow">Chrome DevTools for agents</p>
|
||||
<h1>给 Agent 一个真实浏览器现场</h1>
|
||||
<p>
|
||||
这个页面故意包含 console warning、404 图片、可触发长任务和布局溢出。
|
||||
演示时让 Chrome DevTools MCP 连接到这个页面,要求 Agent 读取 console、network、DOM 和 performance。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="prompt-board">
|
||||
<article>
|
||||
<p class="eyebrow">Official setup</p>
|
||||
<h2>MCP 配置</h2>
|
||||
<pre><code>{
|
||||
"mcpServers": {
|
||||
"chrome-devtools": {
|
||||
"command": "npx",
|
||||
"args": ["chrome-devtools-mcp@latest"]
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<p class="eyebrow">Prompt to run</p>
|
||||
<h2>现场提问</h2>
|
||||
<pre><code>Open http://localhost:3030/demo/devtools-agents/index.html.
|
||||
Inspect console and network errors.
|
||||
Then click "触发长任务" and record a performance trace.
|
||||
Summarize what you found.</code></pre>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="target">
|
||||
<div class="broken-card">
|
||||
<span>Network</span>
|
||||
<h2>这张图片故意 404</h2>
|
||||
<img src="./missing-dashboard.png" alt="Missing dashboard screenshot" />
|
||||
</div>
|
||||
|
||||
<div class="broken-card overflow">
|
||||
<span>Layout</span>
|
||||
<h2>这段文本故意撑破容器</h2>
|
||||
<p>SuperLongUnbrokenEnterpriseDashboardIdentifier_ChromeAgentRuntimeDebugging_2026_Overflow</p>
|
||||
</div>
|
||||
|
||||
<div class="broken-card">
|
||||
<span>Performance</span>
|
||||
<h2>触发可检测的长任务</h2>
|
||||
<button id="long-task">触发长任务</button>
|
||||
<p id="task-result">等待触发。</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="./app.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,139 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--ink: #111827;
|
||||
--muted: #667085;
|
||||
--paper: #f7f8fa;
|
||||
--panel: #ffffff;
|
||||
--line: #d7dce2;
|
||||
--red: #b42318;
|
||||
--blue: #1849a9;
|
||||
--code: #111827;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
background:
|
||||
linear-gradient(90deg, rgb(17 24 39 / 0.05) 1px, transparent 1px),
|
||||
linear-gradient(rgb(17 24 39 / 0.05) 1px, transparent 1px),
|
||||
var(--paper);
|
||||
background-size: 28px 28px;
|
||||
font-family: ui-sans-serif, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: min(1180px, calc(100vw - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 42px 0 60px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 10px;
|
||||
color: var(--red);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 760px;
|
||||
margin-bottom: 18px;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: clamp(42px, 7vw, 86px);
|
||||
line-height: 0.9;
|
||||
}
|
||||
|
||||
.hero p,
|
||||
.broken-card p {
|
||||
max-width: 760px;
|
||||
color: var(--muted);
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.prompt-board,
|
||||
.target {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.target {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
article,
|
||||
.broken-card {
|
||||
border: 1px solid var(--line);
|
||||
background: rgb(255 255 255 / 0.92);
|
||||
box-shadow: 0 18px 58px rgb(17 24 39 / 0.12);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
border: 1px solid #2f3947;
|
||||
background: var(--code);
|
||||
color: #e7edf4;
|
||||
padding: 16px;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.broken-card span {
|
||||
display: inline-flex;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid rgb(180 35 24 / 0.35);
|
||||
padding: 4px 9px;
|
||||
color: var(--red);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.broken-card img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
border: 1px dashed var(--red);
|
||||
background: #fff5f4;
|
||||
}
|
||||
|
||||
.overflow {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.overflow p {
|
||||
width: max-content;
|
||||
color: var(--blue);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 42px;
|
||||
border: 1px solid var(--ink);
|
||||
background: var(--ink);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
padding: 10px 14px;
|
||||
font: inherit;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.prompt-board,
|
||||
.target {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
+100
-535
@@ -3,581 +3,146 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Part 2 Demo - Agentic Web</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--ink: #17202a;
|
||||
--muted: #64717f;
|
||||
--line: #d9e1e8;
|
||||
--panel: #f7f9fb;
|
||||
--paper: #ffffff;
|
||||
--blue: #2357d4;
|
||||
--green: #0f7d58;
|
||||
--amber: #aa6a00;
|
||||
--red: #bb3e37;
|
||||
--violet: #5d4cc2;
|
||||
--shadow: 0 18px 60px rgb(45 62 80 / 0.13);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family:
|
||||
ui-sans-serif,
|
||||
"PingFang SC",
|
||||
"Microsoft YaHei",
|
||||
"Noto Sans SC",
|
||||
sans-serif;
|
||||
color: var(--ink);
|
||||
background:
|
||||
linear-gradient(90deg, rgb(23 32 42 / 0.05) 1px, transparent 1px),
|
||||
linear-gradient(rgb(23 32 42 / 0.05) 1px, transparent 1px),
|
||||
#eef3f7;
|
||||
background-size: 32px 32px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
min-height: 100vh;
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.rail,
|
||||
.stage {
|
||||
border: 1px solid var(--line);
|
||||
background: rgb(255 255 255 / 0.88);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.rail {
|
||||
position: sticky;
|
||||
top: 28px;
|
||||
align-self: start;
|
||||
min-height: calc(100vh - 56px);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 12px;
|
||||
color: var(--blue);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 11ch;
|
||||
margin-bottom: 18px;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: clamp(42px, 5vw, 74px);
|
||||
line-height: 0.92;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.lead {
|
||||
color: var(--muted);
|
||||
font-size: 16px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.quick {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin: 26px 0;
|
||||
}
|
||||
|
||||
.quick button {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
grid-template-columns: 26px 1fr;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 44px;
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.quick button:hover,
|
||||
.quick button.active {
|
||||
border-color: var(--blue);
|
||||
background: #eef4ff;
|
||||
}
|
||||
|
||||
.num {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background: var(--ink);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.note {
|
||||
border-left: 4px solid var(--amber);
|
||||
padding: 12px 14px;
|
||||
background: #fff7e8;
|
||||
color: #6f4b12;
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.stage {
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(280px, 0.8fr);
|
||||
gap: 22px;
|
||||
align-items: stretch;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.statement {
|
||||
min-height: 280px;
|
||||
padding: 28px;
|
||||
color: white;
|
||||
background:
|
||||
linear-gradient(135deg, rgb(23 32 42 / 0.94), rgb(35 87 212 / 0.82)),
|
||||
radial-gradient(circle at 15% 20%, rgb(255 255 255 / 0.18), transparent 28%);
|
||||
}
|
||||
|
||||
.statement h2 {
|
||||
max-width: 720px;
|
||||
margin-bottom: 18px;
|
||||
font-size: clamp(28px, 4vw, 54px);
|
||||
line-height: 1.02;
|
||||
}
|
||||
|
||||
.statement p {
|
||||
max-width: 760px;
|
||||
color: rgb(255 255 255 / 0.78);
|
||||
font-size: 17px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.metric strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.metric span {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
margin-bottom: 12px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.path-card,
|
||||
.resource-card {
|
||||
border: 1px solid var(--line);
|
||||
background: var(--paper);
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.path-card {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
margin-bottom: 14px;
|
||||
padding: 3px 9px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.tag.red {
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
.tag.green {
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
.tag.blue {
|
||||
background: var(--blue);
|
||||
}
|
||||
|
||||
.tag.violet {
|
||||
background: var(--violet);
|
||||
}
|
||||
|
||||
.path-card h3,
|
||||
.resource-card h3 {
|
||||
margin-bottom: 8px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.path-card p,
|
||||
.resource-card p {
|
||||
color: var(--muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.path-card ul {
|
||||
margin: 14px 0 0;
|
||||
padding-left: 18px;
|
||||
color: var(--muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
overflow: hidden;
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
border: 1px solid var(--line);
|
||||
padding: 14px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: var(--ink);
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table td {
|
||||
color: var(--muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.resources {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.resource-card {
|
||||
position: relative;
|
||||
min-height: 165px;
|
||||
transition:
|
||||
transform 160ms ease,
|
||||
box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.resource-card:hover {
|
||||
box-shadow: 0 12px 32px rgb(35 87 212 / 0.13);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.resource-card .url {
|
||||
color: var(--blue);
|
||||
font-size: 13px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.fallback {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.fallback > div {
|
||||
border: 1px solid var(--line);
|
||||
padding: 18px;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.fallback h3 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.fallback p {
|
||||
color: var(--muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.shell,
|
||||
.hero,
|
||||
.fallback {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.rail {
|
||||
position: static;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.cards,
|
||||
.resources {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<title>Chrome API Demo Desk</title>
|
||||
<link rel="stylesheet" href="./shared.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<aside class="rail">
|
||||
<p class="eyebrow">Part 2 Demo Desk</p>
|
||||
<h1>Agentic Web</h1>
|
||||
<p class="eyebrow">Public Demo Desk</p>
|
||||
<h1>Chrome API Lab</h1>
|
||||
<p class="lead">
|
||||
官方 demo 优先,本地页面只负责串联叙事和兜底。现场不依赖实验 flags,也不复刻所有 Chrome API。
|
||||
这里专门放可运行 demo。启动方式统一使用 <strong>npm run dev</strong>,再访问
|
||||
<strong>/demo/</strong> 下的不同能力页面。
|
||||
</p>
|
||||
<nav class="quick" aria-label="Demo sections">
|
||||
<button class="active" data-target="paths"><span class="num">1</span><span>三种操作路径</span></button>
|
||||
<button data-target="webmcp"><span class="num">2</span><span>WebMCP 可运行 demo</span></button>
|
||||
<button data-target="resources"><span class="num">3</span><span>官方 demo 资料</span></button>
|
||||
<button data-target="fallback"><span class="num">4</span><span>现场兜底讲法</span></button>
|
||||
<nav class="nav" aria-label="Chrome API demos">
|
||||
<a class="active" href="./index.html"><span class="num">0</span><span>Demo 总览</span></a>
|
||||
<a href="./webmcp/index.html"><span class="num">1</span><span>WebMCP</span></a>
|
||||
<a href="./prompt-api/index.html"><span class="num">2</span><span>Prompt API</span></a>
|
||||
<a href="./devtools-agents/index.html"><span class="num">3</span><span>DevTools Agents</span></a>
|
||||
<a href="./modern-web-guidance/index.html"><span class="num">4</span><span>Modern Web Guidance</span></a>
|
||||
<a href="../html-in-canvas/index.html"><span class="num">5</span><span>HTML-in-Canvas</span></a>
|
||||
<a href="./view-transitions/index.html"><span class="num">6</span><span>View Transitions</span></a>
|
||||
<a href="./baseline/index.html"><span class="num">7</span><span>Baseline</span></a>
|
||||
</nav>
|
||||
<div class="note">
|
||||
讲述原则:案例数据只说明趋势,不代表所有项目;实验能力必须标注支持边界和渐进增强。
|
||||
这部分只修改 public 静态代码,不进入 Slidev PPT 页面。实验 API 都按渐进增强处理,有能力检测和本地兜底。
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="stage">
|
||||
<div class="hero">
|
||||
<section class="statement">
|
||||
<p class="eyebrow">Core Message</p>
|
||||
<h2>从“AI 猜 UI”到“网页暴露工具”。</h2>
|
||||
<p class="eyebrow">Run with npm run dev</p>
|
||||
<h2>一个入口,分别展示不同 Chrome API 能力。</h2>
|
||||
<p>
|
||||
视觉识别和 DOM 识别让 Agent 尝试理解页面表象;WebMCP 的方向是让页面主动提供结构化能力,
|
||||
让 Agent 调用工具、传入参数、拿到结果。
|
||||
总览页负责现场导航;每个能力页都可以独立打开、独立演示,不依赖 PPT 路由。
|
||||
适合分享时从幻灯片跳出,展示真实浏览器中的 API 边界和 fallback。
|
||||
</p>
|
||||
</section>
|
||||
<aside class="metric-grid" aria-label="Demo principles">
|
||||
<aside class="metrics">
|
||||
<div class="metric">
|
||||
<strong>官方</strong>
|
||||
<span>优先展示 Chrome 官方文档、demo 和命令示例</span>
|
||||
<strong>7</strong>
|
||||
<span>WebMCP、Prompt API、DevTools Agents、Modern Web Guidance、HTML-in-Canvas、View Transitions、Baseline</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<strong>本地</strong>
|
||||
<span>WebMCP 提供可运行工程,其他实验能力用官方 demo 快闪</span>
|
||||
<strong>静态</strong>
|
||||
<span>全部放在 public 静态目录,由 Slidev dev server 直接托管</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<strong>15 分钟</strong>
|
||||
<span>建议 demo 段控制在 12-15 分钟</span>
|
||||
<strong>兜底</strong>
|
||||
<span>实验能力不可用时仍能讲清楚输入、输出和边界</span>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<section id="paths" class="section">
|
||||
<h2>同一个后台任务,三种 Agent 操作路径</h2>
|
||||
<div class="cards">
|
||||
<article class="path-card">
|
||||
<span class="tag red">Vision</span>
|
||||
<h3>看截图</h3>
|
||||
<p>Agent 通过视觉模型识别菜单、按钮、输入框,再一步步点击。</p>
|
||||
<section class="section">
|
||||
<h2>能力页面</h2>
|
||||
<div class="grid">
|
||||
<a class="card" href="./webmcp/index.html">
|
||||
<span class="tag blue">Agentic Web</span>
|
||||
<h3>WebMCP 工具契约</h3>
|
||||
<p>声明式表单工具、JavaScript 注册工具、工具 schema 和复合任务调用日志。</p>
|
||||
<ul>
|
||||
<li>优点:不需要改造页面</li>
|
||||
<li>风险:慢、贵、UI 变化敏感</li>
|
||||
<li>适合:临时探索和低风险页面</li>
|
||||
<li>能力检测:navigator.modelContext</li>
|
||||
<li>兜底:本地 mock runtime</li>
|
||||
</ul>
|
||||
</article>
|
||||
<article class="path-card">
|
||||
<span class="tag green">DOM / A11y</span>
|
||||
<h3>读结构</h3>
|
||||
<p>Agent 读取 DOM 或可访问性树,通过结构化信息定位交互元素。</p>
|
||||
</a>
|
||||
<a class="card" href="./prompt-api/index.html">
|
||||
<span class="tag green">Built-in AI</span>
|
||||
<h3>Prompt API</h3>
|
||||
<p>检测 LanguageModel,可用时创建本地模型会话,不可用时展示 mock 摘要流程。</p>
|
||||
<ul>
|
||||
<li>优点:比截图更结构化</li>
|
||||
<li>风险:复杂组件和隐藏状态仍会误导</li>
|
||||
<li>适合:语义清晰的表单页面</li>
|
||||
<li>能力检测:window.LanguageModel</li>
|
||||
<li>重点:本地模型、下载状态、渐进增强</li>
|
||||
</ul>
|
||||
</article>
|
||||
<article class="path-card">
|
||||
<span class="tag blue">WebMCP</span>
|
||||
<h3>调工具</h3>
|
||||
<p>页面暴露工具名、描述、参数 schema 和执行逻辑,Agent 直接调用业务能力。</p>
|
||||
</a>
|
||||
<a class="card" href="./devtools-agents/index.html">
|
||||
<span class="tag violet">Workflow</span>
|
||||
<h3>DevTools for Agents</h3>
|
||||
<p>提供一个故意带问题的页面,让 Chrome DevTools MCP 检查 console、network 和 performance。</p>
|
||||
<ul>
|
||||
<li>优点:语义明确、速度快、过程可控</li>
|
||||
<li>风险:仍需权限、确认和审计边界</li>
|
||||
<li>适合:复杂后台和重复流程</li>
|
||||
<li>官方工具:chrome-devtools-mcp</li>
|
||||
<li>重点:让 Agent 看到真实浏览器运行时</li>
|
||||
</ul>
|
||||
</article>
|
||||
</a>
|
||||
<a class="card" href="./modern-web-guidance/index.html">
|
||||
<span class="tag green">Guidance</span>
|
||||
<h3>Modern Web Guidance</h3>
|
||||
<p>展示官方 CLI 如何给 AI coding agent 检索现代 Web 用法和 Baseline 边界。</p>
|
||||
<ul>
|
||||
<li>重点:把现代 Web 知识注入 agent</li>
|
||||
<li>兜底:可复制命令和预期输出</li>
|
||||
</ul>
|
||||
</a>
|
||||
<a class="card" href="../html-in-canvas/index.html">
|
||||
<span class="tag amber">Origin Trial</span>
|
||||
<h3>HTML-in-Canvas</h3>
|
||||
<p>展示真实 DOM 表单绘制进 canvas 的 API 方向;不支持时显示 fallback 预览。</p>
|
||||
<ul>
|
||||
<li>能力检测:drawElementImage / requestPaint</li>
|
||||
<li>重点:不是 canvas 手绘控件</li>
|
||||
</ul>
|
||||
</a>
|
||||
<a class="card" href="./baseline/index.html">
|
||||
<span class="tag amber">Compatibility</span>
|
||||
<h3>Baseline</h3>
|
||||
<p>把“能不能放心用”从浏览器版本猜测,改成特性状态和 fallback 策略。</p>
|
||||
<ul>
|
||||
<li>重点:跨浏览器采用判断</li>
|
||||
<li>兜底:本地特性矩阵</li>
|
||||
</ul>
|
||||
</a>
|
||||
<a class="card" href="./view-transitions/index.html">
|
||||
<span class="tag red">UX API</span>
|
||||
<h3>View Transitions</h3>
|
||||
<p>展示 same-document 页面切换动画,并在不支持时退化为普通 DOM 更新。</p>
|
||||
<ul>
|
||||
<li>能力检测:document.startViewTransition</li>
|
||||
<li>重点:体验增强而非业务依赖</li>
|
||||
</ul>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>对比讲述表</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>维度</th>
|
||||
<th>视觉识别</th>
|
||||
<th>DOM / A11y</th>
|
||||
<th>WebMCP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>交互方式</td>
|
||||
<td>观察截图后猜测点击位置</td>
|
||||
<td>读取结构后定位元素</td>
|
||||
<td>调用页面声明的语义工具</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>稳定性</td>
|
||||
<td>受视觉布局影响明显</td>
|
||||
<td>受 DOM 结构和组件状态影响</td>
|
||||
<td>受工具设计、权限和业务逻辑影响</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>现场讲法</td>
|
||||
<td>“像让 AI 蒙着眼睛找按钮”</td>
|
||||
<td>“给了地图,但地图可能不完整”</td>
|
||||
<td>“给了工具入口和参数说明”</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<h2>本地运行</h2>
|
||||
<pre class="code"><code>npm run dev
|
||||
|
||||
<section id="webmcp" class="section">
|
||||
<h2>WebMCP 原始工程 demo</h2>
|
||||
<div class="fallback">
|
||||
<div>
|
||||
<h3>运行入口</h3>
|
||||
<p>
|
||||
打开 <a href="./webmcp/index.html">/demo/webmcp/index.html</a>,展示声明式表单工具、
|
||||
指令式 JS 工具、工具 schema 面板和复合任务调用日志。
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>讲述重点</h3>
|
||||
<p>
|
||||
先让人提交表单,再让工具面板执行同一类任务。观众会看到 Agent 不需要猜 UI,
|
||||
它看到的是工具名、描述、参数 schema 和执行结果。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
# Slidev dev server 启动后访问:
|
||||
http://localhost:3030/demo/index.html
|
||||
http://localhost:3030/demo/webmcp/index.html
|
||||
http://localhost:3030/demo/prompt-api/index.html
|
||||
http://localhost:3030/demo/devtools-agents/index.html
|
||||
http://localhost:3030/demo/modern-web-guidance/index.html
|
||||
http://localhost:3030/html-in-canvas/index.html
|
||||
http://localhost:3030/demo/baseline/index.html
|
||||
http://localhost:3030/demo/view-transitions/index.html
|
||||
|
||||
<section id="resources" class="section">
|
||||
<h2>官方 demo 与资料入口</h2>
|
||||
<div class="resources">
|
||||
<a class="resource-card" href="https://developer.chrome.com/docs/ai/webmcp/imperative-api" target="_blank" rel="noreferrer">
|
||||
<span class="tag blue">WebMCP</span>
|
||||
<h3>Imperative API</h3>
|
||||
<p>使用 navigator.modelContext.registerTool 暴露工具,官方示例包含 pizza layer 和 order status。</p>
|
||||
<div class="url">developer.chrome.com/docs/ai/webmcp/imperative-api</div>
|
||||
</a>
|
||||
<a class="resource-card" href="https://developer.chrome.com/docs/modern-web-guidance" target="_blank" rel="noreferrer">
|
||||
<span class="tag green">MWG</span>
|
||||
<h3>Modern Web Guidance</h3>
|
||||
<p>给 AI 编程工具补充现代 Web 知识、兼容性和最佳实践。</p>
|
||||
<div class="url">developer.chrome.com/docs/modern-web-guidance</div>
|
||||
</a>
|
||||
<a class="resource-card" href="https://developer.chrome.com/docs/devtools/agents/get-started" target="_blank" rel="noreferrer">
|
||||
<span class="tag violet">DevTools</span>
|
||||
<h3>Chrome DevTools for agents</h3>
|
||||
<p>让 coding agent 访问真实浏览器、console、network、performance trace 和 Lighthouse。</p>
|
||||
<div class="url">developer.chrome.com/docs/devtools/agents/get-started</div>
|
||||
</a>
|
||||
<a class="resource-card" href="https://developer.chrome.com/docs/ai/prompt-api" target="_blank" rel="noreferrer">
|
||||
<span class="tag blue">Prompt API</span>
|
||||
<h3>Built-in AI</h3>
|
||||
<p>浏览器内置语言模型能力,适合作为渐进增强展示。</p>
|
||||
<div class="url">developer.chrome.com/docs/ai/prompt-api</div>
|
||||
</a>
|
||||
<a class="resource-card" href="https://developer.chrome.com/blog/html-in-canvas-origin-trial" target="_blank" rel="noreferrer">
|
||||
<span class="tag red">Canvas</span>
|
||||
<h3>HTML-in-Canvas</h3>
|
||||
<p>把真实 DOM 放入 Canvas / WebGL / WebGPU 场景,同时保留浏览器能力。</p>
|
||||
<div class="url">developer.chrome.com/blog/html-in-canvas-origin-trial</div>
|
||||
</a>
|
||||
<a class="resource-card" href="https://view-transitions.chrome.dev/" target="_blank" rel="noreferrer">
|
||||
<span class="tag green">UX</span>
|
||||
<h3>View Transitions</h3>
|
||||
<p>官方交互 demo,用于快闪展示 Web 体验能力升级。</p>
|
||||
<div class="url">view-transitions.chrome.dev</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="fallback" class="section">
|
||||
<h2>现场兜底讲法</h2>
|
||||
<div class="fallback">
|
||||
<div>
|
||||
<h3>如果官方 demo 打不开</h3>
|
||||
<p>
|
||||
继续使用本页的卡片和对比表讲主线:AI 不擅长猜复杂 UI,WebMCP 的价值是结构化暴露工具。
|
||||
官方链接可作为会后资料。
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>如果有人追问落地</h3>
|
||||
<p>
|
||||
只回答原则:渐进增强、权限继承、高危确认、审计日志、工具按需暴露。
|
||||
不在本场扩展到厂商 SDK 选型和企业架构。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
# Modern Web Guidance 官方 CLI
|
||||
scripts/demo-modern-web-guidance.sh</code></pre>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const buttons = Array.from(document.querySelectorAll(".quick button"));
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
buttons.forEach((item) => item.classList.remove("active"));
|
||||
button.classList.add("active");
|
||||
document.getElementById(button.dataset.target)?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
const dialog = document.querySelector("#guided-dialog");
|
||||
const openDialog = document.querySelector("#open-dialog");
|
||||
const copyButton = document.querySelector("#copy");
|
||||
const commands = document.querySelector("#commands");
|
||||
|
||||
openDialog.addEventListener("click", () => {
|
||||
dialog.showModal();
|
||||
});
|
||||
|
||||
copyButton.addEventListener("click", async () => {
|
||||
await navigator.clipboard.writeText(commands.textContent.trim());
|
||||
copyButton.textContent = "已复制";
|
||||
setTimeout(() => {
|
||||
copyButton.textContent = "复制命令";
|
||||
}, 1200);
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Modern Web Guidance Demo</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<section class="hero">
|
||||
<p class="eyebrow">Official CLI demo</p>
|
||||
<h1>Modern Web Guidance</h1>
|
||||
<p>
|
||||
这段演示直接使用 Chrome 官方 CLI:先搜索某个 Web 开发意图,再取回对应指南。
|
||||
重点不是背 API,而是让开发者看到 AI coding agent 如何被现代 Web 知识和 Baseline 边界纠偏。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="grid">
|
||||
<article class="terminal">
|
||||
<div class="terminal-head">
|
||||
<span></span><span></span><span></span>
|
||||
<strong>terminal</strong>
|
||||
</div>
|
||||
<pre><code id="commands">npx modern-web-guidance@latest search "animate a dialog modal backdrop"
|
||||
npx modern-web-guidance@latest retrieve "animate-to-from-top-layer"</code></pre>
|
||||
<button id="copy">复制命令</button>
|
||||
</article>
|
||||
|
||||
<article class="result">
|
||||
<p class="eyebrow">Expected official output</p>
|
||||
<h2>搜索会返回 use case</h2>
|
||||
<ul>
|
||||
<li><strong>light-dismiss-a-dialog</strong>:用原生 dialog 处理 light dismiss。</li>
|
||||
<li><strong>animate-to-from-top-layer</strong>:用现代 CSS 动画进入/离开 top layer。</li>
|
||||
<li><strong>declarative-dialog-popover-control</strong>:用声明式控制 dialog / popover。</li>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="demo">
|
||||
<div>
|
||||
<p class="eyebrow">Guided implementation</p>
|
||||
<h2>指南会把 Agent 拉回现代 Web 写法</h2>
|
||||
<p>
|
||||
这个页面用一个可运行的 dialog 动画做对应说明:优先使用原生 <code><dialog></code>、
|
||||
<code>::backdrop</code>、<code>@starting-style</code> 和 <code>transition-behavior</code>,
|
||||
而不是用大量 JavaScript 重新造弹层。
|
||||
</p>
|
||||
<button id="open-dialog">打开 dialog demo</button>
|
||||
</div>
|
||||
|
||||
<pre class="code"><code>dialog {
|
||||
opacity: 0;
|
||||
transform: translateY(16px) scale(0.96);
|
||||
transition-property: opacity, transform, display, overlay;
|
||||
transition-duration: 220ms;
|
||||
transition-behavior: allow-discrete;
|
||||
}
|
||||
|
||||
dialog[open] {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
dialog[open] {
|
||||
opacity: 0;
|
||||
transform: translateY(16px) scale(0.96);
|
||||
}
|
||||
}</code></pre>
|
||||
</section>
|
||||
|
||||
<dialog id="guided-dialog">
|
||||
<form method="dialog">
|
||||
<p class="eyebrow">Native top layer</p>
|
||||
<h2>这是原生 dialog</h2>
|
||||
<p>演示重点:Modern Web Guidance 让 Agent 找到现代 HTML/CSS 能力,并提醒兼容和降级边界。</p>
|
||||
<button>关闭</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</main>
|
||||
|
||||
<script src="./app.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,222 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--ink: #17202a;
|
||||
--muted: #5f6f78;
|
||||
--paper: #fffef8;
|
||||
--panel: #eff4ef;
|
||||
--line: #cfd9cd;
|
||||
--green: #0b6b4f;
|
||||
--blue: #174ea6;
|
||||
--amber: #a46400;
|
||||
--code: #14202a;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
color: var(--ink);
|
||||
background:
|
||||
linear-gradient(90deg, rgb(23 32 42 / 0.05) 1px, transparent 1px),
|
||||
linear-gradient(rgb(23 32 42 / 0.05) 1px, transparent 1px),
|
||||
#edf4ef;
|
||||
background-size: 30px 30px;
|
||||
font-family: ui-sans-serif, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 42px;
|
||||
border: 1px solid var(--ink);
|
||||
background: var(--ink);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
padding: 10px 14px;
|
||||
font: inherit;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: min(1180px, calc(100vw - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 40px 0 64px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 10px;
|
||||
color: var(--green);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 780px;
|
||||
margin-bottom: 14px;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: clamp(42px, 7vw, 86px);
|
||||
line-height: 0.9;
|
||||
}
|
||||
|
||||
.hero p,
|
||||
.demo p,
|
||||
.result li {
|
||||
color: var(--muted);
|
||||
line-height: 1.72;
|
||||
}
|
||||
|
||||
.grid,
|
||||
.demo {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(320px, 0.82fr);
|
||||
gap: 20px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.terminal,
|
||||
.result,
|
||||
.demo {
|
||||
border: 1px solid var(--line);
|
||||
background: rgb(255 254 248 / 0.92);
|
||||
box-shadow: 0 18px 58px rgb(23 32 42 / 0.12);
|
||||
}
|
||||
|
||||
.terminal,
|
||||
.result {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
background: var(--code);
|
||||
color: #dfe8ef;
|
||||
}
|
||||
|
||||
.terminal-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.terminal-head span {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 999px;
|
||||
background: #e15c4f;
|
||||
}
|
||||
|
||||
.terminal-head span:nth-child(2) {
|
||||
background: #e5ad35;
|
||||
}
|
||||
|
||||
.terminal-head span:nth-child(3) {
|
||||
background: #42aa6d;
|
||||
}
|
||||
|
||||
.terminal-head strong {
|
||||
margin-left: 8px;
|
||||
color: #9fb0be;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
margin: 0 0 16px;
|
||||
white-space: pre-wrap;
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.result ul {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.demo {
|
||||
align-items: start;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
code {
|
||||
border: 1px solid rgb(23 32 42 / 0.13);
|
||||
background: #fff;
|
||||
padding: 1px 5px;
|
||||
}
|
||||
|
||||
.code {
|
||||
border: 1px solid #2d3944;
|
||||
background: var(--code);
|
||||
color: #dfe8ef;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
dialog {
|
||||
width: min(520px, calc(100vw - 36px));
|
||||
border: 1px solid var(--line);
|
||||
padding: 0;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
opacity: 0;
|
||||
transform: translateY(16px) scale(0.96);
|
||||
transition-property: opacity, transform, display, overlay;
|
||||
transition-duration: 220ms;
|
||||
transition-timing-function: ease-out;
|
||||
transition-behavior: allow-discrete;
|
||||
}
|
||||
|
||||
dialog[open] {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
dialog[open] {
|
||||
opacity: 0;
|
||||
transform: translateY(16px) scale(0.96);
|
||||
}
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background: rgb(12 18 24 / 0);
|
||||
transition:
|
||||
display 220ms allow-discrete,
|
||||
overlay 220ms allow-discrete,
|
||||
background-color 220ms ease-out;
|
||||
}
|
||||
|
||||
dialog[open]::backdrop {
|
||||
background: rgb(12 18 24 / 0.42);
|
||||
}
|
||||
|
||||
dialog form {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
dialog {
|
||||
transform: none;
|
||||
transition-duration: 80ms;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 880px) {
|
||||
.grid,
|
||||
.demo {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Prompt API Demo</title>
|
||||
<link rel="stylesheet" href="../shared.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<aside class="rail">
|
||||
<p class="eyebrow">Built-in AI</p>
|
||||
<h1>Prompt API</h1>
|
||||
<p class="lead">
|
||||
演示网页如何检测 Chrome 内置语言模型能力,并在不可用时退回到可解释的本地 mock。
|
||||
</p>
|
||||
<nav class="nav" aria-label="Demo navigation">
|
||||
<a href="../index.html"><span class="num">0</span><span>返回总览</span></a>
|
||||
<a href="../webmcp/index.html"><span class="num">1</span><span>WebMCP</span></a>
|
||||
<a class="active" href="./index.html"><span class="num">2</span><span>Prompt API</span></a>
|
||||
<a href="../devtools-agents/index.html"><span class="num">3</span><span>DevTools Agents</span></a>
|
||||
<a href="../modern-web-guidance/index.html"><span class="num">4</span><span>Modern Web Guidance</span></a>
|
||||
<a href="../baseline/index.html"><span class="num">5</span><span>Baseline</span></a>
|
||||
<a href="../view-transitions/index.html"><span class="num">6</span><span>View Transitions</span></a>
|
||||
</nav>
|
||||
<div class="note">
|
||||
讲述重点:Prompt API 是渐进增强。先检测 availability,再由用户动作触发模型创建和下载进度。
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="stage">
|
||||
<div class="hero">
|
||||
<section class="statement">
|
||||
<p class="eyebrow">window.LanguageModel</p>
|
||||
<h2>把浏览器内置模型当作页面能力,而不是远程服务。</h2>
|
||||
<p>
|
||||
可用时,页面直接创建本地模型会话;不可用时,页面仍能展示同一套输入、输出和错误边界,
|
||||
让观众理解这个 API 的产品形态。
|
||||
</p>
|
||||
</section>
|
||||
<aside class="metrics">
|
||||
<div class="metric">
|
||||
<strong id="support-status">检测中</strong>
|
||||
<span>当前浏览器 Prompt API 状态</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<strong id="availability">未知</strong>
|
||||
<span>LanguageModel.availability() 结果</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<strong id="mode">未运行</strong>
|
||||
<span>native 或 mock 执行路径</span>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<section class="two-col">
|
||||
<div class="panel">
|
||||
<p class="eyebrow">Task</p>
|
||||
<h2>把 Chrome 更新摘要改写成给业务同事看的说明</h2>
|
||||
<label class="field">
|
||||
输入材料
|
||||
<textarea id="prompt-input">Chrome 正在把浏览器能力分成三层:面向用户的体验 API、面向开发者的 DevTools agent workflow,以及面向 AI Agent 的 WebMCP 和 Built-in AI。</textarea>
|
||||
</label>
|
||||
<label class="field">
|
||||
输出风格
|
||||
<select id="tone">
|
||||
<option value="业务说明">业务说明</option>
|
||||
<option value="技术摘要">技术摘要</option>
|
||||
<option value="现场讲稿">现场讲稿</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="controls">
|
||||
<button id="run">运行 Prompt API</button>
|
||||
<button id="check" class="secondary">重新检测</button>
|
||||
</div>
|
||||
<pre class="code"><code>const availability = await LanguageModel.availability();
|
||||
const session = await LanguageModel.create({
|
||||
monitor(m) {
|
||||
m.addEventListener("downloadprogress", updateProgress);
|
||||
},
|
||||
});
|
||||
const result = await session.prompt(prompt);</code></pre>
|
||||
</div>
|
||||
|
||||
<aside class="console">
|
||||
<p class="eyebrow">Execution</p>
|
||||
<h2>输出与日志</h2>
|
||||
<pre id="log">等待运行...</pre>
|
||||
</aside>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script type="module" src="./prompt-api.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,74 @@
|
||||
import { logTo, setText } from "../shared.js";
|
||||
|
||||
const hasPromptApi = "LanguageModel" in window;
|
||||
|
||||
document.querySelector("#check").addEventListener("click", checkAvailability);
|
||||
document.querySelector("#run").addEventListener("click", runPrompt);
|
||||
|
||||
await checkAvailability();
|
||||
|
||||
async function checkAvailability() {
|
||||
setText("#support-status", hasPromptApi ? "支持" : "未暴露");
|
||||
setText("#mode", hasPromptApi ? "Native" : "Mock");
|
||||
|
||||
if (!hasPromptApi) {
|
||||
setText("#availability", "unavailable");
|
||||
logTo("#log", "prompt-api.detect", {
|
||||
supported: false,
|
||||
fallback: "Use local mock response for the presentation.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const availability = await LanguageModel.availability();
|
||||
setText("#availability", availability);
|
||||
logTo("#log", "prompt-api.availability", { availability });
|
||||
} catch (error) {
|
||||
setText("#availability", "error");
|
||||
logTo("#log", "prompt-api.error", { message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function runPrompt() {
|
||||
const source = document.querySelector("#prompt-input").value.trim();
|
||||
const tone = document.querySelector("#tone").value;
|
||||
const prompt = `请用${tone}风格,用三句话解释下面内容:\n\n${source}`;
|
||||
|
||||
if (!hasPromptApi) {
|
||||
const result = createMockSummary(source, tone);
|
||||
logTo("#log", "prompt-api.mock-result", { prompt, result });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setText("#mode", "Native running");
|
||||
const session = await LanguageModel.create({
|
||||
monitor(monitor) {
|
||||
monitor.addEventListener("downloadprogress", (event) => {
|
||||
const percent = Math.round(event.loaded * 100);
|
||||
setText("#availability", `downloading ${percent}%`);
|
||||
});
|
||||
},
|
||||
});
|
||||
const result = await session.prompt(prompt);
|
||||
setText("#mode", "Native complete");
|
||||
logTo("#log", "prompt-api.native-result", { prompt, result });
|
||||
} catch (error) {
|
||||
setText("#mode", "Native error");
|
||||
logTo("#log", "prompt-api.native-error", {
|
||||
prompt,
|
||||
message: error.message,
|
||||
fallback: createMockSummary(source, tone),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createMockSummary(source, tone) {
|
||||
const first = source.split(/[。.!?]/).find(Boolean)?.trim() || source;
|
||||
return [
|
||||
`[${tone}] Chrome 的新能力可以理解为三条线同时推进。`,
|
||||
`第一条是用户体验升级,第二条是开发者工具升级,第三条是让网页能被 AI Agent 结构化调用。`,
|
||||
`这是一段 mock 输出,用来在当前浏览器没有 Prompt API 时保持演示可运行;输入摘要:${first}。`,
|
||||
].join("");
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--ink: #121417;
|
||||
--muted: #65727d;
|
||||
--paper: #fffdf8;
|
||||
--panel: #f4f1e8;
|
||||
--line: #d7d0bf;
|
||||
--blue: #164db3;
|
||||
--green: #0c7356;
|
||||
--amber: #a26000;
|
||||
--red: #b03831;
|
||||
--violet: #6747b8;
|
||||
--code: #18222d;
|
||||
--shadow: 0 20px 60px rgb(35 37 42 / 0.13);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
background:
|
||||
linear-gradient(90deg, rgb(18 20 23 / 0.055) 1px, transparent 1px),
|
||||
linear-gradient(rgb(18 20 23 / 0.055) 1px, transparent 1px),
|
||||
#ece6d8;
|
||||
background-size: 30px 30px;
|
||||
font-family: Avenir Next, Trebuchet MS, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 350px) minmax(0, 1fr);
|
||||
gap: 22px;
|
||||
min-height: 100vh;
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.rail,
|
||||
.stage,
|
||||
.panel,
|
||||
.card,
|
||||
.console {
|
||||
border: 1px solid var(--line);
|
||||
background: rgb(255 253 248 / 0.92);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.rail {
|
||||
position: sticky;
|
||||
top: 28px;
|
||||
align-self: start;
|
||||
min-height: calc(100vh - 56px);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.stage {
|
||||
padding: 26px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 8px;
|
||||
color: var(--blue);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 11ch;
|
||||
margin-bottom: 16px;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: clamp(40px, 5vw, 72px);
|
||||
line-height: 0.94;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 12px;
|
||||
font-size: clamp(24px, 3vw, 38px);
|
||||
line-height: 1.05;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 8px;
|
||||
font-size: 19px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.lead,
|
||||
.muted,
|
||||
.card p,
|
||||
.panel p,
|
||||
li {
|
||||
color: var(--muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin: 26px 0;
|
||||
}
|
||||
|
||||
.nav a,
|
||||
.nav button,
|
||||
.link-button,
|
||||
.primary,
|
||||
.ghost {
|
||||
display: inline-grid;
|
||||
grid-template-columns: 28px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 44px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.nav a:hover,
|
||||
.nav a.active,
|
||||
.nav button:hover,
|
||||
.link-button:hover,
|
||||
.ghost:hover {
|
||||
border-color: var(--blue);
|
||||
background: #eef4ff;
|
||||
}
|
||||
|
||||
.primary {
|
||||
border-color: var(--ink);
|
||||
background: var(--ink);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.num,
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: var(--ink);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.note {
|
||||
border-left: 4px solid var(--amber);
|
||||
background: #fff3d6;
|
||||
color: #6f4707;
|
||||
padding: 12px 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.25fr) minmax(250px, 0.75fr);
|
||||
gap: 18px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.statement {
|
||||
min-height: 250px;
|
||||
padding: 28px;
|
||||
color: white;
|
||||
background:
|
||||
linear-gradient(135deg, rgb(18 20 23 / 0.96), rgb(22 77 179 / 0.83)),
|
||||
radial-gradient(circle at 20% 18%, rgb(255 255 255 / 0.22), transparent 26%);
|
||||
}
|
||||
|
||||
.statement .eyebrow,
|
||||
.statement p {
|
||||
color: rgb(255 255 255 / 0.78);
|
||||
}
|
||||
|
||||
.statement h2 {
|
||||
max-width: 760px;
|
||||
margin-bottom: 16px;
|
||||
font-size: clamp(30px, 5vw, 58px);
|
||||
}
|
||||
|
||||
.metrics,
|
||||
.grid,
|
||||
.two-col {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.metric,
|
||||
.panel,
|
||||
.card,
|
||||
.console {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.metric strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.metric span {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.two-col {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(340px, 0.82fr);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.tag,
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 26px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid transparent;
|
||||
padding: 3px 9px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.tag.blue,
|
||||
.pill.blue {
|
||||
background: var(--blue);
|
||||
}
|
||||
|
||||
.tag.green,
|
||||
.pill.green {
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
.tag.amber,
|
||||
.pill.amber {
|
||||
background: var(--amber);
|
||||
}
|
||||
|
||||
.tag.red,
|
||||
.pill.red {
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
.tag.violet,
|
||||
.pill.violet {
|
||||
background: var(--violet);
|
||||
}
|
||||
|
||||
.pill.neutral {
|
||||
border-color: var(--line);
|
||||
background: var(--paper);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.card {
|
||||
min-height: 190px;
|
||||
transition:
|
||||
transform 160ms ease,
|
||||
box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 14px 34px rgb(22 77 179 / 0.16);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card ul {
|
||||
margin: 14px 0 0;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
min-height: 42px;
|
||||
border: 1px solid var(--ink);
|
||||
background: var(--ink);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
padding: 9px 13px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.controls button.secondary {
|
||||
border-color: var(--line);
|
||||
background: white;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
background: white;
|
||||
color: var(--ink);
|
||||
padding: 11px 12px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 110px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.code,
|
||||
.console pre {
|
||||
overflow: auto;
|
||||
border: 1px solid #303b48;
|
||||
background: var(--code);
|
||||
color: #d8e4ef;
|
||||
padding: 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.code {
|
||||
margin: 14px 0 0;
|
||||
}
|
||||
|
||||
.console {
|
||||
align-self: start;
|
||||
position: sticky;
|
||||
top: 28px;
|
||||
}
|
||||
|
||||
.console pre {
|
||||
max-height: 460px;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.status-row,
|
||||
.list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.status {
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.status span {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
border: 1px solid var(--line);
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: var(--ink);
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table td {
|
||||
color: var(--muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
@media (max-width: 1020px) {
|
||||
.shell,
|
||||
.hero,
|
||||
.two-col {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.rail,
|
||||
.console {
|
||||
position: static;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.grid,
|
||||
.status-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export function logTo(target, title, payload) {
|
||||
const el = typeof target === "string" ? document.querySelector(target) : target;
|
||||
if (!el) return;
|
||||
const stamp = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
||||
const entry = {
|
||||
time: stamp,
|
||||
event: title,
|
||||
...payload,
|
||||
};
|
||||
el.textContent = `${JSON.stringify(entry, null, 2)}\n\n${el.textContent}`.trim();
|
||||
}
|
||||
|
||||
export function setText(selector, value) {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) el.textContent = value;
|
||||
}
|
||||
|
||||
export function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
const panel = document.querySelector("#panel");
|
||||
const buttons = Array.from(document.querySelectorAll("[data-view]"));
|
||||
|
||||
const views = {
|
||||
orders: {
|
||||
title: "订单工作台",
|
||||
body: "状态从列表切换到业务面板,浏览器负责把 DOM 更新过渡成连续动画。",
|
||||
metrics: ["128 个待处理", "96.4% 准时率"],
|
||||
},
|
||||
analytics: {
|
||||
title: "实时分析",
|
||||
body: "适合展示页面状态、卡片位置、详情视图之间的平滑切换。",
|
||||
metrics: ["42ms INP", "3 条异常趋势"],
|
||||
},
|
||||
settings: {
|
||||
title: "权限设置",
|
||||
body: "在支持的浏览器里使用 View Transitions;不支持时保留普通状态切换。",
|
||||
metrics: ["7 个角色", "24 项权限"],
|
||||
},
|
||||
};
|
||||
|
||||
function render(view) {
|
||||
const data = views[view];
|
||||
panel.innerHTML = `
|
||||
<article class="card">
|
||||
<p class="eyebrow">Current view</p>
|
||||
<h2>${data.title}</h2>
|
||||
<p>${data.body}</p>
|
||||
</article>
|
||||
<section class="summary">
|
||||
${data.metrics
|
||||
.map((metric) => `<article class="card"><strong>${metric}</strong><span>View transition keeps the context visible.</span></article>`)
|
||||
.join("")}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function setActive(view) {
|
||||
buttons.forEach((button) => {
|
||||
button.classList.toggle("active", button.dataset.view === view);
|
||||
});
|
||||
render(view);
|
||||
}
|
||||
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const next = button.dataset.view;
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(() => setActive(next));
|
||||
return;
|
||||
}
|
||||
setActive(next);
|
||||
});
|
||||
});
|
||||
|
||||
setActive("orders");
|
||||
@@ -0,0 +1,29 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>View Transitions Demo</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<section class="hero">
|
||||
<p class="eyebrow">Chrome UI capability</p>
|
||||
<h1>View Transitions</h1>
|
||||
<p>点击不同状态,观察浏览器把 DOM 更新包装成一次连续过渡。没有支持时,页面仍然即时切换。</p>
|
||||
</section>
|
||||
|
||||
<section class="app" id="app">
|
||||
<nav class="tabs" aria-label="Demo states">
|
||||
<button class="active" data-view="orders">订单</button>
|
||||
<button data-view="analytics">分析</button>
|
||||
<button data-view="settings">设置</button>
|
||||
</nav>
|
||||
<div class="panel" id="panel"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="./app.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,126 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--ink: #17202a;
|
||||
--muted: #66717c;
|
||||
--paper: #f3f5f7;
|
||||
--panel: #ffffff;
|
||||
--line: #d8dee6;
|
||||
--blue: #1a5fb4;
|
||||
--green: #16784e;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
color: var(--ink);
|
||||
background: var(--paper);
|
||||
font-family: ui-sans-serif, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: min(1040px, calc(100vw - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 42px 0 60px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 10px;
|
||||
color: var(--blue);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 14px;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: clamp(44px, 8vw, 96px);
|
||||
line-height: 0.9;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
max-width: 680px;
|
||||
color: var(--muted);
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.app {
|
||||
margin-top: 26px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
box-shadow: 0 22px 70px rgb(23 32 42 / 0.14);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 40px;
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
padding: 9px 14px;
|
||||
font: inherit;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
button.active {
|
||||
border-color: var(--blue);
|
||||
background: #e8f0ff;
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
min-height: 380px;
|
||||
padding: 22px;
|
||||
view-transition-name: demo-panel;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 0.8fr;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.card strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
::view-transition-old(demo-panel),
|
||||
::view-transition-new(demo-panel) {
|
||||
animation-duration: 320ms;
|
||||
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.tabs,
|
||||
.summary {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
</div>
|
||||
<div class="runtime">
|
||||
<span id="native-status" class="pill">检测中</span>
|
||||
<a class="demo-home" href="../index.html">Demo 总览</a>
|
||||
<a href="https://developer.chrome.com/docs/ai/webmcp" target="_blank" rel="noreferrer">Chrome 官方文档</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -80,11 +80,24 @@ h2 {
|
||||
|
||||
.runtime {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.demo-home {
|
||||
border: 1px solid var(--line);
|
||||
background: var(--paper);
|
||||
padding: 6px 10px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.demo-home:hover {
|
||||
border-color: var(--blue);
|
||||
background: #eef4ff;
|
||||
}
|
||||
|
||||
.pill,
|
||||
.mode {
|
||||
display: inline-flex;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const drawElement = document.getElementById('draw-element');
|
||||
const statusPanel = document.getElementById('support-status');
|
||||
const demoForm = document.getElementById('demo-form');
|
||||
|
||||
const hasHtmlInCanvas =
|
||||
typeof ctx.drawElementImage === 'function' &&
|
||||
typeof canvas.requestPaint === 'function';
|
||||
|
||||
function setStatus(state, message) {
|
||||
statusPanel.dataset.state = state;
|
||||
statusPanel.textContent = message;
|
||||
}
|
||||
|
||||
function resizeCanvas(entry) {
|
||||
const dpcSize = entry.devicePixelContentBoxSize?.[0];
|
||||
const nextWidth = dpcSize?.inlineSize ?? Math.round(entry.contentRect.width * window.devicePixelRatio);
|
||||
const nextHeight = dpcSize?.blockSize ?? Math.round(entry.contentRect.height * window.devicePixelRatio);
|
||||
|
||||
if (canvas.width !== nextWidth) {
|
||||
canvas.width = nextWidth;
|
||||
}
|
||||
|
||||
if (canvas.height !== nextHeight) {
|
||||
canvas.height = nextHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function paint() {
|
||||
ctx.reset();
|
||||
ctx.fillStyle = '#101820';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const x = canvas.width / 25;
|
||||
const y = canvas.height / 25;
|
||||
const transform = ctx.drawElementImage(drawElement, x, y);
|
||||
drawElement.style.transform = transform.toString();
|
||||
}
|
||||
|
||||
demoForm.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
setStatus('ok', '表单提交事件来自真实 DOM:发射指令已拦截,demo 仍停留在当前页面。');
|
||||
});
|
||||
|
||||
if (!hasHtmlInCanvas) {
|
||||
document.body.dataset.api = 'fallback';
|
||||
setStatus(
|
||||
'fail',
|
||||
'当前浏览器未暴露 drawElementImage/requestPaint。请使用 Chrome Canary 149+ 并开启 canvas-draw-element flag。'
|
||||
);
|
||||
} else {
|
||||
document.body.dataset.api = 'native';
|
||||
setStatus('ok', '已检测到 HTML-in-Canvas 实验 API,可以操作 canvas 中绘制的 DOM 表单。');
|
||||
canvas.onpaint = paint;
|
||||
|
||||
const supportsDevicePixelContentBox =
|
||||
typeof ResizeObserverEntry !== 'undefined' &&
|
||||
'devicePixelContentBoxSize' in ResizeObserverEntry.prototype;
|
||||
const resizeOptions = supportsDevicePixelContentBox ? { box: 'device-pixel-content-box' } : {};
|
||||
|
||||
const observer = new ResizeObserver(([entry]) => {
|
||||
resizeCanvas(entry);
|
||||
canvas.requestPaint();
|
||||
});
|
||||
|
||||
observer.observe(canvas, resizeOptions);
|
||||
canvas.requestPaint();
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>HTML-in-Canvas API Demo</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="page-shell">
|
||||
<section class="intro" aria-labelledby="page-title">
|
||||
<p class="eyebrow">Chrome Origin Trial Demo</p>
|
||||
<h1 id="page-title">HTML-in-Canvas API</h1>
|
||||
<p class="lede">
|
||||
这个页面基于 WICG 官方示例改写,演示把真实 DOM 表单绘制进
|
||||
<code><canvas></code>,同时保留输入、选择、滑块和按钮的交互能力。
|
||||
</p>
|
||||
|
||||
<div class="status-panel" id="support-status" role="status">
|
||||
正在检测当前浏览器是否支持实验 API...
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="demo-grid" aria-label="HTML-in-Canvas 演示">
|
||||
<div class="canvas-stage">
|
||||
<canvas id="canvas" width="638" height="318" layoutsubtree="true">
|
||||
<div id="draw-element" class="control-panel">
|
||||
<form id="demo-form" action="#" method="get">
|
||||
<fieldset>
|
||||
<legend>飞船控制台</legend>
|
||||
<label class="field-row" for="ship-name">
|
||||
<span>飞船名称</span>
|
||||
<input type="text" id="ship-name" value="Canvas 远航者" />
|
||||
</label>
|
||||
|
||||
<label class="check-row" for="hyperdrive">
|
||||
<input type="checkbox" id="hyperdrive" checked />
|
||||
<span>启动超空间引擎</span>
|
||||
</label>
|
||||
|
||||
<fieldset class="nested-fieldset">
|
||||
<legend>目标星系</legend>
|
||||
<label class="check-row" for="alpha">
|
||||
<input type="radio" id="alpha" name="system" value="alpha" checked />
|
||||
<span>半人马座 Alpha</span>
|
||||
</label>
|
||||
<label class="check-row" for="beta">
|
||||
<input type="radio" id="beta" name="system" value="beta" />
|
||||
<span>参宿四 Betelgeuse</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<label class="field-row" for="shield-level">
|
||||
<span>护盾强度</span>
|
||||
<input type="range" id="shield-level" min="0" max="100" value="75" />
|
||||
</label>
|
||||
|
||||
<button type="submit">发射</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</canvas>
|
||||
<div class="fallback-preview" aria-hidden="true">
|
||||
<p class="fallback-label">当前浏览器未启用实验 API,下面显示同一 DOM 表单的静态预览。</p>
|
||||
<div class="control-panel preview-panel">
|
||||
<fieldset>
|
||||
<legend>飞船控制台</legend>
|
||||
<label class="field-row">
|
||||
<span>飞船名称</span>
|
||||
<input type="text" value="Canvas 远航者" disabled />
|
||||
</label>
|
||||
<label class="check-row">
|
||||
<input type="checkbox" checked disabled />
|
||||
<span>启动超空间引擎</span>
|
||||
</label>
|
||||
<fieldset class="nested-fieldset">
|
||||
<legend>目标星系</legend>
|
||||
<label class="check-row">
|
||||
<input type="radio" name="preview-system" checked disabled />
|
||||
<span>半人马座 Alpha</span>
|
||||
</label>
|
||||
<label class="check-row">
|
||||
<input type="radio" name="preview-system" disabled />
|
||||
<span>参宿四 Betelgeuse</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<label class="field-row">
|
||||
<span>护盾强度</span>
|
||||
<input type="range" min="0" max="100" value="75" disabled />
|
||||
</label>
|
||||
<button type="button" disabled>发射</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="notes" aria-label="运行说明">
|
||||
<h2>运行条件</h2>
|
||||
<p>
|
||||
Chrome 官方文章说明:该 API 在 Chrome 148-150 处于源试用阶段,本地测试需要
|
||||
Chrome Canary 149+,并开启 <code>chrome://flags/#canvas-draw-element</code>。
|
||||
</p>
|
||||
<h2>可验证点</h2>
|
||||
<ul>
|
||||
<li>表单控件来自真实 DOM,不是 canvas 手绘控件。</li>
|
||||
<li>输入框、复选框、单选项、滑块和按钮可直接操作。</li>
|
||||
<li>JS 每次 paint 后同步 DOM transform,让命中区域与 canvas 绘制位置对齐。</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="./app.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,292 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--ink: #17202a;
|
||||
--muted: #5d6670;
|
||||
--panel: #f7f4ec;
|
||||
--canvas: #101820;
|
||||
--accent: #ffb000;
|
||||
--accent-strong: #e04f2f;
|
||||
--line: #263241;
|
||||
--ok: #1d7f4f;
|
||||
--warn: #9a5b00;
|
||||
font-family: "Avenir Next", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
color: var(--ink);
|
||||
background:
|
||||
linear-gradient(90deg, rgba(23, 32, 42, 0.06) 1px, transparent 1px),
|
||||
linear-gradient(rgba(23, 32, 42, 0.05) 1px, transparent 1px),
|
||||
#fffaf0;
|
||||
background-size: 28px 28px;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
code {
|
||||
border: 1px solid rgba(23, 32, 42, 0.16);
|
||||
border-radius: 4px;
|
||||
padding: 0.08rem 0.28rem;
|
||||
background: rgba(255, 255, 255, 0.74);
|
||||
}
|
||||
|
||||
.page-shell {
|
||||
width: min(1120px, calc(100vw - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 44px 0 56px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
color: var(--accent-strong);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 760px;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: clamp(3rem, 6vw, 5.8rem);
|
||||
line-height: 0.92;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.lede {
|
||||
max-width: 760px;
|
||||
color: var(--muted);
|
||||
font-size: 1.06rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
border: 1px solid rgba(23, 32, 42, 0.18);
|
||||
border-left: 6px solid var(--warn);
|
||||
border-radius: 6px;
|
||||
padding: 10px 14px;
|
||||
color: var(--warn);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-panel[data-state="ok"] {
|
||||
border-left-color: var(--ok);
|
||||
color: var(--ok);
|
||||
}
|
||||
|
||||
.status-panel[data-state="fail"] {
|
||||
border-left-color: var(--accent-strong);
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.55fr) minmax(280px, 0.8fr);
|
||||
gap: 24px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.canvas-stage {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 480px;
|
||||
border: 1px solid rgba(23, 32, 42, 0.24);
|
||||
border-radius: 8px;
|
||||
padding: 22px;
|
||||
background:
|
||||
radial-gradient(circle at 24% 18%, rgba(255, 176, 0, 0.34), transparent 28%),
|
||||
linear-gradient(135deg, #1a2633 0%, #0d141b 100%);
|
||||
box-shadow: 0 24px 70px rgba(23, 32, 42, 0.18);
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: min(638px, 100%);
|
||||
height: auto;
|
||||
aspect-ratio: 638 / 318;
|
||||
border: 2px solid rgba(255, 176, 0, 0.9);
|
||||
border-radius: 6px;
|
||||
background: var(--canvas);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.fallback-preview {
|
||||
display: none;
|
||||
position: absolute;
|
||||
inset: 42px;
|
||||
place-items: center;
|
||||
gap: 14px;
|
||||
border: 1px dashed rgba(255, 176, 0, 0.72);
|
||||
border-radius: 6px;
|
||||
padding: 22px;
|
||||
background: rgba(16, 24, 32, 0.9);
|
||||
}
|
||||
|
||||
body[data-api="fallback"] .fallback-preview {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
body[data-api="fallback"] canvas {
|
||||
opacity: 0.34;
|
||||
}
|
||||
|
||||
.fallback-label {
|
||||
color: #ffd27c;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
width: 578px;
|
||||
color: #101820;
|
||||
}
|
||||
|
||||
.control-panel fieldset {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
border: 2px solid #17202a;
|
||||
border-radius: 6px;
|
||||
padding: 18px;
|
||||
background: #fffaf0;
|
||||
}
|
||||
|
||||
.control-panel legend {
|
||||
padding: 0 8px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.field-row,
|
||||
.check-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.field-row input[type="text"] {
|
||||
width: min(310px, 58%);
|
||||
border: 1px solid #17202a;
|
||||
border-radius: 4px;
|
||||
padding: 7px 9px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.field-row input[type="range"] {
|
||||
width: min(300px, 58%);
|
||||
accent-color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.nested-fieldset {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.nested-fieldset legend {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.control-panel button {
|
||||
justify-self: end;
|
||||
border: 2px solid #17202a;
|
||||
border-radius: 4px;
|
||||
padding: 8px 18px;
|
||||
color: #17202a;
|
||||
background: var(--accent);
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
width: min(578px, 100%);
|
||||
}
|
||||
|
||||
.preview-panel input:disabled,
|
||||
.preview-panel button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.notes {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 14px;
|
||||
border-left: 4px solid var(--line);
|
||||
padding: 20px 0 20px 22px;
|
||||
color: var(--muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.notes h2 {
|
||||
color: var(--ink);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.notes ul {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding-left: 1.2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.page-shell {
|
||||
width: min(100vw - 22px, 680px);
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.1rem;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.canvas-stage {
|
||||
min-height: 360px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.fallback-preview {
|
||||
inset: 24px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.nested-fieldset {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.notes {
|
||||
border-left: 0;
|
||||
border-top: 4px solid var(--line);
|
||||
padding: 18px 0 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user