mirror of
https://github.com/panatgithub/AnkiHeadingSync.git
synced 2026-07-22 06:51:43 +00:00
fix: fixed opaque sticky gap; replaced transparent margin with page header padding; card headers dock below measured page header height; 8px gap is now part of the opaque page header background
修复不透明粘性间隙; 使用页面标题内边距(padding)替换透明外边距(margin); 卡片标题现在停靠在已测量页面标题高度下方; 8px 间隙现在是不透明页面标题背景的一部分。
This commit is contained in:
parent
cdfbf2d12c
commit
0955ba952d
4 changed files with 181 additions and 6 deletions
97
docs/settings-sticky-opaque-gap-decisions.md
Normal file
97
docs/settings-sticky-opaque-gap-decisions.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# 设置页 Sticky 不透明间距修复决策
|
||||
|
||||
本文锁定本轮“8px 间距属于不透明页面标题背景”的最终实现策略。
|
||||
|
||||
## 1. 共享常量决策
|
||||
|
||||
- 保留并继续使用:
|
||||
- `SETTINGS_STICKY_CARD_GAP_PX = 8`
|
||||
- `SETTINGS_PAGE_HEADER_FALLBACK_HEIGHT_PX = 64`
|
||||
|
||||
原因:
|
||||
|
||||
- 这两个常量已经是当前 sticky 布局的统一输入,不需要再引入新的数值来源。
|
||||
|
||||
## 2. CSS 变量决策
|
||||
|
||||
- `containerEl` 继续写入:
|
||||
- `--ahs-settings-sticky-card-gap: 8px`
|
||||
- `--ahs-settings-page-header-height: 64px`
|
||||
- 页面标题高度变量继续由测量结果覆盖。
|
||||
|
||||
原因:
|
||||
|
||||
- gap 变量仍需要参与 pageHeader 底部 padding 计算。
|
||||
- 卡片标题 sticky `top` 仍需要使用页面标题高度变量作为最终停靠位置。
|
||||
|
||||
## 3. 页面标题不透明间距策略
|
||||
|
||||
- 页面标题继续保持:
|
||||
- `position: sticky`
|
||||
- `top: 0px`
|
||||
- `zIndex: 300`
|
||||
- 不透明背景
|
||||
- 取消透明 `marginBottom` 作为间距来源。
|
||||
- 改为:
|
||||
|
||||
```ts
|
||||
paddingTop = "12px"
|
||||
paddingBottom = "calc(12px + var(--ahs-settings-sticky-card-gap, 8px))"
|
||||
marginBottom = "0"
|
||||
```
|
||||
|
||||
原因:
|
||||
|
||||
- 8px 视觉间距必须属于 pageHeader 自身背景绘制区域,否则无法阻止下层内容透出。
|
||||
|
||||
## 4. 保留现有 mask 策略
|
||||
|
||||
- 保留 `settingsPageHeaderMask`。
|
||||
- 继续使用相同不透明背景,并跟随 pageHeader sticky。
|
||||
|
||||
原因:
|
||||
|
||||
- 这层 mask 负责遮住标题上方和左右透明缺口,本轮问题不在这里,不需要重做。
|
||||
|
||||
## 5. 卡片标题 sticky top 策略
|
||||
|
||||
- 每个 `settingsCardToggle` 继续 sticky。
|
||||
- `top` 改回:
|
||||
|
||||
```ts
|
||||
var(--ahs-settings-page-header-height, 64px)
|
||||
```
|
||||
|
||||
- 不再额外 `+ 8px`。
|
||||
|
||||
原因:
|
||||
|
||||
- 8px gap 已经被吸收到 pageHeader 自身 padding 中。
|
||||
- `--ahs-settings-page-header-height` 的真实测量值会包含这段 padding。
|
||||
- 若继续额外加 gap,会多出一段额外间距。
|
||||
|
||||
## 6. 测量与 cleanup 决策
|
||||
|
||||
- 继续复用已有 `ResizeObserver` 和 `getBoundingClientRect().height`。
|
||||
- `hide()` 继续断开 observer。
|
||||
|
||||
原因:
|
||||
|
||||
- 现有测量路径已经满足“测量包含 padding 的真实 header 高度”的需求。
|
||||
|
||||
## 7. 不变边界
|
||||
|
||||
- 不改 DOM 结构。
|
||||
- 不改 `toggleCard()`、`renderCard()`、`expandedCardIds`、`bodyEl.empty()`。
|
||||
- 不改局部刷新逻辑。
|
||||
- 不改设置数据结构和 Anki 同步逻辑。
|
||||
- 不引入 scroll listener、`IntersectionObserver` 或 `requestAnimationFrame`。
|
||||
|
||||
## 8. 与上一轮 sticky gap 方案的差异
|
||||
|
||||
- 上一轮把 8px 间距放在 pageHeader 外部 margin,并让 card header `top` 额外加 gap。
|
||||
- 本轮改为让 8px 成为 pageHeader 自身不透明 padding 的一部分。
|
||||
|
||||
原因:
|
||||
|
||||
- 新截图证明仅固定数值还不够,间距承载层也必须是不透明的 header 本体。
|
||||
75
docs/settings-sticky-opaque-gap-gap-report.md
Normal file
75
docs/settings-sticky-opaque-gap-gap-report.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# 设置页 Sticky 不透明间距差距审计
|
||||
|
||||
本文基于当前仓库真实实现,定位页面级 `Anki Heading Sync` 标题栏与卡片标题栏之间 8px 间距为何仍然会透出下层内容。
|
||||
|
||||
## 1. 当前 8px 视觉间距仍来自 `pageHeaderEl.style.marginBottom`
|
||||
|
||||
- 入口仍在 [src/presentation/settings/PluginSettingTab.ts](src/presentation/settings/PluginSettingTab.ts)。
|
||||
- 当前 `display()` 中页面标题 `pageHeaderEl` 已经是 sticky,且背景不透明。
|
||||
- 但标题与第一张卡片标题之间的 8px 间距目前仍由:
|
||||
|
||||
```ts
|
||||
pageHeaderEl.style.marginBottom = SETTINGS_STICKY_CARD_GAP_VALUE;
|
||||
```
|
||||
|
||||
提供。
|
||||
|
||||
这意味着当前 8px 仍然属于 header 外部透明 margin,而不是 header 自身不透明背景的一部分。
|
||||
|
||||
## 2. 透明 margin 正是截图里透视的根因
|
||||
|
||||
- CSS margin 不属于元素背景绘制区域。
|
||||
- 因此即使 `pageHeaderEl` 本体有不透明背景,margin 区域也仍然是透明的。
|
||||
- 当前截图里 `Anki Heading Sync` 与卡片标题之间会露出下层文字,和这一路径完全一致。
|
||||
|
||||
结论:只要这段 8px 继续由 `marginBottom` 提供,就无法做到“不透明的 sticky 标题间距”。
|
||||
|
||||
## 3. 当前卡片标题 sticky top 还额外加上了 gap
|
||||
|
||||
- 当前每个 `settingsCardToggle` 已经是 sticky。
|
||||
- 但其 `top` 现在写的是:
|
||||
|
||||
```ts
|
||||
calc(var(--ahs-settings-page-header-height, 64px) + var(--ahs-settings-sticky-card-gap, 8px))
|
||||
```
|
||||
|
||||
- 这表示卡片 sticky 偏移仍把 gap 视作“header 外部额外距离”。
|
||||
|
||||
如果本轮把 8px 合并进 pageHeader 自身 padding,那么卡片标题 `top` 就必须回到:
|
||||
|
||||
```ts
|
||||
var(--ahs-settings-page-header-height, 64px)
|
||||
```
|
||||
|
||||
否则会重复多算一次 8px。
|
||||
|
||||
## 4. 当前页面标题高度测量路径已经可以复用
|
||||
|
||||
- 当前仓库已经具备:
|
||||
- `--ahs-settings-page-header-height`
|
||||
- `ResizeObserver`
|
||||
- `getBoundingClientRect().height`
|
||||
- `hide()` 中 observer 清理
|
||||
|
||||
因此,本轮不需要新增测量机制,只需要让真实测量高度包含新的底部 padding。
|
||||
|
||||
## 5. 当前最小可行修复路径
|
||||
|
||||
在不改 DOM 结构、不改业务行为前提下,最小修复应为:
|
||||
|
||||
1. 保留 pageHeader sticky 和现有 mask。
|
||||
2. 取消透明 `marginBottom`。
|
||||
3. 改由 `pageHeaderEl` 自身底部 padding 承载 8px 间距。
|
||||
4. 让 `--ahs-settings-page-header-height` 测量包含这段 padding。
|
||||
5. 让卡片标题 sticky `top` 仅等于页面标题高度变量,不再额外加 gap。
|
||||
|
||||
## 6. 结论
|
||||
|
||||
当前真实差距是:
|
||||
|
||||
1. 8px 间距仍由透明 margin 提供。
|
||||
2. 这会让下层文本和内容穿透该区域。
|
||||
3. 卡片标题 sticky top 仍把 gap 当作 header 外部距离来计算。
|
||||
4. 现有测量和清理路径已经足够支撑修复。
|
||||
|
||||
因此,本轮应把 8px 从透明 margin 收编到 pageHeader 自身 padding 内,使间距属于不透明 header 背景,并让卡片标题直接停靠在测量后的真实 pageHeader 高度下方。
|
||||
|
|
@ -609,7 +609,7 @@ describe("PluginSettingTab", () => {
|
|||
expect(queryByDataset(tab.containerEl, "settingsCardBody", "deck").style.display).toBe("none");
|
||||
});
|
||||
|
||||
it("applies sticky styles to the page header, shared gap, mask, and card headers", () => {
|
||||
it("applies sticky styles to the opaque page header gap, mask, and card headers", () => {
|
||||
const plugin = new FakePlugin();
|
||||
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
|
||||
|
||||
|
|
@ -622,7 +622,8 @@ describe("PluginSettingTab", () => {
|
|||
expect(pageHeader.style.zIndex).toBe("300");
|
||||
expect(pageHeader.style.width).toBe("100%");
|
||||
expect(pageHeader.style.boxSizing).toBe("border-box");
|
||||
expect(pageHeader.style.marginBottom).toBe("var(--ahs-settings-sticky-card-gap, 8px)");
|
||||
expect(pageHeader.style.marginBottom).toBe("0");
|
||||
expect(pageHeader.style.paddingBottom).toBe("calc(12px + var(--ahs-settings-sticky-card-gap, 8px))");
|
||||
expect(pageHeader.style.backgroundColor).toBe("var(--modal-background, var(--background-primary))");
|
||||
expect(pageHeader.style.boxShadow).toBe("none");
|
||||
expect(collectTexts(pageHeader)).toContain("Anki Heading Sync");
|
||||
|
|
@ -640,7 +641,7 @@ describe("PluginSettingTab", () => {
|
|||
for (const cardId of ["card-types", "sync-content", "scope", "deck", "commands"] as const) {
|
||||
const header = queryByDataset(tab.containerEl, "settingsCardToggle", cardId);
|
||||
expect(header.style.position).toBe("sticky");
|
||||
expect(header.style.top).toBe("calc(var(--ahs-settings-page-header-height, 64px) + var(--ahs-settings-sticky-card-gap, 8px))");
|
||||
expect(header.style.top).toBe("var(--ahs-settings-page-header-height, 64px)");
|
||||
expect(header.style.zIndex).toBe("200");
|
||||
expect(header.style.display).toBe("flex");
|
||||
expect(header.style.width).toBe("100%");
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ const SETTINGS_PAGE_HEADER_HEIGHT_FALLBACK = `${SETTINGS_PAGE_HEADER_FALLBACK_HE
|
|||
const SETTINGS_STICKY_CARD_GAP = `${SETTINGS_STICKY_CARD_GAP_PX}px`;
|
||||
const SETTINGS_PAGE_HEADER_HEIGHT_VALUE = `var(${SETTINGS_PAGE_HEADER_HEIGHT_VARIABLE}, ${SETTINGS_PAGE_HEADER_HEIGHT_FALLBACK})`;
|
||||
const SETTINGS_STICKY_CARD_GAP_VALUE = `var(${SETTINGS_STICKY_CARD_GAP_VARIABLE}, ${SETTINGS_STICKY_CARD_GAP})`;
|
||||
const SETTINGS_PAGE_HEADER_PADDING_TOP = "12px";
|
||||
const SETTINGS_PAGE_HEADER_MASK_TOP = "-128px";
|
||||
const SETTINGS_PAGE_HEADER_MASK_SIDE = "-24px";
|
||||
|
||||
|
|
@ -118,8 +119,9 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
pageHeaderEl.style.zIndex = "300";
|
||||
pageHeaderEl.style.width = "100%";
|
||||
pageHeaderEl.style.boxSizing = "border-box";
|
||||
pageHeaderEl.style.padding = "12px 0";
|
||||
pageHeaderEl.style.marginBottom = SETTINGS_STICKY_CARD_GAP_VALUE;
|
||||
pageHeaderEl.style.paddingTop = SETTINGS_PAGE_HEADER_PADDING_TOP;
|
||||
pageHeaderEl.style.paddingBottom = `calc(${SETTINGS_PAGE_HEADER_PADDING_TOP} + ${SETTINGS_STICKY_CARD_GAP_VALUE})`;
|
||||
pageHeaderEl.style.marginBottom = "0";
|
||||
pageHeaderEl.style.overflow = "visible";
|
||||
pageHeaderEl.style.background = SETTINGS_PAGE_HEADER_BACKGROUND;
|
||||
pageHeaderEl.style.backgroundColor = SETTINGS_PAGE_HEADER_BACKGROUND;
|
||||
|
|
@ -171,7 +173,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
headerEl.style.alignItems = "center";
|
||||
headerEl.style.justifyContent = "flex-start";
|
||||
headerEl.style.position = "sticky";
|
||||
headerEl.style.top = `calc(${SETTINGS_PAGE_HEADER_HEIGHT_VALUE} + ${SETTINGS_STICKY_CARD_GAP_VALUE})`;
|
||||
headerEl.style.top = SETTINGS_PAGE_HEADER_HEIGHT_VALUE;
|
||||
headerEl.style.zIndex = "200";
|
||||
headerEl.style.width = "100%";
|
||||
headerEl.style.maxWidth = "100%";
|
||||
|
|
|
|||
Loading…
Reference in a new issue