# Lynx 与 Web CSS 差异指南

Lynx 支持大部分常用 CSS，但也有一些重要差异。

## 重要差异一览

### 1. Display 属性

| Web                     | Lynx         | 说明                                     |
| ----------------------- | ------------ | ---------------------------------------- |
| `display: block`        | ❌ 不支持    | 使用 Flex 或 Linear 代替                 |
| `display: inline`       | ❌ 不支持    | 使用 `<text>` 组件代替                   |
| `display: inline-block` | ❌ 不支持    | 使用 Flex 或 Linear                      |
| `display: table*`       | ❌ 不支持    | 使用 Grid 或 Flex 代替（见下方迁移示例） |
| `display: flex`         | ✅ 支持      | 标准 Flexbox                             |
| `display: grid`         | ✅ 支持      | CSS Grid 子集                            |
| `display: linear`       | ✅ Lynx 特有 | 类似 Flex Column，性能更好               |
| `display: relative`     | ✅ Lynx 特有 | Android-style 相对布局                   |
| `display: none`         | ✅ 支持      | 隐藏元素                                 |

> **⚠️ 注意**：`display: table` / `table-row` / `table-cell` 等在 Lynx 中**完全不支持**。CSS parser 会将其视为无效值并回退到默认布局。Starlight 布局引擎中不存在 TableLayoutAlgorithm。

> **⚠️ 警告**：在非 W3C 标准模式下使用 `display: block` 会触发控制台警告：
>
> ```
> "Unexpected display type!! Fall back to default display."
> ```
>
> 建议使用 `display: flex` 或 `display: linear` 替代。

### 2. Flex 收缩与内容最小尺寸

`flex-shrink` 只有在主轴可用空间不足时才会触发：`flex-direction: row` 时看宽度，`flex-direction: column` 时看高度。如果父容器没有显式 `width` / `height`，也没有被外层布局、最大尺寸或视口约束限制，容器会按内容撑开，不产生负剩余空间，`flex-shrink` 不会改变最终尺寸。

Lynx 和 Web 的关键差异在主轴收缩下限：Web flex item 默认有 `min-width: auto` / `min-height: auto`，文本、图片、嵌套子节点或其它内在内容都可能形成 content-based / `min-content` 下限。Lynx 的 `flex-shrink` 不会因为这些内容自动套用这层保护，除非显式设置 `min-width` / `min-height` 或 `flex-shrink: 0`。可以把 Lynx 的默认表现理解为 Web flex item 在主轴方向显式设置了最小值为 `0`：row 方向类似 `min-width: 0`，column 方向类似 `min-height: 0`。

这个差异既可能出现在 row，也可能出现在 column：

```css
.row {
  display: flex;
  width: 80px;
}

.row .content-item {
  width: 120px;
  flex-shrink: 1;
}

.column {
  display: flex;
  flex-direction: column;
  height: 80px;
}

.column .content-item {
  height: 120px;
  flex-shrink: 1;
}
```

在 Lynx 中，`.content-item` 会继续参与主轴压缩，实际宽度或高度可能小于内部内容需要的尺寸。在 Web 中，row 方向的默认 `min-width: auto` 或 column 方向的默认 `min-height: auto` 可能让内容形成自动最小尺寸，导致该 item 不继续压缩，表现为溢出、其它兄弟节点被压缩，或 H5 预览和 Lynx 结果不一致。

需要 Web 预览页对齐 Lynx 压缩行为时，在 Web 侧显式取消自动最小尺寸：

```css
/* row 方向：允许文本项继续缩小 */
.text-flex-item {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* column 方向：允许纵向子项继续缩小 */
.column-flex-item {
  min-height: 0;
}
```

需要 Lynx 更接近 Web 的“内容不被压缩”效果时，在 Lynx 侧显式保护该项目：

```css
.content-item {
  flex-shrink: 0;
  /* 或按主轴方向设置明确下限 */
  min-width: 120px;
  min-height: 120px;
}
```

### 3. 默认布局行为差异（关键）

**Web 默认是 block/flow layout，Lynx 默认是 linear layout**：

```css
/* Web: 不设置 display 时，默认是 block flow */
div {
  /* 默认 display: block */
}
/* 子元素默认宽度 100%，垂直排列 */

/* Lynx: 不设置 display 时，默认是 linear layout */
view {
  /* 默认 display: linear; linear-direction: column */
}
/* 子元素不会自动填满父元素宽度 */
```

**对子元素尺寸的影响**：

在 Web 的 block layout 中，块级子元素默认 `width: 100%`（自动填满父元素宽度）。在 Lynx 的 linear layout 中，子元素默认尺寸由内容决定，**不会自动填满父元素**。

```css
/* Web: 子元素自动填满父元素 */
.parent {
  width: 200px;
}
.child {
  /* 默认 width: 100%，实际 200px */
}

/* Lynx: 子元素不会自动填满 */
.parent {
  width: 200px;
  display: linear;
}
.child {
  /* 默认宽度由内容决定，可能远小于 200px */
}

/* Lynx 修复方案 1：显式设置 width */
.child {
  width: 100%;
}

/* Lynx 修复方案 2：使用 linear-layout-gravity */
.child {
  linear-layout-gravity: stretch;
}

/* Lynx 修复方案 3：父元素设置 linear-cross-gravity */
.parent {
  linear-cross-gravity: stretch;
}
```

**嵌套元素尺寸问题**：

在 Web 中，嵌套的 block 元素会逐层填满父元素。在 Lynx linear layout 中，嵌套的 view 如果没有显式宽高，可能尺寸为 0（尤其是空 view）。

```css
/* Web: 嵌套 div 自动填满 */
.grandparent {
  width: 100px;
  height: 100px;
}
.parent {
  /* 自动 100x100 */
}
.child {
  /* 自动 100x100 */
}

/* Lynx: 需要每层都显式设置 */
.grandparent {
  width: 100px;
  height: 100px;
  display: linear;
}
.parent {
  width: 100%;
  height: 100%;
}
.child {
  width: 100%;
  height: 100%;
}
```

### 4. 盒模型

**box-sizing 默认**:

```css
/* Web 默认 */
box-sizing: content-box;

/* Lynx 默认 */
box-sizing: border-box;
```

> **注意**: Lynx 默认使用 `border-box`，即 `width` / `height` 已包含 `padding` 和 `border`。这与 Web 浏览器默认的 `content-box`（`width` / `height` 仅指内容区域）有本质区别。
>
> 若从 Web 迁移且需保持原有盒模型行为，可全局重置：
>
> ```css
> * {
>   box-sizing: content-box;
> }
> ```

显式设置 `box-sizing: border-box` 或 `box-sizing: content-box` 可以控制盒模型计算，但它不能消除其它 Web 布局依赖带来的差异。迁移依赖 `float`、浏览器默认 `<body>` margin/line-height、绝对定位包含块或 min/max 尺寸夹取的页面时，需要同时重写这些布局条件；否则即使盒模型计算正确，Lynx 与 Web 仍可能出现像素级偏移。

**Margin collapsing 不支持**:

Lynx 中相邻元素的垂直 margin **不会合并**，而是直接相加。这会导致从 Web 迁移过来的页面出现意料之外的间距。

```css
/* Web: 上下 margin 会合并为 20px */
.item1 {
  margin-bottom: 20px;
}
.item2 {
  margin-top: 20px;
}

/* Lynx: margin 不合并，间距为 40px */
```

**通用解决方案**：

| 场景 | Web 行为 | Lynx 行为 | 修复策略 |
|------|----------|-----------|----------|
| 相邻兄弟元素 | `margin-bottom` 和 `margin-top` 合并为较大值 | 两个 margin **相加** | 只给一个元素设置 margin，或改用 gap/padding |
| 父元素 + 第一个子元素 | 父 `margin-top` 和子 `margin-top` 合并 | 两个 margin **相加** | 移除父元素 margin，由子元素独立控制 |
| 父元素 + 最后一个子元素 | 父 `margin-bottom` 和子 `margin-bottom` 合并 | 两个 margin **相加** | 移除父元素 margin，由子元素独立控制 |
| 空元素相邻 | 空元素的 margin 会与其他 margin 合并 | 空元素的 margin **正常参与相加** | 移除空元素的 margin 或添加 `min-height: 1px` |

**方案 1：只给其中一个元素设置 margin（适用于兄弟元素）**
```css
.item1 { margin-bottom: 20px; }
.item2 { margin-top: 0; }
```

**方案 2：使用 padding 替代其中一个 margin**
```css
.item1 { margin-bottom: 0; padding-bottom: 20px; }
.item2 { margin-top: 0; }
```

**方案 3：使用 gap（如果父元素是 flex 或 grid 布局）**
```css
.parent {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.item1, .item2 { margin: 0; }
```

**方案 4（推荐）：移除父容器 margin，由子元素独立控制（适用于容器+子元素场景）**

当需要模拟 Web 中 "容器有 8px margin-top，子元素有 1em margin-top，最终间距为 1em" 的 collapsed 效果时：
```css
/* Web 原始代码 */
.container {
  margin-top: 8px;
}
.child {
  margin-top: 1em;    /* 16px，与容器的 8px collapse 后仍为 16px */
}

/* Lynx 等效代码 */
.container {
  margin-top: 0;      /* 移除容器顶部 margin */
}
.child {
  margin-top: 1em;    /* 16px，直接得到目标间距 */
}
```

**常见场景**：
- 连续的块级元素使用 margin 控制间距（段落、标题、列表项）
- 容器与第一个/最后一个子元素之间的间距（body 与内容、卡片与内部元素）
- 嵌套组件中外层容器和内层容器之间的 margin 叠加
- 多层嵌套结构中 margin 的累积效应
- 空元素（如仅含 border 的 div）的 margin 处理

**负 margin（negative margin）在 Linear Layout 中的行为**：

在 Lynx 的 `display: linear; linear-direction: column` 默认布局中，负 margin 可以正常工作：

| 方向 | 场景 | 行为 |
|------|------|------|
| **Cross-axis**（`margin-left` / `margin-right`） | 子元素负 margin 拉回到父元素边界 | ✅ **正常工作** |
| **Main-axis**（`margin-top` / `margin-bottom`） | 兄弟元素间负 margin 重叠 | ✅ **正常工作** |

```css
/* 子元素负 margin 回拉 —— WORKS ✓ */
.parent {
  border-left: 2px solid red;
  padding-left: 96px;
}
.child {
  border-left: 2px solid black;
  margin-left: -98px;  /* 子元素向左回拉，覆盖父元素的红色边框 */
}

/* 兄弟元素负 margin 重叠 —— WORKS ✓ */
.sibling1 {
  border-top: 2px solid red;
  padding-bottom: 96px;
}
.sibling2 {
  border-bottom: 2px solid black;
  margin-top: -98px;   /* 向上重叠，与 sibling1 的红色边框贴合 */
}
```

**适配建议**：
- 负 margin（包括 cross-axis 和 main-axis）在 Lynx linear layout 中均可正常工作，无需特殊 workaround。
- 若遇到细微 diff，优先检查 margin collapsing 补偿是否正确，而非负 margin 本身。

**Negative padding values are invalid in CSS but accepted in Lynx**:

In CSS, negative padding values (e.g. `padding-bottom: -1px`) are **invalid values**. When no other valid declaration exists, the property falls back to its initial value (`0`). Lynx's padding length parser (`LengthHandler::Process`) does **not** perform non-negative validation, so negative values are parsed as valid lengths and continue into layout. The layout path only calls `ClampIndefiniteToZero()`, which handles indefinite values, not negative ones. As a result, the negative padding is applied directly, causing layout differences.

```css
/* Web: -1px is an invalid value, falls back to 0 */
padding-bottom: -1px; /* actual effect: 0 */

/* Lynx: -1px is parsed as a valid length and applied directly */
padding-bottom: -1px; /* actual effect: -1px */
```

**Workaround**: Replace negative padding with `0` during WPT adaptation or code review:

```css
/* Replace invalid negative padding with 0 */
.element {
  padding: 0; /* instead of padding: -1px */
}
```

**`<body>` margin + 第一个子元素 margin-top 的特殊处理**：

在 Web 中，`<body>` 的 `margin: 8px` 和第一个 `<p>` 的 `margin-top: 1em` 会 margin collapse，最终顶部间距为 `max(8px, 1em)`（约 16px）。

在 Lynx 中，`.body { margin: 8px; }` 和第一个子元素的 margin-top **不会 collapse**，而是相加：

```css
/* Lynx 适配策略 */
.body {
  display: linear;
  linear-direction: column;
  margin: 8px;
}

/* 如果第一个子元素的 margin-top 为 0，总顶部间距只有 8px */
/* 需要给第一个子元素添加 margin-top: 8px 来补偿 */
.first-child {
  margin-top: 8px;
}
/* 总顶部间距 = 8px (.body) + 8px (first-child) = 16px ✓ */
```

**WPT 适配建议**：

- 将 `.__ua-p`（模拟 `<p>`）等段落类元素的 `margin-top: 1em; margin-bottom: 1em;` 改为 `margin-top: 0.5em; margin-bottom: 0.5em;`
- 这样相邻段落相加后为 `0.5em + 0.5em = 1em`，与 Web 的 margin collapse 效果一致
- 如果第一个子元素的 margin-top 为 0，需要额外添加 `margin-top: 8px` 来模拟 `<body>` + `<p>` 的 collapse 效果
- 若根容器和第一个子元素都有 `8px` 顶部 margin，Web 中 margin collapse 后顶部间距是 `max(8px, 8px) = 8px`；Lynx 中两者会相加为 `16px`。迁移时需要显式分配根容器和第一个子元素的 margin，使相加后的值等于 Web 中 collapse 后的目标间距。

### 5. 长度值

**内在尺寸单位**:

- ✅ `max-content` - 完全支持
- ✅ `fit-content` - 完全支持
- ❌ `min-content` - 不支持

**不支持的单位**:

- ❌ `ch`, `ex`
- ❌ 物理单位：`cm`, `mm`, `in`, `pt`, `pc`

### calc() 函数支持

**✅ 支持的属性（34 个长度属性）**:

- 尺寸：`width`, `height`, `max-width`, `min-width`, `max-height`, `min-height`
- 定位：`top`, `left`, `right`, `bottom`
- 内边距：`padding-left`, `padding-right`, `padding-top`, `padding-bottom`
- 外边距：`margin-left`, `margin-right`, `margin-top`, `margin-bottom`
- Flex：`flex-basis`
- Flex/Grid 间距：`grid-column-gap`, `grid-row-gap`, `column-gap`, `row-gap`
- 文本：`font-size`, `text-indent`
- 变换：`perspective`
- RTL 支持：`margin-inline-start/end`, `padding-inline-start/end`, `inset-inline-start/end`

**❌ 不支持的属性**:

- 颜色属性（`color`, `background-color` 等）
- 枚举属性（`flex-direction`, `justify-content`, `align-items` 等）
- 边框圆角（`border-radius` 等）
- 变换属性（`transform` 等）
- 数值属性（`opacity`, `z-index` 等）

```css
/* ✅ 有效 */
width: calc(100% - 20px);
margin: calc(var(--spacing) * 2);

/* ❌ 无效 */
flex-direction: calc(row); /* 枚举属性 */
color: calc(#ff0000); /* 颜色属性 */
```

**推荐的单位**:

- ✅ `px` - 像素
- ✅ `%` - 百分比
- ✅ `vw`, `vh` - 视口单位（**推荐**，配合 rem 使用）
- ✅ `rem` - 根元素字体倍数（**推荐**，响应式适配首选）
- ⚠️ `rpx` - 响应式像素（Lynx 特有，功能完整但缺乏 Web 兼容性）

### 6. 定位

**不支持 static**:

```css
/* Web */
position: static; /* 默认 */

/* Lynx - 使用 relative 代替 */
position: relative; /* 默认 */
```

### 7. Float 不支持

```css
/* ❌ 不支持 */
float: left;
float: right;
clear: both;

/* ✅ 使用 Flex 代替 */
display: flex;
flex-direction: row;
```

### 8. 文本处理

**Text 组件隔离**:

```css
/* Web: 可以直接在 div 中放文字 */
<div>Hello World</div>

/* Lynx: 必须使用 Text 组件 */
<view>
  <text>Hello World</text>
</view>
```

**文本不换行**:

```css
/* Web: 默认会换行 */
/* Lynx Text 组件: 默认不换行，需要设置 */
text {
  white-space: normal; /* 允许换行 */
}
```

### 9. 伪元素

**::before, ::after 不支持**:

```css
/* ❌ 不支持 */
.element::before {
  content: 'Prefix';
}

.element::after {
  content: '';
  background-color: red;
}
```

> **注意**: `::before` 和 `::after` 在 Lynx 中完全不支持。虽然 CSS 解析器可以识别这些伪元素，但选择器匹配器和渲染引擎并未实现它们，因此使用它们不会有任何效果。

### 10. Z-index

**需要配合 position**:

```css
/* Web: 无 position 也能用 z-index */
z-index: 10;

/* Lynx: 必须设置 position */
position: relative; /* 或 absolute/fixed/sticky */
z-index: 10;
```

**Stacking Context and Compositing Layer**:

In Lynx, `z-index` creates a new **stacking context** and may trigger **compositing layer** promotion. This causes child elements in scrollable containers (e.g., `scroll-view`, `fold-view`) to not follow the scroll movement.

```css
/* ❌ Problem: Child elements with z-index may not follow scroll */
fold-view-header {
  /* Child elements inside header have z-index set */
}
.header-item {
  position: relative;
  z-index: 10; /* This element is promoted to compositing layer, may not follow header scroll */
}

/* ✅ Solution: Add z-index: 0 to parent container to establish the same stacking context */
fold-view-header {
  position: relative;
  z-index: 0; /* Key: Make header itself a compositing layer */
}
.header-item {
  position: relative;
  z-index: 10; /* Now correctly follows fold-view-header scroll */
}
```

**How It Works**:

- When a child element sets `z-index`, it is promoted to an independent compositing layer
- The coordinate transformation of this layer may not follow the parent container's scroll offset
- Adding `z-index: 0` (or any z-index) to the parent container makes it a stacking context, so child elements stay within the same layer and follow the scroll

**Common Scenarios**:

- Elements inside `fold-view-header` with `z-index` don't follow scroll
- Fixed-position elements inside `scroll-view` and their stacking with scrollable content
- Overlay popups and their stacking with scrollable content

### 11. 选择器限制

**不支持**:

- ❌ `:is()`, `:where()`, `:has()` - 较新的选择器
- ❌ 复杂属性选择器: `[attr^="val"]`, `[attr$="val"]`（部分支持）
- ❌ 通用兄弟选择器在复杂场景可能有限制

### 12. 自定义字体

**字体格式**:

```css
/* Web: 支持多种格式 */
@font-face {
  src: url('font.woff2') format('woff2');
}

/* Lynx: 使用系统字体或 base64 内联 */
font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
```

## 快速迁移检查表

从 Web 迁移到 Lynx 时，检查以下项目：

- [ ] 移除所有 `display: block` / `display: inline` / `display: inline-block`
- [ ] 将所有文本包裹在 `<text>` 组件中
- [ ] 移除所有 `float` 和 `clear`，改用 Flex
- [ ] 检查 `position: static`，改用 `relative`
- [ ] 替换 `min-content` / `max-content` 为固定值或百分比
- [ ] 为所有 `z-index` 添加 `position`
- [ ] Check if z-index in scrollable containers (scroll-view/fold-view) causes scroll not to follow
- [ ] If so, add `z-index: 0` to parent container to establish the same stacking context
- [ ] 移除所有 `::before` / `::after` 使用（完全不支持）
- [ ] 检查 margin collapsing 问题
- [ ] 测试 `white-space` 设置

## 常见陷阱

### 陷阱 1：忘记使用 Text 组件

```jsx
// ❌ 错误
<view>Text content</view>

// ✅ 正确
<view>
  <text>Text content</text>
</view>
```

### 陷阱 2：依赖 margin collapsing

```css
/* Web: 会合并为 20px */
.top {
  margin-bottom: 20px;
}
.bottom {
  margin-top: 20px;
}

/* Lynx: 不会合并，为 40px，需要调整 */
.top {
  margin-bottom: 10px;
}
.bottom {
  margin-top: 10px;
}
/* 或者 */
.top {
  margin-bottom: 20px;
}
.bottom {
  margin-top: 0;
}
```

### 陷阱 3：使用不支持的单位

```css
/* ❌ 不支持 */
width: min-content;
width: 100vw; /* vh 支持，但 vmin/vmax 可能不支持 */

/* ✅ 使用替代方案 */
width: auto;
width: 100%;
```

### 陷阱 4：忘记设置 white-space

```css
/* 默认不换行 */
text {
  white-space: nowrap;
}

/* 需要显式设置才能换行 */
text {
  white-space: normal;
}
```

### 陷阱 5：忘记同步 body 样式到 page 元素

Web 的 `body` 元素通常带有浏览器默认的 `margin` 和 `padding`，而 Lynx 的根元素 `page` **默认没有这些样式**。

```css
/* Web: body 有默认 margin/padding */
body {
  margin: 8px;
  padding: 0 8px;
}

/* Lynx: page 默认无 margin/padding，需手动同步 */
page {
  margin: 8px;
  padding: 0 8px;
}
```

从 Web 迁移时，若需保持完全一致的布局，记得将 `body` 上的样式同步到 `page`。

### 陷阱 6：把 Web 的 html/body 背景直接映射成普通容器

Web 中 `html` 是文档根节点，`body` 是页面内容容器，两者在背景绘制上有专门的 canvas 传播规则。Lynx 中没有这两个同名节点；`page` 是页面根节点，也是全页背景承载面。

```css
/* Web: html 提供页面底色，body 提供内容区域背景 */
html {
  background-color: #f5f5f5;
}

body {
  margin: 8px;
  background-color: white;
}
```

迁移到 Lynx 时，先判断 Web 背景的职责：

- 需要全页背景时，设置 `page`。
- 需要内容容器背景时，设置根 `view`。
- Web 中同时使用 `html` 和 `body` 两层背景时，通常把 `html` 背景映射到 `page`，把 `body` 背景映射到根 `view`。

```css
/* Lynx */
page {
  background-color: #f5f5f5;
}

.root {
  margin: 8px;
  background-color: white;
}
```

如果 Web 只依赖 `body` 背景铺满视口，Lynx 中直接设置 `page` 更接近该全页视觉：

```css
/* Web */
body {
  background-color: green;
}

/* Lynx */
page {
  background-color: green;
}
```

不要把一个名为 `.body` 的普通 `view` 当成 Web `body` 的等价物。普通 `view` 的背景只覆盖它自己的布局尺寸；`page` 才是 Lynx 的根节点和全页背景面。

### 陷阱 7：迁移包含 background-attachment 的简写

Web 的 `background` 简写可能包含 `fixed` / `scroll` 等 `background-attachment` 槽位。当前支持属性列表中没有 `background-attachment`，迁移时应明确拆出 Lynx 支持的背景属性，并单独确认 attachment 语义是否真的需要保留：

```css
/* Web */
.box {
  background: url('bg.png') 0 0 / cover no-repeat fixed #fff;
}
```

```css
/* Lynx */
.box {
  background-image: url('bg.png');
  background-position: 0 0;
  background-size: cover;
  background-repeat: no-repeat;
  background-color: #fff;
}
```

```css
/* 如果只需要纯色背景，直接使用 background-color */
.box {
  background-color: green;
}
```
