`。
- 行是页面骨架,语义需要自己补:给 `GridSection` 加 `aria-label` 或配合 `aria-labelledby` 指向行内标题,读屏才会把这一行念成区域。原生属性会原样透传。
- `GridSeparator` 与 `GridCrosshair` 是纯装饰,自带 `aria-hidden`,不会被读屏朗读,也不进入 Tab 顺序;纹路没有动画。`label` 同样会被一并隐藏,需要读屏识别的章节名请写在行内的标题里。
- 线条与文字的颜色都来自主题变量(`--edge`、`border-edge`),在亮暗模式和 14 个颜色主题下对比度一致,不依赖颜色传达信息。
## 注意事项
### 容器宽度
通过父元素上的 `--grid-width` 变量调整宽度,默认 `48rem`。文档页面使用 `80rem`。`GridColumns` 在窄屏下堆叠,`sm` 起显示两列。
视口比容器窄时,`GridContainer` 两侧各留出一段空白(gutter):竖线向内缩,而全宽横线、分隔带仍延伸到屏幕边缘,内容像被框在线里。gutter 是两侧之和,窄屏默认 `2rem`、`sm` 起 `3rem`,容器达到最大宽度后自然居中,不再有空白。通过 `--grid-gutter` 变量覆盖,例如 `className="[--grid-gutter:0px]"` 让竖线贴到屏幕边缘;该变量同时作用于窄屏与 `sm` 以上。
### 主题与工具类
安装命令会注册 `--edge` 变量与 `border-edge` 主题映射,以及以下 Tailwind CSS v4 工具类:两个横线工具类 `screen-line-top`、`screen-line-bottom`;六个分隔带纹理工具类 `pattern-ruler`、`pattern-hatch`、`pattern-dots`、`pattern-grid`、`pattern-plus`、`pattern-wave`(供 GridSeparator 使用);两个面积底纹 `bg-hatch`、`bg-dots`,可直接铺在任意元素上。线条与纹路的颜色都来自 `--edge` / `--foreground`,随颜色主题和亮暗模式变化。
全宽横线需要页面的 `html` 设置 `overflow-x: clip`;Registry 安装会一并写入该规则。
## 源码
---
# 组件
来源:https://yiui.dev/components/
---
# 等轴测标志
来源:https://yiui.dev/components/isometric-mark/
## 安装
## 用法
传入平面形状即可,坐标就是平面 logo 自己的坐标(`y` 向下)。形状数组请定义在组件外面,避免每次渲染重新计算几何。
```tsx title="app/brand-mark.tsx"
import {
IsometricMark,
type IsometricShape,
} from "@/components/ui/isometric-mark/isometric-mark"
const SHAPES: readonly IsometricShape[] = [
{
outer: [
[0, 0],
[100, 0],
[100, 100],
[0, 100],
],
holes: [
[
[35, 35],
[65, 35],
[65, 65],
[35, 65],
],
],
},
]
export function BrandMark() {
return
}
```
宽度由 `className` 控制,高度按 viewBox 自动适配。线宽、斜纹间距、虚线都跟着图形尺寸等比缩放,所以放大缩小时观感一致。
### 朝向与视角
`rotation` 在投影前把图形绕中心旋转,决定它平躺在地上的朝向;`elevation` 是视线的俯仰角,默认约 35.26°(标准等轴测),调大更接近俯视,调小更接近平视。
取 `0`、`90`、`180`、`-90` 时图形的横竖边沿等轴测的两条斜轴,像一块平放的板;斜向的图形可以取任意角度,让它的长边顺着某条斜轴更好看。页首的本站标志用的是 `rotation={-65}`:
```tsx
```
想直观比较不同朝向和俯仰,打开下方任一示例的「调整」面板拖动即可。
## 示例
每个示例的标题栏右侧都有「调整」开关:打开后可以实时调整朝向、俯仰、挤出高度、顶面材质、地面、辅助线与音色,并开关按下、高光与音效;未改动的项显示示例自己的取值。调整时画框固定,容器大小不变。
### 多个形状与色调
`shapes` 可以放多块互不相连的多边形,它们之间的遮挡自动计算;每块可以用 `tone` 单独着色。下面是 shadcn/ui 的标志:两道斜杠各是一块,分别取主题色的深浅,地面用条纹 `floor="lines"`。
### 带孔的形状
`holes` 在外轮廓里挖孔,孔壁同样按朝向自动消隐。下面是 Pi 的像素标志:左边一块挖出方孔,右下的方块独立成一块并着主题色,顶面用网纹材质,地面用棋盘格 `floor="checker"`。
### 静态图形
任何多边形都可以,最简单的一个三角形也行。下面是 Vercel 的三角标:素面材质(`material="plain"`)、包围盒辅助线(`guides`),关掉了 `highlight`;需要完全静态的插图时再关掉 `interactive`。地面用站点的点阵底纹 `floor="dots"`。
### 材质、地面与辅助线
- `material` 是顶面材质:`hatch` 斜纹「/」(默认)、`hatch-reverse` 斜纹「\」、`cross` 网纹、`dots` 点阵、`lines` 横纹、`plain` 素面。斜纹与图形长边接近平行时会有视差感,换一个方向或用横纹即可。
- `guides` 是背景虚线:`true` / `"box"` 画包围盒四边的延长线,适合横平竖直的图形;`"edges"` 沿图形自己最长的两个边方向画外侧切线,适合斜向的图形。
- `floor` 是地面,和辅助线同方向、同一平面,并对齐图形的每条边:`true` / `"grid"` 方格网、`"lines"` 条纹、`"checker"` 棋盘格,`"dots"` 则是站点的点阵底纹(8px 间距、1px 方点,铺在屏幕平面上)。
### 挤出高度与按下
`depth` 是挤出高度,单位与形状坐标相同,默认取图形平面尺寸的 12%。按住指针时图形被压到 `pressedDepth`(默认是 `depth` 的一半,设为 `0` 会被完全压平),松开或移出后弹回;线条高光跟随鼠标在窗口中的位置。触屏与“减少动态效果”下高光自动关闭,按下状态瞬间切换。这些都可以在示例的调整面板里直接试。
### 点击音效
`sound` 打开按下与松开时的合成音效(Web Audio 现场生成,不需要音频文件),默认关闭,本页所有示例都已打开。浏览器要求先有用户操作才能出声,所以音频在第一次按下时才初始化。静音偏好存在浏览器本地,页面上所有标志共用:`useIsometricMarkMuted()` 返回 `[muted, setMuted]`,可以用它做一个静音按钮,页首的示例右上角就是这样。这份偏好与站内其他带音效的组件共用(`localStorage` 的 `travinsy:sound:muted`):同一页面里一处静音,其他组件的静音按钮与音效立刻跟上;换到另一个标签页也会同步。`soundTone` 调音色:`0` 偏闷,是低沉的「咚」;`1` 偏脆,是清脆的「咔」,默认 `0.6`。
```tsx
const [muted, setMuted] = useIsometricMarkMuted()
```
## 几何说明
组件不依赖手工描的路径,所有面都由平面多边形自动算出:
- **投影**:`X = (x - y)·cos30°`,`Y = (x + y)·k₁ - z·k₂`,其中 `k₁ = sin(e)·√2·cos30°`、`k₂ = cos(e)·√2·cos30°`,`e` 是 `elevation`;默认 35.26° 时 `k₁ = 0.5`、`k₂ = 1`,即标准等轴测。观察者位于 `(+x, +y, +z)` 一侧,viewBox 按投影后的包围盒自动计算并留少量边距。
- **面的组成**:顶面在 `z = depth`,外轮廓与孔一起用 `evenodd` 填充;每个环的每条边挤出一面侧墙,孔被挤成通孔。
- **背面剔除**:外轮廓取正面积、孔取负面积,所有环的“实体外侧”法线统一为 `(ey, -ex)`,只保留朝向观察者的墙。与观察方向垂直的墙在屏幕上面积为零,不绘制。
- **消隐**:画家算法。墙在屏幕上只占据 `x - y` 方向上的一段范围,范围重叠的两面墙比较重叠区间中点的 `x + y`,得到“谁挡住谁”,再做拓扑排序,凹多边形也能排对。顺序只取决于平面形状,按下动画时不用重排。顶面最后绘制。
- **按下动画**:一个弹簧驱动的高度值同时重算所有侧墙路径(只是 O(边数) 的字符串重建),顶面路径不变,整体用 `y` 偏移下移,材质纹理因此始终贴着轮廓。
- **地面**:辅助线与地面网格取同样的两个方向。每个方向上,若所有边到外侧切线的距离有足够大的公约数,就按它等距铺线;否则每条边各画一条线,外侧按平均间距延伸。
## API
### IsometricMark
其余属性透传到根 `svg`,`className` 与内置样式合并。
### useIsometricMarkMuted
```ts
function useIsometricMarkMuted(): readonly [
muted: boolean,
setMuted: (muted: boolean) => void,
]
```
读写点击音效的静音偏好。偏好存在 `localStorage` 的 `travinsy:sound:muted`,与站内其他带音效的组件共用,同一页面的所有标志共享(写入后广播 `travinsy:sound-muted-change` 事件)并在标签页之间同步;服务端与水合阶段按未静音渲染。
### 类型
```ts
type IsometricPoint = readonly [number, number]
type IsometricShape = {
/** 外轮廓,顺时针或逆时针都可以 */
outer: readonly IsometricPoint[]
/** 孔,会被挤成通孔 */
holes?: readonly (readonly IsometricPoint[])[]
/** 这块形状的色调(任意 CSS 颜色,如 var(--primary)):顶面与侧墙淡淡着色、线条取同色 */
tone?: string
}
type IsometricMaterial =
"hatch" | "hatch-reverse" | "cross" | "dots" | "lines" | "plain"
type IsometricFloor = "grid" | "lines" | "dots" | "checker"
```
页首的本站标志就用了 `tone`:上三道细斜杠用主题色由浅到深,下三道粗斜杠保持单色线稿。
### data-slot
| 元素 | `data-slot` |
| ---------- | ------------------------------------------------- |
| 根元素 | `isometric-mark` |
| 单面侧墙 | `isometric-mark-wall` |
| 顶面 | `isometric-mark-top` |
| 背景辅助线 | `isometric-mark-guides` |
| 地面 | `isometric-mark-floor`(`data-floor` 为具体材质) |
## 可访问性
- 不传 `label` 时根元素是 `aria-hidden`,读屏完全忽略,适合作为纯装饰;传入 `label` 后变成 `role="img"` 并带上 `aria-label`。
- 按下效果只响应指针,没有键盘等价操作,不承载任何功能。需要可点击时请把它包进 `a` 或 `button`,由外层提供语义与键盘支持。
- 高光在触屏设备(`(hover: none)`)与 `prefers-reduced-motion` 下自动关闭;减少动态效果时按下状态瞬间切换,不播放弹簧。
- 音效默认关闭;打开后请同时提供静音入口(`useIsometricMarkMuted`),音效只是点缀,不承载信息。
- 颜色由 `--foreground`、`--background` 与形状的 `tone` 混合得到;暗色模式下顶面、线条、纹理与着色比例单独加重,亮暗模式与颜色主题下都有足够的对比。
## 注意事项
- 高光位置取鼠标在**窗口**中的相对位置并映射到图形范围,因此鼠标不在图形上时高光也会随之移动;图形不在视口内(含 80px 缓冲)时不监听鼠标。
- 顶面与侧墙是实心填充(`--top`、`--wall`,亮色下等于背景色),用来遮挡后面的线,所以图形背后若有图案会被盖住;地面与背景辅助线画在最底层。
- `depth` 默认取图形平面尺寸的 12%;`pressedDepth` 默认是 `depth` 的一半,设为 `0` 则会被完全压平。
- 颜色变量 `--stroke`、`--pattern`、`--guide`、`--top`、`--wall` 以及着色比例 `--tone-top`、`--tone-wall`、`--tone-line` 定义在根元素的 class 里(亮暗各一套),可以用 `className` 覆盖,例如 `[--stroke:var(--primary)]`。
- `floor="dots"` 按图形的实际渲染尺寸换算 8px,尺寸变化时自动重算。
- 几何计算放在同目录的 `geometry.ts`,是不依赖 React 的纯函数;音效在 `sound.ts`。
## 源码
---
# 实时当地时间
来源:https://yiui.dev/components/local-time/
## 安装
## 用法
组件分三种形态,都从同一个入口导入:
| 组件 | 形态 |
| ------------------------------- | ------------------------------ |
| `LocalTime` | 行内时间,可以放进任何一行文字 |
| `WorldClock` + `WorldClockItem` | 多个时区的世界时钟,列表或卡片 |
| `AnalogClock` | SVG 模拟表盘 |
`timeZone` 接收 IANA 时区名,不是 `+08:00` 这类固定偏移。
```tsx title="app/header.tsx"
import { LocalTime } from "@/components/ui/local-time/local-time"
export function Header() {
return (
)
}
```
输出形如 `21:04:33 // 早 0.5 小时`:前半段是目标时区的当前时间,`//` 后是它与访客时区的差值。
## 示例
### 行内时间的选项
`showDayNight` 在时间前加昼夜图标,`showZone` 附上 UTC 偏移,`animated` 让变化的数字轻微下落淡入,`hour12` 切换 12 小时制,`showSeconds={false}` 去掉秒(此时每分钟才刷新)。
### 世界时钟
`WorldClock` 接收 `zones` 数组(本页顶部的预览把它和 `AnalogClock`、`LocalTime` 拼成一块「协作时区」面板:左边是所在地的表盘,右边是四个协作城市),每一项一行:城市名、时区与 UTC 偏移、当前时间、昼夜图标,右下角是日期差(昨天 / 明天)和与访客的时差。`highlighted` 给某一项加上强调色竖条。
```tsx title="app/clocks.tsx"
import { WorldClock } from "@/components/ui/local-time/local-time"
const zones = [
{ label: "上海", timeZone: "Asia/Shanghai", highlighted: true },
{ label: "伦敦", timeZone: "Europe/London" },
{ label: "纽约", timeZone: "America/New_York" },
]
export function Clocks() {
return
}
```
### 用组合方式写
需要给某一项加自己的属性时,直接把 `WorldClockItem` 作为 children 写出来;`hour12`、`showSeconds` 等设置由 `WorldClock` 统一下发。
### 卡片形态
`variant="cards"` 把每个城市排成带模拟表盘的卡片,两列布局,最后一张落单时自动撑满整行。
### 模拟表盘
`AnalogClock` 默认平滑转动:指针每帧由 `requestAnimationFrame` 直接写入,不触发 React 渲染,离开视口时暂停。`smooth={false}` 或系统开启“减少动态效果”时改为按秒跳格;表盘随昼夜轻微变色,秒针使用主题强调色。
### 文案与区域格式
可见文案集中在 `labels`,`locale` 决定 `Intl.DateTimeFormat` 与 `Intl.NumberFormat` 的结果。旧版单独的文案 props(`placeholderLabel`、`sameTimeLabel`、`aheadLabel`、`behindLabel`、`hourUnit`)继续可用,优先级高于 `labels` 里对应的字段。
```tsx
```
| `labels` 字段 | 默认值 | 用于 |
| ---------------------------------- | ------------------------ | --------------------- |
| `pending` | `当地时间` | 挂载前占位时间之后 |
| `same` | `与你同一时间` | 与访客同一时间 |
| `ahead` / `behind` | `早` / `晚` | 目标时区更靠前 / 靠后 |
| `hourUnit` | `小时` | 时差的单位 |
| `day` / `night` | `白天` / `夜间` | 昼夜图标的读屏文字 |
| `yesterday` / `today` / `tomorrow` | `昨天` / `今天` / `明天` | 世界时钟的日期差 |
| `worldClock` / `live` | `WORLD CLOCK` / `LIVE` | 世界时钟标题栏 |
| `clock` | `当地时间` | 模拟表盘的读屏名称 |
### 时差怎么算
组件把目标时区的 UTC 偏移和 `Date.prototype.getTimezoneOffset()` 相减得到小时数,夏令时(例如 `Europe/London`)自动跟随。差值为 0 时显示 `与你同一时间`。昼夜以目标时区的 6:00–17:59 为白天,只是视觉提示,不区分半球与季节。
## API
### LocalTime
| 元素 | `data-slot` | 其他属性 |
| ----------------- | ----------------- | ---------------------------------------------------------------------------------------------- |
| 根元素 | `local-time` | `data-state="pending"` / `"ready"`;开启 `showDayNight` 时有 `data-daylight="day"` / `"night"` |
| 当前时间 `