chore: import zh skill tweaks
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- Skill 名称:`tweaks`
|
||||
- 中文类目:已生成 HTML 的实时调参面板
|
||||
- 上游仓库:`nexu-io__open-design`
|
||||
- 上游路径:`design-templates/tweaks/SKILL.md`
|
||||
- 上游链接:https://github.com/nexu-io/open-design/blob/HEAD/design-templates/tweaks/SKILL.md
|
||||
- 本仓库为 WeHub 中文 Skill 汉化包,基于 skill 市场筛选 Top200 清单整理
|
||||
- 原作者、版权和许可证信息以上游仓库为准
|
||||
@@ -0,0 +1,228 @@
|
||||
---
|
||||
|
||||
name: tweaks
|
||||
description: |
|
||||
将任意 HTML 制品包裹在一个侧面板中,提供实时的、参数化的控件——强调色、字号比例、密度、动效、主题——这些控件会实时改写 CSS 自定义属性,并持久化到 localStorage。让用户可以在不重新提示 agent 的情况下探索设计的各种变体。当需求中提到「变体」「并排选项」「调一下」「让我调整」「实时 knob」「实时调参」时使用。
|
||||
triggers:
|
||||
- "tweaks"
|
||||
- "variants"
|
||||
- "tweak panel"
|
||||
- "live controls"
|
||||
- "adjust on the fly"
|
||||
- "实时调参"
|
||||
- "可调参数面板"
|
||||
- "side panel"
|
||||
- "knobs"
|
||||
od:
|
||||
mode: prototype
|
||||
platform: desktop
|
||||
scenario: design
|
||||
upstream: "https://github.com/alchaincyf/huashu-design"
|
||||
preview:
|
||||
type: html
|
||||
entry: index.html
|
||||
design_system:
|
||||
requires: false
|
||||
example_prompt: "Wrap this landing page with a tweak panel — accent color, type scale, density, light/dark — persist to localStorage so the user can refresh without losing their choice."
|
||||
|
||||
---
|
||||
|
||||
# Tweaks Skill · 参数化变体面板
|
||||
|
||||
将任意 HTML 制品包裹在一个侧面板中,提供实时控件来改写 CSS 自定义属性,并持久化到 `localStorage`。灵感来自 *huashu-design* 的调参模式。
|
||||
|
||||
## 产出内容
|
||||
|
||||
一个自包含的单一 HTML 文件,包含两层:
|
||||
|
||||
1. **舞台(Stage)**——原始制品(着陆页 / 幻灯片 / 仪表板),经过改写,使所有视觉决策都从 CSS 自定义属性中读取:`--accent`、`--scale`、`--density`、`--mode`、`--motion`。
|
||||
2. **面板(Panel)**——一个固定侧边栏(小视口下为抽屉式),包含绑定到这些自定义属性的表单控件,通过一个极简的 vanilla-JS 桥接层实现。每次更改都会按制品标识持久化到 `localStorage`。
|
||||
|
||||
用户可以:
|
||||
|
||||
- 打开制品,看到舞台以其保存的偏好(或合理的默认值)呈现。
|
||||
- 在面板中调整强调色 / 字号比例 / 密度 / 模式 / 动效,舞台立即更新——无需重渲染。
|
||||
- 按 <kbd>T</kbd> 隐藏/显示面板;按 <kbd>R</kbd> 重置为默认值。
|
||||
- 刷新页面——所有选择都会持久保留。
|
||||
|
||||
## 何时使用
|
||||
|
||||
- 用户生成的内容他们喜欢 80%,剩下的 20% 想自己微调。
|
||||
- 你在展示设计系统/品牌,希望观众实时感受变体效果(而不是让你重新运行 agent)。
|
||||
- 你在交付一个独立演示(例如作品集项目),希望观众可以交互体验。
|
||||
|
||||
## 何时*不要*使用
|
||||
|
||||
- 一次性制品,无需迭代(例如 runbook——参数无意义)。
|
||||
- 制品的价值在于固定比例(例如数据可视化信息图,经过精心平衡——加上 knob 反而会破坏它)。
|
||||
|
||||
## 5 个标准 knob
|
||||
|
||||
> 选择与制品匹配的子集。如果只有 2 个重要,不要全部 5 个都上——杂乱就是退步。
|
||||
|
||||
### 1. `--accent`——强调色
|
||||
|
||||
一个包含 5–8 种精选色板的下拉选择(不要提供一个自由颜色选择器——用户会选到糟糕的颜色然后怪你)。
|
||||
|
||||
```js
|
||||
const ACCENT_PRESETS = [
|
||||
{ id: 'rust', val: '#c96442', label: 'Rust' },
|
||||
{ id: 'cobalt', val: '#2c4d8e', label: 'Cobalt' },
|
||||
{ id: 'sage', val: '#4a7a3f', label: 'Sage' },
|
||||
{ id: 'plum', val: '#7a3f6a', label: 'Plum' },
|
||||
{ id: 'graphite',val: '#3a3a3a', label: 'Graphite' },
|
||||
];
|
||||
```
|
||||
|
||||
制品中所有原先硬编码强调色的地方都使用 `var(--accent)`。边框 / 链接 / 引用块分割线 / CTA 全部同步切换。
|
||||
|
||||
### 2. `--scale`——字号比例(0.85 / 1.0 / 1.15)
|
||||
|
||||
三种设置:*紧凑*(0.85)、*正常*(1.0)、*宽松*(1.15)。所有 `font-size` 声明通过 `calc(... * var(--scale))` 乘以 `var(--scale)`。
|
||||
|
||||
不要超过 ±15%——超出后布局会破坏(列流、断点、行数)。
|
||||
|
||||
### 3. `--density`——布局密度(紧凑 / 正常 / 宽敞)
|
||||
|
||||
三种设置,切换间距比例:*紧凑*(0.75)/ *正常*(1.0)/ *宽敞*(1.4)。所有 `padding` / `gap` / `margin` 声明乘以 `var(--density)`。
|
||||
|
||||
这是影响最大的 knob——也是最脆弱的,因此**每个布局关键容器必须在包裹之前将其基础间距声明为自定义属性**。
|
||||
|
||||
### 4. `--mode`——浅色 / 深色
|
||||
|
||||
一个二态切换。在 `<html>` 元素上设置 `data-mode="light"` 与 `"dark"`,制品的 `:root` 选择器以两组颜色集响应。
|
||||
|
||||
如果制品已经有基于媒体查询的深色模式,*替换*为 data-attr 版本——用户的选择应优先于其操作系统。
|
||||
|
||||
### 5. `--motion`——关闭 / 微妙 / 生动
|
||||
|
||||
三种设置。映射到一个 CSS 变量 `--motion-mult`,用于缩放所有 `transition-duration` / `animation-duration` 声明:
|
||||
|
||||
- *关闭*——`0s`(同时禁用 WebGL canvas / 装饰性动画)。
|
||||
- *微妙*——`1.0`(制品原生的时间设定)。
|
||||
- *生动*——`1.6`(较慢的过渡,更明显的动效)。
|
||||
|
||||
尊重 `prefers-reduced-motion`:如果用户设置了该偏好,默认使用*关闭*,无论存储的偏好是什么。
|
||||
|
||||
## 宿主集成协议(必读)
|
||||
|
||||
Open Design 查看器工具栏有一个 **Tweaks** 开关,用于从 iframe 外部驱动面板可见性。要让该开关绑定到你的面板,你的制品**必须**采用以下两种协议之一(选其一,不要混用)。工具栏一旦检测到任一信号就会启用自身。
|
||||
|
||||
### 协议 A——postMessage(推荐用于 agent 生成的制品)
|
||||
|
||||
当面板通过 JS(React、vanilla、任意动态方式)挂载时使用。
|
||||
|
||||
**制品 → 宿主:**
|
||||
- 挂载时,向 `window.parent` 发送 `{ type: '__edit_mode_available', visible?: boolean }`。告知工具栏存在面板;可选的 `visible` 报告面板的初始状态,使工具栏开关一开始就同步。省略 `visible` 适用于常见的"面板已在屏幕上"的情况(宿主将缺失字段视为 `true`,以保证旧版无参数消息继续工作)。传递 `visible: false` 以声明默认关闭的面板。
|
||||
- 当用户本地关闭面板(× 按钮、Esc 等)时,发送 `{ type: '__edit_mode_dismissed' }`。工具栏切换为"关闭"。
|
||||
|
||||
**宿主 → 制品:**
|
||||
- `{ type: '__activate_edit_mode' }`——打开面板(`setOpen(true)`)。
|
||||
- `{ type: '__deactivate_edit_mode' }`——关闭面板(`setOpen(false)`)。
|
||||
|
||||
最小监听器:
|
||||
|
||||
```js
|
||||
window.addEventListener('message', (e) => {
|
||||
const t = e?.data?.type;
|
||||
if (t === '__activate_edit_mode') setOpen(true);
|
||||
else if (t === '__deactivate_edit_mode') setOpen(false);
|
||||
});
|
||||
// 或者,对于默认关闭的面板:
|
||||
// window.parent.postMessage({ type: '__edit_mode_available', visible: open }, '*');
|
||||
window.parent.postMessage({ type: '__edit_mode_available' }, '*');
|
||||
// 在关闭处理程序中:
|
||||
const dismiss = () => {
|
||||
setOpen(false);
|
||||
window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*');
|
||||
};
|
||||
```
|
||||
|
||||
面板可以默认打开或关闭——宿主会将其开关同步到制品报告的状态。
|
||||
|
||||
### 协议 B——基于 class(由 `assets/wrap.html` 使用)
|
||||
|
||||
仅在直接包裹模板时使用。制品包含一个 `.tw-panel` 元素,并通过切换 `.tw-hidden` 类来控制可见性。查看器的 iframe 桥接层(位于 `apps/web/src/runtime/srcdoc.ts`)在初始渲染时隐藏面板,通过 `MutationObserver` 监听该类,并在两个方向中继状态。制品无需额外 JS,模板已包含所需内容。
|
||||
|
||||
选择器是固定的:`.tw-panel`(面板根元素)和 `.tw-hidden`(隐藏状态)。如果重命名其中任何一个,桥接层将无法找到它。
|
||||
|
||||
### 反模式
|
||||
|
||||
不要发明第三种协议,也不要重命名任何一组标识符。工具栏开关只绑定到 A 或 B。使用自定义 class 且没有 postMessage 的自定义面板将导致工具栏变灰不可用。
|
||||
|
||||
## 实现原语
|
||||
|
||||
阅读 `assets/wrap.html`——它将面板 + 桥接层作为惰性模板提供。你的工作是:
|
||||
|
||||
1. 获取用户现有的制品 HTML。
|
||||
2. 将其强调色 / 模式 / 间距 / 字号提升为自定义属性(搜索硬编码的 `#hex` / `Npx` / `Nrem` 并转换)。
|
||||
3. 将内容粘贴到 `wrap.html` 的标记区域。
|
||||
4. 编辑 `assets/wrap.html` 的 `KNOBS` 数组,只保留你确定与*这个*制品相关的 knob。如果只有 2 个重要,不要全部 5 个都上。
|
||||
5. 将 `STORAGE_KEY` 改为一个唯一的 slug(`tweaks-<artifact-slug>`)。
|
||||
|
||||
`wrap.html` 中的桥接层:
|
||||
- 首次渲染时加载 `localStorage[STORAGE_KEY]` JSON。
|
||||
- 将值作为 `document.documentElement.style.setProperty('--accent', ...)` 应用。
|
||||
- 监听每个表单控件的 `change` 事件并写回。
|
||||
- 暴露 <kbd>T</kbd>(切换面板)和 <kbd>R</kbd>(重置)。
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 步骤 1——获取制品
|
||||
|
||||
与 critique skill 相同的选项:
|
||||
|
||||
1. 项目文件(项目文件夹中的 `index.html`)。
|
||||
2. 粘贴到聊天中的 HTML。
|
||||
3. 由你在此轮对话中生成。
|
||||
|
||||
### 步骤 2——决定哪些 knob 适用
|
||||
|
||||
先阅读制品的 CSS。对每个 knob,决定*是 / 否*:
|
||||
|
||||
- `--accent`——如果制品有 1 个使用了 ≥ 3 次的强调色,则为是。
|
||||
- `--scale`——如果制品以文字驱动(文章、幻灯片、定价页),则为是。
|
||||
- `--density`——如果制品有一致的 gap / padding 节奏(幻灯片、仪表板、着陆页),则为是。runbook 不需要(已经很密集了)。
|
||||
- `--mode`——如果制品有原创的深色模式 token,或者你愿意为其推导,则为是。
|
||||
- `--motion`——如果制品有任何值得缩放的 transition / animation,则为是。静态报告 / critique 报告不需要。
|
||||
|
||||
默认:**3 个 knob 是最佳选择。** 五个太杂,一个不值得做一个面板。
|
||||
|
||||
### 步骤 3——将硬编码值提升为自定义属性
|
||||
|
||||
打开 `assets/wrap.html` 的 `<style>` 块——复制其自定义属性命名方案(`--accent`、`--scale` 等)。在用户的制品中,找到所有相关位置并改写:
|
||||
|
||||
- `color: #c96442` → `color: var(--accent)`
|
||||
- `font-size: 18px` → `font-size: calc(18px * var(--scale))`
|
||||
- `padding: 24px 32px` → `padding: calc(24px * var(--density)) calc(32px * var(--density))`
|
||||
- `transition: opacity 200ms` → `transition: opacity calc(200ms * var(--motion-mult))`
|
||||
|
||||
如果制品已经使用了 `clamp()` 或 `vw`,将*外层*值乘以自定义属性——不要拆解 `clamp(...)`。
|
||||
|
||||
### 步骤 4——粘贴到包裹模板中
|
||||
|
||||
将制品的 `<style>` 和 `<body>` 复制到 `wrap.html` 的标记区域。保持面板 + 桥接层完整。
|
||||
|
||||
### 步骤 5——测试循环
|
||||
|
||||
打开结果,点击每个 knob 至少一次,刷新页面,确认选择已持久化。如果某个 knob 破坏了布局——*移除它*,不要发布。
|
||||
|
||||
## 输出约定
|
||||
|
||||
```
|
||||
<artifact identifier="tweaks-<artifact-slug>" type="text/html" title="<Artifact Title> · Tweaks">
|
||||
<!doctype html>
|
||||
<html>...</html>
|
||||
</artifact>
|
||||
```
|
||||
|
||||
在 artifact 之前放一句话("已将 X 包裹上 3-knob 调参面板——强调色 / 字号比例 / 模式。")。在 `</artifact>` 之后结束。
|
||||
|
||||
## 硬性规则
|
||||
|
||||
- **不要提供自由颜色选择器**——只提供精选色板。用户有了自由度就会选糟糕的颜色;帮他们避免这一点就是全部意义所在。
|
||||
- **按制品标识持久化**——使用 `tweaks-<slug>`,而不是全局键。两个选项卡中打开的两个制品不得共享状态。
|
||||
- **尊重 `prefers-reduced-motion`**——如果用户设置了该偏好,动效默认使用*关闭*,仅在显式点击时覆盖。
|
||||
- **单文件**——除了制品已有的导入外,无外部 CSS / JS / 字体。将面板 + 桥接层内联。
|
||||
- **在小于 720px 的视口上,面板默认隐藏**——通过右上角的"T"按钮滑入抽屉。
|
||||
- **不要发布超过 5 个 knob。** 3 个是最佳选择。
|
||||
@@ -0,0 +1,437 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN" data-mode="light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>[ARTIFACT TITLE] · 调节面板</title>
|
||||
<style>
|
||||
/* =========================================================
|
||||
TWEAKS · WRAP TEMPLATE
|
||||
=========================================================
|
||||
This file is a skeleton. Workflow:
|
||||
1. Replace [ARTIFACT TITLE] in <title>.
|
||||
2. Update STORAGE_KEY in the script (tweaks-<artifact-slug>).
|
||||
3. Decide which knobs apply (1-5 from KNOBS_LIBRARY).
|
||||
4. Paste artifact CSS into the [ARTIFACT_STYLE] region.
|
||||
5. Paste artifact body into the [ARTIFACT_BODY] region.
|
||||
6. Lift hard-coded #hex / Npx / Nrem values to custom
|
||||
properties so the knobs actually move.
|
||||
========================================================= */
|
||||
|
||||
:root {
|
||||
/* ---------- Knob defaults (overridden by JS bridge) ---------- */
|
||||
--accent: #c96442;
|
||||
--scale: 1;
|
||||
--density: 1;
|
||||
--motion-mult: 1;
|
||||
|
||||
/* ---------- Light theme tokens ---------- */
|
||||
--bg: #f6f4ef;
|
||||
--paper: #ffffff;
|
||||
--ink: #1a1a1c;
|
||||
--muted: #6b6964;
|
||||
--rule: #e2dfd7;
|
||||
}
|
||||
[data-mode="dark"] {
|
||||
--bg: #0e0d0c;
|
||||
--paper: #181715;
|
||||
--ink: #f4f1ea;
|
||||
--muted: #8a857a;
|
||||
--rule: #2a2723;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root { --motion-mult: 0; }
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; min-height: 100%; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: 'Inter', -apple-system, system-ui, sans-serif;
|
||||
font-size: calc(16px * var(--scale));
|
||||
line-height: 1.55;
|
||||
transition: background calc(220ms * var(--motion-mult)) ease,
|
||||
color calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
[ARTIFACT_STYLE] — paste artifact-specific CSS here
|
||||
========================================================= */
|
||||
|
||||
/* =========================================================
|
||||
PANEL · fixed sidebar with knobs
|
||||
========================================================= */
|
||||
.tw-panel {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 100;
|
||||
width: 280px;
|
||||
max-width: calc(100vw - 32px);
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.08);
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
transition: transform calc(220ms * var(--motion-mult)) cubic-bezier(.2,.8,.2,1),
|
||||
opacity calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
[data-mode="dark"] .tw-panel { box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
|
||||
.tw-panel.tw-hidden {
|
||||
transform: translateX(calc(100% + 32px));
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tw-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.tw-head .ttl {
|
||||
font-family: 'IBM Plex Mono', ui-monospace, monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.24em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.tw-head .toggle {
|
||||
background: transparent;
|
||||
border: 1px solid var(--rule);
|
||||
color: var(--muted);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 11px;
|
||||
padding: 0;
|
||||
}
|
||||
.tw-head .toggle:hover { color: var(--ink); }
|
||||
|
||||
.tw-body { padding: 14px 18px 18px; }
|
||||
|
||||
.tw-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
|
||||
.tw-row:last-child { margin-bottom: 0; }
|
||||
.tw-row .lbl {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Segmented control */
|
||||
.tw-seg {
|
||||
display: flex;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
}
|
||||
.tw-seg button {
|
||||
flex: 1;
|
||||
padding: 7px 8px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-left: 1px solid var(--rule);
|
||||
cursor: pointer;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
transition: color calc(180ms * var(--motion-mult)) ease,
|
||||
background calc(180ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.tw-seg button:first-child { border-left: 0; }
|
||||
.tw-seg button:hover { color: var(--ink); }
|
||||
.tw-seg button[aria-pressed="true"] {
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
box-shadow: inset 0 -2px 0 var(--accent);
|
||||
}
|
||||
|
||||
/* Swatch grid */
|
||||
.tw-swatches { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
|
||||
.tw-swatch {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: transform calc(160ms * var(--motion-mult)) ease,
|
||||
border-color calc(160ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.tw-swatch:hover { transform: scale(1.06); }
|
||||
.tw-swatch[aria-pressed="true"] { border-color: var(--ink); }
|
||||
|
||||
/* Mini toolbar in panel footer */
|
||||
.tw-foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 18px;
|
||||
border-top: 1px solid var(--rule);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.tw-foot button {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
letter-spacing: inherit;
|
||||
text-transform: inherit;
|
||||
}
|
||||
.tw-foot button:hover { color: var(--ink); }
|
||||
kbd {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 9px;
|
||||
padding: 2px 5px;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 3px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* When panel is hidden, expose a "T" button at top-right */
|
||||
.tw-restore {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 100;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--rule);
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform calc(180ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.tw-restore:hover { transform: scale(1.06); }
|
||||
.tw-restore.tw-show { display: flex; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.tw-panel { left: 16px; right: 16px; width: auto; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- =========================================================
|
||||
[ARTIFACT_BODY] — paste artifact-specific markup here
|
||||
========================================================= -->
|
||||
|
||||
<!-- =========================================================
|
||||
PANEL — knob controls (drop the rows you don't ship)
|
||||
========================================================= -->
|
||||
<aside class="tw-panel" id="tw-panel" aria-label="调节面板">
|
||||
<header class="tw-head">
|
||||
<span class="ttl">调节面板</span>
|
||||
<button class="toggle" id="tw-close" aria-label="隐藏面板" title="隐藏 (T)">×</button>
|
||||
</header>
|
||||
<div class="tw-body">
|
||||
|
||||
<!-- Accent -->
|
||||
<div class="tw-row">
|
||||
<span class="lbl">强调色</span>
|
||||
<div class="tw-swatches" id="tw-accent" role="radiogroup" aria-label="强调色"></div>
|
||||
</div>
|
||||
|
||||
<!-- Mode -->
|
||||
<div class="tw-row">
|
||||
<span class="lbl">主题模式</span>
|
||||
<div class="tw-seg" id="tw-mode" role="radiogroup" aria-label="色彩模式">
|
||||
<button data-val="light" aria-pressed="true">浅色</button>
|
||||
<button data-val="dark" aria-pressed="false">深色</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scale -->
|
||||
<div class="tw-row">
|
||||
<span class="lbl">字号比例</span>
|
||||
<div class="tw-seg" id="tw-scale" role="radiogroup" aria-label="字号比例">
|
||||
<button data-val="0.85" aria-pressed="false">紧凑</button>
|
||||
<button data-val="1" aria-pressed="true">正常</button>
|
||||
<button data-val="1.15" aria-pressed="false">宽松</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Density -->
|
||||
<div class="tw-row">
|
||||
<span class="lbl">密度</span>
|
||||
<div class="tw-seg" id="tw-density" role="radiogroup" aria-label="密度">
|
||||
<button data-val="0.75" aria-pressed="false">紧密</button>
|
||||
<button data-val="1" aria-pressed="true">正常</button>
|
||||
<button data-val="1.4" aria-pressed="false">宽敞</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Motion -->
|
||||
<div class="tw-row">
|
||||
<span class="lbl">动效</span>
|
||||
<div class="tw-seg" id="tw-motion" role="radiogroup" aria-label="动效">
|
||||
<button data-val="0" aria-pressed="false">关闭</button>
|
||||
<button data-val="1" aria-pressed="true">柔和</button>
|
||||
<button data-val="1.6" aria-pressed="false">活泼</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="tw-foot">
|
||||
<span><kbd>T</kbd> 隐藏 · <kbd>R</kbd> 重置</span>
|
||||
<button id="tw-reset" type="button">重置</button>
|
||||
</footer>
|
||||
</aside>
|
||||
|
||||
<button class="tw-restore" id="tw-restore" aria-label="显示面板" title="显示面板 (T)">T</button>
|
||||
|
||||
<script>
|
||||
// =========================================================
|
||||
// BRIDGE — binds knobs ↔ CSS custom props ↔ localStorage
|
||||
// =========================================================
|
||||
// CHANGE THIS to a unique slug per artifact you wrap.
|
||||
const STORAGE_KEY = 'tweaks-default';
|
||||
|
||||
const ACCENT_PRESETS = [
|
||||
{ id: 'rust', val: '#c96442' },
|
||||
{ id: 'cobalt', val: '#2c4d8e' },
|
||||
{ id: 'sage', val: '#4a7a3f' },
|
||||
{ id: 'plum', val: '#7a3f6a' },
|
||||
{ id: 'graphite', val: '#3a3a3a' },
|
||||
];
|
||||
|
||||
const DEFAULTS = {
|
||||
accent: ACCENT_PRESETS[0].val,
|
||||
mode: 'light',
|
||||
scale: 1,
|
||||
density: 1,
|
||||
motion: matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 1,
|
||||
};
|
||||
|
||||
function load() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return { ...DEFAULTS };
|
||||
return { ...DEFAULTS, ...JSON.parse(raw) };
|
||||
} catch { return { ...DEFAULTS }; }
|
||||
}
|
||||
function save(state) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }
|
||||
catch {}
|
||||
}
|
||||
|
||||
function applyState(s) {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--accent', s.accent);
|
||||
root.style.setProperty('--scale', s.scale);
|
||||
root.style.setProperty('--density', s.density);
|
||||
root.style.setProperty('--motion-mult', s.motion);
|
||||
root.setAttribute('data-mode', s.mode);
|
||||
// Reflect to UI
|
||||
paintAccent(s.accent);
|
||||
paintSeg('tw-mode', s.mode);
|
||||
paintSeg('tw-scale', String(s.scale));
|
||||
paintSeg('tw-density', String(s.density));
|
||||
paintSeg('tw-motion', String(s.motion));
|
||||
}
|
||||
|
||||
function paintAccent(val) {
|
||||
const host = document.getElementById('tw-accent');
|
||||
if (!host) return;
|
||||
host.querySelectorAll('button').forEach((b) =>
|
||||
b.setAttribute('aria-pressed', b.dataset.val === val ? 'true' : 'false'),
|
||||
);
|
||||
}
|
||||
function paintSeg(id, val) {
|
||||
const host = document.getElementById(id);
|
||||
if (!host) return;
|
||||
host.querySelectorAll('button').forEach((b) =>
|
||||
b.setAttribute('aria-pressed', b.dataset.val === val ? 'true' : 'false'),
|
||||
);
|
||||
}
|
||||
|
||||
function buildAccent(state) {
|
||||
const host = document.getElementById('tw-accent');
|
||||
if (!host) return;
|
||||
host.innerHTML = '';
|
||||
for (const p of ACCENT_PRESETS) {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.className = 'tw-swatch';
|
||||
b.dataset.val = p.val;
|
||||
b.setAttribute('aria-label', p.id);
|
||||
b.style.background = p.val;
|
||||
b.addEventListener('click', () => {
|
||||
state.accent = p.val;
|
||||
save(state); applyState(state);
|
||||
});
|
||||
host.appendChild(b);
|
||||
}
|
||||
}
|
||||
|
||||
function bindSeg(id, key, parser) {
|
||||
const host = document.getElementById(id);
|
||||
if (!host) return;
|
||||
host.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-val]');
|
||||
if (!btn) return;
|
||||
state[key] = parser ? parser(btn.dataset.val) : btn.dataset.val;
|
||||
save(state); applyState(state);
|
||||
});
|
||||
}
|
||||
|
||||
const state = load();
|
||||
buildAccent(state);
|
||||
bindSeg('tw-mode', 'mode');
|
||||
bindSeg('tw-scale', 'scale', parseFloat);
|
||||
bindSeg('tw-density', 'density', parseFloat);
|
||||
bindSeg('tw-motion', 'motion', parseFloat);
|
||||
applyState(state);
|
||||
|
||||
// ---- Panel show/hide ----
|
||||
const panel = document.getElementById('tw-panel');
|
||||
const restore = document.getElementById('tw-restore');
|
||||
function setPanelVisible(v) {
|
||||
panel.classList.toggle('tw-hidden', !v);
|
||||
restore.classList.toggle('tw-show', !v);
|
||||
}
|
||||
document.getElementById('tw-close').addEventListener('click', () => setPanelVisible(false));
|
||||
restore.addEventListener('click', () => setPanelVisible(true));
|
||||
|
||||
// ---- Reset ----
|
||||
document.getElementById('tw-reset').addEventListener('click', () => {
|
||||
Object.assign(state, DEFAULTS);
|
||||
save(state); applyState(state);
|
||||
});
|
||||
|
||||
// ---- Keyboard shortcuts ----
|
||||
addEventListener('keydown', (e) => {
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
if (e.target.matches('input, textarea, select, [contenteditable]')) return;
|
||||
if (e.key === 't' || e.key === 'T') {
|
||||
e.preventDefault();
|
||||
setPanelVisible(panel.classList.contains('tw-hidden'));
|
||||
} else if (e.key === 'r' || e.key === 'R') {
|
||||
e.preventDefault();
|
||||
Object.assign(state, DEFAULTS);
|
||||
save(state); applyState(state);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+750
@@ -0,0 +1,750 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN" data-mode="light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Filebase · Tweaks 演示</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,400;8..60,600;8..60,700&family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--accent: #c96442;
|
||||
--scale: 1;
|
||||
--density: 1;
|
||||
--motion-mult: 1;
|
||||
|
||||
--bg: #f6f4ef;
|
||||
--paper: #ffffff;
|
||||
--ink: #1a1a1c;
|
||||
--muted: #6b6964;
|
||||
--rule: #e2dfd7;
|
||||
|
||||
--serif: 'Source Serif 4', Georgia, serif;
|
||||
--sans: 'Inter', -apple-system, system-ui, sans-serif;
|
||||
--mono: 'IBM Plex Mono', ui-monospace, monospace;
|
||||
}
|
||||
[data-mode="dark"] {
|
||||
--bg: #0e0d0c;
|
||||
--paper: #181715;
|
||||
--ink: #f4f1ea;
|
||||
--muted: #8a857a;
|
||||
--rule: #2a2723;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root { --motion-mult: 0; }
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; min-height: 100%; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: var(--sans);
|
||||
font-size: calc(16px * var(--scale));
|
||||
line-height: 1.55;
|
||||
transition: background calc(220ms * var(--motion-mult)) ease,
|
||||
color calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
|
||||
/* ============ 布局 ============ */
|
||||
.wrap {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: calc(28px * var(--density)) calc(40px * var(--density));
|
||||
}
|
||||
|
||||
/* ============ 头部 / 导航 ============ */
|
||||
.nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: calc(20px * var(--density)) 0;
|
||||
gap: calc(32px * var(--density));
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: var(--serif);
|
||||
font-weight: 700;
|
||||
font-size: calc(20px * var(--scale));
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.brand-mark {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 6px;
|
||||
background: var(--accent);
|
||||
transition: background calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: calc(28px * var(--density));
|
||||
font-size: calc(14px * var(--scale));
|
||||
color: var(--muted);
|
||||
}
|
||||
.nav-links a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: color calc(180ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.nav-links a:hover { color: var(--ink); }
|
||||
.cta {
|
||||
display: inline-block;
|
||||
padding: calc(10px * var(--density)) calc(18px * var(--density));
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
font-size: calc(13px * var(--scale));
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-decoration: none;
|
||||
transition: background calc(220ms * var(--motion-mult)) ease,
|
||||
transform calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.cta:hover { transform: translateY(-1px); }
|
||||
|
||||
/* ============ 英雄区 ============ */
|
||||
.hero {
|
||||
padding: calc(96px * var(--density)) 0 calc(80px * var(--density));
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr;
|
||||
gap: calc(64px * var(--density));
|
||||
align-items: center;
|
||||
}
|
||||
@media (max-width: 880px) {
|
||||
.hero { grid-template-columns: 1fr; }
|
||||
}
|
||||
.eyebrow {
|
||||
font-family: var(--mono);
|
||||
font-size: calc(11px * var(--scale));
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin-bottom: calc(22px * var(--density));
|
||||
transition: color calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.h1 {
|
||||
font-family: var(--serif);
|
||||
font-weight: 700;
|
||||
font-size: calc(58px * var(--scale));
|
||||
line-height: 1.04;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0 0 calc(22px * var(--density));
|
||||
}
|
||||
.h1 em {
|
||||
font-style: italic;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
transition: color calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.lede {
|
||||
font-size: calc(19px * var(--scale));
|
||||
color: var(--muted);
|
||||
max-width: 38ch;
|
||||
margin: 0 0 calc(36px * var(--density));
|
||||
line-height: 1.5;
|
||||
}
|
||||
.row { display: flex; gap: calc(14px * var(--density)); align-items: center; flex-wrap: wrap; }
|
||||
.secondary {
|
||||
font-size: calc(13px * var(--scale));
|
||||
color: var(--muted);
|
||||
font-family: var(--mono);
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* 英雄区卡片预览 */
|
||||
.hero-card {
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 10px;
|
||||
padding: calc(20px * var(--density));
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.06);
|
||||
font-family: var(--mono);
|
||||
font-size: calc(12px * var(--scale));
|
||||
transition: background calc(220ms * var(--motion-mult)) ease,
|
||||
border-color calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
[data-mode="dark"] .hero-card { box-shadow: 0 12px 40px rgba(0,0,0,0.4); }
|
||||
.hero-card .label {
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: calc(12px * var(--density));
|
||||
font-size: calc(10px * var(--scale));
|
||||
}
|
||||
.hero-card pre {
|
||||
margin: 0;
|
||||
padding: calc(14px * var(--density));
|
||||
background: var(--bg);
|
||||
border-radius: 6px;
|
||||
color: var(--ink);
|
||||
font-family: var(--mono);
|
||||
font-size: calc(12px * var(--scale));
|
||||
line-height: 1.55;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.hero-card .k { color: var(--accent); }
|
||||
.hero-card .c { color: var(--muted); }
|
||||
.hero-card .ok { color: #4a7a3f; }
|
||||
[data-mode="dark"] .hero-card .ok { color: #8db876; }
|
||||
|
||||
/* ============ 统计数据条 ============ */
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: calc(32px * var(--density));
|
||||
padding: calc(48px * var(--density)) 0;
|
||||
border-top: 1px solid var(--rule);
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.stats { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
.stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(6px * var(--density));
|
||||
}
|
||||
.stat .num {
|
||||
font-family: var(--serif);
|
||||
font-weight: 700;
|
||||
font-size: calc(40px * var(--scale));
|
||||
line-height: 1;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--accent);
|
||||
transition: color calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.stat .lbl {
|
||||
font-size: calc(13px * var(--scale));
|
||||
color: var(--muted);
|
||||
max-width: 22ch;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* ============ 功能网格 ============ */
|
||||
.features {
|
||||
padding: calc(80px * var(--density)) 0 calc(40px * var(--density));
|
||||
}
|
||||
.section-title {
|
||||
font-family: var(--serif);
|
||||
font-weight: 600;
|
||||
font-size: calc(34px * var(--scale));
|
||||
letter-spacing: -0.015em;
|
||||
margin: 0 0 calc(48px * var(--density));
|
||||
max-width: 22ch;
|
||||
}
|
||||
.feat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: calc(28px * var(--density));
|
||||
}
|
||||
@media (max-width: 880px) {
|
||||
.feat-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
.feat {
|
||||
padding: calc(28px * var(--density));
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 10px;
|
||||
transition: background calc(220ms * var(--motion-mult)) ease,
|
||||
border-color calc(220ms * var(--motion-mult)) ease,
|
||||
transform calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.feat:hover { transform: translateY(-2px); }
|
||||
.feat .ico {
|
||||
width: 36px; height: 36px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in oklch, var(--accent) 18%, transparent);
|
||||
color: var(--accent);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-family: var(--serif);
|
||||
font-weight: 700;
|
||||
font-size: calc(15px * var(--scale));
|
||||
margin-bottom: calc(20px * var(--density));
|
||||
}
|
||||
.feat h3 {
|
||||
font-family: var(--serif);
|
||||
font-weight: 600;
|
||||
font-size: calc(20px * var(--scale));
|
||||
margin: 0 0 calc(8px * var(--density));
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.feat p {
|
||||
color: var(--muted);
|
||||
font-size: calc(15px * var(--scale));
|
||||
line-height: 1.55;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ============ CTA 横幅 ============ */
|
||||
.banner {
|
||||
margin: calc(60px * var(--density)) 0 calc(40px * var(--density));
|
||||
padding: calc(56px * var(--density)) calc(48px * var(--density));
|
||||
background: var(--ink);
|
||||
color: var(--bg);
|
||||
border-radius: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: calc(40px * var(--density));
|
||||
align-items: center;
|
||||
transition: background calc(220ms * var(--motion-mult)) ease,
|
||||
color calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.banner { grid-template-columns: 1fr; }
|
||||
}
|
||||
.banner h2 {
|
||||
font-family: var(--serif);
|
||||
font-weight: 600;
|
||||
font-size: calc(32px * var(--scale));
|
||||
letter-spacing: -0.015em;
|
||||
line-height: 1.15;
|
||||
margin: 0 0 calc(8px * var(--density));
|
||||
max-width: 22ch;
|
||||
}
|
||||
.banner p {
|
||||
color: rgba(244,241,234,0.68);
|
||||
font-size: calc(15px * var(--scale));
|
||||
margin: 0;
|
||||
}
|
||||
[data-mode="dark"] .banner p { color: rgba(26,26,28,0.68); }
|
||||
|
||||
.ft {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: calc(28px * var(--density)) 0;
|
||||
border-top: 1px solid var(--rule);
|
||||
font-family: var(--mono);
|
||||
font-size: calc(11px * var(--scale));
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
面板 · 与 assets/wrap.html 使用相同的基本元素
|
||||
========================================================= */
|
||||
.tw-panel {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 100;
|
||||
width: 280px;
|
||||
max-width: calc(100vw - 32px);
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.08);
|
||||
font-family: var(--sans);
|
||||
transition: transform calc(220ms * var(--motion-mult)) cubic-bezier(.2,.8,.2,1),
|
||||
opacity calc(220ms * var(--motion-mult)) ease,
|
||||
background calc(220ms * var(--motion-mult)) ease;
|
||||
}
|
||||
[data-mode="dark"] .tw-panel { box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
|
||||
.tw-panel.tw-hidden {
|
||||
transform: translateX(calc(100% + 32px));
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tw-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.tw-head .ttl {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.24em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.tw-head .toggle {
|
||||
background: transparent;
|
||||
border: 1px solid var(--rule);
|
||||
color: var(--muted);
|
||||
width: 24px; height: 24px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
padding: 0;
|
||||
}
|
||||
.tw-head .toggle:hover { color: var(--ink); }
|
||||
.tw-body { padding: 14px 18px 18px; }
|
||||
.tw-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
|
||||
.tw-row:last-child { margin-bottom: 0; }
|
||||
.tw-row .lbl {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.tw-seg {
|
||||
display: flex;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
}
|
||||
.tw-seg button {
|
||||
flex: 1;
|
||||
padding: 7px 8px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-left: 1px solid var(--rule);
|
||||
cursor: pointer;
|
||||
font-family: var(--sans);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
transition: color calc(180ms * var(--motion-mult)) ease,
|
||||
background calc(180ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.tw-seg button:first-child { border-left: 0; }
|
||||
.tw-seg button:hover { color: var(--ink); }
|
||||
.tw-seg button[aria-pressed='true'] {
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
box-shadow: inset 0 -2px 0 var(--accent);
|
||||
}
|
||||
.tw-swatches { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
|
||||
.tw-swatch {
|
||||
width: 100%; aspect-ratio: 1;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: transform calc(160ms * var(--motion-mult)) ease,
|
||||
border-color calc(160ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.tw-swatch:hover { transform: scale(1.06); }
|
||||
.tw-swatch[aria-pressed='true'] { border-color: var(--ink); }
|
||||
.tw-foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 18px;
|
||||
border-top: 1px solid var(--rule);
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.tw-foot button {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
letter-spacing: inherit;
|
||||
text-transform: inherit;
|
||||
}
|
||||
.tw-foot button:hover { color: var(--ink); }
|
||||
kbd {
|
||||
font-family: var(--mono);
|
||||
font-size: 9px;
|
||||
padding: 2px 5px;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 3px;
|
||||
color: var(--ink);
|
||||
}
|
||||
.tw-restore {
|
||||
position: fixed;
|
||||
top: 16px; right: 16px;
|
||||
z-index: 100;
|
||||
width: 36px; height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--rule);
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform calc(180ms * var(--motion-mult)) ease;
|
||||
}
|
||||
.tw-restore:hover { transform: scale(1.06); }
|
||||
.tw-restore.tw-show { display: flex; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.tw-panel { left: 16px; right: 16px; width: auto; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<!-- 导航 -->
|
||||
<nav class="nav">
|
||||
<div class="brand">
|
||||
<span class="brand-mark" aria-hidden="true"></span>
|
||||
<span>Filebase</span>
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a href="#">产品</a>
|
||||
<a href="#">定价</a>
|
||||
<a href="#">文档</a>
|
||||
<a href="#">更新日志</a>
|
||||
<a href="#">客户案例</a>
|
||||
</div>
|
||||
<a class="cta" href="#">免费开始</a>
|
||||
</nav>
|
||||
|
||||
<!-- 英雄区 -->
|
||||
<header class="hero">
|
||||
<div>
|
||||
<div class="eyebrow">B 轮 · 2026</div>
|
||||
<h1 class="h1">带宽账单才是真正的 <em>漏洞</em>。</h1>
|
||||
<p class="lede">一款只传输变更内容的同步引擎。在实际客户负载下,网络传输数据量减少 38 倍。</p>
|
||||
<div class="row">
|
||||
<a class="cta" href="#">免费开始</a>
|
||||
<span class="secondary">无需信用卡</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-card">
|
||||
<div class="label">Filebase 同步 — 典型运行</div>
|
||||
<pre><span class="c">// 每小时定时任务 · feature/render-pass 分支</span>
|
||||
$ filebase sync --watch
|
||||
<span class="k">→</span> 差异: <span class="ok">12.4 MB</span> <span class="c">(共 4.7 GB)</span>
|
||||
<span class="k">→</span> 上传: <span class="ok">8.2 MB</span> <span class="c">(去重后)</span>
|
||||
<span class="k">→</span> 延迟: <span class="ok">340 ms</span> <span class="c">p99</span>
|
||||
<span class="ok">✓ 耗时 2.1 秒完成</span></pre>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 统计数据 -->
|
||||
<section class="stats">
|
||||
<div class="stat">
|
||||
<div class="num">38×</div>
|
||||
<div class="lbl">相比普通同步,网络传输数据量减少</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="num">3,184</div>
|
||||
<div class="lbl">来自后期制作、设计、机器学习领域的付费团队</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="num">99.99%</div>
|
||||
<div class="lbl">过去 12 个月滚动运行时长可用率</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="num">$2.1M</div>
|
||||
<div class="lbl">Q1 用户群体报告的累计出站流量节省</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 功能 -->
|
||||
<section class="features">
|
||||
<h2 class="section-title">团队在第一个月就选择切换的三个理由。</h2>
|
||||
<div class="feat-grid">
|
||||
<article class="feat">
|
||||
<div class="ico" aria-hidden="true">∂</div>
|
||||
<h3>块级差异</h3>
|
||||
<p>编辑一个 4 GB Final Cut 项目中的一帧,只需同步 12 MB。差异比较不在乎文件有多大——它只关心什么发生了变化。</p>
|
||||
</article>
|
||||
<article class="feat">
|
||||
<div class="ico" aria-hidden="true">≈</div>
|
||||
<h3>跨区域去重</h3>
|
||||
<p>柏林的团队上传了一个东京团队已经推送过的检查点。我们能识别出来。不会有任何数据传输。</p>
|
||||
</article>
|
||||
<article class="feat">
|
||||
<div class="ico" aria-hidden="true">∇</div>
|
||||
<h3>即插即用,兼容 S3 / GCS</h3>
|
||||
<p>配置一个环境变量,零应用代码改动。使用现有存储桶、现有 IAM,收获全新的带宽账单。</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA 横幅 -->
|
||||
<section class="banner">
|
||||
<div>
|
||||
<h2>为存储付费。不再为传输买单。</h2>
|
||||
<p>免费开始,无需信用卡。生产团队最快 14 天内完成部署。</p>
|
||||
</div>
|
||||
<a class="cta" href="#" style="background: var(--accent); color: #fff;">预约演示</a>
|
||||
</section>
|
||||
|
||||
<footer class="ft">
|
||||
<span>© 2026 Filebase, Inc.</span>
|
||||
<span>隐私 · 服务条款 · 状态</span>
|
||||
<span>使用 OD tweaks 技能构建</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- ============ 调整面板 ============ -->
|
||||
<aside class="tw-panel" id="tw-panel" aria-label="调整面板">
|
||||
<header class="tw-head">
|
||||
<span class="ttl">调整 · Filebase</span>
|
||||
<button class="toggle" id="tw-close" aria-label="隐藏面板" title="隐藏 (T)">×</button>
|
||||
</header>
|
||||
<div class="tw-body">
|
||||
<div class="tw-row">
|
||||
<span class="lbl">强调色</span>
|
||||
<div class="tw-swatches" id="tw-accent" role="radiogroup" aria-label="强调色"></div>
|
||||
</div>
|
||||
<div class="tw-row">
|
||||
<span class="lbl">模式</span>
|
||||
<div class="tw-seg" id="tw-mode" role="radiogroup" aria-label="颜色模式">
|
||||
<button data-val="light" aria-pressed="true">浅色</button>
|
||||
<button data-val="dark" aria-pressed="false">深色</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-row">
|
||||
<span class="lbl">字号缩放</span>
|
||||
<div class="tw-seg" id="tw-scale" role="radiogroup" aria-label="字号缩放">
|
||||
<button data-val="0.85" aria-pressed="false">紧凑</button>
|
||||
<button data-val="1" aria-pressed="true">标准</button>
|
||||
<button data-val="1.15" aria-pressed="false">宽松</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-row">
|
||||
<span class="lbl">密度</span>
|
||||
<div class="tw-seg" id="tw-density" role="radiogroup" aria-label="密度">
|
||||
<button data-val="0.75" aria-pressed="false">紧凑</button>
|
||||
<button data-val="1" aria-pressed="true">标准</button>
|
||||
<button data-val="1.4" aria-pressed="false">宽敞</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-row">
|
||||
<span class="lbl">动效</span>
|
||||
<div class="tw-seg" id="tw-motion" role="radiogroup" aria-label="动效">
|
||||
<button data-val="0" aria-pressed="false">关闭</button>
|
||||
<button data-val="1" aria-pressed="true">柔和</button>
|
||||
<button data-val="1.6" aria-pressed="false">活泼</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="tw-foot">
|
||||
<span><kbd>T</kbd> 隐藏 · <kbd>R</kbd> 重置</span>
|
||||
<button id="tw-reset" type="button">重置</button>
|
||||
</footer>
|
||||
</aside>
|
||||
|
||||
<button class="tw-restore" id="tw-restore" aria-label="显示面板" title="显示面板 (T)">T</button>
|
||||
|
||||
<script>
|
||||
const STORAGE_KEY = 'tweaks-filebase-example';
|
||||
const ACCENT_PRESETS = [
|
||||
{ id: 'rust', val: '#c96442' },
|
||||
{ id: 'cobalt', val: '#2c4d8e' },
|
||||
{ id: 'sage', val: '#4a7a3f' },
|
||||
{ id: 'plum', val: '#7a3f6a' },
|
||||
{ id: 'graphite', val: '#3a3a3a' },
|
||||
];
|
||||
const DEFAULTS = {
|
||||
accent: ACCENT_PRESETS[0].val,
|
||||
mode: 'light',
|
||||
scale: 1,
|
||||
density: 1,
|
||||
motion: matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 1,
|
||||
};
|
||||
|
||||
function load() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return { ...DEFAULTS };
|
||||
return { ...DEFAULTS, ...JSON.parse(raw) };
|
||||
} catch { return { ...DEFAULTS }; }
|
||||
}
|
||||
function save(s) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); } catch {}
|
||||
}
|
||||
|
||||
function applyState(s) {
|
||||
const r = document.documentElement;
|
||||
r.style.setProperty('--accent', s.accent);
|
||||
r.style.setProperty('--scale', s.scale);
|
||||
r.style.setProperty('--density', s.density);
|
||||
r.style.setProperty('--motion-mult', s.motion);
|
||||
r.setAttribute('data-mode', s.mode);
|
||||
paintAccent(s.accent);
|
||||
paintSeg('tw-mode', s.mode);
|
||||
paintSeg('tw-scale', String(s.scale));
|
||||
paintSeg('tw-density', String(s.density));
|
||||
paintSeg('tw-motion', String(s.motion));
|
||||
}
|
||||
function paintAccent(val) {
|
||||
document.querySelectorAll('#tw-accent button').forEach((b) =>
|
||||
b.setAttribute('aria-pressed', b.dataset.val === val ? 'true' : 'false'),
|
||||
);
|
||||
}
|
||||
function paintSeg(id, val) {
|
||||
document.querySelectorAll('#' + id + ' button').forEach((b) =>
|
||||
b.setAttribute('aria-pressed', b.dataset.val === val ? 'true' : 'false'),
|
||||
);
|
||||
}
|
||||
|
||||
function buildAccent(state) {
|
||||
const host = document.getElementById('tw-accent');
|
||||
host.innerHTML = '';
|
||||
for (const p of ACCENT_PRESETS) {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.className = 'tw-swatch';
|
||||
b.dataset.val = p.val;
|
||||
b.setAttribute('aria-label', p.id);
|
||||
b.style.background = p.val;
|
||||
b.addEventListener('click', () => {
|
||||
state.accent = p.val;
|
||||
save(state); applyState(state);
|
||||
});
|
||||
host.appendChild(b);
|
||||
}
|
||||
}
|
||||
function bindSeg(id, key, parser) {
|
||||
document.getElementById(id).addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-val]');
|
||||
if (!btn) return;
|
||||
state[key] = parser ? parser(btn.dataset.val) : btn.dataset.val;
|
||||
save(state); applyState(state);
|
||||
});
|
||||
}
|
||||
|
||||
const state = load();
|
||||
buildAccent(state);
|
||||
bindSeg('tw-mode', 'mode');
|
||||
bindSeg('tw-scale', 'scale', parseFloat);
|
||||
bindSeg('tw-density', 'density', parseFloat);
|
||||
bindSeg('tw-motion', 'motion', parseFloat);
|
||||
applyState(state);
|
||||
|
||||
const panel = document.getElementById('tw-panel');
|
||||
const restore = document.getElementById('tw-restore');
|
||||
function setPanelVisible(v) {
|
||||
panel.classList.toggle('tw-hidden', !v);
|
||||
restore.classList.toggle('tw-show', !v);
|
||||
}
|
||||
document.getElementById('tw-close').addEventListener('click', () => setPanelVisible(false));
|
||||
restore.addEventListener('click', () => setPanelVisible(true));
|
||||
document.getElementById('tw-reset').addEventListener('click', () => {
|
||||
Object.assign(state, DEFAULTS);
|
||||
save(state); applyState(state);
|
||||
});
|
||||
|
||||
addEventListener('keydown', (e) => {
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
if (e.target.matches('input, textarea, select, [contenteditable]')) return;
|
||||
if (e.key === 't' || e.key === 'T') {
|
||||
e.preventDefault();
|
||||
setPanelVisible(panel.classList.contains('tw-hidden'));
|
||||
} else if (e.key === 'r' || e.key === 'R') {
|
||||
e.preventDefault();
|
||||
Object.assign(state, DEFAULTS);
|
||||
save(state); applyState(state);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user