@manohub/app-kit 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONTRACT.md CHANGED
@@ -348,6 +348,7 @@ import { AppShell, AppPanel, AppTable, AppButton, notify } from '@manohub/app-ki
348
348
  - 关闭拦截用 `beforeClose`(返回 `false` 阻止关闭,可用来弹「确认退出」)。
349
349
  - 内容固定渲染进 `.app-container`(令牌与应用侧样式的作用域锚点),渲染到 body 会「样式全丢」。
350
350
  - 需要「固定高度 + 区域内滚动」时传 `fitContent={false}` + `height`。
351
+ - 内容**高且窄**(配置详情、逐项设置、多页签)用**抽屉**(§4.18),不要硬塞进弹窗。
351
352
 
352
353
  ### 4.11 `AppSection`(区块:标题 + 内容)
353
354
 
@@ -553,6 +554,33 @@ const list = useClientPagination(() => filteredRows.value)
553
554
  `setPage` / `setPageSize`;页码同样是 0 基。
554
555
  - 数据变少(过滤 / 删除)导致当前页越界时**自动回退到最后一页**,不会出现「空白页但分页器显示有数据」。
555
556
 
557
+ ### 4.18 抽屉(`AppDrawer`)
558
+
559
+ ```tsx
560
+ <AppDrawer open={visible} title="MCP 配置" width={640}
561
+ onUpdate:open={(v) => (visible = v)}
562
+ footer={() => (
563
+ <>
564
+ <AppButton tone="secondary" onClick={close}>取消</AppButton>
565
+ <AppButton tone="primary" loading={saving} onClick={save}>保存</AppButton>
566
+ </>
567
+ )}>
568
+ …内容…
569
+ </AppDrawer>
570
+ ```
571
+
572
+ - **判据**:内容「高且窄 / 逐项配置 / 多页签」→ 抽屉;只需一眼确认的短内容 → 弹窗(§4.10)。
573
+ 二者都是侧边浮层**只在移动端才退化**的语义,桌面端不要把弹窗当抽屉用。
574
+ - `open` 受控 + `onUpdate:open`;关闭时同发 `close`。`beforeClose` 返回 `false` 可拦截关闭(支持 Promise)。
575
+ - **`width` 传数字(px)**:上游把宽度原样塞进行内 style,字符串 `"300"` 生成的是非法 CSS(宽度会退化成内容宽),
576
+ 本件默认 `480`。`position` 只支持 `'left' | 'right'`(上游没有 top/bottom 的定位样式)。
577
+ - 页脚用 `footer`(函数或节点)或 `footer` 插槽;**容器(右对齐 / 常驻不滚)由上游提供**
578
+ (`footerTemplate` 与内建按钮两条路都被包进同一容器),应用侧**不要自绘页脚容器**。
579
+ 不给页脚时本件会关掉上游那条恒定 60px 的空白页脚区。
580
+ - 标题栏走内建件(`title` + 右上角关闭);`showHeader={false}` 时关闭入口只剩遮罩点击,
581
+ 需要自定义头部就得自行承担关闭入口。
582
+ - 渲染宿主与包含块见 §9 第 11 条(本件已把宿主固定到 `.app-container` 并把定位基准压回视口)。
583
+
556
584
  ---
557
585
 
558
586
  ## 5. 反例库(这些写法一律违规)
@@ -619,7 +647,7 @@ const list = useClientPagination(() => filteredRows.value)
619
647
  | 日期 / 日期时间选择 | **无**(无 `AppDatePicker`) | 暂用 `AppInput` 并标明格式;需真日期控件走 §7 建件 |
620
648
  | 数字 / 步进输入 | **无**(无 `AppNumber`) | 暂用 `AppInput` + 提交侧校验;需步进控件走 §7 建件 |
621
649
  | 加载骨架屏 | **无**(`AppSkeleton` 属后续批次) | 用 §4.6 三态的 `loading`(表格/树内建,块级用 `AppQueryState`) |
622
- | 抽屉(Drawer) | **无** | 宽内容用 `AppDialog`(定高可滚)或页面级双栏 `AppShell.Split` |
650
+ | 抽屉(Drawer) | **已有 `AppDrawer`**(§4.18) | —— |
623
651
  | 描述列表(Descriptions) | **不再需要** | 摘要 / 详情走 §4.7 的 `AppForm.Item text={…}` 只读文本行 |
624
652
 
625
653
  > 本清单随版本变动;**不存在的能力一律不要在应用侧自绘**,也不要用别的件"拼一个像的"当替代品长期留着。
@@ -755,3 +783,9 @@ pnpm --filter @manohub/app-kit test:unit # vitest(契约单测)+ 护栏
755
783
  而令牌(`--ui-*`)与主题桥接(`--f-theme-*`)都锚在 `.app-container` 上、微前端下应用侧 CSS 还被
756
784
  scopecss 限定在容器内 —— 落到 body 会让弹窗**内容里所有组件与应用样式一起失效**。
757
785
  `AppDialog` / `modalService` 内部已改投 `.app-container`,消费方无需也不应干预渲染宿主。
786
+ 11. **抽屉的宿主与包含块**:`AppDrawer`(§4.18)同样把内容投到 `.app-container`(否则样式全丢),
787
+ 而上游在「宿主不是 `body`」时会顺手给抽屉根节点加 `f-drawer-inline`(把 `position: fixed` 改成 `absolute`)——
788
+ 微前端里绝对定位的包含块不可控(`.app-container` 自身未定位、宿主层级随门户而变),抽屉与遮罩会错位。
789
+ 包内桥接层因此把 `.f-drawer.f-drawer-inline` 的定位压回 `fixed`:**覆盖范围与改造前 `host: "body"` 完全一致**。
790
+ 另:上游 `showFooter` 默认 `true` 且高度取 `footerHeight`(默认 60px),不给内容也留一条空白 ——
791
+ `AppDrawer` 在无页脚时直接关掉该区块,应用侧不必再写「隐藏空 footer」的覆盖样式。
package/README.md CHANGED
@@ -5,10 +5,11 @@
5
5
 
6
6
  - **接入方必读**:[CONTRACT.md](./CONTRACT.md) —— 页面结构契约、组件用法与词表、样式纪律、反例库、护栏用法、已知偏差。
7
7
  **写页面代码前先读它**;它随包分发,冲突时以它为准。
8
+ - **接入 SOP**(安装 → 样式 → 入口 → 护栏 → 验收 → 故障对照 → 新应用从零搭建):[`skills/app-kit/references/adoption.md`](./skills/app-kit/references/adoption.md),随包分发。
8
9
  - **改本包时才看**:`AGENTS.md`(包内实现踩坑与硬性要求,不随包分发)
9
10
  - **入口**:`@manohub/app-kit/entry` → `createSubApp`
10
11
  - **组件/服务**:`@manohub/app-kit` → `AppShell` / `AppPanel` / `AppTree` / `AppTable` / `AppButton` / `notify` …
11
- - **样式**:`@manohub/app-kit/reset.css` + `@manohub/app-kit/styles.css`(应用侧 `style.css` 固定三行,见 CONTRACT.md §1)
12
+ - **样式**:`@manohub/app-kit/reset.css` + `@manohub/app-kit/styles.css`(应用侧 `style.css` 固定三行,见下)
12
13
  - **命令入口**:`appkit`(`package.json` 的 `bin`,在应用包根执行)
13
14
  - `pnpm exec appkit lint [--changed|--strict|--json]` —— 三条护栏;单条排查用 `appkit lint:style` / `lint:component` / `lint:structure`
14
15
  - `pnpm exec appkit install [--also-claude|--dry-run]` —— 落 AI 技能到本工程
@@ -16,6 +17,135 @@
16
17
  - 等价写法(老脚本/钩子可用):`node node_modules/@manohub/app-kit/lint/run-all.mjs`、`node node_modules/@manohub/app-kit/skills/install.mjs`
17
18
  - 各护栏脚本仍单独可跑(用法见 CONTRACT.md §8)
18
19
 
20
+ ## 新项目接入(快速开始)
21
+
22
+ > 下面是「从零接一个子应用」的最小动作序列,完整版与故障对照见随包分发的
23
+ > [`skills/app-kit/references/adoption.md`](./skills/app-kit/references/adoption.md);
24
+ > 每一条契约条款以 [CONTRACT.md](./CONTRACT.md) 为准。
25
+
26
+ ### 1. 安装
27
+
28
+ ```bash
29
+ pnpm add @manohub/app-kit
30
+
31
+ # 自备 peer(必须是工程内唯一实例)
32
+ pnpm add vue vue-router pinia @tanstack/vue-query \
33
+ i18next i18next-vue i18next-browser-languagedetector
34
+ ```
35
+
36
+ - **不要**再单独安装 `@farris/ui-vue`(被骨架层收敛)或 `@manohub/icon`(由骨架层依赖带入)。
37
+ - 应用侧**禁止** `import '@farris/ui-vue'`(护栏会拦),组件一律从 `@manohub/app-kit` 取。
38
+ - 本包发布在 **npm 官方仓**(公开),默认 registry 直接可装。工程若把默认 registry 指到了内网镜像,
39
+ 需显式声明作用域(凭据不入库):
40
+
41
+ ```ini
42
+ # .npmrc
43
+ @manohub:registry=https://registry.npmjs.org/
44
+ ```
45
+
46
+ - 工程配置两项:`tsconfig` 用 `moduleResolution: "bundler"`(包的子路径导出靠 `exports`),
47
+ `types` 含 `vite/client`;需要 JSX 页面时加 `@vitejs/plugin-vue-jsx`。
48
+ - 出现「同名类型互不兼容 / provide 取不到」这类双实例症状时,按 adoption.md §6 收敛单例
49
+ (`resolve.dedupe` + tsconfig `paths`)。
50
+
51
+ ### 2. 样式:固定三行,顺序即契约
52
+
53
+ ```css
54
+ /* src/style.css */
55
+ @import "@manohub/app-kit/reset.css";
56
+ @import "@manohub/app-kit/styles.css";
57
+ @import "./app.css";
58
+ ```
59
+
60
+ `styles.css` 内部已按 farris CSS → 令牌 → 桥接 → 组件样式汇总,**不要**自行打乱顺序或改写内部导入。
61
+
62
+ ### 3. 入口:统一走 `createSubApp`
63
+
64
+ ```ts
65
+ // src/main.ts
66
+ import { createSubApp } from '@manohub/app-kit/entry'
67
+ import App from './App.vue'
68
+ import { routes } from './router'
69
+ import './style.css'
70
+
71
+ export const { mount, unmount } = createSubApp({
72
+ rootComponent: App,
73
+ routes,
74
+ // 用 vue-i18n 的应用以插件注入;本包的 i18n 选项是 i18next 语义,两者不要混用
75
+ // extraPlugins: [i18n],
76
+ // 以 `index.html?xxx=1` 直开的入口(iframe / 选择模式)必须登记,否则首屏守卫会清空 query
77
+ // rootPathAliases: ['/index.html'],
78
+ })
79
+ ```
80
+
81
+ `createSubApp` 已统一负责 `.app-container` 包裹、pinia、farris 插件、VueQuery、路由、挂载、
82
+ 宿主语言监听、首屏守卫、`window.mount/unmount` 与 `microApp.mount/unmount` 协议、非微前端环境自动挂载 ——
83
+ **应用侧不要再手写这些**。全部选项见 adoption.md §3 或 CONTRACT.md §1。
84
+
85
+ ### 4. 第一个页面用模板 A
86
+
87
+ ```tsx
88
+ import { AppShell, AppSearchBox, AppTable, AppPagination } from '@manohub/app-kit'
89
+
90
+ <AppShell>
91
+ <AppShell.Header title="技能分类" toolbar={<AppSearchBox … />} />
92
+ <AppShell.Body mode="table">
93
+ <AppTable framed rows={rows} columns={cols} rowKey="id" />
94
+ </AppShell.Body>
95
+ <AppShell.Footer>
96
+ <AppPagination page={page} pageSize={size} total={total} … />
97
+ </AppShell.Footer>
98
+ </AppShell>
99
+ ```
100
+
101
+ 三种页面模板(列表 / 双栏 / 详情)、筛选与操作位归属、两级滚动契约见 CONTRACT.md §4;
102
+ 写页面前务必过一遍 CONTRACT.md 的「四条铁律」与反例库。
103
+
104
+ ### 5. 护栏接入(建议,机械判定规范合规)
105
+
106
+ ```jsonc
107
+ // appkit-guardrails.config.json(应用包根)
108
+ {
109
+ "$schema": "./node_modules/@manohub/app-kit/lint/guardrails.config.schema.json",
110
+ "apps": [{ "dir": ".", "name": "<app-name>", "prefixes": ["<your-prefix>"], "pending": false }],
111
+ "srcGlobs": ["src/**/*.ts", "src/**/*.tsx"],
112
+ "styleGlobs": ["src/**/*.css"],
113
+ "contractDoc": "node_modules/@manohub/app-kit/CONTRACT.md"
114
+ }
115
+ ```
116
+
117
+ ```jsonc
118
+ // package.json
119
+ {
120
+ "scripts": {
121
+ "lint": "appkit lint",
122
+ "lint:changed": "appkit lint --changed",
123
+ "lint:strict": "appkit lint --strict"
124
+ }
125
+ }
126
+ ```
127
+
128
+ 新工程没有存量,直接 `pending: false`;存量应用先产基线再逐步归零(口径见 CONTRACT.md §8)。
129
+
130
+ ### 6. 技能包落盘(AI 代理用)
131
+
132
+ ```bash
133
+ pnpm exec appkit install # → .codebuddy/skills/(幂等,包升级后重跑即刷新)
134
+ pnpm exec appkit install --also-claude # 同时 → .claude/skills/
135
+ ```
136
+
137
+ ### 7. 验收
138
+
139
+ | 项 | 命令 | 通过标准 |
140
+ |---|---|---|
141
+ | 类型 | `pnpm exec vue-tsc --noEmit` | 0 错(含 `@manohub/app-kit/entry` 等子路径可解析) |
142
+ | 构建 | `pnpm build` | 成功产出 |
143
+ | 护栏 | `pnpm exec appkit lint` | 无违规(或与基线一致;收口用 `--strict`) |
144
+ | 运行 | 启动后打开页面 | 骨架与组件样式正常、弹层落点正常 |
145
+
146
+ 还有一批**本包管不到、必须向宿主平台确认**的项(子应用产物路径与挂载方式、是否有 `index.html?xxx=1`
147
+ 直开入口、dev server 端口与代理、宿主下发的语言键与初始路由约定)—— 清单见 adoption.md §9.7。
148
+
19
149
  ## 分发形态
20
150
 
21
151
  由**源码分发**改为**产物分发**:`exports` 指向 `dist`(含类型声明与 `styles.css` / `reset.css`)。
@@ -26,7 +156,7 @@
26
156
  - `skills/` 是随包分发的 AI 技能包(见下);
27
157
  - 包内不存在 `dist/` 时先执行构建再联调。
28
158
 
29
- ## 技能包(AI 代理用)
159
+ ## 技能包(AI 代理用,随包分发)
30
160
 
31
161
  随包分发三个技能:`app-kit`(入口编排)/ `app-kit-migrate`(存量应用改造)/ `app-kit-dev`(日常页面开发)。
32
162
  在消费方工程根执行即可落到自己的技能目录(幂等,包升级后重跑即刷新):
@@ -0,0 +1,139 @@
1
+ import { type PropType, type SlotsType, type VNodeChild } from 'vue';
2
+ /**
3
+ * 抽屉(封装 farris FDrawer)。
4
+ *
5
+ * 契约:`open` 受控 + `onUpdate:open`(关闭时同发 `close`);`position` 只支持 `left` / `right`
6
+ * (上游 CSS 只给了 `f-drawer-wrapper-left/right` 的定位规则,`top` / `bottom` 无样式);
7
+ * 页脚用 `footer`(函数或节点)给,**不给页脚时不留空档**。
8
+ *
9
+ * 吃掉的上游坑:
10
+ * 1. **被当 DOM 容器的宽度**:上游 `width` 声明成 `String`、默认 `"300"`,值会原样落进行内 style
11
+ * ⇒ `width:300` 不是合法 CSS,抽屉宽度会退化成「内容宽度」。本件默认 `width: 480`(数值 ⇒ `480px`)。
12
+ * 2. **渲染宿主固定为 `.app-container`**:上游 `host` 默认 `"body"`,抽屉落容器外会同时丢令牌
13
+ * (`--ui-*`)与应用侧样式(微前端下被 scopecss 限定在容器内)—— 与 AppDialog 同一件事。
14
+ * 随之而来的第二个坑:上游在 host ≠ body 时给根节点加 `f-drawer-inline`(`position: absolute`),
15
+ * 而绝对定位的包含块在微前端里不可控,包内桥接层已把它改回 `fixed`
16
+ * (覆盖范围与改造前 `host: "body"` 一致)。
17
+ * 3. **页脚容器由上游统一提供**:`footerTemplate` 与内建按钮两条路都被包进 `div.f-drawer-footer px-2`
18
+ * (与 AppDialog 那条不同:modal 的模板路径是裸渲染,容器由本包补)。所以这里只透传内容,
19
+ * 应用侧不要再自绘页脚容器。
20
+ * 4. **恒定渲染的空页脚**:上游 `showFooter` 默认 `true`、高度取 `footerHeight`(默认 60px),
21
+ * 不给内容也会留一条 60px 空白(mcp 原先用应用侧 CSS `#mcp-config-drawer .f-drawer-footer{display:none!important}`
22
+ * 打补丁)。本件在无页脚时直接关掉该区块。
23
+ * 5. 上游没有 `close` 事件(只有 `update:modelValue` / `afterClose`),本件统一成 `onClose`。
24
+ *
25
+ * @example
26
+ * <AppDrawer open={visible} title="MCP 配置" width={640}
27
+ * onUpdate:open={(v) => (visible = v)}
28
+ * footer={() => <AppButton tone="primary" onClick={save}>保存</AppButton>}>
29
+ * …抽屉内容…
30
+ * </AppDrawer>
31
+ */
32
+ export declare const AppDrawer: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
33
+ /** 受控开合 */
34
+ open: {
35
+ type: BooleanConstructor;
36
+ default: boolean;
37
+ };
38
+ /** 抽屉根节点 id(上游必填)。不传时本件补一个稳定值 */
39
+ id: {
40
+ type: StringConstructor;
41
+ default: undefined;
42
+ };
43
+ title: {
44
+ type: StringConstructor;
45
+ default: string;
46
+ };
47
+ /** 抽屉宽度(px)。给数值——上游默认值是字符串 `"300"`,会生成非法 CSS */
48
+ width: {
49
+ type: NumberConstructor;
50
+ default: number;
51
+ };
52
+ position: {
53
+ type: PropType<"left" | "right">;
54
+ default: string;
55
+ };
56
+ /**
57
+ * 是否渲染上游自带标题栏。传 false 时关闭入口只剩遮罩点击
58
+ * (上游的关闭按钮长在标题栏里),需要自定义头部时自行承担关闭入口。
59
+ */
60
+ showHeader: {
61
+ type: BooleanConstructor;
62
+ default: boolean;
63
+ };
64
+ /**
65
+ * 关闭前钩子(点遮罩 / 点关闭按钮时触发)。
66
+ * 返回 `false` 可阻止关闭(如「有未保存改动,请确认」),支持返回 Promise。
67
+ */
68
+ beforeClose: {
69
+ type: PropType<() => boolean | void | Promise<boolean | void>>;
70
+ default: undefined;
71
+ };
72
+ /** 页脚(等价于 `footer` 插槽)。上游负责容器几何,本件只在「有内容」时才开页脚区块 */
73
+ footer: {
74
+ type: PropType<any>;
75
+ default: undefined;
76
+ };
77
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
78
+ [key: string]: any;
79
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("close" | "update:open")[], "close" | "update:open", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
80
+ /** 受控开合 */
81
+ open: {
82
+ type: BooleanConstructor;
83
+ default: boolean;
84
+ };
85
+ /** 抽屉根节点 id(上游必填)。不传时本件补一个稳定值 */
86
+ id: {
87
+ type: StringConstructor;
88
+ default: undefined;
89
+ };
90
+ title: {
91
+ type: StringConstructor;
92
+ default: string;
93
+ };
94
+ /** 抽屉宽度(px)。给数值——上游默认值是字符串 `"300"`,会生成非法 CSS */
95
+ width: {
96
+ type: NumberConstructor;
97
+ default: number;
98
+ };
99
+ position: {
100
+ type: PropType<"left" | "right">;
101
+ default: string;
102
+ };
103
+ /**
104
+ * 是否渲染上游自带标题栏。传 false 时关闭入口只剩遮罩点击
105
+ * (上游的关闭按钮长在标题栏里),需要自定义头部时自行承担关闭入口。
106
+ */
107
+ showHeader: {
108
+ type: BooleanConstructor;
109
+ default: boolean;
110
+ };
111
+ /**
112
+ * 关闭前钩子(点遮罩 / 点关闭按钮时触发)。
113
+ * 返回 `false` 可阻止关闭(如「有未保存改动,请确认」),支持返回 Promise。
114
+ */
115
+ beforeClose: {
116
+ type: PropType<() => boolean | void | Promise<boolean | void>>;
117
+ default: undefined;
118
+ };
119
+ /** 页脚(等价于 `footer` 插槽)。上游负责容器几何,本件只在「有内容」时才开页脚区块 */
120
+ footer: {
121
+ type: PropType<any>;
122
+ default: undefined;
123
+ };
124
+ }>> & Readonly<{
125
+ onClose?: ((...args: any[]) => any) | undefined;
126
+ "onUpdate:open"?: ((...args: any[]) => any) | undefined;
127
+ }>, {
128
+ title: string;
129
+ footer: any;
130
+ width: number;
131
+ id: string;
132
+ position: "left" | "right";
133
+ open: boolean;
134
+ showHeader: boolean;
135
+ beforeClose: () => boolean | void | Promise<boolean | void>;
136
+ }, SlotsType<{
137
+ default?: () => VNodeChild;
138
+ footer?: () => VNodeChild;
139
+ }>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,104 @@
1
+ import { FDrawer } from "@farris/ui-vue";
2
+ import { defineComponent, computed, h } from "vue";
3
+ import { resolveAppContainer } from "../services/app-container.js";
4
+ const FarrisDrawer = FDrawer;
5
+ let drawerSeq = 0;
6
+ const AppDrawer = /* @__PURE__ */ defineComponent({
7
+ name: "AppDrawer",
8
+ inheritAttrs: false,
9
+ emits: ["update:open", "close"],
10
+ props: {
11
+ /** 受控开合 */
12
+ open: {
13
+ type: Boolean,
14
+ default: false
15
+ },
16
+ /** 抽屉根节点 id(上游必填)。不传时本件补一个稳定值 */
17
+ id: {
18
+ type: String,
19
+ default: void 0
20
+ },
21
+ title: {
22
+ type: String,
23
+ default: ""
24
+ },
25
+ /** 抽屉宽度(px)。给数值——上游默认值是字符串 `"300"`,会生成非法 CSS */
26
+ width: {
27
+ type: Number,
28
+ default: 480
29
+ },
30
+ position: {
31
+ type: String,
32
+ default: "right"
33
+ },
34
+ /**
35
+ * 是否渲染上游自带标题栏。传 false 时关闭入口只剩遮罩点击
36
+ * (上游的关闭按钮长在标题栏里),需要自定义头部时自行承担关闭入口。
37
+ */
38
+ showHeader: {
39
+ type: Boolean,
40
+ default: true
41
+ },
42
+ /**
43
+ * 关闭前钩子(点遮罩 / 点关闭按钮时触发)。
44
+ * 返回 `false` 可阻止关闭(如「有未保存改动,请确认」),支持返回 Promise。
45
+ */
46
+ beforeClose: {
47
+ type: Function,
48
+ default: void 0
49
+ },
50
+ /** 页脚(等价于 `footer` 插槽)。上游负责容器几何,本件只在「有内容」时才开页脚区块 */
51
+ footer: {
52
+ type: [Function, Object, Array, String],
53
+ default: void 0
54
+ }
55
+ },
56
+ slots: Object,
57
+ setup(props, {
58
+ slots,
59
+ attrs,
60
+ emit
61
+ }) {
62
+ const fallbackId = `ak-drawer-${drawerSeq += 1}`;
63
+ const drawerId = computed(() => props.id ?? fallbackId);
64
+ const handleUpdate = (value) => {
65
+ emit("update:open", value);
66
+ if (!value) emit("close");
67
+ };
68
+ const renderFooter = () => slots.footer ? slots.footer() : typeof props.footer === "function" ? props.footer() : props.footer;
69
+ return () => {
70
+ const hasFooter = !!slots.footer || props.footer !== void 0;
71
+ return h(FarrisDrawer, {
72
+ id: drawerId.value,
73
+ modelValue: props.open,
74
+ title: props.title,
75
+ width: props.width,
76
+ position: props.position,
77
+ showHeader: props.showHeader,
78
+ beforeClose: props.beforeClose,
79
+ /**
80
+ * 无内容时关掉上游页脚区块 —— 否则恒定渲染一条 `footerHeight`(默认 60px)的空白。
81
+ */
82
+ showFooter: hasFooter,
83
+ /** 渲染宿主:见头注释第 2 条(farris 默认 `"body"` 会让抽屉样式全丢) */
84
+ host: resolveAppContainer(),
85
+ customClass: ["ak-drawer", attrs.class].filter(Boolean).join(" "),
86
+ customStyle: attrs.style,
87
+ "onUpdate:modelValue": handleUpdate
88
+ }, {
89
+ default: () => slots.default?.(),
90
+ /**
91
+ * 页脚走 `footerTemplate` **插槽**而不是同名 prop:上游把它声明成 `Object`,
92
+ * 传函数会多刷一条 prop 类型告警;而渲染逻辑是「prop 优先,其次插槽」,
93
+ * 不传 prop 时插槽这条路完全等价。
94
+ */
95
+ ...hasFooter ? {
96
+ footerTemplate: () => renderFooter()
97
+ } : {}
98
+ });
99
+ };
100
+ }
101
+ });
102
+ export {
103
+ AppDrawer
104
+ };
package/dist/index.d.ts CHANGED
@@ -24,6 +24,7 @@ export { AppTable, type AppTableColumn, type AppTableProps, type AppTableSelecti
24
24
  export { AppPagination } from './components/app-pagination';
25
25
  export { AppBadge, type AppBadgeTone, type AppBadgeShape, type AppBadgeSize, type AppBadgeDot } from './components/app-badge';
26
26
  export { AppDialog } from './components/app-dialog';
27
+ export { AppDrawer } from './components/app-drawer';
27
28
  export { AppTabs, type AppTabItem } from './components/app-tabs';
28
29
  export { AppForm, type AppFormProps, type AppFormItemProps, type AppFormLabelAlign, type AppFormCompound, } from './components/app-form';
29
30
  export { AppSection, type AppSectionProps } from './components/app-section';
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { AppTable } from "./components/app-table.js";
18
18
  import { AppPagination } from "./components/app-pagination.js";
19
19
  import { AppBadge } from "./components/app-badge.js";
20
20
  import { AppDialog } from "./components/app-dialog.js";
21
+ import { AppDrawer } from "./components/app-drawer.js";
21
22
  import { AppTabs } from "./components/app-tabs.js";
22
23
  import { AppForm } from "./components/app-form.js";
23
24
  import { AppSection } from "./components/app-section.js";
@@ -34,6 +35,7 @@ export {
34
35
  AppButton,
35
36
  AppCheckbox,
36
37
  AppDialog,
38
+ AppDrawer,
37
39
  AppFilter,
38
40
  AppForm,
39
41
  AppIconButton,
@@ -110,6 +110,14 @@ body > .fv-tooltip.tooltip {
110
110
  min-height: 0;
111
111
  }
112
112
 
113
+ /* AppDrawer:抽屉被 Teleport 进 `.app-container`(拿令牌与应用侧样式)后,上游会给根节点加
114
+ * `f-drawer-inline` 把它从 `position: fixed` 改成 `absolute` —— 而微前端里绝对定位的包含块
115
+ * 不可控(`.app-container` 自身未定位,宿主层级随门户而变),抽屉与遮罩会错位。
116
+ * 这里把包含块语义压回视口,覆盖范围与改造前 `host: "body"` 时完全一致。 */
117
+ .app-container .f-drawer.f-drawer-inline {
118
+ position: fixed;
119
+ }
120
+
113
121
  /* ===== AppSteps 只读档(clickable=false)的光标 =====
114
122
  * 上游 FStep 的 `clickable` prop 在 1.8.4 里**完全没被使用**,`li.clickable` 只由单个 step 的
115
123
  * `disabled` 决定 ⇒ 整条只读时上游仍给非禁用项手型光标。拦截已由 AppSteps 的门控承担,
@@ -240,6 +240,40 @@ test('规则级豁免:--strict 时豁免一律失效(收口验收口径)',
240
240
  }
241
241
  })
242
242
 
243
+ test('统一入口:--strict 必须透传给三条子护栏(否则收口验收空转)', () => {
244
+ const root = makeConsumer('violations-src')
245
+ try {
246
+ // 三条规则整类豁免 + 不挂起:平时全绿,收口预演必须拦断 —— 差别只在 `--strict` 有没有传下去
247
+ writeConfig(root, [
248
+ {
249
+ dir: 'app',
250
+ name: 'fixture-app',
251
+ prefixes: ['other-'],
252
+ pending: false,
253
+ waivedRules: [
254
+ { rule: 'api/*', reason: '分阶段迁移:组件替换在下一批次', since: '2026-09-18' },
255
+ { rule: 'style/*', reason: '分阶段迁移:样式收口在下一批次', since: '2026-09-18' },
256
+ { rule: 'structure/*', reason: '本 fixture 用于验证参数透传', since: '2026-09-18' },
257
+ ],
258
+ },
259
+ ])
260
+
261
+ const plain = runAuditWith('run-all.mjs', root)
262
+ assert.equal(plain.code, 0, '平时:三整类豁免 → 全绿')
263
+ assert.ok(plain.out.includes('全绿'), `平时汇总行应为全绿,实际:${plain.out}`)
264
+
265
+ const strict = runAuditWith('run-all.mjs', root, ['--strict'])
266
+ assert.equal(strict.code, 1, '--strict:豁免失效 → 必须拦断(收口验收的唯一口径)')
267
+ assert.ok(!strict.out.includes('全绿'), `--strict 不应再报全绿,实际:${strict.out}`)
268
+ // 三条子护栏都要真的按 strict 跑(只透传给一条同样会退化成半空转)
269
+ for (const rule of ['api/farris-import', 'style/visual', 'structure/no-shell']) {
270
+ assert.ok(strict.out.includes(rule), `--strict 报告里应出现 ${rule},实际:${strict.out}`)
271
+ }
272
+ } finally {
273
+ rmSync(root, { recursive: true, force: true })
274
+ }
275
+ })
276
+
243
277
  test('规则级豁免:0 条命中时提示可撤销(豁免只该在「还没迁到那层」期间存在)', () => {
244
278
  const root = makeConsumer('clean-src')
245
279
  try {
package/lint/run-all.mjs CHANGED
@@ -26,6 +26,13 @@ const AUDITS = ['style-audit.mjs', 'component-audit.mjs', 'structure-audit.mjs']
26
26
  const args = parseArgs()
27
27
  const passthrough = process.argv.slice(2).filter((a) => a.startsWith('--app=') || a.startsWith('--cwd='))
28
28
  if (args.changed) passthrough.push('--changed')
29
+ /**
30
+ * `--strict` 必须透传:它决定的是**子护栏**的拦截口径(存量挂起与规则豁免失效),
31
+ * 而不只是本脚本的输出。漏传过一次 —— 表现是 `appkit lint --strict` 恒绿(豁免照旧生效),
32
+ * 收口验收因此变成空转,看输出完全看不出问题。
33
+ * 回归由 `lint/__tests__/guardrails.spec.mjs` 的「统一入口:--strict 必须透传」锁住。
34
+ */
35
+ if (args.strict) passthrough.push('--strict')
29
36
  if (args.json) passthrough.push('--json')
30
37
 
31
38
  const runOne = (script) =>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manohub/app-kit",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "子应用统一骨架层:入口编排(createSubApp)、布局契约(AppShell)、页面组件、原子件、服务、样式底座。farris 被收敛在本包内部,对外只暴露标准 API。",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@farris/ui-vue": "^1.8.4",
66
- "@manohub/icon": "^0.2.3"
66
+ "@manohub/icon": "^0.2.5"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@tanstack/vue-query": "catalog:",