oil-ui-pro 0.15.4 → 0.22.2(上游 ui.oiloil.org 正式包)+ 记 P0-98 跨盘 rename 坑
- oil-ui-pro 升到 0.22.2:references 13→28 份(新增 16、删 imagery.md)、assets 增 cards/(22 组)与 fonts/、SKILL.md 顶层五步结构未改名。 - 升级路径:官方 `npx github:oil-oil/oil-cli update` 稳定失败(error=replace、报「检查目录权限」)。 真身为 EXDEV —— 解压在 os.tmpdir()(C 盘)、技能目录在 E 盘,Windows 上 rename 不能跨盘; CLI 的 catch 把非 CliError 原生异常一律兜底成 replaceFailed,真错误一个字不吐。 做法=借其 prepare() 只做下载+校验,再 cp 到同盘 stage、rename 交换;官方命令复验 up_to_date。 - 回归:product-planning 按名引用的 visual-review.md/tools.md/design-direction.md/style-explorer.md 全部健在;命名闸门 2061|0|27。 - session-mechanism/references/pitfalls.md 新增 P0-98(含通则:更新器把原生错误兜底成指错方向的人话)。
This commit is contained in:
1 parent
053a6a6723
commit
d9445b5888
126 files changed
+10345
-1401
No files matched your search
@@ -0,0 +1,200 @@
|
||||
# oil cards · 组件探索
|
||||
|
||||
`gallery.html` 展示 19 种模板、25 个实例。页首可以按视觉意象、产品场景、数据关系、行动引导筛选,切换「创作团队 / 开发工具」两套示例内容,以及四种外层效果。材质可选原始 / 金属 / 玻璃 / 实体卡,各自带入配套阴影;阴影也可单独覆盖为无 / 轻贴合 / 微悬浮。选择会保留。配色可选单色 / 双色 / 三色,并用取色器或 HEX 输入;有效选择会在当前浏览器保留。默认关闭外层倾斜和流光;组件自身的曲面、数字、数据流等动画仍由动效模式控制。
|
||||
|
||||
这是框架无关的 HTML / CSS / ES module 探索库。内容接口已从绘制逻辑分离;图形构图、裁切、线密度、光场、阻尼保留在组件内部。这里的参数说明只描述当前实现,不是 Skill 规范。
|
||||
|
||||
## 使用
|
||||
|
||||
把对应 `card.html` 片段放进页面,依次加载 `shared/shell.css`、该模板的 `card.css` 和 `shared/finish.css`,通过本地服务器或项目开发服务器加载模块:
|
||||
|
||||
```js
|
||||
import {mount} from './assets/cards/file-mosaic/card.js';
|
||||
|
||||
const instance = mount(document.querySelector('.file-mosaic'), {
|
||||
shadow: 'auto', // 'auto'(默认,跟随材质)| 'none' | 'contact' | 'lifted'
|
||||
material: 'metal', // 'plain'(默认)| 'metal' | 'glass' | 'solid'
|
||||
effects: 'none', // 'none'(默认)| 'tilt' | 'glow' | 'tilt glow'
|
||||
theme: 'dark', // 'light' | 'dark',省略时继承宿主
|
||||
motion: 'auto', // 'auto' | 'hover' | 'off'
|
||||
variant: 'default', // 对应模板的形态,通证为 'token' | 'card'
|
||||
content: {
|
||||
title: '项目里的文件,\n在同一处管理',
|
||||
collection: '项目资源',
|
||||
status: '所有文件已同步',
|
||||
files: [
|
||||
{type: 'code', name: 'client.ts'},
|
||||
{type: 'doc', name: 'README.md'},
|
||||
{type: 'image', name: 'cover.webp'}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// 移除或重新挂载前
|
||||
instance.destroy();
|
||||
```
|
||||
|
||||
内容在 `mount` 时写入。更换内容时先销毁实例、恢复原始 HTML 片段,再重新挂载;不在已挂载的画布上重复 mount。画廊的场景切换示范了这条生命周期。`destroy()` 清理监听器、观察器、帧循环、GPU 资源及组件生成的图形,不承担恢复全部原始内容的责任。
|
||||
|
||||
## 三类公共配置
|
||||
|
||||
| 类别 | 接口 | 行为 |
|
||||
| --- | --- | --- |
|
||||
| 内容 | `options.content` | 所有字段可省略,省略时保留模板内容;字符串作为纯文本写入,`\n` 保留换行,空字符串清空文本。文本字段的 `null` 同省略 |
|
||||
| 外观 | `--card-accent` | 宿主提供一个主色;可选 `--card-accent-2 / -3`,否则从主色推导 |
|
||||
| 外观 | 字体、`--card-radius` | 字体继承宿主,圆角默认 8px;内部字号、间距与比例由模板管理 |
|
||||
| 外观 | `options.theme` 或 `data-theme="light / dark"` | 可设在卡片或祖先容器;options 优先,局部 light 也可覆盖深色宿主 |
|
||||
| 形态 | `options.variant` 或卡片自身 `data-variant` | 通证支持 `token / card`;其余模板传递为组件级样式钩子,保持原有构图 |
|
||||
| 外层阴影 | `options.shadow` 或卡片自身 `data-shadow` | `auto / none / contact / lifted`;默认跟随材质,显式选择可覆盖;只控制外层投影,内部产品窗口的阴影不变 |
|
||||
| 表面材质 | `options.material` 或卡片自身 `data-material` | `plain / metal / glass / solid`;金属细腻漫反射、玻璃透光与背景模糊、实体卡轻薄边缘;每种带入配套投影 |
|
||||
| 外层交互 | `options.effects` 或卡片自身 `data-effects` | `none / tilt / glow / tilt glow`,options 优先;挂载后可以直接修改属性。不从祖先继承 |
|
||||
| 内部动画 | `options.motion` 或 `data-motion="auto / hover / off"` | 卡片或祖先容器设置。auto 在可见时播放;hover 离开后,持续动画减速、指针跟随回正后停止,已开始的短演示播放至完成;off 展示完成状态或静帧;外层交互也服从 off |
|
||||
| 内部动画 | `options.speed` | 默认 1,非负有限数;0 静态。有限演示播放一次,支持 `replay()` 的模板可手动重播 |
|
||||
| 排版 | `data-title="top / bottom"` | 可选;省略使用自然位置。推广卡固定左侧文案,不应用这个切换 |
|
||||
|
||||
`theme / motion / variant` 等 options 在测量与首次绘制前应用,destroy 会恢复挂载前被 options 覆盖的属性。动效模式使用最近的 `data-motion`。若要继承宿主模式,移除片段上自带的 `data-motion="auto"`;卡片自身的属性会覆盖祖先。
|
||||
|
||||
外层倾斜固定在小角度范围内,流光使用局部柔和亮边。离开后回正、淡出;触屏、减少动态偏好、关闭动效、后台或屏外时停止。showcase 案例预览期间停用外层交互。组件不暴露透视距离、倾斜角度、跟随阻尼、光场半径等微调参数。
|
||||
|
||||
标题与舞台使用真实布局行,标题增长会推开下方主体;文字过长时卡片允许增高,不依赖脚本测量后再避让。上方 / 下方标题共用这条规则。文件集合栏、加载状态栏按内容高度排布,后面的预览跟随其位置;指标卡仍按实测文案高度分配数字和图表位置。画廊按容器宽度减少列数,避免把标题挤进狭窄栏位。
|
||||
|
||||
为保持艺术构图,建议纵向卡片 1–3 行、工具卡 1–2 行标题,说明 1–2 行。英文长单词可换行;文件名超出格子宽度显示省略号。内部放大的产品窗口仍按设计保留局部裁切。
|
||||
|
||||
## 内容字段
|
||||
|
||||
大多数卡片都有 `title`,带说明的卡片有 `description`。所有右上角展开按钮已移除。结构化字段如下;数组下标对应模板中固定的展示位置,不代表完整业务列表。
|
||||
|
||||
| 模板 | 额外字段 / 数据 |
|
||||
| --- | --- |
|
||||
| particle-ring | `prompt`, `reply`, `action`, `items[0..1].name/detail/value`;两件服装的 SVG 仍是模板插图,要换品类需要替换插图 |
|
||||
| flow-object | `brand`, `identifier`, `edition`, `status`, `hashLabel`, `hash`, `specs[0..2].label/value`;默认实体铭牌,另有无芯片流光卡片 |
|
||||
| dot-globe | `label`;mesh 另支持 `nodes: Array<{id, label?, position: [x,y,z]}>`、`links: Array<{from,to}>`;地球标签文案不改变地图上的连线坐标 |
|
||||
| line-ribbons | `description`;同一曲面可切换线束、薄绸、点阵、网格 |
|
||||
| ui-slice | `board`, `columns[0..1]`, `backlog[0..3]`, `listTitle`, `tasks[0..2].title/owner`, `progressLabel`;固定三项任务演示 |
|
||||
| terminal | `path`, `shell`, `lines: Array<string \| {text, detail?, tone?}>`;tone 为 `highlight / result / address`,默认普通行;空数组显示 `emptyText`(默认「等待运行」) |
|
||||
| metric | `value: number \| string`, `delta`, `latest`, `series: number[]`;数值自动加英文千位分隔,最多两位小数;字符串可以自带单位或其他格式 |
|
||||
| beam-graph | `nodes[0..4]`, `hub`;节点数与路由结构固定 |
|
||||
| keycaps | `modifier`, `modifierName`, `key`, `keyName`, `placeholder` |
|
||||
| promo-band | `title`, `description`, `action`, `href`;整个卡片是链接 |
|
||||
| vector-space | `query`, `result`, `mode`;四个相关点是视觉示意,结果文案不触发真实检索 |
|
||||
| file-mosaic | `collection`, `status`, `files: Array<{name, type}>`, `emptyText`;type 为 `doc / image / video / code / color / audio`,其他值用文档图标。空数组显示空态 |
|
||||
| stream-window | `path`, `eyebrow`, `heading`, `summary`, `visualLabel`, `listTitle`, `items[0..2]`, `pendingStatus`, `completeStatus`;固定三个内容块的就绪演示 |
|
||||
| portrait-story | `brand`, `quote`, `author`, `role`, `image: string \| {src, alt}`;影像引言独立排版,长文案会撑高卡片 |
|
||||
| capture-notes | `collection`, `tab`, `scope`, `listLabel`, `tagLabel`, `records: Array<{title, team?, tone?, tags?, detail?, waveform?}>`, `emptyText`;点选记录更新前景条,空数组禁用波形播放 |
|
||||
| answer-lens | `eyebrow`, `dashboard`, `status`, `question`, `sources: Array<{label, answer, citation?, chart?}>`, `emptyText`;最多三个来源,选中后对应答案与图表前移;保留微倾角和悬浮徽章;可分别注入环形数值、柱状序列与折线序列 |
|
||||
| responsive-preview | `description`, `labels[0..2]`;三个窗口共享原创 SVG 插图,通过按钮展开 / 收起不同尺寸,支持 `replay()` |
|
||||
| deploy-preview | `project`, `versions[0..1].title/branch/url`, `statuses[0..2]`, `steps[0..2]`, `versionLabels[0..1]`, `resultLabel`, `resultNote`;两个版本的构建预览演示,支持 `replay()` |
|
||||
| showcase | `brand`, `description`, `action`, `href`, `image: string \| {src, alt}`;有图片槽的四张 editorial 卡片可以替换封面,另外两张为程序图形 |
|
||||
|
||||
`metric.series` 接受有限的非负数,过滤无效项后按最大值归一化柱高;推荐 12–32 项,空数组不显示柱条。`delta` 与 `latest` 是独立展示文案,不自动推算,也不代表已接入真实业务数据。
|
||||
|
||||
链接支持相对地址、站内锚点、HTTP(S)、mailto、tel 和本地文件;拒绝脚本协议。图片支持相对地址、HTTP(S)、本地文件、blob 和图片 data URL。缺省图片保留模板。showcase 不提供 `href` 时,默认 CTA 展开视觉预览;提供 `href` 后作为普通导航链接使用。展示图片的 `alt` 由内容提供,背景装饰可为空。
|
||||
|
||||
原始片段中的 `data-text="items.0.name"` 等标记定义文本槽,普通 DOM 内容继续可直接编辑。`shared/content.js` 只填入文本、链接和图片,不生成任意 HTML。
|
||||
|
||||
## 讨论、引文与拓扑的数据槽
|
||||
|
||||
讨论数据沿用同一记录结构,医疗、审核、法律或工程内容不需要更改组件:
|
||||
|
||||
```js
|
||||
mount(element, {
|
||||
theme: 'light', motion: 'auto',
|
||||
content: {
|
||||
title: '让每一次评审,\n都可以回溯。',
|
||||
collection: '工程评审', listLabel: '讨论主题', tagLabel: '领域',
|
||||
records: [{
|
||||
title: '接口边界与验收范围', detail: '评审记录 · 第二轮',
|
||||
tags: [{label: '工程', tone: 'teal'}, {label: '待核对', tone: 'amber'}],
|
||||
waveform: [.2, .7, .4, .9, .3] // 可选 0–1 幅度;省略时使用原有合成声波
|
||||
}]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`team / tone` 是单标签的简写;`tags` 可用字符串或 `{label, tone}`。tone 为 `indigo / teal / amber / rose / slate`,省略时自动轮换。`detail` 作为选中记录的悬停说明。内容与声波仅做展示,不读取麦克风。
|
||||
|
||||
知识透镜的三个来源对应环形、柱状、折线图,点击引文会同时更新选中图表、答案和引用说明:
|
||||
|
||||
```js
|
||||
mount(element, {content: {sources: [
|
||||
{label: '验收进度', answer: '已完成 73% 的验收项。', citation: '验收清单 · 第 2 版', chart: {value: 73, max: 100}},
|
||||
{label: '运行记录', answer: '峰值集中在后半段。', citation: '运行日志 · 本周', chart: {series: [12, 18, 26, 41]}},
|
||||
{label: '性能趋势', answer: '响应时间逐步回落。', citation: '性能报告 · 第 3 页', chart: {series: [80, 62, 54, 31]}}
|
||||
]}});
|
||||
```
|
||||
|
||||
`chart` 省略时保留模板图形;序列过滤非有限数,最多 32 项,空数组清空对应图形;环形数值按 max 归一化并限制到 0–100%。引用和答案只作为纯文本,不从文案推算数值。
|
||||
|
||||
```js
|
||||
mount(element, {mode: 'mesh', content: {
|
||||
title: '让系统之间,\n保持清楚的连接。', label: '推理节点 · 在线',
|
||||
nodes: [
|
||||
{id: 'gateway', label: '入口', position: [-.7, .4, .1]},
|
||||
{id: 'agent', label: '推理节点', position: [.1, 0, .3]},
|
||||
{id: 'store', label: '存储', position: [.7, -.4, -.2]}
|
||||
],
|
||||
links: [{from: 'gateway', to: 'agent'}, {from: 'agent', to: 'store'}]
|
||||
}});
|
||||
```
|
||||
|
||||
拓扑 position 是空间坐标,建议每轴约 -1 到 1,限制到 ±1.5;最多展示 64 个节点、160 条有效链路。省略 links 时连接相邻节点,空数组表示无链路;空 nodes 保留环境星点。上述展示数量约束服务于卡片快照,不代表完整业务数据模型。
|
||||
|
||||
## 艺术变体
|
||||
|
||||
- 粒子环当前保留环形结构。showcase 的 `glow-shapes` 程序背景另支持 `options.shape / data-shape` 为 `circle / heart / star / petal`(默认 heart);这不作用于四张 editorial 位图封面。
|
||||
- 实体通证:`options.variant / data-variant` 为 `token`(默认)或 `card`。token 有激光刻蚀短码、规格与 LED 呼吸脉冲,card 保留无芯片流光与接触阴影;两者使用同一 WebGL 材质。直接修改 data-variant 即可切换。
|
||||
- 空间拓扑:`options.mode / data-mode` 为 `globe`(默认)或 `mesh`,直接修改属性可切换。mesh 显示有空间纵深的星座点场、链路光点和节点脉冲;共享主题、配色与动效控制。
|
||||
- 地球连线:`options.arcs / data-arcs` 选择 1–3,默认 2。
|
||||
- 丝带:实例 `setRenderer('svg' | 'webgl' | 'dots' | 'grid')`;WebGL 不可用时返回 SVG。四个变体共享曲面结构,分别呈现线束、薄绸、点阵和网格。
|
||||
- showcase:HTML 的 `data-background` 选择既有视觉方案,`editorial` 为原色位图,保留其自身配色。
|
||||
|
||||
这里的变体具有不同视觉用途,不需要在每个卡片上全部开启。示例中的裁切、出血、居中与留白按各自构图保留。
|
||||
|
||||
## 构建与验证入口
|
||||
|
||||
```sh
|
||||
python3 scripts/build_cards.py
|
||||
```
|
||||
|
||||
Python 3.10+,仅标准库。构建把片段、CSS、命名 ES 模块、地图掩码与本地封面内联到 `gallery.html`,可直接打开;构建不联网。复制模块到项目时仍需保留其相对导入关系。小打包器只服务当前命名 import/export,不是通用 JS 打包器。
|
||||
|
||||
```text
|
||||
assets/cards/
|
||||
gallery.html 单文件构建产物
|
||||
gallery.template.html
|
||||
gallery.css / gallery.js 画廊布局、控制和实例生命周期
|
||||
gallery-content.js 两套示例内容,组件不依赖此文件
|
||||
gallery-palette.js / gallery-catalog.js 配色编辑和分类筛选,只服务画廊
|
||||
shared/content.js 纯文本、链接、图片槽
|
||||
shared/shell.css / finish.css / runtime.js 外壳、材质、交互、时钟与绘制辅助
|
||||
shared/ne_110m_land.zip / land-mask.js 地图构建输入和输出
|
||||
<模板名>/card.html / card.css / card.js
|
||||
```
|
||||
|
||||
配色查询 `colors=256b70,7dc0af,d4bb83` 接受 1–3 个 3/6 位 HEX(可省略 #),由数量确定单/双/三色;显式 URL 优先于保存的选择,不会仅因打开 URL 而覆盖保存值。`category=visual|product|data|action` 可直接进入对应分类,省略时展示全部。旧的 state/preset 色板 URL 为既有截图入口保留,界面已移除预设按钮。
|
||||
|
||||
共享外壳分别提供图形色和内部强调文字色。文字色仅限制明度、保留色相;图形使用原始色值,showcase 位图封面不染色;portrait-story 用所选主色作为单色摄影覆盖层。
|
||||
|
||||
画廊查询参数:`content=studio|developer`、`shadow=auto|none|contact|lifted`、`material=plain|metal|glass|solid`、`effects=none|tilt|glow|tilt%20glow`、`title=default|top|bottom`、`motion=auto|hover|off`、`speed=0`、`state=sage|mono|clay|dark`、`ribbons=svg|webgl|dots|grid`、`variant=token|card`(通证)、`mode=globe|mesh`(空间拓扑)。`focus=<模板名>` 、`focus=explorations` 或 `focus=peer-cards` 用于局部检查。
|
||||
|
||||
持续动画跟随屏幕刷新率,时间增量参与插值;有限演示完成后停止。屏外、后台、减少动态偏好都停止播放。画布尺寸按未变换的布局尺寸计算,外层倾斜不会反复改变绘制缓冲区。
|
||||
|
||||
使用现代浏览器的原生 CSS 嵌套、相对 OKLCH、color-mix、overflow:clip、dialog、WebGL 1。地图来自 Natural Earth 110m land(数据来源和归档保存在 shared 中);四张 editorial 封面为生成素材、搭配虚构品牌。场景文案、数值、任务与运行输出均为演示内容。
|
||||
|
||||
## 新增同类网站探索
|
||||
|
||||
页首「从视觉形态,到产品细节」包含五个中立模板:影像叙事、讨论记录、知识透镜、响应式展开与构建流水线。影像叙事归入「视觉意象」,其余归入「产品场景」。脚注按形态命名,组件不依赖商业平台名称或默认外链。
|
||||
|
||||
`portrait-story` 的默认人物为 AI 生成的虚构人物,文案是编辑式示例;图片提示词与使用说明保存在 `portrait-story/images/README.md`。其他四张新卡使用 DOM / SVG。讨论条仅演示波形,不访问麦克风;引用答案不调用 AI;尺寸窗口不运行图片优化;构建过程不创建真实部署,预览地址只是文本。
|
||||
|
||||
这五张卡复用公共配色、内容与动效接口,不再添加强度、散布角度、波形频率等艺术微调参数。用户的关闭动效、速度 0 和系统减少动态偏好会同时停止脚本动画与内部 CSS 过渡。
|
||||
|
||||
## 材质与内容的灵活度
|
||||
|
||||
阴影和材质作用于外层卡片;showcase 作用于封面窗口。摄影和封面只调整边缘及接触阴影,不叠加金属亮带或颗粒;实体卡保留轻薄边缘,选择「无阴影」仍有边缘厚度。材质静态渲染,不增加帧循环。玻璃支持真实 backdrop-filter,在不支持的环境中退回更不透明的表面;摄影与不透明画布依然遮住其后方背景,表面呈覆膜效果。画廊选择玻璃时增加淡色宿主背景,便于看清透光。
|
||||
|
||||
`options` 优先于卡片自身属性;这两组配置不从祖先继承,挂载后可直接修改 `data-shadow` / `data-material`。画廊选择保存在 `oil.cards.finish.v2`。切换材质会将阴影控制设为「跟随材质」,之后可单独覆盖;URL 显式材质也默认带入配套阴影,显式 shadow 参数优先。访问预览链接不会自动写入保存值。
|
||||
|
||||
内容通过 `mount(element, {content})` 传入,大部分业务标题、说明、标签、记录、引用、文件和数值可替换,完整字段见上表。挂载后更换内容采用前述销毁、恢复片段、重新挂载流程;当前没有通用的原地 `update()`,也没有画廊内点字编辑器。HTML 文字槽可直接编辑,数据由 JS 生成的列表和数值需修改对应 content。
|
||||
|
||||
图片替换仅适用于现有图片槽:portrait-story 和 showcase 的 editorial / image 类型;程序绘制的曲面与粒子保留艺术结构;知识透镜图表及空间拓扑节点可用上述数据接口替换。构图比例、裁切、图形结构与动画路径保留为模板的设计部分。示意图表和检索文案并不等于已接入真实服务。
|
||||
@@ -0,0 +1,33 @@
|
||||
.card.answer-lens {
|
||||
--_card-stage-track-min:340px; aspect-ratio:auto; background:color-mix(in oklab,var(--card-surface) 95%,var(--card-ink));
|
||||
& .card-stage { display:grid; grid-template-rows:minmax(210px,1fr) auto; }
|
||||
& .answer-scene { position:relative; display:flex; flex-direction:column; overflow:hidden; min-height:250px; }
|
||||
& .answer-dashboard { position:relative; flex:none; margin:0 22px; min-height:224px; padding:22px 22px 110px; border-radius:10px; background:var(--card-panel); border:1px solid color-mix(in oklab,var(--card-ink) 7%,transparent); box-shadow:0 4px 16px #18243206; }
|
||||
& .answer-dash-kicker { display:block; color:var(--card-muted); font-size:9px; margin-bottom:8px; }
|
||||
& .answer-dashboard strong { display:block; font-size:19px; line-height:1.3; font-weight:550; overflow-wrap:anywhere; }
|
||||
& .answer-sync { display:inline-block; font-size:9px; margin-top:8px; padding:3px 6px; color:var(--card-muted); background:var(--card-wash); border-radius:3px; }
|
||||
& .answer-dash-lines { position:absolute; left:22px; right:22px; bottom:25px; display:grid; gap:16px; & i { height:1px; background:var(--card-line); } }
|
||||
& .answer-charts { position:relative; flex:none; height:150px; margin-top:-100px; }
|
||||
& .answer-chart { position:absolute; width:43%; height:178px; padding:13px; background:var(--card-panel); border:1px solid color-mix(in oklab,var(--card-ink) 9%,transparent); border-radius:9px; box-shadow:0 6px 18px #1824320a; transform:translateY(16px) rotate(var(--angle)); transition:transform .65s cubic-bezier(.2,.8,.2,1),border-color .3s; }
|
||||
& .answer-chart:nth-child(1){left:-4%;--angle:-10deg;--chart-color:var(--card-accent);}
|
||||
& .answer-chart:nth-child(2){left:29%;--angle:0deg;--chart-color:var(--_card-accent-2);}
|
||||
& .answer-chart:nth-child(3){left:64%;--angle:10deg;--chart-color:var(--_card-accent-3);}
|
||||
& .answer-chart[data-selected="true"] { transform:translateY(-7px) rotate(var(--angle)); z-index:2; border-color:color-mix(in oklab,var(--card-accent) 40%,var(--card-line)); }
|
||||
& .answer-chart svg { width:100%; height:117px; display:block; }
|
||||
& .answer-chart > span:last-child { font-size:9px; color:var(--card-muted); }
|
||||
& .chart-symbol { position:absolute; top:-12px; right:18px; width:27px; height:27px; display:grid; place-items:center; border-radius:6px; background:var(--card-panel); box-shadow:0 2px 7px #18243216; color:var(--_card-accent-ink); font-size:19px; }
|
||||
& .ring-base,& .ring-value { fill:none; stroke:var(--card-line); stroke-width:9; }
|
||||
& .ring-value { stroke:var(--chart-color); stroke-dasharray:220 302; transform:rotate(-90deg); transform-origin:80px 78px; }
|
||||
& .chart-grid { fill:none; stroke:var(--card-line); stroke-width:1; }
|
||||
& .chart-bars { stroke:var(--chart-color); stroke-width:15; opacity:.75; }
|
||||
& .chart-line { fill:none; stroke:var(--chart-color); stroke-width:2; }
|
||||
& .answer-response { position:relative; margin:0 22px 22px; padding:14px 15px; background:var(--card-panel); border:1px solid color-mix(in oklab,var(--card-ink) 12%,transparent); border-radius:11px; box-shadow:0 5px 18px #1824320a; }
|
||||
& .answer-response > p { font-size:12px; line-height:1.5; margin:0 0 12px; }
|
||||
& .answer-citations { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:10px; }
|
||||
& .answer-citations button { display:flex; align-items:center; gap:5px; color:var(--card-muted); background:var(--card-wash); border:1px solid transparent; border-radius:4px; padding:4px 6px; font-size:9px; cursor:pointer; }
|
||||
& .answer-citations button[aria-pressed="true"] { color:var(--_card-accent-ink); border-color:color-mix(in oklab,var(--card-accent) 25%,var(--card-line)); background:color-mix(in oklab,var(--card-accent) 7%,var(--card-panel)); }
|
||||
& .answer-excerpt { font-size:11px; line-height:1.7; color:var(--card-muted); min-height:3.4em; overflow-wrap:anywhere; }
|
||||
& .answer-reference { display:block; margin-top:8px; padding-top:8px; border-top:1px solid var(--card-line); font:9px ui-monospace,monospace; color:var(--card-muted); overflow-wrap:anywhere; }
|
||||
& .answer-reference[hidden] { display:none; }
|
||||
}
|
||||
@media(prefers-reduced-motion:reduce){.answer-lens *{transition:none!important;}}
|
||||
@@ -0,0 +1,7 @@
|
||||
<article class="card answer-lens" data-card="answer-lens" style="--card-accent:#5865c7">
|
||||
<h2 class="card-heading" data-text="title">找到答案,
|
||||
也看见它从哪里来。</h2>
|
||||
<div class="card-stage"><div class="answer-scene"><div class="answer-dashboard"><span class="answer-dash-kicker" data-text="eyebrow">团队知识库</span><strong data-text="dashboard">本周项目概览</strong><span class="answer-sync" data-text="status">内容持续同步</span><div class="answer-dash-lines" aria-hidden="true"><i></i><i></i><i></i></div></div>
|
||||
<div class="answer-charts" aria-hidden="true"><div class="answer-chart" data-chart="0"><span class="chart-symbol">◷</span><svg viewBox="0 0 160 150"><circle cx="80" cy="78" r="48" class="ring-base"/><circle cx="80" cy="78" r="48" class="ring-value"/></svg><span data-text="sources.0.label">项目进度</span></div><div class="answer-chart" data-chart="1"><span class="chart-symbol">▥</span><svg viewBox="0 0 160 150"><path class="chart-grid" d="M15 40h130M15 75h130M15 110h130"/><path class="chart-bars" d="M34 115V81M67 115V58M100 115V35M132 115V14"/></svg><span data-text="sources.1.label">内容表现</span></div><div class="answer-chart" data-chart="2"><span class="chart-symbol">⌁</span><svg viewBox="0 0 160 150"><path class="chart-grid" d="M15 40h130M15 75h130M15 110h130"/><path class="chart-line" d="M8 120C30 130 30 70 48 78S60 103 77 60s22 26 40-12 20-25 35-42"/></svg><span data-text="sources.2.label">用户反馈</span></div></div></div>
|
||||
<div class="answer-response"><p data-text="question">这周,什么值得继续投入?</p><div class="answer-citations" aria-label="选择答案来源"></div><div class="answer-excerpt" aria-live="polite"></div><span class="answer-reference"></span></div></div>
|
||||
</article>
|
||||
@@ -0,0 +1,25 @@
|
||||
import {shell,clamp} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,options),events=new AbortController(),list=element.querySelector('.answer-citations'),excerpt=element.querySelector('.answer-excerpt'),reference=element.querySelector('.answer-reference'),charts=[...element.querySelectorAll('[data-chart]')];
|
||||
const defaults=[{label:'项目进度',answer:'核心页面已完成评审,下一步可以集中打磨移动端体验。',citation:'项目记录 · 本周评审'},{label:'内容表现',answer:'实用教程获得了更多收藏,可以继续拓展成一个系列。',citation:'内容记录 · 系列表现'},{label:'用户反馈',answer:'大家最常提到的是查找素材,优先把检索入口做清楚。',citation:'反馈记录 · 需求整理'}];
|
||||
const sources=(Array.isArray(options.content?.sources)?options.content.sources:defaults).filter(source=>source && typeof source==='object').slice(0,3);
|
||||
const select=index=>{[...list.children].forEach((b,i)=>b.setAttribute('aria-pressed',String(i===index)));charts.forEach((c,i)=>c.dataset.selected=String(i===index));excerpt.textContent=String(sources[index]?.answer ?? options.content?.emptyText ?? '还没有可引用的来源。');reference.textContent=String(sources[index]?.citation ?? '');reference.hidden=!reference.textContent;};
|
||||
list.replaceChildren();
|
||||
charts.forEach((chart,i)=>{
|
||||
chart.hidden=!sources[i];if(!sources[i])return;
|
||||
chart.querySelector('[data-text]').textContent=String(sources[i].label ?? '');
|
||||
const data=sources[i].chart;if(!data || typeof data!=='object')return;
|
||||
if(i===0 && typeof data.value==='number' && Number.isFinite(data.value)){
|
||||
const max=typeof data.max==='number' && Number.isFinite(data.max) && data.max>0?data.max:100,ratio=clamp(data.value/max),length=2*Math.PI*48;
|
||||
chart.querySelector('.ring-value').style.strokeDasharray=`${length*ratio} ${length*(1-ratio)}`;
|
||||
}
|
||||
if(i>0 && Array.isArray(data.series)){
|
||||
const series=data.series.filter(n=>typeof n==='number' && Number.isFinite(n)).slice(0,32),path=chart.querySelector(i===1?'.chart-bars':'.chart-line');
|
||||
const low=Math.min(0,...series),high=Math.max(1,...series),y=value=>115-(value-low)/(high-low)*95;
|
||||
if(i===1){path.setAttribute('d',series.map((n,j)=>`M${20+(j+.5)*120/series.length} 115V${y(n)}`).join(' '));path.style.strokeWidth=String(Math.min(15,72/Math.max(1,series.length)));}
|
||||
else path.setAttribute('d',series.map((n,j)=>`${j?'L':'M'}${series.length===1?80:15+j*130/(series.length-1)} ${y(n)}`).join(' '));
|
||||
}
|
||||
});
|
||||
sources.forEach((source,index)=>{const b=document.createElement('button');b.type='button';b.textContent=`${index+1} · ${source.label ?? ''}`;b.addEventListener('click',()=>select(index),{signal:events.signal});list.append(b);});select(0);
|
||||
return {destroy(){events.abort();clean();list.replaceChildren();charts.forEach(chart=>{delete chart.dataset.selected;chart.querySelector('.ring-value')?.style.removeProperty('stroke-dasharray');chart.querySelector('.chart-bars')?.style.removeProperty('stroke-width');});}};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.beam-graph {
|
||||
& .card-stage::before { content:""; position:absolute; inset:0; background-image:radial-gradient(color-mix(in oklab,var(--card-accent) 20%,transparent) .7px,transparent .8px); background-size:15px 15px; mask-image:radial-gradient(ellipse in oklab,#000,transparent 72%); }
|
||||
& .beam-lines { position:absolute; width:100%; height:100%; }
|
||||
& .beam-base { fill:none; stroke:color-mix(in oklab,var(--card-muted) 35%,var(--card-surface)); stroke-width:1.5; }
|
||||
& .beam-light,& .beam-glow { fill:none; stroke-linecap:round; stroke-width:2; stroke-dasharray:72 600; }
|
||||
& .beam-glow { stroke-width:6; opacity:.55; }
|
||||
& .beam-node { position:absolute; display:flex; align-items:center; gap:7px; font-size:13px; border:1px solid var(--card-line); padding:10px 15px; border-radius:30px; background:var(--card-panel); transform:translate(-50%,-50%); white-space:nowrap; box-shadow:0 3px 10px #29314b07; & i { width:5px; height:5px; border-radius:50%; background:var(--_card-accent-2); } }
|
||||
& .beam-a { left:18%; top:20%; } & .beam-b { left:18%; top:50%; } & .beam-e { left:18%; top:80%; } & .beam-c { left:82%; top:32%; } & .beam-d { left:82%; top:68%; }
|
||||
& .beam-center { left:50%; top:50%; font-size:22px; font-weight:600; letter-spacing:-.02em; padding:17px 20px; border-color:color-mix(in oklab,var(--card-accent) 25%,var(--card-line)); box-shadow:0 0 0 9px var(--card-surface),0 0 0 10px color-mix(in oklab,var(--card-accent) 35%,var(--card-surface)),0 0 35px color-mix(in oklab,var(--card-accent) 16%,transparent); }
|
||||
@media(max-width:650px) { & .beam-node { font-size:10px; padding:7px 10px; gap:4px; } & .beam-center { font-size:15px; padding:12px 13px; } }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<article class="card beam-graph bottom-title" data-card="beam-graph" data-motion="auto">
|
||||
<div class="card-stage"><svg class="beam-lines" viewBox="0 0 400 250" preserveAspectRatio="none" aria-hidden="true"><defs></defs><g class="beam-base"><path d="M72 50H135Q147 50 147 62V113Q147 125 159 125H200"/><path d="M72 125H200"/><path d="M72 200H135Q147 200 147 188V137Q147 125 159 125H200"/><path d="M200 125H255Q267 125 267 113V92Q267 80 279 80H328"/><path d="M200 125H255Q267 125 267 137V158Q267 170 279 170H328"/></g><g class="beam-glow"></g><g class="beam-light"></g></svg><span class="beam-node beam-a"><i></i><span data-text="nodes.0">灵感</span></span><span class="beam-node beam-b"><i></i><span data-text="nodes.1">内容</span></span><span class="beam-node beam-e"><i></i><span data-text="nodes.2">团队</span></span><span class="beam-node beam-center" data-text="hub">morrow</span><span class="beam-node beam-c"><i></i><span data-text="nodes.3">产品</span></span><span class="beam-node beam-d"><i></i><span data-text="nodes.4">世界</span></span></div>
|
||||
<div class="card-caption"><h2 data-text="title">每一份创造,都能相互连接</h2><p data-text="description">把零散的工具,连成顺畅的工作流。</p></div>
|
||||
</article>
|
||||
@@ -0,0 +1,10 @@
|
||||
import {loop,shell,smooth} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,options);
|
||||
const svg=element.querySelector('svg'),id='beam-'+crypto.randomUUID(),defs=svg.querySelector('defs');
|
||||
const source=[...svg.querySelectorAll('.beam-base path')];
|
||||
defs.innerHTML=`<filter id="${id}-blur" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="3"/></filter>`;
|
||||
const beams=source.map((path,i)=>{const gid=id+'-'+i;defs.insertAdjacentHTML('beforeend',`<linearGradient id="${gid}" gradientUnits="userSpaceOnUse"><stop stop-color="var(--card-accent)" stop-opacity="0"/><stop offset=".55" stop-color="var(--card-accent)"/><stop offset="1" stop-color="color-mix(in oklab,var(--card-accent) 25%,white)"/></linearGradient>`);const copies=['beam-glow','beam-light'].map(cls=>{const n=path.cloneNode();n.setAttribute('stroke',`url(#${gid})`);if(cls==='beam-glow')n.setAttribute('filter',`url(#${id}-blur)`);svg.querySelector('.'+cls).append(n);return n;});return {path,length:path.getTotalLength(),gradient:defs.querySelector('#'+gid),copies};});
|
||||
const clock=loop(element,time=>{for(const [i,b]of beams.entries()){const phase=((time+i*.71+1)%4)/4,head=phase*(b.length+72),tail=Math.max(0,head-72),a=b.path.getPointAtLength(tail),z=b.path.getPointAtLength(Math.min(head,b.length));for(const [n,v]of [['x1',a.x],['y1',a.y],['x2',z.x],['y2',z.y]])b.gradient.setAttribute(n,v);for(const p of b.copies){p.style.strokeDashoffset=String(72-head);p.style.opacity=String(smooth(head/28)*(1-smooth((head-b.length)/72)));}}},options);
|
||||
return {destroy(){clock.destroy();clean();svg.querySelector('.beam-light').replaceChildren();svg.querySelector('.beam-glow').replaceChildren();defs.replaceChildren();}};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
.card.capture-notes {
|
||||
--_card-stage-track-min:340px; aspect-ratio:auto; background:color-mix(in oklab,var(--card-surface) 95%,var(--card-ink));
|
||||
& .card-stage { display:grid; grid-template-rows:minmax(0,1fr) auto; gap:12px; padding:0 22px 22px; }
|
||||
& .capture-sheet { min-height:0; overflow:hidden; background:var(--card-panel); border:1px solid color-mix(in oklab,var(--card-ink) 8%,transparent); border-radius:10px; box-shadow:0 5px 18px #18243207; padding:23px 19px 0; mask-image:linear-gradient(#000 82%,transparent); }
|
||||
& .capture-sheet-title { display:flex; align-items:center; gap:8px; margin-bottom:16px; font-size:20px; line-height:1.3; }
|
||||
& .capture-sheet-title strong { font-weight:550; overflow-wrap:anywhere; }
|
||||
& .capture-sheet-title svg { width:22px; height:22px; flex:none; fill:none; stroke:var(--_card-accent-ink); stroke-width:1.7; stroke-linecap:round; }
|
||||
& .capture-tabs { display:flex; gap:12px; align-items:center; color:var(--card-muted); font-size:10px; line-height:1.5; padding-bottom:13px; border-bottom:1px solid var(--card-line); }
|
||||
& .capture-tabs span:first-child { color:var(--card-ink); background:var(--card-wash); padding:3px 6px; border-radius:4px; }
|
||||
& .capture-tabs i { font-style:normal; margin-left:auto; }
|
||||
& .capture-labels { display:flex; justify-content:space-between; font-size:9px; color:var(--card-muted); padding:12px 0 6px; }
|
||||
& .capture-row { display:flex; align-items:center; gap:8px; width:100%; min-width:0; padding:12px 0; background:transparent; border:0; border-top:1px solid var(--card-line); text-align:left; color:var(--card-ink); cursor:pointer; font-size:11px; line-height:1.3; }
|
||||
& .capture-row::before { content:''; width:9px; height:12px; flex:none; border:1px solid var(--card-muted); border-radius:1px; background:linear-gradient(var(--card-line),var(--card-line)) 2px 4px/5px 1px no-repeat; }
|
||||
& .capture-row span { font-size:11px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
& .capture-row .capture-tags { display:flex; flex:none; gap:4px; margin-left:auto; max-width:42%; }
|
||||
& .capture-row small { flex:0 1 auto; min-width:0; font-size:9px; color:var(--tag-ink); background:color-mix(in oklab,var(--tag-ink) 11%,var(--card-panel)); border-radius:3px; padding:3px 5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
& [data-tone="indigo"] { --tag-ink:light-dark(#5960b5,#a8adf3); }
|
||||
& [data-tone="teal"] { --tag-ink:light-dark(#347768,#88c9b7); }
|
||||
& [data-tone="amber"] { --tag-ink:light-dark(#936b32,#d9b277); }
|
||||
& [data-tone="rose"] { --tag-ink:light-dark(#995c74,#d8a1b5); }
|
||||
& [data-tone="slate"] { --tag-ink:light-dark(#5f7089,#adbdd3); }
|
||||
& .capture-row[aria-pressed="true"] span { color:var(--_card-accent-ink); }
|
||||
& .capture-recorder { display:flex; align-items:center; gap:9px; min-width:0; padding:9px 11px; background:var(--card-panel); border:1px solid color-mix(in oklab,var(--card-ink) 12%,transparent); border-radius:40px; box-shadow:0 6px 16px #1118270a; }
|
||||
& .record-dot { width:6px; height:6px; flex:none; border-radius:50%; background:var(--card-accent); }
|
||||
& .capture-now { font-size:11px; min-width:0; flex:1; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
|
||||
& .capture-wave { display:flex; align-items:center; gap:3px; height:22px; flex:none; }
|
||||
& .capture-wave i { width:2px; height:20px; background:var(--card-muted); border-radius:2px; transform:scaleY(.3); }
|
||||
& .capture-pause { display:grid; place-items:center; border:0; border-radius:50%; width:28px; height:28px; flex:none; background:var(--card-wash); color:var(--card-ink); cursor:pointer; }
|
||||
& .capture-pause svg { width:15px; height:15px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<article class="card capture-notes" data-card="capture-notes" style="--card-accent:#5865c7">
|
||||
<h2 class="card-heading" data-text="title">把每一次讨论,
|
||||
留在同一份记录里。</h2>
|
||||
<div class="card-stage"><div class="capture-sheet"><div class="capture-sheet-title"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M6 10v2a6 6 0 0 0 12 0v-2M12 18v3M8 21h8"/></svg><strong data-text="collection">团队讨论</strong></div><div class="capture-tabs"><span data-text="tab">全部记录</span><span data-text="scope">我的会议</span><i>+</i></div><div class="capture-labels"><span data-text="listLabel">会议主题</span><span data-text="tagLabel">团队</span></div><div class="capture-rows"></div></div>
|
||||
<div class="capture-recorder"><span class="record-dot"></span><span class="capture-now"></span><span class="capture-wave" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span><button class="capture-pause" aria-label="暂停波形动画"><svg viewBox="0 0 20 20"><path d="M7 5v10M13 5v10"/></svg></button></div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,25 @@
|
||||
import {shell,loop,staticMotion,clamp} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,options),content=options.content || {},rows=element.querySelector('.capture-rows'),now=element.querySelector('.capture-now'),bars=[...element.querySelectorAll('.capture-wave i')],button=element.querySelector('.capture-pause');
|
||||
const defaults=[{title:'首页设计评审',team:'设计'},{title:'内容与视觉方向',team:'创作'},{title:'本周项目同步',team:'产品'},{title:'新想法的第一轮讨论',team:'团队'},{title:'发布前的最后一次检查',team:'工程'}];
|
||||
const records=(Array.isArray(content.records)?content.records:defaults).filter(record=>record && typeof record==='object');
|
||||
const still=staticMotion(element,options),events=new AbortController(),tones=['indigo','teal','amber','rose','slate'];let clock,paused=false,phase=0,selected=0;
|
||||
rows.replaceChildren();
|
||||
const select=index=>{selected=index;now.textContent=String(records[index]?.title ?? content.emptyText ?? '还没有讨论记录');now.title=String(records[index]?.detail ?? now.textContent);[...rows.children].forEach((r,i)=>r.setAttribute('aria-pressed',String(i===index)));clock?.invalidate();};
|
||||
records.forEach((record,index)=>{
|
||||
const row=document.createElement('button'),name=document.createElement('span'),tags=document.createElement('span');row.type='button';row.className='capture-row';tags.className='capture-tags';name.textContent=String(record.title ?? '');
|
||||
const values=Array.isArray(record.tags)?record.tags:[{label:record.team ?? '',tone:record.tone}];
|
||||
values.forEach((value,i)=>{const tag=document.createElement('small'),label=typeof value==='object'?value?.label:value,tone=typeof value==='object'?value?.tone:undefined;tag.textContent=String(label ?? '');tag.dataset.tone=tones.includes(tone)?tone:tones[(index+i)%tones.length];tags.append(tag);});
|
||||
row.append(name,tags);row.addEventListener('click',()=>select(index),{signal:events.signal});rows.append(row);
|
||||
});select(0);
|
||||
const start=()=>{clock?.destroy();clock=loop(element,(time,dt)=>{
|
||||
if(!paused)phase+=dt;
|
||||
const waveform=records[selected]?.waveform;
|
||||
bars.forEach((b,i)=>{const sample=Array.isArray(waveform)&&waveform.length?waveform[i%waveform.length]:undefined,base=typeof sample==='number'&&Number.isFinite(sample)?clamp(sample):undefined;
|
||||
const value=base!==undefined?clamp(base*(still()?1:.8+.2*Math.sin(phase*3.2+i*.85)),.08,1):still()?.25+.5*Math.abs(Math.sin(i*1.8)):.2+.8*Math.abs(Math.sin(phase*3.2+i*.85)*Math.cos(phase*1.4-i*.32));b.style.transform=`scaleY(${value})`;
|
||||
});
|
||||
},{...options,continuous:!paused&&records.length>0});};
|
||||
const syncButton=()=>{button.setAttribute('aria-label',paused?'播放波形动画':'暂停波形动画');button.setAttribute('aria-pressed',String(paused));button.querySelector('path').setAttribute('d',paused?'m7 4 8 6-8 6Z':'M7 5v10M13 5v10');};
|
||||
button.disabled=!records.length;syncButton();button.addEventListener('click',()=>{paused=!paused;syncButton();start();},{signal:events.signal});start();
|
||||
return {destroy(){clock.destroy();events.abort();clean();rows.replaceChildren();bars.forEach(bar=>bar.style.removeProperty('transform'));}};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
.card.deploy-preview {
|
||||
--_card-stage-track-min:320px; aspect-ratio:auto;
|
||||
& > .card-heading { text-wrap:balance; }
|
||||
& .card-stage { display:grid; grid-template-rows:minmax(245px,1fr) auto; padding:0 24px; }
|
||||
& .deploy-window { align-self:center; width:100%; overflow:hidden; background:var(--card-panel); border:1px solid color-mix(in oklab,var(--card-ink) 14%,var(--card-panel)); border-radius:10px; box-shadow:0 8px 18px #18243206; }
|
||||
& .deploy-window-bar { display:flex; justify-content:space-between; gap:12px; align-items:center; flex-wrap:wrap; padding:13px 15px; font:10px/1.4 ui-monospace,monospace; color:var(--card-muted); background:var(--card-wash); border-bottom:1px solid var(--card-line); overflow-wrap:anywhere; }
|
||||
& .deploy-branch { padding:2px 5px; border:1px solid var(--card-line); border-radius:4px; }
|
||||
& .deploy-commit { display:grid; grid-template-columns:24px minmax(0,1fr) auto; gap:8px; align-items:center; padding:18px 15px 14px; }
|
||||
& .deploy-commit > svg { width:22px; height:22px; fill:none; stroke:var(--card-muted); stroke-width:1.3; }
|
||||
& .deploy-commit strong { font-size:12px; line-height:1.5; font-weight:550; overflow-wrap:anywhere; }
|
||||
& .deploy-status { color:var(--_card-accent-ink); background:color-mix(in oklab,var(--card-accent) 8%,var(--card-panel)); font-size:9px; padding:4px 6px; border-radius:20px; white-space:nowrap; }
|
||||
& .deploy-progress { margin:0 16px; height:2px; background:var(--card-line); overflow:hidden; }
|
||||
& .deploy-progress > span { display:block; width:100%; height:100%; background:var(--card-accent); transform:scaleX(var(--build-progress,1)); transform-origin:left; }
|
||||
& .deploy-steps { display:flex; justify-content:space-between; gap:8px; padding:9px 16px 15px; color:var(--card-muted); font-size:9px; line-height:1.5; }
|
||||
& .deploy-steps > span[data-ready="true"] { color:var(--_card-accent-ink); }
|
||||
& .deploy-result { display:grid; grid-template-columns:86px minmax(0,1fr); gap:15px; padding:16px; border-top:1px solid var(--card-line); align-items:center; opacity:calc(.2 + .8*var(--preview-ready,1)); transform:translateY(calc((1 - var(--preview-ready,1))*6px)); }
|
||||
& .deploy-mini { height:82px; background:var(--card-wash); border:1px solid var(--card-line); border-radius:4px; padding:9px; }
|
||||
& .deploy-mini > i { display:block; width:35%; height:3px; background:var(--card-muted); opacity:.35; }
|
||||
& .deploy-mini > div { display:flex; gap:4px; margin:7px 0; }
|
||||
& .deploy-mini b { width:50%; height:33px; border-radius:1px; background:color-mix(in oklab,var(--card-accent) 12%,var(--card-panel)); }
|
||||
& .deploy-mini b:first-child { background:color-mix(in oklab,var(--card-accent) 32%,var(--card-panel)); }
|
||||
& .deploy-mini > span { display:block; height:2px; width:80%; margin-top:4px; background:var(--card-line); }
|
||||
& .deploy-result-copy { display:grid; gap:7px; min-width:0; }
|
||||
& .deploy-result-copy small { color:var(--card-muted); font-size:9px; }
|
||||
& .deploy-result-copy strong { font:11px/1.4 ui-monospace,monospace; overflow-wrap:anywhere; }
|
||||
& .deploy-result-copy > span { color:var(--card-muted); font-size:10px; line-height:1.6; overflow-wrap:anywhere; }
|
||||
& .deploy-history { display:flex; gap:18px; padding:18px 0 24px; }
|
||||
& .deploy-history button { display:flex; gap:7px; align-items:center; padding:0; border:0; background:transparent; color:var(--card-muted); font-size:11px; cursor:pointer; }
|
||||
& .deploy-history i { width:7px; height:7px; border:1px solid var(--card-muted); border-radius:50%; }
|
||||
& .deploy-history button[aria-pressed="true"] { color:var(--card-ink); }
|
||||
& .deploy-history button[aria-pressed="true"] i { background:var(--card-accent); border-color:var(--card-accent); }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<article class="card deploy-preview" data-card="deploy-preview" style="--card-accent:#5865c7">
|
||||
<h2 class="card-heading" data-text="title">每一次改动,
|
||||
都能先预览,再分享。</h2>
|
||||
<div class="card-stage"><div class="deploy-window"><div class="deploy-window-bar"><span data-text="project">workspace / website</span><span class="deploy-branch" data-release-field="branch">design/home</span></div><div class="deploy-commit"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="7" cy="5" r="2"/><circle cx="7" cy="19" r="2"/><circle cx="18" cy="5" r="2"/><path d="M7 7v10m11-10c0 6-11 1-11 10"/></svg><strong data-release-field="title">更新作品首页</strong><span class="deploy-status" aria-live="polite"></span></div><div class="deploy-progress"><span></span></div><div class="deploy-steps"><span data-text="steps.0">准备资源</span><span data-text="steps.1">完成构建</span><span data-text="steps.2">生成预览</span></div><div class="deploy-result"><div class="deploy-mini" aria-hidden="true"><i></i><div><b></b><b></b></div><span></span><span></span></div><div class="deploy-result-copy"><small data-text="resultLabel">预览地址</small><strong data-release-field="url">preview.workspace.local</strong><span data-text="resultNote">把这一版,分享给一起创作的人。</span></div></div></div>
|
||||
<div class="deploy-history" aria-label="选择预览版本"><button type="button" data-version="0" aria-pressed="true"><i></i><span data-text="versionLabels.0">当前版本</span></button><button type="button" data-version="1" aria-pressed="false"><i></i><span data-text="versionLabels.1">上一版本</span></button></div></div>
|
||||
</article>
|
||||
@@ -0,0 +1,8 @@
|
||||
import {shell,loop,staticMotion,smooth} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,options),still=staticMotion(element,options),buttons=[...element.querySelectorAll('[data-version]')],steps=[...element.querySelectorAll('.deploy-steps > span')],status=element.querySelector('.deploy-status'),events=new AbortController();
|
||||
const versions=options.content?.versions || [{title:'更新作品首页',branch:'design/home',url:'preview.workspace.local'},{title:'整理作品目录',branch:'design/collection',url:'archive.workspace.local'}];let clock,index=0,last=-1,destroyed=false;
|
||||
function show(next,animate=false){index=next;const version=versions[index] || {};buttons.forEach((b,i)=>{b.hidden=!versions[i];b.setAttribute('aria-pressed',String(i===index));});element.querySelectorAll('[data-release-field]').forEach(el=>el.textContent=String(version[el.dataset.releaseField] ?? ''));clock?.destroy();last=-1;clock=loop(element,time=>{const t=still()||!animate?2.5:time,progress=smooth(t/2),ready=smooth((t-1.6)/.9);element.style.setProperty('--build-progress',progress);element.style.setProperty('--preview-ready',ready);const stage=t<.65?0:t<1.6?1:2;steps.forEach((s,i)=>s.dataset.ready=String(t>=(i+1)*.6));if(stage!==last){status.textContent=(options.content?.statuses || ['准备中','构建中','预览就绪'])[stage];last=stage;}},{...options,duration:2.5});}
|
||||
buttons.forEach((b,i)=>b.addEventListener('click',()=>show(i),{signal:events.signal}));show(0,true);
|
||||
return{replay(){if(!destroyed)show(index,true);},destroy(){if(destroyed)return;destroyed=true;clock.destroy();events.abort();clean();for(const property of ['--build-progress','--preview-ready'])element.style.removeProperty(property);steps.forEach(step=>delete step.dataset.ready);}};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.dot-globe {
|
||||
& canvas { position:absolute; inset:0; }
|
||||
& .globe-label { position:absolute; z-index:2; padding:7px 10px; border:1px solid color-mix(in oklab,var(--card-accent) 20%,var(--card-line)); border-radius:24px; background:var(--card-panel); color:var(--_card-accent-ink); box-shadow:0 4px 18px #28254e10; font-size:10px; white-space:nowrap; transform:translate(9px,-50%); }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<article class="card portrait dot-globe" data-card="dot-globe" data-motion="auto" data-arcs="2">
|
||||
<h2 class="card-heading" data-text="title">让每一份连接,<br>跨越山海</h2>
|
||||
|
||||
<div class="card-stage"><canvas class="globe-wash" aria-hidden="true"></canvas><canvas class="globe-points" aria-hidden="true"></canvas><canvas class="globe-arcs" aria-hidden="true"></canvas><span class="globe-label" data-text="label">节点 · 已连接</span></div>
|
||||
</article>
|
||||
@@ -0,0 +1,93 @@
|
||||
import {random, loop, fitCanvas, glProgram, glColor, watchColors, shell, TAU, mixColor, rgb, clamp, mix, smooth} from '../shared/runtime.js';
|
||||
import {LAND_WIDTH, LAND_HEIGHT, LAND_BITS} from '../shared/land-mask.js';
|
||||
export function mount(element, options = {}) {
|
||||
const cleanShell=shell(element,options),previousMode=element.getAttribute('data-mode');
|
||||
if(options.mode!==undefined)element.dataset.mode=options.mode==='mesh'?'mesh':'globe';
|
||||
const isMesh=()=>element.dataset.mode==='mesh';
|
||||
const label=element.querySelector('.globe-label'),labelText=label.textContent;
|
||||
let canvas=element.querySelector('.globe-points');const backdrop=element.querySelector('.globe-wash'),arcCanvas=element.querySelector('.globe-arcs');
|
||||
const wash=backdrop.getContext('2d'),arc=arcCanvas.getContext('2d'),rng=random(301),bits=Uint8Array.from(atob(LAND_BITS),c=>c.charCodeAt(0));
|
||||
const count=matchMedia('(max-width: 650px)').matches?55000:110000,points=[];
|
||||
const normalize=p=>{const l=Math.hypot(...p);return p.map(n=>n/l);};
|
||||
for(let i=0;i<count;i++){
|
||||
const latitude=Math.asin(-1+2*(i+.5)/count),longitude=i*2.3999632297;
|
||||
let p=[Math.cos(latitude)*Math.sin(longitude),Math.sin(latitude),Math.cos(latitude)*Math.cos(longitude)];
|
||||
const v=[rng()-.5,rng()-.5,rng()-.5],dot=v.reduce((s,n,j)=>s+n*p[j],0),t=normalize(v.map((n,j)=>n-dot*p[j]));
|
||||
p=normalize(p.map((n,j)=>n+t[j]*.007));
|
||||
const lon=Math.atan2(p[0],p[2])*180/Math.PI,lat=Math.asin(p[1])*180/Math.PI;
|
||||
const x=Math.min(LAND_WIDTH-1,Math.floor((lon+180)/360*LAND_WIDTH)),y=Math.min(LAND_HEIGHT-1,Math.floor((90-lat)/180*LAND_HEIGHT)),idx=y*LAND_WIDTH+x;
|
||||
if(bits[idx>>3]&(1<<(idx&7)))points.push([...p,1.2+Math.pow(rng(),2)*1.4,.35+Math.pow(rng(),.4)*.55]);
|
||||
}
|
||||
// A second point field shares the renderer, palette and clock with the globe.
|
||||
const globeCount=points.length,content=options.content || {};
|
||||
const defaults=[[-.98,.64,.18],[-.47,.83,-.32],[.02,.66,.38],[.59,.82,-.18],[1.03,.40,.12],[-1.12,.10,-.4],[-.66,.22,.4],[-.18,.09,-.27],[.36,.29,.24],[.86,-.13,-.18],[-.95,-.46,.21],[-.45,-.28,-.35],[.07,-.39,.38],[.61,-.53,-.25],[-.57,-.85,.1],[.16,-.91,-.18],[1.08,-.70,.25]];
|
||||
const nodes=(Array.isArray(content.nodes)?content.nodes.filter(n=>n && typeof n==='object').slice(0,64):defaults.map((position,i)=>({id:String(i),position}))).map((node,i)=>({
|
||||
id:String(node.id ?? i),label:String(node.label ?? ''),position:Array.isArray(node.position)&&node.position.length===3&&node.position.every(Number.isFinite)?node.position.map(n=>clamp(n,-1.5,1.5)):defaults[i%defaults.length]
|
||||
}));
|
||||
const indexes=new Map(nodes.map((node,i)=>[node.id,i]));
|
||||
const resolve=value=>indexes.get(String(value)) ?? (Number.isInteger(value)&&value>=0&&value<nodes.length?value:undefined);
|
||||
const links=Array.isArray(content.links)?content.links.map(link=>[resolve(link?.from),resolve(link?.to)]).filter(([a,b])=>a!==undefined&&b!==undefined&&a!==b).slice(0,160):nodes.flatMap((node,i)=>nodes.map((other,j)=>({j,d:Math.hypot(...node.position.map((n,k)=>n-other.position[k]))})).filter(({j})=>j!==i).sort((a,b)=>a.d-b.d).slice(0,2).filter(({j})=>j>i).map(({j})=>[i,j]));
|
||||
for(let i=0;i<1800;i++)points.push([(rng()-.5)*2.9,(rng()-.5)*2.6,(rng()-.5)*1.4,.7+rng()*1.1,.08+rng()*.22]);
|
||||
for(const node of nodes){for(let i=0;i<100;i++){const radius=Math.pow(rng(),.65)*.17,angle=rng()*TAU,z=(rng()-.5)*radius;points.push([node.position[0]+Math.cos(angle)*radius,node.position[1]+Math.sin(angle)*radius,node.position[2]+z,.8+rng()*1.3,.14+rng()*.48]);}points.push([...node.position,4,.95]);}
|
||||
const vertex=`precision highp float;attribute vec3 a_position;attribute vec2 a_detail;uniform vec2 u_size;uniform float u_time,u_dpr,u_mode;varying float v_alpha,v_t;
|
||||
void main(){float a=-.70+u_time*.069813;vec3 p=vec3(a_position.x*cos(a)+a_position.z*sin(a),a_position.y,-a_position.x*sin(a)+a_position.z*cos(a));
|
||||
float tilt=.12;vec2 xy=mat2(cos(tilt),-sin(tilt),sin(tilt),cos(tilt))*p.xy;float r=u_size.x*.76;
|
||||
vec2 screen=vec2(u_size.x*.80,u_size.y*.68)+vec2(xy.x,-xy.y)*r;
|
||||
v_alpha=a_detail.y*mix(.12,1.,smoothstep(-.1,.35,p.z));if(p.z<0.)v_alpha*=.20+.80*smoothstep(0.,.30,-p.z);
|
||||
float size=.82+.18*(p.z+1.)*.5;
|
||||
if(u_mode>.5){a=.14+sin(u_time*.07)*.15;p=vec3(a_position.x*cos(a)+a_position.z*sin(a),a_position.y,-a_position.x*sin(a)+a_position.z*cos(a));xy=mat2(cos(tilt),-sin(tilt),sin(tilt),cos(tilt))*p.xy;float perspective=1./(1.+p.z*.24);screen=vec2(u_size.x*.53,u_size.y*.55)+vec2(xy.x,-xy.y)*u_size.x*.43*perspective;size=perspective;v_alpha=a_detail.y*(.85+.15*sin(u_time*.5+a_position.x*7.));}
|
||||
gl_Position=vec4(screen.x/u_size.x*2.-1.,1.-screen.y/u_size.y*2.,0.,1.);gl_PointSize=a_detail.x*u_dpr*size;v_t=clamp((xy.x+xy.y+.85)/1.8,0.,1.);}`;
|
||||
const fragment=`precision mediump float;varying float v_alpha,v_t;uniform vec3 u_a,u_b,u_c;${glColor} void main(){float r=length(gl_PointCoord-.5),soft=1.-smoothstep(.32,.5,r);vec3 c=v_t<.58?pigment(u_a,u_b,v_t/.58):pigment(u_b,u_c,(v_t-.58)/.42);gl_FragColor=vec4(c,v_alpha*soft);}`;
|
||||
let gpu=glProgram(canvas,vertex,fragment),ctx;
|
||||
if(gpu){gpu.attribute('a_position',points.flatMap(p=>p.slice(0,3)),3);gpu.attribute('a_detail',points.flatMap(p=>p.slice(3)),2);}
|
||||
else{const n=canvas.cloneNode();canvas.replaceWith(n);canvas=n;ctx=canvas.getContext('2d');}
|
||||
let palette,washKey="",arcColors=[],fallbackTints=[];const requested=Number(options.arcs??element.dataset.arcs??2),arcs=Number.isFinite(requested)?Math.round(clamp(requested,1,3)):2;
|
||||
const rotate=(p,time)=>{const a=-.70+time*TAU/90,x=p[0]*Math.cos(a)+p[2]*Math.sin(a),z=-p[0]*Math.sin(a)+p[2]*Math.cos(a);return [x*Math.cos(.12)+p[1]*Math.sin(.12),-x*Math.sin(.12)+p[1]*Math.cos(.12),z];};
|
||||
const project=(p,w,h)=>[w*.80+p[0]*w*.76,h*.68-p[1]*w*.76];
|
||||
const meshProject=(p,time,w,h)=>{const a=.14+Math.sin(time*.07)*.15,x=p[0]*Math.cos(a)+p[2]*Math.sin(a),z=-p[0]*Math.sin(a)+p[2]*Math.cos(a),f=1/(1+z*.24);return [w*.53+(x*Math.cos(.12)+p[1]*Math.sin(.12))*w*.43*f,h*.55-(-x*Math.sin(.12)+p[1]*Math.cos(.12))*w*.43*f];};
|
||||
const slerp=(a,b,t)=>{const angle=Math.acos(clamp(a.reduce((s,n,i)=>s+n*b[i],0),-1,1)),s=Math.sin(angle);return a.map((n,i)=>(n*Math.sin((1-t)*angle)+b[i]*Math.sin(t*angle))/s);};
|
||||
const draw=(time)=>{
|
||||
if(!palette)return;const {w,h,dpr}=fitCanvas(canvas);fitCanvas(backdrop);fitCanvas(arcCanvas);
|
||||
const mesh=isMesh(),nextWashKey=mesh+":"+canvas.width+":"+canvas.height+":"+JSON.stringify(palette);
|
||||
if(nextWashKey!==washKey){
|
||||
wash.setTransform(dpr,0,0,dpr,0,0);wash.clearRect(0,0,w,h);const cx=w*.80,cy=h*.68,r=w*.76;
|
||||
if(mesh){const g=wash.createRadialGradient(w*.53,h*.55,0,w*.53,h*.55,w*.65);g.addColorStop(0,rgb(palette.b,.10));g.addColorStop(.55,rgb(palette.a,.035));g.addColorStop(1,rgb(palette.a,0));wash.fillStyle=g;wash.fillRect(0,0,w,h);}
|
||||
else{const g=wash.createRadialGradient(cx-r*.25,cy-r*.35,r*.1,cx,cy,r);for(let i=0;i<=16;i++){const t=i/16;g.addColorStop(t*.85,rgb(mixColor(palette.surface,palette.a,t),.01+.03*t));}g.addColorStop(1,rgb(palette.a,.05));wash.fillStyle=g;wash.beginPath();wash.arc(cx,cy,r,0,TAU);wash.fill();wash.strokeStyle=rgb(palette.a,.075);wash.lineWidth=.6;wash.stroke();}
|
||||
washKey=nextWashKey;arcColors=Array.from({length:201},(_,i)=>mixColor(palette.b,palette.a,i/200));
|
||||
if(!gpu)fallbackTints=Array.from({length:129},(_,i)=>{const t=i/128,color=t<.58?mixColor(palette.a,palette.b,t/.58):mixColor(palette.b,palette.c,(t-.58)/.42);return Array.from({length:33},(_,a)=>rgb(color,a/32));});
|
||||
}
|
||||
if(gpu){const {gl}=gpu;gl.useProgram(gpu.program);gl.viewport(0,0,canvas.width,canvas.height);gl.clearColor(0,0,0,0);gl.clear(gl.COLOR_BUFFER_BIT);gl.enable(gl.BLEND);gl.blendFuncSeparate(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA,gl.ONE,gl.ONE_MINUS_SRC_ALPHA);gl.uniform2f(gpu.uniform('u_size'),w,h);gl.uniform1f(gpu.uniform('u_time'),time);gl.uniform1f(gpu.uniform('u_mode'),mesh?1:0);gl.uniform1f(gpu.uniform('u_dpr'),dpr);for(const [n,c]of [['a',palette.a],['b',palette.b],['c',palette.c]])gl.uniform3fv(gpu.uniform('u_'+n),c);gl.drawArrays(gl.POINTS,mesh?globeCount:0,mesh?points.length-globeCount:globeCount);}
|
||||
else{ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,w,h);for(let i=mesh?globeCount:0;i<(mesh?points.length:globeCount);i++){const p=points[i],q=rotate(p,time),[x,y]=mesh?meshProject(p,time,w,h):project(q,w,h),t=clamp((q[0]+q[1]+.85)/1.8);const rim=clamp(-q[2]/.30),back=q[2]<0?.20+.80*rim*rim*(3-2*rim):1,alpha=mesh?p[4]:p[4]*(q[2]>.1?1:.12)*back;ctx.fillStyle=fallbackTints[Math.round(t*128)][Math.round(alpha*32)];ctx.beginPath();ctx.arc(x,y,p[3]*.45,0,TAU);ctx.fill();}}
|
||||
arc.setTransform(dpr,0,0,dpr,0,0);arc.clearRect(0,0,w,h);
|
||||
if(mesh){
|
||||
const positions=nodes.map(n=>meshProject(n.position,time,w,h));
|
||||
for(const [i,[a,b]] of links.entries()){
|
||||
const A=positions[a],B=positions[b],color=mixColor(palette.a,palette.b,(i%7)/6),t=(time*.13+i*.31)%1;
|
||||
arc.strokeStyle=rgb(color,.23);arc.lineWidth=.65;arc.beginPath();arc.moveTo(...A);arc.lineTo(...B);arc.stroke();
|
||||
const x=mix(A[0],B[0],t),y=mix(A[1],B[1],t),g=arc.createRadialGradient(x,y,0,x,y,7);g.addColorStop(0,rgb(color,.65));g.addColorStop(1,rgb(color,0));arc.fillStyle=g;arc.beginPath();arc.arc(x,y,7,0,TAU);arc.fill();
|
||||
}
|
||||
for(const [i,[x,y]] of positions.entries()){const pulse=.5+.5*Math.sin(time*1.5+i),g=arc.createRadialGradient(x,y,0,x,y,12+pulse*4);g.addColorStop(0,rgb(palette.a,.28));g.addColorStop(1,rgb(palette.a,0));arc.fillStyle=g;arc.beginPath();arc.arc(x,y,16,0,TAU);arc.fill();arc.strokeStyle=rgb(palette.a,.4+pulse*.3);arc.lineWidth=.7;arc.beginPath();arc.arc(x,y,3+pulse*1.2,0,TAU);arc.stroke();}
|
||||
if(label){const i=Math.min(8,nodes.length-1);label.hidden=i<0;if(i>=0){label.style.left=positions[i][0]+'px';label.style.top=positions[i][1]+'px';label.style.opacity='1';label.textContent=String(content.label ?? (nodes[i].label || labelText));}}return;
|
||||
}
|
||||
if(label){label.hidden=false;label.textContent=labelText;}
|
||||
for(let k=0;k<arcs;k++){
|
||||
const A=normalize([-.88,.09+k*.17,.42]),B=k===0?normalize([.027,.752,.659]):normalize([.24,.40,.88]);
|
||||
const cycle=(time+k*3.1+3)%13,head=smooth(cycle/3),fade=cycle>9?1-smooth((cycle-9)/3):1;
|
||||
const tail=cycle>9?smooth((cycle-9)/3)*.85:0;let prev=null,headXY=null;
|
||||
for(let i=0;i<=200;i++){
|
||||
const t=i/200;if(t>head||t<tail)continue;
|
||||
let p=slerp(A,B,t),lift=1+Math.sin(t*Math.PI)*(.38+k*.04);p=rotate(p.map(n=>n*lift),time);
|
||||
const xy=project(p,w,h);headXY=xy;
|
||||
if(prev){arc.strokeStyle=rgb(arcColors[i],fade*Math.pow(t,1.2)*.9);arc.lineWidth=.7+t*.65;arc.beginPath();arc.moveTo(...prev);arc.lineTo(...xy);arc.stroke();}prev=xy;
|
||||
}
|
||||
if(headXY&&fade>0){const [x,y]=headXY,g=arc.createRadialGradient(x,y,0,x,y,8);g.addColorStop(0,rgb(palette.a,fade*.7));g.addColorStop(1,rgb(palette.a,0));arc.fillStyle=g;arc.beginPath();arc.arc(x,y,8,0,TAU);arc.fill();arc.fillStyle=rgb(palette.a,fade);arc.beginPath();arc.arc(x,y,1.9,0,TAU);arc.fill();}
|
||||
const [x,y]=project(rotate(B,time),w,h),pulse=clamp((cycle-3)/1.15);
|
||||
if(head>.99){arc.strokeStyle=rgb(palette.a,fade*.85);arc.lineWidth=1;arc.beginPath();arc.arc(x,y,3.5,0,TAU);arc.stroke();if(pulse<1){arc.strokeStyle=rgb(palette.a,(1-pulse)*.5);arc.beginPath();arc.arc(x,y,4+pulse*14,0,TAU);arc.stroke();}}
|
||||
if(k===0&&label){label.style.left=x+'px';label.style.top=y+'px';label.style.opacity=String(smooth((head-.85)/.15)*fade*smooth((x-10)/20)*smooth((w-100-x)/20)*smooth((y-20)/20)*smooth((h-20-y)/20));}
|
||||
}
|
||||
|
||||
};
|
||||
const clock=loop(element,draw,{...options,continuous:true});const off=watchColors(element,p=>{palette=p;clock.invalidate();});const ro=new ResizeObserver(()=>clock.invalidate());ro.observe(element.querySelector('.card-stage'));
|
||||
const modes=new MutationObserver(()=>clock.invalidate());modes.observe(element,{attributes:true,attributeFilter:['data-mode']});
|
||||
return {destroy(){clock.destroy();off();ro.disconnect();modes.disconnect();gpu?.destroy();cleanShell();if(options.mode!==undefined){if(previousMode===null)element.removeAttribute('data-mode');else element.setAttribute('data-mode',previousMode);}const label=element.querySelector('.globe-label');label.removeAttribute('style');label.hidden=false;}};
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
.file-mosaic {
|
||||
height:440px; aspect-ratio:auto; --_card-stage-track-min:min-content;
|
||||
--file-line:color-mix(in oklab,var(--card-ink) 12%,var(--card-surface));
|
||||
--file-ink:color-mix(in oklab,var(--card-ink) 80%,var(--card-surface));
|
||||
--file-label:color-mix(in oklab,var(--card-ink) 66%,var(--card-surface));
|
||||
& .card-stage { display:grid; grid-template-rows:auto minmax(180px,1fr); mask-image:linear-gradient(#000 90%,transparent 100%); }
|
||||
& .file-collection { position:relative; z-index:2; display:flex; flex-wrap:wrap; align-items:baseline; justify-content:space-between; gap:10px; padding:6px 24px 22px; min-width:0; font-size:12px; & > span { min-width:0; overflow-wrap:anywhere; } & > span:last-child { color:var(--card-muted);font-size:10px; } & i { color:var(--_card-accent-ink);font-style:normal; } }
|
||||
& .file-viewport { position:relative; overflow:clip; min-height:180px; }
|
||||
& .file-grid { position:absolute; top:0; left:24px; width:588px; display:grid; grid-template-columns:repeat(5,108px); grid-auto-rows:106px; gap:12px; }
|
||||
& .file-tile { display:flex; flex-direction:column; justify-content:center; align-items:center; gap:9px; border:1px solid var(--file-line); border-radius:calc(var(--_card-radius)*.8); background:var(--card-surface); color:var(--file-label); font:9px/12px ui-monospace,monospace; }
|
||||
& .file-tile > span { max-width:calc(100% - 18px); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
& .file-empty { grid-column:1 / -1; margin:25px 0; color:var(--card-muted); font-size:12px; }
|
||||
& .file-art { display:flex; align-items:center; justify-content:center; width:78px; height:44px; flex:none; color:var(--file-ink); }
|
||||
& .file-art svg { width:31px; height:31px; fill:none; stroke:currentColor; stroke-width:1.35; stroke-linecap:round; stroke-linejoin:round; }
|
||||
& .file-thumbnail { display:flex; height:38px; width:64px; border-radius:2px; overflow:hidden;
|
||||
& i { flex:1; background:var(--card-accent); }
|
||||
& i:nth-child(2) { background:var(--_card-accent-2); }
|
||||
& i:nth-child(3) { background:var(--_card-accent-3); }
|
||||
& i:nth-child(4) { background:var(--file-ink); }
|
||||
}
|
||||
& .file-wave { display:flex; height:35px; gap:3px; align-items:center; & i { width:2px; height:var(--h);border-radius:1px;background:currentColor; } }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<article class="card file-mosaic" data-card="file-mosaic" data-motion="auto">
|
||||
<h2 class="card-heading" data-text="title">每一份素材,<br>都有合适的位置</h2>
|
||||
|
||||
<div class="card-stage"><div class="file-collection"><span data-text="collection">品牌素材</span><span><span data-text="status">所有文件已同步</span> <i>✓</i></span></div><div class="file-viewport"><div class="file-grid" aria-hidden="true"></div></div></div>
|
||||
</article>
|
||||
@@ -0,0 +1,20 @@
|
||||
import {shell} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,options);
|
||||
const grid=element.querySelector('.file-grid');
|
||||
const icons={doc:'<path d="M7 3h13l6 6v21H7Z M20 3v7h6 M12 16h9 M12 21h9"/>',image:'<rect x="3" y="5" width="28" height="24" rx="3"/><circle cx="12" cy="13" r="3"/><path d="m5 26 8-8 5 4 5-7 7 9"/>',video:'<rect x="3" y="7" width="20" height="21" rx="3"/><path d="m23 13 8-4v17l-8-4"/>',code:'<path d="m11 10-8 7 8 7 M23 10l8 7-8 7 M19 5l-5 24"/>'};
|
||||
const fallback=[['image','cover.png'],['color','palette.fig'],['doc','brief.pdf'],['video','intro.mp4'],['code','tokens.json'],['audio','theme.wav'],['image','texture.jpg'],['doc','notes.md'],['image','poster.png'],['video','reel.mov'],['doc','license.txt'],['code','theme.css'],['image','scene.webp'],['audio','voice.wav'],['doc','guide.pdf']].map(([type,name])=>({type,name}));
|
||||
const files=Array.isArray(options.content?.files)?options.content.files:fallback;
|
||||
grid.replaceChildren();
|
||||
for(const file of files){
|
||||
const type=file.type || 'doc',tile=document.createElement('div'),art=document.createElement('div'),label=document.createElement('span');
|
||||
tile.className='file-tile';art.className='file-art';
|
||||
art.innerHTML=type==='color'?'<div class="file-thumbnail"><i></i><i></i><i></i><i></i></div>':type==='audio'?'<div class="file-wave">'+[12,23,32,17,27,35,22,13,24].map(h=>`<i style="--h:${h}px"></i>`).join('')+'</div>':`<svg viewBox="0 0 34 34">${icons[type] || icons.doc}</svg>`;
|
||||
label.textContent=String(file.name ?? '');label.title=label.textContent;tile.append(art,label);grid.append(tile);
|
||||
}
|
||||
if(!files.length){const empty=document.createElement('p');empty.className='file-empty';empty.textContent=options.content?.emptyText ?? '还没有文件';grid.append(empty);}
|
||||
|
||||
element.querySelector('.file-collection i').hidden=!files.length;
|
||||
if(!files.length && options.content?.status===undefined)element.querySelector('[data-text="status"]').textContent='暂无文件';
|
||||
return{destroy(){clean();grid.replaceChildren();}};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
.flow-object {
|
||||
& .card-stage { overflow: visible; perspective: 1100px; }
|
||||
& .flow-aura { position:absolute; inset:0 -10% -10%; background:radial-gradient(ellipse at 70% 66% in oklab,color-mix(in oklab,var(--card-accent) 17%,transparent),transparent 64%),radial-gradient(ellipse at 24% 82% in oklab,color-mix(in oklab,var(--_card-accent-3) 14%,transparent),transparent 59%); }
|
||||
& .physical-card { position: absolute; top:21%; left:17.5%; width:65%; aspect-ratio: 685/1078; overflow: hidden; overflow:clip; border-radius: calc(var(--_card-radius) * 1.9); background: linear-gradient(150deg in oklab,var(--_card-accent-2),var(--card-accent),var(--_card-accent-3)); box-shadow: 0 5px 12px -4px #35164536,0 26px 54px -17px color-mix(in oklab,var(--_card-accent-2) 35%,transparent),inset 0 0 0 1px #ffffff50; transform-style: preserve-3d; }
|
||||
& .flow-surface { position: absolute; inset: 0; }
|
||||
& .card-edition { position: absolute; top: 7%; left: 10%; font: 9px ui-monospace, monospace; letter-spacing: .18em; color: #fff9; }
|
||||
& .card-white { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||
& .physical-wordmark { position: absolute; right: 10%; bottom: 6%; font-size: 31px; color: #34314c; font-weight: 650; letter-spacing: -.08em; & span { font-size: 9px; position: absolute; right: -8px; top: 5px; letter-spacing: 0; } }
|
||||
& .physical-number { position: absolute; bottom: 8%; left: 10%; font: 9px ui-monospace, monospace; color: #97929d; letter-spacing: .14em; }
|
||||
& .token-engraving { position:absolute; inset:17% 10% 27%; color:#fff; text-shadow:0 1px 1px #17214460,0 -1px 0 #ffffff30; font:9px ui-monospace,monospace; }
|
||||
& .token-status { display:flex; align-items:center; gap:7px; font-size:8px; letter-spacing:.16em; }
|
||||
& .token-led { flex:none; width:5px; height:5px; border-radius:50%; background:#d4ffec; box-shadow:0 0 3px 1px #e4fffccc,0 0 13px #b3ffdf70; opacity:var(--token-pulse,.8); }
|
||||
& .token-hash-label { display:block; margin-top:20%; font-size:7px; opacity:.7; letter-spacing:.24em; }
|
||||
& .token-hash { display:block; margin-top:7px; font-size:clamp(13px,1.6vw,20px); font-weight:500; letter-spacing:.08em; overflow-wrap:anywhere; }
|
||||
& .token-specs { display:grid; gap:9px; margin-top:18%; padding-top:12px; border-top:1px solid #ffffff38; }
|
||||
& .token-specs > div { display:flex; flex-wrap:wrap; justify-content:space-between; gap:4px; }
|
||||
& .token-specs span { opacity:.66; font-size:7px; letter-spacing:.12em; }
|
||||
& .token-specs b { font-weight:400; font-size:8px; letter-spacing:.04em; }
|
||||
& .token-registration { display:flex; gap:4px; margin-top:14%; height:13px; opacity:.45; }
|
||||
& .token-registration i { width:1px; height:100%; background:currentColor; }
|
||||
& .token-registration i:nth-child(3n) { height:55%; }
|
||||
&[data-variant="card"] .token-engraving { display:none; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<article class="card portrait flow-object" data-card="flow-object" data-motion="auto">
|
||||
<h2 class="card-heading" data-text="title">让数字身份,<br>拥有可以触碰的形态</h2>
|
||||
|
||||
<div class="card-stage">
|
||||
<div class="flow-aura" aria-hidden="true"></div>
|
||||
<div class="physical-card">
|
||||
<canvas class="flow-surface" aria-hidden="true"></canvas>
|
||||
<span class="card-edition" data-text="edition">IDENTITY / 01</span>
|
||||
<div class="token-engraving">
|
||||
<div class="token-status"><i class="token-led" aria-hidden="true"></i><span data-text="status">VERIFIED</span></div>
|
||||
<span class="token-hash-label" data-text="hashLabel">SIGNATURE</span>
|
||||
<strong class="token-hash" data-text="hash">7F2A:9C04</strong>
|
||||
<div class="token-specs"><div><span data-text="specs.0.label">PROTOCOL</span><b data-text="specs.0.value">OPEN / 01</b></div><div><span data-text="specs.1.label">SCOPE</span><b data-text="specs.1.value">WORKSPACE</b></div><div><span data-text="specs.2.label">REVISION</span><b data-text="specs.2.value">2026.10</b></div></div>
|
||||
<div class="token-registration" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div>
|
||||
</div>
|
||||
<svg class="card-white" viewBox="0 0 300 470" preserveAspectRatio="none" aria-hidden="true"><path d="M0 344Q160 386 300 324V470H0Z" fill="white"/></svg>
|
||||
<span class="physical-wordmark" data-text="brand">identity</span>
|
||||
<span class="physical-number" data-text="identifier">ID / 0842</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,27 @@
|
||||
import {loop, watchColors, shell, mix, clamp, staticMotion} from '../shared/runtime.js';
|
||||
import {coatedSurface} from './material.js';
|
||||
|
||||
export function mount(element, options = {}) {
|
||||
const variant=options.variant ?? element.dataset.variant ?? 'token';
|
||||
const cleanShell=shell(element,{...options,variant:variant==='card'?'card':'token'});
|
||||
const surface=coatedSurface(element.querySelector('.flow-surface'));
|
||||
const card=element.querySelector('.physical-card');
|
||||
let palette,x=0,y=0,tx=0,ty=0;
|
||||
const still=staticMotion(element,options);
|
||||
const draw=(time,dt)=>{
|
||||
if(still())x=y=tx=ty=0;
|
||||
if(dt || still()){
|
||||
const ease=1-Math.exp(-dt*7);x=mix(x,tx,ease);y=mix(y,ty,ease);
|
||||
card.style.transform=`rotateX(${-y*5}deg) rotateY(${x*7}deg)`;
|
||||
}
|
||||
surface.draw(time,palette,[x,y]);
|
||||
element.style.setProperty('--token-pulse',still()?.8:.65+.35*(.5+.5*Math.sin(time*2.2)));
|
||||
};
|
||||
const clock=loop(element,draw,{...options,settled:()=>Math.abs(x-tx)+Math.abs(y-ty)<.001});
|
||||
const off=watchColors(element,p=>{palette=p;clock.invalidate();});
|
||||
const ro=new ResizeObserver(()=>{surface.resize();clock.invalidate();});ro.observe(card);
|
||||
const move=e=>{if(e.pointerType==='touch' || still())return;const r=element.getBoundingClientRect();tx=clamp((e.clientX-r.left)/r.width*2-1,-1,1);ty=clamp((e.clientY-r.top)/r.height*2-1,-1,1);};
|
||||
const leave=()=>{tx=ty=0;};
|
||||
element.addEventListener('pointermove',move);element.addEventListener('pointerleave',leave);element.addEventListener('pointercancel',leave);
|
||||
return {destroy(){clock.destroy();off();ro.disconnect();surface.destroy();cleanShell();element.removeEventListener('pointermove',move);element.removeEventListener('pointerleave',leave);element.removeEventListener('pointercancel',leave);card.style.removeProperty('transform');element.style.removeProperty('--token-pulse');}};
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import {glProgram, glNoise, glColor} from '../shared/runtime.js';
|
||||
|
||||
// One coated surface, not a stack of ribbons. Paint stays still; light travels.
|
||||
const vertex = `attribute vec2 a_position;varying vec2 v_uv;
|
||||
void main(){v_uv=a_position*.5+.5;gl_Position=vec4(a_position,0.,1.);}`;
|
||||
const fragment = `precision highp float;
|
||||
varying vec2 v_uv;uniform vec2 u_resolution,u_pointer;
|
||||
uniform float u_time;uniform vec3 u_a,u_b,u_c;
|
||||
${glNoise}
|
||||
${glColor}
|
||||
float bell(float d,float w){return exp(-d*d/(w*w));}
|
||||
void main(){
|
||||
// Fit the field to the short side: a wider host reveals more of the material.
|
||||
vec2 uv=vec2(v_uv.x,1.-v_uv.y);
|
||||
vec2 p=(uv-.5)*u_resolution/min(u_resolution.x,u_resolution.y);
|
||||
float grain=hash(gl_FragCoord.xy)-.5;
|
||||
float dye=p.y+.42*p.x+.07*sin(p.x*2.1);
|
||||
vec3 color=mix(lab(u_b),lab(u_a),smoothstep(-.48,.30,dye));
|
||||
// Warm pigment is a broad off-centre pool, not another repeated band.
|
||||
float warm=smoothstep(-.01,.43,p.y+.18*p.x-.12*sin(p.x*2.));
|
||||
color=mix(color,lab(u_c),warm);
|
||||
color=mix(color,lab(vec3(1.)),.18+.09*bell(dye+.52,.40));
|
||||
float drift=.055*sin(u_time*.32)+u_pointer.x*.065-u_pointer.y*.035;
|
||||
float d=p.y+.43*p.x-.065*p.x*p.x+.10-drift;
|
||||
// One narrow glint nested inside an asymmetric, much wider bloom.
|
||||
float width=max(1.6/min(u_resolution.x,u_resolution.y),.005);
|
||||
float glint=bell(d,width*1.5);
|
||||
float shoulder=bell(d-.012,.035)*.20;
|
||||
float bloom=bell(d-.055,.14)*.29;
|
||||
float envelope=.66+.34*bell(p.x-.10-.22*sin(u_time*.19),.85);
|
||||
float brushed=simplex(vec2(p.x*.9,d*220.));
|
||||
float echo=bell(d+.038+.008*sin(p.x*4.),.014)*.07;
|
||||
float grazing=(glint*.29+shoulder+bloom+echo)*envelope;
|
||||
color=mix(color,lab(vec3(1.)),clamp(grazing,0.,.78));
|
||||
// A broad reflected softbox moves independently of the pigment boundary.
|
||||
float softbox=bell(p.x*.70-p.y*.22+.27*sin(u_time*.25)+u_pointer.x*.14,.52);
|
||||
color.x+=softbox*.035;
|
||||
color.x+=brushed*.009*bell(d,.14);
|
||||
vec3 outcolor=srgb(color)+grain*.020;
|
||||
gl_FragColor=vec4(outcolor,1.);
|
||||
}`;
|
||||
|
||||
export function coatedSurface(canvas) {
|
||||
let gpu, uniforms, lastFrame=null, disposed=false;
|
||||
// Use the untransformed plane, so tilting the host cannot stretch its grain.
|
||||
const resize=()=>{
|
||||
const dpr=Math.min(devicePixelRatio || 1,2.5);
|
||||
const w=Math.max(1,Math.round(canvas.clientWidth*dpr)),h=Math.max(1,Math.round(canvas.clientHeight*dpr));
|
||||
if(canvas.width!==w || canvas.height!==h){canvas.width=w;canvas.height=h;}
|
||||
};
|
||||
const initialize=()=>{
|
||||
gpu=glProgram(canvas,vertex,fragment);
|
||||
canvas.dataset.renderer=gpu?'webgl':'css';
|
||||
if(!gpu)return;
|
||||
gpu.attribute('a_position',[-1,-1,1,-1,-1,1,-1,1,1,-1,1,1],2);
|
||||
uniforms=Object.fromEntries(['resolution','pointer','time','a','b','c'].map(n=>[n,gpu.uniform('u_'+n)]));
|
||||
resize();
|
||||
};
|
||||
const lost=e=>{if(disposed)return;e.preventDefault();gpu=null;canvas.dataset.renderer='css';};
|
||||
const restored=()=>{if(!disposed){initialize();if(lastFrame)draw(...lastFrame);}};
|
||||
canvas.addEventListener('webglcontextlost',lost);
|
||||
canvas.addEventListener('webglcontextrestored',restored);
|
||||
initialize();
|
||||
function draw(time,palette,pointer=[0,0]){
|
||||
lastFrame=[time,palette,pointer];
|
||||
if(!gpu || !palette || disposed)return;
|
||||
const {gl}=gpu;gl.useProgram(gpu.program);gl.viewport(0,0,canvas.width,canvas.height);
|
||||
gl.uniform2f(uniforms.resolution,canvas.width,canvas.height);
|
||||
gl.uniform2f(uniforms.pointer,...pointer);gl.uniform1f(uniforms.time,time);
|
||||
for(const n of ['a','b','c'])gl.uniform3fv(uniforms[n],palette[n]);
|
||||
gl.drawArrays(gl.TRIANGLES,0,6);
|
||||
}
|
||||
return {
|
||||
resize,draw,
|
||||
destroy(){if(disposed)return;disposed=true;canvas.removeEventListener('webglcontextlost',lost);canvas.removeEventListener('webglcontextrestored',restored);gpu?.destroy();gpu=null;lastFrame=null;canvas.width=canvas.height=1;delete canvas.dataset.renderer;}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export function createCatalog(query) {
|
||||
const descriptions={
|
||||
all:'',
|
||||
visual:'用光场、曲面和影像建立品牌印象。先选视觉语言,再替换品牌与内容。',
|
||||
product:'把真实产品的一小段体验放大呈现:任务、文件、运行输出与内容就绪。',
|
||||
data:'表达数量、范围和连接关系。数据可以替换,图形应保持清楚的阅读顺序。',
|
||||
action:'引导一个清楚的下一步:快捷操作、资源入口或推广行动。'
|
||||
};
|
||||
const items=[...document.querySelectorAll('main [data-family]')],buttons=[...document.querySelectorAll('[data-family-filter]')],summary=document.querySelector('.category-summary');
|
||||
const groups=[...document.querySelectorAll('.signature-grid,.exploration-grid,.utility-grid,.peer-grid')];
|
||||
const controller=new AbortController();
|
||||
buttons.forEach(button=>{const family=button.dataset.familyFilter;button.querySelector('span').textContent=String(items.filter(item=>family==='all'||item.dataset.family===family).length);});
|
||||
function select(family){
|
||||
document.querySelector('main').dataset.category=family;
|
||||
items.forEach(item=>{item.hidden=family!=='all'&&item.dataset.family!==family;});
|
||||
groups.forEach(group=>{group.hidden=![...group.querySelectorAll('[data-family]')].some(item=>!item.hidden);});
|
||||
document.querySelector('.peer-intro').hidden=document.querySelector('.peer-grid').hidden;
|
||||
document.querySelector('.showcase-intro').hidden=document.querySelector('.showcase-section').hidden;
|
||||
buttons.forEach(button=>button.setAttribute('aria-pressed',String(button.dataset.familyFilter===family)));
|
||||
summary.textContent=descriptions[family];summary.hidden=!summary.textContent;
|
||||
}
|
||||
buttons.forEach(button=>button.addEventListener('click',()=>select(button.dataset.familyFilter),{signal:controller.signal}));
|
||||
const requested=query.get('category');select(!query.has('focus')&&Object.hasOwn(descriptions,requested)?requested:'all');
|
||||
return {destroy(){controller.abort();}};
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Gallery examples only. Components do not depend on these scenarios.
|
||||
const contentScenes = {
|
||||
studio: {
|
||||
'portrait-story':{brand:'morrow / journal',quote:'让工具退后一步,\n把空间留给真正的创作。',author:'创作手记',role:'关于工具与创作'},
|
||||
'capture-notes':{title:'把每一次讨论,\n留在同一份记录里。'},
|
||||
'answer-lens':{title:'找到答案,\n也看见它从哪里来。'},
|
||||
'responsive-preview':{title:'一份内容,\n在不同屏幕上自然展开。'},
|
||||
'deploy-preview':{title:'每一次改动,\n都能先预览,再分享。'},
|
||||
'particle-ring': {title:'把需求说清楚,\n找到合适的选择'},
|
||||
'flow-object': {title:'让数字身份,\n拥有可以触碰的形态',brand:'identity',identifier:'ID / 0842',hash:'7F2A:9C04',status:'VERIFIED'},
|
||||
'dot-globe': {title:'连接不同地区,\n保持同一步调',label:'巴黎 · 已连接'},
|
||||
'line-ribbons': {title:'各有方向,\n也能一起前进',description:'让不同的想法,在这里汇合。'},
|
||||
'ui-slice': {title:'从计划到完成,进度始终清楚',description:'把任务、负责人和完成状态放在一起。'},
|
||||
terminal: {title:'把重复的步骤,交给工具',description:'一次运行,完成构建、检查与发布。'},
|
||||
metric: {title:'用数据看见每一次进展',description:'过去 30 天 · 工作流运行次数'},
|
||||
'beam-graph': {title:'连接工具,也连接工作',description:'从输入到结果,让信息沿着同一条路径流动。'},
|
||||
keycaps: {title:'常用操作,一步直达',description:'少一次寻找,多一点专注。'},
|
||||
'promo-prism': {title:'一起开始。',description:'找到需要的资源,与团队一起推进下一项计划。',action:'查看资源',href:'#explorations'},
|
||||
'promo-facets': {title:'从这里出发。',description:'把工具和内容准备好,开始你的下一件作品。',action:'浏览作品',href:'#showcase'},
|
||||
'vector-space': {title:'不只匹配关键词,\n也理解内容的关联',query:'自然的灵感',result:'4 个相关结果',mode:'语义搜索'},
|
||||
'file-mosaic': {title:'不同类型的文件,\n放在同一个空间',collection:'品牌素材',status:'所有文件已同步'},
|
||||
'stream-window': {title:'内容逐步就绪,\n先看到重要的部分'},
|
||||
morrow:{description:'用有辨识度的视觉,介绍你的产品。',action:'查看品牌故事'},
|
||||
duotone:{description:'让人物和色彩,传达品牌的个性。',action:'查看视觉方向'},
|
||||
serein:{description:'用真实的纹理,为内容留出呼吸。',action:'查看影像表达'},
|
||||
folded:{description:'用清楚的形体,说明产品的结构。',action:'查看产品表达'},
|
||||
cubiq:{description:'把抽象的概念,变成可以理解的形状。',action:'查看空间表达'},
|
||||
vellum:{description:'保留轻盈的层次,让文字更容易被看见。',action:'查看材质表达'}
|
||||
},
|
||||
developer: {
|
||||
'portrait-story':{brand:'fieldnotes / engineering',quote:'把复杂留给系统,\n把清楚留给使用它的人。',author:'构建笔记',role:'关于工程与体验'},
|
||||
'capture-notes':{title:'让技术讨论,\n成为可以查阅的上下文。',collection:'工程记录',tab:'所有讨论',scope:'我的项目',records:[{title:'查询接口设计评审',team:'后端'},{title:'状态与加载边界',team:'前端'},{title:'预览环境接入',team:'工程'},{title:'移动端性能复盘',team:'体验'},{title:'下一版本的验收范围',team:'产品'}]},
|
||||
'answer-lens':{title:'从项目资料里,\n找到有依据的答案。',eyebrow:'工程知识库',dashboard:'项目运行概览',question:'接下来应该先优化哪里?',sources:[{label:'运行日志',answer:'高峰时段集中出现重复查询,先检查缓存与请求合并。'},{label:'性能记录',answer:'首屏最重的是图片资源,可以优先调整加载顺序与尺寸。'},{label:'反馈工单',answer:'更多用户需要看懂失败原因,优先补齐可恢复的错误反馈。'}]},
|
||||
'responsive-preview':{title:'布局会改变,\n内容的主次依然清楚。',description:'同一份资源,适配不同的显示空间。',labels:['资源原图','桌面视图','移动视图']},
|
||||
'deploy-preview':{title:'改动可以预览,\n讨论就有了共同的页面。',project:'workspace / application',versions:[{title:'完善查询接口',branch:'feat/query',url:'query.preview.local'},{title:'补齐登录状态',branch:'feat/session',url:'session.preview.local'}],resultNote:'在进入正式环境之前,一起确认细节。'},
|
||||
'particle-ring': {title:'让自然语言,\n成为产品入口',prompt:'想找适合周末出行的衣服,\n有什么推荐?',reply:'根据你的需求,\n这两件适合轻装出发。',action:'查看推荐结果'},
|
||||
'flow-object': {title:'把访问权限,\n放进一枚实体通证',brand:'access',identifier:'ID / 1024',edition:'WORKSPACE / 02',hash:'A104:7E2D',status:'AUTHORIZED',specs:[{label:'PROTOCOL',value:'OPEN / 02'},{label:'SCOPE',value:'AGENT'},{label:'REVISION',value:'2026.10'}]},
|
||||
'dot-globe': {title:'从本地出发,\n服务不同地区',label:'巴黎 · 节点在线'},
|
||||
'line-ribbons': {title:'独立构建,\n顺畅协作',description:'让数据、服务和团队保持连接。'},
|
||||
'ui-slice': {title:'让每次交付,都有清楚的进度',description:'把发布前的检查,放进同一份清单。',board:'本次迭代',columns:['待处理','进行中'],backlog:['编写接口文档','补充测试用例','集成登录服务','验证预览环境'],listTitle:'版本发布检查',tasks:[{title:'完成代码评审',owner:'李'},{title:'通过集成测试',owner:'周'},{title:'确认发布窗口',owner:'陈'}],progressLabel:'发布检查'},
|
||||
terminal: {title:'一条命令,完成环境检查',description:'运行步骤和输出结果,按顺序呈现。',path:'~/workspace',shell:'shell',lines:[{text:'› mono check --target=preview'},{text:'读取项目配置',detail:'mono.config'},{text:'✓ 依赖检查完成',tone:'highlight',detail:'1.2s'},{text:'✓ 连接预览环境'},{text:'✓ 所有检查通过',tone:'result'},{text:'preview.mono.local',tone:'address'}]},
|
||||
metric: {title:'每一次调用,都有据可查',description:'过去 24 小时 · API 请求量',value:86532,delta:'↗ 12.4%',latest:'最近一小时 4,208',series:[640,760,580,820,910,880,1240,1380,1120,1800,1680,2040,1950,2160,2480,2320,2780,3100,2950,3450,3280,3780,3900,4208]},
|
||||
'beam-graph': {title:'把服务串起来,让数据流起来',description:'不同来源的事件,在同一条工作流中处理。',nodes:['事件','数据','队列','存储','通知'],hub:'API'},
|
||||
keycaps: {title:'用快捷键,直接找到下一步',description:'打开命令面板,搜索文件、操作或文档。',modifier:'⌘',modifierName:'command',key:'K',keyName:'command menu',placeholder:'搜索文件、命令或文档…'},
|
||||
'promo-prism': {title:'快速接入。',description:'从清楚的接口和示例开始,把能力接进现有产品。',action:'查看组件示例',href:'#explorations'},
|
||||
'promo-facets': {title:'开始构建。',description:'复用已有的视觉与交互,把时间留给产品本身。',action:'浏览视觉模板',href:'#showcase'},
|
||||
'vector-space': {title:'描述你要找什么,\n找到相关的答案',query:'如何部署预览环境',result:'4 段相关内容',mode:'知识检索'},
|
||||
'file-mosaic': {title:'代码、文档和资源,\n在同一处管理',collection:'项目资源',status:'已同步到工作区',files:[{type:'code',name:'client.ts'},{type:'doc',name:'API.md'},{type:'color',name:'tokens.fig'},{type:'image',name:'diagram.png'},{type:'code',name:'schema.json'},{type:'doc',name:'README.md'},{type:'code',name:'worker.ts'},{type:'doc',name:'changelog.md'},{type:'image',name:'preview.webp'},{type:'video',name:'walkthrough.mp4'},{type:'doc',name:'LICENSE'},{type:'code',name:'theme.css'},{type:'image',name:'cover.png'},{type:'audio',name:'intro.wav'},{type:'doc',name:'guide.pdf'}]},
|
||||
'stream-window': {title:'按需加载内容,\n让等待更短一些',path:'workspace / overview',eyebrow:'项目工作区',heading:'从这里,继续构建。',summary:'打开最近的项目,查看运行状态与交付进度。',visualLabel:'服务拓扑',listTitle:'最近的项目',items:['开发者文档','组件工作台','预览环境'],pendingStatus:'正在加载工作区',completeStatus:'工作区已就绪'},
|
||||
morrow:{brand:'signal',description:'从一个想法开始,把能力接进你的产品。',action:'查看集成方式',href:'#explorations'},
|
||||
duotone:{brand:'duotone',description:'把复杂的信息,组织成清楚的表达。',action:'查看内容组件',href:'#explorations'},
|
||||
serein:{brand:'relay',description:'让数据在服务之间,自然地流动。',action:'查看连接组件',href:'#explorations'},
|
||||
folded:{brand:'layer',description:'从稳定的结构,构建新的功能。',action:'查看界面组件',href:'#explorations'},
|
||||
cubiq:{brand:'blocks',description:'把独立能力,组合成完整的体验。',action:'查看组合方式',href:'#explorations'},
|
||||
vellum:{brand:'canvas',description:'保留空间,让产品内容成为主角。',action:'查看视觉模板',href:'#showcase'}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
// The editor owns its committed palette; incomplete HEX input is only a draft.
|
||||
export function createPaletteEditor({query, onChange}) {
|
||||
const root=document.querySelector('.palette-editor'),modeControl=root.querySelector('#palette-mode');
|
||||
const slots=[...root.querySelectorAll('[data-color-slot]')],feedback=root.querySelector('.palette-feedback');
|
||||
const defaults={mode:'single',colors:['#5865c7','#9aa5e8','#d6c9b7']},key='oil.cards.palette.v1';
|
||||
const modes={single:1,duo:2,trio:3};
|
||||
const hex=value=>{
|
||||
if(typeof value!=='string')return null;
|
||||
const raw=value.trim().replace(/^#/,'');
|
||||
if(/^[a-f\d]{3}$/i.test(raw))return '#'+[...raw].map(c=>c+c).join('').toLowerCase();
|
||||
return /^[a-f\d]{6}$/i.test(raw)?'#'+raw.toLowerCase():null;
|
||||
};
|
||||
let state={mode:defaults.mode,colors:[...defaults.colors]},frame=0;
|
||||
try {const saved=JSON.parse(localStorage.getItem(key));if(Object.hasOwn(modes,saved?.mode)&&Array.isArray(saved.colors)&&saved.colors.length===3&&saved.colors.every(hex))state={mode:saved.mode,colors:saved.colors.map(hex)};}catch{}
|
||||
// Existing screenshot URLs remain valid; explicit URL colors override saved input.
|
||||
const legacy={dawn:['#7a5cff','#ff4f8b','#ff9a4a'],sage:['#4c9a94','#81bca2','#c7d5aa'],mono:['#44454c','#929399','#dedfe2'],clay:['#ac715d','#cf9d85','#e6caae']};
|
||||
const requestedPreset=query.get('preset')||query.get('state');
|
||||
if(Object.hasOwn(legacy,requestedPreset))state={mode:'trio',colors:[...legacy[requestedPreset]]};
|
||||
const requested=query.get('colors')?.split(',');
|
||||
if(requested?.length>=1&&requested.length<=3&&requested.every(hex))state={mode:['single','duo','trio'][requested.length-1],colors:[...requested.map(hex),...defaults.colors.slice(requested.length)]};
|
||||
const controller=new AbortController(),listen=(el,event,fn)=>el.addEventListener(event,fn,{signal:controller.signal});
|
||||
const save=()=>{try{localStorage.setItem(key,JSON.stringify(state));}catch{}};
|
||||
const render=()=>{
|
||||
modeControl.value=state.mode;
|
||||
slots.forEach((slot,i)=>{slot.hidden=i>=modes[state.mode];slot.querySelector('input[type=color]').value=state.colors[i];const text=slot.querySelector('.palette-hex');text.value=state.colors[i].toUpperCase();text.removeAttribute('aria-invalid');});
|
||||
feedback.textContent='';
|
||||
};
|
||||
const refresh=()=>{save();if(!frame)frame=requestAnimationFrame(()=>{frame=0;onChange();});};
|
||||
const showErrors=()=>{
|
||||
const invalid=slots.some((slot,i)=>i<modes[state.mode]&&slot.querySelector('.palette-hex').getAttribute('aria-invalid')==='true');
|
||||
feedback.textContent=invalid?'请输入 3 或 6 位 HEX 色值。':'';
|
||||
};
|
||||
slots.forEach((slot,i)=>{
|
||||
const color=slot.querySelector('input[type=color]'),text=slot.querySelector('.palette-hex');
|
||||
listen(color,'input',()=>{state.colors[i]=color.value;text.value=color.value.toUpperCase();text.removeAttribute('aria-invalid');showErrors();refresh();});
|
||||
listen(text,'input',()=>{const value=hex(text.value);text.setAttribute('aria-invalid',String(!value));if(value){state.colors[i]=value;color.value=value;refresh();}showErrors();});
|
||||
listen(text,'blur',()=>{const value=hex(text.value);if(value)text.value=value.toUpperCase();});
|
||||
listen(text,'keydown',e=>{if(e.key==='Escape'){text.value=state.colors[i].toUpperCase();text.removeAttribute('aria-invalid');showErrors();}if(e.key==='Enter')text.blur();});
|
||||
});
|
||||
listen(modeControl,'change',()=>{state.mode=modeControl.value;render();refresh();});
|
||||
listen(root.querySelector('.palette-reset'),'click',()=>{state={mode:defaults.mode,colors:[...defaults.colors]};render();refresh();});
|
||||
render();
|
||||
return {
|
||||
get colors(){const [a,b,c]=state.colors;return state.mode==='single'?[a,a,a]:state.mode==='duo'?[a,b,b]:[a,b,c];},
|
||||
destroy(){controller.abort();cancelAnimationFrame(frame);}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
* { box-sizing: border-box; }
|
||||
[hidden] { display:none !important; }
|
||||
html { --page-bg:#fff; --page-ink:#283244; --page-muted:#8a929f; --page-line:#e8ecf2; background:var(--page-bg); color:var(--page-ink); color-scheme: light; }
|
||||
html[data-theme="dark"] { --page-bg:#0e1117; --page-ink:#edf0f5; --page-muted:#7f8795; --page-line:#ffffff12; color-scheme:dark; }
|
||||
/* A quiet host backdrop makes the glass sample's transmission visible. */
|
||||
html[data-material="glass"] { background-image:radial-gradient(ellipse at 12% 25%,color-mix(in oklab,var(--gallery-accent) 18%,transparent),transparent 56%),radial-gradient(ellipse at 88% 70%,color-mix(in oklab,var(--gallery-accent) 12%,transparent),transparent 55%); background-attachment:fixed; }
|
||||
body { margin:0; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB",sans-serif; -webkit-font-smoothing:antialiased; }
|
||||
button,input,a { -webkit-tap-highlight-color:transparent; }
|
||||
button { font-family:inherit; }
|
||||
button:focus-visible,a:focus-visible,input:focus-visible { outline:2px solid var(--page-ink); outline-offset:4px; }
|
||||
.gallery-page { max-width:1360px; padding:0 40px; margin:auto; }
|
||||
.gallery-header { height:72px; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid var(--page-line); }
|
||||
.gallery-mark { text-decoration:none; color:inherit; font-size:25px; font-weight:650; letter-spacing:-.08em; & span { font-size:13px; letter-spacing:0; font-weight:400; margin-left:7px; color:var(--page-muted); } }
|
||||
.gallery-edition { font-size:11px; color:var(--page-muted); letter-spacing:.02em; }
|
||||
.gallery-intro { display:flex; align-items:center; justify-content:space-between; padding:33px 0 32px; gap:20px; & h1 { margin:0; font-size:26px; font-weight:450; letter-spacing:-.055em; } }
|
||||
.gallery-controls { display:flex; flex-wrap:wrap; align-items:center; gap:12px; font-size:11px; }
|
||||
.theme-options { display:flex; padding:2px; border:1px solid var(--page-line); border-radius:5px; & button { background:transparent; color:var(--page-muted); border:0; padding:4px 7px; font-size:10px; cursor:pointer; border-radius:3px; &.selected { background:var(--page-line); color:var(--page-ink); } } }
|
||||
.radius-control { display:flex; align-items:center; gap:7px; color:var(--page-muted); white-space:nowrap; & input { width:54px; height:2px; accent-color:var(--page-ink); cursor:pointer; } & output { width:13px; font:10px ui-monospace,monospace; } }
|
||||
figure { margin:0; min-width:0; }
|
||||
.replay-motion { font:inherit; color:var(--page-muted); background:transparent; border:1px solid var(--page-line); border-radius:4px; padding:4px 7px; cursor:pointer; }
|
||||
figcaption .replay-motion { border:0; padding:0; font-size:10px; text-decoration:underline; text-underline-offset:3px; }
|
||||
figcaption { display:flex; justify-content:space-between; gap:8px; padding:12px 1px 0; color:var(--page-muted); font:10px/1.5 ui-monospace,monospace; & > span:last-child { font-family:inherit; font-size:9px; opacity:.75; } }
|
||||
.signature-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,290px),1fr)); gap:18px; }
|
||||
.ribbons-figure { margin-top:30px; }
|
||||
.promo-figure { margin-top:30px; }
|
||||
.exploration-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,290px),1fr)); gap:18px; margin-top:36px; }
|
||||
.renderer-options { display:inline-flex; gap:3px; & button { font-family:inherit; font-size:11px; line-height:1.4; border:1px solid transparent; padding:3px 8px; border-radius:4px; background:transparent; color:var(--page-muted); cursor:pointer; } & button[aria-pressed="true"] { border-color:var(--page-line); color:var(--page-ink); } }
|
||||
.utility-grid { margin-top:36px; }
|
||||
.metric-figure { margin-top:30px; }
|
||||
.utility-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:24px; }
|
||||
.showcase-intro { display:flex; flex-direction:column; justify-content:center; position:relative; padding:45px 0 5px; & > span { font:10px ui-monospace,monospace; color:var(--page-muted); letter-spacing:.07em; } & h2 { margin:18px 0 15px; font-weight:400; font-size:36px; line-height:1.25; letter-spacing:-.06em; } & p { font-size:13px; color:var(--page-muted); margin:0; } }
|
||||
.carousel-navigation { position:absolute; bottom:23px; right:0; display:flex; gap:8px; & button { width:34px; height:34px; border:1px solid var(--page-line); background:transparent; border-radius:4px; color:var(--page-ink); font-size:17px; cursor:pointer; &:hover { background:var(--page-line); } } }
|
||||
.showcase-section { margin-top:35px; }
|
||||
.gallery-footer { border-top:1px solid var(--page-line); padding:25px 0 35px; margin-top:48px; display:flex; justify-content:space-between; font-size:10px; color:var(--page-muted); }
|
||||
body.focus-only {
|
||||
& .gallery-page { width:440px; padding:20px; }
|
||||
& .gallery-header,& .gallery-intro,& .palette-editor,& .category-controls,& .category-summary,& .gallery-footer,& .showcase-intro { display:none; }
|
||||
& main { display:block; }
|
||||
& .signature-grid,& .utility-grid,& .closing-grid,& .exploration-grid { display:contents; }
|
||||
& .exploration-grid.focused { display:grid; margin:0; & figure { display:block; } }
|
||||
& figure { display:none; margin:0; }
|
||||
& figure.focused { display:block; }
|
||||
& .showcase-section { display:none; margin:0; }
|
||||
& .showcase-section.focused { display:block; }
|
||||
& .showcase-card { flex-basis:calc((100% - 48px) / 4); }
|
||||
&.focus-wide .gallery-page { width:min(1280px,100vw); }
|
||||
}
|
||||
@media(max-width:1050px) { .gallery-intro { flex-direction:column; align-items:flex-start; gap:20px; } .gallery-controls { width:100%; } }
|
||||
@media(max-width:650px) {
|
||||
.gallery-page { padding:0 20px; }
|
||||
.gallery-header { height:60px; }
|
||||
.gallery-edition { font-size:9px; }
|
||||
.gallery-intro { padding:24px 0; gap:18px; & h1 { font-size:25px; } }
|
||||
.gallery-controls { flex-wrap:wrap; gap:10px; justify-content:space-between; }
|
||||
.radius-control { gap:5px; & input { width:40px; } }
|
||||
.signature-grid { grid-template-columns:1fr; gap:26px; }
|
||||
.exploration-grid { grid-template-columns:1fr; gap:26px; }
|
||||
.closing-grid { grid-template-columns:1fr; }
|
||||
.showcase-intro { grid-column:auto; padding:30px 0 24px; & h2 { font-size:32px; } }
|
||||
.gallery-footer { font-size:9px; }
|
||||
body.focus-only .gallery-page { width:100%; }
|
||||
body.focus-only .showcase-card { flex-basis:min(332px,86vw); }
|
||||
}
|
||||
|
||||
.attribute-control { display:flex; align-items:center; gap:5px; color:var(--page-muted); white-space:nowrap; & select { border:1px solid var(--page-line); border-radius:4px; color:var(--page-ink); background:var(--page-bg); padding:4px; font:inherit; cursor:pointer; } }
|
||||
@media(min-width:1051px) { .gallery-intro { flex-wrap:wrap; } .gallery-controls { gap:12px; } }
|
||||
@media(max-width:650px) { .utility-grid { grid-template-columns:1fr; } .showcase-intro { padding:35px 0 25px; } }
|
||||
|
||||
.palette-editor { display:flex; flex-wrap:wrap; align-items:center; gap:14px 24px; padding:18px 0; border-top:1px solid var(--page-line); border-bottom:1px solid var(--page-line); }
|
||||
.palette-editor .attribute-control { font-size:12px; }
|
||||
.palette-editor .attribute-control select { padding:7px 8px; }
|
||||
.palette-color { display:flex; align-items:center; gap:8px; min-width:0; }
|
||||
.palette-color > label { font-size:12px; color:var(--page-muted); }
|
||||
.palette-color input[type="color"] { appearance:none; width:30px; height:30px; flex:none; border:1px solid var(--page-line); padding:3px; border-radius:6px; background:var(--page-bg); cursor:pointer; }
|
||||
.palette-color input[type="color"]::-webkit-color-swatch-wrapper { padding:0; }
|
||||
.palette-color input[type="color"]::-webkit-color-swatch { border:0; border-radius:3px; }
|
||||
.palette-color input[type="color"]::-moz-color-swatch { border:0; border-radius:3px; }
|
||||
.palette-hex { width:90px; min-width:0; padding:7px 6px; border:1px solid var(--page-line); border-radius:5px; background:var(--page-bg); color:var(--page-ink); font:12px/1.2 ui-monospace,monospace; }
|
||||
.palette-hex[aria-invalid="true"] { border-color:#b94e46; }
|
||||
.palette-reset { margin-left:auto; font-family:inherit; font-size:12px; color:var(--page-muted); background:transparent; border:0; padding:8px 0; cursor:pointer; }
|
||||
.palette-reset:hover { color:var(--page-ink); }
|
||||
.palette-note { color:var(--page-muted); font-size:12px; }
|
||||
.palette-feedback { flex-basis:100%; font-size:12px; color:var(--page-ink); }
|
||||
.palette-feedback:empty { display:none; }
|
||||
.category-controls { display:flex; flex-wrap:wrap; gap:8px 20px; padding-top:20px; margin-bottom:24px; }
|
||||
.category-controls button { display:flex; align-items:baseline; gap:7px; font-family:inherit; font-size:13px; border:0; border-bottom:2px solid transparent; padding:8px 0; color:var(--page-muted); background:transparent; cursor:pointer; }
|
||||
.category-controls button[aria-pressed="true"] { border-color:var(--page-ink); color:var(--page-ink); }
|
||||
.category-controls button span { font:11px ui-monospace,monospace; color:var(--page-muted); }
|
||||
.category-summary { margin:0 0 24px; max-width:70ch; font-size:13px; line-height:1.6; color:var(--page-muted); }
|
||||
/* Filtered results share tracks; a lone portrait must not stretch into a banner. */
|
||||
main[data-category]:not([data-category="all"]) {
|
||||
display:grid; grid-template-columns:repeat(auto-fill,minmax(min(100%,290px),1fr)); gap:30px 18px; align-items:start;
|
||||
& .signature-grid,& .exploration-grid,& .utility-grid { display:contents; }
|
||||
& .ribbons-figure,& .promo-figure,& .metric-figure,& .showcase-intro,& .showcase-section { grid-column:1 / -1; margin:0; }
|
||||
}
|
||||
@media(max-width:650px) {
|
||||
.palette-editor { gap:14px 20px; }
|
||||
.palette-mode { margin-right:calc(100% - 120px); }
|
||||
.palette-reset { margin-left:0; }
|
||||
.category-controls { column-gap:18px; }
|
||||
}
|
||||
|
||||
/* New studies keep the first three portrait/feature views together. */
|
||||
.peer-intro { padding:8px 0 24px; }
|
||||
.peer-intro > span { font:10px ui-monospace,monospace; letter-spacing:.06em; color:var(--page-muted); }
|
||||
.peer-intro h2 { font-size:24px; line-height:1.3; font-weight:450; letter-spacing:-.04em; margin:12px 0 8px; }
|
||||
.peer-intro p { font-size:12px; line-height:1.6; color:var(--page-muted); margin:0; }
|
||||
.peer-grid { display:grid; grid-template-columns:repeat(6,minmax(0,1fr)); gap:28px 18px; margin-bottom:44px; align-items:start; }
|
||||
.peer-grid > figure { grid-column:span 2; }
|
||||
.peer-grid > .peer-wide { grid-column:span 3; }
|
||||
.peer-grid figcaption { flex-wrap:wrap; }
|
||||
.peer-grid figcaption a { color:inherit; text-decoration:none; font:10px/1.5 -apple-system,BlinkMacSystemFont,sans-serif; }
|
||||
.peer-grid figcaption a:hover { text-decoration:underline; text-underline-offset:3px; }
|
||||
main[data-category]:not([data-category="all"]) {
|
||||
& .peer-grid { display:contents; }
|
||||
& .peer-grid > figure { grid-column:auto; }
|
||||
& .peer-intro { grid-column:1 / -1; padding:0; }
|
||||
}
|
||||
body.focus-only {
|
||||
& .peer-intro { display:none; }
|
||||
& .peer-grid { display:contents; }
|
||||
& .peer-grid.focused { display:grid; margin:0; & figure { display:block; } }
|
||||
}
|
||||
@media(max-width:1050px) {.peer-grid {grid-template-columns:repeat(2,minmax(0,1fr)); & > figure,& > .peer-wide {grid-column:auto;}}}
|
||||
@media(max-width:650px) {.peer-grid {grid-template-columns:1fr;gap:28px;} .peer-intro h2 {font-size:22px;}}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,65 @@
|
||||
const query=new URLSearchParams(location.search);
|
||||
let theme=query.get('theme')==='dark'||query.get('state')==='dark'?'dark':'light';
|
||||
const cards=[...document.querySelectorAll('[data-card]')];
|
||||
const title=['top','bottom'].includes(query.get('title'))?query.get('title'):'default',motion=query.get('motion')||'auto';
|
||||
function setAttribute(attribute,value){if(attribute==='material')document.documentElement.dataset.material=value;for(const card of cards){if(attribute==='title'&&value==='default')delete card.dataset.title;else card.dataset[attribute]=value;}}
|
||||
setAttribute('title',title);setAttribute('motion',motion);
|
||||
const effects=['none','tilt','glow','tilt glow'].includes(query.get('effects'))?query.get('effects'):'none';
|
||||
setAttribute('effects',effects);
|
||||
const finishChoices={shadow:['auto','none','contact','lifted'],material:['plain','metal','glass','solid']},finishKey='oil.cards.finish.v2';
|
||||
let savedFinish={};try{savedFinish=JSON.parse(localStorage.getItem(finishKey)) || {};}catch{}
|
||||
const requestedMaterial=finishChoices.material.includes(query.get('material'))?query.get('material'):null;
|
||||
const finish={
|
||||
material:requestedMaterial || (finishChoices.material.includes(savedFinish.material)?savedFinish.material:'plain'),
|
||||
shadow:finishChoices.shadow.includes(query.get('shadow'))?query.get('shadow'):requestedMaterial?'auto':finishChoices.shadow.includes(savedFinish.shadow)?savedFinish.shadow:'auto'
|
||||
};
|
||||
for(const [key,value] of Object.entries(finish))setAttribute(key,value);
|
||||
for(const control of document.querySelectorAll('[data-attribute]')){
|
||||
control.value=({title,motion,effects,...finish})[control.dataset.attribute];
|
||||
control.addEventListener('change',()=>{
|
||||
const key=control.dataset.attribute;setAttribute(key,control.value);
|
||||
if(Object.hasOwn(finishChoices,key)){
|
||||
finish[key]=control.value;
|
||||
if(key==='material'){finish.shadow='auto';setAttribute('shadow','auto');document.querySelector('[data-attribute=shadow]').value='auto';}
|
||||
try{localStorage.setItem(finishKey,JSON.stringify(finish));}catch{}
|
||||
}
|
||||
});
|
||||
}
|
||||
function appearance(){
|
||||
document.documentElement.dataset.theme=theme;
|
||||
const [a,b,c]=paletteEditor.colors;
|
||||
document.documentElement.style.setProperty('--gallery-accent',a);
|
||||
for(const card of cards){card.style.setProperty('--card-accent',a);card.style.setProperty('--card-accent-2',b);card.style.setProperty('--card-accent-3',c);card.style.setProperty('--card-radius',document.querySelector('#radius').value+'px');card.dispatchEvent(new Event('card-theme-change'));}
|
||||
for(const button of document.querySelectorAll('[data-theme-choice]')){const active=button.dataset.themeChoice===theme;button.classList.toggle('selected',active);button.setAttribute('aria-pressed',active);}
|
||||
}
|
||||
const paletteEditor=modules['gallery-palette.js'].createPaletteEditor({query,onChange:appearance});
|
||||
const catalog=modules['gallery-catalog.js'].createCatalog(query);
|
||||
appearance();
|
||||
const focus=query.get('focus');
|
||||
if(focus){document.body.classList.add('focus-only');if(['line-ribbons','metric','promo-band','showcase','explorations','peer-cards'].includes(focus))document.body.classList.add('focus-wide');document.querySelector(`[data-template="${CSS.escape(focus)}"]`)?.classList.add('focused');}
|
||||
const original=cards.map(card=>card.innerHTML);
|
||||
function flowVariant(value){document.querySelector('.flow-object').dataset.variant=value;for(const b of document.querySelectorAll('[data-flow-variant]'))b.setAttribute('aria-pressed',String(b.dataset.flowVariant===value));}
|
||||
function globeMode(value){document.querySelector('.dot-globe').dataset.mode=value;for(const b of document.querySelectorAll('[data-globe-mode]'))b.setAttribute('aria-pressed',String(b.dataset.globeMode===value));}
|
||||
flowVariant(query.get('variant')==='card'?'card':'token');globeMode(query.get('mode')==='mesh'?'mesh':'globe');
|
||||
for(const b of document.querySelectorAll('[data-flow-variant]'))b.addEventListener('click',()=>flowVariant(b.dataset.flowVariant));
|
||||
for(const b of document.querySelectorAll('[data-globe-mode]'))b.addEventListener('click',()=>globeMode(b.dataset.globeMode));
|
||||
let scene=Object.hasOwn(contentScenes,query.get('content'))?query.get('content'):'studio',mounts=[];
|
||||
function mountScene(){
|
||||
mounts.forEach(instance=>instance.destroy());
|
||||
cards.forEach((card,i)=>{card.innerHTML=original[i];});
|
||||
mounts=cards.map(card=>modules[card.dataset.card+'/card.js'].mount(card,{speed:Number(query.get('speed')??1),content:contentScenes[scene][card.dataset.example || card.dataset.card] || {}}));
|
||||
ribbonRenderer(selectedRenderer);
|
||||
}
|
||||
let selectedRenderer=['webgl','dots','grid'].includes(query.get('ribbons'))?query.get('ribbons'):'svg';
|
||||
mountScene();
|
||||
const sceneControl=document.querySelector('#content-scene');sceneControl.value=scene;
|
||||
sceneControl.addEventListener('change',()=>{scene=sceneControl.value;mountScene();});
|
||||
for(const button of document.querySelectorAll('[data-replay]'))button.addEventListener('click',()=>{
|
||||
mounts.forEach((instance,i)=>{if(button.dataset.replay==='all' || cards[i].dataset.card===button.dataset.replay)instance.replay?.();});
|
||||
});
|
||||
function ribbonRenderer(value){const ribbon=mounts[cards.findIndex(card=>card.dataset.card==='line-ribbons')];const actual=ribbon.setRenderer(value);selectedRenderer=actual;for(const b of document.querySelectorAll('[data-ribbon-renderer]'))b.setAttribute('aria-pressed',String(b.dataset.ribbonRenderer===actual));}
|
||||
for(const b of document.querySelectorAll('[data-ribbon-renderer]'))b.addEventListener('click',()=>ribbonRenderer(b.dataset.ribbonRenderer));
|
||||
for(const button of document.querySelectorAll('[data-theme-choice]'))button.addEventListener('click',()=>{theme=button.dataset.themeChoice;appearance();});
|
||||
document.querySelector('#radius').addEventListener('input',e=>{document.querySelector('.radius-control output').value=e.target.value;appearance();});
|
||||
for(const button of document.querySelectorAll('[data-slide]'))button.addEventListener('click',()=>document.querySelector('.showcase-row').scrollBy({left:Number(button.dataset.slide)*348,behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'instant':'smooth'}));
|
||||
window.addEventListener('pagehide',event=>{if(!event.persisted){mounts.forEach(instance=>instance.destroy());paletteEditor.destroy();catalog.destroy();}});
|
||||
@@ -0,0 +1,71 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN" data-theme="light">
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="icon" href="data:,"><title>看得见的产品力 · oil cards</title><style>{{styles}}</style></head>
|
||||
<body>
|
||||
<div class="gallery-page">
|
||||
<header class="gallery-header"><a class="gallery-mark" href="#top" aria-label="回到顶部">oil<span> / cards</span></a><span class="gallery-edition">19 种模板 · 25 个实例</span></header>
|
||||
<div class="gallery-intro" id="top"><h1>卡片探索。</h1><div class="gallery-controls" aria-label="外观控制">
|
||||
<label class="attribute-control">内容 <select id="content-scene" aria-label="内容场景"><option value="studio">创作团队</option><option value="developer">开发工具</option></select></label>
|
||||
<label class="attribute-control">外层效果 <select data-attribute="effects" aria-label="外层卡片效果"><option value="none">关闭</option><option value="tilt">倾斜</option><option value="glow">流光</option><option value="tilt glow">倾斜与流光</option></select></label>
|
||||
<label class="attribute-control">材质 <select data-attribute="material" aria-label="卡片材质"><option value="plain">原始</option><option value="metal">金属</option><option value="glass">玻璃</option><option value="solid">实体卡</option></select></label>
|
||||
<label class="attribute-control">阴影 <select data-attribute="shadow" aria-label="卡片阴影"><option value="auto">跟随材质</option><option value="none">无</option><option value="contact">轻贴合</option><option value="lifted">微悬浮</option></select></label>
|
||||
|
||||
<div class="theme-options" aria-label="选择主题"><button data-theme-choice="light" class="selected" aria-pressed="true">浅</button><button data-theme-choice="dark" aria-pressed="false">深</button></div>
|
||||
<label class="attribute-control">标题 <select data-attribute="title" aria-label="标题位置"><option value="default">默认</option><option value="top">在上</option><option value="bottom">在下</option></select></label>
|
||||
<label class="attribute-control">动效 <select data-attribute="motion" aria-label="动效模式"><option value="auto">自动</option><option value="hover">悬停</option><option value="off">关闭</option></select></label>
|
||||
<button class="replay-motion" data-replay="all">重播演示</button>
|
||||
<label class="radius-control">圆角 <input id="radius" type="range" min="0" max="24" value="8" aria-label="卡片圆角"><output for="radius">8</output></label>
|
||||
</div></div>
|
||||
<section class="palette-editor" aria-label="自定义配色">
|
||||
<label class="attribute-control palette-mode">配色 <select id="palette-mode" aria-label="配色数量"><option value="single">单色</option><option value="duo">双色</option><option value="trio">三色</option></select></label>
|
||||
<div class="palette-color" data-color-slot="0"><label for="color-primary">主色</label><input type="color" id="color-primary" aria-label="选择主色" value="#5865c7"><input class="palette-hex" id="hex-primary" aria-label="主色 HEX" value="#5865C7" maxlength="7" spellcheck="false" autocomplete="off"></div>
|
||||
<div class="palette-color" data-color-slot="1" hidden><label for="color-secondary">辅色</label><input type="color" id="color-secondary" aria-label="选择辅色" value="#9aa5e8"><input class="palette-hex" id="hex-secondary" aria-label="辅色 HEX" value="#9AA5E8" maxlength="7" spellcheck="false" autocomplete="off"></div>
|
||||
<div class="palette-color" data-color-slot="2" hidden><label for="color-tertiary">点缀</label><input type="color" id="color-tertiary" aria-label="选择点缀色" value="#d6c9b7"><input class="palette-hex" id="hex-tertiary" aria-label="点缀色 HEX" value="#D6C9B7" maxlength="7" spellcheck="false" autocomplete="off"></div>
|
||||
<button class="palette-reset" type="button">重置配色</button>
|
||||
<span class="palette-note">图形跟随配色,封面保留原色</span>
|
||||
<span class="palette-feedback" role="status" aria-live="polite"></span>
|
||||
</section>
|
||||
<nav class="category-controls" aria-label="卡片分类">
|
||||
<button type="button" data-family-filter="all" aria-pressed="true">全部 <span></span></button>
|
||||
<button type="button" data-family-filter="visual" aria-pressed="false">视觉意象 <span></span></button>
|
||||
<button type="button" data-family-filter="product" aria-pressed="false">产品场景 <span></span></button>
|
||||
<button type="button" data-family-filter="data" aria-pressed="false">数据关系 <span></span></button>
|
||||
<button type="button" data-family-filter="action" aria-pressed="false">行动引导 <span></span></button>
|
||||
</nav>
|
||||
<p class="category-summary" hidden></p>
|
||||
<main>
|
||||
<div class="peer-intro" id="peer-cards"><span>形态探索 / 影像 · 知识 · 系统</span><h2>从视觉形态,到产品细节。</h2><p>让取景、层次和每一次变化,解释产品能做什么。</p></div>
|
||||
<section class="peer-grid" data-template="peer-cards" aria-label="新卡片探索">
|
||||
<figure data-template="portrait-story" data-family="visual">{{portrait-story}}<figcaption><span>15 / portrait-story</span><span>影像叙事</span></figcaption></figure>
|
||||
<figure data-template="capture-notes" data-family="product">{{capture-notes}}<figcaption><span>16 / capture-notes</span><span>讨论记录</span></figcaption></figure>
|
||||
<figure data-template="answer-lens" data-family="product">{{answer-lens}}<figcaption><span>17 / answer-lens</span><span>知识透镜</span></figcaption></figure>
|
||||
<figure class="peer-wide" data-template="responsive-preview" data-family="product">{{responsive-preview}}<figcaption><span>18 / responsive-preview</span><button class="replay-motion" data-replay="responsive-preview">重播尺寸展开</button><span>响应式展开</span></figcaption></figure>
|
||||
<figure class="peer-wide" data-template="deploy-preview" data-family="product">{{deploy-preview}}<figcaption><span>19 / deploy-preview</span><button class="replay-motion" data-replay="deploy-preview">重播构建过程</button><span>构建流水线</span></figcaption></figure>
|
||||
</section>
|
||||
|
||||
<section class="signature-grid" aria-label="主视觉卡片">
|
||||
<figure data-template="particle-ring" data-family="visual">{{particle-ring}}<figcaption><span>01 / particle-ring</span><span>光场与内容</span></figcaption></figure>
|
||||
<figure data-template="flow-object" data-family="visual">{{flow-object}}<figcaption><span>02 / flow-object</span><span class="renderer-options" aria-label="实体通证形态"><button data-flow-variant="token" aria-pressed="true">实体铭牌</button><button data-flow-variant="card" aria-pressed="false">流光卡片</button></span></figcaption></figure>
|
||||
<figure data-template="dot-globe" data-family="data">{{dot-globe}}<figcaption><span>03 / dot-globe</span><span class="renderer-options" aria-label="空间拓扑形态"><button data-globe-mode="globe" aria-pressed="true">点阵地球</button><button data-globe-mode="mesh" aria-pressed="false">星座拓扑</button></span></figcaption></figure>
|
||||
</section>
|
||||
<figure id="line-ribbons" class="ribbons-figure" data-template="line-ribbons" data-family="visual">{{line-ribbons}}<figcaption><span>04 / line-ribbons</span><span class="renderer-options" aria-label="比较丝带材质"><button data-ribbon-renderer="svg" aria-pressed="true">线织</button><button data-ribbon-renderer="webgl" aria-pressed="false">薄绸</button><button data-ribbon-renderer="dots" aria-pressed="false">点阵</button><button data-ribbon-renderer="grid" aria-pressed="false">网格</button></span></figcaption></figure>
|
||||
<figure id="promo-band" class="promo-figure" data-template="promo-band" data-family="action">{{promo-band}}<figcaption><span>11 / promo-band</span><span>光谱 / 折面</span></figcaption></figure>
|
||||
<section id="explorations" class="exploration-grid" data-template="explorations" aria-label="更多产品表达">
|
||||
<figure data-template="vector-space" data-family="data">{{vector-space}}<figcaption><span>12 / vector-space</span><span>语义空间</span></figcaption></figure>
|
||||
<figure data-template="file-mosaic" data-family="product">{{file-mosaic}}<figcaption><span>13 / file-mosaic</span><span>资源阵列</span></figcaption></figure>
|
||||
<figure data-template="stream-window" data-family="product">{{stream-window}}<figcaption><span>14 / stream-window</span><button class="replay-motion" data-replay="stream-window">重播渐进呈现</button></figcaption></figure>
|
||||
</section>
|
||||
<section class="utility-grid" aria-label="产品体验卡片">
|
||||
<figure data-template="ui-slice" data-family="product">{{ui-slice}}<figcaption><span>06 / ui-slice</span><button class="replay-motion" data-replay="ui-slice">重播动效</button></figcaption></figure>
|
||||
<figure data-template="terminal" data-family="product">{{terminal}}<figcaption><span>07 / terminal</span><button class="replay-motion" data-replay="terminal">重播动效</button></figcaption></figure>
|
||||
<figure data-template="beam-graph" data-family="data">{{beam-graph}}<figcaption><span>09 / beam-graph</span><span>节点与路径</span></figcaption></figure>
|
||||
<figure data-template="keycaps" data-family="action">{{keycaps}}<figcaption><span>10 / keycaps</span><button class="replay-motion" data-replay="keycaps">重播动效</button></figcaption></figure>
|
||||
</section>
|
||||
<figure class="metric-figure" data-template="metric" data-family="data">{{metric}}<figcaption><span>08 / metric</span><button class="replay-motion" data-replay="metric">重播动效</button></figcaption></figure>
|
||||
<div class="showcase-intro"><span>05 / SHOWCASE</span><h2>每个品牌,<br>都有自己的光。</h2><p>从一抹颜色开始,找到属于你的表达。</p><div class="carousel-navigation"><button data-slide="-1" aria-label="向前浏览">←</button><button data-slide="1" aria-label="向后浏览">→</button></div></div>
|
||||
<section class="showcase-section" id="showcase" data-template="showcase" data-family="visual">{{showcase}}</section>
|
||||
</main>
|
||||
<footer class="gallery-footer"><span>oil cards / 第一辑</span><span>为有想法的产品,留一方舞台。</span></footer>
|
||||
</div>
|
||||
<script>{{scripts}}</script>
|
||||
</body></html>
|
||||
@@ -0,0 +1,22 @@
|
||||
.keycaps {
|
||||
& .card-stage { --key-group-top:calc(50% - 107px); }
|
||||
& .keycap-row { position:absolute; width:100%; top:var(--key-group-top); display:flex; align-items:center; justify-content:center; gap:21px; }
|
||||
& .keycap {
|
||||
--_key-radius:calc(var(--_card-radius)*1.6);
|
||||
width:128px; height:136px; flex-shrink:0; position:relative;
|
||||
/* A shallow sidewall carries the depth; avoid stacking another dark rim. */
|
||||
background:linear-gradient(180deg in oklab,color-mix(in oklab,var(--card-panel),var(--card-muted) 4%),color-mix(in oklab,var(--card-panel),var(--card-muted) 15%));
|
||||
border:1px solid color-mix(in oklab,var(--card-panel),var(--card-muted) 20%);
|
||||
border-radius:var(--_key-radius);
|
||||
box-shadow:0 1px 2px #0000000a,0 calc(6px - var(--key-press,0)*2px) calc(10px - var(--key-press,0)*3px) -6px #00000024;
|
||||
}
|
||||
& .keycap-top { position:absolute; inset:2px 2px 6px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; background:linear-gradient(180deg in oklab,var(--card-panel),color-mix(in oklab,var(--card-panel),var(--card-muted) 2%)); border:1px solid color-mix(in oklab,var(--card-panel),var(--card-muted) 13%); border-radius:max(0px,calc(var(--_key-radius) - 3px)); box-shadow:0 1px 1px #00000007; transform:translateY(calc(var(--key-press,0)*2.5px));
|
||||
& > span { font-size:48px; line-height:1.2; font-weight:400; } & small { font-size:11px; color:var(--card-muted); }
|
||||
}
|
||||
& .key-letter .keycap-top > span { color:var(--_card-accent-ink); }
|
||||
& .key-plus { font-size:17px; color:var(--card-muted); }
|
||||
& .command-panel { position:absolute; left:50%; top:calc(var(--key-group-top) + 164px); width:min(420px,86%); height:50px; transform:translate(-50%,calc((1 - var(--command-visible,0))*8px)); display:flex; gap:12px; align-items:center; padding:13px 15px; border:1px solid var(--card-line); border-radius:calc(var(--_card-radius)*.8); background:var(--card-panel); color:var(--card-muted); box-shadow:0 2px 6px -3px #00000012; font-size:12px; opacity:var(--command-visible,0); & > span:first-child { font-size:22px; color:var(--card-ink); } & kbd { font:9px ui-monospace,monospace; margin-left:auto; border:1px solid var(--card-line); border-radius:3px; padding:3px; } }
|
||||
|
||||
@media(max-width:650px) { & .keycap-row { gap:16px; } }
|
||||
@media(max-width:360px) { & .keycap-row { gap:14px; } & .keycap { width:104px; height:116px; } }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<article class="card bottom-title keycaps" data-card="keycaps" data-motion="auto">
|
||||
<div class="card-stage"><div class="keycap-row"><div class="keycap key-mod"><div class="keycap-top"><span data-text="modifier">⌘</span><small data-text="modifierName">command</small></div></div><span class="key-plus">+</span><div class="keycap key-letter"><div class="keycap-top"><span data-text="key">K</span><small data-text="keyName">quick action</small></div></div></div><div class="command-panel"><span>⌕</span><span data-text="placeholder">搜索命令或项目…</span><kbd>Esc</kbd></div></div>
|
||||
<div class="card-caption"><h2 data-text="title">快一步,回到心流</h2><p data-text="description">常用动作,始终触手可及。</p></div>
|
||||
</article>
|
||||
@@ -0,0 +1,14 @@
|
||||
import {loop,shell,smooth,staticMotion} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,options);
|
||||
const keys=[...element.querySelectorAll('.keycap')],stage=element.querySelector('.card-stage'),still=staticMotion(element,options);
|
||||
const clock=loop(element,time=>{
|
||||
const staticState=still();
|
||||
keys.forEach((key,i)=>{
|
||||
const down=smooth((time-.7-i*.085)/.23),up=smooth((time-1.12-i*.045)/.46);
|
||||
key.style.setProperty('--key-press',staticState?0:down*(1-up));
|
||||
});
|
||||
stage.style.setProperty('--command-visible',staticState?1:smooth((time-.98)/.48));
|
||||
},{...options,duration:1.85});
|
||||
return {replay:()=>clock.restart(),destroy(){clock.destroy();clean();keys.forEach(k=>k.style.removeProperty('--key-press'));stage.style.removeProperty('--command-visible');}};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
.card.line-ribbons {
|
||||
aspect-ratio:auto;
|
||||
display:grid; grid-template-columns:minmax(0,1fr); grid-template-rows:auto auto minmax(140px,1fr); min-height:min-content;
|
||||
/* A spanning ratio spacer preserves the art format without capping text height. */
|
||||
&::before { content:""; grid-column:1; grid-row:1 / -1; aspect-ratio:2.7; align-self:start; pointer-events:none; }
|
||||
& > .card-heading, & > .ribbon-note { grid-column:1; }
|
||||
& > .card-heading { grid-row:1; position:relative; inset:auto; flex:none; margin:24px 24px 0 24px; }
|
||||
/* Upper-left space is reserved by the paths, without a horizontal mask. */
|
||||
&:not([data-title]) > .card-stage, &[data-title="top"] > .card-stage { inset:0; }
|
||||
& .ribbon-art, & .ribbon-surface { position:absolute; width:100%; height:100%; inset:0; }
|
||||
&[data-surface-ready="true"] .ribbon-art { visibility:hidden; }
|
||||
&[data-renderer="dots"] .ribbon-mist, &[data-renderer="grid"] .ribbon-mist { opacity:.13; }
|
||||
& .ribbon-mist { position:absolute; inset:0; background:
|
||||
radial-gradient(ellipse 31% 65% at 49% 106% in oklab,color-mix(in oklab,oklch(from var(--_card-accent-2) calc(l - .12) calc(c * 1.08) h) 88%,transparent) 8%,color-mix(in oklab,var(--_card-accent-2) 65%,transparent) 38%,transparent 96%),
|
||||
radial-gradient(ellipse 39% 70% at 75% 95% in oklab,color-mix(in oklab,color-mix(in oklab,var(--_card-accent-2) 45%,var(--card-accent)) 66%,transparent),transparent 96%),
|
||||
radial-gradient(ellipse 33% 90% at 98% 40% in oklab,color-mix(in oklab,var(--card-accent) 72%,transparent),transparent 94%); }
|
||||
& .ribbon-note { grid-row:2; position:relative; z-index:4; margin:24px 24px 0; flex:none; line-height:1.5; font-size:13px; color:var(--card-muted); }
|
||||
&[data-title="bottom"] {
|
||||
grid-template-rows:minmax(140px,1fr) auto auto;
|
||||
& > .card-heading { grid-row:2; margin:24px 24px 0; }
|
||||
& .ribbon-note { grid-row:3; margin:8px 24px 12px; }
|
||||
& > .card-stage { position:relative; grid-column:1; grid-row:1; inset:auto; min-height:140px; }
|
||||
}
|
||||
@media(max-width:650px) {
|
||||
&::before { aspect-ratio:1.2; }
|
||||
& .ribbon-note { max-width:190px; line-height:1.5; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<article class="card banner line-ribbons" data-card="line-ribbons" data-motion="auto">
|
||||
<h2 class="card-heading" data-text="title">各自精彩,<br>也能一起向前</h2>
|
||||
|
||||
<div class="card-stage"><div class="ribbon-mist"></div><svg class="ribbon-art" viewBox="0 0 1280 474" preserveAspectRatio="xMidYMid slice" aria-hidden="true"></svg></div>
|
||||
<p class="ribbon-note" data-text="description">让灵感汇合,让协作发生。</p>
|
||||
</article>
|
||||
@@ -0,0 +1,26 @@
|
||||
import {shell,loop,svgStops} from '../shared/runtime.js';
|
||||
import {createSurface} from './surface.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,options);
|
||||
const svg=element.querySelector('.ribbon-art'),ns='http://www.w3.org/2000/svg';
|
||||
// Unique IDs per instance, no shared counters or page-singleton assumptions.
|
||||
const id='ribbon-'+crypto.randomUUID();
|
||||
svg.innerHTML=`<defs><linearGradient id="${id}" x1="0" y1="1" x2="1" y2="0">${svgStops([[0.0,"var(--_card-accent-2)",1.0],[0.5,"var(--_card-accent-2)",1.0],[1.0,"var(--card-accent)",1.0]])}</linearGradient></defs>`;
|
||||
const ribbons=[
|
||||
[[-160,390,210,285,850,410,1350,-170],[-160,460,430,465,775,250,1390,-50]],
|
||||
[[-170,455,400,540,800,50,1120,-210],[-170,310,490,640,855,250,1300,-40]],
|
||||
[[280,650,650,180,1110,500,1420,60],[480,680,730,420,1150,80,1450,320]]
|
||||
];
|
||||
for(let r=0;r<ribbons.length;r++)for(let i=0;i<128;i++){
|
||||
const t=(1-Math.cos(Math.PI*i/127))/2,[a,b]=ribbons[r],p=a.map((n,j)=>n+(b[j]-n)*t);
|
||||
const path=document.createElementNS(ns,'path');path.setAttribute('d',`M${p[0]} ${p[1]}C${p.slice(2).join(' ')}`);path.setAttribute('fill','none');path.setAttribute('stroke',`url(#${id})`);path.setAttribute('stroke-width','.8');path.setAttribute('opacity',String(r===1?.64:.46));svg.append(path);
|
||||
}
|
||||
let surface=null,destroyed=false;
|
||||
const frame=()=>svg.setAttribute('viewBox',element.clientWidth<=650?'100 60 560 474':'0 0 1280 474');
|
||||
const framing=new ResizeObserver(frame);framing.observe(element);frame();
|
||||
const clock=loop(element,t=>{if(surface)surface.draw(t);svg.style.transform=`translate(${Math.sin(t/17)*5}px,${Math.sin(t/19)*3}px)`;},options);
|
||||
const setRenderer=value=>{if(destroyed)return null;surface?.destroy();surface=null;if(['webgl','dots','grid'].includes(value))surface=createSurface(element,ribbons,()=>clock.invalidate(),value);const actual=surface?value:'svg';element.dataset.renderer=actual;clock.invalidate();return actual;};
|
||||
if(['webgl','dots','grid'].includes(element.dataset.renderer))setRenderer(element.dataset.renderer);
|
||||
|
||||
return {setRenderer,destroy(){if(destroyed)return;destroyed=true;clock.destroy();surface?.destroy();surface=null;framing.disconnect();clean();svg.replaceChildren();svg.style.removeProperty('transform');}};
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import {fitCanvas,glProgram,glColor,watchColors} from '../shared/runtime.js';
|
||||
|
||||
// Fixed camera, three translucent ruled surfaces. Thread coverage is filtered in
|
||||
// screen space; below one pixel it converges to a film instead of a moire pattern.
|
||||
export function createSurface(element, ribbons, invalidate, variant='webgl') {
|
||||
const canvas=document.createElement('canvas');canvas.className='ribbon-surface';canvas.setAttribute('aria-hidden','true');
|
||||
element.querySelector('.card-stage').append(canvas);
|
||||
const vertex=`attribute vec3 a_position;attribute float a_thread,a_along;
|
||||
uniform vec2 u_size;uniform vec4 u_view;uniform float u_time,u_dpr;varying vec3 v_position;varying float v_thread,v_along;
|
||||
void main(){vec3 p=a_position;float wave=sin(p.x*.004-u_time*.11);
|
||||
p.y+=wave*3.*sin(a_thread*3.14159265);p.z+=wave*12.;
|
||||
v_position=p;v_thread=a_thread;v_along=a_along;float scale=max(u_size.x/u_view.z,u_size.y/u_view.w);
|
||||
vec2 xy=(p.xy-u_view.xy-u_view.zw*.5)*scale/(u_size*.5);
|
||||
gl_Position=vec4(xy.x,-xy.y,0.,1.);gl_PointSize=(1.+.65*sin(a_thread*3.14159265))*u_dpr;}`;
|
||||
const fragment=`#extension GL_OES_standard_derivatives : enable
|
||||
precision highp float;varying vec3 v_position;varying float v_thread,v_along;
|
||||
uniform vec3 u_a,u_b;uniform float u_dark,u_style;${glColor}
|
||||
float wire(float phase){
|
||||
float footprint=max(fwidth(phase),.001),distance=abs(fract(phase+.5)-.5);
|
||||
float line=1.-smoothstep(footprint*.18,footprint*.9,distance);
|
||||
return line*(1.-smoothstep(.28,.85,footprint));
|
||||
}
|
||||
void main(){vec3 n=normalize(cross(dFdx(v_position),dFdy(v_position)));
|
||||
float grazing=1.-abs(n.z);float light=abs(dot(n,normalize(vec3(-.35,-.45,1.))));
|
||||
float phase=v_thread*128.;float footprint=max(fwidth(phase),.001);
|
||||
float distance=abs(fract(phase)-.5);
|
||||
float thread=1.-smoothstep(.18-footprint*.5,.18+footprint*.5,distance);
|
||||
thread=mix(thread,.36,smoothstep(.45,1.15,footprint));
|
||||
float edge=exp(-min(v_thread,1.-v_thread)*40.);
|
||||
float hue=smoothstep(360.,1350.,v_position.x)*.92;
|
||||
vec3 c=pigment(u_b,u_a,hue);vec3 l=lab(c);
|
||||
l.x+=.05*light-.08*grazing;c=srgb(l);
|
||||
float alpha=.15+thread*.39+grazing*.28+edge*.10+u_dark*.08;
|
||||
alpha*=smoothstep(0.,max(fwidth(v_thread)*1.1,.0001),min(v_thread,1.-v_thread));
|
||||
if(u_style>1.5){
|
||||
float grid=max(wire(v_thread*22.),wire(v_along*88.));
|
||||
alpha=grid*(.40+.24*light)+edge*.055;
|
||||
}else if(u_style>.5){
|
||||
float point=1.-smoothstep(.22,.50,length(gl_PointCoord-.5));
|
||||
alpha=point*(.55+.30*sin(v_thread*3.14159265));c=pigment(u_b,u_a,hue);
|
||||
}
|
||||
gl_FragColor=vec4(c*alpha,alpha);}`;
|
||||
const gpu=glProgram(canvas,vertex,fragment);
|
||||
if(!gpu){canvas.remove();return null;}
|
||||
const positions=[],threads=[],alongs=[];
|
||||
const bezier=(a,t)=>{const q=1-t;return [0,1].map(k=>q*q*q*a[k]+3*q*q*t*a[k+2]+3*q*t*t*a[k+4]+t*t*t*a[k+6]);};
|
||||
const point=(r,u,v)=>{
|
||||
const [a,b]=ribbons[r],A=bezier(a,u),B=bezier(b,u),s=variant==='dots'?v:(1-Math.cos(v*Math.PI))/2;
|
||||
positions.push(A[0]+(B[0]-A[0])*s,A[1]+(B[1]-A[1])*s,Math.sin(v*Math.PI*2+u*3+r)*22);
|
||||
threads.push(v);alongs.push(u);
|
||||
};
|
||||
const U=128,V=40;
|
||||
if(variant==='dots'){
|
||||
for(let r=0;r<ribbons.length;r++)for(let u=0;u<=128;u++)for(let v=0;v<=24;v++)point(r,u/128,v/24);
|
||||
}else for(let r=0;r<ribbons.length;r++)for(let u=0;u<U;u++)for(let v=0;v<V;v++){
|
||||
for(const [du,dv]of [[0,0],[1,0],[0,1],[1,0],[1,1],[0,1]])point(r,(u+du)/U,(v+dv)/V);
|
||||
}
|
||||
gpu.attribute('a_position',positions,3);gpu.attribute('a_thread',threads,1);gpu.attribute('a_along',alongs,1);
|
||||
let palette,lost=false;
|
||||
const off=watchColors(element,p=>{palette=p;invalidate();});
|
||||
const ro=new ResizeObserver(invalidate);ro.observe(canvas);
|
||||
const lostContext=e=>{e.preventDefault();lost=true;canvas.hidden=true;element.dataset.surfaceReady='false';};
|
||||
canvas.addEventListener('webglcontextlost',lostContext);
|
||||
element.dataset.surfaceReady='true';
|
||||
return {
|
||||
draw(time){
|
||||
if(!palette||lost)return;
|
||||
const {w,h,dpr}=fitCanvas(canvas,1);const {gl}=gpu;
|
||||
gl.useProgram(gpu.program);gl.viewport(0,0,canvas.width,canvas.height);
|
||||
gl.clearColor(0,0,0,0);gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
gl.enable(gl.BLEND);gl.blendFunc(gl.ONE,gl.ONE_MINUS_SRC_ALPHA);
|
||||
gl.uniform2f(gpu.uniform('u_size'),w,h);gl.uniform1f(gpu.uniform('u_time'),time);
|
||||
gl.uniform1f(gpu.uniform('u_dpr'),dpr);gl.uniform1f(gpu.uniform('u_style'),variant==='dots'?1:variant==='grid'?2:0);
|
||||
gl.uniform4fv(gpu.uniform('u_view'),element.clientWidth<=650?[100,60,560,474]:[0,0,1280,474]);
|
||||
gl.uniform3fv(gpu.uniform('u_a'),palette.a);gl.uniform3fv(gpu.uniform('u_b'),palette.b);
|
||||
gl.uniform1f(gpu.uniform('u_dark'),palette.dark?1:0);
|
||||
gl.drawArrays(variant==='dots'?gl.POINTS:gl.TRIANGLES,0,threads.length);
|
||||
},
|
||||
destroy(){off();ro.disconnect();canvas.removeEventListener('webglcontextlost',lostContext);gpu.destroy();canvas.remove();delete element.dataset.surfaceReady;}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
.metric {
|
||||
min-height:calc(var(--_card-heading-height,44px) + 163px);
|
||||
& .metric-copy { position:absolute; left:30px; right:55%; top:25px; z-index:2; }
|
||||
& .metric-copy h2 { font-size:19px; margin:0 0 9px; font-weight:550; letter-spacing:-.035em; }
|
||||
& .metric-copy p { font-size:11px; color:var(--card-muted); margin:0; }
|
||||
& .metric-number { position:absolute; left:30px; bottom:42px; display:flex; align-items:center; gap:18px; }
|
||||
& .metric-value { display:flex; align-items:baseline; font-size:72px; line-height:1; font-weight:300; letter-spacing:-.025em; font-variant-numeric:tabular-nums; }
|
||||
& .metric-readable { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); }
|
||||
& .metric-digit { display:block; width:.58em; height:1em; overflow:clip; mask-image:linear-gradient(transparent,#000 10%,#000 90%,transparent); }
|
||||
& .metric-reel { display:flex; flex-direction:column; }
|
||||
& .metric-reel > span { display:block; height:1em; flex:none; }
|
||||
& .metric-separator { display:block; width:.22em; }
|
||||
& .metric-delta { font-size:12px; white-space:nowrap; color:var(--_card-accent-ink); padding:6px 8px; background:color-mix(in oklab,var(--card-accent) 7%,var(--card-surface)); border-radius:5px; & small { display:none; } }
|
||||
& .metric-chart { position:absolute; right:30px; bottom:0; width:45%; height:78%; }
|
||||
& .metric-bars { height:100%; display:flex; align-items:flex-end; gap:5px; border-bottom:1px solid var(--card-line);
|
||||
& i { flex:1; height:var(--height); background:color-mix(in oklab,var(--card-muted) 23%,var(--card-surface)); transform-origin:bottom; transform:scaleY(var(--bar-growth,1)); border-radius:2px 2px 0 0; }
|
||||
& .recent { background:color-mix(in oklab,var(--card-accent) 19%,var(--card-surface)); }
|
||||
& .current { background:linear-gradient(in oklab,var(--_card-accent-2),var(--card-accent)); }
|
||||
}
|
||||
& .metric-today { position:absolute; right:0; top:-16px; font-size:10px; color:var(--_card-accent-ink); opacity:var(--metric-settled,1); }
|
||||
& .metric-delta { opacity:var(--metric-settled,1); transform:translateY(calc((1 - var(--metric-settled,1))*3px)); }
|
||||
& .metric-axis { display:none; }
|
||||
&[data-title="bottom"] { & .metric-copy { top:auto; bottom:23px; } & .metric-number { bottom:auto; top:27px; } & .metric-chart { height:65%; bottom:0; } }
|
||||
@media(max-width:950px) { & .metric-value { font-size:55px; } & .metric-delta { font-size:10px; } }
|
||||
@media(max-width:650px) {
|
||||
min-height:calc(var(--_card-heading-height,44px) + 228px);
|
||||
& .metric-copy { left:24px; right:24px; top:24px; }
|
||||
& .metric-copy h2 { font-size:18px; }
|
||||
& .metric-number { left:24px; top:max(97px,calc(var(--_card-heading-height,44px) + 44px)); bottom:auto; gap:10px; }
|
||||
& .metric-value { font-size:52px; }
|
||||
& .metric-chart { width:calc(100% - 48px); right:24px; height:min(36%,calc(100% - var(--_card-heading-height,44px) - 132px)); }
|
||||
& .metric-bars { gap:4px; }
|
||||
&[data-title="bottom"] {
|
||||
min-height:calc(var(--_card-heading-height,44px) + 242px);
|
||||
& .metric-number { top:32px; }
|
||||
& .metric-chart { bottom:max(102px,calc(var(--_card-heading-height,44px) + 55px)); height:min(35%,calc(100% - var(--_card-heading-height,44px) - 162px)); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<article class="card strip metric" data-card="metric" data-motion="auto">
|
||||
<div class="metric-copy"><h2 data-text="title">每一步增长,都有迹可循</h2><p data-text="description">过去 30 天 · 工作流运行次数</p></div>
|
||||
<div class="metric-number"><span class="metric-value">24,860</span><span class="metric-delta" data-text="delta">↗ 18.6%</span></div>
|
||||
<div class="metric-chart"><span class="metric-today" data-text="latest">今日 1,284</span><div class="metric-bars"><i class="" style="--height:12%"></i><i class="" style="--height:18%"></i><i class="" style="--height:15%"></i><i class="" style="--height:24%"></i><i class="" style="--height:29%"></i><i class="" style="--height:22%"></i><i class="" style="--height:34%"></i><i class="" style="--height:32%"></i><i class="" style="--height:41%"></i><i class="" style="--height:45%"></i><i class="" style="--height:38%"></i><i class="" style="--height:49%"></i><i class="" style="--height:46%"></i><i class="" style="--height:59%"></i><i class="" style="--height:56%"></i><i class="" style="--height:66%"></i><i class="" style="--height:61%"></i><i class="" style="--height:74%"></i><i class="recent" style="--height:70%"></i><i class="recent" style="--height:81%"></i><i class="recent" style="--height:77%"></i><i class="recent" style="--height:87%"></i><i class="recent" style="--height:83%"></i><i class="current" style="--height:96%"></i></div><div class="metric-axis"><span>09.06</span><span>10.05</span></div></div>
|
||||
</article>
|
||||
@@ -0,0 +1,38 @@
|
||||
import {loop,shell,smooth,staticMotion} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,options);
|
||||
const value=element.querySelector('.metric-value'),chart=element.querySelector('.metric-bars');
|
||||
const content=options.content || {};
|
||||
if(content.value!==undefined)value.textContent=typeof content.value==='number' && Number.isFinite(content.value)?content.value.toLocaleString('en-US',{maximumFractionDigits:2}):String(content.value);
|
||||
if(Array.isArray(content.series)){
|
||||
const series=content.series.filter(n=>typeof n==='number' && Number.isFinite(n) && n>=0),max=Math.max(1,...series);
|
||||
chart.replaceChildren(...series.map((n,i)=>{const bar=document.createElement('i');bar.style.setProperty('--height',`${n/max*96}%`);if(i===series.length-1)bar.className='current';else if(i>=series.length-6)bar.className='recent';return bar;}));
|
||||
}
|
||||
const bars=[...chart.querySelectorAll('i')];
|
||||
const formatted=value.textContent.trim(),still=staticMotion(element,options);
|
||||
const digits=[];
|
||||
value.replaceChildren();
|
||||
const readable=document.createElement('span');readable.className='metric-readable';readable.textContent=formatted;value.append(readable);
|
||||
for(const char of formatted){
|
||||
const slot=document.createElement('span');slot.setAttribute('aria-hidden','true');
|
||||
if(!/\d/.test(char)){slot.className='metric-separator';slot.textContent=char;value.append(slot);continue;}
|
||||
slot.className='metric-digit';
|
||||
const strip=document.createElement('span');strip.className='metric-reel';
|
||||
const end=Number(char);
|
||||
for(let i=0;i<=end;i++){const n=document.createElement('span');n.textContent=String(i%10);strip.append(n);}
|
||||
slot.append(strip);value.append(slot);digits.push({strip,end});
|
||||
}
|
||||
const clock=loop(element,time=>{
|
||||
const staticState=still();
|
||||
digits.forEach(({strip,end},i)=>{
|
||||
const p=staticState?1:smooth((time-.12-i*.065)/2.05);
|
||||
strip.style.transform=`translateY(${-end*p}em)`;
|
||||
});
|
||||
bars.forEach((bar,i)=>{
|
||||
const p=staticState?1:smooth((time-.10-i*.018)/1.85);
|
||||
bar.style.setProperty('--bar-growth',Math.max(.008,p));
|
||||
});
|
||||
element.style.setProperty('--metric-settled',staticState?1:smooth((time-1.70)/.70));
|
||||
},{...options,duration:Math.max(2.65,bars.length*.018+1.95,digits.length*.065+2.17)});
|
||||
return {replay:()=>clock.restart(),destroy(){clock.destroy();clean();value.textContent=formatted;element.style.removeProperty('--metric-settled');bars.forEach(b=>b.style.removeProperty('--bar-growth'));}};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
.particle-ring {
|
||||
& .ring-mist { position:absolute; inset:0 -15%; background:radial-gradient(ellipse at 37% 33% in oklab,color-mix(in oklab,var(--_card-accent-2) 17%,transparent),transparent 64%); mask-image:linear-gradient(in oklab,transparent,#000 12%,#000 82%,transparent); }
|
||||
& .ring-canvas { position:absolute; inset:0; }
|
||||
& .shop-conversation { position:absolute; top:21%; left:10%; width:80%; z-index:2; font-size:11.5px; line-height:1.55; }
|
||||
& .shop-message { width:88%; margin:0 0 13px; background:var(--card-panel); padding:11px 13px; border:1px solid var(--card-line); border-radius:calc(var(--_card-radius)*.7); box-shadow:0 5px 14px #3023520a; }
|
||||
& .customer { margin-left:12%; }
|
||||
& .shop-products { display:grid; grid-template-columns:1fr 1fr; padding:8px; gap:8px; background:var(--card-panel); border:1px solid var(--card-line); border-radius:calc(var(--_card-radius)*.85); box-shadow:0 18px 35px -15px #2523422b,0 3px 10px #2523420a; }
|
||||
& .shop-product { display:flex; flex-direction:column; border:1px solid var(--card-line); border-radius:calc(var(--_card-radius)*.45); overflow:hidden; padding-bottom:8px;
|
||||
& strong,& span,& b { padding:0 9px; }
|
||||
& strong { margin-top:8px; font-size:11px; font-weight:550; }
|
||||
& span { font-size:9px; color:var(--card-muted); }
|
||||
& b { margin-top:3px; font-size:10px; font-weight:600; }
|
||||
}
|
||||
& .garment { background:var(--card-wash); height:116px; display:grid; place-items:center; & svg { width:87%; height:96%; } }
|
||||
& .shop-cta { grid-column:1/-1; background:color-mix(in oklab,var(--card-accent) 11%,var(--card-surface)); color:var(--_card-accent-ink); padding:11px; text-align:center; border-radius:calc(var(--_card-radius)*.4); font-weight:550; & > span:last-child { margin-left:10px; } }
|
||||
}
|
||||
:where([data-theme="dark"]) .particle-ring .garment, .particle-ring[data-theme="dark"] .garment { background:color-mix(in oklab,var(--card-wash) 35%,#bdcde5); }
|
||||
@@ -0,0 +1,16 @@
|
||||
<article class="card portrait particle-ring" data-card="particle-ring" data-motion="auto">
|
||||
<h2 class="card-heading" data-text="title">从一句想法,<br>到一件心仪好物</h2>
|
||||
|
||||
<div class="card-stage">
|
||||
<div class="ring-mist"></div><canvas class="ring-canvas" aria-hidden="true"></canvas>
|
||||
<div class="shop-conversation">
|
||||
<p class="shop-message customer" data-text="prompt">周末去海边,想穿得舒服一点。<br>有什么适合我的推荐?</p>
|
||||
<p class="shop-message assistant" data-text="reply">轻盈、好搭,也能应对海风。<br>这两件刚刚好。</p>
|
||||
<div class="shop-products">
|
||||
<div class="shop-product"><div class="garment"><svg viewBox="0 0 100 100" aria-label="海蓝棉质T恤"><path d="M34 15 21 20 8 41 23 50 29 41 29 86Q50 91 71 86L71 41 77 50 92 41 79 20 66 15Q50 26 34 15Z" fill="#096bc7"/><path d="M34 15Q50 35 66 15L63 17Q50 28 37 17Z" fill="#0357a8"/><path d="M30 43 35 39 37 84 30 86Z" fill="#0056aa" opacity=".6"/></svg></div><strong data-text="items.0.name">海岸棉质短袖</strong><span data-text="items.0.detail">海蓝 · M</span><b data-text="items.0.value">¥ 129</b></div>
|
||||
<div class="shop-product"><div class="garment"><svg viewBox="0 0 100 100" aria-label="藏青连帽衫"><path d="M34 15Q50 4 66 15L69 27 81 32 90 82 76 87 69 51 70 90Q50 94 30 90L31 51 24 87 10 82 19 32 31 27Z" fill="#142746"/><path d="M34 15Q28 40 50 43Q72 40 66 15L61 18Q50 12 39 18Z" fill="#0a172d"/><path d="M37 19Q50 34 63 19M50 43v13" fill="none" stroke="#354b6c" stroke-width="2"/><path d="M34 48 37 87 45 88 41 48Z" fill="#2e4261" opacity=".8"/><path d="M36 68Q50 62 64 68L61 80H39Z" fill="#102039"/></svg></div><strong data-text="items.1.name">轻风连帽外套</strong><span data-text="items.1.detail">藏青 · M</span><b data-text="items.1.value">¥ 249</b></div>
|
||||
<div class="shop-cta"><span data-text="action">一起带去海边</span> <span>↗</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,42 @@
|
||||
import {random, simplex, loop, fitCanvas, glProgram, glNoise, glColor, watchColors, shell, clamp, mix, mixColor, rgb, TAU,staticMotion} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const cleanShell=shell(element,options);
|
||||
const garmentGradient=element.querySelector('linearGradient');
|
||||
if(garmentGradient){const old=garmentGradient.id,id='garment-'+crypto.randomUUID();garmentGradient.id=id;for(const path of element.querySelectorAll('svg [fill]'))if(path.getAttribute('fill')===`url(#${old})`)path.setAttribute('fill',`url(#${id})`);}
|
||||
let canvas = element.querySelector('canvas'), palette, w = 1, h = 1, dpr = 1;
|
||||
const rng=random(91), count=matchMedia('(max-width: 650px)').matches?8000:16000;
|
||||
const data=Array.from({length:count},()=>[rng()*TAU,rng(),.6+Math.pow(rng(),3)*3.4,.25+.75*Math.pow(rng(),1.3)]);
|
||||
const vertex=`precision highp float;attribute vec4 a_seed;uniform vec2 u_size,u_mouse;uniform float u_time,u_dpr,u_active;varying float v_alpha;varying float v_y;${glNoise}
|
||||
void main(){vec2 unit=vec2(cos(a_seed.x),sin(a_seed.x));float r=u_size.x*.59;
|
||||
float fine=simplex(unit*45.+a_seed.y*15.+u_time*.075),coarse=simplex(unit*8.+a_seed.y*4.+u_time*.035);
|
||||
float coreWidth=mix(.008,.030,smoothstep(-.6,.6,simplex(unit*2.2+vec2(u_time*.14,-u_time*.10))));
|
||||
float scatter=a_seed.y<.6?(fine*.75+coarse*.25)*coreWidth*r:(fine*150.+coarse*45.)*pow((a_seed.y-.6)/.4,2.4);
|
||||
vec2 base=vec2(u_size.x*.5,u_size.y*.43)+unit*r;
|
||||
float recover=(1.-smoothstep(0.,u_size.x*.29,distance(base,u_mouse)))*u_active;
|
||||
vec2 p=base+unit*scatter*(1.-recover);
|
||||
gl_Position=vec4(p.x/u_size.x*2.-1.,1.-p.y/u_size.y*2.,0.,1.);gl_PointSize=a_seed.z*u_dpr;v_alpha=a_seed.w;v_y=clamp(p.y/u_size.y,0.,1.);
|
||||
}`;
|
||||
const fragment=`precision mediump float;uniform vec3 u_a,u_b;varying float v_alpha,v_y;${glColor} void main(){float r=length(gl_PointCoord-.5);float alpha=(1.-smoothstep(.15,.5,r))*v_alpha;gl_FragColor=vec4(pigment(u_b,u_a,v_y),alpha);}`;
|
||||
let gpu=glProgram(canvas,vertex,fragment),ctx=null;
|
||||
if(!gpu){ const replacement=canvas.cloneNode();canvas.replaceWith(replacement);canvas=replacement;ctx=canvas.getContext('2d'); }
|
||||
else gpu.attribute('a_seed',data.flat(),4);
|
||||
let mouse=[-1000,-1000],target=[-1000,-1000],active=0,targetActive=0;
|
||||
const still=staticMotion(element,options);
|
||||
const noise=simplex(77);
|
||||
const draw=(time,dt)=>{
|
||||
if(!palette)return;
|
||||
if(still())active=targetActive=0;
|
||||
if(dt){mouse=mouse.map((n,i)=>mix(n,target[i],1-Math.exp(-dt*5.6)));active=mix(active,targetActive,1-Math.exp(-dt*3.2));}
|
||||
({w,h,dpr}=fitCanvas(canvas));
|
||||
if(gpu){const {gl}=gpu;gl.viewport(0,0,canvas.width,canvas.height);gl.clearColor(0,0,0,0);gl.clear(gl.COLOR_BUFFER_BIT);gl.useProgram(gpu.program);gl.enable(gl.BLEND);gl.blendFuncSeparate(gl.SRC_ALPHA,palette.dark?gl.ONE:gl.ONE_MINUS_SRC_ALPHA,gl.ONE,gl.ONE_MINUS_SRC_ALPHA);
|
||||
gl.uniform2f(gpu.uniform('u_size'),w,h);gl.uniform2f(gpu.uniform('u_mouse'),...mouse);gl.uniform1f(gpu.uniform('u_active'),active);gl.uniform1f(gpu.uniform('u_time'),time+12);gl.uniform1f(gpu.uniform('u_dpr'),dpr);gl.uniform3fv(gpu.uniform('u_a'),palette.a);gl.uniform3fv(gpu.uniform('u_b'),palette.b);gl.drawArrays(gl.POINTS,0,count);
|
||||
}else{ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,w,h);for(const [a,s,size,alpha]of data){const x=Math.cos(a),y=Math.sin(a),base=w*.59,f=noise(x*45+s*15+1,y*45+s*15+1),c=noise(x*8+s*4,y*8+s*4),n=clamp((noise(x*2.2+1.68,y*2.2-1.2)+.6)/1.2),width=mix(.008,.030,n*n*(3-2*n)),r=base+(s<.6?(f*.75+c*.25)*width*base:(f*150+c*45)*((s-.6)/.4)**2.4);const px=w*.5+x*r,py=h*.43+y*r;ctx.fillStyle=rgb(mixColor(palette.b,palette.a,clamp(py/h)),alpha);ctx.beginPath();ctx.arc(px,py,size*.5,0,TAU);ctx.fill();}}
|
||||
};
|
||||
const clock=loop(element,draw,{...options,continuous:!!gpu,settled:()=>Math.abs(active-targetActive)<.001});
|
||||
const off=watchColors(element,p=>{palette=p;clock.invalidate();});
|
||||
const ro=new ResizeObserver(()=>clock.invalidate());ro.observe(element.querySelector('.card-stage'));
|
||||
const move=e=>{if(e.pointerType==='touch' || still())return;const r=canvas.getBoundingClientRect();target=[e.clientX-r.left,e.clientY-r.top];targetActive=1;};
|
||||
const leave=()=>targetActive=0;element.addEventListener('pointermove',move);element.addEventListener('pointerleave',leave);element.addEventListener('pointercancel',leave);
|
||||
|
||||
return {destroy(){clock.destroy();off();ro.disconnect();gpu?.destroy();cleanShell();element.removeEventListener('pointermove',move);element.removeEventListener('pointerleave',leave);element.removeEventListener('pointercancel',leave);}};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
.card.portrait-story {
|
||||
--_card-finish-base:transparent; --_card-material-reflection:0;
|
||||
position:relative; display:grid; grid-template-columns:minmax(0,1fr); grid-template-rows:auto minmax(170px,1fr) auto;
|
||||
padding:26px; aspect-ratio:4/5; min-height:auto; color:#fff; background:#171a20; border:0;
|
||||
& .story-photo { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; object-position:50% 32%; filter:grayscale(1); transform:scale(1.025); transition:transform 1s cubic-bezier(.2,.7,.2,1); }
|
||||
& .story-tint { position:absolute; inset:0; background:var(--card-accent); mix-blend-mode:multiply; opacity:.62; transition:opacity .8s; }
|
||||
& .story-shade { position:absolute; inset:0; background:linear-gradient(180deg,#0003 0%,transparent 35%,#080a1052 55%,#080a10ed 100%); }
|
||||
& .story-brand { position:relative; z-index:2; grid-row:1; margin:0; font-size:19px; line-height:1.2; font-weight:550; letter-spacing:-.06em; overflow-wrap:anywhere; }
|
||||
& .story-copy { position:relative; z-index:2; grid-row:3; min-width:0; }
|
||||
& > .card-finish { z-index:1; }
|
||||
& blockquote { margin:0 0 22px; font-family:"Songti SC","Noto Serif CJK SC",Georgia,serif; font-size:clamp(23px,2.2vw,30px); line-height:1.45; letter-spacing:-.03em; }
|
||||
& .story-copy p { display:flex; flex-wrap:wrap; gap:4px 12px; margin:0; font-size:11px; line-height:1.6; color:#ffffffb8; }
|
||||
&:is(:hover,:focus-within) .story-photo { transform:scale(1); }
|
||||
&:is(:hover,:focus-within) .story-tint { opacity:.32; }
|
||||
}
|
||||
@media(prefers-reduced-motion:reduce){.portrait-story *{transition:none!important;}}
|
||||
@@ -0,0 +1,7 @@
|
||||
<article class="card portrait-story" data-card="portrait-story" style="--card-accent:#5865c7">
|
||||
<img class="story-photo" src="portrait-story/images/editorial-portrait.jpg" alt="窗边的创作者,黑白编辑摄影" data-image="image">
|
||||
<div class="story-tint" aria-hidden="true"></div><div class="story-shade" aria-hidden="true"></div>
|
||||
<h2 class="story-brand" data-text="brand">morrow / journal</h2>
|
||||
<div class="story-copy"><blockquote data-text="quote">让工具退后一步,
|
||||
把空间留给真正的创作。</blockquote><p><span data-text="author">创作手记</span><span data-text="role">关于工具与创作</span></p></div>
|
||||
</article>
|
||||
@@ -0,0 +1,5 @@
|
||||
import {shell} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,options);
|
||||
return {destroy(){clean();}};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Editorial portrait
|
||||
|
||||
`editorial-portrait.jpg` is an AI-generated fictional person, created with the built-in imagegen tool on 2026-10-05. The portrait and its attribution are fictional editorial examples. The gallery uses editorial sample copy, with text and attribution rendered separately in HTML.
|
||||
|
||||
The original generated PNG is not shipped; the JPEG was resized to an 850px maximum dimension for local embedding.
|
||||
|
||||
Prompt:
|
||||
|
||||
> Use case: photorealistic-natural. Asset type: editorial portrait photograph for a reusable portrait story card in a design component gallery. Create an original fictional female creative director around 40, short wavy dark hair, textured charcoal knit shirt, natural confident thoughtful expression, looking slightly away from the camera. Vertical 3:4 framing, head and shoulders occupying upper two-thirds, face centered near x55% y32%; bottom quarter falls into a dark shirt and deep studio shadows as quiet space for HTML text overlaid later. Monochrome black and white film photograph, very fine grain, imperfect natural skin texture, soft window key light from left, restrained rich tonal depth, blurred architecture studio background. Documentary editorial photograph, quiet and tactile, not a glossy corporate headshot. No text, no letters, no logo, no watermark, no frame, no interface, no color overlay. Original person, no resemblance to any famous person or reference website customer.
|
||||
|
||||
`transparent_background: false`. The photo is a fixed raster asset; a separate CSS tint uses the user's accent color. Hosts can replace it through `content.image` and provide their own alt text.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 205 KiB |
@@ -0,0 +1,27 @@
|
||||
.promo-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:16px; }
|
||||
.promo-band {
|
||||
display:flex; align-items:flex-start; min-height:180px; aspect-ratio:auto; padding:30px 24px;
|
||||
text-decoration:none; background:var(--card-wash);
|
||||
& .promo-copy { position:relative; z-index:2; width:65%; }
|
||||
& p { margin:0 0 20px; color:var(--card-muted); font-size:16px; line-height:1.5; letter-spacing:-.025em; }
|
||||
& strong { color:var(--card-ink); font-weight:650; }
|
||||
& .promo-link { color:color-mix(in oklab,var(--_card-accent-ink) 85%,var(--card-ink)); font-size:16px; font-weight:600; }
|
||||
& .promo-link > span:last-child { display:inline-block; margin-left:4px; transition:transform .3s ease; }
|
||||
&:is(:hover,:focus-visible) .promo-link > span:last-child { transform:translateX(4px); }
|
||||
& .promo-art { position:absolute; right:0; top:0; bottom:0; width:43%; pointer-events:none; }
|
||||
&[data-art="prism"] .promo-art {
|
||||
width:44%; clip-path:polygon(43% 0,100% 0,100% 100%,0 100%);
|
||||
background:
|
||||
radial-gradient(ellipse at 93% 1% in oklab,var(--_card-accent-2),transparent 69%),
|
||||
radial-gradient(ellipse at 4% 97% in oklab,color-mix(in oklab,var(--card-accent) 80%,#4812bd),transparent 74%),
|
||||
linear-gradient(155deg in oklab,var(--_card-accent-2),var(--card-accent) 68%);
|
||||
&::after { content:""; position:absolute; inset:0; background:linear-gradient(119deg,var(--card-wash) 11%,transparent 37%); }
|
||||
}
|
||||
& svg { width:100%; height:100%; display:block; }
|
||||
& .facet-back { fill:var(--_card-accent-3); }
|
||||
& .facet-top { fill:oklch(from var(--_card-accent-3) calc(l - .01) calc(c * 1.45) calc(h + 12)); }
|
||||
& .facet-front { fill:oklch(from var(--_card-accent-3) calc(l + .13) calc(c * .95) calc(h + 36)); }
|
||||
& .facet-side { fill:oklch(from var(--_card-accent-3) calc(l + .07) calc(c * 1.3) calc(h + 30)); }
|
||||
@media(max-width:650px) { min-height:204px; padding:25px 20px; & .promo-copy { width:73%; } & p { font-size:15px; } & .promo-art { width:32%; opacity:.85; } &[data-art="prism"] .promo-art { width:34%; } }
|
||||
}
|
||||
@media(max-width:850px) { .promo-grid { grid-template-columns:1fr; } }
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="promo-grid">
|
||||
<a class="card promo-band" data-card="promo-band" data-art="prism" data-example="promo-prism" data-href="href" data-motion="auto" href="#showcase">
|
||||
<div class="promo-copy"><p><strong data-text="title">创作者计划。</strong> <span data-text="description">让灵感获得支持,与志同道合的人一起,把好想法变成作品。</span></p><span class="promo-link"><span data-text="action">探索计划</span> <span aria-hidden="true">→</span></span></div>
|
||||
<div class="promo-art" aria-hidden="true"></div>
|
||||
</a>
|
||||
<a class="card promo-band" data-card="promo-band" data-art="facets" data-example="promo-facets" data-href="href" data-motion="auto" href="#showcase">
|
||||
<div class="promo-copy"><p><strong data-text="title">从这里出发。</strong> <span data-text="description">把创作所需的工具和资源放在一起,专注完成你的下一件作品。</span></p><span class="promo-link"><span data-text="action">开始创作</span> <span aria-hidden="true">→</span></span></div>
|
||||
<div class="promo-art" aria-hidden="true"><svg viewBox="0 0 400 280" preserveAspectRatio="none"><path class="facet-back" d="M135 0H400V280H0Z"/><path class="facet-top" d="M135 0H400V76L280 280Z"/><path class="facet-front" d="M110 50 280 280H0Z"/><path class="facet-side" d="M400 76V280H280Z"/></svg></div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
import {shell} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,options);
|
||||
// A real link: content and destination stay in HTML; no extra geometry knobs.
|
||||
|
||||
return {destroy(){clean();}};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.card.responsive-preview {
|
||||
--_card-stage-track-min:320px; aspect-ratio:auto;
|
||||
& .card-stage { display:grid; grid-template-rows:minmax(245px,1fr) auto; }
|
||||
& .size-scene { position:relative; min-height:245px; overflow:hidden; background:radial-gradient(ellipse at 50% 70%,var(--card-wash),transparent 65%); }
|
||||
& .size-frame { position:absolute; overflow:hidden; border:1px solid color-mix(in oklab,var(--card-ink) 16%,var(--card-panel)); border-radius:10px; background:var(--card-panel); box-shadow:0 6px 14px #18243209; }
|
||||
& .size-frame-bar { height:29px; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid var(--card-line); color:var(--card-muted); font:9px ui-monospace,monospace; padding:0 10px; }
|
||||
& .size-frame-bar i { width:4px; height:4px; background:var(--card-line); border-radius:50%; box-shadow:7px 0 var(--card-line),14px 0 var(--card-line); }
|
||||
& .size-frame > svg { display:block; width:100%; height:calc(100% - 29px); }
|
||||
& .size-frame-0 { width:59%; height:190px; left:20%; top:10px; transform:translate(calc(var(--spread,1)*12%),calc(var(--spread,1)*-1px)); }
|
||||
& .size-frame-1 { width:47%; height:154px; left:26%; top:44px; transform:translate(calc(var(--spread,1)*-18%),calc(var(--spread,1)*6px)); opacity:var(--spread,1); }
|
||||
& .size-frame-2 { width:27%; height:137px; left:36%; top:61px; transform:translate(calc(var(--spread,1)*-87%),calc(var(--spread,1)*33px)); opacity:var(--spread,1); }
|
||||
& .preview-land-back { fill:color-mix(in oklab,var(--card-accent) 17%,var(--card-panel)); }
|
||||
& .preview-land-front { fill:color-mix(in oklab,var(--card-accent) 32%,var(--card-panel)); }
|
||||
& .preview-contours { stroke:color-mix(in oklab,var(--card-accent) 45%,var(--card-panel)); stroke-width:1; }
|
||||
& .preview-sun { fill:color-mix(in oklab,var(--_card-accent-2) 36%,var(--card-panel)); }
|
||||
& .size-toolbar { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:12px; padding:18px 24px 24px; font-size:11px; line-height:1.5; color:var(--card-muted); }
|
||||
& .size-toggle { display:flex; gap:12px; align-items:center; background:transparent; color:var(--_card-accent-ink); border:0; padding:0; font:inherit; cursor:pointer; white-space:nowrap; }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<article class="card responsive-preview" data-card="responsive-preview" style="--card-accent:#5865c7">
|
||||
<h2 class="card-heading" data-text="title">一份内容,
|
||||
在不同屏幕上自然展开。</h2>
|
||||
<div class="card-stage"><div class="size-scene"><div class="size-frame size-frame-0"><div class="size-frame-bar"><i></i><span data-text="labels.0">原始画面</span></div><svg viewBox="0 0 300 180" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><path class="preview-land-back" d="M0 98C54 85 56 30 118 42s76 75 182-10V180H0Z"/><path class="preview-land-front" d="M0 147C62 48 114 127 169 75s83 27 131 0V180H0Z"/><g class="preview-contours" fill="none"><path d="M-10 160C62 61 114 140 169 88s83 27 145-6M-10 174C62 75 114 154 169 102s83 27 145-6M-10 188C62 89 114 168 169 116s83 27 145-6M-10 202C62 103 114 182 169 130s83 27 145-6"/></g><circle cx="244" cy="37" r="14" class="preview-sun"/></svg></div><div class="size-frame size-frame-1"><div class="size-frame-bar"><i></i><span data-text="labels.1">宽屏</span></div><svg viewBox="0 0 300 180" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><path class="preview-land-back" d="M0 98C54 85 56 30 118 42s76 75 182-10V180H0Z"/><path class="preview-land-front" d="M0 147C62 48 114 127 169 75s83 27 131 0V180H0Z"/><g class="preview-contours" fill="none"><path d="M-10 160C62 61 114 140 169 88s83 27 145-6M-10 174C62 75 114 154 169 102s83 27 145-6M-10 188C62 89 114 168 169 116s83 27 145-6M-10 202C62 103 114 182 169 130s83 27 145-6"/></g><circle cx="244" cy="37" r="14" class="preview-sun"/></svg></div><div class="size-frame size-frame-2"><div class="size-frame-bar"><i></i><span data-text="labels.2">手机</span></div><svg viewBox="0 0 300 180" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><path class="preview-land-back" d="M0 98C54 85 56 30 118 42s76 75 182-10V180H0Z"/><path class="preview-land-front" d="M0 147C62 48 114 127 169 75s83 27 131 0V180H0Z"/><g class="preview-contours" fill="none"><path d="M-10 160C62 61 114 140 169 88s83 27 145-6M-10 174C62 75 114 154 169 102s83 27 145-6M-10 188C62 89 114 168 169 116s83 27 145-6M-10 202C62 103 114 182 169 130s83 27 145-6"/></g><circle cx="244" cy="37" r="14" class="preview-sun"/></svg></div></div><div class="size-toolbar"><span data-text="description">保留主体,重新安排取景。</span><button type="button" class="size-toggle" aria-pressed="true">收起尺寸 <span aria-hidden="true">↗</span></button></div></div>
|
||||
</article>
|
||||
@@ -0,0 +1,7 @@
|
||||
import {shell,loop,staticMotion,smooth} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,options),still=staticMotion(element,options),button=element.querySelector('.size-toggle');let value=0,target=1,clock,destroyed=false;
|
||||
const animate=next=>{const from=value;target=next;clock?.destroy();button.setAttribute('aria-pressed',String(!!target));button.firstChild.textContent=target?'收起尺寸 ':'比较尺寸 ';clock=loop(element,time=>{value=still()?target:from+(target-from)*smooth(time/.9);element.style.setProperty('--spread',value);},{...options,duration:.9});};
|
||||
const toggle=()=>animate(target?0:1);button.addEventListener('click',toggle);animate(1);
|
||||
return{replay(){if(!destroyed){value=0;animate(1);}},destroy(){if(destroyed)return;destroyed=true;clock.destroy();button.removeEventListener('click',toggle);clean();element.style.removeProperty('--spread');}};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Content values are plain text. A line break is content, not injected markup.
|
||||
export function contentValue(content, path) {
|
||||
return path.split('.').reduce((value,key)=>value!=null && Object.hasOwn(value,key)?value[key]:undefined,content);
|
||||
}
|
||||
function url(value, image=false) {
|
||||
if(typeof value!=='string' || !value.trim())return null;
|
||||
try {
|
||||
const protocol=new URL(value,document.baseURI).protocol;
|
||||
if(['http:','https:','file:'].includes(protocol) || (!image && ['mailto:','tel:'].includes(protocol)) || (image && (protocol==='blob:' || /^data:image\/(png|jpeg|webp|gif|svg\+xml)[;,]/i.test(value))))return value;
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
export function applyContent(element, content={}) {
|
||||
if(!content || typeof content!=='object')return;
|
||||
for(const node of element.querySelectorAll('[data-text]')) {
|
||||
const value=contentValue(content,node.dataset.text);
|
||||
if(value!==undefined && value!==null)node.textContent=Array.isArray(value)?value.join('\n'):String(value);
|
||||
}
|
||||
for(const node of [element,...element.querySelectorAll('[data-href]')]) {
|
||||
if(!node.hasAttribute('data-href'))continue;
|
||||
const value=contentValue(content,node.dataset.href);
|
||||
if(value===undefined)continue;
|
||||
const href=url(value);if(href)node.setAttribute('href',href);else node.removeAttribute('href');
|
||||
}
|
||||
for(const node of element.querySelectorAll('[data-image]')) {
|
||||
const value=contentValue(content,node.dataset.image);
|
||||
if(!value)continue;
|
||||
const src=url(typeof value==='string'?value:value.src,true);
|
||||
if(src){node.src=src;if(typeof value==='object' && value.alt!==undefined)node.alt=String(value.alt);}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/* Light, edge and contact shadow are authored together for each material. */
|
||||
:where(.card,.showcase-card) {
|
||||
--_card-shadow-contact:0 1px 2px #18253808,0 3px 6px -3px #18253812;
|
||||
--_card-shadow-lifted:0 2px 5px -3px #18253812,0 10px 20px -12px #18253820;
|
||||
--_card-shadow-glass:0 1px 3px #18253805,0 7px 16px -10px #18253814;
|
||||
--_card-shadow-solid:0 1px 1px #18253809,0 4px 8px -5px #18253816;
|
||||
--_card-material-shadow:0 0 #0000;
|
||||
--_card-metal-base:color-mix(in oklab,var(--card-surface) 95%,var(--card-muted));
|
||||
--_card-metal-band:linear-gradient(180deg,#ffffff66,transparent 36%,#17263b05);
|
||||
--_card-metal-rim:inset 0 1px 0 #ffffff70,inset 0 -1px 0 #17223514;
|
||||
--_card-glass-rim:inset 0 1px 0 #ffffff80,inset 1px 0 0 #ffffff29,inset 0 -1px 0 #35496d0d;
|
||||
--_card-solid-rim:inset 0 1px 0 #ffffff80,inset 0 -1px 0 color-mix(in oklab,var(--card-ink) 11%,var(--card-surface));
|
||||
--_card-finish-noise:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='.025'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cpath filter='url(%23n)' d='M0 0h160v160H0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
:where([data-theme="dark"]) :where(.card,.showcase-card),:where(.card,.showcase-card)[data-theme="dark"] {
|
||||
--_card-shadow-contact:0 1px 2px #0003,0 3px 6px -3px #0004;
|
||||
--_card-shadow-lifted:0 2px 5px -3px #0005,0 10px 20px -12px #0008;
|
||||
--_card-shadow-glass:0 1px 3px #0002,0 7px 16px -10px #0006;
|
||||
--_card-shadow-solid:0 1px 1px #0004,0 4px 8px -5px #0006;
|
||||
--_card-metal-base:color-mix(in oklab,var(--card-surface) 93%,var(--card-muted));
|
||||
--_card-metal-band:linear-gradient(180deg,#ffffff07,transparent 36%,#0000000a);
|
||||
--_card-metal-rim:inset 0 1px 0 #ffffff1c,inset 0 -1px 0 #0003;
|
||||
--_card-glass-rim:inset 0 1px 0 #ffffff22,inset 1px 0 0 #ffffff0b,inset 0 -1px 0 #0002;
|
||||
--_card-solid-rim:inset 0 1px 0 #ffffff16,inset 0 -1px 0 #0005;
|
||||
}
|
||||
:is(.card,.showcase-card)[data-shadow="auto"] {--_card-outer-shadow:var(--_card-material-shadow);}
|
||||
:is(.card,.showcase-card)[data-shadow="none"] {--_card-outer-shadow:0 0 #0000;}
|
||||
:is(.card,.showcase-card)[data-shadow="contact"] {--_card-outer-shadow:var(--_card-shadow-contact);}
|
||||
:is(.card,.showcase-card)[data-shadow="lifted"] {--_card-outer-shadow:var(--_card-shadow-lifted);}
|
||||
.card,.showcase-card > .showcase-window {box-shadow:var(--_card-outer-shadow,var(--_card-material-shadow));}
|
||||
.card-finish {display:none;position:absolute;inset:0;z-index:0;border-radius:inherit;pointer-events:none;user-select:none;box-shadow:var(--_card-material-rim,none);}
|
||||
.card-finish::before {content:"";position:absolute;inset:0;border-radius:inherit;background-image:var(--_card-finish-art,none);background-size:var(--_card-finish-size,auto);opacity:var(--_card-material-reflection,1);}
|
||||
:is(.card,.showcase-card)[data-material="metal"] {
|
||||
--_card-material-rim:var(--_card-metal-rim);
|
||||
--_card-material-shadow:var(--_card-shadow-contact);
|
||||
& .card-finish {display:block;background-color:var(--_card-finish-base,var(--_card-metal-base));--_card-finish-art:var(--_card-metal-band);}
|
||||
}
|
||||
:is(.card,.showcase-card)[data-material="glass"] {
|
||||
--_card-material-rim:var(--_card-glass-rim);
|
||||
--_card-material-shadow:var(--_card-shadow-glass);
|
||||
& .card-finish {display:block;--_card-finish-art:linear-gradient(126deg,#ffffff24,transparent 34% 65%,#ffffff0b);}
|
||||
}
|
||||
/* Keep a readable fallback when a host cannot blur the backdrop. */
|
||||
.card[data-material="glass"],.showcase-card[data-material="glass"] > .showcase-window {background:color-mix(in oklab,var(--card-surface) 90%,transparent);}
|
||||
@supports (backdrop-filter:blur(1px)) {
|
||||
.card[data-material="glass"],.showcase-card[data-material="glass"] > .showcase-window {background:color-mix(in oklab,var(--card-surface) 62%,transparent);backdrop-filter:blur(18px) saturate(1.12);}
|
||||
}
|
||||
:is(.card,.showcase-card)[data-material="solid"] {
|
||||
--_card-material-rim:var(--_card-solid-rim);
|
||||
--_card-material-shadow:var(--_card-shadow-solid);
|
||||
& .card-finish {display:block;background-color:var(--_card-finish-base,color-mix(in oklab,var(--card-surface) 98%,var(--card-accent)));--_card-finish-art:var(--_card-finish-noise),linear-gradient(160deg,#ffffff18,transparent 35%,#15203406);--_card-finish-size:160px 160px,100% 100%;}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Generated from Natural Earth 110m land (public domain).
|
||||
export const LAND_WIDTH = 360;
|
||||
export const LAND_HEIGHT = 180;
|
||||
export const LAND_BITS = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADg/wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADA/v8/AOD//z8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+///D/////5//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAH/j/+//////8PAAAAgB0AAHgAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAOD//wP8//////8BAACAfx4AAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAA4+I73//n///////8AAAAAfwAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAAAAAOAQACDwP4D///////8BAAAAPAQAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAADzAgPP5H8D//////38AAAAAAAAAAAAA4AEAAAD+DwAAAAAAAAAAAAAAAAAAAIC/g7PfBwAA/v////8AAAAAAAAAAADgBwAAwP//PwAA4B8AAAAAAAAAAAAAAAAMAAD8AgAA+P////8AAAAAAAAAAABwAAAA/P//AwAAAAAAAAAAAAAAAAAAAP8Ahvc5OwAA8P///38AAAAAAAAAAAAcAADg////34cHAAcAAAAAAAAAAAAAgN9/xzf8RwAA4P///z8AAAAAAAAAAAAPAB7g//////8fgB8AAAAAAQAAAAAAAO//B3f8/wsA4P///z8AAAAAAAAAAAAPAG////////8f2f8HAAAAAAD4HwAAAAD8H/D4/38A4P///xMAAAAAAPAHAAAAgN//////////////DwAAAAD+///h/4//H+/Bg/8BwP7//w8AAAAAwP8fAAAAj9///////////////88/AeD///////8DB8TPB/4AAP///wAAAAAA8P//AwDjf77/////////////////DwD////////////vh/kPwP//BwAAAAAA/P//H/P//5//////////////////34H8////////////Afw/gP//AQAAAAAA/v+fH////+///////////////////vD///////////9fAPwYgP8PAOA/AAAA/+N/8P//////////////////////QID4///////////Pw/0DAP8HAMAfAACA//F//v////////////////////8/AAP4///////////BBPAHAP4HAAAAAADg//z///////////////////////9/AID///////////8AQ8ABAPwBAAAAAAD8P/7///////////////////////9fAMD//////////z8AwA8AAPgBAAAAAAD+H/7/////////////////////Of8BAID///z//////z8AwD8AAMABAAAAAAD+P/z///////////////////9/wH8AAAD8MwD//////x8AwH8MAAAAAAAAAAD+P2D///////////////////95cAAAAACAAwD8/////38AwP8eAAAAAAAAgAGcH/D//////////////////wEAOAAAAADADgDA/////38AgP8/AAAAAAAAwANAH/T//////////////////wAAfgAAAAAwAAAA//////8PgP8/AAAAAAAAwAFwD/7/////////////////PwAAfwAAAAAGAAAA//////8/wP//AAAAAAAAwAOwA/7/////////////////DwCAPwAAAIAAAAAA/v//////8///BwAAAAAAOAcgcP//////////////////HwAAHwAAAAAAAACA+P//////4///DwAAAAAAOA74/////////////////////wUADwAAAAAAAAAA8P//////4///DwAAAAAAGD///////////////////////wUAAwAAAAAAAAAA8P//////7///FwAAAAAAAB///////////////////////wUAAgAAAAAAAAAA0P//////////DAAAAAAAgOH//////////////////////w0AAAAAAAAAAAAAYP////////8xLAAAAAAAAPz//////////////////////wwAAAAAAAAAAAAAAP7//////38HfgAAAAAAgP///////////////////////wQAAAAAAAAAAAAAAP3///////8HUAAAAAAAAP7/////////////////////fwQAAAAAAAAAAAAAAP////////+XAAAAAAAAAPz///93/P//////////////PwwAAAAAAAAAAAAAAP////////9/AAAAAAAAAPj//f9j/v//////////////HwAAAAAAAAAAAAAAAP////////8MAAAAAAAAAPj/+P8B/P//////////////DwQAAAAAAAAAAAAAAP///////z8AAAAAAAAAcPzH8/8A8P//////////////Bx4AAAAAAAAAAAAAAP///////x8AAAAAAAAA+H+Aw/8AwP////////////9/AA8AAAAAAAAAAAAAAP///////x8AAAAAAAAA+D8Aj//w4f////////////8/AAAAAAAAAAAAAAAAAP///////wMAAAAAAAAA+B8wuE/+/////////////98fAAMAAAAAAAAAAAAAAP///////wMAAAAAAAAA+A8wEMf//////////////0cOAAMAAAAAAAAAAAAAAP7//////wEAAAAAAAAA+A8AAI7//////////////wMOAAEAAAAAAAAAAAAAAP7//////wAAAAAAAAAA+AcABob//////////////zccwAEAAAAAAAAAAAAAAPz//////wAAAAAAAAAAQOB/AAQ2/////////////x8c8AEAAAAAAAAAAAAAAPj//////wAAAAAAAAAAQPx/AAAA/////////////w8Y/gEAAAAAAAAAAAAAAPD/////fwAAAAAAAAAA4P8/AAAA/////////////w+EGwAAAAAAAAAAAAAAAMD/////HwAAAAAAAAAA8P9/AACA/////////////x/AAwAAAAAAAAAAAAAAAID/////DwAAAAAAAAAA+P//BwaA/////////////x/AAAAAAAAAAAAAAAAAAID9////BwAAAAAAAAAA/P//D3+E/////////////z9AAAAAAAAAAAAAAAAAAAD5////BwAAAAAAAAAA/P//f////////////////x8AAAAAAAAAAAAAAAAAAADy/x8GBgAAAAAAAAAA/P/////v/8///////////z8AAAAAAAAAAAAAAAAAAAD0/w8ABgAAAAAAAAAA/v////+//4///////////z8AAAAAAAAAAAAAAAAAAADu/wcADgAAAAAAAACA//////8f/x/+/////////x8AAAAAAAAAAAAAAAAAAACI/wcALAAAAAAAAADA//////8//z/g/////////w8AAAAAAAAAAAAAAAAAAACQ/wcACAAAAAAAAADg//////9//r+A/////////wcAAAAAAAAAAAAAAAAAAAAQ/wMAAAAAAAAAAADg//////9//n8cgP///////ycAAAAAAAAAAAAAAAAAAAAA/gMAAAAAAAAAAADw////////+P9/AP///////xEAAAAAAAAAAAAAAAAAAAAA/AMAHQAAAAAAAADw////////+P//APz/f///fxAAAAAAAAAAAAAAAAAAAAAA+AcAYAAAAAAAAAD4////////+f9/APz/B///BgAAAAAAAAAAAAAAAAAAAAAA/AccgAEAAAAAAADw////////8f9/AOD/B/5/AAAAAAAAAAAAAAAAAQAAAAAA+A8eADgAAAAAAADw////////4f8/AOD/Afw/BgAAAAAAAAAAAAAAAAAAAAAA8J8PAPwCAAAAAADw////////4/8fAOD/APw/AgAAAAAAAAAAAAAAAAAAAAAAgP8PAAAAAAAAAADw////////x/8HAOB/APx/ADAAAAAAAAAAAAAAAAAAAAAAAP4PAAAAAAAAAAD4////////h/8BAOA/APz/ADAAAAAAAAAAAAAAAAAAAAAAAID/AAAAAAAAAAD4////////j/8AAMAPAMD/ATAAAAAAAAAAAAAAAAAAAAAAAAD/AQAAAAAAAAD4////////nx8AAMAPAMD/ATAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAD4////////vwcAAIAPAMD/AcAAAAAAAAAAAAAAAAAAAAAAAADgAQAAAAAAAAD4////////fwAAAIAPAMD8AQABAAAAAAAAAAAAAAAAAAAAAADAAAgAAAAAAADw////////f2AAAAAPAID4AUACAAAAAAAAAAAAAAAAAAAAAADAAe9TAAAAAADg/////////34AAAAPAEBwAAgAAAAAAAAAAAAAAAAAAAAAAAAAE+9/AAAAAADA/////////38AAAAXAEAgAAQCAAAAAAAAAAAAAAAAAAAAAAAAz///AAAAAACA/////////z8AAAASAMAAAIACAAAAAAAAAAAAAAAAAAAAAAAAyP//AQAAAACA/////////z8AAAAwAIAAAEAHA
Line truncated
|
||||
Binary file not shown.
@@ -0,0 +1,252 @@
|
||||
import {applyContent} from './content.js';
|
||||
export const TAU = Math.PI * 2;
|
||||
export const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
|
||||
export const mix = (a, b, t) => a + (b - a) * t;
|
||||
export const smooth = t => { t=clamp(t);return t*t*t*(t*(t*6-15)+10); };
|
||||
export function staticMotion(element, options={}) {
|
||||
const media=matchMedia('(prefers-reduced-motion: reduce)');
|
||||
return ()=>Number(options.speed)===0 || media.matches || element.closest('[data-motion]')?.dataset.motion==='off';
|
||||
}
|
||||
export function random(seed = 17) {
|
||||
return () => { seed |= 0; seed = seed + 0x6D2B79F5 | 0; let t = Math.imul(seed ^ seed >>> 15, 1 | seed); t ^= t + Math.imul(t ^ t >>> 7, 61 | t); return ((t ^ t >>> 14) >>> 0) / 4294967296; };
|
||||
}
|
||||
/* Original triangular-lattice 2D simplex implementation, deterministic gradients. */
|
||||
export function simplex(seed = 43) {
|
||||
const rng = random(seed), p = Array.from({length: 256}, (_, i) => i);
|
||||
for (let i = 255; i; i--) { const j = Math.floor(rng() * (i + 1)); [p[i], p[j]] = [p[j], p[i]]; }
|
||||
const perm = [...p, ...p], grads = Array.from({length: 8}, (_, i) => [Math.cos(i * TAU / 8), Math.sin(i * TAU / 8)]);
|
||||
return (x, y) => {
|
||||
const s = (x + y) * .3660254038, i = Math.floor(x + s), j = Math.floor(y + s), u = (i + j) * .2113248654;
|
||||
const x0 = x - i + u, y0 = y - j + u, a = x0 > y0 ? 1 : 0, b = 1 - a;
|
||||
const xs = [x0, x0 - a + .2113248654, x0 - .5773502692], ys = [y0, y0 - b + .2113248654, y0 - .5773502692];
|
||||
let n = 0;
|
||||
for (let k = 0; k < 3; k++) {
|
||||
const ii = i + (k === 1 ? a : k), jj = j + (k === 1 ? b : k), g = grads[perm[(ii & 255) + perm[jj & 255]] & 7];
|
||||
const h = Math.max(0, .5 - xs[k] * xs[k] - ys[k] * ys[k]);
|
||||
n += h ** 4 * (xs[k] * g[0] + ys[k] * g[1]);
|
||||
}
|
||||
return n * 70;
|
||||
};
|
||||
}
|
||||
export function colors(element) {
|
||||
const cs = getComputedStyle(element), c = document.createElement('canvas'); c.width = c.height = 1;
|
||||
const ctx = c.getContext('2d', {willReadFrequently: true});
|
||||
const read = name => {
|
||||
ctx.clearRect(0, 0, 1, 1); ctx.fillStyle = cs.getPropertyValue(name).trim(); ctx.fillRect(0, 0, 1, 1);
|
||||
return [...ctx.getImageData(0, 0, 1, 1).data].slice(0, 3).map(n => n / 255);
|
||||
};
|
||||
return {a: read('--card-accent'), b: read('--_card-accent-2'), c: read('--_card-accent-3'), surface: read('--card-surface'), dark: cs.getPropertyValue('--card-surface').trim() === '#14171e'};
|
||||
}
|
||||
export const rgb = (c, alpha = 1) => `rgba(${c.map(n => Math.round(n * 255)).join(',')},${alpha})`;
|
||||
export function mixColor(a, b, t) {
|
||||
const linear = c => c.map(n => n <= .04045 ? n / 12.92 : ((n + .055) / 1.055) ** 2.4);
|
||||
const lab = c => {const [r,g,b]=linear(c),l=Math.cbrt(.412221*r+.536333*g+.051446*b),m=Math.cbrt(.211903*r+.680700*g+.107397*b),s=Math.cbrt(.088302*r+.281719*g+.629979*b);return [.210454*l+.793618*m-.004072*s,1.977998*l-2.428592*m+.450594*s,.025904*l+.782772*m-.808676*s];};
|
||||
const A=lab(a),B=lab(b),[L,x,y]=A.map((n,i)=>mix(n,B[i],t)),l=(L+.396338*x+.215804*y)**3,m=(L-.105561*x-.063854*y)**3,s=(L-.089484*x-1.291486*y)**3;
|
||||
return [4.076742*l-3.307711*m+.230970*s,-1.268438*l+2.609757*m-.341319*s,-.004196*l-.703419*m+1.707615*s].map(n=>clamp(n<=.0031308?12.92*n:1.055*Math.max(0,n)**(1/2.4)-.055));
|
||||
}
|
||||
/* SVG has no OKLab interpolation mode. Dense OKLab stops preserve the palette. */
|
||||
export function svgStops(stops) {
|
||||
return stops.flatMap(([x,c,a=1],i)=>{
|
||||
if(i===stops.length-1)return [`<stop offset="${x}" stop-color="${c}" stop-opacity="${a}"/>`];
|
||||
const [nx,nc,na=1]=stops[i+1];
|
||||
return Array.from({length:8},(_,j)=>{const t=j/8,color=j?`color-mix(in oklab,${c} ${(1-t)*100}%,${nc})`:c;return `<stop offset="${x+(nx-x)*t}" stop-color="${color}" stop-opacity="${a+(na-a)*t}"/>`;});
|
||||
}).join('');
|
||||
}
|
||||
export const glColor = `
|
||||
vec3 lab(vec3 c){c=mix(c/12.92,pow((c+.055)/1.055,vec3(2.4)),step(vec3(.04045),c));vec3 lms=vec3(dot(c,vec3(.412221,.536333,.051446)),dot(c,vec3(.211903,.680700,.107397)),dot(c,vec3(.088302,.281719,.629979)));lms=pow(max(lms,0.),vec3(1./3.));return vec3(dot(lms,vec3(.210454,.793618,-.004072)),dot(lms,vec3(1.977998,-2.428592,.450594)),dot(lms,vec3(.025904,.782772,-.808676)));}
|
||||
vec3 srgb(vec3 c){float l=c.x+.396338*c.y+.215804*c.z,m=c.x-.105561*c.y-.063854*c.z,s=c.x-.089484*c.y-1.291486*c.z;l=l*l*l;m=m*m*m;s=s*s*s;vec3 v=max(vec3(4.076742*l-3.307711*m+.230970*s,-1.268438*l+2.609757*m-.341319*s,-.004196*l-.703419*m+1.707615*s),0.);return clamp(mix(v*12.92,1.055*pow(v,vec3(1./2.4))-.055,step(vec3(.0031308),v)),0.,1.);}
|
||||
vec3 pigment(vec3 a,vec3 b,float t){return srgb(mix(lab(a),lab(b),t));}
|
||||
`;
|
||||
export function watchColors(element, fn) {
|
||||
let signature = '';
|
||||
const update = () => {
|
||||
const cs = getComputedStyle(element), next = ['--card-accent','--_card-accent-2','--_card-accent-3','--card-surface'].map(n => cs.getPropertyValue(n)).join('|');
|
||||
if (next !== signature) { signature = next; fn(colors(element)); }
|
||||
};
|
||||
const observer = new MutationObserver(update);
|
||||
for (let p = element; p; p = p.parentElement) observer.observe(p, {attributes: true, attributeFilter: ['style','data-theme','class']});
|
||||
element.addEventListener('card-theme-change', update); update();
|
||||
return () => { observer.disconnect(); element.removeEventListener('card-theme-change', update); };
|
||||
}
|
||||
/* Timeline motion eases to rest; pointer damping keeps real dt until settled. */
|
||||
export function loop(element, draw, {continuous = true, speed = 1, duration = Infinity, settled = () => true} = {}) {
|
||||
let visible=false,hovered=element.matches(':hover'),id=0,elapsed=0,previous=0,destroyed=false,started=false;
|
||||
const rate=Number.isFinite(Number(speed)) && Number(speed)>=0?Number(speed):1;
|
||||
const media=matchMedia('(prefers-reduced-motion: reduce)');
|
||||
const finite=Number.isFinite(duration),mode=()=>element.closest('[data-motion]')?.dataset.motion || 'auto';
|
||||
let activity=mode()==='hover'?0:1;
|
||||
const allowed=()=>continuous && rate>0 && visible && !document.hidden && !media.matches && mode()!=='off';
|
||||
const running=()=>allowed() && elapsed<duration && (mode()!=='hover' || hovered || (finite && started) || (!finite && (activity>0 || !settled())));
|
||||
const stop=()=>{cancelAnimationFrame(id);id=0;previous=0;};
|
||||
const tick=now=>{
|
||||
id=0;
|
||||
if(destroyed || !running()){stop();element.dataset.paused='true';return;}
|
||||
const dt=Math.min(now-previous,64)/1000*rate;previous=now;started=true;
|
||||
const target=mode()!=='hover' || hovered?1:0,before=activity;
|
||||
activity=mix(activity,target,1-Math.exp(-dt*(target?18:10)));
|
||||
if(!target && activity<.002)activity=0;
|
||||
// A short demonstration finishes once started; continuous art slows to a stop.
|
||||
elapsed=Math.min(duration,elapsed+dt*(finite?1:(before+activity)*.5));
|
||||
draw(elapsed,dt);
|
||||
if(running())id=requestAnimationFrame(tick);else{stop();element.dataset.paused='true';}
|
||||
};
|
||||
const sync=()=>{
|
||||
if(destroyed)return;
|
||||
if(finite && (!rate || media.matches || mode()==='off'))elapsed=duration;
|
||||
const active=running();element.dataset.paused=String(!active);
|
||||
if(active){if(!id){previous=performance.now();id=requestAnimationFrame(tick);}}
|
||||
else{stop();draw(elapsed,0);}
|
||||
};
|
||||
const enter=()=>{hovered=true;sync();},leave=()=>{hovered=false;sync();};
|
||||
const io=new IntersectionObserver(entries=>{visible=entries[0].isIntersecting;sync();});io.observe(element);
|
||||
const observer=new MutationObserver(sync);
|
||||
for(let p=element;p;p=p.parentElement)observer.observe(p,{attributes:true,attributeFilter:['data-motion']});
|
||||
element.addEventListener('pointerenter',enter);element.addEventListener('pointerleave',leave);element.addEventListener('pointercancel',leave);
|
||||
document.addEventListener('visibilitychange',sync);media.addEventListener('change',sync);
|
||||
draw(0,0);
|
||||
return {invalidate(){if(!destroyed)draw(elapsed,0);},restart(){if(!destroyed){elapsed=0;started=false;sync();}},destroy(){destroyed=true;stop();io.disconnect();observer.disconnect();document.removeEventListener('visibilitychange',sync);media.removeEventListener('change',sync);element.removeEventListener('pointerenter',enter);element.removeEventListener('pointerleave',leave);element.removeEventListener('pointercancel',leave);delete element.dataset.paused;}};
|
||||
}
|
||||
export function fitCanvas(canvas, scale = 1) {
|
||||
const width=canvas.clientWidth,height=canvas.clientHeight,dpr = Math.min(devicePixelRatio || 1, 2.5) * scale;
|
||||
const w = Math.max(1, Math.round(width * dpr)), h = Math.max(1, Math.round(height * dpr));
|
||||
if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }
|
||||
return {w: width, h: height, dpr};
|
||||
}
|
||||
export function glProgram(canvas, vertex, fragment) {
|
||||
const gl = canvas.getContext('webgl', {alpha: true, antialias: false, premultipliedAlpha: true});
|
||||
if (!gl) return null;
|
||||
gl.getExtension('OES_standard_derivatives');
|
||||
const program = gl.createProgram(), shaders = [];
|
||||
try {
|
||||
for (const [type, source] of [[gl.VERTEX_SHADER,vertex], [gl.FRAGMENT_SHADER,fragment]]) {
|
||||
const s = gl.createShader(type); shaders.push(s); gl.shaderSource(s, source); gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
|
||||
gl.attachShader(program, s);
|
||||
}
|
||||
gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program));
|
||||
gl.useProgram(program);
|
||||
} catch (_) { shaders.forEach(s => gl.deleteShader(s)); gl.deleteProgram(program);gl.getExtension('WEBGL_lose_context')?.loseContext();canvas.width=canvas.height=1;return null; }
|
||||
const buffers = [];let disposed=false;
|
||||
return {gl, program, uniform(name) { return gl.getUniformLocation(program, name); }, attribute(name, data, size) {
|
||||
const b = gl.createBuffer(); buffers.push(b); gl.bindBuffer(gl.ARRAY_BUFFER, b); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
|
||||
const p = gl.getAttribLocation(program, name); gl.enableVertexAttribArray(p); gl.vertexAttribPointer(p, size, gl.FLOAT, false, 0, 0);
|
||||
}, destroy() {
|
||||
if(disposed)return;disposed=true;
|
||||
buffers.forEach(b => gl.deleteBuffer(b)); shaders.forEach(s => gl.deleteShader(s)); gl.deleteProgram(program);
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext();canvas.width=canvas.height=1;
|
||||
}};
|
||||
}
|
||||
export const glNoise = `
|
||||
float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
|
||||
vec2 grad(vec2 p){float a=hash(p)*6.2831853;return vec2(cos(a),sin(a));}
|
||||
float simplex(vec2 p){
|
||||
vec2 i=floor(p+(p.x+p.y)*.3660254); vec2 x=p-i+(i.x+i.y)*.2113249;
|
||||
vec2 j=x.x>x.y?vec2(1,0):vec2(0,1);vec2 y=x-j+.2113249,z=x-.5773503;
|
||||
vec3 h=max(.5-vec3(dot(x,x),dot(y,y),dot(z,z)),0.);h=h*h*h*h;
|
||||
return 70.*dot(h,vec3(dot(grad(i),x),dot(grad(i+j),y),dot(grad(i+1.),z)));
|
||||
}`;
|
||||
export function shell(element, options = {}) {
|
||||
// Apply the public contract before a component measures, reads colors or draws.
|
||||
const attributes=new Map();
|
||||
for(const key of ['theme','motion','variant','effects','shadow','material']){
|
||||
const value=options[key];
|
||||
if(value===undefined || (key==='theme' && !['light','dark'].includes(value)) || (key==='motion' && !['auto','hover','off'].includes(value)))continue;
|
||||
attributes.set(key,element.getAttribute('data-'+key));element.dataset[key]=String(value);
|
||||
}
|
||||
applyContent(element,options.content);
|
||||
const button = options.trigger ?? null;
|
||||
const rate=Number.isFinite(Number(options.speed ?? 1)) && Number(options.speed ?? 1)>=0?Number(options.speed ?? 1):1;
|
||||
const media=matchMedia('(prefers-reduced-motion: reduce)');
|
||||
const still=()=>!rate || element.closest('[data-motion]')?.dataset.motion==='off' || media.matches;
|
||||
let x=0,y=0,tx=0,ty=0,rx=0,ry=0,trx=0,try_=0,on=false,frame=0,previous=0,destroyed=false,entryRect=null;
|
||||
let dialog=null,placeholder=null,animation=null,lightX=0,lightY=0;
|
||||
const surface=element.querySelector('.showcase-window') || element;
|
||||
// A separate background layer leaves artwork pseudo-elements and text intact.
|
||||
const finish=document.createElement('span');finish.className='card-finish';finish.setAttribute('aria-hidden','true');surface.prepend(finish);
|
||||
const effects=()=>new Set((element.dataset.effects || '').split(/\s+/));
|
||||
const enabled=()=>{const e=effects();return !dialog && !still() && (e.has('glow') || e.has('tilt'));};
|
||||
const paint = () => {
|
||||
element.style.setProperty('--pointer-x',`${x-lightX}px`);element.style.setProperty('--pointer-y',`${y-lightY}px`);
|
||||
element.style.setProperty('--_card-rotate-x',`${rx}deg`);element.style.setProperty('--_card-rotate-y',`${ry}deg`);
|
||||
};
|
||||
const stop = () => {cancelAnimationFrame(frame);frame=0;previous=0;};
|
||||
const reset = () => {on=false;stop();rx=ry=trx=try_=0;entryRect=null;element.style.setProperty('--pointer-on',0);paint();};
|
||||
const schedule = () => {if(!frame && !destroyed){previous=performance.now();frame=requestAnimationFrame(follow);}};
|
||||
const leave = () => {
|
||||
on=false;entryRect=null;trx=try_=0;element.style.setProperty('--pointer-on',0);
|
||||
if(document.hidden || !enabled()){reset();return;}
|
||||
if(Math.abs(rx)+Math.abs(ry)>.005)schedule();else stop();
|
||||
};
|
||||
const follow = now => {
|
||||
frame=0;
|
||||
if(destroyed || document.hidden || !enabled()){reset();return;}
|
||||
const dt=Math.min(now-previous,64)/1000*rate;previous=now;
|
||||
const ease=1-Math.exp(-dt*(on?18:10));
|
||||
if(on){x=mix(x,tx,ease);y=mix(y,ty,ease);}
|
||||
rx=mix(rx,trx,ease);ry=mix(ry,try_,ease);
|
||||
const positionDone=!on || Math.abs(x-tx)+Math.abs(y-ty)<.1;
|
||||
const rotationDone=Math.abs(rx-trx)+Math.abs(ry-try_)<.005;
|
||||
if(positionDone && on){x=tx;y=ty;}if(rotationDone){rx=trx;ry=try_;}
|
||||
paint();
|
||||
if(!positionDone || !rotationDone)frame=requestAnimationFrame(follow);else previous=0;
|
||||
};
|
||||
const move = e => {
|
||||
if(e.pointerType==='touch' || document.hidden || !enabled()){reset();return;}
|
||||
// A fixed entry rectangle avoids feeding the tilted bounds back into tilt.
|
||||
const r=entryRect || (entryRect=element.getBoundingClientRect());
|
||||
tx=e.clientX-r.left-element.clientLeft;ty=e.clientY-r.top-element.clientTop;
|
||||
const active=effects();
|
||||
trx=active.has('tilt')?-clamp((e.clientY-r.top)/r.height*2-1,-1,1)*2.4:0;
|
||||
try_=active.has('tilt')?clamp((e.clientX-r.left)/r.width*2-1,-1,1)*2.4:0;
|
||||
if(!on){x=tx;y=ty;on=true;paint();}
|
||||
element.style.setProperty('--pointer-on',active.has('glow')?1:0);
|
||||
if(Math.abs(x-tx)+Math.abs(y-ty)>=.1 || Math.abs(rx-trx)+Math.abs(ry-try_)>=.005)schedule();
|
||||
};
|
||||
const sync = () => {element.toggleAttribute('data-static-motion',still());if(document.hidden || !enabled())reset();else if(on){const active=effects();if(!active.has('tilt'))trx=try_=rx=ry=0;element.style.setProperty('--pointer-on',active.has('glow')?1:0);paint();}};
|
||||
const observer=new MutationObserver(()=>{measure();sync();});
|
||||
for(let p=element;p;p=p.parentElement)observer.observe(p,{attributes:true,attributeFilter:['data-motion','data-effects','data-title']});
|
||||
const io=new IntersectionObserver(entries=>{if(!entries[0].isIntersecting)reset();});io.observe(element);
|
||||
const heading=element.querySelector(':scope > .card-heading, :scope > .card-caption, :scope > .metric-copy');
|
||||
const measure=()=>{if(heading)element.style.setProperty('--_card-heading-height',`${heading.offsetHeight}px`);if(surface!==element){lightX=surface.offsetLeft;lightY=surface.offsetTop;}};
|
||||
const ro=new ResizeObserver(()=>{measure();if(on)leave();});ro.observe(element);if(heading)ro.observe(heading);measure();
|
||||
element.addEventListener('pointerenter',move);element.addEventListener('pointermove',move);element.addEventListener('pointerleave',leave);element.addEventListener('pointercancel',leave);
|
||||
document.addEventListener('visibilitychange',sync);media.addEventListener('change',sync);
|
||||
sync();
|
||||
window.addEventListener('scroll',leave,{passive:true,capture:true});
|
||||
const restore = () => {
|
||||
if (!dialog) return; reset(); animation?.cancel(); placeholder.replaceWith(element); dialog.remove(); dialog=null; placeholder=null;
|
||||
element.dispatchEvent(new Event('card-theme-change')); button?.focus({preventScroll:true});
|
||||
};
|
||||
const close = () => {
|
||||
if (!dialog) return;
|
||||
if (still()) { restore(); return; }
|
||||
const from=element.getBoundingClientRect(), to=placeholder.getBoundingClientRect();
|
||||
animation=element.animate([{transform:'none'},{transform:`translate(${to.left-from.left}px,${to.top-from.top}px) scale(${to.width/from.width},${to.height/from.height})`}],{duration:450/rate,easing:'cubic-bezier(.165,.84,.44,1)',fill:'forwards'});
|
||||
animation.finished.then(restore).catch(()=>{});
|
||||
};
|
||||
const open = event => {
|
||||
if(button?.tagName === 'A') { if(!button.getAttribute('href')?.startsWith('#'))return; event?.preventDefault(); }
|
||||
if (dialog) return; reset(); const from=element.getBoundingClientRect();
|
||||
placeholder=document.createElement('div');placeholder.style.cssText=`height:${from.height}px;width:100%`;element.before(placeholder);
|
||||
dialog=document.createElement('dialog');dialog.className=`card-dialog ${element.classList.contains('showcase-card')?'showcase-dialog':element.classList.contains('banner') || element.classList.contains('strip')?'wide':''}`;
|
||||
dialog.setAttribute('aria-label',element.querySelector('h2, .showcase-wordmark')?.textContent || '查看卡片');
|
||||
const cs=getComputedStyle(element);
|
||||
// The element changes parent; carry its inherited host typography with it.
|
||||
dialog.style.font=cs.font;
|
||||
for(const n of ['--card-accent','--card-accent-2','--card-accent-3','--card-radius']) {const value=cs.getPropertyValue(n).trim();if(value)dialog.style.setProperty(n,value);}
|
||||
dialog.dataset.theme=element.closest('[data-theme]')?.dataset.theme || 'light';
|
||||
dialog.dataset.motion=element.closest('[data-motion]')?.dataset.motion || 'auto';
|
||||
const exit=document.createElement('button');exit.className='dialog-close';exit.textContent='×';exit.setAttribute('aria-label','关闭');exit.addEventListener('click',close);
|
||||
dialog.append(element,exit);document.body.append(dialog);dialog.showModal();
|
||||
dialog.addEventListener('cancel',e=>{e.preventDefault();close();});dialog.addEventListener('click',e=>{if(e.target===dialog)close();});
|
||||
const to=element.getBoundingClientRect();
|
||||
if(!still()) animation=element.animate([{transform:`translate(${from.left-to.left}px,${from.top-to.top}px) scale(${from.width/to.width},${from.height/to.height})`},{transform:'none'}],{duration:700/rate,easing:'cubic-bezier(.165,.84,.44,1)'});
|
||||
element.dispatchEvent(new Event('card-theme-change'));
|
||||
};
|
||||
button?.addEventListener('click',open);
|
||||
return () => { if(destroyed)return;destroyed=true;restore();stop();finish.remove();observer.disconnect();io.disconnect();ro.disconnect();window.removeEventListener('scroll',leave,true);media.removeEventListener('change',sync);document.removeEventListener('visibilitychange',sync);element.removeEventListener('pointerenter',move);element.removeEventListener('pointermove',move);element.removeEventListener('pointerleave',leave);element.removeEventListener('pointercancel',leave);button?.removeEventListener('click',open);element.removeAttribute('data-static-motion');element.style.removeProperty('--pointer-on');element.style.removeProperty('--pointer-x');element.style.removeProperty('--pointer-y');element.style.removeProperty('--_card-rotate-x');element.style.removeProperty('--_card-rotate-y');element.style.removeProperty('--_card-heading-height');
|
||||
// Keep template canvases for remounting, but release their backing stores.
|
||||
for(const canvas of element.querySelectorAll('canvas'))canvas.width=canvas.height=1;
|
||||
for(const [key,value] of attributes){if(value===null)element.removeAttribute('data-'+key);else element.setAttribute('data-'+key,value);}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/* The host supplies the accent and font. Geometry is intentionally fixed. */
|
||||
:where(.card, .showcase-card, .card-dialog) {
|
||||
--_card-accent-2: var(--card-accent-2, oklch(from var(--card-accent) calc(l + .06) c calc(h + 50)));
|
||||
--_card-accent-3: var(--card-accent-3, oklch(from var(--card-accent) calc(l + .10) calc(c * .8) calc(h + 100)));
|
||||
--_card-accent-ink: oklch(from var(--card-accent) min(l,.5) c h);
|
||||
--_card-accent-2-ink: oklch(from var(--_card-accent-2) min(l,.5) c h);
|
||||
--_card-accent-3-ink: oklch(from var(--_card-accent-3) min(l,.5) c h);
|
||||
/* A checkmark sits on the chosen color itself, independently of page theme. */
|
||||
--_card-on-accent: oklch(from var(--card-accent) clamp(0, round(up, calc(.65 - l), 1), 1) 0 h);
|
||||
--_card-radius: var(--card-radius, 8px);
|
||||
color-scheme:light;
|
||||
--card-surface: #fff;
|
||||
--card-panel: #fff;
|
||||
--card-ink: #233044;
|
||||
--card-muted: #7b8798;
|
||||
--card-line: #e5edf5;
|
||||
--card-wash: #f5f7fa;
|
||||
--card-shadow: 0 12px 32px -12px #23364c26, 0 2px 8px #23364c06;
|
||||
--_card-edge-core: color-mix(in oklab, var(--card-accent) 56%, var(--card-surface));
|
||||
color: var(--card-ink);
|
||||
}
|
||||
:where([data-theme="dark"]) :where(.card, .showcase-card, .card-dialog), :where(.card, .showcase-card, .card-dialog)[data-theme="dark"] {
|
||||
--_card-accent-ink: oklch(from var(--card-accent) max(l,.8) c h);
|
||||
--_card-accent-2-ink: oklch(from var(--_card-accent-2) max(l,.8) c h);
|
||||
--_card-accent-3-ink: oklch(from var(--_card-accent-3) max(l,.8) c h);
|
||||
color-scheme:dark;
|
||||
--card-surface: #14171e;
|
||||
--card-panel: #20242d;
|
||||
--card-ink: #edf0f5;
|
||||
--card-muted: #979fad;
|
||||
--card-line: #ffffff12;
|
||||
--card-wash: #191e27;
|
||||
--card-shadow: 0 14px 40px -12px #0008, 0 0 0 1px #ffffff08;
|
||||
--_card-edge-core: color-mix(in oklab, var(--card-accent) 48%, #fff);
|
||||
}
|
||||
/* A local light theme also works inside a dark host. */
|
||||
:is(.card, .showcase-card, .card-dialog)[data-theme="light"] {
|
||||
--_card-accent-ink: oklch(from var(--card-accent) min(l,.5) c h);
|
||||
--_card-accent-2-ink: oklch(from var(--_card-accent-2) min(l,.5) c h);
|
||||
--_card-accent-3-ink: oklch(from var(--_card-accent-3) min(l,.5) c h);
|
||||
color-scheme:light;
|
||||
--card-surface: #fff;
|
||||
--card-panel: #fff;
|
||||
--card-ink: #233044;
|
||||
--card-muted: #7b8798;
|
||||
--card-line: #e5edf5;
|
||||
--card-wash: #f5f7fa;
|
||||
--card-shadow: 0 12px 32px -12px #23364c26, 0 2px 8px #23364c06;
|
||||
--_card-edge-core: color-mix(in oklab, var(--card-accent) 56%, var(--card-surface));
|
||||
}
|
||||
|
||||
.card {
|
||||
width:100%; min-width:0;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
/* Clip without an internal scroll box when focus/scrollIntoView targets art. */
|
||||
overflow: clip;
|
||||
border: 1px solid var(--card-line);
|
||||
border-radius: var(--_card-radius);
|
||||
background: var(--card-surface);
|
||||
aspect-ratio: 1.2;
|
||||
&.portrait { aspect-ratio: .6; }
|
||||
&.banner { aspect-ratio: 2.7; }
|
||||
&.strip { aspect-ratio: 4.7; }
|
||||
& .card-heading {
|
||||
position: absolute; z-index: 4; top: 24px; left: 24px; right: 24px;
|
||||
margin: 0; font-size: clamp(22px, 1.8vw, 26px); font-weight: 400;
|
||||
line-height: 1.12; letter-spacing: -.045em;
|
||||
}
|
||||
& .card-stage { position: absolute; inset: 78px 0 0; overflow: hidden; overflow:clip; }
|
||||
&.bottom-title {
|
||||
& .card-stage { inset: 0 0 94px; }
|
||||
& .card-caption { position: absolute; bottom: 23px; left: 24px; right: 24px; z-index: 4; }
|
||||
& h2 { margin: 0 0 8px; font-size: 18px; font-weight: 550; letter-spacing: -.035em; }
|
||||
& .card-caption p { margin: 0; font-size: 13px; line-height: 1.55; color: var(--card-muted); }
|
||||
}
|
||||
& canvas { display: block; width: 100%; height: 100%; }
|
||||
}
|
||||
.card::after, .showcase-window::before {
|
||||
content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 9;
|
||||
padding: 1px; border-radius: inherit;
|
||||
background: radial-gradient(150px circle at var(--pointer-x, -300px) var(--pointer-y, -300px) in oklab,
|
||||
var(--_card-edge-core) 0%,
|
||||
color-mix(in oklab, var(--card-accent) 45%, transparent) 24%,
|
||||
color-mix(in oklab, var(--card-accent) 16%, transparent) 52%,
|
||||
transparent 76%);
|
||||
mask: linear-gradient(in oklab,#fff 0 0) content-box, linear-gradient(in oklab,#fff 0 0);
|
||||
mask-composite: exclude; opacity: 0;
|
||||
transition: opacity .45s cubic-bezier(.16,1,.3,1);
|
||||
}
|
||||
|
||||
.card:hover::after, .showcase-card:hover .showcase-window::before { transition-duration:.2s; }
|
||||
.card[data-effects~="glow"]::after, .showcase-card[data-effects~="glow"] .showcase-window::before { opacity:calc(var(--pointer-on,0) * .85); }
|
||||
:is(.card,.showcase-card)[data-effects~="tilt"] { transform:perspective(1000px) rotateX(var(--_card-rotate-x,0deg)) rotateY(var(--_card-rotate-y,0deg)); }
|
||||
.card-dialog {
|
||||
border: 0; padding: 0; background: transparent; overflow: visible;
|
||||
width: min(560px, calc((100vh - 48px) * .6), calc(100vw - 40px)); max-height: calc(100vh - 48px);
|
||||
&.wide { width: min(1100px, calc(100vw - 40px)); }
|
||||
&::backdrop { background: #10172682; backdrop-filter: blur(8px); }
|
||||
& .card { width: 100%; max-height: calc(100vh - 48px); }
|
||||
& :is(.card,.showcase-card)[data-effects~="tilt"] { transform:none; }
|
||||
& .dialog-close { position: absolute; right: -12px; top: -12px; width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--card-line); background: var(--card-panel); color: var(--card-ink); z-index: 20; cursor: pointer; font-size: 20px; }
|
||||
}
|
||||
@media (max-width: 650px) {
|
||||
.card {
|
||||
& .card-heading { font-size: 24px; }
|
||||
&.banner { aspect-ratio: 1.2; }
|
||||
&.strip { aspect-ratio: 1.2; }
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.card *, .showcase-card * { transition-duration: 0s !important; animation: none !important; }
|
||||
.card[data-effects]::after, .showcase-card[data-effects] .showcase-window::before { opacity:0; transition:none; }
|
||||
:is(.card,.showcase-card)[data-effects~="tilt"] { transform:none; }
|
||||
}
|
||||
|
||||
/* Text occupies a real row; the art keeps its own crop and minimum stage. */
|
||||
.card:has(> .card-stage):not(.line-ribbons) {
|
||||
--_card-stage-min:240px;
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1fr);
|
||||
grid-template-rows:auto minmax(var(--_card-stage-track-min,var(--_card-stage-min)),1fr);
|
||||
row-gap:var(--_card-stage-gap,26px);
|
||||
min-height:min-content;
|
||||
& > .card-heading, & > .card-caption {
|
||||
position:relative; inset:auto; min-width:0; order:0;
|
||||
margin:24px 24px 0; flex:none;
|
||||
}
|
||||
& > .card-caption h2 { font-size:22px; line-height:1.15; font-weight:450; margin:0 0 8px; }
|
||||
& > .card-stage {
|
||||
position:relative; inset:auto; order:1; min-width:0; min-height:var(--_card-stage-min);
|
||||
margin:0;
|
||||
}
|
||||
&[data-title="bottom"] {
|
||||
grid-template-rows:minmax(var(--_card-stage-track-min,var(--_card-stage-min)),1fr) auto; row-gap:28px;
|
||||
&:has(> .card-caption) { row-gap:25px; }
|
||||
& > .card-stage { order:0; }
|
||||
& > .card-heading { order:1; margin:0 24px 28px; }
|
||||
& > .card-caption { order:1; margin:0 24px 23px; }
|
||||
}
|
||||
}
|
||||
.card:is(.ui-slice,.terminal,.beam-graph,.keycaps) {
|
||||
height:420px; aspect-ratio:auto; --_card-stage-gap:20px;
|
||||
}
|
||||
/* Runtime resolves the closest motion mode, speed=0 and reduced motion together. */
|
||||
:is(.card,.showcase-card)[data-static-motion] *, .card[data-motion="off"] * { transition-duration:0s!important; animation:none!important; }
|
||||
.card[data-motion="off"]::after, .showcase-card[data-motion="off"] .showcase-window::before { opacity:0; transition:none; }
|
||||
:is(.card,.showcase-card)[data-motion="off"][data-effects~="tilt"] { transform:none; }
|
||||
[data-text] { white-space:pre-line; overflow-wrap:anywhere; }
|
||||
@@ -0,0 +1,34 @@
|
||||
.showcase-row { display: flex; gap: 16px; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: thin; scrollbar-color: var(--page-line) transparent; padding: 16px 16px 26px; margin: -12px -16px -8px; }
|
||||
.showcase-card {
|
||||
--_card-finish-base:transparent; --_card-material-reflection:0;
|
||||
position:relative; flex: 0 0 332px; min-width: 0; scroll-snap-align: start;
|
||||
& .card-finish { z-index:1; }
|
||||
& .showcase-window { position: relative; width: 100%; aspect-ratio: 332/448; overflow: hidden; border-radius: var(--_card-radius); transform: scaleX(1); transition: transform .8s cubic-bezier(.165,.84,.44,1); }
|
||||
& .showcase-art { position: absolute; inset: 0 -1.8%; transform: scaleX(1); transition: transform .8s cubic-bezier(.165,.84,.44,1); background: linear-gradient(in oklab,#090b14,color-mix(in oklab,var(--card-accent) 46%,#19112b)); & svg, & img { width: 100%; height: 100%; display: block; object-fit: cover; } }
|
||||
& .showcase-wordmark { position: absolute; z-index: 3; bottom: 16px; left: 20px; font-size: 25px; font-weight: 650; letter-spacing: -.02em; color: #fff; transform-origin: left center; transition: transform .8s cubic-bezier(.165,.84,.44,1); }
|
||||
& .showcase-emblem { position:absolute; z-index:2; width:48%; height:auto; left:26%; top:29%; fill:none; stroke:#fff; stroke-width:15; stroke-linecap:round; stroke-linejoin:round; }
|
||||
& .showcase-art img.showcase-cropped { position:relative; width:140%; height:140%; left:-3%; top:-5%; }
|
||||
&[data-background="editorial"] .showcase-window::after { content:""; position:absolute; inset:60% 0 0; background:linear-gradient(0deg,#02061060,transparent); pointer-events:none; }
|
||||
&:hover .showcase-window { transform: scaleX(1.036); }
|
||||
&:hover .showcase-art, &:hover .showcase-wordmark { transform: scaleX(.965251); }
|
||||
& p { font-size: 15px; line-height: 1.55; margin: 16px 0 12px; padding-right: 16px; min-height: 47px; letter-spacing: -.02em; }
|
||||
& .showcase-link { padding: 0; border: 0; background: transparent; color: var(--_card-accent-ink); font: inherit; text-decoration: none; font-size: 14px; font-weight: 550; cursor: pointer; & > span:last-child { display: inline-block; margin-left: 3px; transition: transform .3s; } &:hover > span:last-child { transform: translate(2px,-2px); } }
|
||||
& small { display: block; margin-top: 20px; color: var(--card-muted); font: 10px/1.5 ui-monospace, monospace; }
|
||||
&[data-background="iso-object"] .showcase-art { background: color-mix(in oklab,var(--card-accent) 48%,#241824); }
|
||||
&[data-background="membrane"] { & .showcase-art { background: color-mix(in oklab,var(--card-accent) 12%,#f8f3ef); } & .showcase-wordmark { color: #514451; } }
|
||||
&[data-background="image"] .showcase-window::after { content: ""; position: absolute; inset: 0; background: linear-gradient(0deg in oklab,color-mix(in oklab,var(--card-accent) 65%,#12141b) 0%,color-mix(in oklab,var(--card-accent) 15%,transparent) 65%,transparent); }
|
||||
@media(max-width:650px) { flex-basis: min(332px, 86vw); }
|
||||
}
|
||||
.card-dialog.showcase-dialog {
|
||||
width: min(420px, calc((100dvh - 200px) * .741), calc(100vw - 48px));
|
||||
background: var(--card-surface); border-radius: var(--_card-radius); padding: 12px;
|
||||
& .showcase-card { width: 100%; }
|
||||
& .showcase-card p { padding-left: 4px; margin-top: 12px; }
|
||||
& .showcase-link { display: none; }
|
||||
& .showcase-wordmark { font-size:clamp(18px,3vh,25px); }
|
||||
& small { display: none; }
|
||||
}
|
||||
|
||||
.showcase-card { display:flex; flex-direction:column; }
|
||||
.showcase-card[data-title="top"] { & .showcase-caption { order:-1; margin-bottom:16px; } & .showcase-caption p { margin:0 0 10px; } }
|
||||
.showcase-card[data-motion="off"] { & .showcase-window,& .showcase-art,& .showcase-wordmark { transition-duration:0s; transform:none; } }
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="showcase-row" aria-label="品牌视觉展示">
|
||||
<article class="showcase-card" data-card="showcase" data-example="morrow" data-motion="auto" data-background="editorial"><div class="showcase-window"><div class="showcase-art"><img data-image="image" alt="珊瑚色与紫色的发光粒子,汇成四枚环绕的种子" src="showcase/images/morrow-thermal.jpg" width="750" height="1000" loading="lazy"></div><span class="showcase-wordmark" data-text="brand">morrow</span></div><div class="showcase-caption"><p data-text="description">把每一个微小的喜欢,聚成新的灵感。</p><a class="showcase-link" data-href="href" href="#showcase"><span data-text="action">走近 morrow</span> <span>↗</span></a></div><small>01 · 热感颗粒</small></article>
|
||||
<article class="showcase-card" data-card="showcase" data-example="duotone" data-motion="auto" data-background="editorial"><div class="showcase-window"><div class="showcase-art"><img data-image="image" alt="蓝色卷发的创作者抬头望向光,暖橙与青蓝交织" src="showcase/images/duotone-portrait.jpg" width="750" height="1000" loading="lazy"></div><span class="showcase-wordmark" data-text="brand">duotone</span></div><div class="showcase-caption"><p data-text="description">换一个角度,让想象拥有自己的颜色。</p><a class="showcase-link" data-href="href" href="#showcase"><span data-text="action">走近 duotone</span> <span>↗</span></a></div><small>02 · 双色人物</small></article>
|
||||
<article class="showcase-card" data-card="showcase" data-example="serein" data-motion="auto" data-background="editorial"><div class="showcase-window"><div class="showcase-art"><img data-image="image" alt="深青绿色的水流,在银白浪纹中旋转" src="showcase/images/serein-current.jpg" width="750" height="1000" loading="lazy"></div><svg class="showcase-emblem" viewBox="0 0 160 160" aria-hidden="true"><path d="M46 32H113C143 32 140 74 112 78L47 83C19 87 20 128 49 128H116"/></svg><span class="showcase-wordmark" data-text="brand">serein</span></div><div class="showcase-caption"><p data-text="description">顺着灵感的方向,让每一帧自然流动。</p><a class="showcase-link" data-href="href" href="#showcase"><span data-text="action">走近 serein</span> <span>↗</span></a></div><small>03 · 流动摄影与矢量字形</small></article>
|
||||
<article class="showcase-card" data-card="showcase" data-example="folded" data-motion="auto" data-background="editorial"><div class="showcase-window"><div class="showcase-art"><img data-image="image" class="showcase-cropped" alt="薄荷绿的折叠门形结构,悬浮在深色网格上" src="showcase/images/folded-portal.jpg" width="750" height="1000" loading="lazy"></div><span class="showcase-wordmark" data-text="brand">folded</span></div><div class="showcase-caption"><p data-text="description">在清晰的结构里,创造更多可能。</p><a class="showcase-link" data-href="href" href="#showcase"><span data-text="action">走近 folded</span> <span>↗</span></a></div><small>04 · 折面与秩序</small></article>
|
||||
<article class="showcase-card" data-card="showcase" data-example="cubiq" data-motion="auto" data-background="iso-object"><div class="showcase-window"><div class="showcase-art"></div><span class="showcase-wordmark" data-text="brand">cubiq</span></div><div class="showcase-caption"><p data-text="description">好想法,也可以有触手可及的形状。</p><a class="showcase-link" data-href="href" href="#showcase"><span data-text="action">走近 cubiq</span> <span>↗</span></a></div><small>iso-object · 通用身份参数</small></article>
|
||||
<article class="showcase-card" data-card="showcase" data-example="vellum" data-motion="auto" data-background="membrane"><div class="showcase-window"><div class="showcase-art"></div><span class="showcase-wordmark" data-text="brand">vellum</span></div><div class="showcase-caption"><p data-text="description">轻一些,留出想象的空间。</p><a class="showcase-link" data-href="href" href="#showcase"><span data-text="action">走近 vellum</span> <span>↗</span></a></div><small>membrane · 通用身份参数</small></article>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
import {loop, shell, svgStops} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,{...options,trigger:options.content?.href!==undefined?null:element.querySelector('.showcase-link')});
|
||||
const art=element.querySelector('.showcase-art'),type=element.dataset.background||'glow-shapes',id='s'+crypto.randomUUID();
|
||||
const defs=`<defs>
|
||||
<filter id="${id}-grain"><feTurbulence type="fractalNoise" baseFrequency=".72" numOctaves="2" seed="8" stitchTiles="stitch"/><feColorMatrix type="saturate" values="0"/></filter>
|
||||
<filter id="${id}-edge" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="6"/></filter>
|
||||
<filter id="${id}-fog" x="-80%" y="-80%" width="260%" height="260%"><feGaussianBlur stdDeviation="16"/></filter>
|
||||
<radialGradient id="${id}-heart" cx="50%" cy="49%" r="63%">${svgStops([[0.0,"var(--_card-accent-3)",1.0],[0.3,"var(--_card-accent-3)",1.0],[0.56,"var(--_card-accent-2)",1.0],[0.88,"var(--card-accent)",1.0],[1.0,"var(--card-accent)",0.0]])}</radialGradient>
|
||||
<linearGradient id="${id}-back" x1="0" y1="0" x2="1" y2="1">${svgStops([[0.0,"var(--card-accent)",1.0],[1.0,"color-mix(in oklab,var(--card-accent) 48%,#10121b)",1.0]])}</linearGradient>
|
||||
<pattern id="${id}-grid" width="24" height="24" patternUnits="userSpaceOnUse"><path d="M24 0H0V24" fill="none" stroke="white" stroke-opacity=".14" stroke-width=".7"/></pattern>
|
||||
<radialGradient id="${id}-fade">${svgStops([[0.0,"white",1.0],[0.62,"white",1.0],[1.0,"black",1.0]])}</radialGradient><mask id="${id}-mask"><rect width="332" height="448" fill="url(#${id}-fade)"/></mask>
|
||||
<linearGradient id="${id}-memA" x1="0" y1="0" x2=".4" y2="1">${svgStops([[0.0,"var(--_card-accent-2)",0.04],[0.44,"var(--_card-accent-2)",0.55],[0.51,"white",0.35],[0.62,"var(--card-accent)",0.4],[1.0,"var(--card-accent)",0.04]])}</linearGradient>
|
||||
<linearGradient id="${id}-memB" x1="0" y1="0" x2="1" y2="1">${svgStops([[0.0,"var(--_card-accent-3)",0.03],[0.45,"var(--_card-accent-3)",0.46],[0.52,"white",0.32],[0.7,"var(--_card-accent-2)",0.26],[1.0,"var(--card-accent)",0.04]])}</linearGradient>
|
||||
</defs>`;
|
||||
let content='';
|
||||
if(type==='glow-shapes'){
|
||||
const paths={heart:'M0 31C-51 0-38-39-15-29Q0-25 0-13Q0-25 15-29C38-39 51 0 0 31Z',circle:'M32 0A32 32 0 1 0-32 0A32 32 0 1 0 32 0',star:'M0-33 9-10 33-10 14 5 21 30 0 15-21 30-14 5-33-10-9-10Z',petal:'M0-36C21-35 35-21 28-4L9 34Q-24 24-24 1Q-19-31 0-36Z'};
|
||||
const shape=paths[options.shape||element.dataset.shape]||paths.heart;
|
||||
for(let i=0;i<5;i++){const a=-Math.PI/2+i*Math.PI*2/5,x=166+75*Math.cos(a),y=170+75*Math.sin(a),rotation=[-5,14,-9,10,-13][i];content+=`<g transform="translate(${x} ${y}) rotate(${rotation})"><path d="${shape}" fill="var(--card-accent)" opacity=".48" filter="url(#${id}-fog)"/><path d="${shape}" fill="url(#${id}-heart)" filter="url(#${id}-edge)"/><path d="${shape}" transform="scale(.88)" fill="url(#${id}-heart)" opacity=".8"/></g>`;}
|
||||
}else if(type==='grid-solid'){
|
||||
content=`<rect width="332" height="448" fill="url(#${id}-grid)" mask="url(#${id}-mask)"/><path d="M244 130 70 256 96 303 270 177Z" fill="url(#${id}-back)"/><path d="M70 124 244 130 270 177 96 171Z" fill="var(--card-accent)"/><path d="M70 256 244 262 270 309 96 303Z" fill="var(--card-accent)"/><path d="M244 130 270 177M70 256 96 303" fill="none" stroke="color-mix(in oklab,var(--card-accent) 65%,white)" stroke-width="1"/>`;
|
||||
}else if(type==='iso-object'){
|
||||
content=`<path d="M156 252 216 216 276 252 216 288Z" fill="#14101c"/><g stroke="#2b1b38" stroke-width=".85" stroke-linejoin="round"><path d="M108 130 168 94 228 130 168 166Z" fill="var(--_card-accent-2)"/><path d="M108 130 168 166 168 246 108 210Z" fill="var(--_card-accent-3)"/><path d="M168 166 228 130 228 210 168 246Z" fill="var(--card-accent)"/></g>`;
|
||||
}else if(type==='membrane'){
|
||||
content=`<g class="membrane-folds"><path d="M-90 392C26 172 164 433 400-80L455 207C270 357 100 453-90 392Z" fill="url(#${id}-memA)"/><path d="M-69 279C124 418 157-2 399 72L392 436C235 376 110 555-69 279Z" fill="url(#${id}-memB)"/><path d="M-25 364C129 166 211 324 373 91L425 271C266 237 166 424-25 364Z" fill="url(#${id}-memA)" opacity=".7"/><g fill="none" stroke="white" stroke-opacity=".75" stroke-width=".8"><path d="M-90 392C26 172 164 433 400-80"/><path d="M-69 279C124 418 157-2 399 72"/><path d="M-25 364C129 166 211 324 373 91"/></g><g fill="none" stroke="white" stroke-opacity=".18" stroke-width=".4"><path d="M-68 401C51 225 174 426 405-56"/><path d="M-53 409C66 258 201 425 414-32"/><path d="M-48 296C133 412 179 22 405 88"/></g></g>`;
|
||||
}
|
||||
const hasImage=type==='image'||type==='editorial';
|
||||
if(!hasImage)art.innerHTML=`<svg viewBox="0 0 332 448" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">${defs}<g class="showcase-subject">${content}</g><rect width="332" height="448" filter="url(#${id}-grain)" opacity=".06"/></svg>`;
|
||||
const fold=art.querySelector('.membrane-folds'),subject=art.querySelector('.showcase-subject');
|
||||
const clock=loop(element,t=>{if(fold)fold.setAttribute('transform',`translate(${Math.sin(t/15)*3} ${Math.cos(t/17)*4})`);else if(subject&&type==='glow-shapes')subject.setAttribute('transform',`translate(0 ${Math.sin(t/12)*2})`);},{...options,continuous:!!fold||type==='glow-shapes'});
|
||||
|
||||
return {destroy(){clock.destroy();clean();if(!hasImage)art.replaceChildren();}};
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 268 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 326 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 343 KiB |
@@ -0,0 +1,18 @@
|
||||
.stream-window {
|
||||
height:440px;aspect-ratio:auto; --_card-stage-track-min:min-content;
|
||||
& .card-stage { display:grid; grid-template-rows:auto minmax(180px,1fr); mask-image:linear-gradient(#000 84%,transparent); }
|
||||
& .stream-status { display:flex; align-items:center; gap:7px; padding:6px 24px 20px; min-width:0;font-size:11px;color:var(--card-muted); & > i { flex:none; width:5px;height:5px;border-radius:50%;background:var(--card-accent); } & > span:not(.stream-count) { min-width:0; overflow-wrap:anywhere; } & .stream-count { flex:none; white-space:nowrap; margin-left:auto;font:10px ui-monospace,monospace; } }
|
||||
& .stream-viewport { position:relative; overflow:clip; min-height:180px; }
|
||||
& .stream-browser { position:absolute;left:24px;top:0;width:470px;height:324px;border:1px solid var(--card-line);border-radius:7px;background:var(--card-panel);box-shadow:0 8px 24px #18273e07; }
|
||||
& .stream-toolbar { height:33px;border-bottom:1px solid var(--card-line);display:flex;align-items:center;gap:4px;padding:0 13px; & > span { width:5px;height:5px;border-radius:50%;background:var(--card-line); } & > i { overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;font:9px ui-monospace,monospace;color:var(--card-muted);margin-left:10px; } }
|
||||
& .stream-sidebar { position:absolute;top:33px;bottom:0;left:0;width:46px;border-right:1px solid var(--card-line);display:flex;align-items:center;flex-direction:column;gap:19px;padding-top:21px; & > i { width:11px;height:11px;border:1px solid var(--card-muted);opacity:.3;border-radius:3px; } & b { width:14px;height:14px;border-radius:4px;background:var(--card-accent);opacity:.8; } }
|
||||
& .stream-content { padding:20px 20px 20px 66px; }
|
||||
& .stream-chunk { position:relative; --ready:1; }
|
||||
& .chunk-content { opacity:var(--content-opacity,1);transform:translateY(calc((1 - var(--ready)) * 5px)); }
|
||||
& .chunk-skeleton { position:absolute;inset:0;opacity:var(--skeleton-opacity,0);display:flex;flex-direction:column;gap:10px; & > i,& > b { height:8px;background:var(--card-line);border-radius:3px;width:68%; } & > b { height:32px;width:85%; } & > i:last-child { width:45%; } }
|
||||
& .stream-summary { min-height:82px; & small { font-size:10px;color:var(--card-muted); } & strong { display:block;font-size:19px;line-height:1.5;font-weight:550;margin-top:5px; } & p { font-size:10px;margin:4px 0;color:var(--card-muted); } }
|
||||
& .stream-panels { display:grid;grid-template-columns:155px 150px;gap:15px;margin-top:15px; }
|
||||
& .stream-visual { height:143px; & .chunk-skeleton > b { width:100%;height:100%; } & .chunk-content { height:100%;overflow:hidden;border-radius:5px;background:color-mix(in oklab,var(--card-accent) 7%,var(--card-panel)); } & span { position:absolute;bottom:12px;left:13px;font-size:11px; } }
|
||||
& .stream-orbit { width:170px;height:170px;position:absolute;top:-42px;left:28px;transform:rotate(-30deg); & i { position:absolute;inset:0;border:1px solid color-mix(in oklab,var(--card-accent) 45%,transparent);border-radius:50%; } & i:nth-child(2) { inset:22px; } & i:nth-child(3) { inset:44px;background:radial-gradient(circle at 30% 20% in oklab,var(--_card-accent-2),var(--card-accent));border:0; } }
|
||||
& .stream-list { padding:6px 0; & small { font-size:10px;color:var(--card-muted); } & p { display:flex;align-items:center;gap:8px;font-size:11px;margin:17px 0; } & i { display:inline-block;width:15px;height:15px;border-radius:3px;background:color-mix(in oklab,var(--card-accent) 12%,var(--card-panel));border:1px solid color-mix(in oklab,var(--card-accent) 20%,var(--card-line)); } }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<article class="card stream-window" data-card="stream-window" data-motion="auto">
|
||||
<h2 class="card-heading" data-text="title">不必等待全部,<br>先让重要的出现</h2>
|
||||
|
||||
<div class="card-stage">
|
||||
<div class="stream-status"><i></i><span>内容陆续就绪</span><span class="stream-count">3 / 3</span></div>
|
||||
<div class="stream-viewport"><div class="stream-browser"><div class="stream-toolbar"><span></span><span></span><span></span><i data-text="path">studio / overview</i></div>
|
||||
<div class="stream-sidebar"><b></b><i></i><i></i><i></i><i></i></div>
|
||||
<div class="stream-content">
|
||||
<div class="stream-chunk stream-summary"><div class="chunk-skeleton"><i></i><b></b></div><div class="chunk-content"><small data-text="eyebrow">你的创作空间</small><strong data-text="heading">让想法,开始生长。</strong><p data-text="summary">继续上一件作品,或从新的灵感出发。</p></div></div>
|
||||
<div class="stream-panels"><div class="stream-chunk stream-visual"><div class="chunk-skeleton"><b></b></div><div class="chunk-content"><div class="stream-orbit"><i></i><i></i><i></i></div><span data-text="visualLabel">探索灵感</span></div></div><div class="stream-chunk stream-list"><div class="chunk-skeleton"><i></i><i></i><i></i></div><div class="chunk-content"><small data-text="listTitle">最近的作品</small><p><i></i><span data-text="items.0">晨间随想</span></p><p><i></i><span data-text="items.1">无界之境</span></p><p><i></i><span data-text="items.2">光的形状</span></p></div></div></div>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,19 @@
|
||||
import {loop,shell,smooth,staticMotion} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,options);
|
||||
const chunks=[...element.querySelectorAll('.stream-chunk')],count=element.querySelector('.stream-count'),status=element.querySelector('.stream-status > span'),still=staticMotion(element,options);
|
||||
const total=chunks.length,pending=options.content?.pendingStatus ?? '内容陆续就绪',complete=options.content?.completeStatus ?? '全部内容已就绪';
|
||||
let previous=-1;
|
||||
const clock=loop(element,time=>{
|
||||
const staticState=still();let ready=0;
|
||||
chunks.forEach((chunk,i)=>{
|
||||
const v=staticState?1:smooth((time-.15-i*.65)/.72);
|
||||
chunk.style.setProperty('--ready',v);
|
||||
chunk.style.setProperty('--skeleton-opacity',1-smooth(v/.38));
|
||||
chunk.style.setProperty('--content-opacity',smooth((v-.42)/.58));
|
||||
if(v===1)ready++;
|
||||
});
|
||||
if(ready!==previous){count.textContent=`${ready} / ${total}`;status.textContent=ready===total?complete:pending;previous=ready;}
|
||||
},{...options,duration:2.35});
|
||||
return{replay:()=>clock.restart(),destroy(){clock.destroy();clean();chunks.forEach(c=>{for(const p of ['--ready','--skeleton-opacity','--content-opacity'])c.style.removeProperty(p);});count.textContent=`${total} / ${total}`;status.textContent=complete;}};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
.terminal {
|
||||
& .card-stage { background:radial-gradient(ellipse at 83% 87% in oklab,color-mix(in oklab,var(--card-accent) 13%,transparent),transparent 62%); mask-image:linear-gradient(in oklab,#000 82%,transparent 100%); }
|
||||
& .terminal-window { position: absolute; top:3%; left:5%; width:95%; height:calc(100% + 50px); border: 1px solid var(--card-line); border-radius: calc(var(--_card-radius) * .7); background: var(--card-panel); }
|
||||
& .terminal-toolbar { display: flex; align-items: center; gap: 5px; padding: 12px 14px; border-bottom: 1px solid var(--card-line); color: var(--card-muted); & > span { width: 5px; height: 5px; border-radius: 50%; border: 1px solid color-mix(in oklab,var(--card-muted) 40%,transparent); } & em { margin-left: 12px; font: 9px ui-monospace, monospace; } & b { margin-left: auto; margin-right: 20px; font: 9px ui-monospace, monospace; opacity: .6; } }
|
||||
& .terminal-lines { padding: 15px 0; font: 13px/1.8 ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
& p { padding: 6px 15px; margin: 0; opacity: var(--line-visible,1); transform: translateY(calc((1 - var(--line-visible,1)) * 4px)); white-space: nowrap; }
|
||||
& p > span:first-child { color: var(--_card-accent-2-ink); }
|
||||
& .terminal-highlight { background:color-mix(in oklab,var(--card-accent) 9%,var(--card-panel));border-left:2px solid var(--card-accent);padding-left:13px; }
|
||||
& .terminal-flag { color: var(--_card-accent-3-ink); }
|
||||
& .terminal-dim { color: var(--card-muted); }
|
||||
& .terminal-address { color:var(--_card-accent-ink); font-size:15px; font-weight:550; }
|
||||
& .terminal-result span { color: var(--_card-accent-ink); }
|
||||
}
|
||||
}
|
||||
|
||||
.terminal .terminal-cursor { opacity:var(--cursor-on,1); }
|
||||
@media(max-width:650px) { .terminal .terminal-lines { font-size:12px; line-height:1.8; padding:15px 0; & p { padding:7px 12px; } } .terminal .terminal-toolbar { padding:12px; } .terminal .terminal-lines .terminal-address { font-size:14px; } }
|
||||
@@ -0,0 +1,4 @@
|
||||
<article class="card bottom-title terminal" data-card="terminal" data-motion="auto">
|
||||
<div class="card-stage"><div class="terminal-window"><div class="terminal-toolbar"><span></span><span></span><span></span><em data-text="path">~/morrow</em><b data-text="shell">zsh</b></div><div class="terminal-lines"><p><span class="terminal-prompt">›</span> aura deploy <span class="terminal-flag">--production</span></p><p>读取项目配置 <span class="terminal-dim">aura.config</span></p><p class="terminal-highlight"><span>✓</span> 资源构建完成 <span class="terminal-dim">1.8s</span></p><p><span>✓</span> 连接全球边缘节点</p><p class="terminal-result"><span>✓</span> 已发布,下一站是世界。</p><p class="terminal-address">morrow.aura.local <span class="terminal-cursor">▎</span></p></div></div></div>
|
||||
<div class="card-caption"><h2 data-text="title">从这里,发布到世界</h2><p data-text="description">一个命令,让每一份创造被看见。</p></div>
|
||||
</article>
|
||||
@@ -0,0 +1,24 @@
|
||||
import {loop,shell,smooth,staticMotion} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,options);
|
||||
const content=options.content || {};
|
||||
if(Array.isArray(content.lines)){
|
||||
const container=element.querySelector('.terminal-lines');container.replaceChildren();
|
||||
const lines=content.lines.length?content.lines:[{text:content.emptyText || '等待运行'}];
|
||||
for(const item of lines){
|
||||
const line=typeof item==='string'?{text:item}:item,row=document.createElement('p');
|
||||
if(['highlight','result','address'].includes(line.tone))row.className='terminal-'+line.tone;
|
||||
row.append(document.createTextNode(String(line.text ?? '')));
|
||||
if(line.detail){const detail=document.createElement('span');detail.className='terminal-dim';detail.textContent=' '+line.detail;row.append(detail);}
|
||||
container.append(row);
|
||||
}
|
||||
const cursor=document.createElement('span');cursor.className='terminal-cursor';cursor.textContent=' ▎';container.lastElementChild.append(cursor);
|
||||
}
|
||||
const rows=[...element.querySelectorAll('.terminal-lines p')],cursor=element.querySelector('.terminal-cursor'),still=staticMotion(element,options);
|
||||
const clock=loop(element,time=>{
|
||||
const staticState=still();
|
||||
rows.forEach((r,i)=>r.style.setProperty('--line-visible',staticState?1:smooth((time-.12-i*.42)/.52)));
|
||||
cursor.style.setProperty('--cursor-on',staticState?1:.35+.65*(.5+.5*Math.cos(time*5)));
|
||||
},{...options,duration:Math.max(1,rows.length*.42+.78)});
|
||||
return {replay:()=>clock.restart(),destroy(){clock.destroy();clean();rows.forEach(r=>r.style.removeProperty('--line-visible'));cursor.style.removeProperty('--cursor-on');}};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
.ui-slice {
|
||||
& .card-stage { background:radial-gradient(ellipse at 45% 65% in oklab,color-mix(in oklab,var(--card-accent) 10%,transparent),transparent 74%); }
|
||||
& .slice-back { position:absolute; width:91%; top:0; left:17%; height:98%; overflow:clip; background:var(--card-wash); border:1px solid var(--card-line); border-radius:calc(var(--_card-radius)*.8); color:var(--card-muted); mask-image:linear-gradient(in oklab,#000 68%,transparent); }
|
||||
& .slice-toolbar { border-bottom:1px solid var(--card-line); font-size:12px; padding:16px; & > span:last-child { float:right; margin-right:50px; } }
|
||||
& .slice-columns { display:grid; grid-template-columns:1fr 1fr; padding:14px; gap:12px; font-size:12px; & p { padding:15px 9px; margin:9px 0; background:var(--card-panel); border-radius:calc(var(--_card-radius)*.45); color:var(--card-ink); } }
|
||||
& .slice-front { position:absolute; width:82%; left:5%; top:25%; background:var(--card-panel); border:1px solid var(--card-line); border-radius:calc(var(--_card-radius)*.8); box-shadow:0 22px 45px -15px #24324f30,0 3px 9px #24324f0a; padding:20px; font-size:14px; }
|
||||
& .slice-front-heading { display:flex; align-items:center; gap:7px; font-weight:550; font-size:15px; margin-bottom:11px; }
|
||||
& .slice-symbol,& .slice-more { color:var(--card-muted); } & .slice-more { margin-left:auto; }
|
||||
& .slice-task { display:flex; gap:10px; align-items:center; padding:8px 0; margin:0; & i { width:16px; height:16px; border:1px solid var(--card-muted); border-radius:5px; font-size:10px; display:grid; place-items:center; font-style:normal; } & b { margin-left:auto; width:26px; height:26px; border-radius:50%; background:#dce9f7; display:grid; place-items:center; font-size:10px; font-weight:450; color:#385677; } &:nth-of-type(2) b { background:#f4dfdb; color:#7c514b; } &:nth-of-type(3) b { background:#dde9df; color:#48614d; } &.done,&.checked { & i { background:var(--card-accent); color:var(--_card-on-accent); border-color:var(--card-accent); } & > span { color:var(--card-muted); text-decoration:line-through; } } }
|
||||
|
||||
& .slice-demo i { background:color-mix(in oklab,var(--card-accent) calc(var(--task-done,0)*100%),transparent);color:color-mix(in oklab,var(--_card-on-accent) calc(var(--task-done,0)*100%),transparent);border-color:color-mix(in oklab,var(--card-accent) calc(var(--task-done,0)*100%),var(--card-muted));transform:scale(calc(1 - sin(var(--task-done,0)*3.14159)*.12)); }
|
||||
& .slice-demo > span { color:color-mix(in oklab,var(--card-muted) calc(var(--task-done,0)*100%),var(--card-ink)); }
|
||||
& .slice-progress { display:flex; flex-wrap:wrap; justify-content:space-between; margin-top:15px; font-size:10px; color:var(--card-muted); & > div { width:100%; height:4px; margin-top:9px; background:var(--card-line); border-radius:3px; overflow:clip; & i { display:block; width:100%; height:100%; background:var(--card-accent); border-radius:inherit; transform-origin:left;transform:scaleX(.33333); } } }
|
||||
@media(max-width:650px) { & .slice-front { padding:18px; font-size:13px; } & .slice-front-heading { font-size:13px; } & .slice-progress { font-size:10px; } }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<article class="card bottom-title ui-slice" data-card="ui-slice" data-motion="auto">
|
||||
<div class="card-stage">
|
||||
<div class="slice-back"><div class="slice-toolbar"><span data-text="board">本周计划</span> <span>⋯</span></div><div class="slice-columns"><div><span data-text="columns.0">待开始</span><p data-text="backlog.0">整理首页内容</p><p data-text="backlog.1">确认品牌素材</p></div><div><span data-text="columns.1">进行中</span><p data-text="backlog.2">打磨购买体验</p><p data-text="backlog.3">移动端检查</p></div></div></div>
|
||||
<div class="slice-front"><div class="slice-front-heading"><span class="slice-symbol">◈</span> <span data-text="listTitle">上线前的最后一公里</span> <span class="slice-more">⋯</span></div><p class="slice-task done"><i>✓</i><span data-text="tasks.0.title">确认首页文案</span><b data-text="tasks.0.owner">林</b></p><p class="slice-task slice-demo"><i></i><span data-text="tasks.1.title">检查移动端体验</span><b data-text="tasks.1.owner">周</b></p><p class="slice-task"><i></i><span data-text="tasks.2.title">准备发布素材</span><b data-text="tasks.2.owner">陈</b></p><div class="slice-progress"><span data-text="progressLabel">发布清单</span><span class="slice-count">1 / 3</span><div><i></i></div></div></div>
|
||||
</div>
|
||||
<div class="card-caption"><h2 data-text="title">让好想法,稳稳落地</h2><p data-text="description">从计划到完成,团队始终在同一页。</p></div>
|
||||
</article>
|
||||
@@ -0,0 +1,12 @@
|
||||
import {loop,shell,smooth,staticMotion} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,options);
|
||||
const row=element.querySelector('.slice-demo'),box=row.querySelector('i'),bar=element.querySelector('.slice-progress i'),count=element.querySelector('.slice-count'),still=staticMotion(element,options);
|
||||
box.textContent='✓';
|
||||
const clock=loop(element,time=>{
|
||||
const staticState=still(),p=staticState?1:smooth((time-1.1)/.32),progress=staticState?1:smooth((time-1.2)/.90);
|
||||
row.style.setProperty('--task-done',p);row.classList.toggle('checked',p===1);
|
||||
bar.style.transform=`scaleX(${(1+progress)/3})`;count.textContent=progress===1?'2 / 3':'1 / 3';
|
||||
},{...options,duration:2.3});
|
||||
return {replay:()=>clock.restart(),destroy(){clock.destroy();clean();row.classList.remove('checked');row.style.removeProperty('--task-done');box.textContent='';bar.style.removeProperty('transform');count.textContent='1 / 3';}};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.vector-space {
|
||||
height:440px; aspect-ratio:auto; --_card-stage-track-min:min-content;
|
||||
& .card-stage { display:flex; flex-direction:column; justify-content:space-between; gap:100px; overflow:clip; background:radial-gradient(ellipse at 63% 52% in oklab,color-mix(in oklab,var(--card-accent) 5%,transparent),transparent 70%); }
|
||||
& .vector-canvas,& .vector-fallback { position:absolute; width:100%; height:100%; inset:0; }
|
||||
&[data-vector-ready="true"] .vector-fallback { display:none; }
|
||||
& .vector-query { display:flex; align-items:center; gap:9px; position:relative; z-index:2; align-self:flex-start; margin:4px 24px 0; max-width:calc(100% - 48px); padding:9px 13px; border:1px solid var(--card-line); border-radius:6px; background:var(--card-panel); font-size:12px; box-shadow:0 3px 9px #00000003; }
|
||||
& .vector-query span { min-width:0; }
|
||||
& .vector-query svg { flex:none; width:14px; height:14px; fill:none; stroke:var(--card-muted); stroke-width:1.4; }
|
||||
& .vector-result { display:flex; align-items:center; gap:7px; flex-wrap:wrap; position:relative; z-index:2; margin:0 24px 21px; font-size:11px; }
|
||||
& .vector-result i { flex:none; width:5px; height:5px; border-radius:50%; background:var(--card-accent); }
|
||||
& .vector-tag { margin-left:auto; color:var(--card-muted); }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<article class="card vector-space" data-card="vector-space" data-motion="auto">
|
||||
<h2 class="card-heading" data-text="title">从海量信息中,<br>找到彼此的关联</h2>
|
||||
|
||||
<div class="card-stage">
|
||||
<div class="vector-query"><svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="8" cy="8" r="5"/><path d="m12 12 5 5"/></svg><span data-text="query">自然的灵感</span></div>
|
||||
<svg class="vector-fallback" viewBox="0 0 400 300" aria-hidden="true"><g fill="none" stroke="currentColor" stroke-width=".8" opacity=".3"><path d="m200 55 107 58v122l-107 58-107-58V113Z M93 113l107 58 107-58 M200 171v122 M200 55v116"/></g><g fill="var(--card-accent)"><circle cx="195" cy="179" r="5"/><circle cx="228" cy="161" r="5"/><circle cx="212" cy="197" r="5"/><circle cx="250" cy="185" r="5"/></g></svg>
|
||||
<canvas class="vector-canvas" aria-hidden="true"></canvas>
|
||||
<div class="vector-result"><i></i><span data-text="result">4 个相关结果</span><span class="vector-tag" data-text="mode">语义搜索</span></div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,41 @@
|
||||
import {loop,shell,random,glProgram,fitCanvas,watchColors,mix,staticMotion} from '../shared/runtime.js';
|
||||
export function mount(element, options = {}) {
|
||||
const clean=shell(element,options);
|
||||
const canvas=element.querySelector('canvas');
|
||||
const vertex=`attribute vec3 a_position;attribute float a_selected;uniform vec2 u_size,u_rotation;uniform float u_dpr,u_halo;varying float v_selected,v_depth;
|
||||
void main(){float c=cos(u_rotation.x),s=sin(u_rotation.x);vec3 p=a_position;p.xz=mat2(c,-s,s,c)*p.xz;c=cos(u_rotation.y);s=sin(u_rotation.y);p.yz=mat2(c,-s,s,c)*p.yz;
|
||||
float perspective=1./(1.+p.z*.24);v_depth=perspective;v_selected=a_selected;
|
||||
gl_Position=vec4(p.x*perspective*.94/(u_size.x/u_size.y),p.y*perspective*.84-.02,0.,1.);
|
||||
gl_PointSize=mix(2.5,7.,a_selected)*u_dpr*perspective+u_halo*a_selected*15.*u_dpr;}`;
|
||||
const fragment=`precision highp float;uniform vec3 u_accent,u_ink;uniform float u_points,u_halo,u_lineAlpha;varying float v_selected,v_depth;
|
||||
void main(){float a=1.;if(u_points>.5){float r=length(gl_PointCoord-.5)*2.;a=1.-smoothstep(.65,1.,r);if(u_halo>.5){a=exp(-r*r*5.)*.23*v_selected;}}
|
||||
vec3 color=mix(u_ink,u_accent,v_selected);a*=u_points>.5?mix(.18,.95,v_selected)*v_depth:u_lineAlpha;
|
||||
gl_FragColor=vec4(color*a,a);}`;
|
||||
const gpu=glProgram(canvas,vertex,fragment),rng=random(29),positions=[],selected=[];
|
||||
const add=(v,s=0)=>{positions.push(...v);selected.push(s);};
|
||||
const corners=Array.from({length:8},(_,i)=>[(i&1?.65:-.65),(i&2?.65:-.65),(i&4?.65:-.65)]);
|
||||
for(let i=0;i<8;i++)for(const bit of [1,2,4])if(!(i&bit)){add(corners[i]);add(corners[i|bit]);}
|
||||
const related=[[-.08,-.06,.28],[.12,.04,.35],[.29,-.12,.23],[.08,-.27,.29]];
|
||||
for(let i=0;i<3;i++){add(related[i],1);add(related[i+1],1);}
|
||||
for(let i=0;i<140;i++)add([(rng()-.5)*2.1,(rng()-.5)*1.75,(rng()-.5)*1.8]);
|
||||
related.forEach(v=>add(v,1));
|
||||
if(gpu){gpu.attribute('a_position',positions,3);gpu.attribute('a_selected',selected,1);element.dataset.vectorReady='true';}
|
||||
else canvas.hidden=true;
|
||||
let palette,tx=0,ty=0,rx=0,ry=0,lost=false;
|
||||
const still=staticMotion(element,options);
|
||||
const draw=(time,dt)=>{if(!gpu||!palette||lost)return;if(still())rx=ry=tx=ty=0;if(dt){rx=mix(rx,tx,1-Math.exp(-dt*2.5));ry=mix(ry,ty,1-Math.exp(-dt*2.5));}const {w,h,dpr}=fitCanvas(canvas);const {gl}=gpu;
|
||||
gl.useProgram(gpu.program);gl.viewport(0,0,canvas.width,canvas.height);gl.clearColor(0,0,0,0);gl.clear(gl.COLOR_BUFFER_BIT);gl.enable(gl.BLEND);gl.blendFunc(gl.ONE,gl.ONE_MINUS_SRC_ALPHA);
|
||||
gl.uniform2f(gpu.uniform('u_size'),w,h);gl.uniform2f(gpu.uniform('u_rotation'),-.62+Math.sin(time*.12)*.06+rx,.35+ry);
|
||||
gl.uniform1f(gpu.uniform('u_dpr'),dpr);gl.uniform3fv(gpu.uniform('u_accent'),palette.a);gl.uniform3fv(gpu.uniform('u_ink'),palette.dark?[.8,.84,.9]:[.18,.23,.3]);
|
||||
gl.uniform1f(gpu.uniform('u_halo'),0);gl.uniform1f(gpu.uniform('u_points'),0);gl.uniform1f(gpu.uniform('u_lineAlpha'),.21);gl.drawArrays(gl.LINES,0,24);
|
||||
gl.uniform1f(gpu.uniform('u_lineAlpha'),.32);gl.drawArrays(gl.LINES,24,6);
|
||||
gl.uniform1f(gpu.uniform('u_points'),1);gl.uniform1f(gpu.uniform('u_halo'),1);gl.drawArrays(gl.POINTS,30,144);
|
||||
gl.uniform1f(gpu.uniform('u_halo'),0);gl.drawArrays(gl.POINTS,30,144);
|
||||
};
|
||||
const clock=loop(element,draw,{...options,settled:()=>Math.abs(rx-tx)+Math.abs(ry-ty)<.001}),off=watchColors(element,p=>{palette=p;clock.invalidate();}),ro=new ResizeObserver(()=>clock.invalidate());ro.observe(canvas);
|
||||
const move=e=>{if(e.pointerType==='touch' || still())return;const r=element.getBoundingClientRect();tx=((e.clientX-r.left)/r.width-.5)*.25;ty=((e.clientY-r.top)/r.height-.5)*.16;};
|
||||
const leave=()=>{tx=ty=0;},loss=e=>{e.preventDefault();lost=true;canvas.hidden=true;delete element.dataset.vectorReady;};
|
||||
element.addEventListener('pointermove',move);element.addEventListener('pointerleave',leave);element.addEventListener('pointercancel',leave);canvas.addEventListener('webglcontextlost',loss);
|
||||
|
||||
return{destroy(){clean();clock.destroy();off();ro.disconnect();gpu?.destroy();element.removeEventListener('pointermove',move);element.removeEventListener('pointerleave',leave);element.removeEventListener('pointercancel',leave);canvas.removeEventListener('webglcontextlost',loss);delete element.dataset.vectorReady;}};
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright 2022 The Instrument Serif Project Authors (https://github.com/Instrument/instrument-serif)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
@@ -1,35 +1,32 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="connect-src 'none'"><meta name="viewport" content="width=device-width,initial-scale=1"><title>风格对比</title><style>
|
||||
:root{--canvas:#f4f4f4;--surface:#fff;--well:#e8e8e8;--raised:#fff;--line:#e2e2e2;--line-2:#d0d0d0;--text:#161616;--soft:#5c5c5c;--faint:#8c8c8c;--ink:#161616;--hl:#ffe28a;--bar-h:48px;--mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;--brand:"Helvetica Neue",Helvetica,Arial,sans-serif;color-scheme:light;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:var(--text);background:var(--canvas);font-synthesis:none;-webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums;
|
||||
<html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="connect-src 'none'"><meta name="viewport" content="width=device-width,initial-scale=1"><title></title><link rel="icon" type="image/png" href="logo.png"><style>
|
||||
@font-face{font-family:"Instrument Serif";src:url("fonts/instrument-serif/InstrumentSerif-Regular.ttf") format("truetype");font-style:normal;font-weight:400;font-display:swap}
|
||||
:root{--canvas:#faf8f1;--surface:#faf8f1;--well:#eeeee5;--raised:#faf8f1;--line:#dcded3;--line-2:#c2c9bc;--text:#303c35;--soft:#606b60;--faint:#81897e;--ink:#303c35;--accent:#435a47;--chip:#e8eddf;--hl:#ffe28a;--bar-h:68px;--sans:-apple-system,BlinkMacSystemFont,"Inter","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;--mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;--brand:"Instrument Serif",Georgia,serif;color-scheme:light;font-family:var(--sans);color:var(--text);background:var(--canvas);font-synthesis:none;-webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums;
|
||||
/* 动效:咔嗒用于控件,滑行用于画面与切换,弹出用于确认;弹簧曲线由刚度/阻尼采样 */
|
||||
--snap:cubic-bezier(.2,.9,.25,1.04);--snap-d:330ms;--glide:cubic-bezier(.2,.8,.2,1);--glide-d:440ms;--pop:cubic-bezier(.3,1.5,.5,1);--exit:cubic-bezier(.4,0,1,1)}
|
||||
@supports (transition-timing-function:linear(0,1)){:root{
|
||||
--snap:linear(0,.058,.186,.339,.491,.626,.739,.827,.893,.941,.973,.994,1.005,1.011,1.013,1.013,1.012,1.01,1.008,1.006,1);
|
||||
--glide:linear(0,.028,.097,.186,.286,.384,.48,.565,.644,.71,.768,.816,.856,.889,.915,.936,.953,.966,.976,.983,.989,.993,1);
|
||||
--pop:linear(0,.087,.289,.533,.765,.954,1.084,1.156,1.176,1.161,1.126,1.083,1.042,1.009,.986,.973,.969,.971,.977,.985,.992,.998,1.002,1.005,1.005,1.005,1)}}
|
||||
*{box-sizing:border-box}body{margin:0;min-width:320px;background:var(--canvas)}h1,h2,p,dl,dd{margin:0}button,textarea,input{font:inherit;color:inherit}button{background:none;border:0;padding:0;cursor:pointer;-webkit-tap-highlight-color:transparent}:focus-visible{outline:2px solid var(--ink);outline-offset:3px}[hidden]{display:none!important}
|
||||
*{box-sizing:border-box}body{margin:0;min-width:320px;background:var(--canvas)}h1,h2,p,dl,dd{margin:0}button,textarea,input{font:inherit;color:inherit}button{background:none;border:0;padding:0;cursor:pointer;-webkit-tap-highlight-color:transparent}:focus-visible{outline:2px solid var(--accent);outline-offset:3px}[hidden]{display:none!important}
|
||||
|
||||
.bar{position:sticky;top:0;z-index:10;min-height:48px;display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:14px;padding:6px 20px;background:#f4f4f4e6;backdrop-filter:saturate(1.4) blur(14px);border-bottom:1px solid var(--line);view-transition-name:bar}
|
||||
.bar{position:sticky;top:0;z-index:10;min-height:68px;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:14px;padding:14px 24px;background:var(--canvas);border-bottom:1px solid var(--line);view-transition-name:bar}
|
||||
.bar-l{display:flex;align-items:center;gap:12px;min-width:0}
|
||||
.brand{display:flex;align-items:center;gap:8px;flex:none;color:var(--ink);text-decoration:none}.brand img{width:28px;height:28px;display:block;transition:transform .5s var(--snap)}.brand:hover img{transform:rotate(-10deg) scale(1.06)}.brand:focus-visible{outline:2px solid var(--ink);outline-offset:4px;border-radius:6px}
|
||||
.wm{font-family:var(--brand);font-size:19px;line-height:1;font-weight:700;letter-spacing:-.045em;display:flex;align-items:flex-start;gap:3px}.wm b{font-size:9.5px;line-height:1;font-weight:700;letter-spacing:.06em;padding:2px 4px 1.5px;margin-top:-1px;border-radius:4px;background:var(--hl);box-shadow:inset 0 0 0 1.4px var(--ink)}
|
||||
.sep{width:1px;height:18px;background:var(--line-2);flex:none}.proj{font-size:14px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.round{font-size:13px;color:var(--faint);white-space:nowrap}
|
||||
.bar-c{display:flex;gap:8px}.bar-r{display:flex;justify-content:flex-end;align-items:center;gap:12px}
|
||||
.seg{position:relative;display:flex;padding:2px;border-radius:8px;background:var(--well)}
|
||||
.seg button{position:relative;z-index:1;font-size:13px;line-height:1.5;padding:4px 11px;border-radius:6px;color:var(--soft);white-space:nowrap;transition:color .2s}.seg button:hover{color:var(--text)}.seg button[aria-pressed=true]{color:var(--text)}
|
||||
.thumb{position:absolute;left:0;top:0;width:0;border-radius:6px;background:var(--raised);box-shadow:0 1px 2px #0000001f,0 2px 8px -2px #00000014,inset 0 0 0 .5px #0000000f;transition:transform var(--snap-d) var(--snap),width var(--snap-d) var(--snap)}
|
||||
.toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--soft);cursor:pointer;white-space:nowrap}.toggle input{appearance:none;width:28px;height:16px;margin:0;border-radius:99px;background:var(--line-2);position:relative;cursor:pointer;transition:background .2s}.toggle input::after{content:"";position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:99px;background:#fff;box-shadow:0 1px 2px #0003;transition:transform var(--snap-d) var(--snap),width .15s}.toggle input:checked{background:var(--ink)}.toggle input:checked::after{transform:translateX(12px)}.toggle:active input::after{width:14px}.toggle:active input:checked::after{transform:translateX(10px)}
|
||||
.filter{position:relative;font-size:13px}.filter summary{list-style:none;cursor:pointer;color:var(--soft);padding:4px 10px;border-radius:6px;box-shadow:inset 0 0 0 1px var(--line);white-space:nowrap;transition:color .15s,box-shadow .15s}.filter summary::-webkit-details-marker{display:none}.filter summary:hover,.filter[open] summary{color:var(--text);box-shadow:inset 0 0 0 1px var(--line-2)}
|
||||
.filter-list{position:absolute;right:0;top:calc(100% + 8px);min-width:220px;display:grid;gap:2px;padding:6px;border-radius:10px;background:var(--raised);box-shadow:0 18px 44px -8px #00000026,0 0 0 1px #00000012;transform-origin:top right}.filter[open]>.filter-list{animation:pop-in var(--snap-d) var(--snap) both}
|
||||
.filter-list label{display:flex;align-items:center;gap:10px;padding:7px 8px;border-radius:6px;cursor:pointer;font-size:14px;transition:background .12s}.filter-list label:hover{background:#0000000a}.filter-list input{accent-color:var(--ink);width:15px;height:15px;margin:0}.filter-list .n{font-family:var(--mono);color:var(--faint);font-size:13px}
|
||||
.brand{display:flex;align-items:center;gap:10px;flex:none;color:var(--ink);text-decoration:none}.brand img{width:30px;height:30px;display:block;transition:transform .5s var(--snap)}.brand:hover img{transform:rotate(-10deg) scale(1.06)}.brand:focus-visible{outline:2px solid var(--accent);outline-offset:4px;border-radius:6px}
|
||||
.wm{font-family:var(--brand);font-size:36px;line-height:1;font-weight:400;letter-spacing:-.025em;white-space:nowrap}.pro-badge{align-self:center;margin-left:-2px;padding:4px 6px;border-radius:3px;background:var(--chip);color:var(--accent);font-family:var(--sans);font-size:10px;font-weight:600;line-height:1;letter-spacing:.08em}
|
||||
.sep{width:1px;height:18px;background:var(--line-2);flex:none}.proj{font-size:14px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.bar-c{display:flex;gap:8px;flex-wrap:wrap;min-width:0}
|
||||
.seg{position:relative;display:flex;padding:2px;border-radius:4px;background:var(--well)}
|
||||
.seg button{position:relative;z-index:1;font-size:13px;line-height:1.5;padding:4px 11px;border-radius:3px;color:var(--soft);white-space:nowrap;transition:color .2s}.seg button:hover{color:var(--text)}.seg button[aria-pressed=true]{color:var(--accent)}
|
||||
.thumb{position:absolute;left:0;top:0;width:0;border-radius:3px;background:var(--chip);box-shadow:inset 0 0 0 .5px #435a4714;transition:transform var(--snap-d) var(--snap),width var(--snap-d) var(--snap)}
|
||||
|
||||
.intro{max-width:1840px;margin:0 auto;padding:28px 24px 0}.intro p{font-size:14px;line-height:1.75;color:var(--soft);max-width:68ch}
|
||||
|
||||
.grid{max-width:1840px;margin:0 auto;padding:24px 24px 64px;display:grid;grid-template-columns:repeat(var(--cols,3),minmax(0,1fr));gap:48px 28px;align-items:start}
|
||||
.grid.first .card,.intro.first{animation:rise .7s var(--glide) both;animation-delay:calc(var(--i,0) * 70ms + 60ms)}
|
||||
.card{min-width:0;display:flex;flex-direction:column}
|
||||
.cap{display:flex;align-items:center;gap:12px;padding-top:14px}.cap .no{font-family:var(--mono);font-size:13px;color:var(--faint);transition:color .2s}.cap .name{font-size:16px;font-weight:600;line-height:1.4;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cap .strip-mini{margin:0 auto 0 4px;width:40px;flex:none}.cap .no.base{font-family:inherit;font-size:12px;font-weight:600;line-height:1.6;padding:0 8px;border-radius:99px;background:var(--well);color:var(--soft)}
|
||||
.lede{margin-top:8px;font-size:13.5px;line-height:1.7;color:var(--soft);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.grid[data-notes=off] .lede{display:none}
|
||||
.cap{display:flex;align-items:center;gap:12px;padding-top:14px}.cap .no{font-family:var(--mono);font-size:13px;color:var(--faint);transition:color .2s}.cap .name{font-size:16px;font-weight:600;line-height:1.4;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cap .strip-mini{margin:0 auto 0 4px;width:40px;flex:none}.cap .no.base{flex:none;white-space:nowrap;font-family:inherit;font-size:12px;font-weight:600;line-height:1.6;padding:0 8px;border-radius:99px;background:var(--well);color:var(--soft)}
|
||||
.lede{margin-top:8px;font-size:13.5px;line-height:1.7;color:var(--soft);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||||
.name{font-size:19px;line-height:1.35;font-weight:600;overflow-wrap:anywhere}.strip-mini{display:flex;height:4px;width:96px;margin-top:9px;border-radius:2px;overflow:hidden;box-shadow:0 0 0 1px #0000001f}.strip-mini i{flex:1}
|
||||
.frame{position:relative;background:var(--surface);border-radius:4px;box-shadow:0 0 0 1px #00000014,0 24px 48px -30px #00000040;transition:box-shadow .25s,transform .45s var(--glide)}.frame:hover{box-shadow:0 0 0 1px #0000002e,0 36px 64px -30px #00000052;transform:translateY(-3px)}.frame:active{transform:translateY(-1px) scale(.995);transition-duration:.25s,.12s}.frame[data-viewport=mobile]{padding:20px 0;background:var(--well)}
|
||||
.peek{position:absolute;inset:auto 0 0 0;z-index:2;padding:10px 12px;font-size:12.5px;color:#fff;background:linear-gradient(transparent,#000b);opacity:0;transform:translateY(6px);transition:opacity .18s,transform .3s var(--glide);pointer-events:none;border-radius:0 0 4px 4px}.frame:hover .peek,.hit:focus-visible~.peek{opacity:1;transform:none}
|
||||
@@ -43,10 +40,10 @@
|
||||
.pick-mark{position:absolute;top:10px;right:10px;z-index:3;display:none;align-items:center;gap:6px;padding:4px 10px 4px 8px;border-radius:99px;background:var(--ink);color:#fff;font-size:13px;font-weight:600;line-height:1.4;box-shadow:0 6px 16px #0000003d;pointer-events:none;transform-origin:85% 50%}.pick-mark::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--hl)}
|
||||
.is-chosen .pick-mark{display:inline-flex}.is-chosen .frame{box-shadow:0 0 0 2px var(--ink),0 30px 60px -30px #00000052}.is-chosen .cap .no{color:var(--text)}
|
||||
|
||||
.spec{display:grid;grid-template-columns:32px minmax(0,1fr);gap:10px 12px;font-size:13px;line-height:1.6}.spec dt{color:var(--faint)}.spec dd{color:var(--soft);min-width:0}
|
||||
.pick{display:inline-flex;align-items:center;justify-content:center;gap:8px;font-size:14px;line-height:1.5;padding:8px 16px;border-radius:8px;box-shadow:inset 0 0 0 1px var(--line-2);color:var(--text);white-space:nowrap;transition:background .2s,color .2s,box-shadow .2s,transform .35s var(--snap)}.pick:hover{background:#0000000a}.pick:active{transform:scale(.96);transition-duration:.2s,.2s,.2s,.08s}
|
||||
.spec{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:10px 12px;font-size:13px;line-height:1.6}.spec dt{color:var(--faint)}.spec dd{color:var(--soft);min-width:0;overflow-wrap:anywhere}
|
||||
.pick{display:inline-flex;align-items:center;justify-content:center;gap:8px;font-size:14px;line-height:1.5;padding:8px 16px;border-radius:8px;box-shadow:inset 0 0 0 1px var(--line-2);color:var(--text);white-space:nowrap;transition:background .2s,color .2s,box-shadow .2s,transform .35s var(--snap)}.pick:hover{background:var(--chip)}.pick:active{transform:scale(.96);transition-duration:.2s,.2s,.2s,.08s}
|
||||
.pick kbd{font-family:var(--mono);font-size:12px;color:var(--faint);padding:0 5px;border-radius:4px;box-shadow:inset 0 0 0 1px var(--line-2);transition:color .2s,box-shadow .2s}
|
||||
.pick[data-on]{background:var(--ink);color:#fff;box-shadow:none}.pick[data-on]:hover{background:#333}.pick[data-on] kbd{color:#bdbdbd;box-shadow:inset 0 0 0 1px #ffffff40}
|
||||
.pick[data-on]{background:var(--accent);color:#fff;box-shadow:none}.pick[data-on]:hover{background:var(--ink)}.pick[data-on] kbd{color:var(--chip);box-shadow:inset 0 0 0 1px #ffffff40}
|
||||
.pick[data-on]::before{content:"";width:9px;height:5px;margin:-3px 1px 0 0;border-left:1.6px solid;border-bottom:1.6px solid;transform:rotate(-45deg)}
|
||||
.cap .pick{flex:none;font-size:13px;padding:4px 12px;border-radius:6px}
|
||||
|
||||
@@ -59,37 +56,31 @@
|
||||
.nav{position:absolute;top:50%;z-index:2;width:44px;height:44px;margin-top:-22px;border-radius:50%;display:grid;place-items:center;font-size:18px;background:#ffffffd9;color:var(--ink);box-shadow:0 0 0 1px #00000014,0 8px 24px -6px #00000033;backdrop-filter:blur(8px);opacity:.85;transition:opacity .2s,transform .35s var(--snap)}.nav:hover{opacity:1}.nav:active{transform:scale(.9);transition-duration:.2s,.08s}.nav.prev{left:14px}.nav.next{right:14px}.nav:disabled{opacity:0;pointer-events:none;transform:scale(.8)}
|
||||
.info{grid-row:1;grid-column:2;overflow:auto;border-left:1px solid var(--line);background:var(--surface);display:flex;flex-direction:column;view-transition-name:info}
|
||||
.film{position:relative;display:grid;gap:2px;padding:10px;border-bottom:1px solid var(--line)}
|
||||
.film-ind{position:absolute;left:0;top:0;width:0;height:0;border-radius:8px;background:#f0f0f0;box-shadow:inset 0 0 0 1px var(--line);transition:transform var(--snap-d) var(--snap),width var(--snap-d) var(--snap),height var(--snap-d) var(--snap)}
|
||||
.film-ind{position:absolute;left:0;top:0;width:0;height:0;border-radius:8px;background:var(--chip);box-shadow:inset 0 0 0 1px var(--line);transition:transform var(--snap-d) var(--snap),width var(--snap-d) var(--snap),height var(--snap-d) var(--snap)}
|
||||
.film button{position:relative;z-index:1;display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:3px 10px;padding:7px 10px;border-radius:8px;color:var(--soft);text-align:left;transition:color .2s,background .15s}.film button:hover{color:var(--text)}.film button:not([aria-current=true]):hover{background:#00000006}.film button[aria-current=true]{color:var(--text)}
|
||||
.film .fn{font-family:var(--mono);font-size:12.5px;color:var(--faint);grid-row:span 2}.film .ft{font-size:13.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.film .strip-mini{margin:0;width:48px;height:3px}
|
||||
.film .ft.chosen::after{content:"";width:6px;height:6px;border-radius:50%;background:var(--ink);display:inline-block;margin-left:8px;vertical-align:middle}
|
||||
.info-body{padding:20px 20px 24px;display:flex;flex-direction:column;gap:16px}
|
||||
.idx{font-size:48px;line-height:.9;font-weight:200;letter-spacing:-.02em;color:var(--faint)}.info .name{font-size:20px;line-height:1.35;margin-top:-6px}.north{font-size:14px;line-height:1.7;color:#2b2b2b}
|
||||
.idx{font-size:48px;line-height:.9;font-weight:200;letter-spacing:-.02em;color:var(--faint)}.info .name{font-size:20px;line-height:1.35;margin-top:-6px}.north{font-size:14px;line-height:1.7;color:var(--text)}
|
||||
.big-sw{display:grid;gap:6px}.big-sw div{display:grid;grid-template-columns:32px 1fr;align-items:center;gap:10px;font-family:var(--mono);font-size:12.5px;color:var(--soft)}.big-sw i{height:20px;border-radius:4px;box-shadow:inset 0 0 0 1px #00000017}
|
||||
.info .pick{align-self:stretch;padding:8px 14px;font-size:13.5px}
|
||||
.tools{display:flex;gap:10px;flex-wrap:wrap;align-items:center}.tool{font-size:13px;padding:5px 12px;border-radius:6px;color:var(--soft);box-shadow:inset 0 0 0 1px var(--line-2);transition:color .15s,background .2s}.tool:hover{color:var(--text)}.tool[aria-pressed=true]{background:var(--ink);color:#fff;box-shadow:none}
|
||||
.tools{display:flex;gap:10px;flex-wrap:wrap;align-items:center}.tool{font-size:13px;padding:5px 12px;border-radius:6px;color:var(--soft);box-shadow:inset 0 0 0 1px var(--line-2);transition:color .15s,background .2s}.tool:hover{color:var(--text)}.tool[aria-pressed=true]{background:var(--accent);color:#fff;box-shadow:none}
|
||||
.hint{font-size:12px;line-height:1.9;color:var(--faint)}.hint kbd{font-family:var(--mono);font-size:12px;padding:0 5px;border-radius:4px;box-shadow:inset 0 0 0 1px var(--line-2);color:var(--soft)}
|
||||
.src{font-size:12px;line-height:1.6;color:var(--faint);overflow-wrap:anywhere}
|
||||
|
||||
.picked{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--soft);white-space:nowrap;max-width:220px;padding-left:12px;border-left:1px solid var(--line);overflow:hidden}.picked #decide-name{display:block;overflow:hidden;text-overflow:ellipsis}.picked.on #decide-name{color:var(--text);font-weight:600}
|
||||
.picked .strip-mini,.choice-empty{flex:none;margin:0;width:22px;height:12px;border-radius:3px}.choice-empty{box-shadow:inset 0 0 0 1px var(--line-2)}
|
||||
.notes-panel{width:340px;padding:10px}.notes-panel textarea{width:100%;min-height:110px;resize:vertical;padding:10px 12px;border:0;border-radius:6px;background:#f6f6f6;box-shadow:inset 0 0 0 1px var(--line);font-size:14px;line-height:1.7;transition:box-shadow .15s,background .15s}.notes-panel textarea:focus{outline:0;background:#fff;box-shadow:inset 0 0 0 1px var(--faint)}.notes-panel textarea::placeholder{color:var(--faint)}
|
||||
.state{font-size:12px;line-height:1.6;color:var(--faint);padding:8px 2px 0}.state:empty{display:none}.state[data-warn]{color:#b3401f}
|
||||
|
||||
.toast{position:fixed;left:50%;bottom:24px;z-index:40;display:flex;align-items:center;gap:12px;max-width:min(560px,calc(100vw - 32px));padding:10px 16px 10px 12px;border-radius:12px;background:var(--ink);color:#fff;box-shadow:0 18px 44px -10px #00000066;font-size:13.5px;line-height:1.5;opacity:0;transform:translate(-50%,14px) scale(.96);pointer-events:none;transition:opacity .16s var(--exit),transform .2s var(--exit)}
|
||||
.toast.show{opacity:1;transform:translate(-50%,0);pointer-events:auto;transition:opacity .18s ease-out,transform var(--snap-d) var(--snap)}
|
||||
.toast .tick{flex:none;width:22px;height:22px;border-radius:50%;background:var(--hl);display:grid;place-items:center}.toast .tick::before{content:"";width:8px;height:4px;margin-top:-2px;border-left:1.6px solid var(--ink);border-bottom:1.6px solid var(--ink);transform:rotate(-45deg)}.toast[data-fail] .tick{background:#c2462e}.toast[data-fail] .tick::before{width:2px;height:9px;margin:0;border:0;background:#fff;transform:none}
|
||||
.toast b{font-weight:600;white-space:nowrap}.toast code{font-family:var(--mono);font-size:12.5px;color:#a8a8a8;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;user-select:all}.toast[data-fail] code{white-space:pre-wrap}
|
||||
.toast b{font-weight:600;min-width:0}.toast code{flex:1;font-family:var(--mono);font-size:12.5px;color:#a8a8a8;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;user-select:all}.toast[data-fail] code{white-space:pre-wrap}
|
||||
|
||||
.none,.empty{max-width:560px;margin:18vh auto;padding:24px;text-align:center;font-size:15px;line-height:1.8;color:var(--soft)}.none button{margin-top:12px;color:var(--text);text-decoration:underline;text-underline-offset:4px}
|
||||
.empty{max-width:560px;margin:18vh auto;padding:24px;text-align:center;font-size:15px;line-height:1.8;color:var(--soft)}
|
||||
|
||||
@keyframes rise{from{opacity:0;transform:translateY(16px)}}
|
||||
@keyframes pop-in{from{opacity:0;transform:translateY(-4px) scale(.96)}}
|
||||
@keyframes vt-out{to{opacity:0;transform:scale(.97)}}
|
||||
@keyframes vt-in{from{opacity:0;transform:scale(.98)}}
|
||||
@keyframes info-in{from{transform:translateX(100%)}}
|
||||
@keyframes info-out{to{transform:translateX(100%)}}
|
||||
/* 并排与单张之间、视口与筛选变化用页面过渡:同一方向的画面从卡片放大到单张,其他卡片淡出,侧栏从右侧滑入 */
|
||||
/* 并排与单张之间、视口变化用页面过渡:同一方向的画面从卡片放大到单张,其他卡片淡出,侧栏从右侧滑入 */
|
||||
::view-transition-group(*){animation-duration:var(--glide-d);animation-timing-function:var(--glide)}
|
||||
::view-transition-old(*),::view-transition-new(*){height:100%;object-fit:cover;object-position:50% 0}
|
||||
::view-transition-old(root),::view-transition-new(root){animation-duration:.2s}
|
||||
@@ -98,65 +89,160 @@
|
||||
::view-transition-new(info):only-child{animation:info-in var(--glide-d) var(--glide) both}
|
||||
::view-transition-old(info):only-child{animation:info-out .22s var(--exit) both}
|
||||
::view-transition-old(bar){display:none}::view-transition-new(bar){animation:none}
|
||||
/* 视口、筛选、说明开关和缩放只改变形状与位置:外框直接变形,内容不做叠影 */
|
||||
/* 视口和缩放只改变形状与位置:外框直接变形,内容不做叠影 */
|
||||
html[data-vt=reshape]::view-transition-old(*){animation:none;opacity:0}
|
||||
html[data-vt=reshape]::view-transition-new(*){animation:none}
|
||||
html[data-vt=reshape]::view-transition-old(*):only-child{animation:vt-out .18s var(--exit) both;opacity:1}
|
||||
html[data-vt=reshape]::view-transition-new(*):only-child{animation:vt-in .3s .08s ease-out both}
|
||||
|
||||
@media(max-width:1180px){.grid{--cols:2!important}.bar{grid-template-columns:minmax(0,1fr) auto;row-gap:10px}.bar-r{grid-column:1/-1;justify-content:flex-start;flex-wrap:wrap}.loupe{grid-template-columns:minmax(0,1fr) 280px}}
|
||||
@media(max-width:760px){.bar{grid-template-columns:1fr;padding:10px 16px}.bar-c{flex-wrap:wrap}.intro{padding:24px 16px 0}.grid{--cols:1!important;padding:24px 16px 48px;gap:48px}.loupe{position:static;display:flex;flex-direction:column}.surface{flex:none}.track{height:auto;align-items:flex-start}.slide{height:auto;padding:16px 12px}.info{border-left:0;border-top:1px solid var(--line)}.film{display:flex;overflow-x:auto}.film button{flex:none}.info-body{padding:20px 16px}.nav{display:none}.picked{border-left:0;padding-left:0}.notes-panel{width:min(340px,calc(100vw - 32px))}.filter-list{right:auto;left:0;transform-origin:top left}}
|
||||
@media(max-width:1180px){.grid{--cols:2!important}.bar{grid-template-columns:minmax(0,1fr) auto;row-gap:10px}.loupe{grid-template-columns:minmax(0,1fr) 280px}}
|
||||
@media(max-width:760px){.bar{grid-template-columns:1fr;padding:10px 16px}.bar-c .seg{min-width:0}.bar-c .seg button{padding-inline:9px}.bar-c{flex-wrap:wrap}.intro{padding:24px 16px 0}.grid{--cols:1!important;padding:24px 16px 48px;gap:48px}.loupe{position:static;display:flex;flex-direction:column}.surface{flex:none}.track{height:auto;align-items:flex-start}.slide{height:auto;padding:16px 12px}.info{border-left:0;border-top:1px solid var(--line)}.film{display:flex;overflow-x:auto}.film button{flex:none}.info-body{padding:20px 16px}.nav{display:none}}
|
||||
@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition-duration:0s!important;animation-duration:0s!important;animation-delay:0s!important}.frame:hover{transform:none}}
|
||||
</style></head><body>
|
||||
<header class="bar" id="bar" hidden>
|
||||
<div class="bar-l"><a class="brand" href="https://www.oiloil.org" target="_blank" rel="noopener" title="Oil UI · oiloil.org" aria-label="Oil UI,打开 oiloil.org"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAAAHhlWElmTU0AKgAAAAgABAEaAAUAAAABAAAAPgEbAAUAAAABAAAARgEoAAMAAAABAAIAAIdpAAQAAAABAAAATgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAAGCgAwAEAAAAAQAAAGAAAAAAmrxreQAAAAlwSFlzAAALEwAACxMBAJqcGAAAJzZJREFUeAHtXQeAVEXSrsk7m4FdMuyCGAiCCioKKgb0PAUURDCBIhgxe+b/znQnqChGVO7MijmfYg6ACqKSc16UILJsYOPs9P993dPz3gwLLriKnDS8ee917qrq6uqq6rciu8NuCOyGwG4I7IbAnxUCvj/rwDnujIzQ3j6fZ0Q47JWqquiqnQEL785odGe32bJly3BKin9UWXn15JqoOtLj8VTu7D79adpv3DitiT/g+9wf8C5MS/MfJ0o8f5rB7+yBZiMEAr5pQMDHzZtn5GytPx07dgw2aNAgKz9fUraWZ3f8dkJAgdIDQd8LQMBXubm56cnFs7JS2obCgcuDQd+7gaB3Aa7loRT/F5wxyXl3v+8ABNLTg6f6/d712dkpee7iXIgB9CeQtgFsaXYg5L83HPYNCof9h6SlBTt3FAm689f38x+e/5Fy99ijQWZRUXXziorqVkpFW0UiNc2i0WiDaFRSARCOQYlXyv1eb5HH49vg9XrWgtILgsG01SNHjlw7d+5czxtvvvqtz+t7AHX8h0BkvSkpgctrampuEK/nu5SgZ9Tee3eZ/O2331Yz/U8dOO1TU33H+4PefwIQH+BajqsMl+IVCgZVVmamys3NUU0aN1Y5jRqqTLynhII6PZYvgvtPuL72+z1vBoPe+S1ahFty0c3Pz09JSfE95Qt4V4bDwYFEBvLtlLDTGk4ebevWWQ1++qn06MrKmgGg7MOR3tzv80nr1q2lffsO0rFjB9lzzz0lLy9PGjVqJFlZWRIOh8Xj9UqkOiKVlRVSUlIiRcXFsubHH2XZsmUyb948mTd3rixZulRKN2+uQZ3rIHdP8fo9GV6vL69Jk+xjCgo2/Gj7QkRkZ4faRiLSFs/BYNC/ukWL/PmYQVU2T33fdzoCGjUK7lNaGh1WWRk5BYNrk5GRId27d5e/HHec9OjRQ/baay9p0LBhreNWgJKZFB6BLF9rnvLyclmxYoVM/+Yb+fDDD+Xzzz+XVQUFzFuIGfBRSjjweKPslKnrN5T1j0bUuVGJtkJNnG0R3MO4F/n9gRvLyirfZaH/mdCgQXqnQMA7HgMqwqX2329/NXrUKLVgwQIF/g7YmgAeraqrq2NXFe68qlUkHpf4bNKdfDWRiK1K39evX69efOEFdeIJJ6hQKEQMRjGJlvv83o3pqaELuUhzo0ZRtEmT9MZYkAdjoe77PwN4DioY8t+FARWDwtRRRx2lXnvtNbV58+Y4oByAW8An3w0SqquqgIgtEcL46ipbxsnLuCgQqgOQ/M20aWrE8BEqPT2diKjAWvFKVlZq1/8ZYLsHQv4aTg8OBqdYgnh10IEHqbfeeENTMwFCSjWAjwHMReFbQ4iZBcxvyjizwsbVVlcsb6TaIAK/M2fMUGeecbry+XxExOZQyD+6bdsGWe7+79LPTZum50LWfpyAb9iwobpr9GhVWlqiARABILYGYMYTqPZKzOcA1wLenc/GETnm2SDFncfWZzHx7rv/Vft16aIXFr/PMz0jI3zILg14dj41K9DN65cZBP5RRx6pvv7qKz3emqjl7bUD0gLfAskBnAGkm+ptGtmR8+ysDU6cYVl8t/XaO+MYNm7cqK64/HLl83qJiE2pqYERnL0cyy4XwHIGoNPYGHnVNVdfrSAeqqrKShWJs5tEQFhAWaDY+5bxFmmWut3AdtKc8jaO+cyzG1k2H++QADQiuFBzj4H+K+wV7ujVq5d/l0IAKOdidLgqMyNDPfrII2pTYaH6ecMGVVFRAQTUDvhkQNv35Lub+i3wmMc+2/uWcRb4boQ5zyyn24pJTt9/953q0rmzRgLE1We6dm3OnfcfP4Sh1EIvo7k5Oerll15SG3/+WVN/WVlZHPgWOHrAMeDZ59ruFqjuu63DxNnZ4Kb2ROAaALvTbZkY4F1IZN0MP6xerY44/HA7E17q06frHxsJoPyLAPyali1aqInvvafWrV2rgY8NUZz1WMBZQNcG1OQ0yzLceROfCVgHuBrYFEcT4gzATftOXnd/TH6TxniGn376SR3bu7eZCSmBZ7t27Rqor2lQr4sLeOUp5eVVzzdt0jQwfvxj0qVLFyi9lN7Jpqam6mewVPTdNMtf580OycTwd2vBlEss6UFNNsa9Zto2TF1MMW3b+lmO/bHvzGfL2Lvf7xfMYunXr59MnjJFIKbeXVERuQYitbuYaWI7f01vtrNQbdmzs1P3Lyoq+zA9Na3Rgw8+KD0PO0wPhGqEzKxMwdqWUMw2bGItyNwjYorNlVBUv1jAEYW2tMllytmaTJpt24KUOS0qbKnE9kzLTpwPSFi1apUcDxXJvAULJDU1dAHUE4+a0jv+Wy82YSrSSksrIOd7Gl13/XVy2OGHA+BRSYGyjLqd2oDPoRmwmEGaZ/dA3MB3UhlrUmx6IiBtqilhczqlTLop49TKdm19tg9MNXF8guSmFYNPPvWUwFom5eWVd2Vmhg+2uXf0Xi9eEWUVkXsi1dE+gwedKiNHXgIPA6M8xKaLKsVt9M2CwA0ggXreKz6/TyC+xi+td9MoI/AM1SeDzNZi4p1U58mC1HbJANmWs7F8dzMri2ISVatWraRpkyby5ptvhWpqIl2z8hu8UFFYscNG/V+NgLS00LEV5dVj8lvnee8dOxb8MSTQtwiBT0qxILaDS7jHEzFk/CevJdDLyjZLQcEqWbpkiVYtE6FpWEOCQdQNIFgwGkC5a3QzHKaawCeHZdlY3m0NBsQmn52ZTLcddPJxTdt///1lKVTcM2bMaqbKol4oDD9i7t890LYK69NM6O3Vww8+qBYvWqT1Kguh0dxcWgr9jls2T5ZIkOaSUqj1nDJ5sho6dKhq2yZfpaelqZSUkEpNSVEZeO7YoYN67tlntabUSD9uicZpx0pPtd0TpSanjK0vMd3210pLsTsVeuCpFE/z8/KIobJfw4p+1e6uuHjTMPSl84BTTpajjj5aG0QCwYA0yM7W/J/U6lCeXRZJJ+i3AlVpHb7SlI/9gpx33nmaus4//wLp3LmznkXMvWbNGvl66lRdP98NPTr1Wfpkmptm7bs7nXHmnTn5ZIJlM3wzdThpNoe+IxqsR5q3aCH/93//J+cOHx4uK6u6ZeBAOeHll4VGn+0Kya3UufB1F+7bduzjc7/w+lJaPPPU09IBFqvKykrNglqgc6lpaTGx01aZOGAbyztmEdjNUg3gA7puWxuMXTRKJHfbgiwRKazbpFig25hYeS4s+tGjiYB1kw3a+qPRmpgAYWtheVOWBiAuzMeA8CCaKkhFfSAV/Zc5tifskBSkZh6btmZNySMVldEWfU44Qfbt3EkvvFDnShoA7wB/y447IHHSKCXR3Ejgc1DbujQA4kXtg4MQPjlvDijMIs53prIcLp0RtI/79dddK10P6Co9e/SU8zETX3v1VSkt3awR4ra2sYguhgq53lHq8yID1qm/9eol281RdggBi9d5B3475+fe4XCqDD7tNKkorwDQDPXkNm6MLtqgu2pfYoCxcebOX15mG2UWYlKhvYhUEyywYwV0pJtxONQfA2+c+k1WV3lToVMRkvbdt7OcfPLJmvXNnj1bzjrrLCDkAHnyiScwQ41ExmLsp62fwkbv3r3lkEMPxfhVz+nTUw6LV13Hh+3GmFrYJ+exCauvnbukRGDW07JxBB0hMmgkBzWgl3awvBtAOyBmjAEW83pjAF6/bp0sX74c0k8BKK8UG51UXXebNvnSpGkzPRzLEgzCnJpNbebXItJJZVG3dMR3U4Oth5LN6WecwQQduHZBkJBHHnlELrzoQvnqq69k7H33QQoLOENDzqiKasnsggsukClTpvgqK6vOQ/Snppbf6FfN63/uqce10ETwwP33q9mzZmnJZ87s2WoDdCZYoGIGEEoZVoJwSxwmDoNW1A+9DnMktviqcW6ulnYawWDTvFkzlduokcpIT1N8h4FevTBhgoJ4ymIJWk8j7bDO2tpK7oPNY+4s60g+Ng2KuZg2lG198vHHqtcRRyiIxDEJzJUP5Sm90YbQJj+fMCmETTl/e0C/XSxIqfMC8xYXDfl06jpp26atdOvWTS+0pCSyCj82T5yf7ImhR9MVpttAOZ/sBYo68NsecsaZZ8CtpFruuOMOrWeZM2eO8JqNC1QlY8aM0TPrnGHDpMehPeTjjz7S5fVMQ6WaEjRFu1sxsaZNG296xTjLuJwYPjlzBnA36xDcXY486ij56OOPpXVea0g/FHJsfbG2sVBzv9O3r7bbZ5eX1/xVV/Zb/Ki5Aw5+9o5u3PWp/iefrGbNnKm++/ZbNX/ePLVqxQot1ydSlZvCjEG8qqpS3XD9dSoQCKhBgwbpOkhpNtBoTvtw3HgeS2BbJ2GmBFFuzN1361hS6haUb43xeo+R2D6p3ZkxJo0y/bYC1ra4Ct2ZLaasqctoTD/++CNFwxMM++8Bfw6WfgER27cGqOre38z+WesWDoHvDqkZnddXgCqHeLOWtmJ35PH6vFKNhfq8EefJq5AwuLhZvguAa0omg6XEofk49wmgLhv2xb7gtddfl/vBLine truncated
|
||||
<div class="bar-l"><a class="brand" href="https://www.oiloil.org" target="_blank" rel="noopener" data-i18n-title="brandTitle" data-i18n-aria-label="brandLabel"><img src="logo.png" alt=""><span class="wm">Oil UI</span><span id="pro-badge" class="pro-badge">PRO</span></a><span class="sep" aria-hidden="true"></span><span class="proj" id="project"></span></div>
|
||||
<div class="bar-c">
|
||||
<div class="seg" role="group" aria-label="查看方式"><span class="thumb" aria-hidden="true"></span><button type="button" data-layout="compare" aria-pressed="true">并排</button><button type="button" data-layout="loupe" aria-pressed="false">单张</button></div>
|
||||
<div class="seg" role="group" aria-label="预览视口"><span class="thumb" aria-hidden="true"></span><button type="button" data-viewport="desktop" aria-pressed="true">桌面 1280</button><button type="button" data-viewport="mobile" aria-pressed="false">手机 390</button></div>
|
||||
</div>
|
||||
<div class="bar-r">
|
||||
<label class="toggle"><input id="notes-toggle" type="checkbox" checked>设计说明</label>
|
||||
<details class="filter"><summary id="filter-summary"></summary><div id="filter-list" class="filter-list"></div></details>
|
||||
<span class="picked" id="picked" title="当前选择的方向"><span id="choice-chip" class="choice-empty"></span><span id="decide-name">未选择方向</span></span>
|
||||
<details class="filter"><summary id="notes-open">备注</summary><div class="filter-list notes-panel"><textarea id="notes" aria-label="备注" placeholder="喜欢哪里、要改哪里,可以用编号指明方向。"></textarea><p id="state" class="state"></p></div></details>
|
||||
<div class="seg" role="group" data-i18n-aria-label="layoutLabel"><span class="thumb" aria-hidden="true"></span><button type="button" data-layout="compare" aria-pressed="true" data-i18n="compare"></button><button type="button" data-layout="loupe" aria-pressed="false" data-i18n="loupe"></button></div>
|
||||
<div class="seg" role="group" data-i18n-aria-label="viewportLabel"><span class="thumb" aria-hidden="true"></span><button type="button" data-viewport="desktop" aria-pressed="true" data-i18n="desktop"></button><button type="button" data-viewport="mobile" aria-pressed="false" data-i18n="mobile"></button></div>
|
||||
</div>
|
||||
</header>
|
||||
<div id="compare-view" hidden>
|
||||
<section class="intro"><p id="brief"></p></section>
|
||||
<main id="grid" class="grid" aria-label="候选方向"></main>
|
||||
<main id="grid" class="grid" data-i18n-aria-label="candidates"></main>
|
||||
</div>
|
||||
<main id="loupe" class="loupe" hidden aria-label="单张查看">
|
||||
<main id="loupe" class="loupe" hidden data-i18n-aria-label="singleView">
|
||||
<div class="view">
|
||||
<button type="button" class="nav prev" id="prev" aria-label="上一个方向">←</button>
|
||||
<button type="button" class="nav prev" id="prev" data-i18n-aria-label="previous">←</button>
|
||||
<div id="surface" class="surface"><div id="track" class="track"></div></div>
|
||||
<button type="button" class="nav next" id="next" aria-label="下一个方向">→</button>
|
||||
<button type="button" class="nav next" id="next" data-i18n-aria-label="next">→</button>
|
||||
</div>
|
||||
<aside id="info" class="info"><nav id="film" class="film" aria-label="切换方向"></nav><div id="info-body" class="info-body" aria-live="polite"></div></aside>
|
||||
<aside id="info" class="info"><nav id="film" class="film" data-i18n-aria-label="switchDirection"></nav><div id="info-body" class="info-body" aria-live="polite"></div></aside>
|
||||
</main>
|
||||
<div id="none" class="none" hidden>当前没有要比较的方向。<br><button type="button" id="show-all">显示全部</button></div>
|
||||
<div id="empty" class="empty" hidden>尚未加入设计小样。按 references/style-explorer.md 准备 manifest 后用生成器组装。</div>
|
||||
<div id="empty" class="empty" hidden data-i18n="empty"></div>
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"><span class="tick"></span><b></b><code></code></div>
|
||||
<script>
|
||||
const DATA = /*__OIL_UI_DATA__*/ null;
|
||||
// Interface strings: edit both locales here; manifest content stays as supplied.
|
||||
const I18N = {
|
||||
"zh": {
|
||||
"htmlLang": "zh-CN",
|
||||
"pageTitle": "风格对比",
|
||||
"projectTitle": "{project} · 风格对比 · Oil UI{edition}",
|
||||
"brandTitle": "Oil UI{edition} · oiloil.org",
|
||||
"brandLabel": "Oil UI{edition},打开 oiloil.org",
|
||||
"layoutLabel": "查看方式",
|
||||
"compare": "并排",
|
||||
"loupe": "单张",
|
||||
"viewportLabel": "预览视口",
|
||||
"desktop": "桌面 1280",
|
||||
"mobile": "手机 390",
|
||||
"candidates": "候选方向",
|
||||
"singleView": "单张查看",
|
||||
"previous": "上一个方向",
|
||||
"next": "下一个方向",
|
||||
"switchDirection": "切换方向",
|
||||
"empty": "尚未加入设计小样。按 references/style-explorer.md 准备 manifest 后用生成器组装。",
|
||||
"baseline": "现状",
|
||||
"serviceUnavailable": "开发服务器没有运行",
|
||||
"serviceHelp": "在对话里跟 Agent 说“打开对比页”就行。",
|
||||
"copyCommand": "复制命令",
|
||||
"copyCommandLabel": "复制启动命令",
|
||||
"live": "本地运行",
|
||||
"image": "静态图",
|
||||
"interactive": "可操作",
|
||||
"fonts": "字体",
|
||||
"traits": "特征",
|
||||
"viewDirection": "单张查看 {name}",
|
||||
"selected": "已选择",
|
||||
"peek": "点击单张查看 · 实际尺寸",
|
||||
"fit": "适应窗口",
|
||||
"actual": "实际尺寸 100%",
|
||||
"keyboardHint": "<kbd>←</kbd> <kbd>→</kbd> 切换方向 <kbd>P</kbd> 选择并复制<br><kbd>Esc</kbd> 回到并排",
|
||||
"selectedCopy": "已选择 · 复制",
|
||||
"select": "选择",
|
||||
"selectDirection": "选这个方向",
|
||||
"recopyTitle": "再点一次,重新复制这个方向",
|
||||
"selectTitle": "选择这个方向,并复制给 Agent",
|
||||
"copyRound": "{round}:",
|
||||
"copyChoice": "{round}选 {number} {name}",
|
||||
"copied": "已复制,粘贴给 Agent",
|
||||
"copyFailed": "复制失败,请手动复制"
|
||||
},
|
||||
"en": {
|
||||
"htmlLang": "en",
|
||||
"pageTitle": "Style comparison",
|
||||
"projectTitle": "{project} · Style comparison · Oil UI{edition}",
|
||||
"brandTitle": "Oil UI{edition} · oiloil.org",
|
||||
"brandLabel": "Oil UI{edition} — visit oiloil.org",
|
||||
"layoutLabel": "View mode",
|
||||
"compare": "Compare",
|
||||
"loupe": "Single",
|
||||
"viewportLabel": "Preview size",
|
||||
"desktop": "Desktop 1280",
|
||||
"mobile": "Mobile 390",
|
||||
"candidates": "Design directions",
|
||||
"singleView": "Single view",
|
||||
"previous": "Previous direction",
|
||||
"next": "Next direction",
|
||||
"switchDirection": "Switch direction",
|
||||
"empty": "No previews yet. Prepare a manifest as described in references/style-explorer.md, then run the generator.",
|
||||
"baseline": "Current",
|
||||
"serviceUnavailable": "The dev server isn't running",
|
||||
"serviceHelp": "Ask your agent to “open the comparison page.”",
|
||||
"copyCommand": "Copy command",
|
||||
"copyCommandLabel": "Copy start command",
|
||||
"live": "Live",
|
||||
"image": "Image",
|
||||
"interactive": "Interactive",
|
||||
"fonts": "Fonts",
|
||||
"traits": "Traits",
|
||||
"viewDirection": "View {name}",
|
||||
"selected": "Selected",
|
||||
"peek": "Click to view alone · actual size",
|
||||
"fit": "Fit to window",
|
||||
"actual": "Actual size (100%)",
|
||||
"keyboardHint": "<kbd>←</kbd> <kbd>→</kbd> Switch direction · <kbd>P</kbd> Select and copy<br><kbd>Esc</kbd> Back to compare",
|
||||
"selectedCopy": "Selected · Copy",
|
||||
"select": "Select",
|
||||
"selectDirection": "Choose this direction",
|
||||
"recopyTitle": "Click again to copy this direction",
|
||||
"selectTitle": "Choose this direction and copy a message for your agent",
|
||||
"copyRound": "Round {round}: ",
|
||||
"copyChoice": "{round}Go with {number} {name}",
|
||||
"copied": "Copied. Paste it to your agent.",
|
||||
"copyFailed": "Couldn't copy. Please copy the text manually."
|
||||
}
|
||||
};
|
||||
const IS_PRO = DATA?.edition !== 'open';
|
||||
const LANG = DATA?.lang || (/^zh/i.test(navigator.language || '') ? 'zh' : 'en');
|
||||
function t(key, values={}) {
|
||||
values={edition:IS_PRO?' Pro':'',...values};
|
||||
return I18N[LANG][key].replace(/\{(\w+)\}/g, (_, name)=>String(values[name] ?? ''));
|
||||
}
|
||||
(()=>{
|
||||
const $=s=>document.querySelector(s),$$=s=>document.querySelectorAll(s);
|
||||
document.documentElement.lang=t('htmlLang');
|
||||
document.title=DATA?.project?t('projectTitle',{project:DATA.project}):t('pageTitle');
|
||||
$$('[data-i18n]').forEach(n=>n.textContent=t(n.dataset.i18n));
|
||||
['aria-label','title','placeholder'].forEach(attr=>$$(`[data-i18n-${attr}]`).forEach(n=>n.setAttribute(attr,t(n.getAttribute(`data-i18n-${attr}`)))));
|
||||
$('#pro-badge').hidden=!IS_PRO;
|
||||
if(!DATA?.candidates?.length){$('#empty').hidden=false;return;}
|
||||
const C=DATA.candidates,SIZES={desktop:{w:1280,h:900},mobile:{w:390,h:844}},KEY=`oil-ui:${DATA.fingerprint||DATA.project}`;
|
||||
const byId=Object.fromEntries(C.map(c=>[c.id,c])),DIRS=C.filter(c=>!c.baseline),no=c=>c.baseline?'现状':String(DIRS.indexOf(c)+1).padStart(2,'0'),vtName=c=>`stage-${C.indexOf(c)}`;
|
||||
const byId=Object.fromEntries(C.map(c=>[c.id,c])),DIRS=C.filter(c=>!c.baseline),no=c=>c.baseline?t('baseline'):String(DIRS.indexOf(c)+1).padStart(2,'0'),vtName=c=>`stage-${C.indexOf(c)}`;
|
||||
const root=document.documentElement,calm=matchMedia('(prefers-reduced-motion: reduce)'),narrow=()=>matchMedia('(max-width:760px)').matches;
|
||||
const ease=n=>getComputedStyle(root).getPropertyValue(`--${n}`).trim()||'ease-out';
|
||||
let st={layout:'compare',viewport:'desktop',notesOn:true,visible:C.map(c=>c.id),current:C[0].id,chosen:null,notes:''},stored=true,zoomActual=false,compareScroll=0,gridKey='',loupeKey='',toastTimer=0;
|
||||
try{const saved=JSON.parse(localStorage.getItem(KEY)||'null');if(saved&&typeof saved==='object'){Object.assign(st,saved);st.visible=(Array.isArray(st.visible)?st.visible:[]).filter(id=>byId[id]);if(!byId[st.chosen])st.chosen=null;if(!byId[st.current])st.current=C[0].id;if(!['compare','loupe'].includes(st.layout))st.layout='compare';if(!SIZES[st.viewport])st.viewport='desktop';}}catch{stored=false;}
|
||||
function save(){if(!stored)return;try{localStorage.setItem(KEY,JSON.stringify(st));}catch{stored=false;status();}}
|
||||
let st={layout:'compare',viewport:'desktop',current:C[0].id,chosen:null},stored=true,zoomActual=false,compareScroll=0,gridKey='',loupeKey='',toastTimer=0;
|
||||
try{const saved=JSON.parse(localStorage.getItem(KEY)||'null');if(saved&&typeof saved==='object'){for(const key of Object.keys(st))if(key in saved)st[key]=saved[key];if(!byId[st.chosen])st.chosen=null;if(!byId[st.current])st.current=C[0].id;if(!['compare','loupe'].includes(st.layout))st.layout='compare';if(!SIZES[st.viewport])st.viewport='desktop';}}catch{stored=false;}
|
||||
function save(){if(!stored)return;try{localStorage.setItem(KEY,JSON.stringify(st));}catch{stored=false;}}
|
||||
const el=(tag,cls,text)=>{const n=document.createElement(tag);if(cls)n.className=cls;if(text!=null)n.textContent=text;return n;};
|
||||
const shown=()=>C.filter(c=>st.visible.includes(c.id));
|
||||
const shown=()=>C;
|
||||
const services=new Map(C.filter(c=>c.kind==='url').map(c=>[c.content,{ready:null,pending:false}]));
|
||||
const startCommand=DATA.serve?(DATA.serve.cwd?`cd "${DATA.serve.cwd.replace(/[\\"$`]/g,'\\$&')}" && ${DATA.serve.command}`:DATA.serve.command):'';
|
||||
|
||||
function serviceNotice(){
|
||||
const note=el('div','service-note'),message=el('div','service-message');note.hidden=true;note.setAttribute('role','status');
|
||||
message.append(el('h3',null,'开发服务器没有运行'),el('p',null,'在对话里跟 Agent 说“打开对比页”就行。'));
|
||||
if(startCommand){const row=el('div','service-command'),button=el('button','tool','复制命令');button.type='button';button.dataset.copyServe='';button.setAttribute('aria-label','复制启动命令');button.addEventListener('click',()=>copy(startCommand));row.append(el('code',null,startCommand),button);message.append(row);}
|
||||
message.append(el('h3',null,t('serviceUnavailable')),el('p',null,t('serviceHelp')));
|
||||
if(startCommand){const row=el('div','service-command'),button=el('button','tool',t('copyCommand'));button.type='button';button.dataset.copyServe='';button.setAttribute('aria-label',t('copyCommandLabel'));button.addEventListener('click',()=>copy(startCommand));row.append(el('code',null,startCommand),button);message.append(row);}
|
||||
note.append(message);return note;
|
||||
}
|
||||
function syncService(s,c){
|
||||
@@ -192,8 +278,7 @@ const DATA = /*__OIL_UI_DATA__*/ null;
|
||||
const placeThumbs=instant=>$$('.seg').forEach(s=>slideTo(s.querySelector('.thumb'),s.querySelector('[aria-pressed=true]'),instant));
|
||||
const placeFilm=instant=>{const f=$('#film');slideTo(f.querySelector('.film-ind'),f.querySelector('[aria-current=true]'),instant);};
|
||||
|
||||
document.title=`${DATA.project} · Oil UI`;
|
||||
$('#project').textContent=DATA.project;$('#round').textContent=DATA.round||'';
|
||||
$('#project').textContent=DATA.project;
|
||||
$('#brief').textContent=DATA.brief||'';
|
||||
|
||||
function stripMini(c){const s=el('span','strip-mini');(c.palette||[]).forEach(h=>{const i=el('i');i.style.background=h;s.append(i);});return s;}
|
||||
@@ -206,13 +291,13 @@ const DATA = /*__OIL_UI_DATA__*/ null;
|
||||
if(c.kind==='image')m.src=c.content;else if(c.kind!=='url')m.srcdoc=c.content;
|
||||
s.append(m);
|
||||
if(c.kind==='url'){s.append(serviceNotice());syncService(s,c);}
|
||||
if(c.kind!=='html'||c.interactive)s.append(el('span','static',c.kind==='url'?'本地运行':c.kind==='image'?'静态图':'可操作'));
|
||||
if(c.kind!=='html'||c.interactive)s.append(el('span','static',c.kind==='url'?t('live'):c.kind==='image'?t('image'):t('interactive')));
|
||||
return s;
|
||||
}
|
||||
function fit(s){
|
||||
if(!s.isConnected)return;
|
||||
const size=SIZES[st.viewport],m=s.querySelector('iframe,img'),box=s.parentElement;let r;
|
||||
if(box.classList.contains('slide')){const cs=getComputedStyle(box),w=box.clientWidth-parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight),h=narrow()?Infinity:box.clientHeight-parseFloat(cs.paddingTop)-parseFloat(cs.paddingBottom);r=box.dataset.zoom==='actual'?1:Math.min(w/size.w,h/size.h,1);}
|
||||
const native=byId[s.dataset.id].nativeViewport,size=native?{w:native.width,h:native.height}:SIZES[st.viewport],m=s.querySelector('iframe,img'),box=s.parentElement;let r;
|
||||
if(box.classList.contains('slide')){const cs=getComputedStyle(box),w=box.clientWidth-parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight),h=narrow()?(native?innerHeight-$('#bar').offsetHeight-parseFloat(cs.paddingTop)-parseFloat(cs.paddingBottom):Infinity):box.clientHeight-parseFloat(cs.paddingTop)-parseFloat(cs.paddingBottom);r=box.dataset.zoom==='actual'?1:Math.min(w/size.w,h/size.h,1);}
|
||||
else r=Math.min(box.clientWidth/size.w,1);
|
||||
if(!(r>0))return;
|
||||
s.style.width=`${size.w*r}px`;s.style.height=`${size.h*r}px`;m.style.width=`${size.w}px`;m.style.height=`${size.h}px`;m.style.transform=`scale(${r})`;
|
||||
@@ -221,14 +306,14 @@ const DATA = /*__OIL_UI_DATA__*/ null;
|
||||
|
||||
function specList(c){
|
||||
const dl=el('dl','spec');const row=(k,v)=>{dl.append(el('dt',null,k),el('dd',null,v));};
|
||||
row('字体',c.typography);row('特征',(c.traits||[]).join(' · '));return dl;
|
||||
row(t('fonts'),c.typography);row(t('traits'),(c.traits||[]).join(' · '));return dl;
|
||||
}
|
||||
function pickButton(c){const b=el('button','pick');b.type='button';b.dataset.pick=c.id;b.addEventListener('click',()=>choose(c.id));return b;}
|
||||
function card(c,i){
|
||||
const a=el('article','card');a.dataset.id=c.id;a.style.setProperty('--i',i);a.setAttribute('aria-label',c.name);
|
||||
const frame=el('div','frame');frame.style.viewTransitionName=vtName(c);const s=stage(c,false);
|
||||
const hit=el('button','hit');hit.type='button';hit.setAttribute('aria-label',`单张查看 ${c.name}`);hit.addEventListener('click',()=>setLayout('loupe',c.id));
|
||||
s.append(hit);frame.append(s,el('span','pick-mark','已选择'),el('span','peek','点击单张查看 · 实际尺寸'));
|
||||
const hit=el('button','hit');hit.type='button';hit.setAttribute('aria-label',t('viewDirection',{name:c.name}));hit.addEventListener('click',()=>setLayout('loupe',c.id));
|
||||
s.append(hit);frame.append(s,el('span','pick-mark',t('selected')),el('span','peek',t('peek')));
|
||||
const cap=el('div','cap');cap.style.viewTransitionName=`cap-${C.indexOf(c)}`;cap.append(el('span',c.baseline?'no base':'no',no(c)),el('h2','name',c.name),stripMini(c),pickButton(c));
|
||||
const lede=el('p','lede',c.concept);lede.title=c.concept;lede.style.viewTransitionName=`lede-${C.indexOf(c)}`;
|
||||
a.append(frame,cap,lede);return a;
|
||||
@@ -236,8 +321,8 @@ const DATA = /*__OIL_UI_DATA__*/ null;
|
||||
function syncCompare(){
|
||||
const list=shown(),g=$('#grid'),key=list.map(c=>c.id).join();
|
||||
if(key!==gridKey){gridKey=key;g.replaceChildren(...list.map(card));}
|
||||
g.dataset.notes=st.notesOn?'on':'off';g.style.setProperty('--cols',Math.min(Math.max(list.length,1),st.viewport==='mobile'?4:3));
|
||||
$$('.card .frame').forEach(f=>f.dataset.viewport=st.viewport);
|
||||
g.style.setProperty('--cols',Math.min(Math.max(list.length,1),st.viewport==='mobile'?4:3));
|
||||
$$('.card .frame').forEach(f=>f.dataset.viewport=byId[f.parentElement.dataset.id].nativeViewport?'native':st.viewport);
|
||||
}
|
||||
|
||||
// 单张查看:所有方向预先排成一条横向轨道,切换时整条轨道滑动,不重新加载画面
|
||||
@@ -263,28 +348,25 @@ const DATA = /*__OIL_UI_DATA__*/ null;
|
||||
function renderInfo(dir){
|
||||
const c=byId[st.current],body=$('#info-body');
|
||||
const sw=el('div','big-sw');(c.palette||[]).forEach(h=>{const d=el('div');const x=el('i');x.style.background=h;d.append(x,h.toUpperCase());sw.append(d);});
|
||||
const tools=el('div','tools');const z=el('button','tool',zoomActual?'适应窗口':'实际尺寸 100%');z.id='zoom';z.type='button';z.setAttribute('aria-pressed',String(zoomActual));z.addEventListener('click',toggleZoom);tools.append(z);
|
||||
const hint=el('p','hint');hint.innerHTML='<kbd>←</kbd> <kbd>→</kbd> 切换方向 <kbd>P</kbd> 选择并复制<br><kbd>Esc</kbd> 回到并排';
|
||||
const tools=el('div','tools');const z=el('button','tool',zoomActual?t('fit'):t('actual'));z.id='zoom';z.type='button';z.setAttribute('aria-pressed',String(zoomActual));z.addEventListener('click',toggleZoom);tools.append(z);
|
||||
const hint=el('p','hint');hint.innerHTML=t('keyboardHint');
|
||||
const blocks=[el('span','idx',no(c)),el('h2','name',c.name)];
|
||||
if(st.notesOn)blocks.push(el('p','north',c.concept),sw,specList(c));
|
||||
blocks.push(pickButton(c),tools,hint,el('p','src',c.kind==='image'?`静态图:${c.sourceLabel}。缩放不代表响应式重排。`:c.kind==='url'?`实时运行 ${c.sourceLabel}。空白时确认开发服务器已启动,且允许被嵌入。`:c.interactive?`单张查看时可以直接操作 ${c.sourceLabel};无法联网,也不保留浏览器存储。`:`预览禁用脚本与提交;交互请直接打开 ${c.sourceLabel}。`));
|
||||
blocks.push(el('p','north',c.concept),sw,specList(c));
|
||||
blocks.push(pickButton(c),tools,hint);
|
||||
body.replaceChildren(...blocks);paintChoice();
|
||||
if(dir)animate(body,[{opacity:0,transform:`translateX(${dir*18}px)`},{opacity:1,transform:'none'}],{duration:420,easing:ease('glide')});
|
||||
}
|
||||
function toggleZoom(){
|
||||
morph(()=>{zoomActual=!zoomActual;const sl=$('.slide.is-current');sl.dataset.zoom=zoomActual?'actual':'fit';fitAll();sl.scrollTo(0,0);const z=$('#zoom');z.textContent=zoomActual?'适应窗口':'实际尺寸 100%';z.setAttribute('aria-pressed',String(zoomActual));},'reshape');
|
||||
morph(()=>{zoomActual=!zoomActual;const sl=$('.slide.is-current');sl.dataset.zoom=zoomActual?'actual':'fit';fitAll();sl.scrollTo(0,0);const z=$('#zoom');z.textContent=zoomActual?t('fit'):t('actual');z.setAttribute('aria-pressed',String(zoomActual));},'reshape');
|
||||
}
|
||||
|
||||
function render(){
|
||||
const list=shown(),loupe=st.layout==='loupe'&&list.length>0;
|
||||
$('#compare-view').hidden=st.layout!=='compare'||!list.length;$('#loupe').hidden=!loupe;$('#none').hidden=list.length>0;
|
||||
$('#compare-view').hidden=st.layout!=='compare'||!list.length;$('#loupe').hidden=!loupe;
|
||||
syncCompare();syncLoupe(0);
|
||||
$$('[data-layout]').forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.layout===st.layout)));
|
||||
$$('[data-viewport]').forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.viewport===st.viewport)));
|
||||
placeThumbs(false);placeFilm(true);
|
||||
$('#notes-toggle').checked=st.notesOn;
|
||||
$('#filter-summary').textContent=`比较范围 ${list.length}/${C.length}`;
|
||||
$$('#filter-list input').forEach(i=>i.checked=st.visible.includes(i.value));
|
||||
paintChoice();fitAll();
|
||||
}
|
||||
function setLayout(layout,id){
|
||||
@@ -307,51 +389,40 @@ const DATA = /*__OIL_UI_DATA__*/ null;
|
||||
function step(d){const list=shown(),i=list.findIndex(c=>c.id===st.current),n=list[i+d];if(n)go(n.id);else nudge(d);}
|
||||
function nudge(d){animate($('.slide.is-current .stage'),[{transform:'none'},{transform:`translateX(${d*14}px)`},{transform:'none'}],{duration:320,easing:'ease-out'});}
|
||||
|
||||
// 选择即复制:再次点击“已选择”会带上最新备注重新复制,不再有单独的复制按钮
|
||||
// 选择即复制:再次点击“已选择”重新复制这个方向
|
||||
function choose(id){
|
||||
const fresh=st.chosen!==id;st.chosen=id;save();paintChoice();
|
||||
if(fresh){
|
||||
animate($(`.card[data-id="${CSS.escape(id)}"] .pick-mark`),[{opacity:0,transform:'scale(.4)'},{opacity:1,transform:'scale(1)'}],{duration:500,easing:ease('pop')});
|
||||
animate($('#decide-name'),[{opacity:0,transform:'translateY(10px)'},{opacity:1,transform:'none'}],{duration:380,easing:ease('glide')});
|
||||
animate($('#choice-chip'),[{transform:'scale(.3)'},{transform:'scale(1)'}],{duration:500,easing:ease('pop')});
|
||||
}
|
||||
copy();
|
||||
}
|
||||
function paintChoice(){
|
||||
$$('.card').forEach(a=>a.classList.toggle('is-chosen',a.dataset.id===st.chosen));
|
||||
$$('[data-pick]').forEach(b=>{const on=b.dataset.pick===st.chosen,short=!!b.closest('.cap');b.replaceChildren(document.createTextNode(on?(short?'已选择':'已选择 · 复制'):(short?'选择':'选这个方向')));if(!short)b.append(el('kbd',null,'P'));b.toggleAttribute('data-on',on);b.title=on?'再点一次,带上最新备注重新复制':'选择这个方向,并复制给 Agent';});
|
||||
$$('[data-pick]').forEach(b=>{const on=b.dataset.pick===st.chosen,short=!!b.closest('.cap');b.replaceChildren(document.createTextNode(on?(short?t('selected'):t('selectedCopy')):(short?t('select'):t('selectDirection'))));if(!short)b.append(el('kbd',null,'P'));b.toggleAttribute('data-on',on);b.title=on?t('recopyTitle'):t('selectTitle');});
|
||||
$$('#film button').forEach(b=>b.querySelector('.ft').classList.toggle('chosen',b.dataset.id===st.chosen));
|
||||
const c=byId[st.chosen];$('#decide-name').textContent=c?`${no(c)} ${c.name}`:'未选择方向';$('#picked').classList.toggle('on',!!c);
|
||||
const chip=$('#choice-chip'),next=c?stripMini(c):el('span','choice-empty');next.id='choice-chip';chip.replaceWith(next);
|
||||
status();
|
||||
}
|
||||
function status(){$('#notes-open').textContent=st.notes?'备注 ·':'备注';const s=$('#state');if(!stored){s.textContent='浏览器存储不可用:关闭页面后选择和备注不会保留。';s.dataset.warn='';}else{s.textContent='备注会和选择一起复制。改完备注,再点一次“已选择”重新复制。';delete s.dataset.warn;}}
|
||||
function copyText(){const c=byId[st.chosen];let t=`${DATA.round?DATA.round+':':''}选 ${no(c)} ${c.name}`;if(st.notes.trim())t+=`\n备注:${st.notes.trim()}`;return t;}
|
||||
function copyText(){const c=byId[st.chosen];let text=t('copyChoice',{round:DATA.round?t('copyRound',{round:DATA.round}):'',number:no(c),name:c.name});return text;}
|
||||
async function copy(t=copyText()){
|
||||
let ok=false;
|
||||
try{await navigator.clipboard.writeText(t);ok=true;}catch{const a=el('textarea');a.value=t;a.setAttribute('readonly','');a.style.cssText='position:fixed;opacity:0';document.body.append(a);a.select();try{ok=document.execCommand('copy');}catch{}a.remove();}
|
||||
toast(ok,t);
|
||||
}
|
||||
function toast(ok,t){
|
||||
const n=$('#toast');n.toggleAttribute('data-fail',!ok);n.querySelector('b').textContent=ok?'已复制,粘贴给 Agent':'复制失败,请手动复制';n.querySelector('code').textContent=ok?t.replace(/\n/g,' · '):t;
|
||||
function toast(ok,text){
|
||||
const n=$('#toast');n.toggleAttribute('data-fail',!ok);n.querySelector('b').textContent=ok?t('copied'):t('copyFailed');n.querySelector('code').textContent=ok?text.replace(/\n/g,' · '):text;
|
||||
n.classList.remove('show');n.offsetWidth;n.classList.add('show');
|
||||
clearTimeout(toastTimer);toastTimer=setTimeout(()=>n.classList.remove('show'),ok?2400:8000);
|
||||
}
|
||||
|
||||
const fl=$('#filter-list');
|
||||
C.forEach(c=>{const l=el('label'),i=el('input');i.type='checkbox';i.value=c.id;i.addEventListener('change',()=>{st.visible=C.map(x=>x.id).filter(id=>id===c.id?i.checked:st.visible.includes(id));save();morph(render,'reshape');});l.append(i,el('span','n',no(c)),c.name);fl.append(l);});
|
||||
$('#show-all').addEventListener('click',()=>{st.visible=C.map(c=>c.id);save();morph(render,'reshape');});
|
||||
$$('button[data-layout]').forEach(b=>b.addEventListener('click',()=>setLayout(b.dataset.layout)));
|
||||
$$('button[data-viewport]').forEach(b=>b.addEventListener('click',()=>{if(st.viewport===b.dataset.viewport)return;st.viewport=b.dataset.viewport;save();morph(render,'reshape');}));
|
||||
$('#notes-toggle').addEventListener('change',e=>{st.notesOn=e.target.checked;save();morph(render,'reshape');});
|
||||
// 每个候选都声明了固定画布时,桌面和手机的切换不起作用,收起来。
|
||||
if(C.every(c=>c.nativeViewport))$('[data-i18n-aria-label="viewportLabel"]').style.display='none';
|
||||
$('#prev').addEventListener('click',()=>step(-1));$('#next').addEventListener('click',()=>step(1));
|
||||
document.addEventListener('keydown',e=>{
|
||||
if(e.target.closest?.('textarea,input,summary')||e.metaKey||e.ctrlKey||e.altKey||st.layout!=='loupe')return;
|
||||
if(e.key==='ArrowLeft')step(-1);else if(e.key==='ArrowRight')step(1);else if(e.key==='Escape')setLayout('compare');else if(e.key.toLowerCase()==='p')choose(st.current);
|
||||
});
|
||||
const notes=$('#notes');notes.value=st.notes||'';
|
||||
notes.addEventListener('input',()=>{st.notes=notes.value;save();status();});
|
||||
$('#notes-open').parentElement.addEventListener('toggle',e=>{if(e.target.open)notes.focus();});
|
||||
function syncBars(){root.style.setProperty('--bar-h',`${$('#bar').offsetHeight}px`);placeThumbs(true);placeFilm(true);refit();}
|
||||
window.addEventListener('resize',syncBars,{passive:true});
|
||||
document.fonts?.ready.then(()=>{placeThumbs(true);placeFilm(true);});
|
||||
|
||||
Reference in new issue
Block a user