fix(ui): 四个 UI 组件级修复 — word-diff 阈值/Markdown 锚点/Tooltip 翻转/HC 玻璃对比度 (#1505 #1506 #1507 #1509) - #1616
Conversation
MAX_LINE_CHARS 2000→800、MAX_LINE_WORDS 500→200,避免大 diff 行触发 O(n*m) LCS 卡顿 UI;补边界值测试并移除 TODO。
引入 rehype-slug 为 h1-h6 生成 slug id(保留中文、重复加 -1/-2),标题 hover 显示链接图标可跳转锚点。
打开时测量并在 scroll/resize 时重测,超出视口右/下边界翻转到对侧 data-side;补翻转与 reduced-motion 测试,移除 TODO。
forced-colors 下玻璃背景 token 映射 Canvas、prefers-contrast: more 下映射实体表面并关闭 blur,保证文字对比度;移除 TODO(a11y #20)。
📝 WalkthroughWalkthroughThe PR adds Markdown heading anchors, viewport-aware tooltip flipping, high-contrast glass-token overrides, and lower word-diff guard thresholds with updated tests. ChangesMarkdown heading anchors
Viewport-aware tooltip placement
High-contrast glass tokens
Word-diff guard calibration
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/shared/src/styles/tokens-base.css`:
- Around line 513-516: Update the forced-colors baseline comment above the
related styles to reflect that the repository now includes forced-colors and
prefers-contrast media queries with Windows High Contrast adaptations. Remove
the obsolete “zero queries/no adaptation” claim, or label it explicitly as
historical scan output with its date.
- Around line 574-575: Update the forced-colors token definitions near
--glass-text-muted and --glass-text-disabled so the primary --glass-text token
also maps to CanvasText, or ensure the affected glass surfaces use
forced-color-adjust: auto. Keep the existing muted and disabled mappings intact
and ensure primary glass text follows the system palette.
- Around line 556-558: Update the comment describing the high-contrast Canvas
and CanvasText fallback to remove the fixed “21:1” contrast guarantee. State
that contrast is system-defined, or narrow the claim to the specific palette
scope covered by testing, while preserving the explanation that the decorative
glass background becomes an opaque system surface.
In `@app/shared/src/ui/Markdown.module.css`:
- Around line 48-57: Reorder the rules in the stylesheet so the standalone
.headingAnchor:hover and .headingAnchor:focus-visible declarations precede the
more specific .root :is(h1, h2, h3, h4, h5, h6):hover .headingAnchor selector.
Combine the existing focus-visible declarations into one rule while preserving
their current styles.
In `@app/shared/src/ui/Markdown.tsx`:
- Around line 104-110: Update HeadingWithAnchor’s props type to declare id as
optional string without explicitly including undefined, while preserving the
rest-props spread so generated IDs continue reaching the anchor.
In `@app/shared/src/ui/Tooltip.test.tsx`:
- Around line 320-327: Update the reduced-motion test in the “Tooltip reduced
motion (`#1507`)” describe block to extract the `@media` (prefers-reduced-motion:
no-preference) block from tooltipCss, assert animation: tooltipIn appears within
that block, and assert it does not appear outside the rule. Add an unconditional
animation: tooltipIn fixture so the test detects regressions where the animation
is defined outside the media query.
In `@app/shared/src/ui/Tooltip.tsx`:
- Around line 127-139: The side-change reset in Tooltip.tsx must not clear
flipped while the tooltip is open. Update the side-dependent effect around
measureAndMaybeFlip to reset only when closed, or perform the reset within the
layout measurement before applying the measured flip; preserve immediate
correction when an open tooltip changes to an overflowing side. Update the
related expectations in app/shared/src/ui/Tooltip.test.tsx lines 299-317 to
match the corrected open-tooltip behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 4e3d4ddf-bbc0-4a03-b293-9eb280f9947c
⛔ Files ignored due to path filters (1)
app/pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (9)
app/shared/package.jsonapp/shared/src/styles/tokens-base.cssapp/shared/src/ui/Markdown.module.cssapp/shared/src/ui/Markdown.test.tsxapp/shared/src/ui/Markdown.tsxapp/shared/src/ui/Tooltip.test.tsxapp/shared/src/ui/Tooltip.tsxapp/shared/src/ui/diffWordTokens.test.tsapp/shared/src/ui/diffWordTokens.ts
| /* ═══ Forced colors (Windows High Contrast) baseline — a11y #20 ═══════════ | ||
| tri-scan P2/N1: 全仓零 forced-colors / prefers-contrast 媒体查询。 | ||
| Windows 高对比模式用户无适配——玻璃 alpha 背景被系统色覆盖、品牌色不映射 | ||
| 系统语义色。本块保证 HC 模式下文字可见、边框不消失、焦点环用系统色。 | ||
| 系统语义色。本块保证 HC 模式下文字可见、边框不消失、焦点环用系统色; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Update the stale forced-colors status comment.
The text says the repository has zero forced-colors and prefers-contrast media queries and no Windows High Contrast adaptation. This file now contains both media queries and the adaptation. If this is historical scan output, label it with a date; otherwise replace it with the current status.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/shared/src/styles/tokens-base.css` around lines 513 - 516, Update the
forced-colors baseline comment above the related styles to reflect that the
repository now includes forced-colors and prefers-contrast media queries with
Windows High Contrast adaptations. Remove the obsolete “zero queries/no
adaptation” claim, or label it explicitly as historical scan output with its
date.
| /* 玻璃背景 token → 不透明 Canvas(#1509):HC 下玻璃「装饰背景层」降级为 | ||
| 实体系统表面,文字层由系统色强制为 CanvasText —— 无论用户选择浅色还是 | ||
| 深色 HC 主题,对比度都是 21:1,不再依赖已知 app-bg 配色的假设。 */ |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the targeted comment and nearby forced-color CSS context.
if [ -f app/shared/src/styles/tokens-base.css ]; then
echo "== tokens-base.css context =="
sed -n '520,590p' app/shared/src/styles/tokens-base.css
echo
echo "== relevant token references =="
rg -n "glass|backdrop-filter|forced-colors|Canvas|CanvasText|app-bg" app/shared/src/styles/tokens-base.css app -g '*.{css,scss,ts,tsx}' || true
else
echo "app/shared/src/styles/tokens-base.css not found"
echo "Files:"
git ls-files | rg 'tokens-base\.css$|tokens|style'
fi
# Search for precise 21:1 claim in repository.
echo
echo "== 21:1 references =="
rg -n "21:1|对比度都是 21:1|contrast|高对比度|forced-colors|forced-colors" app --glob '*.{css,scss,ts,tsx,md,mdx}' || trueRepository: TokenDanceLab/AgentHub
Length of output: 50379
🌐 Web query:
CSS Color Module Canvas forced-colors CanvasText contrast ratio guaranteed 21:1
💡 Result:
There is no guarantee of a 21:1 contrast ratio for Canvas elements (or any other elements) in CSS Forced Colors Mode. Forced Colors Mode, such as Windows High Contrast Mode, overrides author-defined colors with a user-selected, high-contrast palette [1][2]. The browser maps these to CSS system color keywords like Canvas (background) and CanvasText (text) [3][4]. While this mode is designed to improve legibility for users with low vision by providing a high-contrast environment, the specific contrast ratio depends entirely on the palette configured by the user at the operating system or browser level [3][2]. Key points regarding this behavior include: - No 21:1 Guarantee: While Forced Colors Mode is intended to ensure adequate contrast, it does not mathematically enforce a 21:1 (maximum) contrast ratio [5]. The resulting contrast is determined by the specific color pairings the user chooses in their OS settings [2][6]. - System Colors: When forced-colors: active is detected, browsers automatically adjust colors to system keywords like Canvas and CanvasText [1][7]. Authors should use these keywords to ensure their content remains readable and respects the user's chosen palette [2][4]. - Backplates: To further assist with legibility—particularly when text is rendered over complex backgrounds—browsers may automatically draw text backplates (an opaque background behind the text) [1][8]. - Canvas-Specific Context: In the context of the HTML <canvas> element, content drawn via the Canvas API is not automatically recolored by the browser's Forced Colors Mode in the same way as DOM-based content [9]. Authors must manually detect the forced-colors media feature using JavaScript or CSS and manually adjust the colors used in their canvas drawing commands to match the system palette [2][9]. In summary, while Forced Colors Mode provides a high-contrast mechanism, it is not a mechanism for guaranteeing a 21:1 contrast ratio, and Canvas elements require explicit author intervention to support forced colors [1][2][9].
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/%40media/forced-colors
- 2: https://specification.website/spec/accessibility/forced-colors/
- 3: https://github.com/mdn/content/blob/main/files/en-us/web/css/reference/values/system-color/index.md
- 4: https://polypane.app/blog/forced-colors-explained-a-practical-guide/
- 5: https://a11ypath.com/guides/color-contrast/
- 6: https://benmyers.dev/encyclopedia/forced-colors-mode/
- 7: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/forced-colors
- 8: https://www.w3.org/TR/css-color-adjust-1/
- 9: Do not expose forced colors WICG/html-in-canvas#102
Remove the fixed 21:1 guarantee.
Canvas and CanvasText are user-selected forced-colors system colors. The CSS specification does not define an exact 21:1 ratio for this mapping. Describe the intent as system-defined contrast, or document the specific palette scope covered by testing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/shared/src/styles/tokens-base.css` around lines 556 - 558, Update the
comment describing the high-contrast Canvas and CanvasText fallback to remove
the fixed “21:1” contrast guarantee. State that contrast is system-defined, or
narrow the claim to the specific palette scope covered by testing, while
preserving the explanation that the decorative glass background becomes an
opaque system surface.
Source: MCP tools
| --glass-text-muted: CanvasText !important; | ||
| --glass-text-disabled: CanvasText !important; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
rg -n -C 2 -i -- '--glass-text|--text-[0-9]+|forced-color-adjust|glass' app/shared/src --glob '*.{css,scss,ts,tsx}'Repository: TokenDanceLab/AgentHub
Length of output: 50379
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== tokens-base glass token context =="
sed -n '540,600p' app/shared/src/styles/tokens-base.css
echo "== forced-color-adjust occurrences =="
rg -n -- '--glass-text|forced-color-adjust' app/shared/src --glob '*.{css,scss,ts,tsx}' --max-count 200
echo "== glass text occurrences narrowed =="
rg -n -- 'var\(--glass-text|glass-text' app/shared/src --glob '*.{css,scss,ts,tsx}' --max-count 200Repository: TokenDanceLab/AgentHub
Length of output: 3558
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== forced-color-adjust context =="
sed -n '620,645p' app/shared/src/styles/tokens-base.css
echo "== all glass text usages =="
rg -n --no-heading 'var\(--glass-text(?:--[a-z0-9-]+)?)|--glass-text--' app/shared/src --glob '*.{css,scss,ts,tsx}' || true
echo "== all glass class/context references likely using text tokens =="
rg -n --no-heading '\bglass\b|glass-|glass ' app/shared/src --glob '*.{css,scss,ts,tsx}' | head -n 300 || true
echo "== Card surface selectors =="
sed -n '1,260p' app/shared/src/ui/Card.module.css 2>/dev/null || true
ast-grep outline app/shared/src/ui/Card.module.css 2>/dev/null || trueRepository: TokenDanceLab/AgentHub
Length of output: 30121
Map primary glass text tokens in forced colors.
Only --glass-text-muted and --glass-text-disabled map to CanvasText; --glass-text/--text-* remains author-colored. Either add a primary glass text token mapping or keep those glass surfaces on forced-color-adjust: auto where text should follow the system palette.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/shared/src/styles/tokens-base.css` around lines 574 - 575, Update the
forced-colors token definitions near --glass-text-muted and
--glass-text-disabled so the primary --glass-text token also maps to CanvasText,
or ensure the affected glass surfaces use forced-color-adjust: auto. Keep the
existing muted and disabled mappings intact and ensure primary glass text
follows the system palette.
Source: MCP tools
| .root :is(h1, h2, h3, h4, h5, h6):hover .headingAnchor, | ||
| .headingAnchor:focus-visible { | ||
| opacity: 1; | ||
| } | ||
|
|
||
| .headingAnchor:hover { | ||
| color: var(--primary); | ||
| } | ||
|
|
||
| .headingAnchor:focus-visible { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Fix the selector order so Stylelint passes.
The selector at Line 48 has higher specificity than .headingAnchor:hover and .headingAnchor:focus-visible. Stylelint reports no-descending-specificity errors for these rules. Move the hover and focus rules before the heading-hover rule. Combine the two focus rules when you move them.
Proposed fix
-.root :is(h1, h2, h3, h4, h5, h6):hover .headingAnchor,
-.headingAnchor:focus-visible {
- opacity: 1;
-}
-
.headingAnchor:hover {
color: var(--primary);
}
.headingAnchor:focus-visible {
+ opacity: 1;
outline: none;
color: var(--primary);
border-radius: var(--r-xs, 4px);
box-shadow: var(--focus-ring);
}
+
+.root :is(h1, h2, h3, h4, h5, h6):hover .headingAnchor {
+ opacity: 1;
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .root :is(h1, h2, h3, h4, h5, h6):hover .headingAnchor, | |
| .headingAnchor:focus-visible { | |
| opacity: 1; | |
| } | |
| .headingAnchor:hover { | |
| color: var(--primary); | |
| } | |
| .headingAnchor:focus-visible { | |
| .headingAnchor:hover { | |
| color: var(--primary); | |
| } | |
| .headingAnchor:focus-visible { | |
| opacity: 1; | |
| outline: none; | |
| color: var(--primary); | |
| border-radius: var(--r-xs, 4px); | |
| box-shadow: var(--focus-ring); | |
| } | |
| .root :is(h1, h2, h3, h4, h5, h6):hover .headingAnchor { | |
| opacity: 1; | |
| } |
🧰 Tools
🪛 Stylelint (17.14.0)
[error] 49-49: Expected selector ".headingAnchor:focus-visible" to come before selector ".root :is(h1, h2, h3, h4, h5, h6):hover .headingAnchor", at line 48 (no-descending-specificity)
(no-descending-specificity)
[error] 53-53: Expected selector ".headingAnchor:hover" to come before selector ".root :is(h1, h2, h3, h4, h5, h6):hover .headingAnchor", at line 48 (no-descending-specificity)
(no-descending-specificity)
[error] 57-57: Expected selector ".headingAnchor:focus-visible" to come before selector ".root :is(h1, h2, h3, h4, h5, h6):hover .headingAnchor", at line 48 (no-descending-specificity)
(no-descending-specificity)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/shared/src/ui/Markdown.module.css` around lines 48 - 57, Reorder the
rules in the stylesheet so the standalone .headingAnchor:hover and
.headingAnchor:focus-visible declarations precede the more specific .root
:is(h1, h2, h3, h4, h5, h6):hover .headingAnchor selector. Combine the existing
focus-visible declarations into one rule while preserving their current styles.
Source: Linters/SAST tools
| function HeadingWithAnchor({ | ||
| tag, | ||
| id, | ||
| node: _node, | ||
| children, | ||
| ...rest | ||
| }: { tag: HeadingTag; id?: string | undefined; node?: unknown } & HTMLAttributes<HTMLHeadingElement>) { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -a -HI '^tsconfig.*\.json$' . -x rg -n -C 2 '"exactOptionalPropertyTypes"\s*:\s*true' {}
rg -n -C 3 'id\?:\s*string\s*\|\s*undefined|<HeadingWithAnchor' app/shared/src/ui/Markdown.tsxRepository: TokenDanceLab/AgentHub
Length of output: 494
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Markdown excerpt =="
sed -n '90,130p' app/shared/src/ui/Markdown.tsx
echo
echo "== tsconfig files with exactOptionalPropertyTypes =="
for f in $(fd -HI '^tsconfig.*\.json$' .); do
if rg -q '"exactOptionalPropertyTypes"\s*:\s*true' "$f"; then
echo "-- $f --"
sed -n '1,20p' "$f"
fi
doneRepository: TokenDanceLab/AgentHub
Length of output: 2714
Keep HeadingWithAnchor.id as an optional string under exactOptionalPropertyTypes.
id?: string | undefined accepts an explicit undefined, which violates the shared rule to conditionally assign only defined values. Use id?: string, then pass the spread rest props so generated IDs still reach the anchor.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/shared/src/ui/Markdown.tsx` around lines 104 - 110, Update
HeadingWithAnchor’s props type to declare id as optional string without
explicitly including undefined, while preserving the rest-props spread so
generated IDs continue reaching the anchor.
Source: Coding guidelines
| // ── Reduced motion ──────────────────────────────────────────────────── | ||
| describe('Tooltip reduced motion (#1507)', () => { | ||
| it('gates the enter animation behind prefers-reduced-motion: no-preference', () => { | ||
| // jsdom cannot evaluate media queries, so assert the CSS contract: | ||
| // the animation must live inside the no-preference block only. | ||
| expect(tooltipCss).toMatch(/@media \(prefers-reduced-motion: no-preference\)/); | ||
| expect(tooltipCss).toMatch(/animation:\s*tooltipIn/); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)Tooltip\.test\.tsx$|(^|/)Tooltip\.' || true
echo "== outline =="
ast-grep outline app/shared/src/ui/Tooltip.test.tsx --view expanded || true
echo "== relevant test section =="
sed -n '290,350p' app/shared/src/ui/Tooltip.test.tsx
echo "== tooltipCss usages =="
rg -n "tooltipCss|prefers-reduced-motion|animation:\s*tooltipIn|Animation" app/shared/src/ui/Tooltip.test.tsx app/shared/src/ui/Tooltip.tsx app/shared/src/ui/Tooltip* 2>/dev/null || true
echo "== tooltip source style excerpts =="
rg -n "animation|`@media`|prefers-reduced-motion|tooltipIn" app/shared/src/ui/Tooltip.tsx app/shared/src/ui/Tooltip.* 2>/dev/null || trueRepository: TokenDanceLab/AgentHub
Length of output: 4555
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
css = Path('app/shared/src/ui/Tooltip.module.css').read_text()
checks = [
re.search(r'`@media` \(prefers-reduced-motion: no-preference\)', css) is not None,
re.search(r'animation:\s*tooltipIn', css) is not None,
]
print('current-both-pass', all(checks))
unconditional_fixture = """
.other { animation: tooltipIn 0.12s ease-out backwards; }
`@keyframes` tooltipIn { 0% { opacity: 0; } 100% { opacity: 1; } }
`@media` (prefers-reduced-motion: no-preference) {
/* no animation here */
}
"""
bad_checks = [
'prefers-reduced-motion' in unconditional_fixture,
re.search(r'animation:\s*tooltipIn', unconditional_fixture) is not None,
]
bad_all_pass = all(bad_checks)
print('unconditional-fixture-current-checks-match-current-repo-outcome', bad_all_pass)
media_block = css[css.find('`@media` (prefers-reduced-motion: no-preference)'):css.find('`@keyframes` tooltipIn')]
has_inside = re.search(r'animation:\s*tooltipIn', media_block) is not None
outside = css[:css.find('`@media` (prefers-reduced-motion: no-preference)')] + css[css.find('`@keyframes` tooltipIn'):]
has_outside = re.search(r'animation:\s*tooltipIn', outside) is not None
print('inside-media', has_inside)
print('outside-media', has_outside)
PYRepository: TokenDanceLab/AgentHub
Length of output: 290
Scope the animation assertion to the reduced-motion media rule.
These whole-file regex checks match unrelated CSS rules too. Extract the @media (prefers-reduced-motion: no-preference) block, assert animation: tooltipIn is inside it, and assert it is not present outside that rule. Add an unconditional animation: tooltipIn fixture to show the assertion catches regresses.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/shared/src/ui/Tooltip.test.tsx` around lines 320 - 327, Update the
reduced-motion test in the “Tooltip reduced motion (`#1507`)” describe block to
extract the `@media` (prefers-reduced-motion: no-preference) block from
tooltipCss, assert animation: tooltipIn appears within that block, and assert it
does not appear outside the rule. Add an unconditional animation: tooltipIn
fixture so the test detects regressions where the animation is defined outside
the media query.
| // Reset the flip when the requested side changes (e.g. consumer switches | ||
| // side while the tooltip is closed); the open-path measure below re-applies | ||
| // the correct value when the tooltip is visible. | ||
| useEffect(() => { | ||
| setFlipped(false); | ||
| }, [side]); | ||
|
|
||
| // Measure right after the tooltip enters the DOM (before paint) so the | ||
| // flipped side never flashes. | ||
| useLayoutEffect(() => { | ||
| if (!open) return; | ||
| measureAndMaybeFlip(); | ||
| }, [open, measureAndMaybeFlip]); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files:"
git ls-files | rg '^app/shared/src/ui/(Tooltip\.tsx|Tooltip\.test\.tsx)$' || true
echo
echo "Tooltip outline:"
ast-grep outline app/shared/src/ui/Tooltip.tsx --view expanded || true
echo
echo "Tooltip relevant lines:"
sed -n '1,220p' app/shared/src/ui/Tooltip.tsx | cat -n
echo
echo "Test relevant lines:"
sed -n '1,380p' app/shared/src/ui/Tooltip.test.tsx | cat -nRepository: TokenDanceLab/AgentHub
Length of output: 22986
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Tooltip remainder:"
sed -n '220,360p' app/shared/src/ui/Tooltip.tsx | cat -n
echo
echo "Tests for rerendering after focus/delay:"
rg -n "rerender|side=|focus|show|data-side|act\\(" app/shared/src/ui/Tooltip.test.tsx -A 4 -B 4
echo
echo "Behavioral probe of React effect order/states in this update pattern:"
node - <<'JS'
const states = [];
function flushUpdates() {
for (const fn of updates.splice(0)) fn();
}
let updates = [];
function setNext(state, setStateFn) {
if (setStateFn && typeof setStateFn === 'function') {
try {
const nextState = setStateFn(state);
if (Object.is(state, nextState)) return state;
const newState = nextState;
states.push(`setFlipped(${nextState})`);
return newState;
} catch (e) {
throw e;
}
} else if (!Object.is(state, stateNext)) {
states.push(`setFlipped(${stateNext})`);
return stateNext;
}
return state;
}
let flipped = false;
let rerenderFlipped = flipped;
const state = { flipped };
function setState(x) {
if (x && typeof x === 'function') {
const stateNext = x(flipped);
if (!Object.is(flipped, stateNext)) flipped = stateNext;
} else if (!Object.is(flipped, x)) flipped = x;
}
function rerender() {
// layout effect callback
rerenderFlipped = setNext(rerenderFlipped, () => true);
// passive effect callback scheduled before commit is observed here as "scheduled", not applied
states.push('scheduled side-reset: setFlipped(false)');
flushUpdates();
}
rerender();
console.log({ flipped, rerenderFlipped, scheduled: states.join(' then ')});
JSRepository: TokenDanceLab/AgentHub
Length of output: 9555
🌐 Web query:
React 19 effects order layout effects passive effects state updates commit
💡 Result:
In React 19, the execution order of effects during the commit phase remains consistent with recent versions, following a strict sequence that separates synchronous DOM-related work from asynchronous passive tasks [1][2][3]. When React completes the render phase and proceeds to the commit phase, it performs the following steps in order [3][4]: 1. DOM Mutation: React applies the necessary changes to the host tree [3][4]. 2. Layout Effects: React synchronously executes all useLayoutEffect callbacks [1][2][5]. Because this occurs after the DOM is updated but before the browser paints, it is the appropriate phase for measuring DOM elements or making synchronous DOM adjustments to prevent visual flickering [6][7][5]. 3. Browser Paint: The browser paints the updated UI to the screen [1][7][4]. 4. Passive Effects: React schedules and runs all useEffect callbacks [1][2][4]. These are processed as a separate task after the paint, ensuring they do not block the browser from updating the screen [1][7][5]. Regarding state updates and effect behavior: - Synchronous State Updates: If a state update is triggered within useLayoutEffect, React will perform a second, synchronous render immediately [6]. This ensures the UI remains consistent, and as part of this process, React will also flush pending passive effects before the final paint [6]. - Re-render Lifecycle: During re-renders, the cleanup functions for both useLayoutEffect and useEffect are executed before their respective new setup functions [1][8]. Specifically, layout cleanups run first, followed by layout setup, and then passive cleanups followed by passive setup [1][7]. - Consistency: React ensures that cleanups always run before the next instance of the same effect type, and no two instances of the same effect are active simultaneously [1]. Developers should rely on useLayoutEffect only when synchronous execution is required for visual correctness (e.g., measuring or repositioning elements) to avoid blocking the main thread unnecessarily [7][5]. For most other side effects, such as data fetching or subscriptions, useEffect is the standard, preferred choice [7][5].
Citations:
- 1: https://dev.to/samabaasi/how-react-works-part-5-the-react-lifecycle-from-the-inside-when-things-actually-run-oj4
- 2: https://gist.github.com/jsnanigans/6940daa7546e92543a9bf38b5fd1de0d
- 3: https://deepwiki.com/react/react/2.4-commit-phase-and-host-effects
- 4: https://react.wiki/performance/initial-vs-rerender/
- 5: https://lqtiendev.com/difference-between-useeffect-and-uselayouteffect-updates-for-react-19-e8cc9c93e439
- 6: why setState in useLayoutEffect forces useEffects to run before paint? react/react#17334
- 7: https://itlead.org/interview-questions/react/how-uselayouteffect-works-in-react-and-how-does-it-differ-from-useeffect
- 8: https://react.dev/reference/react/useEffect
🌐 Web query:
React 17 19 React 18 useLayoutEffect state updates pending passive effects before paint flush pending passive effects update state in useLayoutEffect
💡 Result:
The behavior of useLayoutEffect and its interaction with state updates and passive effects remains consistent across React 17, 18, and 19 [1][2][3]. Core Behavior and Timing useLayoutEffect is a version of useEffect that fires synchronously after all DOM mutations but before the browser paints the screen [1][4][5]. Because it executes in this window, it allows you to measure DOM elements and perform synchronous state updates that need to be reflected before the user sees any visual changes [1][6][5]. State Updates inside useLayoutEffect When you trigger a state update inside useLayoutEffect, React schedules a re-render [1][5]. Crucially, because useLayoutEffect must finish before the browser paints, React will process this re-render and execute all resulting effects (including any pending useEffect calls) synchronously before the browser is allowed to paint [1][6][5]. This prevents the user from seeing any intermediate "flicker" or inconsistent states that would occur if the paint happened before the second render [1][7]. Interaction with Passive Effects (useEffect) Passive effects (useEffect) are typically deferred until after the browser paints [1][5]. However, the behavior changes based on how the effect is triggered: 1. Standard Lifecycle: In most cases, useEffect runs asynchronously after the paint [1][5][8]. If a useLayoutEffect triggers a state update, React will "flush" all pending effects (including those from useEffect) synchronously as part of that re-render cycle, effectively forcing them to run before the paint [6][5]. 2. React 18/19 Discrete Inputs: Starting in React 18, if a useEffect is triggered by a discrete user input (e.g., a click), React will execute the useEffect function itself synchronously before layout and paint [9][5]. However, it is important to note that state updates scheduled inside that useEffect are still deferred—unlike useLayoutEffect, where state updates are processed immediately [9][5]. Summary of Differences * useLayoutEffect: Fires synchronously after DOM mutations, before paint [1][4]. State updates are processed immediately, and it forces all pending effects (including useEffect) to run before paint to maintain visual consistency [1][6][5]. * useEffect: Generally fires after paint [1][5]. In React 18/19, it may fire before paint if triggered by discrete user input, but state updates inside it are still treated as non-blocking/deferred [9][5]. If you observe unexpected renders or performance issues when using useLayoutEffect, it is likely due to the "cascading" nature of synchronous re-renders, which block the browser from painting until all work is finished [1][6]. Whenever possible, prefer useEffect to avoid blocking the main thread [1][7].
Citations:
- 1: https://react.dev/reference/react/useLayoutEffect
- 2: https://gist.github.com/jsnanigans/6940daa7546e92543a9bf38b5fd1de0d
- 3: https://medium.com/@ytahirkose/react-hooks-from-16-8-to-19-a-complete-guide-1edfb4c94806
- 4: https://blog.saeloun.com/2022/07/28/difference-between-useeffect-and-useeffectlayout-hooks/
- 5: https://legacy.reactjs.org/docs/hooks-reference.html
- 6: why setState in useLayoutEffect forces useEffects to run before paint? react/react#17334
- 7: https://uk.react.dev/reference/react/useLayoutEffect
- 8: https://lqtiendev.com/difference-between-useeffect-and-uselayouteffect-updates-for-react-19-e8cc9c93e439
- 9: https://stackoverflow.com/questions/73886356/difference-between-useeffect-and-uselayouteffect-in-react-18
🌐 Web query:
React 18 source scheduleUpdateOnFibre flushPassiveEffects updateLane Immediate priority layout effect state update current commit
💡 Result:
In React 18, the process of handling state updates and their subsequent rendering involves a structured pipeline driven by the fiber architecture, priority lanes, and the commit phase [1][2]. 1. Triggering an Update: When a state update is initiated (e.g., via useState's dispatch function), the process begins by calculating the update's priority using a Lane [3][4]. Lanes in React 18 represent specific categories of priority (e.g., SyncLane for high-priority user events, or various lanes for concurrent rendering) [5][4]. 2. scheduleUpdateOnFiber: This function is the primary entry point for any update [2][6]. It performs three main tasks: - It traverses the fiber tree upward to the root, marking the fibers and the root with the update's lane [5][2]. - It calls ensureRootIsScheduled, which communicates with the Scheduler to request a callback for the render work based on the lane's priority [5][7][6]. - If the update is high priority (SyncLane) and not currently within a transition or batch, React may immediately schedule a microtask to flush the synchronous work [5][8]. 3. Render and Commit Phases: - Render Phase: The React work loop executes, using the assigned lanes to determine which work is most urgent [5][2]. This phase is interruptible to allow concurrent rendering [2]. - Commit Phase: Once rendering is complete, the commit phase applies changes to the host environment (e.g., DOM) [9][1]. It is synchronous and consists of distinct sub-passes: - Mutation: Modifies the host tree [1][2]. - Layout: Runs synchronous layout effects (e.g., useLayoutEffect), which execute after the DOM is updated but before the browser paints [1][2]. - Passive: Schedules useEffect hooks to run asynchronously after the browser paints [1][2]. 4. flushPassiveEffects: This function is responsible for executing pending passive effects (useEffect) [10][1]. In React 18, after the commit phase is complete and the browser has painted, flushPassiveEffects is invoked to process these deferred effects, ensuring that side effects do not block the critical rendering path [1]. React 18's prioritization mechanism ensures that critical updates (e.g., user input) can interrupt ongoing lower-priority background work by constantly comparing the highest-priority lane in the root against the currently executing task [5][4].
Citations:
- 1: https://deepwiki.com/react/react/2.4-commit-phase-and-host-effects
- 2: https://deepwiki.com/react/react/2.1-fiber-data-model-and-work-loop
- 3: https://jser.dev/react/2022/03/26/lanes-in-react/
- 4: https://www.mo4tech.com/react-source-code-3-no-longer-mysterious-priority-mechanism.html
- 5: https://j1032w.github.io/blog/concurrent-rendering-and-update-priority-in-react18
- 6: https://www.besthub.dev/articles/how-react-s-fiber-scheduler-breaks-down-tasks-for-smooth-rendering-1a5a0aa93e7d
- 7: https://jser.dev/2023-06-19-how-does-usestate-work/
- 8: react/react@ed6c091
- 9: How React calculates and updates UI reactwg/react-18#55
- 10: react/react@e89d74ee6
🌐 Web query:
React useLayoutEffect state update pending passive effects run before paint React 18 source
💡 Result:
In React 18, useLayoutEffect is guaranteed to run synchronously after DOM mutations but before the browser performs its paint [1][2]. Because it is synchronous and blocks the browser from painting, state updates scheduled within a useLayoutEffect are also processed before the paint [1]. Regarding the behavior of passive effects (useEffect) when a useLayoutEffect triggers a state update: If a useLayoutEffect triggers a re-render (a cascading update), React will synchronously flush the remaining pending effects, including passive effects, before the browser paints [3]. This ensures that the UI remains consistent by preventing the user from seeing an intermediate, uncorrected state [3]. It is important to clarify that while useEffect is typically deferred until after the browser has painted, this is not a strict guarantee [4][5]. React may occasionally run passive effects before paint if there is remaining time in the frame or if necessary to maintain consistency (e.g., when triggered by discrete user inputs like clicks) [6]. However, unlike useLayoutEffect, the primary intent and default behavior of useEffect is to be non-blocking [1][2]. In summary, useLayoutEffect and any state updates resulting from it effectively block the painting process [1], and they can force pending passive effects to flush synchronously before the screen is updated to ensure visual coherence [3].
Citations:
- 1: https://react.dev/reference/react/useLayoutEffect
- 2: https://borstch.com/blog/development/when-to-use-uselayouteffect-over-useeffect-in-react-18
- 3: why setState in useLayoutEffect forces useEffects to run before paint? react/react#17334
- 4: Add a note that useEffect is not guaranteed to run after paint reactjs/react.dev#4107
- 5: https://jser.dev/2023-08-09-effects-run-paint/
- 6: New in 18: useEffect fires synchronously when it's the result of a discrete input reactwg/react-18#128
🌐 Web query:
React 18 source processConcurrentRoots flushPassiveEffects after paint layout effects flush pending effects before paint
💡 Result:
In React 18, the timing of passive effects (those scheduled via useEffect) and their relationship to the browser paint cycle is managed through the Fiber reconciler's commit phase [1][2]. Passive Effects and Paint By default, the function passed to useEffect is scheduled to fire asynchronously after the browser has completed the paint process [3][2][4]. This ensures that the main thread remains unblocked for critical layout and painting tasks, providing a more fluid user experience [3][5]. React accomplishes this by scheduling the flushPassiveEffects callback with the Scheduler (typically at NormalPriority), which executes once the browser is free to process it after rendering is complete [1][2][4]. Exceptions: Synchronous Execution React 18 introduced specific scenarios where useEffect callbacks are executed synchronously before paint [3][5]: 1. Discrete User Input: If an effect is the direct result of a discrete user interaction (e.g., a click or press), React will synchronously flush these effects before the next frame is painted [3][5]. This ensures the state is consistent and observable by the event system before subsequent interactions are processed [5]. 2. flushSync: When an update is wrapped in flushSync, any resulting effects are also flushed synchronously before the browser repaints [3]. Commit Lifecycle Integration In the React Fiber implementation, the commit phase is subdivided into several distinct stages [1][2]: 1. Before Mutation Phase: Executes commitBeforeMutationEffects [1][2]. 2. Mutation Phase: Performs DOM mutations via commitMutationEffects [1][2]. 3. Layout Phase: Executes commitLayoutEffects (including useLayoutEffect and ref updates) synchronously after mutations but before the browser paints [3][2]. 4. Passive Effects Scheduling: After layout effects are processed, React schedules flushPassiveEffects [1][2]. Regarding processConcurrentRoots and flushPassiveEffects, recent internal implementation changes (such as those observed in React 18+ codebase) have emphasized flushing pending passive effects before initiating new work [6]. This ensures that if a previous render's effects scheduled additional work, that work is correctly accounted for and prioritized before the reconciler decides how to proceed with the next concurrent task [6]. React guarantees that all passive effects from a previous render are flushed before beginning a new update, maintaining consistency across the component tree [3].
Citations:
- 1: https://github.com/changcheng1/react18-source-implementation
- 2: https://deepwiki.com/facebook/react/2.1-fiber-work-loop-and-scheduling
- 3: https://legacy.reactjs.org/docs/hooks-reference.html
- 4: https://jser.dev/2023-08-09-effects-run-paint/
- 5: New in 18: useEffect fires synchronously when it's the result of a discrete input reactwg/react-18#128
- 6: react/react@3c7667a
🌐 Web query:
React GitHub React 18 commit afterMutationEffectsOnFiber flushPassiveEffects before paint scheduling flushPendingPassiveEffects
💡 Result:
In React's fiber architecture, the scheduling and execution of passive effects (those associated with useEffect) are tightly managed within the commit phase to ensure performance and consistency [1][2]. Commit Phase Integration The commit phase is subdivided into specific stages to handle different types of effects. Passive effects are specifically handled after the mutation phase (where DOM changes occur) and the layout phase (where useLayoutEffect runs) [3][2]. Specifically, React identifies roots that have pending passive effects during the commit cycle [1]. If passive effects are present, React schedules them to be flushed asynchronously using the Scheduler, typically after the browser has had the opportunity to paint [1][2]. Scheduling and Execution Timing - Default Behavior: Under normal circumstances, useEffect callbacks are scheduled as a separate task to run after the browser has finished painting [1][2]. This prevents blocking the main thread and allows the browser to present updates to the user immediately [4]. - React 18+ Synchronous Execution: Starting in React 18, React may synchronously flush passive effects before paint if the update triggering the effect is a discrete user input (e.g., a click event) or is wrapped in flushSync [5][4][6]. This ensures that state changes are processed and observed by the event system before subsequent discrete events are handled, maintaining predictable behavior for event-dependent logic [4][7]. Technical Mechanism The function responsible for orchestrating this is flushPassiveEffects (sometimes referenced as flushPendingEffects in newer versions) [8][9][1]. When React determines passive effects are required, it uses the Scheduler to enqueue this function [10][1]. Recent architectural changes have sought to consolidate how this work is scheduled, often treating passive effects as continuations within the regular work-loop flow to avoid premature or deferred execution issues [8][10]. In summary, while passive effects are designed to be deferred until after paint to prioritize user interaction, React maintains the ability to flush them synchronously when consistency demands it, particularly in response to discrete user-triggered updates [1][5][4].
Citations:
- 1: https://jser.dev/2023-08-09-effects-run-paint/
- 2: https://dev.to/samabaasi/how-react-works-part-5-the-react-lifecycle-from-the-inside-when-things-actually-run-oj4
- 3: https://jser.dev/2023-07-02-use-insertion-effect/
- 4: New in 18: useEffect fires synchronously when it's the result of a discrete input reactwg/react-18#128
- 5: Issue:
useEffectexecuted before browser can paint DOM update react/react#20863 - 6: https://legacy.reactjs.org/blog/2022/03/29/react-v18.html
- 7: https://github.com/react/react/blob/8acc812c673023ad9b3d5940e94584254a46af2d/CHANGELOG.md
- 8: [Fiber] Don't work on scheduled tasks while we're in an async commit but flush it eagerly if we're sync react/react#31987
- 9: react/react@7df32c4c8
- 10: react/react@facec3e
🌐 Web query:
React repo: "flushPendingEffects" "beforePaint" "layout"
💡 Result:
In the React repository, flushPendingEffects is a central internal function used during the commit phase to synchronously flush various pending work—including mutation effects, layout effects, and passive effects—to ensure the UI remains consistent before the browser proceeds to paint [1][2]. The function plays a critical role in React's commit pipeline by forcing the completion of pending phases. Its behavior ensures that the UI state is fully updated, preventing visual inconsistencies (such as "flickering") that could occur if React allowed the browser to paint before all related layout or state-dependent updates were processed [3][4]. Key aspects of its functionality include: 1. Synchronization: It serves as a unified helper that replaces several individual flush calls (such as flushPassiveEffects), ensuring that if React is in a synchronous commit or requires consistent state, all remaining work is flushed eagerly [1]. 2. Blocking Paint: Because it includes flushLayoutEffects, any state updates or measurements triggered within useLayoutEffect are handled by the reconciler before the browser repaints the screen [5][4]. If a chain of updates occurs (e.g., a state update inside useLayoutEffect), React continues to process these synchronously, effectively blocking the paint until the layout is stable [3][6]. 3. Interaction with Passive Effects: While useEffect is typically deferred until after the browser paints, flushPendingEffects can cause passive effects to be flushed earlier (e.g., synchronously at the end of a task) if a high-priority or synchronous update necessitates it [7]. This ensures that external systems observing the React DOM tree see a consistent state [7]. In essence, flushPendingEffects acts as a "safety gate" in the commit loop, guaranteeing that regardless of the scheduling, the browser does not paint until the necessary synchronous phases—specifically those that determine final layout—are completed [1][2].
Citations:
- 1: [Fiber] Don't work on scheduled tasks while we're in an async commit but flush it eagerly if we're sync react/react#31987
- 2: https://zalt.me/blog/2026/01/react-chaos-commit
- 3: why setState in useLayoutEffect forces useEffects to run before paint? react/react#17334
- 4: https://generalistprogrammer.com/tutorials/react-uselayouteffect-complete-guide
- 5: https://react.dev/reference/react/useLayoutEffect
- 6: https://www.developerway.com/articles/no-more-flickering-ui
- 7: Flush discrete passive effects before paint react/react#21150
🌐 Web query:
React repo: "hasScheduledToggle" "flushPendingEffects" "updateTransition"
💡 Result:
In the React repository, the terms hasScheduledToggle, flushPendingEffects, and updateTransition refer to mechanisms within React's Fiber reconciler related to scheduling, commit orchestration, and transition management [1][2]. flushPendingEffects This is a critical function in the React Fiber reconciler (typically found in ReactFiberRootScheduler.js) that synchronously flushes any remaining phases of the commit process that have not yet been completed [1][2][3]. It is called at the beginning of performSyncWorkOnRoot to ensure that all pending passive effects and other commit-related tasks are processed before any new render work begins [1][2][3]. As of recent updates (circa 2025-2026), flushPendingEffects also plays a role in orchestrating View Transitions by aborting in-progress transitions if a synchronous update is forced [1][4][2]. updateTransition This generally relates to React's transition system, which distinguishes between "urgent" (e.g., user input) and "transition" (e.g., UI updates that can be deferred) work. While useTransition is the public-facing hook, the underlying implementation tracks transition state to manage priorities and batching [5][6]. Transitions are integrated into the commit pipeline, and logic surrounding them often involves ensuring that updates are grouped correctly, especially when async actions are involved [7][5]. hasScheduledToggle This appears to be an internal flag or helper function used within specific experimental features or sub-systems, such as those related to View Transitions or internal gesture-based UI components [1]. It is often used to track whether a specific interruptible task or animation state has been initiated, allowing React to decide whether it should defer certain work or flush it eagerly to maintain consistency [1][4]. Collectively, these functions and flags represent the complexity of React’s modern commit pipeline, where React must balance synchronous requirements (like flushSync) with the asynchronous nature of features like View Transitions and concurrent rendering [1][4][2]. [1][4][2][3]
Citations:
- 1: [Fiber] Don't work on scheduled tasks while we're in an async commit but flush it eagerly if we're sync react/react#31987
- 2: https://zalt.me/blog/2026/01/react-chaos-commit
- 3: Flush subtree effects preactjs/preact#5055
- 4: Don't flush synchronous work if we're in the middle of a ViewTransition async sequence react/react#32760
- 5: Batch async actions even if useTransition is unmounted react/react#28078
- 6: useTransition: After startTransition, it does not react to passed props changes react/react#17332
- 7: [Fiber] Always flush Default priority in the microtask if a Transition was scheduled react/react#33186
🌐 Web query:
React 18 "flushPassiveEffectsAfterPaint" "schedulePassiveEffects" commit "flushPendingEffects"
💡 Result:
In React 18, the functions schedulePassiveEffects, flushPassiveEffectsAfterPaint (often appearing as flushPassiveEffectsImpl or similar implementations), and flushPendingEffects are internal mechanisms used by the React Fiber reconciler to manage the lifecycle of passive effects (useEffect) in relation to the browser's render pipeline [1][2][3]. Key mechanisms: 1. schedulePassiveEffects: This function is invoked during the commit phase—specifically within commitLayoutEffects [2]. Its primary role is to traverse the fiber tree, identify components with passive effects that need to be run (or cleaned up), and enqueue them into pending queues (pendingPassiveHookEffectsUnmount and pendingPassiveHookEffectsMount) [4][2]. 2. flushPassiveEffects (and related flushers): React manages passive effects by deferring them until after the browser has painted [5][6]. The scheduler tracks these pending effects. While the default behavior is to run them asynchronously after the paint to avoid blocking the main thread, React may flush them synchronously in specific scenarios, such as when the update is the result of a discrete user input (like a click) or wrapped in flushSync [5][7][8]. This ensures the UI remains consistent for subsequent discrete events [5][7]. 3. flushPendingEffects: This is a broader utility function used to ensure consistent state during the commit process [1][9]. It orchestrates the flushing of various effect phases (mutation, layout, and passive) [9]. In recent iterations of React, flushPassiveEffects has been consolidated or renamed in some contexts to flushPendingEffects to provide a unified way to synchronously flush remaining work, ensuring that the system state is predictable, particularly when managing complex features like View Transitions [1][9]. The interaction between these functions is designed to maintain the "concurrent" nature of React 18: rendering is interruptible, but the final commit phase must resolve all pending effects to ensure the DOM and component state are synchronized before the browser performs a paint or handles further input [10][11]. When React determines it must be synchronous (due to priority or event type), these functions effectively bypass the asynchronous deferral and execute the effect queues immediately [5][7][6].
Citations:
- 1: [Fiber] Don't work on scheduled tasks while we're in an async commit but flush it eagerly if we're sync react/react#31987
- 2: https://iwsr-657.xlog.app/React-Hooks-useEffect-yu-useLayoutEffect
- 3: https://www.mo4tech.com/react-source-code-commit-phase-details.html
- 4: Flush all passive destroy fns before calling create fns react/react#17947
- 5: New in 18: useEffect fires synchronously when it's the result of a discrete input reactwg/react-18#128
- 6: https://jser.dev/2023-08-09-effects-run-paint/
- 7: Flush discrete passive effects before paint react/react#21150
- 8: Bug: useEffect fires synchronously even though it is not the result of a discrete input react/react#31567
- 9: https://zalt.me/blog/2026/01/react-chaos-commit
- 10: Questions about specifics of Concurrent scheduling reactwg/react-18#27
- 11: https://legacy.reactjs.org/blog/2022/03/29/react-v18.html
Do not reset the flip while the tooltip is open.
When an open tooltip switches to an overflowing side, measureAndMaybeFlip() sets flipped to true, then the side effect sets it back to false. This can leave data-side on the overflowing requested side until a later resize/scroll re-measure. Keep the side-reset closed-only, or move the reset into the layout measurement before scheduling flipped.
📍 Affects 2 files
app/shared/src/ui/Tooltip.tsx#L127-L139(this comment)app/shared/src/ui/Tooltip.test.tsx#L299-L317
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/shared/src/ui/Tooltip.tsx` around lines 127 - 139, The side-change reset
in Tooltip.tsx must not clear flipped while the tooltip is open. Update the
side-dependent effect around measureAndMaybeFlip to reset only when closed, or
perform the reset within the layout measurement before applying the measured
flip; preserve immediate correction when an open tooltip changes to an
overflowing side. Update the related expectations in
app/shared/src/ui/Tooltip.test.tsx lines 299-317 to match the corrected
open-tooltip behavior.
…1505 #1506 #1507 #1509) (#1616) * fix(diff): 校准 word-diff 阈值至保守值 (#1505) MAX_LINE_CHARS 2000→800、MAX_LINE_WORDS 500→200,避免大 diff 行触发 O(n*m) LCS 卡顿 UI;补边界值测试并移除 TODO。 * feat(markdown): 标题锚点 slug 与 hover 链接 (#1506) 引入 rehype-slug 为 h1-h6 生成 slug id(保留中文、重复加 -1/-2),标题 hover 显示链接图标可跳转锚点。 * fix(tooltip): 视口溢出时翻转 tooltip 定位 (#1507) 打开时测量并在 scroll/resize 时重测,超出视口右/下边界翻转到对侧 data-side;补翻转与 reduced-motion 测试,移除 TODO。 * fix(styles): HC 玻璃面板降级为不透明背景 (#1509) forced-colors 下玻璃背景 token 映射 Canvas、prefers-contrast: more 下映射实体表面并关闭 blur,保证文字对比度;移除 TODO(a11y #20)。 --------- Co-authored-by: Codex <codex@vectorcontrol.tech>
摘要
一个 PR 收敛 4 个独立 UI 组件级 issue(worktree
.worktrees/fix-ui-components,4 个 commit)。#1505 word-diff 阈值校准
MAX_LINE_CHARS2000→800、MAX_LINE_WORDS500→200,保持 fail-safe 语义(超阈值跳过 word-diff 回退行级 diff)shouldSkipWordDiff上方补充阈值含义、降级行为与校准依据注释,移除TODO(#1505)#1506 Markdown 标题锚点链接
rehype-slug@^6.0.0(与 unified 11 / react-markdown 10 兼容),app/shared/package.json+app/pnpm-lock.yaml已提交-1/-2后缀)#1507 Tooltip 视口翻转
data-side(bottom↔top、right↔left),保持现有 API 与 DOM 结构不变(无 portal)TODO(#1507)#1509 HC 主题玻璃面板对比度
forced-colors: active:玻璃背景 token 统一重映射为不透明Canvas(文字层由系统色强制,对比 21:1),仅阴影等装饰保留forced-color-adjust: noneprefers-contrast: more块(Firefox):玻璃降级为实体表面 token、关闭 backdrop blur、玻璃文字映射常规文字 token,零硬编码色值TODO(a11y #20 → #1509),注释说明设计决策;default 主题玻璃效果不变验证
pnpm --dir app/shared exec vitest run src/ui/diffWordTokens.test.ts src/ui/Markdown.test.tsx src/ui/Tooltip.test.tsx:68 passed / 68pnpm --dir app/shared exec tsc --noEmit:改动文件零错误(既有 53 个错误与 master 一致,非本 PR 引入)git diff --check:无空白错误pnpm --dir app add rehype-slug安装成功,lockfile 已提交改动文件
app/shared/src/ui/diffWordTokens.ts+diffWordTokens.test.tsapp/shared/src/ui/Markdown.tsx+Markdown.module.css+Markdown.test.tsxapp/shared/src/ui/Tooltip.tsx+Tooltip.test.tsxapp/shared/src/styles/tokens-base.cssapp/shared/package.json+app/pnpm-lock.yaml(rehype-slug)Summary by CodeRabbit