Browse Source

feat: fmode-code-lit 规则仓 + 全局技能(Lit 原生 ESM 无构建)

规则仓(.claude/rules):
- architecture / project-structure / components / services / context-di
- routing / tabs-keepalive / http-utils / deployment / pitfalls
- modules/:功能模块·通用能力·业务模块 划分最佳实践 + 场景化示例

无构建硬约束:
- 禁用装饰器(浏览器原生不执行),改用 static properties + customElements.define
  + ContextConsumer;规则与示例全部贯彻
- @lit-labs/context 已废弃 → 统一用 @lit/context
- @lit-labs/signals 需 importmap 映射 signal-polyfill(入口 dist/index.js)

示例(examples/hello-lit,零构建可直接上 OBS+CDN):
- index.html(<base>+importmap) / app.js(provide+根组件)
- 组件·服务·工具分层;多标签 KeepAlive;History 路由;smoke.html 自检

全局技能(skills/fmode-code-lit):
- SKILL.md 触发词:lit 项目/组件/无构建前端
- scaffold / new(component|service|page) / install-rules / install / smoke CLI
- 生成物经 node --check + 无装饰器扫描自检通过

复盘(docs/retrospective.md):纯原生 ESM × Lit 3.x 对标 Angular zoneless

部署遵循 FMODE 实际:Repose 依赖托管 + OBS+CDN(fmode.cn) History 回退 200。
AK/SK 不入库,仅引用既有部署脚本。
liuyuyang 2 ngày trước cách đây
commit
19f7afb52e
52 tập tin đã thay đổi với 3747 bổ sung và 0 xóa
  1. 68 0
      .claude/_lit-dev-manual.md
  2. 66 0
      .claude/rules/architecture.md
  3. 132 0
      .claude/rules/components.md
  4. 102 0
      .claude/rules/context-di.md
  5. 139 0
      .claude/rules/deployment.md
  6. 113 0
      .claude/rules/http-utils.md
  7. 118 0
      .claude/rules/modules/README.md
  8. 98 0
      .claude/rules/modules/examples.md
  9. 89 0
      .claude/rules/pitfalls.md
  10. 98 0
      .claude/rules/project-structure.md
  11. 124 0
      .claude/rules/routing.md
  12. 108 0
      .claude/rules/services.md
  13. 136 0
      .claude/rules/tabs-keepalive.md
  14. 17 0
      .gitignore
  15. 84 0
      CLAUDE.md
  16. 375 0
      LICENSE
  17. 71 0
      README.md
  18. 85 0
      docs/retrospective.md
  19. 70 0
      examples/hello-lit/README.md
  20. 27 0
      examples/hello-lit/deploy.sh
  21. 17 0
      examples/hello-lit/package.json
  22. 26 0
      examples/hello-lit/src/app.js
  23. 73 0
      examples/hello-lit/src/components/app-shell.js
  24. 36 0
      examples/hello-lit/src/components/page-view.js
  25. 24 0
      examples/hello-lit/src/components/side-menu.js
  26. 33 0
      examples/hello-lit/src/components/tabs-bar.js
  27. 30 0
      examples/hello-lit/src/components/tabs-container.js
  28. 18 0
      examples/hello-lit/src/importmap.json
  29. 30 0
      examples/hello-lit/src/index.html
  30. 30 0
      examples/hello-lit/src/modules/auth/auth-service.js
  31. 46 0
      examples/hello-lit/src/modules/home/page-home.js
  32. 22 0
      examples/hello-lit/src/modules/page-404.js
  33. 35 0
      examples/hello-lit/src/modules/user/page-user-detail.js
  34. 53 0
      examples/hello-lit/src/modules/user/page-user-list.js
  35. 30 0
      examples/hello-lit/src/modules/user/user-service.js
  36. 7 0
      examples/hello-lit/src/services/contexts.js
  37. 34 0
      examples/hello-lit/src/services/tabs-service.js
  38. 58 0
      examples/hello-lit/src/smoke.html
  39. 16 0
      examples/hello-lit/src/utils/format.js
  40. 52 0
      examples/hello-lit/src/utils/http.js
  41. 15 0
      examples/hello-lit/src/utils/router.js
  42. 13 0
      package.json
  43. 109 0
      skills/fmode-code-lit/README.md
  44. 151 0
      skills/fmode-code-lit/SKILL.md
  45. 38 0
      skills/fmode-code-lit/bin/fmode-code-lit.js
  46. 37 0
      skills/fmode-code-lit/package.json
  47. 72 0
      skills/fmode-code-lit/scripts/install-rules.mjs
  48. 57 0
      skills/fmode-code-lit/scripts/install.mjs
  49. 129 0
      skills/fmode-code-lit/scripts/new.mjs
  50. 259 0
      skills/fmode-code-lit/scripts/scaffold.mjs
  51. 60 0
      skills/fmode-code-lit/scripts/smoke.mjs
  52. 17 0
      skills/fmode-code-lit/skill-package-manifest.json

+ 68 - 0
.claude/_lit-dev-manual.md

@@ -0,0 +1,68 @@
+# Lit 原生 ESM 无构建开发最佳实践(源稿)
+
+> 用户提供规范文档全文。用于构建 fmode-code-lit 规则仓库。目标:零编译、零打包、纯原生 JS,基于浏览器原生 ESM + Import Map,复用 npm 原始包托管在内部 FMODE Repose,业务代码直接上传 OBS+CDN,保存刷新即预览。对标新版 Angular(History 干净路由、DI 依赖注入、Signal 细粒度响应式、组件/服务分层、后台多标签,无 Zone.js)。技术底座:原生浏览器 API(Fetch/History/localStorage)+ Lit 官方原生包。
+
+## 一、整体架构
+- **无构建无打包**:不用 rollup/esbuild/vite/webpack,源码浏览器直接执行
+- **依赖统一托管**:lit、@lit-labs/* 从 npm 下原始 ESM 原样同步到内部 FMODE Repose,全业务共享
+- **业务资源独立托管**:业务源码(index.html、js 组件)部署 OBS+CDN
+- **分层(对标 Angular)**:WebComponent(LitElement) 只渲染视图/DOM事件,不写复杂业务逻辑;Service(普通 ESM Class) 存全局状态/接口/业务计算,与 DOM 解耦;共享工具纯 ESM 函数无状态
+- **响应式**:@lit-labs/signals(细粒度信号)
+- **DI**:@lit-labs/context(组件树注入)
+- **路由**:@lit-labs/router History 干净URL(支持微信 OAuth 回调,需 CDN 回退规则)
+
+### 部署拓扑
+- FMODE Repose:/npm/lit/v3.x.x/、/npm/@lit-labs/context|signals|router/vx/、/npm/importmap/lit-v3.json(公共 importmap)
+- OBS+CDN:index.html(入口引入 importmap)、app.js(入口)、/routes/、/components/、/services/、/utils/
+
+### 运行流程
+浏览器打开 → CDN 回退规则返回 /admin/index.html(200非302) → index.html 加载公共 importmap → 加载 app.js → import lit 依赖 → 初始化路由/Store/根组件渲染
+
+## 二、前置基础设施(硬性)
+1. FMODE Repose:.js 必须 Content-Type: text/javascript;CORS 允许业务域名;开启 HTTP/2/3;依赖按版本目录化
+2. OBS+CDN History 路由:规则1 匹配真实静态资源直接返回;规则2 不存在路径返回 index.html(200,禁 302/301);index.html head 加 base 标签(子目录部署写 <base href="/admin/">)
+3. ImportMap:公共托管 FMODE Repose 全业务复用;business 的 index.html 用 <script type="importmap" src="公共importmap">
+
+## 三、项目目录规范(无打包,src 直接上传 OBS 原样保留)
+```
+src/
+  index.html (importmap+base)
+  app.js (路由/全局DI/根组件)
+  components/ (side-menu/user-card/tabs-container)
+  routes/ (page-user-list/detail/404)
+  services/ (auth/table/router/service.js)
+  utils/ (http/storage/format.js)
+```
+分层约束:组件只渲染+事件+读 Signal+调 Service,禁大量业务逻辑/直接 fetch/硬编码接口;Service 纯JS 存 Signal+接口+校验+转换,不依赖 DOM/LitElement 可单测,禁导入 html 创建 DOM;utils 纯无状态函数。
+
+## 四、核心模块规范(示例)
+- Service:signal/computed/effect + 全局单例 export const authService = new AuthService()
+- Context DI:createContext 令牌 + provide/consume
+- 页面组件:SignalWatcher(LitElement) 混入自动响应 signal + consume(context)
+- 根应用:provide 注入全局 service + Router 挂载 + 多标签
+
+## 五、后台多标签+KeepAlive
+同一 index.html 内前端路由切换不刷新;KeepAlive 用 display:none 隐藏非激活页容器,组件实例常驻,表单/内部状态不丢失。根组件维护 tabs 数组渲染所有已开标签页,由路由判断激活控制 display。
+
+## 六、Lit 核心特性利用
+html 模板(自动转义反 XSS+缓存);css 标签(Shadow DOM 样式隔离);ShadowDOM(DOM/CSS 隔离);@property(组件本地 UI 状态);⚠️ 区分:本地 UI 用 Lit property,跨组件全局业务状态用 @lit-labs/signals;requestUpdate/updateComplete(备选);Slot 插槽(封装通用布局)。
+
+## 七、状态管理选型
+| 场景 | 方案 |
+|---|---|
+| 全局业务状态多页共享(登录/字典) | Signal Service + Context 注入(首选) |
+| 组件本地状态(弹窗/输入) | Lit @property |
+| 简单应用不想引 signals | EventTarget 简易 Store + Context |
+| 一次性持久化配置 | localStorage 封装 |
+
+## 八、开发工作流(极简)
+写 JS 组件/Service/页面→同步上传 OBS+CDN→浏览器刷新即见→依赖升级:npm 更新 lit 同步 node_modules 到 FMODE Repose + 改 importmap 版本号。IDE 提示:本地 npm install lit @lit-labs/* 仅用于 TS/JS 类型提示,不打进产物。
+
+## 九、风险与坑
+1. ESM 大量小文件→Repose 开 HTTP/2;2. CORS 必配允许业务域名否则模块加载失败;3. ImportMap 现代浏览器原生,老浏览器需 polyfill;4. 勿混用多版本 lit(自定义元素冲突);5. History 路由勿 302,必须 CDN 200 地址栏不变;6. 子目录部署必须 base href 否则相对 JS 404;7. Signal 和 Lit property 两套响应式分开,全局用 Signal,组件内 UI 用 property。
+
+## 十、优势
+零构建保存刷新即用/依赖统一托管一次上传全复用/原生 ESM 无黑盒/对标 Angular zoneless+Signal+DI 无 Zone 无庞大框架/History 干净 URL 支持微信 OAuth/ShadowDOM 样式隔离组件跨项目复用/分层清晰 /Fetch 原生不引重 HTTP 库。
+
+## 十一、扩展方向
+封装全局 HTTP 工具(token 拦截/错误处理);封装通用弹窗/表格/表单 WebComponent 到内部组件库托管 FMODE Repose;路由守卫鉴权;全局 loading/消息提示组件 + 全局 Signal 控制。

+ 66 - 0
.claude/rules/architecture.md

@@ -0,0 +1,66 @@
+# 架构与分层
+
+> 目标:零编译、零打包、纯原生 JS。基于浏览器原生 ESM + Import Map,依赖托管在内部 **FMODE Repose**,
+> 业务代码直接上传 **OBS + CDN**,保存刷新即预览。对标新版 Angular(History 干净路由、DI、Signal 细粒度响应式、
+> 组件/服务分层、后台多标签,**无 Zone.js**)。
+
+## 一、整体架构
+
+- **无构建无打包**:不用 rollup / esbuild / vite / webpack,源码浏览器直接执行。
+- **依赖统一托管**:`lit`、`@lit-labs/*` 等从 npm 下 **原始 ESM** 原样同步到 **FMODE Repose**,全业务共享。
+- **业务资源独立托管**:业务源码(`index.html`、`js` 组件)部署到 **OBS + CDN**。
+- **分层(对标 Angular)**:
+  - **WebComponent(`LitElement`)**:只渲染视图 + DOM 事件,不写复杂业务逻辑。
+  - **Service(普通 ESM Class)**:存全局状态 / 接口 / 业务计算,与 DOM 解耦。
+  - **Utils(纯 ESM 函数)**:无状态共享工具。
+- **响应式**:`@lit-labs/signals`(细粒度信号)。
+- **DI**:`@lit-labs/context`(组件树注入)。
+- **路由**:`@lit-labs/router`,History 干净 URL(支持微信 OAuth 回调,需 CDN 回退规则)。
+
+## 二、分层职责表(越界即返工)
+
+| 层 | 目录 | 允许 | 禁止 |
+|----|------|------|------|
+| 视图组件 | `components/` `routes/` | 渲染、DOM 事件、读 Signal、调 Service、`@property` 本地状态 | 大量业务逻辑、直接 `fetch`、硬编码接口地址、跨组件直接 new Service |
+| 服务 | `services/` | 持有 Signal、封装接口、校验、数据转换、业务计算 | 依赖 DOM / `LitElement`、`import { html }` 创建 DOM |
+| 工具 | `utils/` | 纯无状态函数、通用封装(http/storage/format) | 持有可变全局状态、依赖上面两层 |
+
+> 判定口诀:**能被单测、不 import Lit 的,就不是组件**。服务必须能在 Node 里 `import` 后直接跑。
+
+## 三、运行流程
+
+```
+浏览器打开 URL
+  → CDN 回退规则:不存在的路径 200(非 302)返回 index.html
+  → index.html 加载公共 importmap(指向 FMODE Repose)
+  → 加载 app.js
+  → import lit 依赖
+  → 初始化 路由 / Store / 根组件渲染
+```
+
+## 四、部署拓扑(内网细节,勿外泄)
+
+- **FMODE Repose**(静态依赖仓):`/npm/lit/v3.x.x/`、`/npm/@lit-labs/{context,signals,router}/vx/`、`/npm/importmap/lit-v3.json`(公共 importmap)。
+- **OBS + CDN**(业务托管):`index.html`(引入 importmap)、`app.js`、`/routes/`、`/components/`、`/services/`、`/utils/`。
+
+> ⚠️ 本仓为 **private**。上面是内部拓扑,对外文档/开源材料中只写「内部依赖仓 + OBS+CDN」即可,**不写域名、桶名、AK/SK、obsutil 路径**。
+
+## 五、为什么这么选(对标 Angular zoneless)
+
+| 维度 | 传统 Angular(Zone) | 本方案(Lit + Signals) |
+|------|--------------------|--------------------------|
+| 变更检测 | Zone.js 全局猴子补丁,全组件树 check | Signal 精确到依赖了该信号的组件 |
+| 构建 | 必须 ng build | **无构建**,保存刷新即见 |
+| 依赖 | 打包进 bundle | Repose 共享,一次上传全业务复用 |
+| 路由 | 框架内置 | 原生 History,干净 URL,OAuth 友好 |
+| 样式隔离 | ViewEncapsulation | Shadow DOM 原生隔离 |
+| 心智负担 | 框架黑盒 | 原生 API,无魔法 |
+
+详见 [../docs/retrospective.md](../../docs/retrospective.md)。
+
+## 六、扩展方向
+
+- 全局 HTTP 工具(token 拦截 / 错误处理统一)。
+- 通用弹窗 / 表格 / 表单 WebComponent → 内部组件库,托管 FMODE Repose。
+- 路由守卫鉴权。
+- 全局 loading / 消息提示组件 + 全局 Signal 控制。

+ 132 - 0
.claude/rules/components.md

@@ -0,0 +1,132 @@
+# 组件规范(LitElement)
+
+> 组件只做一件事:**把数据渲染成 DOM,并把用户事件转成对 Service 的调用**。
+> 复杂业务逻辑、请求、状态计算 —— 全部下沉到 Service。
+
+## 〇、无构建环境的第一约束:**禁用装饰器**
+
+> ⚠️ **禁用 `@customElement` / `@property` / `@state` / `@consume` 等装饰器语法。**
+> 装饰器是 **TypeScript / Babel 的编译期特性**,浏览器**原生不执行**。无构建(源码即产物)下,
+> 一旦写装饰器,浏览器直接 `SyntaxError`。所有 Lit 官方「装饰器版」文档**不能照抄**。
+
+一律改用**标准静态类字段 + 编程式 API**:
+
+| 装饰器版(❌ 禁) | 无构建版(✅ 用) |
+|-------------------|-------------------|
+| `@customElement('x-y')` | 类末尾 `customElements.define('x-y', XY)` |
+| `@property({ type: String }) name` | `static properties = { name: { type: String } }` |
+| `@state() kw = ''` | `static properties = { kw: { type: String, state: true } }` + constructor 初始化 |
+| `@consume({ context })` | `new ContextConsumer(this, { context, subscribe, callback })` |
+| `@provide({ context })` | 模板里的 `provide(ctx, value)` 指令(见 context-di.md) |
+| `@query('#x')` | `this.renderRoot.querySelector('#x')` |
+| `@queryAssignedElements` | `this.querySelectorAll('slot')` / `slot.assignedElements()` |
+
+> `static properties` 是 Lit 的**标准 API**(装饰器只是它的语法糖),无构建下完全可用。
+
+## 一、最小组件骨架(无装饰器)
+
+```js
+// components/user-card.js
+import { LitElement, html, css } from 'lit';
+
+export class UserCard extends LitElement {
+  static properties = {
+    name: { type: String },
+    user: { type: Object },
+  };
+
+  static styles = css`
+    :host { display: block; padding: 12px; border: 1px solid #eee; border-radius: 8px; }
+    .name { font-weight: 600; }
+  `;
+
+  constructor() {
+    super();
+    this.name = '';
+    this.user = null;
+  }
+
+  render() {
+    return html`
+      <div class="name">${this.name}</div>
+      <slot></slot>
+    `;
+  }
+
+  /** 事件只做转发,不做业务 */
+  _onClick() {
+    this.dispatchEvent(new CustomEvent('card-click', {
+      detail: { user: this.user },
+      bubbles: true,
+      composed: true,   // 穿透 Shadow DOM 边界,必须
+    }));
+  }
+}
+
+customElements.define('user-card', UserCard);
+```
+
+## 二、Lit 核心特性用法
+
+| 特性 | 用途 | 要点 |
+|------|------|------|
+| `` html`...` `` | 声明式模板 | 自动转义(**天然防 XSS**);模板字面量按内容缓存,只更新变化部分 |
+| `` css`...` `` | 组件样式 | 在 Shadow DOM 内生效,样式隔离 |
+| Shadow DOM | DOM/CSS 隔离 | 默认 `open`;跨边界事件需 `composed: true` |
+| `@property` | 对外属性(UI 状态 / 入参) | 会触发重渲染,默认 attribute 反射 |
+| `@state` | 内部私有状态 | 不对外暴露、不反射到 attribute |
+| `requestUpdate()` / `updateComplete` | 手动触发 / 等待渲染 | 备选手段,优先用 property 驱动 |
+| `<slot>` | 插槽 | 封装通用布局组件(弹窗外壳、面板) |
+
+## 三、响应式:本地 vs 全局(关键分界)
+
+> ⚠️ **两套响应式必须分开**,混用会导致更新时机错乱。
+
+| 场景 | 怎么用 |
+|------|--------|
+| 组件内部 UI(弹窗开关、输入框值、tab 高亮) | Lit `@property` / `@state` + `this.requestUpdate()` |
+| 跨组件全局业务状态(登录态、字典、购物车、权限) | `@lit-labs/signals`,组件用 `SignalWatcher` 混入 |
+
+需要全局 signal 的组件:用 `SignalWatcher(LitElement)` 包裹,signal 变化自动重渲染。
+
+```js
+import { LitElement, html } from 'lit';
+import { SignalWatcher } from '@lit-labs/signals';
+import { authService } from '../services/auth-service.js';
+
+export class PageDashboard extends SignalWatcher(LitElement) {
+  render() {
+    // authService.user 是 signal:读取即订阅,变化自动重渲染
+    return html`<h1>你好,${authService.user.get()?.name ?? '访客'}</h1>`;
+  }
+}
+```
+
+## 四、组件通信
+
+| 方向 | 手段 |
+|------|------|
+| 父 → 子 | `static properties` 声明的属性(attribute 或 `.prop=${x}` 绑定) |
+| 子 → 父 | `dispatchEvent(new CustomEvent('x', { bubbles, composed }))` |
+| 任意 ↔ 任意 | **Service + Signal**(首选),组件只读 signal / 调方法 |
+| 组件树深层传递依赖 | `@lit-labs/context` 的 `provide()` 指令 + `ContextConsumer`,见 [context-di.md](./context-di.md) |
+
+- 事件名用 `kebab-case`,`detail` 用对象。
+- 跨 Shadow DOM 边界必须 `composed: true`,否则父级监听不到。
+- **禁止**用全局事件总线撒逻辑——用 Service。
+
+## 五、绝对禁止
+
+- ❌ 用装饰器(`@customElement`/`@property`/`@consume`…)—— 无构建下浏览器不执行,见第〇节。
+- ❌ 组件里 `fetch` / 写接口地址 —— 交给 Service。
+- ❌ 组件里写复杂业务计算 / 校验 —— 交给 Service 或 utils。
+- ❌ 组件 new 另一个组件需要的 Service —— 用 `ContextConsumer` 或直接 import 单例。
+- ❌ 用全局 signal 存「只影响本组件」的状态 —— 用 `static properties` 的 `state: true`。
+- ❌ 跨版本混用 `lit` —— 自定义元素注册冲突,见 [pitfalls.md](./pitfalls.md)。
+
+## 六、可复用组件的封装原则
+
+- **只暴露语义属性**(`label`、`value`、`disabled`),不暴露内部实现细节。
+- **用 slot 做内容投影**,不要用 `<... .innerHTML>` 传 HTML。
+- **样式可覆盖**:暴露 CSS 自定义属性(`--card-bg`)而非改内部 class。
+- 通用性强的组件抽到独立目录并考虑托管到 **FMODE Repose 组件库**,跨项目复用。

+ 102 - 0
.claude/rules/context-di.md

@@ -0,0 +1,102 @@
+# 依赖注入(@lit-labs/context)
+
+> Context 用于把「全局 Service / 配置」注入到组件树深层,避免逐层 `@property` 透传(prop drilling)。
+> 与 Angular 的 DI 对标,但更轻:**令牌 + provide + consume**。
+
+## 一、三件套
+
+| API | 作用 |
+|-----|------|
+| `createContext<T>(key)` | 定义一个注入令牌(模块级唯一) |
+| `provide({ context, initialValue })` | 在某个祖先元素上提供值(通常根组件) |
+| `consume({ context, subscribe })` | 在子组件里消费值 |
+
+## 二、定义令牌
+
+```js
+// services/contexts.js —— 令牌集中定义,避免散落
+import { createContext } from '@lit-labs/context';
+import { authService } from './auth-service.js';
+
+export const authContext = createContext('fmode.auth');
+// 也可以为普通配置/值建令牌
+export const configContext = createContext('fmode.config');
+```
+
+- 令牌 key 用命名空间前缀(`fmode.xxx`)避免与第三方冲突。
+- 令牌必须**同一模块实例**:多处 `createContext` 会得到不同令牌 → 注入失败。集中定义。
+
+## 三、在根组件提供(provide)
+
+```js
+// app.js 或根组件
+import { LitElement, html } from 'lit';
+import { provide } from '@lit-labs/context';
+import { authContext, configContext } from './services/contexts.js';
+import { authService } from './services/auth-service.js';
+
+export class AppRoot extends LitElement {
+  render() {
+    return html`
+      ${provide(authContext, authService)}
+      ${provide(configContext, { apiBase: '/api', appName: 'Admin' })}
+      <app-shell></app-shell>
+    `;
+  }
+}
+```
+
+- `provide` 是一个「无渲染」的模板指令,放在祖先组件 render 里,向下对所有后代生效。
+- 通常**只在根组件 provide**(全局单例 Service),不要把 provide 散在各处。
+
+## 四、在子组件消费(consume)
+
+> ⚠️ 无构建下**禁用 `@consume` 装饰器**,改用编程式 `ContextConsumer`。
+
+```js
+import { LitElement, html } from 'lit';
+import { ContextConsumer } from '@lit-labs/context';
+import { authContext } from '../services/contexts.js';
+
+export class UserBadge extends LitElement {
+  static properties = { _auth: { state: true } };
+
+  #consumer = new ContextConsumer(this, {
+    context: authContext,
+    subscribe: true,                       // 提供的「实例」被替换时回调
+    callback: (value) => { this._auth = value; },
+  });
+
+  render() {
+    return html`<span>${this._auth?.user.get()?.name ?? '未登录'}</span>`;
+  }
+}
+
+customElements.define('user-badge', UserBadge);
+```
+
+- `subscribe: true`:当提供的**值本身**被替换为另一个实例时,组件重新响应(不是 signal 变化,而是「注入对象换了」)。
+- 注入对象内部的 signal 变化,由 `SignalWatcher` 负责驱动重渲染 —— 两者配合。
+- 父组件提供用模板指令 `provide(ctx, value)`(见第三节),同样无需装饰器。
+
+## 五、何时用 Context,何时直接 import
+
+| 情况 | 做法 |
+|------|------|
+| 全局单例 Service,确定唯一 | **直接 `import` 单例** 即可(最简单,推荐) |
+| 需要「同组件树不同区块用不同实例」(多实例 / 可测试替换) | **Context**(provide 不同实例) |
+| 需要向下传配置 / 主题 / 环境值 | **Context** |
+
+> 务实建议:**默认直接 import 单例**;只有当「同一组件要能跑在不同注入环境」时才上 Context。别为了 DI 而 DI。
+
+## 六、与 SignalWatcher 配合
+
+- `consume` 拿到的是 **Service 实例**(不一定变化)。
+- Service 里的 **signal** 变化 → 由混入 `SignalWatcher` 的组件自动重渲染。
+- 两者职责不同:`consume` 解决「拿到谁」,`SignalWatcher` 解决「谁变了要重渲染」。
+
+## 七、坑
+
+- 令牌模块重复实例化 → consume 拿不到 → 返回 `undefined`。**令牌必须集中定义、单例导出**。
+- 在 `render()` 外读取 consumed 属性可能为 `undefined`(首次渲染前);用可选链。
+- provide 的作用域是「该元素的子树」,provide 写错层级 → 后代拿不到。

+ 139 - 0
.claude/rules/deployment.md

@@ -0,0 +1,139 @@
+# 部署规范(FMODE Repose + OBS/CDN)
+
+> 本仓为 **private**。本篇含内部部署细节,**禁止**复制到对外文档 / 开源材料 / 公开仓库。
+> 无论对内对外,**AK/SK 密钥一律不写进任何文档或代码**,只从既有部署脚本引用。
+
+## 一、两条托管线
+
+| 内容 | 托管方 | 说明 |
+|------|--------|------|
+| **依赖**(lit / @lit-labs/*) | **FMODE Repose** | 静态依赖仓,从 npm 下**原始 ESM** 原样同步,按版本目录化,全业务共享 |
+| **业务码**(index.html / js) | **OBS + CDN** | `obsutil` 同步 `src/` 到华为云 OBS,`public-read`,CDN 加速 |
+
+## 二、FMODE Repose(静态依赖仓)
+
+**存放布局**
+
+```
+/npm/lit/v3.x.x/                      # lit 运行时(原始 ESM)
+/npm/@lit-labs/context/vx/            # context
+/npm/@lit-labs/signals/vx/            # signals
+/npm/@lit-labs/router/vx/             # router
+/npm/importmap/lit-v3.json            # 公共 importmap(全业务复用)
+```
+
+**硬性要求**
+
+1. `.js` 响应的 `Content-Type` **必须** `text/javascript`(否则浏览器拒绝作为模块执行)。
+2. **CORS 允许业务域名**(否则跨域模块加载失败)。
+3. 开启 **HTTP/2(或 /3)** —— ESM 小文件多,靠多路复用降延迟。
+4. 依赖按**版本目录**化(`/vX.Y.Z/`),升级 = 新增目录 + 改 importmap 版本号,**不覆盖旧版本**。
+5. 保持 npm 包的**相对 import 结构**原样(原样同步,不重写 import)。
+
+**同步方式**:把 `node_modules/lit`、`node_modules/@lit-labs/*` 原样上传到 Repose 对应版本目录。
+(仓库内提供同步脚本入口占位,实际凭据从环境 / 既有脚本注入。)
+
+**公共 importmap**(托管在 Repose,业务引用)
+
+```json
+{
+  "imports": {
+    "lit": "/npm/lit/v3.2.1/lit.js",
+    "lit/": "/npm/lit/v3.2.1/",
+    "lit/decorators.js": "/npm/lit/v3.2.1/decorators.js",
+    "@lit-labs/signals": "/npm/@lit-labs/signals/v0.1.3/index.js",
+    "@lit-labs/context": "/npm/@lit-labs/context/v0.5.1/index.js",
+    "@lit-labs/router": "/npm/@lit-labs/router/v0.2.0/router.js"
+  }
+}
+```
+
+> 版本号以仓库实际落地为准,此处为示例形态。升级依赖 = 追加新路径 + 改这里的版本段。
+
+## 三、OBS + CDN(业务部署)
+
+**入口 index.html**
+
+```html
+<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="utf-8" />
+  <base href="/<project>/" /><!-- 二级路径部署必须;根部署可省略或 "/" -->
+  <link rel="importmap" href="..."><!-- 见下 -->
+  <script type="importmap" src="<公共 importmap URL>"></script>
+</head>
+<body>
+  <app-root></app-root>
+  <script type="module" src="./app.js"></script>
+</body>
+</html>
+```
+
+- 公共 importmap 由 **Repose 托管**,全业务用同一份:
+  `<script type="importmap" src="<Repose>/npm/importmap/lit-v3.json"></script>`。
+- `<base href>` 决定相对路径基准,子目录部署**必须**写对。
+
+**上传(obsutil)**
+
+- 工具:`/opt/data/bin/obsutil`
+- 桶(bucket)与端点、AK/SK:**从既有部署脚本引用**(如 `fmode-app/scripts/deploy.sh`),**禁止**写入本仓文档/代码。
+- 典型命令形态:
+
+```bash
+# 同步业务源码到 OBS 前缀,公开读
+obsutil sync ./src/ obs://<bucket>/<project>/ \
+  -e=obs.cn-south-1.myhuaweicloud.com -acl=public-read -f
+```
+
+- **index.html 先删后传**:`sync` 按 size+mtime 判 Skip,`index.html` 名字固定、内容随发布变化,需先 `rm` 再 `cp`,避免被 Skip。
+- 静态资源设 `Cache-Control`:`index.html` 用 `no-cache`(保证回退拿到最新),带 hash 的资源可长缓存。
+
+**CDN 刷新**:上传后调用 CDN refresh(仓库内 `scripts/deploy.sh` 已接入既有刷新脚本)。刷新**范围**由传入的 URL 域名决定。
+
+## 四、History 路由的 CDN 回退(关键)
+
+| 规则 | 匹配 | 动作 |
+|------|------|------|
+| 规则 1 | 命中真实静态资源 | 直接返回该文件 |
+| 规则 2 | 路径不存在 | 返回 `index.html`,状态码 **200**,**禁止 301/302** |
+
+- 用 **200** 而非 302:302 会改写地址栏 / 丢 `location.pathname`,破坏路由参数与微信 OAuth 回调。
+- `index.html` 设 `no-cache`,确保回退总是拿到最新壳。
+
+## 五、部署脚本(模板)
+
+```bash
+#!/usr/bin/env bash
+# scripts/deploy.sh —— 无构建,直接同步 src/ 到 OBS,再刷 CDN
+set -euo pipefail
+PROJECT="${1:?usage: deploy.sh <project>}"
+BUCKET="${OBS_BUCKET:?set OBS_BUCKET}"           # 例: obs://www-fmode.cn
+PREFIX="$PROJECT"
+EP="obs.cn-south-1.myhuaweicloud.com"
+OBS=/opt/data/bin/obsutil
+
+# 1) index.html 先删后传(防 Skip)+ no-cache
+"$OBS" rm "$BUCKET/$PREFIX/index.html" -e="$EP" -f >/dev/null 2>&1 || true
+"$OBS" cp ./src/index.html "$BUCKET/$PREFIX/index.html" -e="$EP" -acl=public-read -f \
+  -meta="Cache-Control:no-cache#Content-Type:text/html; charset=utf-8"
+
+# 2) 其余源码同步
+"$OBS" sync ./src/ "$BUCKET/$PREFIX/" -e="$EP" -acl=public-read -f
+
+# 3) 刷新 CDN(调用既有刷新脚本,凭据由该脚本/环境提供)
+# node <既有刷新脚本> "https://<cdn-domain>/$PREFIX/"
+```
+
+- **凭据注入**:AK/SK 从环境变量 / 既有脚本读取,**不落本仓**。
+- 无构建 → 无需构建步骤,`src/` 即产物。
+
+## 六、发布 checklist
+
+- [ ] `<base href>` 与部署前缀一致
+- [ ] importmap 指向 Repose,版本号正确
+- [ ] 依赖已同步到 Repose(`Content-Type: text/javascript` + CORS 放行)
+- [ ] OBS 上传完成,`index.html` 已先删后传
+- [ ] CDN 回退规则:不存在路径 200 返回 index.html
+- [ ] CDN 刷新完成
+- [ ] 直接命中深链接(如 `/users/42`)刷新可用

+ 113 - 0
.claude/rules/http-utils.md

@@ -0,0 +1,113 @@
+# HTTP 与工具(utils)
+
+> 用原生 `fetch` 薄封装,**不引 axios**。工具层是**纯无状态函数**,谁都能 import,不持有全局状态。
+
+## 一、utils 的边界
+
+| 允许 | 禁止 |
+|------|------|
+| 纯函数(格式化、校验、URL 拼接、深拷贝) | 持有可变全局状态 |
+| 通用封装(http、storage) | import `components/` 或 `services/` |
+| 依赖浏览器 API(fetch/localStorage/URL) | 依赖 Lit / DOM 渲染 |
+
+> utils 必须可在 Node 或任意上下文 import 而不产生副作用。
+
+## 二、HTTP 封装(utils/http.js)
+
+```js
+// utils/http.js
+import { authService } from '../services/auth-service.js';
+
+const BASE = '/api';   // 或从 config 注入
+
+class HttpError extends Error {
+  constructor(status, body) { super(`HTTP ${status}`); this.status = status; this.body = body; }
+}
+
+async function request(method, path, { body, headers = {}, query } = {}) {
+  let url = BASE + path;
+  if (query) url += '?' + new URLSearchParams(query).toString();
+
+  const finalHeaders = { 'Content-Type': 'application/json', ...headers };
+  const token = authService?.getToken?.();
+  if (token) finalHeaders.Authorization = `Bearer ${token}`;
+
+  const res = await fetch(url, {
+    method,
+    headers: finalHeaders,
+    body: body != null ? JSON.stringify(body) : undefined,
+    credentials: 'same-origin',
+  });
+
+  if (res.status === 401) {
+    authService.logout();
+    // 触发跳转登录(由路由/根组件响应 signal)
+    throw new HttpError(401, null);
+  }
+  const ct = res.headers.get('content-type') || '';
+  const data = ct.includes('application/json') ? await res.json() : await res.text();
+  if (!res.ok) throw new HttpError(res.status, data);
+  return data;
+}
+
+export const http = {
+  get: (p, o) => request('GET', p, o),
+  post: (p, body, o) => request('POST', p, { ...o, body }),
+  put: (p, body, o) => request('PUT', p, { ...o, body }),
+  del: (p, o) => request('DELETE', p, o),
+};
+```
+
+设计要点:
+- **token 自动注入**(从 `authService` 取)。
+- **401 统一处理**:登出 + 让 UI 响应 signal 跳登录。
+- **统一错误**:抛 `HttpError`,携带 `status` / `body`,调用方可 `catch`。
+- **DTO 解析**按 content-type 分流。
+- 循环依赖注意:`http` 引用 `authService`,`authService` 又用 `http` → 用**惰性调用**(函数内取)而非模块顶层解构,避免 ESM 循环初始化问题。
+
+## 三、Storage 封装(utils/storage.js)
+
+```js
+export const storage = {
+  get(key, fallback = null) {
+    try { const v = localStorage.getItem(key); return v == null ? fallback : JSON.parse(v); }
+    catch { return fallback; }
+  },
+  set(key, value) { localStorage.setItem(key, JSON.stringify(value)); },
+  remove(key) { localStorage.removeItem(key); },
+};
+```
+
+- 加 try/catch(隐私模式 / 配额满会抛)。
+- JSON 序列化,避免手写类型转换。
+- 敏感数据(token)单独考虑,不要明文长期存(见安全)。
+
+## 四、格式化(utils/format.js)
+
+```js
+export const formatMoney = (n, currency = '¥') =>
+  `${currency}${Number(n ?? 0).toFixed(2)}`;
+
+export const formatDate = (d, fmt = 'YYYY-MM-DD') => { /* ... */ };
+
+export const debounce = (fn, ms = 200) => {
+  let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
+};
+```
+
+纯函数,无副作用,可单测。
+
+## 五、其他常用工具
+
+| 工具 | 用途 |
+|------|------|
+| `utils/router.js` | `navigate()`(基于 `document.baseURI`,尊重 `<base href>`) |
+| `utils/dom.js` | 少量原生 DOM 辅助(慎用,优先 Lit 指令) |
+| `utils/validate.js` | 表单校验纯函数 |
+
+## 六、禁止
+
+- ❌ 在 utils 里 `import { html }`。
+- ❌ 在 utils 里持有跨调用的可变状态(那属于 Service)。
+- ❌ 在组件里直接 `fetch`(一律走 `http`)。
+- ❌ 手写重复的 URL 拼接 / 时间格式化(抽到 utils)。

+ 118 - 0
.claude/rules/modules/README.md

@@ -0,0 +1,118 @@
+# 模块划分最佳实践
+
+> 无构建项目最容易「长成一坨」:所有组件堆在 `components/`、所有逻辑塞进一个超大 service。
+> 本篇给出**功能模块 / 通用能力 / 业务模块**的拆分准则与落地目录。
+
+## 一、三种模块,三种归属
+
+| 类型 | 定义 | 归属 | 是否可跨项目 |
+|------|------|------|--------------|
+| **通用能力(util skill)** | 与业务无关的纯能力:http、storage、format、debounce | `utils/` | ✅ 可抽成包托管 Repose |
+| **通用组件(UI kit)** | 与业务无关的可复用 UI:弹窗、表格、表单、布局壳 | `components/` | ✅ 可抽成组件库 |
+| **功能模块(feature)** | 一块完整业务功能:用户管理、订单、报表 | `modules/<feature>/` | ❌ 业务专属 |
+
+> 判别法:**换一个业务项目,它还有用吗?** 有用 → 通用(utils/components);没用 → 功能模块。
+
+## 二、推荐目录(功能模块化)
+
+```
+src/
+  app.js
+  components/                 # 跨模块通用 UI
+    app-shell.js
+    tabs-bar.js
+    base-table.js
+  modules/                    # 功能模块(一块一块业务)
+    user/
+      page-user-list.js       # 页面组件
+      page-user-detail.js
+      user-service.js         # 模块内 service
+      user-model.js           # 模块内数据结构/转换
+    order/
+      page-order-list.js
+      order-service.js
+  services/                   # 跨模块共享 service(全局状态)
+    auth-service.js
+    dict-service.js
+  utils/                      # 通用能力
+    http.js
+    storage.js
+    format.js
+  index.html
+```
+
+规则:
+- **模块内聚**:一个功能模块的页面、service、model 放在同一目录,便于整体搬走 / 整体删除。
+- **跨模块共享**才上提到 `services/`(如 auth、字典、权限)。
+- **通用 UI** 才留在顶层 `components/`;只在某模块用的组件放在该模块目录内。
+
+## 三、模块的对外契约
+
+一个功能模块**只暴露页面组件**(供路由挂载)与必要的 service。内部实现(子组件、model)不外泄。
+
+```
+modules/user/
+  index.js            # 可选:统一出口
+    export { PageUserList } from './page-user-list.js';
+    export { userService } from './user-service.js';
+```
+
+- 别的模块**不要**深挖 `modules/user/内部子组件.js`。
+- 需要跨模块调用的,通过模块出口或上提的共享 service。
+
+## 四、Service 的粒度
+
+| 粒度 | 例子 | 归属 |
+|------|------|------|
+| 全局域(登录、权限、字典) | `auth-service` | `services/` |
+| 模块域(仅本模块用) | `user-service` | `modules/user/` |
+| ❌ 万能 service | `app-service`(什么都塞) | 拆掉 |
+
+- 一个 service 一个关注点。
+- 模块 service 若被第二个模块需要 → 上提到 `services/`(**上提,而非复制**)。
+
+## 五、什么时候抽到 Repose(组件库 / 能力包)
+
+满足**全部**以下条件才抽:
+
+1. ≥2 个项目在用;
+2. 接口稳定(属性/事件契约不频繁变);
+3. 无业务耦合(不依赖具体业务 model / 接口)。
+
+抽出的产物:
+- **UI 组件库**:一批 `kebab-case` 自定义元素,托管到 FMODE Repose,业务用 importmap 引入。
+- **能力包**:`http`、`storage` 等 utils 打成 ESM 包。
+
+> 抽早了是负担。先用项目内 `components/` `utils/` 沉淀,稳定后再上提。
+
+## 六、依赖方向(严格单向)
+
+```
+routes/modules  →  components  →  services  →  utils
+       │                │              │
+       └────────────────┴──────────────┘   禁止反向
+```
+
+- 上层可依赖下层,**下层禁止反向依赖上层**。
+- `utils` 不认识 `services` / `components`。
+- `services` 不 `import` Lit / 组件。
+- 反向依赖一旦出现,说明职责放错了层。
+
+## 七、模块划分 checklist
+
+- [ ] 换业务就失效的代码,是否都收在 `modules/<feature>/` 里?
+- [ ] 跨模块共享的状态是否上提到 `services/`(而非每个模块各存一份)?
+- [ ] 通用 UI 是否在 `components/`,且不 import 任何业务 model?
+- [ ] 依赖方向是否单向(无 `utils → services` 之类的反向 import)?
+- [ ] 是否有「万能 service / 万能组件」需要拆?
+- [ ] 满足复用条件的组件 / 工具,是否已考虑上提 Repose?
+
+## 八、反模式
+
+| 反模式 | 症状 | 修复 |
+|--------|------|------|
+| 上帝 service | 一个 service 上千行,改哪都怕 | 按关注点拆分 |
+| 组件内联业务 | 页面组件里 assert 校验 + fetch + 拼接口 | 逻辑下沉 service |
+| utils 有状态 | utils 里存了「上次结果」 | 移到 service |
+| 复制粘贴跨模块 | 两个模块各有一份 tableservice | 上提共享 |
+| 过早抽象 | 只有一个用户就搞组件库 | 先项目内沉淀 |

+ 98 - 0
.claude/rules/modules/examples.md

@@ -0,0 +1,98 @@
+# 场景化模块拆分示例
+
+> 配套 [README.md](./README.md),给出一个「运营后台」的完整拆分演练。
+
+## 一、需求
+
+一个运营后台,含:
+- 登录 / 登出、当前用户、权限判断
+- 侧边菜单(按权限过滤)
+- 用户管理(列表 / 详情 / 编辑)
+- 订单管理(列表 / 详情)
+- 数据字典(下拉选项,多处用)
+- 多标签工作区
+
+## 二、拆分结果
+
+```
+src/
+  app.js                         # 装配:provide 全局 service + 路由 + 根壳
+  components/                    # 通用 UI(无业务耦合)
+    app-shell.js                 # 布局壳(菜单 + 标签 + 内容区)
+    side-menu.js                 # 侧边菜单(接收菜单数据,不自己拉接口)
+    tabs-bar.js                  # 多标签栏
+    tabs-container.js            # KeepAlive 容器
+    base-table.js                # 通用表格(列配置驱动)
+    base-modal.js                # 通用弹窗(slot 内容)
+  modules/
+    auth/
+      page-login.js
+      auth-service.js            # 登录态 + token + 权限码
+    user/
+      page-user-list.js
+      page-user-detail.js
+      user-service.js            # 用户 CRUD
+      user-model.js              # 用户数据结构 + 转换
+    order/
+      page-order-list.js
+      page-order-detail.js
+      order-service.js
+  services/                      # 跨模块共享
+    dict-service.js              # 数据字典(权限/订单模块都用)
+    menu-service.js              # 菜单树(app-shell 用)
+  utils/
+    http.js
+    storage.js
+    format.js
+    router.js
+```
+
+## 三、归属决策演示
+
+| 代码 | 归属 | 理由 |
+|------|------|------|
+| 登录态、token、权限码 | `modules/auth/auth-service.js` | 认证是独立功能域 |
+| 用户 CRUD | `modules/user/user-service.js` | 仅用户模块用 |
+| 订单 CRUD | `modules/order/order-service.js` | 仅订单模块用 |
+| 数据字典 | `services/dict-service.js` | **跨模块**(用户、订单都拉下拉项)→ 上提 |
+| 菜单树 | `services/menu-service.js` | app-shell 用,且依赖权限 → 跨模块 |
+| 表格 / 弹窗 | `components/base-table.js` 等 | 无业务耦合,可复用/可抽库 |
+| http / storage | `utils/` | 纯能力 |
+
+## 四、关键交互(数据流)
+
+```
+page-user-list(视图)
+  ├─ 读 dictService.roles(signal)→ 渲染筛选项
+  ├─ 调 userService.fetchList(query) → 拿到数据
+  └─ 渲染 base-table(传入列配置 + 数据)
+        └─ 行点击 → dispatchEvent('row-click')
+              └─ page-user-list 收到 → router.navigate('/users/:id')
+                    └─ tabsService.open(...) + toggle 激活 → KeepAlive
+```
+
+原则回顾:
+- **组件只读 signal、只转发事件**,不 fetch。
+- **service 持有状态与业务**,无 DOM。
+- **跨模块**的状态在 `services/`,**模块内**的在模块目录。
+
+## 五、依赖方向检查
+
+- `page-user-list` → `base-table`、`user-service`、`dictService` ✅(上层依赖下层/共享)
+- `user-service` → `utils/http` ✅
+- `utils/http` → `auth-service`(取 token)⚠️ —— 这是「工具反向依赖服务」。
+  - **处理**:不改依赖方向,而是让 `auth-service` **注入** token getter 给 http,或 `http` 通过参数接收 token。
+  - 例:`configureHttp({ getToken: () => authService.getToken() })` 在 `app.js` 装配时调用。这样 `utils` 不 import `services`。
+
+## 六、可测试性验证
+
+- `user-service` / `dict-service` 可在 Node 直接 import,mock `http` 后单测。
+- `format.js` / `storage.js` 纯函数,直接断言。
+- `base-table` 组件逻辑(列排序/选择)用 DOM 测试或抽纯函数到 utils 测。
+
+## 七、演进路径(从项目内到 Repose)
+
+1. 项目内先用 `components/base-table.js`。
+2. 第二个项目也要 → 抽到独立包 `fmode-ui-table`(ESM,无构建)。
+3. 托管到 FMODE Repose,两项目 importmap 引入。
+4. 保持契约稳定(属性/事件版本化)。

+ 89 - 0
.claude/rules/pitfalls.md

@@ -0,0 +1,89 @@
+# 风险与坑位
+
+> 无构建方案的红线。踩中任何一条,症状往往隐蔽(白屏 / 404 / 路由丢失),
+> 排查时先按本表逐条对照。
+
+## 〇、用了装饰器语法(`@customElement` 等)
+
+**症状**:整个模块 `SyntaxError: Invalid or unexpected token` / `Unexpected character '@'`,白屏。
+**原因**:装饰器是 **TS/Babel 编译期**特性,浏览器原生 JS **不执行**。Lit 官方文档默认「装饰器 + 构建」,
+无构建下照抄即挂。
+**对策**:一律用 `static properties` + `customElements.define()` + `ContextConsumer` 编程式 API。
+详见 [components.md](./components.md) 第〇节。**本项目所有示例均无装饰器**。
+
+## 一、CORS(最高频)**症状**:模块加载失败、`Failed to fetch dynamically imported module`、控制台 CORS 报错。
+**原因**:FMODE Repose 未放行业务域名。
+**对策**:Repose 的 CORS 必须允许所有业务域名;跨域模块请求带 `Origin`,响应需 `Access-Control-Allow-Origin`。
+> ESM 模块请求是 **CORS 请求**(比普通脚本严格),勿用「能下载就能跑」判断。
+
+## 二、Content-Type 不是 `text/javascript`
+
+**症状**:`Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of ...`。
+**原因**:依赖仓把 `.js` 当 `text/plain` / `application/octet-stream` 返回。
+**对策**:Repose 上传时显式设 `Content-Type: text/javascript; charset=utf-8`。
+
+## 三、多版本 lit 混用
+
+**症状**:自定义元素重复注册报错 `Failed to execute 'define' ... has already been used`;或响应式失效。
+**原因**:importmap / 直接路径引到了两个不同版本的 lit(如 `lit@3.1` 与 `lit@3.2` 各被加载一次,`LitElement` 类不同)。
+**对策**:**全业务统一 importmap**,只允许一个 lit 版本;所有包都从同一 Repose 版本段解析。除 `lit` 本身,注意 `lit-html` / `lit-element` / `@lit/reactive-element` 也要**单实例**(importmap 里补全子路径映射)。
+
+## 四、History 路由被 CDN 302
+
+**症状**:深链接刷新后地址栏变成 `/index.html`,路由参数丢失;微信 OAuth 回调失败。
+**原因**:CDN 回退用 301/302 重定向。
+**对策**:回退**必须 200** 返回 `index.html`,地址栏不变。见 [deployment.md](./deployment.md)。
+
+## 五、子目录部署缺 `<base href>`
+
+**症状**:`./app.js`、相对 import 全部 404(被解析到域名根)。
+**原因**:子目录部署(`/admin/`)未写 `<base href="/admin/">`。
+**对策**:子目录部署**必须**写对 `<base href>`;`navigate()` 基于 `document.baseURI` 拼 URL。
+
+## 六、相对 import 漏写 `.js` 后缀
+
+**症状**:`Failed to resolve module specifier './foo'` / 404。
+**原因**:浏览器原生 ESM **不做扩展名补全**。
+**对策**:相对导入一律带 `.js`(`import x from './foo.js'`)。裸标识符(`lit`)走 importmap。
+
+## 七、Signal 与 Lit property 混用
+
+**症状**:更新时机错乱、该刷不刷、或刷两次。
+**原因**:把「组件本地 UI 状态」也塞进全局 signal,或把「跨组件业务状态」放在 `@property` 里手动同步。
+**对策**:**划清界限** —— 组件内 UI 用 `@property`/`@state`;跨组件业务用 signal + `SignalWatcher`。
+
+## 八、ESM 小文件多 → 请求多
+
+**症状**:首屏依赖瀑布、加载慢。
+**原因**:无打包 = 每个模块一次请求。
+**对策**:Repose 开 **HTTP/2/3** 多路复用;合理目录深度(别过深);`Cache-Control` 长缓存版本化依赖。
+
+## 九、老浏览器不支持 Import Map
+
+**症状**:低版本浏览器无法解析裸标识符。
+**原因**:Import Map 是现代浏览器原生特性。
+**对策**:内部后台一般可接受现代浏览器基线;如需兼容,引入 import-map polyfill(es-module-shims)放在所有 module 之前。
+
+## 十、循环依赖导致初始化时机问题
+
+**症状**:某模块拿到 `undefined`(如 `http` 顶层解构了尚未初始化的 `authService`)。
+**原因**:ESM 循环引用 + 顶层立即求值。
+**对策**:跨模块引用**延迟到函数内**取值(别在模块顶层解构相互引用的单例)。
+
+## 十一、无构建下的其他红线
+
+| 坑 | 说明 |
+|----|------|
+| 引入需要打包器转译的依赖(CJS/TS) | 浏览器无法直接跑 CJS;只选 **原生 ESM** 依赖 |
+| 用 `.ts` 源码直发 OBS | 浏览器不认 TS;无构建下**源码即 JS** |
+| 直接改 `src/` 后忘记刷 CDN | 看到旧版本;发布流程要含 CDN 刷新 |
+| `index.html` 被 CDN 长缓存 | 回退/更新拿到旧壳;`index.html` 设 `no-cache` |
+| 自定义元素标签忘带连字符 | `define` 抛错,如 `approot` 应 `app-root` |
+
+## 十二、排查口诀
+
+1. 看控制台 **MIME / CORS / 404** 三类报错——先分清是「依赖没托管好」还是「业务路径错」。
+2. 打开 Network:模块请求的 **Content-Type** 与 **状态码**。
+3. 深链接刷新 404 → 查 CDN 回退规则。
+4. 相对资源 404 → 查 `<base href>`。
+5. 元素重复注册 → 查 lit 是否多版本。

+ 98 - 0
.claude/rules/project-structure.md

@@ -0,0 +1,98 @@
+# 项目目录规范
+
+> 无打包 → `src/` 目录结构会**原样**上传到 OBS(不存在 tree-shaking / 文件重命名 / hash),
+> 所以目录即 URL 路径,文件即模块。摆放必须克制、可预测。
+
+## 一、标准目录
+
+```
+src/
+  index.html            # 入口:<base> + <script type="importmap"> + <script type="module" src="./app.js">
+  app.js                # 应用装配:全局 DI provide + 路由挂载 + 根组件渲染 + 多标签
+  components/           # 通用组件(可跨页复用)
+    side-menu.js
+    user-card.js
+    tabs-container.js
+  routes/               # 页面级组件(一页一文件)
+    page-user-list.js
+    page-user-detail.js
+    page-404.js
+  services/             # 业务服务(状态 + 接口 + 计算)
+    auth-service.js
+    table-service.js
+    router-service.js
+  utils/                # 无状态工具
+    http.js
+    storage.js
+    format.js
+```
+
+`examples/hello-lit/` 是本规范的完整可运行样例。
+
+## 二、命名约定
+
+| 类型 | 文件 | 类 / 导出 | 自定义元素标签 |
+|------|------|-----------|----------------|
+| 通用组件 | `kebab-case.js` | `PascalCase`(如 `UserCard`) | `kebab-case`(`<user-card>`,**必须含连字符**) |
+| 页面组件 | `page-xxx.js` | `PageXxx` | `<page-xxx>` |
+| 服务 | `xxx-service.js` | `XxxService` | 单例:`export const xxxService = new XxxService()` |
+| 工具 | `xxx.js` | 具名导出纯函数 | — |
+
+- 目录一律小写 kebab-case。
+- 自定义元素标签**必须**包含短横线(HTML 规范,否则注册失败)。
+- 一个文件一个默认/具名导出的组件类,文件名与标签对应。
+
+## 三、为什么要「目录即路径」
+
+无构建下浏览器按 URL 解析相对 import:
+
+```js
+// 在 /projects/admin/routes/page-user-list.js 中
+import { authService } from '../services/auth-service.js';  // → /projects/admin/services/auth-service.js
+```
+
+因此:
+- **相对导入必须带 `.js` 后缀**(浏览器原生 ESM,不做扩展名补全)。❌ `import x from './foo'`
+- 移动文件 = 改 URL,改文件必须同步改所有引用。
+- 不要深层嵌套(超过 3 层)——ESM 小文件多,路径越长越啰嗦。
+
+## 四、二级子路径部署
+
+子目录部署(如 `/projects/admin/`)时,**必须在 `index.html` 的 `<head>` 写 `<base href>`**:
+
+```html
+<base href="/<project>/"><!-- 例:/admin/ 或 /dev/fmode-app/ -->
+```
+
+否则浏览器会把 `./app.js` 解析到域名根,相对 JS 全部 **404**。
+
+- `<base>` 影响:所有**相对 URL**(`href`/`src`/`fetch` 相对路径/`import` 裸相对路径)。
+- 路由若用 History API 生成链接,需基于 `base` 或应用前缀拼装,见 [routing.md](./routing.md)。
+- 切换部署路径只需改这一处 `<base href>` + importmap 里的业务前缀(若有)。
+
+## 五、依赖不进业务仓
+
+- **禁止**在业务仓 `node_modules` 里放 `lit` 供运行时 `import`。
+- **禁止**把 `lit` 源码拷进 `src/`。
+- 运行时依赖全部由 `index.html` 的 importmap 指向 **FMODE Repose** 解析,见 [deployment.md](./deployment.md)。
+- 本地 `npm install lit @lit-labs/*` **仅**用于 IDE/TS 类型提示,**不参与产物**。
+
+```jsonc
+// package.json —— 仅开发期类型提示,不打包
+{
+  "private": true,
+  "devDependencies": { "lit": "^3.2.0", "@lit-labs/signals": "^0.1.0" },
+  "scripts": { "deploy": "bash scripts/deploy.sh" }
+}
+```
+
+## 六、文件摆放决策表
+
+| 这个东西是… | 放哪 |
+|-------------|------|
+| 全站复用的 UI(菜单、卡片、弹层) | `components/` |
+| 一个路由对应的一整页 | `routes/page-*.js` |
+| 有状态、要跨页共享的业务逻辑 | `services/*-service.js` |
+| 无状态、纯计算的函数 | `utils/` |
+| 全局装配(DI、路由、根标签) | `app.js` |
+| 第三方运行时依赖 | **不放项目里**,走 Repose + importmap |

+ 124 - 0
.claude/rules/routing.md

@@ -0,0 +1,124 @@
+# 路由(@lit-labs/router)
+
+> 使用 History 模式得到**干净 URL**(无 `#`)。这是内部后台 + 微信 OAuth 回调的硬性前提:
+> OAuth 回调 URL 不能带 hash,且必须由 CDN 返回 **200**(不能 302)。
+
+## 一、基本用法
+
+```js
+// services/router-service.js 或 app.js
+import { Router } from '@lit-labs/router';
+
+export function createRouter(host, routes) {
+  return new Router(host, {
+    routes: [
+      { path: '/', render: () => html`<page-home></page-home>` },
+      { path: '/users', render: () => html`<page-user-list></page-user-list>` },
+      { path: '/users/:id', render: ({ id }) => html`<page-user-detail .userId=${id}></page-user-detail>` },
+      { path: '/*', render: () => html`<page-404></page-404>` },
+    ],
+  });
+}
+```
+
+在根组件里挂载:
+
+```js
+import { Router } from '@lit-labs/router';
+
+export class AppShell extends LitElement {
+  #router = new Router(this, [
+    { path: '/', render: () => html`<page-home></page-home>` },
+    { path: '/users', render: () => html`<page-user-list></page-user-list>` },
+    { path: '/*', render: () => html`<page-404></page-404>` },
+  ]);
+
+  render() {
+    return html`
+      <side-menu></side-menu>
+      <main>${this.#router.outlet()}</main>
+    `;
+  }
+}
+```
+
+- `path` 支持参数段 `:id` 与通配 `/*`。
+- `render(params)` 返回模板;参数从 `params` 取。
+- `outlet()` 是渲染出口。
+
+## 二、导航(干净 URL)
+
+```js
+import { navigate } from './router-utils.js';
+
+navigate('/users/42');          // pushState
+navigate('/login', { replace: true });
+```
+
+`utils/router.js` 里封装,基于 `<base href>` 拼前缀:
+
+```js
+export function navigate(path, { replace = false } = {}) {
+  const url = new URL(path, document.baseURI); // 尊重 <base href>
+  if (replace) history.replaceState({}, '', url);
+  else history.pushState({}, '', url);
+  window.dispatchEvent(new PopStateEvent('popstate'));
+}
+```
+
+- **基于 `document.baseURI`** 拼 URL,子目录部署(`/admin/`)才不会跳错。
+- 手动 `pushState` 后派发 `popstate`,让 Router 感知。
+
+## 三、CDN 回退规则(History 路由的前提)
+
+History 模式下,用户在 `/users/42` 直接刷新 → 该路径在 OBS 上不存在 → **必须由 CDN 回退规则**处理:
+
+| 规则 | 行为 |
+|------|------|
+| 规则 1 | 路径命中真实静态资源(`.js` / `.css` / `.png` …)→ 直接返回 |
+| 规则 2 | 路径不存在 → 返回 `index.html`,**状态码 200**,**禁止 301/302** |
+
+> ⚠️ 若用 302 重定向到 `/index.html`,浏览器地址栏会被改写、`location.pathname` 丢失,路由参数全丢,微信 OAuth 回调也会失败。**必须 200**。
+
+配置见 [deployment.md](./deployment.md)。
+
+## 四、路由守卫(鉴权)
+
+```js
+const requireAuth = (render) => (params) => {
+  if (!authService.isLoggedIn.get()) {
+    navigate('/login');
+    return html``;   // 或渲染登录页占位
+  }
+  return render(params);
+};
+
+new Router(this, [
+  { path: '/users', render: requireAuth((p) => html`<page-user-list></page-user-list>`) },
+]);
+```
+
+- 守卫是**纯函数包装**,不引入框架概念。
+- 异步守卫(先校验 token 再放行)可返回 Promise 模板,或先渲染 loading。
+
+## 五、OAuth 回调(微信)
+
+- 回调地址设为业务前缀下的干净路径,如 `https://<cdn-domain>/<project>/oauth/callback`。
+- 该路径在 CDN 上不存在 → 由回退规则 200 返回 `index.html` → 前端读取 `location.search` 里的 `code`。
+- **全程不能出现 302**,否则微信校验的 `redirect_uri` 与实际落地 URL 不一致。
+
+## 六、多标签的交互
+
+若应用是多标签后台(见 [tabs-keepalive.md](./tabs-keepalive.md)),路由切换需驱动「打开/激活标签」而非整体重渲染:
+
+```js
+// Router 命中新路径时 → tabsService.open(path, { title, component })
+// 已存在同 path 标签 → 仅激活;不存在 → 新开
+```
+
+## 七、坑
+
+- 忘记 CDN 回退 → 刷新 404。
+- `<base href>` 配错 → 相对 import 404。
+- 用 `<a href="/x">` 原生跳转 → 整页刷新(非 SPA)。内部导航一律 `navigate()` + `popstate`。
+- 混用 hash 路由与 History 路由 → 混乱。**只用 History**。

+ 108 - 0
.claude/rules/services.md

@@ -0,0 +1,108 @@
+# 服务规范(Service / Signal)
+
+> Service 是普通 ESM Class:**存全局状态(Signal)、封装接口、做校验与转换、写业务计算**。
+> 它**不依赖 DOM、不 import Lit**,因此能在 Node 里直接单测。
+
+## 一、Service 骨架(Signal + 单例)
+
+```js
+// services/auth-service.js
+import { signal, computed } from '@lit-labs/signals';
+import { http } from '../utils/http.js';
+
+class AuthService {
+  /** 私有信号:当前用户(null 表示未登录) */
+  #user = signal(null);
+  /** 私有信号:token,初始化时从 localStorage 恢复 */
+  #token = signal(localStorage.getItem('token') || '');
+
+  /** 对外只读视图 */
+  get user() { return this.#user; }
+  get isLoggedIn() { return computed(() => this.#user.get() != null); }
+
+  async login(username, password) {
+    const { token, user } = await http.post('/api/auth/login', { username, password });
+    this.#token.set(token);
+    this.#user.set(user);
+    localStorage.setItem('token', token);
+    return user;
+  }
+
+  logout() {
+    this.#token.set('');
+    this.#user.set(null);
+    localStorage.removeItem('token');
+  }
+
+  getToken() { return this.#token.get(); }
+}
+
+/** 全局单例:全应用共享同一份状态 */
+export const authService = new AuthService();
+```
+
+要点:
+- 状态用 `signal()` 持有;派生值用 `computed()`;副作用用 `effect()`。
+- 对外通过 getter 暴露**只读** signal,写操作走方法(封装校验)。
+- **单例导出**(`export const xxxService = new XxxService()`),全应用 import 同一实例。
+- 不在 Service 里 `import { html }` 或操作 `document`。
+
+## 二、Signal 三件套
+
+| API | 用途 | 例 |
+|-----|------|----|
+| `signal(v)` | 可读写信号 | `const count = signal(0); count.set(1); count.get();` |
+| `computed(fn)` | 派生只读信号 | `const total = computed(() => items.get().length);` |
+| `effect(fn)` | 副作用(自动追踪依赖) | `effect(() => console.log(count.get()));` |
+
+> 在组件里用 `SignalWatcher(LitElement)` 混入后,`render()` 里**读取** signal 即自动订阅。
+
+## 三、状态放哪(再强调)
+
+| 场景 | 方案 |
+|------|------|
+| 全局业务状态,多页共享(登录 / 字典 / 权限) | **Signal Service**(本文件) + Context 注入 |
+| 组件本地状态(弹窗 / 输入) | Lit `@property` / `@state` |
+| 简单应用不想引 signals | `EventTarget` 简易 Store(下方) |
+| 一次性持久化配置 | `utils/storage.js` 封装 |
+
+## 四、不引 signals 时的简易 Store
+
+```js
+// services/store.js —— 仅当不想引 @lit-labs/signals 时的轻量替代
+export class Store extends EventTarget {
+  #state = {};
+  get state() { return this.#state; }
+  setState(patch) {
+    this.#state = { ...this.#state, ...patch };
+    this.dispatchEvent(new CustomEvent('change', { detail: this.#state }));
+  }
+}
+export const store = new Store();
+```
+
+组件里监听 `store.addEventListener('change', () => this.requestUpdate())`。
+
+## 五、Service 与 DOM 的边界
+
+- Service **可以** 读写 `localStorage`、调 `fetch`、算业务 —— 这些是「环境无关」的。
+- Service **不可以** 持有元素引用、操作 `document`、`import Lit`。
+- 需要通知 UI 的地方 → 改 signal,UI 自然响应,**不要**反向去 call 组件方法。
+
+## 六、可测试性(这是分层的回报)
+
+```js
+// auth-service.test.js(Node 直接跑,无需浏览器)
+import { authService } from './auth-service.js';
+// 依赖 http 可注入 mock;signal 可直接 set/get 断言
+```
+
+- Service 无 DOM 依赖 → 可在 Node / CI 单测。
+- 依赖的外部调用(http)建议通过构造参数注入,便于 mock。
+- 业务计算放纯函数到 `utils/`,Service 只做编排。
+
+## 七、命名与目录
+
+- 文件:`services/<name>-service.js`。
+- 类:`<Name>Service`;单例:`export const <name>Service = new <Name>Service()`。
+- 一个 Service 一个关注点:`auth-service`、`table-service`、`router-service`,不要「万能 service」。

+ 136 - 0
.claude/rules/tabs-keepalive.md

@@ -0,0 +1,136 @@
+# 后台多标签 + KeepAlive
+
+> 同一 `index.html` 内前端路由切换**不刷新页面**;KeepAlive 用 `display:none` 隐藏非激活页容器,
+> **组件实例常驻**,表单 / 内部状态不丢失。对标后台管理系统的多 Tab 工作区。
+
+## 一、核心思路
+
+```
+根组件维护 tabs 数组 → 渲染所有已打开标签页的容器
+每个标签页由一个「路由判断激活」的 display 控制显隐
+激活的标签 display:block,非激活 display:none(组件不销毁)
+```
+
+- **不销毁 = 状态保留**:组件实例一直在 DOM 里,`@state` 里的表单值、滚动位置都在。
+- **不是 `<iframe>` / 不是多页**:单页应用,只是同页多容器。
+
+## 二、tabsService(状态)
+
+```js
+// services/tabs-service.js
+import { signal } from '@lit-labs/signals';
+
+class TabsService {
+  #tabs = signal([
+    { id: 'home', path: '/', title: '首页', closable: false },
+  ]);
+  #activeId = signal('home');
+
+  get tabs() { return this.#tabs; }
+  get activeId() { return this.#activeId; }
+
+  /** 打开(或激活已存在的)标签:同 path 不重复开 */
+  open({ id, path, title, closable = true }) {
+    const list = this.#tabs.get();
+    if (!list.some((t) => t.id === id)) {
+      this.#tabs.set([...list, { id, path, title, closable }]);
+    }
+    this.#activeId.set(id);
+  }
+
+  close(id) {
+    const list = this.#tabs.get().filter((t) => t.id !== id);
+    this.#tabs.set(list);
+    if (this.#activeId.get() === id && list.length) {
+      this.#activeId.set(list[list.length - 1].id);
+    }
+  }
+
+  activate(id) { this.#activeId.set(id); }
+}
+
+export const tabsService = new TabsService();
+```
+
+## 三、tabs-container(渲染 + KeepAlive 容器)
+
+```js
+// components/tabs-container.js
+import { LitElement, html, css } from 'lit';
+import { SignalWatcher } from '@lit-labs/signals';
+import { tabsService } from '../services/tabs-service.js';
+
+export class TabsContainer extends SignalWatcher(LitElement) {
+  static styles = css`
+    .pane { display: none; }
+    .pane.active { display: block; }
+  `;
+
+  render() {
+    const tabs = tabsService.tabs.get();
+    const activeId = tabsService.activeId.get();
+    return html`
+      <!-- 所有已打开标签页的容器常驻;仅激活的显示 -->
+      ${tabs.map((t) => html`
+        <div class="pane ${t.id === activeId ? 'active' : ''}" data-tab="${t.id}">
+          <page-router-view .route=${t.path} .active=${t.id === activeId}></page-router-view>
+        </div>
+      `)}
+    `;
+  }
+}
+```
+
+要点:
+- 用 `display:none/block` 控制显隐,**不要**用条件渲染(`? :` 会销毁实例)。
+- 每个 pane 内容由 `route` 决定渲染哪个页面组件。
+- 页面组件可在 `active` 变化时决定是否暂停轮询 / 动画(省资源)。
+
+## 四、标签栏 UI(tabs-bar)
+
+```js
+// components/tabs-bar.js
+export class TabsBar extends SignalWatcher(LitElement) {
+  render() {
+    return html`
+      <nav>
+        ${tabsService.tabs.get().map((t) => html`
+          <a class=${t.id === tabsService.activeId.get() ? 'on' : ''}
+             @click=${() => tabsService.activate(t.id)}>
+            ${t.title}
+            ${t.closable ? html`<button @click=${(e) => { e.stopPropagation(); tabsService.close(t.id); }}>×</button>` : ''}
+          </a>
+        `)}
+      </nav>
+    `;
+  }
+}
+```
+
+## 五、与路由联动
+
+路由命中新路径时打开/激活对应标签:
+
+```js
+// 在 route change handler
+tabsService.open({
+  id: path,            // 用 path 做唯一 id(也可带参数合成)
+  path,
+  title: routeTitle(path),
+});
+```
+
+- 同 path → 仅激活(不重复开)。
+- 带参数的路由(`/users/:id`)用「实际路径」做 id,`/users/1` 与 `/users/2` 是两个标签。
+
+## 六、KeepAlive 的「刷新」语义
+
+- **刷新单个标签**:给 pane 换一个 `key`(强制重建该子组件)——用 Lit 的 `repeat()` + 改变 key,或临时移除再插回。
+- **关闭标签**:从 `tabs` 移除 → 容器消失 → 实例被 GC,状态释放。
+- **整页刷新**:浏览器级 `location.reload()`,所有标签重置。
+
+## 七、性能与注意
+
+- 标签数量多时,非激活 pane 里的定时器 / 动画应暂停(监听 `active` 属性)。
+- 大列表页面全部常驻会占内存 —— 对「重量级页面」可选用「关闭即销毁 + 重进重建」策略,或对非激活 pane 做懒初始化(首次激活才渲染内容)。
+- 事件在非激活 pane 里仍会冒泡(DOM 存在)——注意别让隐藏 pane 的全局监听重复响应;用 `active` 守卫。

+ 17 - 0
.gitignore

@@ -0,0 +1,17 @@
+# 依赖(仅本地类型提示,不参与部署产物)
+node_modules/
+
+# 构建产物(本方案无构建,预留)
+dist/
+
+# 编辑器 / 系统
+.DS_Store
+.idea/
+.vscode/*
+!.vscode/extensions.json
+*.log
+
+# 本地私有配置(可能含部署凭据,绝不入库)
+.env
+.env.local
+*.local.json

+ 84 - 0
CLAUDE.md

@@ -0,0 +1,84 @@
+# fmode-code-lit — Lit 原生 ESM 无构建开发规则总纲
+
+> 本文件是 AI(Claude Code / FmodeCode)在本仓、或引用本仓规则时的**第一入口**。
+> 写任何 Lit 前端代码前,先读本文件,再按下表索引到具体规则篇。
+> 源稿:`.claude/_lit-dev-manual.md`(用户提供的《Lit 原生 ESM 无构建开发最佳实践》)。
+
+## 0. 三条铁律(违反即返工)
+
+1. **源码即产物**:不引入任何构建/打包器(rollup / esbuild / vite / webpack)。写好的 `.js` 直接上传 OBS,浏览器原生 ESM 执行。
+2. **依赖不落地项目**:`lit`、`@lit-labs/*` 等运行时依赖一律从 **FMODE Repose** 公共 importmap 引入,**禁止**把依赖源码拷进业务仓、禁止业务仓 `import` 裸 `node_modules`。
+3. **分层不可越界**:组件(渲染)→ 服务(业务与状态)→ 工具(无状态函数)。组件禁止直接 `fetch`、禁止写复杂业务逻辑;服务禁止触碰 DOM。
+
+## 1. 规则索引
+
+| # | 规则篇 | 内容 | 何时读 |
+|---|--------|------|--------|
+| 1 | [architecture.md](./.claude/rules/architecture.md) | 整体架构、分层职责、运行流程、部署拓扑 | 起项目 / 定架构 |
+| 2 | [project-structure.md](./.claude/rules/project-structure.md) | 目录规范、命名、文件摆放、二级路径 base | 建目录 / 放文件 |
+| 3 | [components.md](./.claude/rules/components.md) | LitElement 组件规范、Shadow DOM、属性/事件、Slot | 写组件 |
+| 4 | [services.md](./.claude/rules/services.md) | Service 分层、Signal 状态、单例、可单测 | 写服务 / 状态 |
+| 5 | [context-di.md](./.claude/rules/context-di.md) | `@lit-labs/context` 令牌与 provide/consume | 做依赖注入 |
+| 6 | [routing.md](./.claude/rules/routing.md) | `@lit-labs/router` History 路由、守卫、404 | 做路由 |
+| 7 | [tabs-keepalive.md](./.claude/rules/tabs-keepalive.md) | 后台多标签 + KeepAlive(实例常驻) | 做后台壳 |
+| 8 | [http-utils.md](./.claude/rules/http-utils.md) | 原生 Fetch 封装、token 拦截、错误处理、utils | 写请求 / 工具 |
+| 9 | [deployment.md](./.claude/rules/deployment.md) | FMODE Repose 依赖托管 + OBS+CDN 业务部署 + importmap | 上线 / 配依赖 |
+| 10 | [pitfalls.md](./.claude/rules/pitfalls.md) | CORS / 版本冲突 / 302 陷阱 / base href 等坑位 | 排查问题 |
+| 11 | [modules/](./.claude/rules/modules/) | 功能模块 / 通用能力 / 业务模块 划分最佳实践 | 模块拆分 |
+| 12 | [retrospective.md](./docs/retrospective.md) | 对标 Angular zoneless 的技术复盘 | 理解选型理由 |
+
+## 2. 架构一句话
+
+浏览器打开 URL → CDN 回退规则把不存在的路径 **200(非 302)** 返回 `index.html` → `index.html` 加载 **公共 importmap**(指向 FMODE Repose)→ 加载 `app.js` → `import` lit 依赖 → 初始化 Store / 路由 / 根组件 → 渲染。全程**零编译**。
+
+## 3. 默认技术选型
+
+| 关注点 | 选择 | 备注 |
+|--------|------|------|
+| 视图 | Lit 3.x `LitElement` | Shadow DOM 隔离 |
+| 全局状态 | `@lit-labs/signals` + Service 单例 | 跨组件共享 |
+| 组件本地状态 | Lit `@property` / `@state` | 弹窗、输入框 |
+| 依赖注入 | `@lit-labs/context` | 令牌 + provide/consume |
+| 路由 | `@lit-labs/router` | History 干净 URL |
+| 请求 | 原生 `fetch` 薄封装 | 不引 axios |
+| 持久化 | `localStorage` 薄封装 | 一次性配置 |
+| 部署 | OBS + CDN | 无构建直传 |
+
+## 4. 状态放哪?(高频决策)
+
+| 场景 | 方案 |
+|------|------|
+| 全局业务状态,多页共享(登录态、字典、权限) | **Signal Service + Context 注入**(首选) |
+| 组件本地 UI 状态(弹窗开关、表单输入) | Lit `@property` / `@state` |
+| 简单应用不想引 signals | `EventTarget` 简易 Store + Context |
+| 一次性持久化配置 | `localStorage` 封装(`utils/storage.js`) |
+
+> ⚠️ 两套响应式**必须分开**:跨组件全局业务状态用 **Signal**;组件内部 UI 用 **Lit property**。混用会导致更新时机错乱。
+
+## 5. 部署速查
+
+- **依赖** → 同步 npm 原始 ESM 到 **FMODE Repose**(按版本目录化,开 HTTP/2,`.js` 必须 `text/javascript`,CORS 放行业务域名),全业务共享一份公共 importmap。
+- **业务码** → `obsutil` 同步 `src/` 到 OBS(华为云 `obs://www-fmode.cn/<project>/`,南一端点),`public-read`,CDN 指向 `fmode.cn/<project>/`,最后刷新 CDN。
+- **子目录部署必须写 `<base href="/<project>/">`**,否则相对 JS 404。
+- **History 回退**:CDN 配置「不存在路径返回 `index.html` 且状态码 **200**」,**严禁 302/301**(否则地址栏被改写、微信 OAuth 回调失败)。
+
+详见 [deployment.md](./.claude/rules/deployment.md)。
+
+## 6. 命名约定(速记)
+
+- 组件文件:`kebab-case.js`,类名 `PascalCase`,自定义元素标签 `kebab-case`(必须含连字符)
+- 服务文件:`xxx-service.js`,类 `XxxService`,单例导出 `export const xxxService = new XxxService()`
+- 工具文件:`xxx.js`,导出纯函数
+- 页面路由:`routes/page-xxx.js`,标签 `<page-xxx>`
+- 目录一律小写 kebab-case
+
+## 7. 动手清单(新建项目时)
+
+1. `index.html`:`<base>` + `<script type="importmap" src="<公共importmap>">` + `<script type="module" src="./app.js">`
+2. 建 `components/ routes/ services/ utils/`
+3. `app.js`:provide 全局 Service + 挂载 Router + 根组件
+4. 每个 Service 用 Signal 存状态,单例导出
+5. 页面组件 `SignalWatcher(LitElement)` + `consume` Service
+6. 部署脚本:`obsutil sync` + CDN refresh
+
+> 具体脚手架:用全局技能 `fmode-code-lit`(`npx fmode-code-lit@latest workspace`)一键生成。

+ 375 - 0
LICENSE

@@ -0,0 +1,375 @@
+Mozilla Public License Version 2.0
+==================================
+
+Copyright (c) 未来飞马
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in 
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.

+ 71 - 0
README.md

@@ -0,0 +1,71 @@
+# fmode-code-lit · Lit 原生 ESM 无构建开发规则仓
+
+> **未来飞马 — 让 AI 进化提前发生,让 AI 落地快人一步**
+
+`fmode-code-lit` 是 FMODE 组织下的 **前端开发规则仓库**:为「无构建 / 无打包 / 纯原生 ESM + Lit 3.x」的轻量级 Web 应用
+定义一套可被 AI(Claude Code / FmodeCode)直接执行的工程规则、目录范式、组件/服务分层规范与部署规范。
+
+本仓 **不是** 一个前端框架,也 **不是** 一个 npm 库。它是一份 **规则 + 示例 + 脚手架技能** 的组合体:
+
+| 交付物 | 路径 | 作用 |
+|--------|------|------|
+| 规则总纲 | [`CLAUDE.md`](./CLAUDE.md) | AI 进入本仓/引用本仓时的第一入口,索引全部规则 |
+| 分层规则 | [`.claude/rules/`](./.claude/rules/) | 架构、目录、组件、服务、DI、路由、多标签、部署、坑位逐篇规范 |
+| 模块划分 | [`.claude/rules/modules/`](./.claude/rules/modules/) | 功能模块 / 通用能力 / 业务模块 的拆分最佳实践 |
+| 源稿 | [`.claude/_lit-dev-manual.md`](./.claude/_lit-dev-manual.md) | 用户提供的《Lit 原生 ESM 无构建开发最佳实践》原始规范 |
+| 可运行示例 | [`examples/hello-lit/`](./examples/hello-lit/) | 零构建、可直接上 OBS+CDN 的最小多标签后台示例 |
+| 全局技能 | [`skills/fmode-code-lit/`](./skills/fmode-code-lit/) | 给 AI 的 Lit 项目初始化技能(触发词:lit 项目 / 组件 / 无构建前端) |
+| 复盘 | [`docs/retrospective.md`](./docs/retrospective.md) | 纯原生 ESM × Lit 3.x,对标 Angular zoneless |
+
+## 一句话记住它
+
+> **源码即产物。写完的 `.js` 不需要编译,浏览器原生 ESM 直接跑;依赖走 FMODE Repose 统一托管,业务码走 OBS+CDN,保存刷新即预览。**
+
+## 技术底座
+
+- **运行时**:浏览器原生 ESM `<script type="module">` + Import Map
+- **视图**:Lit 3.x(`LitElement` / `html` / `css` / Shadow DOM)
+- **响应式**:`@lit-labs/signals`(跨组件细粒度信号)
+- **依赖注入**:`@lit-labs/context`(组件树 Context)
+- **路由**:`@lit-labs/router`(History 干净 URL,支持微信 OAuth 回调)
+- **分层**:WebComponent(视图) / Service(状态与业务) / Utils(无状态函数)
+- **部署**:FMODE Repose(依赖仓)+ OBS + CDN(业务托管,History 回退 200)
+
+## 快速上手
+
+```bash
+# 1. 用全局技能初始化一个新 Lit 项目
+npx --yes fmode-code-lit@latest workspace   # 或:安装到项目 ./.claude/skills/fmode-code-lit
+
+# 2. 或直接拷贝示例改
+cp -r examples/hello-lit my-admin && cd my-admin
+
+# 3. 部署(无构建,直接同步 src/ 到 OBS+CDN)
+./scripts/deploy.sh my-admin
+```
+
+## 适用 / 不适用
+
+| 适用 | 不适用 |
+|------|--------|
+| 内部中后台、运营后台、活动页、嵌入页 | 需要 SSR / SEO 首屏的重内容站点 |
+| 组件跨项目复用、样式强隔离 | 需要复杂构建期优化(tree-shaking 极致、TS 全量类型)的大团队应用 |
+| 追求「改一行、刷新即见」的快速迭代 | 需要兼容 IE / 老浏览器(Import Map 需 polyfill) |
+| 依赖只是 lit 系 + 少量 ESM 库 | 依赖包含大量 CJS / 需要打包器转译的 npm 包 |
+
+## 目录约定(本仓)
+
+```
+fmode-code-lit/
+├── CLAUDE.md                    # 规则总纲(AI 入口)
+├── .claude/
+│   ├── _lit-dev-manual.md       # 源稿
+│   └── rules/                   # 分层规则(见下)
+├── examples/hello-lit/          # 可运行示例
+├── skills/fmode-code-lit/       # 全局技能源码
+└── docs/retrospective.md        # 技术复盘
+```
+
+## 许可证
+
+MPL-2.0 © 未来飞马 Fmode

+ 85 - 0
docs/retrospective.md

@@ -0,0 +1,85 @@
+# 复盘:纯原生 ESM × Lit 3.x,对标 Angular zoneless
+
+> 本文记录技术选型的**动机、取舍与现代对标**,作为规则背后的「为什么」。
+> 结论先行:**在内部中后台场景,这套无构建方案以更少的心智负担,达成了 Angular zoneless 想达成的目标。**
+
+## 一、我们想要什么(对标面)
+
+新版 Angular 的几个现代卖点,正是内部后台长期缺的东西:
+
+| Angular 现代能力 | 我们要的等价物 |
+|------------------|----------------|
+| **zoneless(无 Zone.js)** | 没有全局猴子补丁,更新精确、可预测 |
+| **Signals** | 细粒度响应式,改一个值只重渲依赖它的地方 |
+| **DI(`inject`)** | 依赖注入,组件树拿到服务 |
+| **Standalone 组件** | 组件即自定义元素,随处 import 即用 |
+| **干净路由(无 hash)** | History 路由,OAuth 友好 |
+| **组件样式隔离** | Shadow DOM 原生隔离 |
+
+## 二、Angular vs 本方案:逐项对照
+
+| 维度 | Angular(Zone + Signals) | Lit 原生 ESM + Signals |
+|------|---------------------------|------------------------|
+| 构建 | 必须 `ng build`(编译 TS/模板) | **无构建**,源码即产物 |
+| 变更检测 | Zone.js 打补丁 → 框架调度;zoneless 后靠 Signal 图 | **无 Zone**,Signal 直接驱动订阅者 |
+| 依赖分发 | 打包进 bundle | Repose 统一托管,一次上传全业务复用 |
+| 组件模型 | 装饰器 + 模板 DSL(框架私有) | **标准 Web Components** + 标准模板字面量 |
+| 样式隔离 | ViewEncapsulation(模拟/Shadow) | **Shadow DOM 原生** |
+| 路由 | `@angular/router` | `@lit-labs/router`(History) |
+| DI | 框架 DI 容器 | `@lit-labs/context`(轻)或直接 import 单例 |
+| 心智模型 | 框架概念多(模块/组件/指令/管道…) | **浏览器原生 API**,概念少 |
+| 升级成本 | 框架版本强绑定 | 只升级 lit 版本号(改 importmap) |
+| 产物透明度 | 打包黑盒 | **无黑盒**,模块即文件 |
+
+## 三、为什么「无 Zone」在这里成立
+
+Angular 需要 Zone.js 是因为它要**拦截所有异步 API**(setTimeout/Promise/XHR/事件),
+在异步完成后触发全局变更检测——代价是全局猴子补丁 + 大范围 check。
+
+本方案的响应式来源是**显式的 Signal**:
+
+- 状态放进 `signal()`;组件读取时订阅;`.set()` 时精确通知**读过的**组件。
+- 没有隐式依赖跟踪全局,没有「任何异步都触发全树 check」。
+- 副作用用 `effect()` 显式声明。
+
+结论:**Signal 本来就是 zoneless 的前提,Lit + signals 是天然的 zoneless 组合**。
+
+## 四、取舍与代价(诚实的部分)
+
+| 得到 | 代价 |
+|------|------|
+| 无构建、保存刷新即见 | 失去 tree-shaking / minify / 代码分割(靠 HTTP/2 + importmap + 缓存缓解) |
+| 依赖不打包 | 浏览器发起的模块请求更多(HTTP/2 多路复用) |
+| 标准 Web Components | 无框架级 TS 模板类型检查(本地 `npm i` 仅为提示) |
+| 概念少 | 需要团队懂原生 ESM / 浏览器语义(如 CORS、MIME、History) |
+| Shadow DOM 隔离 | 全局主题需用 CSS 自定义属性穿透 |
+
+**适用边界**:内部中后台、组件复用、快速迭代——预算充足。重 SEO / SSR 的站点仍应上框架。
+
+## 五、与「传统打包 SPA」的实际收益对比
+
+| 环节 | 传统打包 | 本方案 |
+|------|----------|--------|
+| 改一行代码到看见效果 | 触发 rebuild(秒~分钟级) | **刷新浏览器** |
+| 依赖升级 | 重新 build 全量 | 改 importmap 版本号 |
+| 多项目共享组件 | 各自打包发 npm | Repose 共享,改一处全生效 |
+| 调试 | sourcemap 还原 | **源码即运行的代码**,无映射 |
+| 部署 | 上传 dist | 上传 `src/`(无构建产物) |
+
+## 六、可迁移到其他场景的判断表
+
+| 你的项目 | 建议 |
+|----------|------|
+| 内部工具 / 运营后台 / 数据看板 | ✅ 本方案 |
+| 组件库 / 设计系统 | ✅ 本方案(Web Components 天然跨栈复用) |
+| 活动页 / 嵌入页(体积敏感、迭代快) | ✅ 本方案 |
+| 内容站 / 电商前台(SEO 首屏) | ❌ 用带 SSR 的方案(Astro 等) |
+| 超大型多团队应用,强 TS 类型约束 | ⚠️ 可用,但需团队接受「无构建期类型检查」的约束 |
+
+## 七、经验总结
+
+1. **构建的收益主要在「优化」,不在「能用」**。当网络条件(HTTP/2 + CDN)与缓存补齐后,无构建对内部后台是净收益。
+2. **Signal 是响应式的现代答案**,它让「无 Zone」不是妥协而是更优——Angular 自己也在往这走。
+3. **标准 > 框架私有**:Web Components + 原生 ESM 的产出,十年后仍能跑;框架 DSL 迭代则需跟着升级。
+4. **分层是纪律问题,不是技术问题**:无框架约束时,靠规则(本仓)与评审守住「组件只渲染、服务管业务」。
+5. **部署细节是最大的坑**:CORS / MIME / History 回退 / base href——这些与代码无关,却决定成败,务必进 checklist。

+ 70 - 0
examples/hello-lit/README.md

@@ -0,0 +1,70 @@
+# hello-lit —— 无构建 Lit 示例
+
+最小可运行的多标签后台示例,**零构建 / 零打包**,源码即产物。
+
+## 目录
+
+```
+src/
+  index.html              # <base> + importmap + 入口
+  importmap.json          # 本地开发的 importmap(生产用 Repose 公共 importmap)
+  app.js                  # 装配:provide + 根组件
+  components/             # 通用 UI
+    app-shell.js          # 布局壳(菜单 + 标签 + KeepAlive 内容)
+    side-menu.js
+    tabs-bar.js           # 多标签栏
+    tabs-container.js     # KeepAlive 容器
+    page-view.js          # 按 path 渲染页面
+  modules/                # 功能模块
+    home/page-home.js
+    user/page-user-list.js
+    user/page-user-detail.js
+    user/user-service.js
+    auth/auth-service.js
+    page-404.js
+  services/               # 跨模块共享
+    contexts.js           # 注入令牌
+    tabs-service.js       # 多标签状态(signal)
+  utils/                  # 无状态工具
+    http.js
+    router.js
+    format.js
+```
+
+## 本地运行(两种方式)
+
+**方式 A:指向 Repose(需内网可达)**
+`index.html` 里用 `<script type="importmap" src="/npm/importmap/lit-v3.json">`。起个静态服务器即可:
+
+```bash
+npx --yes serve src
+# 浏览器打开 http://localhost:3000/hello-lit/   (注意 <base href="/hello-lit/">)
+```
+
+**方式 B:本地依赖(离线)**
+
+```bash
+cd examples/hello-lit
+npm install lit @lit-labs/signals @lit-labs/context @lit-labs/router
+# 把 index.html 的 importmap 换成 ./importmap.json,
+# 并把 importmap.json 的路径指向 ./node_modules/...
+npx serve src
+```
+
+> ⚠️ 本地开发注意:`<base href="/hello-lit/">` 要求站点根下有 `/hello-lit/` 前缀。
+> 若直接在 `src/` 根起服务,可把 `<base href>` 临时改为 `./` 并把 index.html 的
+> importmap 换成 `./importmap.json`(相对路径,无需前缀)。
+
+## 演示点
+
+- **无构建**:直接改 `src/` 下任意 `.js`,刷新即见。
+- **分层**:`page-user-list` 只读 `userService.users`(signal)+ 转发点击;逻辑在 service。
+- **Signal 响应式**:`SignalWatcher` 组件读 signal 自动重渲染。
+- **Context**:`app.js` 用 `provide(authContext, authService)`;`page-home` 用 `ContextConsumer` 消费。
+- **History 路由**:`navigate()` 基于 `document.baseURI`,干净 URL。
+- **多标签 KeepAlive**:切到「用户列表」输入筛选词 → 去详情 → 切回,输入**不丢**(实例常驻)。
+- **无装饰器**:全部用 `static properties` + `customElements.define()`。
+
+## 部署
+
+见 [../../.claude/rules/deployment.md](../../.claude/rules/deployment.md) 与 `deploy.sh`。

+ 27 - 0
examples/hello-lit/deploy.sh

@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+# examples/hello-lit/deploy.sh —— 无构建部署:同步 src/ 到 OBS + CDN 刷新
+#
+# 凭据不写死在仓库:AK/SK 由既有部署脚本 / 环境变量提供。
+# 本脚本仅演示命令形态,实际发布请复用项目既有 deploy 脚本。
+set -euo pipefail
+
+PROJECT="${1:-hello-lit}"
+BUCKET="${OBS_BUCKET:?需设置 OBS_BUCKET,例如 obs://www-fmode.cn}"
+EP="${OBS_ENDPOINT:-obs.cn-south-1.myhuaweicloud.com}"
+OBS="${OBSUTIL:-/opt/data/bin/obsutil}"
+SRC="$(cd "$(dirname "$0")" && pwd)/src"
+
+echo "==> 同步 $SRC -> $BUCKET/$PROJECT/"
+
+# 1) index.html 先删后传(sync 按 size+mtime 判 Skip,index.html 内容常变)
+"$OBS" rm "$BUCKET/$PROJECT/index.html" -e="$EP" -f >/dev/null 2>&1 || true
+"$OBS" cp "$SRC/index.html" "$BUCKET/$PROJECT/index.html" \
+  -e="$EP" -acl=public-read -f \
+  -meta="Cache-Control:no-cache#Content-Type:text/html; charset=utf-8"
+
+# 2) 其余源码同步(无构建:src/ 即产物)
+"$OBS" sync "$SRC/" "$BUCKET/$PROJECT/" -e="$EP" -acl=public-read -f
+
+echo "==> 刷新 CDN(调用既有刷新脚本,凭据由该脚本/环境提供)"
+# node <既有刷新脚本> "https://<cdn-domain>/$PROJECT/"
+echo "完成。请确认 CDN 回退规则:不存在路径 200 返回 index.html(禁 302)。"

+ 17 - 0
examples/hello-lit/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "hello-lit",
+  "private": true,
+  "type": "module",
+  "description": "fmode-code-lit 无构建 Lit 示例(源码即产物,无需 build)",
+  "_comment": "devDependencies 仅用于本地 IDE 类型提示与离线运行,不参与部署产物。",
+  "scripts": {
+    "serve": "npx --yes serve src",
+    "deploy": "bash deploy.sh"
+  },
+  "devDependencies": {
+    "lit": "^3.2.1",
+    "@lit-labs/signals": "^0.1.3",
+    "@lit-labs/context": "^0.5.1",
+    "@lit-labs/router": "^0.2.0"
+  }
+}

+ 26 - 0
examples/hello-lit/src/app.js

@@ -0,0 +1,26 @@
+/**
+ * app.js —— 应用装配入口
+ * 职责:provide 全局服务 + 挂载路由 + 渲染根组件。不含业务逻辑。
+ */
+import { LitElement, html, css } from 'lit';
+import { provide } from '@lit/context';
+import { authContext } from './services/contexts.js';
+import { authService } from './modules/auth/auth-service.js';
+
+import './components/app-shell.js';
+
+export class AppRoot extends LitElement {
+  static styles = css`
+    :host { display: block; background: var(--bg); }
+  `;
+
+  render() {
+    return html`
+      <!-- 全局注入:整棵组件树可 consume -->
+      ${provide(authContext, authService)}
+      <app-shell></app-shell>
+    `;
+  }
+}
+
+customElements.define('app-root', AppRoot);

+ 73 - 0
examples/hello-lit/src/components/app-shell.js

@@ -0,0 +1,73 @@
+/**
+ * components/app-shell.js —— 布局壳:侧边菜单 + 多标签栏 + KeepAlive 内容区。
+ *
+ * 关键:内容区不用 router.outlet()(每次路由切换会销毁重建),
+ * 而是交给 <tabs-container> 渲染「所有已打开标签」的常驻容器,
+ * 路由只负责「打开/激活标签」。
+ */
+import { LitElement, html, css } from 'lit';
+import { Router } from '@lit-labs/router';
+import { SignalWatcher } from '@lit-labs/signals';
+import { tabsService } from '../services/tabs-service.js';
+import { currentPath } from '../utils/router.js';
+
+import './side-menu.js';
+import './tabs-bar.js';
+import './tabs-container.js';
+
+/** 路由表:path 模式 → 标签标题(供打开标签时命名) */
+const ROUTES = [
+  { pattern: '/', title: '首页' },
+  { pattern: '/users', title: '用户列表' },
+  { pattern: '/users/:id', title: '用户详情' },
+];
+
+export class AppShell extends SignalWatcher(LitElement) {
+  static styles = css`
+    :host { display: grid; grid-template-columns: 200px 1fr; min-height: 100vh; }
+    aside { border-right: 1px solid var(--border); padding: 12px; background: #fff; }
+    main { display: flex; flex-direction: column; min-width: 0; }
+  `;
+
+  /** Router 仅用于 history 集成与 popstate 派发;渲染出口不用它 */
+  #router = new Router(this, [{ path: '/*', render: () => html`` }]);
+
+  #onPop = () => this.#syncTabWithRoute();
+
+  connectedCallback() {
+    super.connectedCallback();
+    window.addEventListener('popstate', this.#onPop);
+    this.#syncTabWithRoute();
+  }
+
+  disconnectedCallback() {
+    window.removeEventListener('popstate', this.#onPop);
+    super.disconnectedCallback();
+  }
+
+  /** 路由变化 → 打开/激活标签(已存在则仅激活) */
+  #syncTabWithRoute() {
+    const path = currentPath();
+    const title = this.#titleFor(path);
+    tabsService.open({ id: path, path, title });
+  }
+
+  #titleFor(path) {
+    if (path === '/') return '首页';
+    if (path === '/users') return '用户列表';
+    if (path.startsWith('/users/')) return `用户详情 ${path.split('/').pop()}`;
+    return '页面';
+  }
+
+  render() {
+    return html`
+      <aside><side-menu></side-menu></aside>
+      <main>
+        <tabs-bar></tabs-bar>
+        <tabs-container></tabs-container>
+      </main>
+    `;
+  }
+}
+
+customElements.define('app-shell', AppShell);

+ 36 - 0
examples/hello-lit/src/components/page-view.js

@@ -0,0 +1,36 @@
+/**
+ * components/page-view.js —— 按 path 渲染对应页面组件。
+ * 每个 pane 一个实例;实例常驻(KeepAlive),状态不丢。
+ * 页面组件从各功能模块引入。
+ */
+import { LitElement, html } from 'lit';
+
+import '../modules/home/page-home.js';
+import '../modules/user/page-user-list.js';
+import '../modules/user/page-user-detail.js';
+import '../modules/page-404.js';
+
+export class PageView extends LitElement {
+  static properties = {
+    path: { type: String },
+  };
+
+  /** 跳过 Shadow DOM,让页面组件直接参与布局(页面自带样式) */
+  createRenderRoot() { return this; }
+
+  /**
+   * 每个 pane 的 path 固定不变,故此处条件渲染是稳定的:
+   * 一个 pane 对应一个页面实例,随 pane 一起常驻(KeepAlive)。
+   */
+  render() {
+    const p = this.path || '/';
+    if (p === '/') return html`<page-home></page-home>`;
+    if (p === '/users') return html`<page-user-list></page-user-list>`;
+    if (p.startsWith('/users/')) {
+      return html`<page-user-detail .userId=${p.split('/').pop()}></page-user-detail>`;
+    }
+    return html`<page-404></page-404>`;
+  }
+}
+
+customElements.define('page-view', PageView);

+ 24 - 0
examples/hello-lit/src/components/side-menu.js

@@ -0,0 +1,24 @@
+/**
+ * components/side-menu.js —— 侧边菜单;只导航,不进业务。
+ */
+import { LitElement, html, css } from 'lit';
+import { navigate } from '../utils/router.js';
+
+export class SideMenu extends LitElement {
+  static styles = css`
+    :host { display: block; }
+    h1 { font-size: 15px; margin: 0 0 12px; color: var(--brand); }
+    a { display: block; padding: 8px 10px; border-radius: 6px; cursor: pointer; font-size: 14px; }
+    a:hover { background: var(--bg); }
+  `;
+
+  render() {
+    return html`
+      <h1>Hello Lit</h1>
+      <a @click=${() => navigate('/')}>首页</a>
+      <a @click=${() => navigate('/users')}>用户列表</a>
+    `;
+  }
+}
+
+customElements.define('side-menu', SideMenu);

+ 33 - 0
examples/hello-lit/src/components/tabs-bar.js

@@ -0,0 +1,33 @@
+/**
+ * components/tabs-bar.js —— 多标签栏:激活 / 关闭。
+ */
+import { LitElement, html, css } from 'lit';
+import { SignalWatcher } from '@lit-labs/signals';
+import { tabsService } from '../services/tabs-service.js';
+import { navigate } from '../utils/router.js';
+
+export class TabsBar extends SignalWatcher(LitElement) {
+  static styles = css`
+    :host { display: flex; gap: 4px; padding: 8px 12px; border-bottom: 1px solid var(--border); background: #fff; }
+    .tab { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-size: 13px; }
+    .tab.on { border-color: var(--brand); color: var(--brand); }
+    button { border: 0; background: none; cursor: pointer; color: #999; }
+  `;
+
+  render() {
+    const activeId = tabsService.activeId.get();
+    return html`
+      ${tabsService.tabs.get().map((t) => html`
+        <span class="tab ${t.id === activeId ? 'on' : ''}"
+              @click=${() => { tabsService.activate(t.id); navigate(t.path); }}>
+          ${t.title}
+          ${t.closable
+            ? html`<button @click=${(e) => { e.stopPropagation(); tabsService.close(t.id); }}>×</button>`
+            : ''}
+        </span>
+      `)}
+    `;
+  }
+}
+
+customElements.define('tabs-bar', TabsBar);

+ 30 - 0
examples/hello-lit/src/components/tabs-container.js

@@ -0,0 +1,30 @@
+/**
+ * components/tabs-container.js —— KeepAlive 容器。
+ * 渲染所有已打开标签的 pane,仅激活的 display:block,其余 display:none(实例常驻)。
+ */
+import { LitElement, html, css } from 'lit';
+import { SignalWatcher } from '@lit-labs/signals';
+import { tabsService } from '../services/tabs-service.js';
+import './page-view.js';   // <page-view> 由本容器渲染,须在此注册
+
+export class TabsContainer extends SignalWatcher(LitElement) {
+  static styles = css`
+    :host { display: block; padding: 16px; }
+    .pane { display: none; }
+    .pane.active { display: block; }
+  `;
+
+  render() {
+    const tabs = tabsService.tabs.get();
+    const activeId = tabsService.activeId.get();
+    return html`
+      ${tabs.map((t) => html`
+        <div class="pane ${t.id === activeId ? 'active' : ''}">
+          <page-view .path=${t.path}></page-view>
+        </div>
+      `)}
+    `;
+  }
+}
+
+customElements.define('tabs-container', TabsContainer);

+ 18 - 0
examples/hello-lit/src/importmap.json

@@ -0,0 +1,18 @@
+{
+  "_comment": "本地开发用 importmap。生产环境由 FMODE Repose 的 /npm/importmap/lit-v3.json 统一提供。版本以实际同步到 Repose 的为准。",
+  "imports": {
+    "lit": "/npm/lit/v3.3.3/lit.js",
+    "lit/": "/npm/lit/v3.3.3/",
+    "lit-html": "/npm/lit-html/v3.3.3/lit-html.js",
+    "lit-html/": "/npm/lit-html/v3.3.3/",
+    "lit-element": "/npm/lit-element/v4.2.2/lit-element.js",
+    "lit-element/": "/npm/lit-element/v4.2.2/",
+    "@lit/reactive-element": "/npm/@lit/reactive-element/v2.1.2/reactive-element.js",
+    "@lit/reactive-element/": "/npm/@lit/reactive-element/v2.1.2/",
+    "@lit/context": "/npm/@lit/context/v1.1.6/index.js",
+    "@lit-labs/context": "/npm/@lit/context/v1.1.6/index.js",
+    "@lit-labs/signals": "/npm/@lit-labs/signals/v0.3.0/index.js",
+    "signal-polyfill": "/npm/signal-polyfill/v0.2.2/dist/index.js",
+    "@lit-labs/router": "/npm/@lit-labs/router/v0.1.4/index.js"
+  }
+}

+ 30 - 0
examples/hello-lit/src/index.html

@@ -0,0 +1,30 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="utf-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1" />
+  <title>Hello Lit · fmode-code-lit 示例</title>
+
+  <!-- 二级子路径部署必须:否则 ./app.js 等相对路径 404 -->
+  <base href="/hello-lit/" />
+
+  <!--
+    公共 importmap 托管在 FMODE Repose,全业务复用。
+    本地开发可临时换成同目录的 ./importmap.json(见下方注释)。
+  -->
+  <script type="importmap" src="/npm/importmap/lit-v3.json"></script>
+  <!-- 本地无 Repose 时改用: <script type="importmap" src="./importmap.json"></script> -->
+
+  <style>
+    :root { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
+    body { margin: 0; }
+    app-root { display: block; min-height: 100vh; }
+    /* 全局主题可通过 CSS 自定义属性穿透 Shadow DOM */
+    :root { --brand: #2b6cb0; --bg: #f7fafc; --border: #e2e8f0; }
+  </style>
+</head>
+<body>
+  <app-root></app-root>
+  <script type="module" src="./app.js"></script>
+</body>
+</html>

+ 30 - 0
examples/hello-lit/src/modules/auth/auth-service.js

@@ -0,0 +1,30 @@
+/**
+ * modules/auth/auth-service.js —— 全局登录态(Signal Service)。
+ * 纯 ESM Class,无 DOM 依赖,可在 Node 单测。
+ */
+import { signal, computed } from '@lit-labs/signals';
+
+class AuthService {
+  #user = signal(null);
+  #token = signal(localStorage.getItem('token') || '');
+
+  get user() { return this.#user; }
+  get isLoggedIn() { return computed(() => this.#user.get() != null); }
+  getToken() { return this.#token.get(); }
+
+  /** 示例:本地模拟登录(真实项目走 http.post) */
+  login(name = 'Demo User') {
+    const token = 'demo-token';
+    this.#token.set(token);
+    this.#user.set({ name, roles: ['admin'] });
+    localStorage.setItem('token', token);
+  }
+
+  logout() {
+    this.#token.set('');
+    this.#user.set(null);
+    localStorage.removeItem('token');
+  }
+}
+
+export const authService = new AuthService();

+ 46 - 0
examples/hello-lit/src/modules/home/page-home.js

@@ -0,0 +1,46 @@
+/**
+ * modules/home/page-home.js —— 首页。
+ * 演示:SignalWatcher 读全局 signal + 通过 ContextConsumer 消费注入的服务。
+ *
+ * ⚠️ 无构建环境下禁用装饰器语法(@consume/@state 需编译转换),
+ *    一律用 static properties + 编程式 API。详见 rules/components.md。
+ */
+import { LitElement, html, css } from 'lit';
+import { SignalWatcher } from '@lit-labs/signals';
+import { ContextConsumer } from '@lit/context';
+import { authContext } from '../../services/contexts.js';
+
+export class PageHome extends SignalWatcher(LitElement) {
+  static properties = {
+    /** 注入的 AuthService 实例;用 state 属性触发重渲染 */
+    _auth: { state: true },
+  };
+
+  #authConsumer = new ContextConsumer(this, {
+    context: authContext,
+    subscribe: true,
+    // 编程式消费:拿到的实例存入 state 属性,赋值即触发重渲染
+    callback: (v) => { this._auth = v; },
+  });
+
+  static styles = css`
+    :host { display: block; }
+    h2 { margin-top: 0; }
+    .card { background: #fff; border: 1px solid var(--border); border-radius: 8px; padding: 16px; max-width: 560px; }
+    code { background: var(--bg); padding: 1px 5px; border-radius: 4px; }
+  `;
+
+  render() {
+    const user = this._auth?.user.get();
+    return html`
+      <h2>首页</h2>
+      <div class="card">
+        <p>欢迎,<b>${user ? user.name : '访客'}</b>。</p>
+        <p>这是 <code>fmode-code-lit</code> 的无构建示例:源码即产物,刷新即见。</p>
+        <p>切到「用户列表」后回来,输入框内容与状态仍保留(KeepAlive)。</p>
+      </div>
+    `;
+  }
+}
+
+customElements.define('page-home', PageHome);

+ 22 - 0
examples/hello-lit/src/modules/page-404.js

@@ -0,0 +1,22 @@
+/**
+ * modules/page-404.js —— 兜底页。
+ */
+import { LitElement, html, css } from 'lit';
+import { navigate } from '../utils/router.js';
+
+export class Page404 extends LitElement {
+  static styles = css`
+    :host { display: block; padding: 24px; }
+    button { margin-top: 12px; padding: 6px 12px; border-radius: 6px; border: 1px solid var(--border); cursor: pointer; }
+  `;
+
+  render() {
+    return html`
+      <h2>404</h2>
+      <p>页面不存在。</p>
+      <button @click=${() => navigate('/')}>回首页</button>
+    `;
+  }
+}
+
+customElements.define('page-404', Page404);

+ 35 - 0
examples/hello-lit/src/modules/user/page-user-detail.js

@@ -0,0 +1,35 @@
+/**
+ * modules/user/page-user-detail.js —— 用户详情页。演示:入参 userId 来自路由。
+ */
+import { LitElement, html, css } from 'lit';
+import { SignalWatcher } from '@lit-labs/signals';
+import { userService } from './user-service.js';
+import { navigate } from '../../utils/router.js';
+
+export class PageUserDetail extends SignalWatcher(LitElement) {
+  static properties = {
+    userId: { type: String },
+  };
+
+  static styles = css`
+    :host { display: block; }
+    .card { background: #fff; border: 1px solid var(--border); border-radius: 8px; padding: 16px; max-width: 420px; }
+    button { margin-top: 12px; padding: 6px 12px; border-radius: 6px; border: 1px solid var(--border); cursor: pointer; }
+  `;
+
+  render() {
+    const u = userService.findById(this.userId);
+    if (!u) return html`<h2>用户详情</h2><p>未找到用户 ${this.userId}。</p>`;
+    return html`
+      <h2>用户详情</h2>
+      <div class="card">
+        <p>ID:${u.id}</p>
+        <p>姓名:${u.name}</p>
+        <p>角色:${u.role}</p>
+        <button @click=${() => navigate('/users')}>返回列表</button>
+      </div>
+    `;
+  }
+}
+
+customElements.define('page-user-detail', PageUserDetail);

+ 53 - 0
examples/hello-lit/src/modules/user/page-user-list.js

@@ -0,0 +1,53 @@
+/**
+ * modules/user/page-user-list.js —— 用户列表页。
+ * 演示:读模块 service 的 signal;点击行 → 路由跳详情;本地筛选框演示 KeepAlive 保状态。
+ */
+import { LitElement, html, css } from 'lit';
+import { SignalWatcher } from '@lit-labs/signals';
+import { userService } from './user-service.js';
+import { navigate } from '../../utils/router.js';
+
+export class PageUserList extends SignalWatcher(LitElement) {
+  static properties = {
+    /** 组件本地 UI 状态:用 Lit property/state,不用全局 signal(无构建下无装饰器) */
+    keyword: { type: String, state: true },
+  };
+
+  constructor() {
+    super();
+    this.keyword = '';
+  }
+
+  static styles = css`
+    :host { display: block; }
+    input { padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; margin-bottom: 12px; }
+    table { border-collapse: collapse; width: 100%; max-width: 560px; background: #fff; }
+    th, td { border: 1px solid var(--border); padding: 8px 10px; text-align: left; font-size: 14px; }
+    tr { cursor: pointer; }
+    tr:hover td { background: var(--bg); }
+  `;
+
+  render() {
+    const kw = this.keyword.trim().toLowerCase();
+    const list = userService.users.get().filter((u) => !kw || u.name.toLowerCase().includes(kw));
+    return html`
+      <h2>用户列表</h2>
+      <input placeholder="按姓名筛选…"
+             .value=${this.keyword}
+             @input=${(e) => { this.keyword = e.target.value; }} />
+      <table>
+        <thead><tr><th>ID</th><th>姓名</th><th>角色</th></tr></thead>
+        <tbody>
+          ${list.map((u) => html`
+            <tr @click=${() => navigate('/users/' + u.id)}>
+              <td>${u.id}</td><td>${u.name}</td><td>${u.role}</td>
+            </tr>
+          `)}
+        </tbody>
+      </table>
+      ${list.length === 0 ? html`<p>无匹配用户。</p>` : ''}
+    `;
+  }
+}
+
+customElements.define('page-user-list', PageUserList);

+ 30 - 0
examples/hello-lit/src/modules/user/user-service.js

@@ -0,0 +1,30 @@
+/**
+ * modules/user/user-service.js —— 用户模块服务。
+ * 真实项目用 http;示例用内存数据,保持零后端可跑。
+ */
+import { signal } from '@lit-labs/signals';
+
+class UserService {
+  #users = signal([
+    { id: 1, name: '张三', role: 'admin' },
+    { id: 2, name: '李四', role: 'editor' },
+    { id: 3, name: '王五', role: 'viewer' },
+  ]);
+
+  get users() { return this.#users; }
+
+  findById(id) {
+    return this.#users.get().find((u) => String(u.id) === String(id)) ?? null;
+  }
+
+  async fetchList() {
+    // 真实项目:return (await http.get('/users')).items;
+    return this.#users.get();
+  }
+
+  update(id, patch) {
+    this.#users.set(this.#users.get().map((u) => (String(u.id) === String(id) ? { ...u, ...patch } : u)));
+  }
+}
+
+export const userService = new UserService();

+ 7 - 0
examples/hello-lit/src/services/contexts.js

@@ -0,0 +1,7 @@
+/**
+ * services/contexts.js —— 注入令牌集中定义
+ * 令牌必须同一模块实例;散落定义会导致 consume 拿不到值。
+ */
+import { createContext } from '@lit/context';
+
+export const authContext = createContext('hello.auth');

+ 34 - 0
examples/hello-lit/src/services/tabs-service.js

@@ -0,0 +1,34 @@
+/**
+ * services/tabs-service.js —— 多标签 + KeepAlive 的状态。
+ * 用 signal 持有标签数组与激活项,容器按此渲染并 display 切换。
+ */
+import { signal } from '@lit-labs/signals';
+
+class TabsService {
+  #tabs = signal([{ id: '/', path: '/', title: '首页', closable: false }]);
+  #activeId = signal('/');
+
+  get tabs() { return this.#tabs; }
+  get activeId() { return this.#activeId; }
+
+  /** 打开或激活标签:同 id 不重复开 */
+  open({ id, path, title, closable = true }) {
+    const list = this.#tabs.get();
+    if (!list.some((t) => t.id === id)) {
+      this.#tabs.set([...list, { id, path, title, closable }]);
+    }
+    this.#activeId.set(id);
+  }
+
+  activate(id) { this.#activeId.set(id); }
+
+  close(id) {
+    const list = this.#tabs.get().filter((t) => t.id !== id);
+    this.#tabs.set(list);
+    if (this.#activeId.get() === id && list.length) {
+      this.#activeId.set(list[list.length - 1].id);
+    }
+  }
+}
+
+export const tabsService = new TabsService();

+ 58 - 0
examples/hello-lit/src/smoke.html

@@ -0,0 +1,58 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="utf-8" />
+  <title>hello-lit · 无构建自检</title>
+  <style>body{font-family:system-ui;padding:20px}#out{white-space:pre-wrap;font-family:ui-monospace,monospace;font-size:13px}.ok{color:#2f855a}.bad{color:#c53030}</style>
+</head>
+<body>
+  <h1>hello-lit 自检</h1>
+  <p>验证依赖解析、信号、Context、路由、多标签容器是否可用。</p>
+  <div id="out">running…</div>
+
+  <!-- 与 index.html 相同的 importmap 来源;本地可切到 ./importmap.json -->
+  <script type="importmap" src="./importmap.json"></script>
+  <script type="module">
+    const out = document.getElementById('out');
+    let ok = 0, bad = 0;
+    const line = (s, good) => { ok += good ? 1 : 0; bad += good ? 0 : 1;
+      out.innerHTML += `\n<span class="${good ? 'ok' : 'bad'}">${good ? '✓' : '✗'} ${s}</span>`; };
+
+    try {
+      const { signal, computed, SignalWatcher } = await import('@lit-labs/signals');
+      line('@lit-labs/signals 解析', !!signal && !!computed && !!SignalWatcher);
+      const { provide, createContext, ContextConsumer } = await import('@lit/context');
+      line('@lit/context 解析', !!provide && !!createContext && !!ContextConsumer);
+      const { Router } = await import('@lit-labs/router');
+      line('@lit-labs/router 解析', !!Router);
+      const { LitElement, html } = await import('lit');
+      line('lit 解析', !!LitElement && !!html);
+
+      // 信号响应
+      const n = signal(0); const dbl = computed(() => n.get() * 2);
+      n.set(21); line('signal/computed 生效', dbl.get() === 42);
+
+      // 应用模块(含 Signal/Context 用法)
+      const { authService } = await import('./modules/auth/auth-service.js');
+      const { userService } = await import('./modules/user/user-service.js');
+      const { tabsService } = await import('./services/tabs-service.js');
+      authService.login('自检'); line('authService 登录', authService.isLoggedIn.get() === true);
+      authService.logout();
+      line('userService 读取', userService.users.get().length === 3);
+      tabsService.open({ id: '/users', path: '/users', title: '用户列表' });
+      line('tabsService 多标签', tabsService.tabs.get().some((t) => t.id === '/users'));
+
+      // 组件注册
+      await import('./components/app-shell.js');
+      line('自定义元素 <app-shell> 已注册', !!customElements.get('app-shell'));
+      await import('./components/tabs-container.js');
+      line('自定义元素 <tabs-container> 已注册', !!customElements.get('tabs-container'));
+      await import('./modules/user/page-user-list.js');
+      line('自定义元素 <page-user-list> 已注册', !!customElements.get('page-user-list'));
+    } catch (e) {
+      line('异常:' + e.message, false);
+    }
+    out.innerHTML += `\n\n合计:${ok} 通过 / ${bad} 失败`;
+  </script>
+</body>
+</html>

+ 16 - 0
examples/hello-lit/src/utils/format.js

@@ -0,0 +1,16 @@
+/**
+ * utils/format.js —— 纯无状态格式化函数。
+ */
+export const formatMoney = (n, currency = '¥') => `${currency}${Number(n ?? 0).toFixed(2)}`;
+
+export const formatDate = (d) => {
+  const dt = d instanceof Date ? d : new Date(d);
+  if (Number.isNaN(dt.getTime())) return '';
+  const p = (x) => String(x).padStart(2, '0');
+  return `${dt.getFullYear()}-${p(dt.getMonth() + 1)}-${p(dt.getDate())}`;
+};
+
+export const debounce = (fn, ms = 200) => {
+  let t;
+  return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
+};

+ 52 - 0
examples/hello-lit/src/utils/http.js

@@ -0,0 +1,52 @@
+/**
+ * utils/http.js —— 原生 fetch 薄封装。
+ * 无状态;token 通过 configureHttp 注入,避免 utils 反向依赖 services。
+ */
+let getToken = () => '';
+let onUnauthorized = () => {};
+
+/** 装配时由 app 调用,注入 token 获取与 401 处理 */
+export function configureHttp({ token, unauthorized } = {}) {
+  if (token) getToken = token;
+  if (unauthorized) onUnauthorized = unauthorized;
+}
+
+export class HttpError extends Error {
+  constructor(status, body) {
+    super(`HTTP ${status}`);
+    this.status = status;
+    this.body = body;
+  }
+}
+
+const BASE = '/api';
+
+async function request(method, path, { body, headers = {}, query } = {}) {
+  let url = BASE + path;
+  if (query) url += '?' + new URLSearchParams(query).toString();
+
+  const h = { 'Content-Type': 'application/json', ...headers };
+  const token = getToken();
+  if (token) h.Authorization = `Bearer ${token}`;
+
+  const res = await fetch(url, {
+    method,
+    headers: h,
+    body: body != null ? JSON.stringify(body) : undefined,
+    credentials: 'same-origin',
+  });
+
+  if (res.status === 401) { onUnauthorized(); throw new HttpError(401, null); }
+
+  const ct = res.headers.get('content-type') || '';
+  const data = ct.includes('application/json') ? await res.json() : await res.text();
+  if (!res.ok) throw new HttpError(res.status, data);
+  return data;
+}
+
+export const http = {
+  get: (p, o) => request('GET', p, o),
+  post: (p, body, o) => request('POST', p, { ...o, body }),
+  put: (p, body, o) => request('PUT', p, { ...o, body }),
+  del: (p, o) => request('DELETE', p, o),
+};

+ 15 - 0
examples/hello-lit/src/utils/router.js

@@ -0,0 +1,15 @@
+/**
+ * utils/router.js —— 基于 document.baseURI 的导航,尊重 <base href>。
+ */
+export function navigate(path, { replace = false } = {}) {
+  const url = new URL(path, document.baseURI);
+  if (replace) history.replaceState({}, '', url);
+  else history.pushState({}, '', url);
+  window.dispatchEvent(new PopStateEvent('popstate'));
+}
+
+/** 当前路径(去掉 base 前缀,供路由匹配) */
+export function currentPath() {
+  const base = new URL(document.baseURI).pathname.replace(/\/$/, '');
+  return location.pathname.replace(base, '') || '/';
+}

+ 13 - 0
package.json

@@ -0,0 +1,13 @@
+{
+  "name": "fmode-code-lit-rules",
+  "private": true,
+  "type": "module",
+  "description": "fmode-code-lit 规则仓:Lit 原生 ESM 无构建前端开发规范。本仓不含运行时依赖(源码即产物)。",
+  "_comment": "此 package.json 仅用于本地工具链与示例;规则仓本体不被打包。",
+  "scripts": {
+    "smoke": "node skills/fmode-code-lit/scripts/smoke.mjs",
+    "install-skill": "node skills/fmode-code-lit/scripts/install.mjs install --force",
+    "serve-example": "npx --yes serve examples/hello-lit/src"
+  },
+  "license": "MPL-2.0"
+}

+ 109 - 0
skills/fmode-code-lit/README.md

@@ -0,0 +1,109 @@
+# fmode-code-lit · Lit 原生 ESM 无构建前端技能
+
+> **未来飞马 — 让 AI 进化提前发生,让 AI 落地快人一步**
+
+[![License: MPL-2.0](https://img.shields.io/badge/License-MPL--2.0-brightgreen.svg)](LICENSE)
+[![ESM](https://img.shields.io/badge/module-native--ESM-orange.svg)](#)
+
+---
+
+## 简介
+
+`fmode-code-lit` 是教 AI 用 **无构建 / 无打包 / 纯原生 ESM + Lit 3.x** 写前端的 Claude Code 技能:
+源码即产物,浏览器直接跑;依赖走 **FMODE Repose** 统一托管,业务码走 **OBS + CDN**。
+
+配套 **规则仓**(本仓 `.claude/rules/`)提供分层、组件、服务、DI、路由、多标签、部署等完整规范。
+
+适用 **FmodeCode / Claude Code / FmodeAgent**;本技能以原生 ESM 与零依赖脚本交付,Node ≥ 18 直接运行。
+
+## 核心能力
+
+- 🏗 **项目脚手架** —— 一键生成无构建 Lit 项目骨架(index.html + importmap + 分层目录)
+- 🧩 **单元生成器** —— `component` / `service` / `page` 按规范生成(**无装饰器**)
+- 📏 **规则安装器** —— 把 `.claude/rules/` 前端规则装入任意项目
+- 🚀 **部署模板** —— `obsutil` 同步 OBS + CDN 刷新的脚本
+- 📚 **完整规则仓** —— 架构 / 组件 / 服务 / DI / 路由 / 多标签 / 部署 / 坑位
+
+## 快速开始
+
+### 1) 给项目装技能 + 规则
+
+```bash
+# 装到当前项目的 .claude/skills/(随项目走,推荐)
+npx --yes fmode-code-lit@latest workspace
+
+# 或装到用户级 ~/.claude/skills/(全局)
+npx --yes fmode-code-lit@latest install
+
+# 把前端规则写入项目 ./.claude/rules/
+npx --yes fmode-code-lit@latest rules .
+```
+
+### 2) 新建无构建 Lit 项目
+
+```bash
+npx --yes fmode-code-lit@latest scaffold ./my-admin --project my-admin
+cd my-admin && npx serve src      # 本地预览
+```
+
+### 3) 生成组件 / 服务 / 页面
+
+```bash
+npx --yes fmode-code-lit@latest new component user-card
+npx --yes fmode-code-lit@latest new service auth-service
+npx --yes fmode-code-lit@latest new page page-order-list
+```
+
+### 4) 部署
+
+```bash
+OBS_BUCKET=obs://<bucket> bash scripts/deploy.sh my-admin
+```
+
+## 三条铁律
+
+1. **源码即产物** —— 不引入任何打包器,写好的 `.js` 直接上传。
+2. **依赖不落地项目** —— `lit` / `@lit/*` / `@lit-labs/*` 由 importmap 指向 Repose。
+3. **分层不越界** —— 组件(渲染)→ 服务(业务/状态)→ 工具(纯函数)。
+
+## ⚠️ 无构建硬约束:禁用装饰器
+
+浏览器**原生不执行**装饰器语法(`@customElement` / `@property` / `@state` / `@consume`)。
+一律用 `static properties` + `customElements.define()` + `ContextConsumer`。本技能生成的所有代码均无装饰器。
+
+## 依赖版本(2026-10 实测)
+
+```
+lit@3.3.3  lit-html@3.3.3  lit-element@4.2.2  @lit/reactive-element@2.1.2
+@lit/context@1.1.6  @lit-labs/signals@0.3.0  signal-polyfill@0.2.2  @lit-labs/router@0.1.4
+```
+
+> `@lit-labs/signals` 依赖 `signal-polyfill`(importmap 需映射,入口 `dist/index.js`)。
+> `@lit-labs/context` 已废弃 → 用 `@lit/context`。
+
+## 目录
+
+```
+skills/fmode-code-lit/
+├── SKILL.md                     # 技能说明(AI 读取)
+├── skill-package-manifest.json  # 打包清单
+├── package.json                 # npx 入口
+├── bin/fmode-code-lit.js        # CLI
+└── scripts/
+    ├── scaffold.mjs             # 项目脚手架
+    ├── new.mjs                  # 组件/服务/页面生成器
+    ├── install-rules.mjs        # 规则安装器
+    ├── install.mjs              # 技能安装器
+    └── smoke.mjs                # 自检
+```
+
+## 自检
+
+```bash
+node skills/fmode-code-lit/scripts/smoke.mjs
+# ✓ 生成物全部通过语法检查 / ✓ 生成物无装饰器语法 / … → SMOKE OK
+```
+
+## 许可
+
+MPL-2.0 © 未来飞马 Fmode

+ 151 - 0
skills/fmode-code-lit/SKILL.md

@@ -0,0 +1,151 @@
+---
+name: fmode-code-lit
+description: "用 Lit 原生 ESM 无构建方式写前端(零编译/零打包/浏览器直跑)。触发词:lit 项目、lit 组件、WebComponent、无构建前端、原生 ESM、importmap、@lit-labs/signals、@lit-labs/context、多标签 KeepAlive、FMODE OBS+CDN 前端部署。适用场景:(1) 新建 lit 无构建项目并生成目录/importmap/骨架 (2) 写 LitElement 组件/Service/路由 (3) 给已有项目补齐前端开发规则 (.claude/rules) (4) 部署到 OBS+CDN 并配 History 回退。产出遵循 fmode-code-lit 规则仓的分层与命名规范。"
+description_en: "Build front-ends the Lit native-ESM no-build way (zero compile, zero bundling, runs directly in the browser). Triggers: lit project, lit component, WebComponent, no-build frontend, native ESM, importmap, @lit-labs/signals, @lit-labs/context, multi-tab keep-alive, FMODE OBS+CDN front-end deploy. Use for: (1) scaffolding a new lit no-build project with dirs/importmap/skeleton, (2) writing LitElement components/services/routing, (3) adding front-end dev rules (.claude/rules) to an existing project, (4) deploying to OBS+CDN with History fallback. Output follows the fmode-code-lit rules repo's layering and naming conventions."
+schema_version: 1.0.0
+level: system
+category: development
+keywords:
+  - lit
+  - webcomponents
+  - native-esm
+  - esm
+  - importmap
+  - no-build
+  - no-bundle
+  - signals
+  - context
+  - router
+  - keepalive
+  - frontend
+  - fmode
+version: 1.0.0
+author: Yuyang001 (FmodeAgent)
+license: MPL-2.0
+copyright: "Copyright (c) 2026 未来飞马 Fmode"
+tags: [未来飞马, 智能体技能, 超级技能, 开发级, 前端, Lit, WebComponent, 无构建, ESM, FmodeCode, Claude Code]
+---
+
+# fmode-code-lit — Lit 原生 ESM 无构建前端
+
+## Overview
+
+教 AI 用 **无构建 / 无打包 / 纯原生 ESM + Lit 3.x** 的方式开发前端:源码即产物,浏览器直接跑,
+依赖走 **FMODE Repose** 统一托管,业务码走 **OBS + CDN**。分层(组件 / 服务 / 工具)、
+`@lit-labs/signals` 响应式、`@lit/context` 依赖注入、`@lit-labs/router` History 路由、后台多标签 KeepAlive。
+
+> 完整规则见 `fmode-code-lit` **规则仓**(本技能随仓发布)。
+> 本技能负责「**初始化 + 生成代码**」,规则仓负责「**规范细则**」。
+
+## 何时触发
+
+用户出现以下意图时启用:
+
+- 「新建 / 初始化一个 **lit 项目**」「写一个 **lit 组件**」「**无构建** 前端」「**原生 ESM** + importmap」
+- 「WebComponent / Shadow DOM 组件」「signals 响应式」「context 依赖注入」
+- 「后台多标签 / KeepAlive」「History 干净路由」
+- 「把这套 **前端开发规则** 给项目装上」「部署到 OBS+CDN」
+
+## 三条铁律(生成任何代码前先记住)
+
+1. **源码即产物**:不引入 rollup/esbuild/vite/webpack。写好的 `.js` 直接上传。
+2. **依赖不落地项目**:`lit`、`@lit/*`、`@lit-labs/*` 由 **importmap** 指向 FMODE Repose,禁止把依赖拷进业务仓。
+3. **分层不越界**:组件→服务→工具。组件禁 `fetch`/复杂业务;服务禁 DOM。
+
+## ⚠️ 无构建环境的硬约束(最容易踩)
+
+**禁用一切装饰器语法**:`@customElement` / `@property` / `@state` / `@consume` / `@provide` / `@query`。
+装饰器是 TS/Babel 编译期特性,浏览器原生 JS **不执行**,照抄 Lit 官方「装饰器版」文档会直接 `SyntaxError`。
+
+一律改用标准静态字段 + 编程式 API:
+
+| 装饰器版(❌) | 无构建版(✅) |
+|----------------|----------------|
+| `@customElement('x-y')` | `customElements.define('x-y', XY)` |
+| `@property({type:String}) name` | `static properties = { name: { type: String } }` |
+| `@state() kw=''` | `static properties = { kw: { type: String, state: true } }` |
+| `@consume({context})` | `new ContextConsumer(this, { context, subscribe, callback })` |
+| `@provide({context})` | 模板里 `provide(ctx, value)` 指令 |
+
+## 用法
+
+### 1) 初始化一个新 lit 无构建项目
+
+```bash
+# 在目标目录生成标准骨架(index.html + importmap + 分层目录 + 示例组件)
+node <skill_dir>/scripts/scaffold.mjs ./my-admin --project my-admin
+```
+
+生成结构:
+
+```
+my-admin/
+  src/
+    index.html            # <base> + importmap + 入口
+    importmap.json        # 本地 importmap(生产用 Repose 公共 importmap)
+    app.js                # provide + 路由 + 根组件
+    components/  routes/  services/  utils/
+    .claude/rules/        # 前端规则(从规则仓同步)
+  package.json            # 仅类型提示,不打包
+```
+
+### 2) 给已有项目装上规则
+
+```bash
+node <skill_dir>/scripts/install-rules.mjs .      # 写入 ./.claude/rules/ + CLAUDE.md 片段
+```
+
+### 3) 生成单个组件 / 服务
+
+```bash
+node <skill_dir>/scripts/new.mjs component user-card
+node <skill_dir>/scripts/new.mjs service auth-service
+node <skill_dir>/scripts/new.mjs page page-order-list
+```
+
+### 4) 部署
+
+```bash
+bash scripts/deploy.sh <project>   # obsutil 同步 src/ → OBS → CDN 刷新
+```
+
+## 关键约定速查
+
+| 项 | 约定 |
+|----|------|
+| 组件文件 | `kebab-case.js`,类 `PascalCase`,标签含连字符 |
+| 页面 | `page-*.js`,标签 `<page-*>` |
+| 服务 | `*-service.js`,类 `*Service`,单例 `export const xService = new XService()` |
+| 相对导入 | **必须带 `.js` 后缀** |
+| 全局状态 | `@lit-labs/signals` 的 signal + `SignalWatcher` |
+| 组件本地状态 | `static properties`(`state: true`) |
+| 依赖注入 | `@lit/context` 的 `provide()` + `ContextConsumer`(**非** `@lit-labs/context`,已废弃) |
+| 路由 | `@lit-labs/router`,History 干净 URL |
+| 子目录部署 | 必须 `<base href="/<project>/">` |
+
+## 依赖版本(2026-10 实测可用)
+
+```
+lit@3.3.3  lit-html@3.3.3  lit-element@4.2.2  @lit/reactive-element@2.1.2
+@lit/context@1.1.6  @lit-labs/signals@0.3.0  signal-polyfill@0.2.2  @lit-labs/router@0.1.4
+```
+
+> 注意:`@lit-labs/signals` 依赖 **`signal-polyfill`**,importmap 必须映射它(入口 `dist/index.js`)。
+> `@lit-labs/context` 已废弃 → 用 **`@lit/context`**。
+
+## 部署要点
+
+- 依赖 → FMODE Repose(按版本目录,`.js` 必须 `Content-Type: text/javascript`,CORS 放行业务域名,开 HTTP/2)。
+- 业务码 → `obsutil sync src/ obs://<bucket>/<project>/`(`public-read`),再刷 CDN。
+- **CDN History 回退**:不存在路径返回 `index.html` 且状态码 **200**,**禁 302**(否则地址栏改写、OAuth 回调失败、路由丢失)。
+- `<base href>` 与部署前缀一致;`index.html` 设 `no-cache`。
+
+## 常见坑(详见规则仓 `.claude/rules/pitfalls.md`)
+
+装饰器 / CORS / MIME 非 JS / lit 多版本混用 / CDN 302 / base href 缺失 / 相对 import 漏 `.js` /
+signal 与 property 混用 / 老浏览器无 Import Map / 循环依赖。
+
+## 诚实失败
+
+生成前先探测目标目录;已存在文件**不覆盖**(除非 `--force`)。若依赖版本无法从 Repose 解析,
+明确报错并给出配置指引,**不生成一个跑不起来的骨架**。

+ 38 - 0
skills/fmode-code-lit/bin/fmode-code-lit.js

@@ -0,0 +1,38 @@
+#!/usr/bin/env node
+/**
+ * fmode-code-lit CLI
+ * 用法:
+ *   npx fmode-code-lit install      # 装技能到 ~/.claude/skills/(默认)
+ *   npx fmode-code-lit workspace    # 装技能到 ./.claude/skills/
+ *   npx fmode-code-lit check        # 校验安装
+ *   npx fmode-code-lit scaffold <dir> --project <name>   # 生成无构建 Lit 项目骨架
+ *   npx fmode-code-lit new <component|service|page> <name>
+ *   npx fmode-code-lit rules [dir]  # 把前端规则装入目标项目 ./.claude/rules/
+ */
+const { spawnSync } = require('node:child_process');
+const path = require('node:path');
+
+const SCRIPTS = path.join(__dirname, '..', 'scripts');
+const [cmd = 'install', ...rest] = process.argv.slice(2);
+
+const MAP = {
+  install: 'install.mjs',
+  workspace: 'install.mjs',
+  check: 'install.mjs',
+  scaffold: 'scaffold.mjs',
+  new: 'new.mjs',
+  rules: 'install-rules.mjs',
+};
+
+const script = MAP[cmd];
+if (!script) {
+  console.error(`未知命令:${cmd}\n可用:${Object.keys(MAP).join(', ')}`);
+  process.exit(2);
+}
+// workspace 需作为首参透传给 install.mjs(install.mjs 默认 install)
+const argv = (cmd === 'workspace' || cmd === 'install' || cmd === 'check')
+  ? [path.join(SCRIPTS, script), cmd, ...rest]
+  : [path.join(SCRIPTS, script), ...rest];
+
+const r = spawnSync(process.execPath, argv, { stdio: 'inherit' });
+process.exit(r.status ?? 1);

+ 37 - 0
skills/fmode-code-lit/package.json

@@ -0,0 +1,37 @@
+{
+  "name": "fmode-code-lit",
+  "version": "1.0.0",
+  "description": "Claude Code 技能:Lit 原生 ESM 无构建前端开发(零编译/零打包),分层规范 + importmap + signals + context + router + 多标签 KeepAlive。提供项目脚手架、组件/服务/页面生成器与规则安装器。",
+  "type": "commonjs",
+  "bin": {
+    "fmode-code-lit": "bin/fmode-code-lit.js"
+  },
+  "scripts": {
+    "smoke": "node skills/fmode-code-lit/scripts/smoke.mjs"
+  },
+  "files": [
+    "bin/",
+    "README.md",
+    "LICENSE",
+    "skill-package-manifest.json",
+    "skills/"
+  ],
+  "keywords": [
+    "claude-code",
+    "claude-skill",
+    "fmode",
+    "lit",
+    "webcomponents",
+    "native-esm",
+    "importmap",
+    "no-build",
+    "signals",
+    "frontend"
+  ],
+  "license": "MPL-2.0",
+  "repository": {
+    "type": "git",
+    "url": "git+ssh://git@git.fmode.cn/fmode/fmode-code-lit.git"
+  },
+  "homepage": "https://git.fmode.cn/fmode/fmode-code-lit"
+}

+ 72 - 0
skills/fmode-code-lit/scripts/install-rules.mjs

@@ -0,0 +1,72 @@
+#!/usr/bin/env node
+/**
+ * install-rules.mjs —— 把 fmode-code-lit 的前端规则装入目标项目的 ./.claude/rules/。
+ *
+ * 用法:
+ *   node install-rules.mjs [targetDir] [--force]
+ *   node install-rules.mjs [targetDir] --from <rulesRepoDir>   # 指定规则仓根目录
+ *
+ * 规则来源优先级:--from > 技能同仓 ../../.claude/rules > 内置最小规则集。
+ * 已存在文件默认不覆盖(--force 覆盖)。
+ */
+import { mkdirSync, writeFileSync, existsSync, readdirSync, readFileSync, statSync, copyFileSync } from 'node:fs';
+import { join, resolve, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const args = process.argv.slice(2);
+const target = resolve(args.find((a) => !a.startsWith('--') && a !== undefined && !a.startsWith('-')) || '.');
+const force = args.includes('--force');
+const fromIdx = args.indexOf('--from');
+const fromDir = fromIdx > -1 ? resolve(args[fromIdx + 1]) : null;
+
+// 规则来源候选
+const candidates = [
+  fromDir,
+  resolve(__dirname, '../../../.claude/rules'), // 技能在 <repo>/skills/fmode-code-lit/scripts/
+  resolve(__dirname, '../../.claude/rules'),
+].filter(Boolean);
+const srcRules = candidates.find((d) => d && existsSync(d) && existsSync(join(d, 'architecture.md')));
+
+const destRules = join(target, '.claude', 'rules');
+mkdirSync(destRules, { recursive: true });
+
+let created = 0, skipped = 0;
+function copyTree(from, to) {
+  for (const name of readdirSync(from)) {
+    const s = join(from, name), d = join(to, name);
+    const st = statSync(s);
+    if (st.isDirectory()) { mkdirSync(d, { recursive: true }); copyTree(s, d); continue; }
+    if (!name.endsWith('.md')) continue;
+    if (existsSync(d) && !force) { skipped++; console.log(`  skip  ${d}`); continue; }
+    copyFileSync(s, d); created++; console.log(`  write ${d}`);
+  }
+}
+
+if (srcRules) {
+  console.log(`\n规则来源:${srcRules}`);
+  copyTree(srcRules, destRules);
+} else {
+  console.log('\n未找到规则仓,写入内置最小规则集。');
+  const minimal = {
+    'lit-no-build.md': `# Lit 无构建前端 · 最小规则\n\n1. 源码即产物:不引入任何打包器。\n2. 依赖由 importmap 指向 FMODE Repose,禁止拷进项目。\n3. **禁用装饰器**(@customElement/@property/@state/@consume)——用 static properties + customElements.define + ContextConsumer。\n4. 分层:组件(渲染)→服务(业务/状态)→工具(纯函数);组件禁 fetch,服务禁 DOM。\n5. 相对导入必须带 .js 后缀。\n6. 全局状态用 @lit-labs/signals;组件本地状态用 static properties(state:true)。\n7. 子目录部署必须 <base href=\"/<project>/\">。\n8. CDN History 回退:不存在路径 200 返回 index.html,禁 302。\n`,
+  };
+  for (const [name, content] of Object.entries(minimal)) {
+    const d = join(destRules, name);
+    if (existsSync(d) && !force) { skipped++; continue; }
+    writeFileSync(d, content); created++; console.log(`  write ${d}`);
+  }
+}
+
+// CLAUDE.md 片段(追加式,不覆盖已有正文)
+const claudeMd = join(target, 'CLAUDE.md');
+const snippet = `\n## 前端(Lit 原生 ESM 无构建)\n\n本项目前端遵循 \`.claude/rules/\` 下的 Lit 无构建规则:源码即产物、依赖走 importmap、\n分层(组件/服务/工具)、**禁用装饰器**、@lit-labs/signals 响应式、@lit-labs/router History 路由。\n详见 \`.claude/rules/architecture.md\` 与 \`.claude/rules/components.md\`。\n`;
+if (existsSync(claudeMd)) {
+  const cur = readFileSync(claudeMd, 'utf8');
+  if (!cur.includes('Lit 原生 ESM 无构建')) { writeFileSync(claudeMd, cur + snippet); console.log(`  append ${claudeMd}`); }
+  else console.log(`  skip  ${claudeMd}(已含片段)`);
+} else {
+  writeFileSync(claudeMd, `# 项目规则\n${snippet}`); created++; console.log(`  write ${claudeMd}`);
+}
+
+console.log(`\n✓ 完成:新建 ${created},跳过 ${skipped}。规则目录:${destRules}\n`);

+ 57 - 0
skills/fmode-code-lit/scripts/install.mjs

@@ -0,0 +1,57 @@
+#!/usr/bin/env node
+/**
+ * install.mjs —— 把 fmode-code-lit 技能安装到 Claude Code 技能目录。
+ *
+ * 用法:
+ *   node install.mjs install     # → ~/.claude/skills/fmode-code-lit/(全局,默认)
+ *   node install.mjs workspace   # → ./.claude/skills/fmode-code-lit/(当前项目)
+ *   node install.mjs install --target <dir> [--force]
+ *   node install.mjs check       # 校验安装
+ */
+import { mkdirSync, copyFileSync, existsSync, readdirSync, statSync } from 'node:fs';
+import { join, resolve, dirname } from 'node:path';
+import { homedir } from 'node:os';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const SKILL_SRC = resolve(__dirname, '..');           // <...>/skills/fmode-code-lit
+const SKILL_NAME = 'fmode-code-lit';
+
+const args = process.argv.slice(2);
+const cmd = args[0] && !args[0].startsWith('--') ? args[0] : 'install';
+const force = args.includes('--force');
+const tIdx = args.indexOf('--target');
+const globalTarget = join(homedir(), '.claude', 'skills', SKILL_NAME);
+const workspaceTarget = join(process.cwd(), '.claude', 'skills', SKILL_NAME);
+
+function copyTree(from, to) {
+  mkdirSync(to, { recursive: true });
+  for (const name of readdirSync(from)) {
+    if (name === 'node_modules') continue;
+    const s = join(from, name), d = join(to, name);
+    if (statSync(s).isDirectory()) copyTree(s, d);
+    else if (!existsSync(d) || force) copyFileSync(s, d);
+  }
+}
+
+function verify(target) {
+  const must = ['SKILL.md'];
+  const missing = must.filter((f) => !existsSync(join(target, f)));
+  if (missing.length) { console.error(`✗ 安装不完整,缺少:${missing.join(', ')}`); process.exit(1); }
+  console.log(`✓ 技能已就位:${target}`);
+  console.log(`  SKILL.md 存在,scripts/ ${existsSync(join(target, 'scripts')) ? '存在' : '缺失'}`);
+}
+
+function main() {
+  const target = cmd === 'workspace' || cmd === 'install-workspace'
+    ? workspaceTarget
+    : (tIdx > -1 ? resolve(args[tIdx + 1]) : globalTarget);
+  if (cmd === 'check') { verify(target); return; }
+  if (!existsSync(SKILL_SRC)) { console.error(`✗ 找不到技能源:${SKILL_SRC}`); process.exit(1); }
+  console.log(`安装 ${SKILL_NAME} → ${target}${force ? '(--force 覆盖)' : ''}`);
+  copyTree(SKILL_SRC, target);
+  verify(target);
+  console.log(`\n触发词:lit 项目 / lit 组件 / 无构建前端 / WebComponent / importmap`);
+}
+
+main();

+ 129 - 0
skills/fmode-code-lit/scripts/new.mjs

@@ -0,0 +1,129 @@
+#!/usr/bin/env node
+/**
+ * new.mjs —— 生成单个 Lit 无构建单元(组件 / 服务 / 页面)。
+ *
+ * 用法:
+ *   node new.mjs component user-card         [--dir src/components]
+ *   node new.mjs service   auth-service      [--dir src/services]
+ *   node new.mjs page      page-order-list   [--dir src/routes]
+ *
+ * 生成物一律无装饰器,遵循规则仓命名与分层规范。
+ */
+import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
+import { join, resolve } from 'node:path';
+
+const args = process.argv.slice(2);
+const kind = args[0];
+const name = args[1];
+const dirIdx = args.indexOf('--dir');
+const force = args.includes('--force');
+
+const KINDS = {
+  component: { dir: 'src/components', file: (n) => n + '.js' },
+  service: { dir: 'src/services', file: (n) => n + '.js' },   // 文件名已含 -service 后缀
+  page: { dir: 'src/routes', file: (n) => n + '.js' },
+};
+
+if (!kind || !KINDS[kind] || !name) {
+  console.error(`用法:node new.mjs <component|service|page> <name> [--dir <dir>] [--force]`);
+  process.exit(2);
+}
+if (!/^[a-z][a-z0-9-]*$/.test(name)) {
+  console.error(`✗ 名称须为小写 kebab-case:${name}`);
+  process.exit(2);
+}
+
+const spec = KINDS[kind];
+const dir = dirIdx > -1 ? resolve(args[dirIdx + 1]) : resolve(spec.dir);
+// service:允许传 order 或 order-service,统一补一个 -service 后缀(避免 order-service-service)
+const baseName = kind === 'service' ? name.replace(/-service$/, '') : name;
+const fileBase = kind === 'service' ? baseName + '-service' : name;
+const pascal = fileBase.split('-').map((s) => s[0].toUpperCase() + s.slice(1)).join('');
+// 标签必须含连字符:page- 前缀已保证;component 若单段则补前缀
+const tag = kind === 'service' ? null : (fileBase.includes('-') ? fileBase : 'x-' + fileBase);
+const path = join(dir, spec.file(fileBase));
+const relToUtils = kind === 'page' ? '../../utils' : '../utils';
+
+if (existsSync(path) && !force) {
+  console.error(`✗ 已存在:${path}(用 --force 覆盖)`);
+  process.exit(1);
+}
+mkdirSync(dir, { recursive: true });
+
+let content = '';
+if (kind === 'component') {
+  content = `/**
+ * ${name} —— 通用组件。
+ * 无装饰器:static properties + customElements.define。
+ */
+import { LitElement, html, css } from 'lit';
+
+export class ${pascal} extends LitElement {
+  static properties = {
+    label: { type: String },
+  };
+
+  static styles = css\`
+    :host { display: block; }
+  \`;
+
+  constructor() { super(); this.label = ''; }
+
+  render() {
+    return html\`<div>\${this.label}</div>\`;
+  }
+}
+
+customElements.define('${tag}', ${pascal});
+`;
+} else if (kind === 'service') {
+  const endpoint = baseName;
+  const singleton = pascal.charAt(0).toLowerCase() + pascal.slice(1);
+  content = `/**
+ * ${pascal} —— 业务服务(Signal + 单例)。无 DOM 依赖,可单测。
+ */
+import { signal, computed } from '@lit-labs/signals';
+// import { http } from '${relToUtils}/http.js';
+
+class ${pascal} {
+  #items = signal([]);
+  get items() { return this.#items; }
+  get count() { return computed(() => this.#items.get().length); }
+
+  async fetchAll() {
+    // const data = await http.get('/${endpoint}');
+    // this.#items.set(data.items ?? []);
+    return this.#items.get();
+  }
+
+  set(items) { this.#items.set(items); }
+}
+
+export const ${singleton} = new ${pascal}();
+`;
+} else {
+  content = `/**
+ * ${name} —— 页面组件(路由挂载)。
+ */
+import { LitElement, html, css } from 'lit';
+import { SignalWatcher } from '@lit-labs/signals';
+// import { navigate } from '${relToUtils}/router.js';
+
+export class ${pascal} extends SignalWatcher(LitElement) {
+  static properties = {};
+
+  static styles = css\`
+    :host { display: block; }
+  \`;
+
+  render() {
+    return html\`<h2>${pascal}</h2>\`;
+  }
+}
+
+customElements.define('${tag}', ${pascal});
+`;
+}
+
+writeFileSync(path, content);
+console.log(`✓ 已生成 ${path}`);

+ 259 - 0
skills/fmode-code-lit/scripts/scaffold.mjs

@@ -0,0 +1,259 @@
+#!/usr/bin/env node
+/**
+ * scaffold.mjs —— 生成 Lit 原生 ESM 无构建项目骨架。
+ *
+ * 用法:
+ *   node scaffold.mjs <targetDir> [--project <name>] [--force]
+ *
+ * 原则:已存在的文件默认不覆盖;不引入任何构建工具;所有代码无装饰器。
+ */
+import { mkdirSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
+import { join, resolve, basename } from 'node:path';
+
+const args = process.argv.slice(2);
+const target = resolve(args.find((a) => !a.startsWith('--')) || '.');
+const force = args.includes('--force');
+const projIdx = args.indexOf('--project');
+const project = projIdx > -1 ? args[projIdx + 1] : basename(target);
+
+if (!project || !/^[a-z0-9][a-z0-9-]*$/.test(project)) {
+  console.error(`✗ 项目名非法:${project}(须为小写 kebab-case)`);
+  process.exit(2);
+}
+
+let created = 0, skipped = 0;
+function write(rel, content) {
+  const p = join(target, rel);
+  if (existsSync(p) && !force) { skipped++; console.log(`  skip  ${rel}(已存在)`); return; }
+  mkdirSync(join(p, '..'), { recursive: true });
+  writeFileSync(p, content);
+  created++; console.log(`  write ${rel}`);
+}
+
+const DEP_VERSIONS = {
+  lit: '3.3.3', 'lit-html': '3.3.3', 'lit-element': '4.2.2', '@lit/reactive-element': '2.1.2',
+  '@lit/context': '1.1.6', '@lit-labs/signals': '0.3.0', 'signal-polyfill': '0.2.2', '@lit-labs/router': '0.1.4',
+};
+
+console.log(`\n生成 Lit 无构建项目:${target}\n项目名:${project}\n`);
+
+// 目录
+for (const d of ['src/components', 'src/services', 'src/utils', 'src/routes']) {
+  mkdirSync(join(target, d), { recursive: true });
+}
+
+// index.html
+write('src/index.html', `<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="utf-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1" />
+  <title>${project}</title>
+  <!-- 二级子路径部署必须;根部署可改 "/" -->
+  <base href="/${project}/" />
+  <!-- 生产:指向 FMODE Repose 公共 importmap -->
+  <script type="importmap" src="/npm/importmap/lit-v3.json"></script>
+  <!-- 本地无 Repose 时改用: <script type="importmap" src="./importmap.json"></script> -->
+  <style>:root{--brand:#2b6cb0;--bg:#f7fafc;--border:#e2e8f0;font-family:system-ui,sans-serif}body{margin:0}</style>
+</head>
+<body>
+  <app-root></app-root>
+  <script type="module" src="./app.js"></script>
+</body>
+</html>
+`);
+
+// importmap.json(本地开发用)
+write('src/importmap.json', JSON.stringify({
+  _comment: '本地开发用;生产由 FMODE Repose 的 /npm/importmap/lit-v3.json 提供。',
+  imports: {
+    lit: `/npm/lit/v${DEP_VERSIONS.lit}/lit.js`,
+    'lit/': `/npm/lit/v${DEP_VERSIONS.lit}/`,
+    'lit-html': `/npm/lit-html/v${DEP_VERSIONS['lit-html']}/lit-html.js`,
+    'lit-element': `/npm/lit-element/v${DEP_VERSIONS['lit-element']}/lit-element.js`,
+    '@lit/reactive-element': `/npm/@lit/reactive-element/v${DEP_VERSIONS['@lit/reactive-element']}/reactive-element.js`,
+    '@lit/context': `/npm/@lit/context/v${DEP_VERSIONS['@lit/context']}/index.js`,
+    '@lit-labs/signals': `/npm/@lit-labs/signals/v${DEP_VERSIONS['@lit-labs/signals']}/index.js`,
+    'signal-polyfill': `/npm/signal-polyfill/v${DEP_VERSIONS['signal-polyfill']}/dist/index.js`,
+    '@lit-labs/router': `/npm/@lit-labs/router/v${DEP_VERSIONS['@lit-labs/router']}/index.js`,
+  },
+}, null, 2) + '\n');
+
+// app.js
+write('src/app.js', `/**
+ * app.js —— 应用装配:provide 全局服务 + 挂载路由 + 根组件。不含业务逻辑。
+ */
+import { LitElement, html, css } from 'lit';
+import { provide } from '@lit/context';
+import { authContext } from './services/contexts.js';
+import { authService } from './services/auth-service.js';
+
+export class AppRoot extends LitElement {
+  static styles = css\`:host{display:block;background:var(--bg)}\`;
+  render() {
+    return html\`
+      \${provide(authContext, authService)}
+      <app-shell></app-shell>
+    \`;
+  }
+}
+customElements.define('app-root', AppRoot);
+`);
+
+// services/contexts.js
+write('src/services/contexts.js', `/**
+ * 注入令牌集中定义(令牌必须同一模块实例)。
+ */
+import { createContext } from '@lit/context';
+export const authContext = createContext('${project}.auth');
+`);
+
+// services/auth-service.js
+write('src/services/auth-service.js', `/**
+ * 全局登录态(Signal Service)。纯 ESM Class,无 DOM 依赖,可单测。
+ */
+import { signal, computed } from '@lit-labs/signals';
+
+class AuthService {
+  #user = signal(null);
+  #token = signal(localStorage.getItem('token') || '');
+  get user() { return this.#user; }
+  get isLoggedIn() { return computed(() => this.#user.get() != null); }
+  getToken() { return this.#token.get(); }
+
+  async login(username, password) {
+    // TODO: 接真实接口
+    // const { token, user } = await http.post('/auth/login', { username, password });
+    const token = 'demo-token', user = { name: username };
+    this.#token.set(token); this.#user.set(user);
+    localStorage.setItem('token', token);
+  }
+  logout() { this.#token.set(''); this.#user.set(null); localStorage.removeItem('token'); }
+}
+export const authService = new AuthService();
+`);
+
+// utils/http.js
+write('src/utils/http.js', `/**
+ * 原生 fetch 薄封装。token 通过 configureHttp 注入,避免 utils 反向依赖 services。
+ */
+let getToken = () => '';
+let onUnauthorized = () => {};
+export function configureHttp({ token, unauthorized } = {}) {
+  if (token) getToken = token;
+  if (unauthorized) onUnauthorized = unauthorized;
+}
+export class HttpError extends Error {
+  constructor(status, body) { super('HTTP ' + status); this.status = status; this.body = body; }
+}
+const BASE = '/api';
+async function request(method, path, { body, headers = {}, query } = {}) {
+  let url = BASE + path;
+  if (query) url += '?' + new URLSearchParams(query).toString();
+  const h = { 'Content-Type': 'application/json', ...headers };
+  const t = getToken(); if (t) h.Authorization = 'Bearer ' + t;
+  const res = await fetch(url, { method, headers: h, body: body != null ? JSON.stringify(body) : undefined, credentials: 'same-origin' });
+  if (res.status === 401) { onUnauthorized(); throw new HttpError(401, null); }
+  const ct = res.headers.get('content-type') || '';
+  const data = ct.includes('application/json') ? await res.json() : await res.text();
+  if (!res.ok) throw new HttpError(res.status, data);
+  return data;
+}
+export const http = {
+  get: (p, o) => request('GET', p, o),
+  post: (p, body, o) => request('POST', p, { ...o, body }),
+  put: (p, body, o) => request('PUT', p, { ...o, body }),
+  del: (p, o) => request('DELETE', p, o),
+};
+`);
+
+// utils/router.js
+write('src/utils/router.js', `/**
+ * 基于 document.baseURI 的导航,尊重 <base href>。
+ */
+export function navigate(path, { replace = false } = {}) {
+  const url = new URL(path, document.baseURI);
+  if (replace) history.replaceState({}, '', url); else history.pushState({}, '', url);
+  window.dispatchEvent(new PopStateEvent('popstate'));
+}
+export function currentPath() {
+  const base = new URL(document.baseURI).pathname.replace(/\\/$/, '');
+  return location.pathname.replace(base, '') || '/';
+}
+`);
+
+// components/app-shell.js
+write('src/components/app-shell.js', `/**
+ * 布局壳:侧边菜单 + 路由出口。多标签版本见规则仓 tabs-keepalive.md。
+ */
+import { LitElement, html, css } from 'lit';
+import { Router } from '@lit-labs/router';
+import './page-home.js';
+
+export class AppShell extends LitElement {
+  static styles = css\`:host{display:block;padding:16px}\`;
+  #router = new Router(this, [
+    { path: '/', render: () => html\`<page-home></page-home>\` },
+    { path: '/*', render: () => html\`<page-home></page-home>\` },
+  ]);
+  render() { return html\`<main>\${this.#router.outlet()}</main>\`; }
+}
+customElements.define('app-shell', AppShell);
+`);
+
+// components/page-home.js
+write('src/components/page-home.js', `/**
+ * 首页示例组件。无装饰器;用 static properties。
+ */
+import { LitElement, html, css } from 'lit';
+import { SignalWatcher } from '@lit-labs/signals';
+import { ContextConsumer } from '@lit/context';
+import { authContext } from '../services/contexts.js';
+
+export class PageHome extends SignalWatcher(LitElement) {
+  static properties = { _auth: { state: true } };
+  #consumer = new ContextConsumer(this, { context: authContext, subscribe: true, callback: (v) => { this._auth = v; } });
+  static styles = css\`h1{color:var(--brand)}\`;
+  render() {
+    const name = this._auth?.user.get()?.name;
+    return html\`<h1>${project}: \${name || 'hello lit'}</h1>\`;
+  }
+}
+customElements.define('page-home', PageHome);
+`);
+
+// package.json
+write('package.json', JSON.stringify({
+  name: project, private: true, type: 'module',
+  _comment: 'devDependencies 仅用于 IDE 类型提示,不参与部署产物。',
+  scripts: { serve: 'npx --yes serve src', deploy: 'bash scripts/deploy.sh ' + project },
+  devDependencies: {
+    lit: '^' + DEP_VERSIONS.lit, '@lit/context': '^' + DEP_VERSIONS['@lit/context'],
+    '@lit-labs/signals': '^' + DEP_VERSIONS['@lit-labs/signals'], '@lit-labs/router': '^' + DEP_VERSIONS['@lit-labs/router'],
+  },
+}, null, 2) + '\n');
+
+// scripts/deploy.sh
+write('scripts/deploy.sh', `#!/usr/bin/env bash
+# 无构建部署:同步 src/ 到 OBS + CDN 刷新。凭据由环境/既有脚本提供,不写入仓库。
+set -euo pipefail
+PROJECT="\${1:-${project}}"
+BUCKET="\${OBS_BUCKET:?需设置 OBS_BUCKET}"
+EP="\${OBS_ENDPOINT:-obs.cn-south-1.myhuaweicloud.com}"
+OBS="\${OBSUTIL:-/opt/data/bin/obsutil}"
+SRC="$(cd "$(dirname "$0")/.." && pwd)/src"
+"$OBS" rm "$BUCKET/$PROJECT/index.html" -e="$EP" -f >/dev/null 2>&1 || true
+"$OBS" cp "$SRC/index.html" "$BUCKET/$PROJECT/index.html" -e="$EP" -acl=public-read -f \\
+  -meta="Cache-Control:no-cache#Content-Type:text/html; charset=utf-8"
+"$OBS" sync "$SRC/" "$BUCKET/$PROJECT/" -e="$EP" -acl=public-read -f
+echo "完成。确认 CDN 回退:不存在路径 200 返回 index.html(禁 302)。"
+`);
+
+// .gitignore
+write('.gitignore', 'node_modules/\ndist/\n.DS_Store\n');
+
+console.log(`\n✓ 完成:新建 ${created} 个文件,跳过 ${skipped} 个。`);
+console.log(`\n下一步:`);
+console.log(`  1. 本地预览: cd ${target} && npx serve src`);
+console.log(`  2. 或在 index.html 指向 Repose 公共 importmap 后直接部署`);
+console.log(`  3. 规则细则见 fmode-code-lit 规则仓 .claude/rules/\n`);

+ 60 - 0
skills/fmode-code-lit/scripts/smoke.mjs

@@ -0,0 +1,60 @@
+#!/usr/bin/env node
+/**
+ * smoke.mjs —— 技能自检。
+ * 生成一个临时项目并校验:文件齐全、JS 语法通过、无装饰器。
+ * 用法:node scripts/smoke.mjs
+ */
+import { mkdtempSync, rmSync, readdirSync, statSync, readFileSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { tmpdir } from 'node:os';
+import { execFileSync } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const scaffold = join(__dirname, 'scaffold.mjs');
+const newGen = join(__dirname, 'new.mjs');
+
+let fail = 0;
+const check = (name, cond) => { console.log(`${cond ? '✓' : '✗'} ${name}`); if (!cond) fail++; };
+
+function walk(d, out = []) {
+  for (const e of readdirSync(d, { withFileTypes: true })) {
+    const p = join(d, e.name);
+    if (e.isDirectory()) walk(p, out); else out.push(p);
+  }
+  return out;
+}
+
+const dir = mkdtempSync(join(tmpdir(), 'fmode-code-lit-smoke-'));
+try {
+  execFileSync(process.execPath, [scaffold, dir, '--project', 'smoke-app'], { stdio: 'pipe' });
+  const files = walk(dir);
+  check('scaffold 生成 index.html', files.some((f) => f.endsWith('src/index.html')));
+  check('scaffold 生成 importmap.json', files.some((f) => f.endsWith('src/importmap.json')));
+  check('scaffold 生成 app.js', files.some((f) => f.endsWith('src/app.js')));
+  check('scaffold 生成分层目录', ['components', 'services', 'utils'].every((d2) => files.some((f) => f.includes(`/src/${d2}/`))));
+
+  execFileSync(process.execPath, [newGen, 'component', 'smoke-card', '--dir', join(dir, 'src/components')], { stdio: 'pipe' });
+  execFileSync(process.execPath, [newGen, 'service', 'smoke-service', '--dir', join(dir, 'src/services')], { stdio: 'pipe' });
+  check('new component 生成', files.some((f) => f.endsWith('smoke-card.js')) || walk(dir).some((f) => f.endsWith('smoke-card.js')));
+  check('new service 无重复后缀', walk(dir).some((f) => f.endsWith('smoke-service.js')) && !walk(dir).some((f) => f.endsWith('smoke-service-service.js')));
+
+  // 全量语法校验 + 装饰器扫描
+  let syntaxOk = true, decoratorFound = false;
+  for (const f of walk(dir).filter((f) => f.endsWith('.js'))) {
+    try { execFileSync(process.execPath, ['--check', f], { stdio: 'pipe' }); } catch { syntaxOk = false; console.log('  语法失败:', f); }
+    if (/^\s*@(customElement|property|state|consume|provide|query)\b/m.test(readFileSync(f, 'utf8'))) decoratorFound = true;
+  }
+  check('生成物全部通过语法检查', syntaxOk);
+  check('生成物无装饰器语法', !decoratorFound);
+
+  // importmap JSON 合法
+  let mapOk = false;
+  try { JSON.parse(readFileSync(join(dir, 'src/importmap.json'), 'utf8')); mapOk = true; } catch {}
+  check('importmap.json 为合法 JSON', mapOk);
+} finally {
+  rmSync(dir, { recursive: true, force: true });
+}
+
+console.log(fail === 0 ? '\nSMOKE OK' : `\nSMOKE FAILED(${fail})`);
+process.exit(fail === 0 ? 0 : 1);

+ 17 - 0
skills/fmode-code-lit/skill-package-manifest.json

@@ -0,0 +1,17 @@
+{
+  "name": "fmode-code-lit",
+  "version": "1.0.0",
+  "description": "Claude Code 独立技能包:用 Lit 原生 ESM 无构建方式写前端(零编译/零打包/浏览器直跑),分层规范 + importmap + signals 响应式 + context 注入 + router History 路由 + 多标签 KeepAlive,配套规则仓与 OBS+CDN 部署。",
+  "plugin": "fmode-code-lit",
+  "skills": [
+    "fmode-code-lit"
+  ],
+  "entrySkill": "fmode-code-lit",
+  "npmPackage": "fmode-code-lit",
+  "smokeCommand": "node scripts/smoke.mjs",
+  "installCommand": "npx fmode-code-lit@latest install",
+  "workspaceInstallCommand": "npx fmode-code-lit@latest workspace",
+  "workspaceSkillPath": ".claude/skills/fmode-code-lit/SKILL.md",
+  "globalSkillPath": "~/.claude/skills/fmode-code-lit/SKILL.md",
+  "installHint": "工作区安装:npx fmode-code-lit@latest workspace,写入 ./.claude/skills/fmode-code-lit/。用户级安装:npx fmode-code-lit@latest install,写入 ~/.claude/skills/fmode-code-lit/。随后可用 npx fmode-code-lit@latest scaffold <dir> --project <name> 生成无构建 Lit 项目骨架。规则细则见 fmode-code-lit 规则仓 .claude/rules/。"
+}