Skip to content

fix(web-shell): isolate component styles from host CSS - #7466

Merged
ytahdn merged 1 commit into
QwenLM:mainfrom
chiga0:codex/web-shell-host-style-isolation
Jul 22, 2026
Merged

fix(web-shell): isolate component styles from host CSS#7466
ytahdn merged 1 commit into
QwenLM:mainfrom
chiga0:codex/web-shell-host-style-isolation

Conversation

@ytahdn

@ytahdn ytahdn commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This change gives Web Shell-owned UI a scoped baseline with enough specificity to beat ordinary host-page element and universal selectors, while keeping existing CSS Module scoping low-specificity for consumer customization. It also keeps Tailwind utilities out of cascade layers that would otherwise lose to unlayered host CSS and routes the scheduled-task reference picker through the shared Web Shell portal root.

Why it's needed

When Web Shell is embedded as a component, host rules such as * { padding: 0 }, h2 { ... }, and button { ... } can override Tailwind/shadcn styles even when the component CSS is loaded correctly. This makes Web Shell-owned controls and portal content visually inconsistent with the standalone application.

Reviewer Test Plan

How to verify

  • Embed the production Web Shell library in a host page, then load ordinary unlayered *, h2, and button rules after the component stylesheet. Confirm representative buttons and headings retain the same padding, margin, box sizing, typography, colors, background, and border radius in both the main root and dialog portal root.
  • Open the scheduled-task editor and its skill reference picker. Confirm the picker remains visually styled and is mounted beneath the marked Web Shell portal root.
  • Confirm host !important rules and higher-specificity class/id rules remain outside the isolation guarantee.

Evidence (Before & After)

Before: ordinary host universal and element selectors changed computed styles for Web Shell buttons and headings because layered Tailwind rules lost to unlayered host CSS.

After: adding the same hostile host rules after the production component CSS produced no computed-style differences for representative root and portal buttons/headings across padding, margin, box sizing, typography, colors, background, border radius, line height, and height.

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

Production library build in Chromium, plus focused Vitest coverage for the generated CSS artifact and scheduled-task portal placement.

Risk & Scope

  • Main risk or tradeoff: unlayering Tailwind utilities changes their cascade relationship with host CSS, so the generated artifact locks in reset, utility, and CSS Module ordering.
  • Not validated / out of scope: host !important declarations, higher-specificity class/id selectors, and arbitrary properties Web Shell does not declare.
  • Breaking changes / migration notes: none; component consumers do not need to change props, render callbacks, Tailwind configuration, or stylesheet imports.

Linked Issues

N/A

中文说明

本 PR 做了什么

本次改动为 Web Shell 自有 UI 增加了带作用域且权重足以压过宿主普通元素选择器和通配选择器的基础样式,同时保持现有 CSS Modules 的低权重作用域,避免影响接入方定制。它还让 Tailwind utilities 不再处于会输给宿主未分层 CSS 的 cascade layer 中,并将定时任务引用选择器统一挂载到 Web Shell 的共享 portal root。

为什么需要

当 Web Shell 作为组件嵌入其他页面时,宿主的 * { padding: 0 }h2 { ... }button { ... } 等规则可能覆盖 Tailwind/shadcn 样式,即使组件 CSS 已正确加载。结果是 Web Shell 自有控件和 portal 内容的视觉表现与独立应用不一致。

Reviewer Test Plan

如何验证

  • 在宿主页面中引入生产版 Web Shell 组件,并在组件样式之后加载普通未分层的 *h2button 规则。确认主 root 和 dialog portal root 中的代表性按钮、标题在 padding、margin、box sizing、字体、颜色、背景和圆角方面保持不变。
  • 打开定时任务编辑器及其技能引用选择器。确认选择器样式正常,并挂载在带标记的 Web Shell portal root 下。
  • 确认宿主 !important 规则和更高权重的 class/id 规则仍不在本次隔离保证范围内。

证据(修改前后)

修改前:由于分层的 Tailwind 规则会输给宿主未分层 CSS,普通宿主通配选择器和元素选择器会改变 Web Shell 按钮和标题的计算样式。

修改后:在生产组件 CSS 之后添加同样的宿主冲突规则,主 root 和 portal 中代表性按钮、标题的计算样式均无差异;核对属性包括 padding、margin、box sizing、字体、颜色、背景、圆角、行高和高度。

测试平台

OS 状态
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

环境(可选)

在 Chromium 中验证生产 library build,并使用定向 Vitest 覆盖生成 CSS 产物和定时任务 portal 挂载位置。

风险与范围

  • 主要风险或取舍:取消 Tailwind utilities 的 layer 会改变其与宿主 CSS 的级联关系,因此生成产物测试固定了 reset、utilities 和 CSS Modules 的顺序。
  • 未验证/不在范围内:宿主 !important 声明、更高权重的 class/id 选择器,以及 Web Shell 自身未声明的任意属性。
  • 破坏性变更/迁移说明:无;组件接入方无需修改 props、render 回调、Tailwind 配置或样式引入方式。

关联 Issue

N/A

@ytahdn

ytahdn commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Web Shell host style isolation

Scope

Verify that ordinary host-page selectors do not override Web Shell-owned UI in
the application root or portal root.

Host rules

* {
  padding: 0;
  box-sizing: content-box;
}

h2 {
  margin: 40px;
  color: rgb(1, 2, 3);
  font: 40px serif;
}

button {
  padding: 0;
  border-radius: 0;
  background: red;
  font: 30px serif;
}

Expected behavior

  • Web Shell root buttons and headings retain their component styles.
  • Dialog buttons and headings rendered through the portal root retain their
    component styles.
  • The scheduled-task reference picker is a descendant of the marked Web Shell
    portal root.

Result

Passed against the production library artifact in Chromium. Computed styles
for representative root and portal buttons/headings were unchanged before and
after adding the hostile host rules. The live scheduled-task reference picker
was also confirmed inside the Web Shell portal root.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: This is an observed bug with clear evidence. CSS cascade layers lose to unlayered host styles per spec, so when Web Shell is embedded in a host page, ordinary * { padding: 0 } or h2 { ... } rules override the component's layered Tailwind/shadcn styles. The PR describes before/after computed-style verification in Chromium.

Direction: Aligned. CSS isolation is fundamental to Web Shell's purpose as an embeddable component — if host styles can freely override component internals, the component is unreliable by design. No CHANGELOG reference needed here; this is a correctness fix for the component boundary.

Size: Not applicable — no core paths touched. 8 files, 289 additions / 161 deletions (~317 production lines, ~133 test lines).

Approach: The scope feels right. The core insight — unlayer the Tailwind imports and switch :where():is() on the root scope to get attribute-selector specificity — is the standard fix for this cascade problem. The new preflight.css extracts the reset from @layer base into unlayered scoped CSS, and the portal-root fix for the scheduled-task picker ensures portal content also gets the scoped styles. Every change in the diff serves the stated goal; no drive-by refactors or unrelated edits.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题: 这是一个有明确证据的已观测 bug。根据 CSS 规范,cascade layer 中的样式会输给未分层的宿主样式,因此当 Web Shell 嵌入宿主页面时,普通的 * { padding: 0 }h2 { ... } 规则会覆盖组件内分层的 Tailwind/shadcn 样式。PR 描述了在 Chromium 中的 before/after 计算样式验证。

方向: 对齐。CSS 隔离是 Web Shell 作为可嵌入组件的基本需求——如果宿主样式可以随意覆盖组件内部样式,组件就不可靠。

规模: 不适用——未触及核心路径。8 个文件,289 行新增 / 161 行删除(约 317 行生产代码,约 133 行测试代码)。

方案: 范围合理。核心思路——取消 Tailwind 导入的 layer 并将根作用域的 :where() 切换为 :is() 以获得属性选择器权重——是解决此级联问题的标准方法。新的 preflight.css 将重置样式从 @layer base 提取到未分层的作用域 CSS 中,定时任务选择器的 portal-root 修复确保 portal 内容也能获得作用域样式。diff 中的每个改动都服务于既定目标,没有顺手重构或无关改动。

进入代码审查 🔍

Qwen Code · qwen3.7-max

Reviewed at 9ede81e62afd4e6f3ed114be78a4457e99403971 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: Given "host CSS overrides Web Shell component styles," I'd (1) remove @layer wrappers from Tailwind imports since layered styles lose to unlayered host CSS per spec, (2) switch :where():is() on the root scope so the [data-web-shell-root] attribute selector gives (0,1,0) specificity — enough to beat any element or universal selector, (3) extract the base reset from @layer base into unlayered scoped CSS, and (4) ensure portal content also gets scoped styles.

Comparison with the diff: The PR's approach matches this exactly. No simpler path missed.

Findings: No critical blockers or AGENTS.md violations.

  • The :is() / :where() split is correct: root-scope theme variables and preflight use :is() for specificity, while CSS Module scoping (COMPONENT_SCOPE in vite.lib.config.ts) stays :where() for consumer customization. This is the right tradeoff.
  • The new preflight.css adds font-size: 14px, color: var(--foreground), and heading-level resets (padding, background-color, font, etc.) that weren't in the old @layer base. These are reasonable for a self-contained component and directly serve the isolation goal.
  • CSS import order change (moving import './styles/globals.css' to the top of App.tsx and WebShellTranscript.tsx) ensures correct cascade ordering in the bundle.
  • The ScheduledTasksDialog portal-root fix (portalRoot ?? document.body) correctly routes the reference picker through the shared portal root so it inherits scoped styles.

Testing

Build artifact tests (9/9 pass)

 ✓ build-artifact.test.ts (9 tests) 236ms

 Test Files  1 passed (1)
      Tests  9 passed (9)
   Duration  651ms

The new keeps component resets and utilities above plain host selectors test verifies: preflight resets and Tailwind utilities are unlayered, ordered correctly (resets → utilities → CSS modules), and use :is() selectors on the root scope.

ScheduledTasksDialog tests (41/41 pass)

 ✓ components/dialogs/ScheduledTasksDialog.test.tsx (41 tests) 740ms

 Test Files  1 passed (1)
      Tests  41 passed (41)
   Duration  1.89s

Includes the new renders the reference picker inside the Web Shell portal root test.

Built CSS output verification

Inspected dist/index.js after vite build --config vite.lib.config.ts:

@layer base: 0 occurrences ✓
@layer theme: 0 occurrences ✓
@layer utilities: 0 occurrences ✓
@layer components: 0 occurrences ✓
:is([data-web-shell-root]): 27 occurrences ✓
box-sizing:border-box: 19 occurrences ✓ (preflight present, unlayered)
:where([data-web-shell-root][data-web-shell-shadcn]): 3516 occurrences (CSS modules, intentionally low-specificity)

CLI smoke test

$ npm run dev -- -p 'say hello' --output-format text
Hello! How can I help you today?
中文说明

代码审查

独立方案: 针对"宿主 CSS 覆盖 Web Shell 组件样式"的问题,我会 (1) 移除 Tailwind 导入的 @layer 包装(因为分层样式会输给未分层的宿主 CSS),(2) 将根作用域的 :where() 切换为 :is(),使 [data-web-shell-root] 属性选择器提供 (0,1,0) 权重——足以压过任何元素或通配选择器,(3) 将基础重置从 @layer base 提取到未分层的作用域 CSS,(4) 确保 portal 内容也获得作用域样式。

与 diff 对比: PR 的方案与此完全一致。没有遗漏更简单的路径。

发现: 无关键阻塞问题或 AGENTS.md 违规。

  • :is() / :where() 的分离是正确的:根作用域主题变量和 preflight 使用 :is() 获得权重,而 CSS Module 作用域(vite.lib.config.ts 中的 COMPONENT_SCOPE)保持 :where() 以便接入方定制。
  • 新的 preflight.css 增加了 font-size: 14pxcolor: var(--foreground) 和标题级别重置,这些在旧的 @layer base 中没有。对于自包含组件来说是合理的。
  • CSS 导入顺序变更确保 bundle 中的正确级联顺序。
  • ScheduledTasksDialog 的 portal-root 修复正确地将引用选择器路由到共享 portal root。

测试

构建产物测试 9/9 通过,ScheduledTasksDialog 测试 41/41 通过。构建 CSS 输出验证确认无 @layer 包装、:is() 选择器存在、preflight 未分层。CLI 冒烟测试正常。

Qwen Code · qwen3.7-max

Reviewed at 9ede81e62afd4e6f3ed114be78a4457e99403971 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — Clean across every stage; would merge without hesitation.

The problem is real and well-understood: CSS cascade layers lose to unlayered host styles per spec, so any host page with * { padding: 0 } or h2 { ... } breaks Web Shell's embedded UI. The fix is the standard approach — unlayer the Tailwind imports and switch :where():is() on the root scope for attribute-selector specificity. My independent proposal matched the PR's approach exactly; no simpler path was missed.

The implementation is focused and mechanical: every file serves the isolation goal, no drive-by refactors, no scope creep. The :is() / :where() split is well-reasoned — higher specificity where the component needs to defend its styles, low specificity where consumers need to customize. The new preflight.css is a clean extraction of the old @layer base reset with a few sensible additions (font-size, color, heading resets) for self-containment.

Tests are comprehensive: the build-artifact test locks in the cascade ordering (resets → utilities → CSS modules, all unlayered), and the ScheduledTasksDialog test covers the portal-root placement. The built CSS output confirms zero @layer wrappers and 27 :is() root-scope selectors.

If I had to maintain this in six months, I'd thank the author — the approach is standard, the tests pin the important invariants, and the code is straightforward.

中文说明

置信度:5/5 — 每个阶段都很干净,毫不犹豫地合并。

问题是真实且 well-understood 的:根据 CSS 规范,cascade layer 中的样式会输给未分层的宿主样式,因此任何带有 * { padding: 0 }h2 { ... } 的宿主页面都会破坏 Web Shell 的嵌入 UI。修复方案是标准方法——取消 Tailwind 导入的 layer 并将根作用域的 :where() 切换为 :is() 以获得属性选择器权重。我的独立方案与 PR 完全一致,没有遗漏更简单的路径。

实现聚焦且机械化:每个文件都服务于隔离目标,没有顺手重构,没有范围蔓延。:is() / :where() 的分离设计合理——在组件需要保护样式的地方使用高权重,在接入方需要定制的地方保持低权重。

测试全面:构建产物测试固定了级联顺序,ScheduledTasksDialog 测试覆盖了 portal-root 挂载。构建 CSS 输出确认零 @layer 包装和 27 个 :is() 根作用域选择器。

Qwen Code · qwen3.7-max

Reviewed at 9ede81e62afd4e6f3ed114be78a4457e99403971 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head 9ede81e. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

ℹ️ No screenshot changed against the PR base — but this PR edits 5 render-shaping files:

  • packages/web-shell/client/App.tsx
  • packages/web-shell/client/components/WebShellTranscript.tsx
  • packages/web-shell/client/components/dialogs/ScheduledTasksDialog.tsx
  • packages/web-shell/client/styles/globals.css
  • packages/web-shell/client/styles/preflight.css

Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds whatever state the UI is gated on; it then appears here as a head-only (NEW) capture.

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Overview (AI Generated)

PR: #7466 — fix(web-shell): isolate component styles from host CSS
Type: Bug Fix (CSS)
Change size: +289/-161 across 8 files
HEAD: 9ede81e6

Findings Summary

  • Critical/Major/Minor: 0
  • Nit: 1

Review

Well-executed CSS specificity isolation for embedded Web Shell components. Three coordinated changes:

1. Layer removal: Removed @layer theme, base, components, utilities declaration and unlayered Tailwind imports. This prevents Tailwind utilities from losing to unlayered host CSS in the cascade.

2. Selector strategy shift: Changed from :where(A, B) (zero specificity) to :is(A:where(B)) (specificity from [data-web-shell-root] attribute). The :is() pseudo-class takes the highest specificity from its arguments, so [data-web-shell-root] provides enough specificity to beat host *, h2, and button selectors, while :where([data-web-shell-shadcn]) keeps the shadcn qualifier at zero specificity for consumer customization.

3. Preflight extraction: Moved CSS reset from @layer base into unlayered preflight.css. Reset rules now use the same :is(...:where(...)) selector strategy, ensuring they beat unlayered host resets through specificity rather than cascade layers.

Verified correct:

  • Build artifact test locks in reset → utilities → CSS Modules ordering
  • No @layer declarations in generated CSS (test asserts empty conflictingLayers)
  • CSS Modules remain low-specificity (:where(...)) for consumer customization
  • Import order moved to top of App.tsx and WebShellTranscript.tsx — globals loaded before components
  • Scheduled task reference picker now renders inside WebShellPortalRootContext — inherits portal styling

Nit

The preflight.css selectors are highly repetitive (each rule block repeats the same 4-line :is(...) selector prefix). A PostCSS plugin or build-time transformation could deduplicate this, but the explicit repetition is acceptable for a generated artifact that needs to be self-contained.

Final Verdict

LGTM. Clean CSS isolation strategy that correctly solves the embedded-component specificity problem without breaking consumer customization. The :is():where() pattern is the right choice for this use case.


This review was generated by QoderWork AI

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Approve to Comment: CI still running. Reviewed.

— qwen3.7-max via Qwen Code /review

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

整体 LGTM,样式隔离方案清晰。几个小建议:

  1. preflight.cssfont-size: 14px 硬编码会覆盖用户浏览器字体缩放偏好,建议改为 0.875rem 或 CSS 变量
  2. COMPONENT_SCOPECOMPONENT_ROOT_SCOPE 写法不一致(:where vs :is),建议加注释说明设计意图
  3. ScheduledTasksDialog.tsxportalRoot ?? document.body 回退时样式会裸奔,建议 dev 模式加 console.warn
  4. tw-animate-css / shadcn/tailwind.css 如果上游更新引入 @layer,运行时行为会静默回退,建议关注

以上均为非阻塞建议,不影响合入。

@ytahdn
ytahdn added this pull request to the merge queue Jul 22, 2026
Merged via the queue into QwenLM:main with commit 1e921ce Jul 22, 2026
80 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants