@manohub/app-kit 0.1.0

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.
Files changed (103) hide show
  1. package/CONTRACT.md +592 -0
  2. package/README.md +27 -0
  3. package/dist/atoms/app-button.d.ts +90 -0
  4. package/dist/atoms/app-button.js +74 -0
  5. package/dist/atoms/app-checkbox.d.ts +86 -0
  6. package/dist/atoms/app-checkbox.js +78 -0
  7. package/dist/atoms/app-icon-button.d.ts +79 -0
  8. package/dist/atoms/app-icon-button.js +54 -0
  9. package/dist/atoms/app-input.d.ts +118 -0
  10. package/dist/atoms/app-input.js +83 -0
  11. package/dist/atoms/app-layout.d.ts +348 -0
  12. package/dist/atoms/app-layout.js +102 -0
  13. package/dist/atoms/app-notice.d.ts +90 -0
  14. package/dist/atoms/app-notice.js +59 -0
  15. package/dist/atoms/app-query-state.d.ts +191 -0
  16. package/dist/atoms/app-query-state.js +161 -0
  17. package/dist/atoms/app-radio-card.d.ts +80 -0
  18. package/dist/atoms/app-radio-card.js +72 -0
  19. package/dist/atoms/app-search-box.d.ts +80 -0
  20. package/dist/atoms/app-search-box.js +64 -0
  21. package/dist/atoms/app-select.d.ts +106 -0
  22. package/dist/atoms/app-select.js +74 -0
  23. package/dist/atoms/app-switch.d.ts +72 -0
  24. package/dist/atoms/app-switch.js +66 -0
  25. package/dist/atoms/app-textarea.d.ts +89 -0
  26. package/dist/atoms/app-textarea.js +61 -0
  27. package/dist/atoms/app-tooltip.d.ts +70 -0
  28. package/dist/atoms/app-tooltip.js +48 -0
  29. package/dist/atoms/atoms.css +679 -0
  30. package/dist/atoms/state-illustration.d.ts +21 -0
  31. package/dist/atoms/state-illustration.js +101 -0
  32. package/dist/components/app-badge.d.ts +72 -0
  33. package/dist/components/app-badge.js +40 -0
  34. package/dist/components/app-dialog.d.ts +164 -0
  35. package/dist/components/app-dialog.js +121 -0
  36. package/dist/components/app-filter.d.ts +173 -0
  37. package/dist/components/app-filter.js +200 -0
  38. package/dist/components/app-form.d.ts +446 -0
  39. package/dist/components/app-form.js +159 -0
  40. package/dist/components/app-pagination.d.ts +78 -0
  41. package/dist/components/app-pagination.js +57 -0
  42. package/dist/components/app-panel.d.ts +399 -0
  43. package/dist/components/app-panel.js +213 -0
  44. package/dist/components/app-section.d.ts +68 -0
  45. package/dist/components/app-section.js +28 -0
  46. package/dist/components/app-steps.d.ts +115 -0
  47. package/dist/components/app-steps.js +108 -0
  48. package/dist/components/app-table.d.ts +415 -0
  49. package/dist/components/app-table.js +479 -0
  50. package/dist/components/app-tabs.d.ts +58 -0
  51. package/dist/components/app-tabs.js +54 -0
  52. package/dist/components/app-tree.d.ts +273 -0
  53. package/dist/components/app-tree.js +222 -0
  54. package/dist/components/components.css +597 -0
  55. package/dist/composables/use-client-pagination.d.ts +39 -0
  56. package/dist/composables/use-client-pagination.js +29 -0
  57. package/dist/entry/create-query-client.d.ts +8 -0
  58. package/dist/entry/create-query-client.js +14 -0
  59. package/dist/entry/create-sub-app.d.ts +45 -0
  60. package/dist/entry/create-sub-app.js +110 -0
  61. package/dist/entry/index.d.ts +4 -0
  62. package/dist/entry/index.js +12 -0
  63. package/dist/entry/initial-guard.d.ts +12 -0
  64. package/dist/entry/initial-guard.js +23 -0
  65. package/dist/index.d.ts +37 -0
  66. package/dist/index.js +67 -0
  67. package/dist/providers/setup-i18n.d.ts +19 -0
  68. package/dist/providers/setup-i18n.js +24 -0
  69. package/dist/services/app-container.d.ts +15 -0
  70. package/dist/services/app-container.js +7 -0
  71. package/dist/services/app-context.d.ts +4 -0
  72. package/dist/services/app-context.js +17 -0
  73. package/dist/services/index.d.ts +5 -0
  74. package/dist/services/loading.d.ts +16 -0
  75. package/dist/services/loading.js +27 -0
  76. package/dist/services/message-box.d.ts +84 -0
  77. package/dist/services/message-box.js +65 -0
  78. package/dist/services/modal.d.ts +51 -0
  79. package/dist/services/modal.js +51 -0
  80. package/dist/services/notify.d.ts +24 -0
  81. package/dist/services/notify.js +22 -0
  82. package/dist/shell/AppShell.d.ts +139 -0
  83. package/dist/shell/AppShell.js +171 -0
  84. package/dist/shell/shell.css +153 -0
  85. package/dist/styles/farris-bridge.css +129 -0
  86. package/dist/styles/index.css +16 -0
  87. package/dist/styles/markdown.css +85 -0
  88. package/dist/styles/reset.css +74 -0
  89. package/dist/styles/tokens.css +111 -0
  90. package/lint/__tests__/fixtures/clean-src/styles.css +14 -0
  91. package/lint/__tests__/fixtures/clean-src/views/good-page.tsx +15 -0
  92. package/lint/__tests__/fixtures/violations-src/styles.css +18 -0
  93. package/lint/__tests__/fixtures/violations-src/views/bad-page.tsx +38 -0
  94. package/lint/__tests__/fixtures/violations-src/views/no-shell-page.tsx +4 -0
  95. package/lint/__tests__/guardrails.spec.mjs +144 -0
  96. package/lint/component-audit.mjs +131 -0
  97. package/lint/guardrails.config.schema.json +74 -0
  98. package/lint/pre-commit.sample +26 -0
  99. package/lint/run-all.mjs +59 -0
  100. package/lint/shared.mjs +347 -0
  101. package/lint/structure-audit.mjs +254 -0
  102. package/lint/style-audit.mjs +200 -0
  103. package/package.json +78 -0
@@ -0,0 +1,129 @@
1
+ /**
2
+ * farris 桥接层:单向令牌映射 + farris 覆盖白名单。
3
+ *
4
+ * - 品牌色注入 farris:全仓唯一一处,替代各 app 自建 --f-theme-* 覆盖块。
5
+ * - 覆盖白名单内的 `!important` 是全仓唯一允许出现 `!important` 的地方;
6
+ * 压过 farris 统一用 `.app-container .f-*` 两段式,不打 important 军备竞赛。
7
+ */
8
+
9
+ /* 品牌色注入 farris 主题槽位(全仓唯一一处,替代各 app 自建的 --f-theme-* 覆盖块)。
10
+ 槽位语义沿用 farris 主题变量:01/03 主色、02/04/06/07 渐变、05/08 浅主色;
11
+ aid-02/03/04 为浅底(表格选中行、悬停底色等)。取值一律由 --ui-primary 派生,
12
+ 局部换主题只需在更深的容器上重设 --ui-primary。 */
13
+ .app-container {
14
+ --f-theme-01: color-mix(in oklab, var(--ui-primary) 88%, black);
15
+ --f-theme-02: linear-gradient(135deg, var(--ui-primary), color-mix(in oklab, var(--ui-primary) 88%, black));
16
+ --f-theme-03: var(--ui-primary);
17
+ --f-theme-04: linear-gradient(135deg, color-mix(in oklab, var(--ui-primary) 85%, white), var(--ui-primary));
18
+ --f-theme-05: color-mix(in oklab, var(--ui-primary) 85%, white);
19
+ --f-theme-06: linear-gradient(135deg, color-mix(in oklab, var(--ui-primary) 85%, white), var(--ui-primary));
20
+ --f-theme-07: linear-gradient(214deg, var(--ui-primary), color-mix(in oklab, var(--ui-primary) 85%, white));
21
+ --f-theme-08: color-mix(in oklab, var(--ui-primary) 85%, white);
22
+ --f-aid-02: color-mix(in oklab, var(--ui-primary) 16%, white);
23
+ --f-aid-03: color-mix(in oklab, var(--ui-primary) 10%, white);
24
+ --f-aid-04: color-mix(in oklab, var(--ui-primary) 6%, white);
25
+
26
+ /* 文本控件高度(= farris `.form-control` 的规则原文 `height: calc(2px + 1.53574rem)`):
27
+ rem ⇒ 高度随根字号变(门户里实测 ≈24px),所以本包任何「要与控件首行对齐」的地方
28
+ (AppForm 的 label 列)**必须复用它**,硬写 px 令牌(32)会当场对不齐。
29
+ 取值与上游绑定的地方只此一处,由 app-form.spec.ts 锁住。 */
30
+ --ui-form-control-height: calc(2px + 1.53574rem);
31
+ }
32
+
33
+ /* ===== FNotify 位置修复 =====
34
+ * farris index.css 中第二个全局 .farris-notify 规则
35
+ * (display:flex; width:100%; left:0; top:0; ...)
36
+ * 覆盖了第一个规则(position:fixed; z-index:999999),导致 notify 通过 inline style
37
+ * 计算的 left/top/transform 定位失效。这里重置为默认值让 inline style 重新生效。
38
+ */
39
+ .app-container .farris-notify {
40
+ width: auto;
41
+ display: block;
42
+ left: auto;
43
+ top: auto;
44
+ }
45
+
46
+ /* FPageHeader 自带 margin 0 .5rem,在壳层布局中由 AppShell.Header 控制间距 */
47
+ .app-container .f-page-header {
48
+ margin: 0;
49
+ flex-shrink: 0;
50
+ }
51
+
52
+ /* farris 弹层(popover/modal teleport 到 body)不受 .app-container 作用域约束,须全局覆盖 */
53
+ body > .popover.popover-fitcontent.bs-popover-top {
54
+ position: fixed !important;
55
+ /* 已用 position:fixed 修正包含块;下文「变量平移」是另一套互斥修法,此处显式关掉避免叠加 */
56
+ translate: none !important;
57
+ }
58
+ .popover.f-combo-list-wrapper.popover-limitmax .f-combo-list-container {
59
+ max-height: none !important;
60
+ }
61
+
62
+ /* FTooltip(AppTooltip)浮层在微前端下会因 farris 自身定位规则偏移,
63
+ 原先 mcp / repository 各自在应用 style.css 里打补丁,现统一到桥接层白名单。 */
64
+ .fv-tooltip.tooltip {
65
+ position: fixed !important;
66
+ translate: none !important;
67
+ }
68
+
69
+ /* ===== 微前端容器原点补偿(变量驱动,宿主应用写入、桥接层消费) =====
70
+ * 问题:farris 面板定位把 `host === "body"` 当成「包含块在文档原点 (0,0)」,这只有在
71
+ * 面板包含块正好是初始包含块时才成立。子应用被门户挂进带 `position: relative` 的容器时,
72
+ * 包含块变成那个容器 ⇒ 面板整体多平移一个「容器文档原点」(实测横竖各多 259/185)。
73
+ *
74
+ * 修法:宿主应用把容器原点偏差写进 body 上的 CSS 变量(`--ibp-popup-shift-x/y`,
75
+ * 见 vm 子应用的 `utils/app-host.ts`),这里只负责把变量施加到弹层上:
76
+ * `translate` 是独立于 `transform` 的属性,不会覆盖 farris 用来对齐箭头的行内 `transform`。
77
+ *
78
+ * 为什么放桥接层而不是应用侧:弹层 Teleport 到 body、不受 `.app-container` 作用域约束,
79
+ * 应用侧的 `.popover { … }` 会同时撞护栏的「选择器前缀」与「farris 内部类」两条红线
80
+ * (原先 vm 子应用就是靠应用侧规则绕过,属历史债)。
81
+ *
82
+ * 对未写该变量的应用是 **no-op**(默认 0px,等于不位移);
83
+ * 与上面 `position: fixed` 的两条规则互斥,那两条已显式 `translate: none` 关掉本条。
84
+ */
85
+ body > .popover,
86
+ body > .fv-tooltip.tooltip {
87
+ translate: var(--ibp-popup-shift-x, 0px) var(--ibp-popup-shift-y, 0px);
88
+ }
89
+
90
+ /* ===== .ak-table(AppTable)表头高度(按需) =====
91
+ * AppTable 的 `headerHeight` 通过 `--ak-table-header-height` 下发并同时挂 `ak-table--custom-header`;
92
+ * farris 把表头高度按固定常量**内联**写到 `.fv-grid-header` 上,故必须用 `!important` 压过:
93
+ * 内联样式非 important,会被这里的 important 规则覆盖。
94
+ *
95
+ * 用途:`AppTableColumn.title` 传函数(双行表头)时装不下 32px 的默认表头。
96
+ * 不传 `headerHeight` 的应用不会挂这个类 ⇒ 本条完全不生效(对其余应用零影响)。
97
+ */
98
+ .ak-table.ak-table--custom-header .fv-grid-header,
99
+ .ak-table.ak-table--custom-header .fv-grid-header-columns,
100
+ .ak-table.ak-table--custom-header .fv-grid-header-primary,
101
+ .ak-table.ak-table--custom-header .fv-grid-header-cell {
102
+ height: var(--ak-table-header-height, 32px) !important;
103
+ align-items: stretch !important;
104
+ }
105
+
106
+ /* FDrower/FTabs 的 fill 模式在 1.8.4 下类名不生效(mcp 配置抽屉原先自带补丁) */
107
+ .app-container .f-drawer-body {
108
+ display: flex;
109
+ flex-direction: column;
110
+ min-height: 0;
111
+ }
112
+
113
+ /* ===== AppSteps 只读档(clickable=false)的光标 =====
114
+ * 上游 FStep 的 `clickable` prop 在 1.8.4 里**完全没被使用**,`li.clickable` 只由单个 step 的
115
+ * `disabled` 决定 ⇒ 整条只读时上游仍给非禁用项手型光标。拦截已由 AppSteps 的门控承担,
116
+ * 这里只把光标压回默认值(选择器两段式压过上榜单类规则,无需 !important)。 */
117
+ .app-container .ak-steps--static .f-progress-step li.clickable {
118
+ cursor: default;
119
+ }
120
+
121
+ /* ===== 消息框(FMessageBox)正文按**文本**换行 =====
122
+ * `messageBox` 的口径是"用文本传消息"(见 services/message-box.ts):换行用 `\n`。
123
+ * 上游这两行默认 `white-space: normal`,`\n` 会被折成空格 ⇒ 调用方只能拼 `<div>` 来分段。
124
+ * 这里放开换行,文本路才真正可用;已经带 HTML 标签的长文说明不受影响(块级标签照旧成段)。 */
125
+ .toast-msg-title,
126
+ .toast-msg-detail {
127
+ white-space: pre-line;
128
+ }
129
+
@@ -0,0 +1,16 @@
1
+ /**
2
+ * app-kit 样式汇总入口(apps 侧 `@import "@manohub/app-kit/styles.css"` 唯一入口)。
3
+ *
4
+ * 导入顺序即契约:farris CSS 最前(供内部封装件使用),随后令牌 → 桥接 → 组件样式。
5
+ * apps 侧 style.css 固定三行:
6
+ * @import "@manohub/app-kit/reset.css";
7
+ * @import "@manohub/app-kit/styles.css";
8
+ * @import "./app.css";
9
+ */
10
+ @import "@farris/ui-vue/index.css";
11
+ @import "./tokens.css";
12
+ @import "./farris-bridge.css";
13
+ @import "../shell/shell.css";
14
+ @import "../atoms/atoms.css";
15
+ @import "../components/components.css";
16
+ @import "./markdown.css";
@@ -0,0 +1,85 @@
1
+ /**
2
+ * markdown 富文本预设:h1~h6 / 列表 / 代码块 / 引用的元素级排版。
3
+ *
4
+ * 业务侧禁止自写富文本元素排版(护栏禁 font-size / 裸元素选择器),
5
+ * markdown 容器统一挂 `.ak-markdown`,排版一次定型在这里。
6
+ */
7
+
8
+ .ak-markdown {
9
+ font-size: var(--ui-font-body);
10
+ line-height: 1.7;
11
+ color: var(--ui-base-content);
12
+ word-break: break-word;
13
+ }
14
+
15
+ .ak-markdown h1 { font-size: 24px; font-weight: 600; margin: 16px 0 8px; }
16
+ .ak-markdown h2 { font-size: 20px; font-weight: 600; margin: 14px 0 8px; }
17
+ .ak-markdown h3 { font-size: 17px; font-weight: 600; margin: 12px 0 6px; }
18
+ .ak-markdown h4 { font-size: 15px; font-weight: 600; margin: 12px 0 6px; }
19
+ .ak-markdown h5,
20
+ .ak-markdown h6 { font-size: 14px; font-weight: 600; margin: 10px 0 6px; }
21
+
22
+ .ak-markdown p { margin: 8px 0; }
23
+
24
+ .ak-markdown ul,
25
+ .ak-markdown ol {
26
+ margin: 8px 0;
27
+ padding-left: 24px;
28
+ }
29
+ .ak-markdown ul { list-style: disc; }
30
+ .ak-markdown ol { list-style: decimal; }
31
+ .ak-markdown li { margin: 4px 0; }
32
+
33
+ .ak-markdown blockquote {
34
+ margin: 8px 0;
35
+ padding: 8px 12px;
36
+ border-left: 3px solid var(--ui-primary);
37
+ background: var(--ui-base-200);
38
+ color: var(--ui-base-content-muted);
39
+ }
40
+
41
+ .ak-markdown code {
42
+ font-family: 'JetBrains Mono', Consolas, 'Courier New', monospace;
43
+ font-size: 13px;
44
+ background: var(--ui-base-200);
45
+ border-radius: var(--ui-radius-sm);
46
+ padding: 2px 4px;
47
+ }
48
+
49
+ .ak-markdown pre {
50
+ margin: 8px 0;
51
+ padding: 12px;
52
+ background: var(--ui-base-200);
53
+ border-radius: var(--ui-radius-md);
54
+ overflow-x: auto;
55
+ }
56
+ .ak-markdown pre code {
57
+ background: transparent;
58
+ padding: 0;
59
+ }
60
+
61
+ .ak-markdown a {
62
+ color: var(--ui-primary);
63
+ text-decoration: none;
64
+ }
65
+ .ak-markdown a:hover {
66
+ text-decoration: underline;
67
+ }
68
+
69
+ .ak-markdown table {
70
+ border-collapse: collapse;
71
+ margin: 8px 0;
72
+ }
73
+ .ak-markdown th,
74
+ .ak-markdown td {
75
+ border: 1px solid var(--ui-base-300);
76
+ padding: 6px 10px;
77
+ }
78
+ .ak-markdown th {
79
+ background: var(--ui-base-200);
80
+ font-weight: 600;
81
+ }
82
+
83
+ .ak-markdown img {
84
+ max-width: 100%;
85
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * 通用最小 reset(scope 到 .app-container)。
3
+ *
4
+ * 放在导入链最前(先于 farris CSS),让 farris 自己的 base 覆盖 reset,
5
+ * 从根上消除「全局 reset 压过 UI 库」的冲突。
6
+ *
7
+ * ⚠️ 作用域一律用 `:where(.app-container)` 包裹:
8
+ * `:where()` 特异性为 0,等价于 Tailwind preflight 的纯元素选择器(0,0,1),
9
+ * 而 `.app-container button`(0,1,1)会反压 farris 的 `.btn-primary`(0,1,0)——
10
+ * 曾导致主按钮文字变黑(farris 的 `color: var(--f-text-00)` 被 reset 的 `color: inherit` 覆盖)。
11
+ *
12
+ * 注意:
13
+ * - micro-app 的 scopecss 不作用域化 html/:root/body,因此任何全局选择器都要克制;
14
+ * html/body/#app 的高度规则仅供独立运行场景,micro-app 下由宿主容器承担高度。
15
+ * - 不要加 `.app-container ul { list-style: none }`:markdown 富文本依赖 ul 圆点,
16
+ * 富文本排版统一走 app-kit 的 `.ak-markdown` 预设。
17
+ */
18
+
19
+ :where(.app-container),
20
+ :where(.app-container) *,
21
+ :where(.app-container) *::before,
22
+ :where(.app-container) *::after {
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ .app-container {
27
+ font-family: 'Noto Sans SC', Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
28
+ font-size: 14px;
29
+ line-height: 1.5;
30
+ color: var(--ui-base-content);
31
+ background: var(--ui-base-100);
32
+ height: 100%;
33
+ min-width: 320px;
34
+ display: flex;
35
+ flex-direction: column;
36
+ overflow: hidden;
37
+ font-synthesis: none;
38
+ text-rendering: optimizeLegibility;
39
+ -webkit-font-smoothing: antialiased;
40
+ -moz-osx-font-smoothing: grayscale;
41
+ }
42
+
43
+ :where(.app-container) h1,
44
+ :where(.app-container) h2,
45
+ :where(.app-container) h3,
46
+ :where(.app-container) h4,
47
+ :where(.app-container) h5,
48
+ :where(.app-container) h6,
49
+ :where(.app-container) p,
50
+ :where(.app-container) figure {
51
+ margin: 0;
52
+ }
53
+
54
+ :where(.app-container) button,
55
+ :where(.app-container) input,
56
+ :where(.app-container) textarea,
57
+ :where(.app-container) select {
58
+ font: inherit;
59
+ color: inherit;
60
+ }
61
+
62
+ /* 独立运行场景:子应用挂 #app 需要整链高度;micro-app 下此规则被宿主容器覆盖,无副作用 */
63
+ html,
64
+ body,
65
+ #app {
66
+ margin: 0;
67
+ padding: 0;
68
+ width: 100%;
69
+ height: 100%;
70
+ overflow: hidden;
71
+ }
72
+ body label {
73
+ margin-bottom: 0;
74
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * 令牌层 —— 全仓唯一视觉令牌定义处。
3
+ *
4
+ * 锚定 `.app-container` 而非 `:root`:micro-app 的 scopecss 不作用域化 `:root`,
5
+ * 定义在 `:root` 的变量会泄漏到宿主全局(多子应用互染、卸载残留);
6
+ * `.app-container` 由 createSubApp 统一包裹,取值一致且作用域安全。
7
+ *
8
+ * 局部换主题 = 在更深的容器上重设变量,如 `.role-tree-pane { --ui-primary: #7C3AED; }`。
9
+ */
10
+
11
+ .app-container {
12
+ /* 品牌槽位:全仓唯一一处定义 */
13
+ --ui-primary: #2660FF;
14
+ --ui-primary-content: #FFFFFF;
15
+ --ui-primary-hover: #1A4FE0;
16
+ --ui-primary-weak: rgba(38, 96, 255, 0.08);
17
+ --ui-primary-subtle: #F0F7FF;
18
+
19
+ /* 中性阶(收敛自批次 5 盘点的 55 种字面量灰/白色,禁止再自由取值) */
20
+ --ui-base-50: #FAFAFA;
21
+ --ui-base-100: #FFFFFF;
22
+ --ui-base-200: #F5F7FA;
23
+ --ui-base-300: #E6E6E6;
24
+ --ui-base-400: #DCDCDC;
25
+ --ui-base-content: #1A1A1A;
26
+ --ui-base-content-muted: #666666;
27
+ --ui-base-content-subtle: #999999;
28
+
29
+ /* 状态色:主色 + 浅底 + 描边三档(标签 / 提示块 / 校验态) */
30
+ --ui-info: #3B82F6; --ui-info-content: #FFFFFF;
31
+ --ui-success: #22C55E; --ui-success-content: #FFFFFF;
32
+ --ui-warning: #F59E0B; --ui-warning-content: #1A1A1A;
33
+ --ui-error: #DC2626; --ui-error-content: #FFFFFF;
34
+ --ui-info-weak: #EFF6FF; --ui-info-border: #BFDBFE;
35
+ --ui-success-weak: #ECFDF5; --ui-success-border: #A7F3D0;
36
+ --ui-warning-weak: #FFFBEB; --ui-warning-border: #FDE68A;
37
+ --ui-error-weak: #FEF2F2; --ui-error-border: #FECACA;
38
+ --ui-success-strong: #059669;
39
+ --ui-warning-strong: #B45309;
40
+
41
+ /* 密度档位(页头 18px/56px 条高依据 MH后台-0911 设计稿 node 0:3888) */
42
+ --ui-font-caption: 13px;
43
+ --ui-font-body: 14px;
44
+ --ui-font-heading: 16px;
45
+ --ui-font-title: 18px;
46
+ --ui-font-display: 20px;
47
+
48
+ --ui-line-caption: 20px;
49
+ --ui-line-body: 22px;
50
+ --ui-line-heading: 24px;
51
+ --ui-line-title: 28px;
52
+
53
+ --ui-weight-regular: 400;
54
+ --ui-weight-medium: 500;
55
+ --ui-weight-semibold: 600;
56
+
57
+ --ui-font-mono: Consolas, Monaco, 'Courier New', monospace;
58
+
59
+ --ui-radius-sm: 4px;
60
+ /* 控件档圆角 6px:对齐 @aihub/theme 的 --f-radius-control(树节点行 / 说明条 / 输入控件都用它) */
61
+ --ui-radius-control: 6px;
62
+ --ui-radius-md: 8px;
63
+ --ui-radius-full: 9999px;
64
+
65
+ /* 说明条 / 警告提示(AppNotice):数值取自 @aihub/theme `components/alert.css` 实测
66
+ (高 42、投影 0 2 10 黑 8%、文案 15px、警示/错误底取 Alert 专页样张,与语义 03 档两说时以专页为准) */
67
+ --ui-notice-height: 42px;
68
+ --ui-notice-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
69
+ --ui-notice-font: 15px;
70
+ --ui-notice-warning-bg: #fdf4ea;
71
+ --ui-notice-error-bg: #fdecec;
72
+
73
+ /* 树节点行:对齐 @aihub/theme `components/tree.css` 实测
74
+ (行高 30、字号 13、缩进步进 10、箭头 8、点击瞬态 #F4F5F9、滑过 Ne11、选中 Au02+Th03) */
75
+ --ui-tree-row-height: 30px;
76
+ --ui-tree-row-height-compact: 28px;
77
+ --ui-tree-indent-base: 8px;
78
+ --ui-tree-indent-step: 10px;
79
+ --ui-tree-active-bg: #f4f5f9;
80
+ --ui-tree-caret-size: 8px;
81
+
82
+ --ui-space-1: 4px; --ui-space-2: 8px; --ui-space-3: 12px;
83
+ --ui-space-4: 16px; --ui-space-6: 24px;
84
+
85
+ --ui-control-height-sm: 28px;
86
+ --ui-control-height-md: 32px;
87
+ --ui-row-height: 32px;
88
+ --ui-header-height: 48px;
89
+
90
+ /* 表单 label 列宽(AppForm.Item):120 与设计稿「标签 + 一行控件」的表单同名宽;
91
+ 需要不同宽度时在表单上覆盖(组件只下发 --ak-form-label-width,不在消费方写 CSS) */
92
+ --ui-form-label-width: 120px;
93
+
94
+ /* 栅格单元最小宽度(AppLayout.Row):比表单的 --ui-form-column-min(320) 窄一档 ——
95
+ 表单字段要放下「label + 控件」,而栅格单元(卡片/内容块)可以更窄。
96
+ 用于「列数上限 N + 最小列宽」公式里 track 的下限(实测:600px 仍排 2 列、500px 折成 1 列)。 */
97
+ --ui-grid-column-min: 260px;
98
+
99
+ /* 两列表单的列最小宽度(AppForm columns=2):容器放不下两列时自动回落一列 —— 320 是
100
+ 「label 120 + 一个还能用的输入框」的下限 */
101
+ --ui-form-column-min: 320px;
102
+
103
+ /* 阴影:细线 + 弹层投影(拟物重投影一律收敛到这里) */
104
+ --ui-shadow-card: 0 2px 20px rgba(3, 18, 51, 0.05);
105
+ --ui-shadow-elevated: 0 2px 20px rgba(3, 18, 51, 0.15);
106
+ --ui-shadow-popover: 0 6px 16px rgba(15, 23, 42, 0.08);
107
+ --ui-overlay-weak: rgba(15, 23, 42, 0.04);
108
+ --ui-overlay-strong: rgba(15, 23, 42, 0.06);
109
+ --ui-mask: rgba(0, 14, 51, 0.1);
110
+ --ui-mask-light: rgba(255, 255, 255, 0.6);
111
+ }
@@ -0,0 +1,14 @@
1
+ /* 合规样本:只写布局属性、只用登记前缀、无 rem / !important / 视觉属性 */
2
+ .other-page {
3
+ display: flex;
4
+ flex-direction: column;
5
+ height: 100%;
6
+ min-height: 0;
7
+ }
8
+
9
+ .other-page-body {
10
+ flex: 1;
11
+ min-height: 0;
12
+ padding: 8px;
13
+ overflow: auto;
14
+ }
@@ -0,0 +1,15 @@
1
+ // 合规样本:AppShell 骨架 + Header/body 契约 + 面板 toolbar 字段 ≤3 + 组件用法合规
2
+ import { AppShell, AppPanel, AppTable, AppInput } from '@manohub/app-kit'
3
+
4
+ export default function GoodPage() {
5
+ return (
6
+ <AppShell>
7
+ <AppShell.Header title="示例页面" />
8
+ <AppShell.Body mode="plain">
9
+ <AppPanel title="列表" toolbar={<AppInput />}>
10
+ <AppTable framed rows={[]} columns={[]} rowKey="id" />
11
+ </AppPanel>
12
+ </AppShell.Body>
13
+ </AppShell>
14
+ )
15
+ }
@@ -0,0 +1,18 @@
1
+ /* 故意违规样本:样式护栏(前缀未登记 / 视觉属性 / rem / !important / 裸元素 / .ak- / 令牌 / Split 竖直边框) */
2
+ .my-app-header {
3
+ color: #ffffff;
4
+ padding: 0.5rem;
5
+ border-radius: 8px;
6
+ }
7
+
8
+ button {
9
+ margin: 0 !important;
10
+ }
11
+
12
+ .ak-panel {
13
+ --my-token: 12px;
14
+ }
15
+
16
+ .my-side-pane {
17
+ border-right: 1px solid #cccccc;
18
+ }
@@ -0,0 +1,38 @@
1
+ // 故意违规样本:组件使用 + 页面结构护栏
2
+ import { FDataGrid } from '@farris/ui-vue'
3
+ import { AppShell, AppPanel, AppInput, AppSearchBox, AppSelect, AppTextarea } from '@manohub/app-kit'
4
+
5
+ export default function BadPage() {
6
+ return (
7
+ <AppShell>
8
+ <AppShell.Header />
9
+ <AppShell.Body>
10
+ <div class="my-page-header">自绘页头</div>
11
+ <button class="btn btn-primary">确定</button>
12
+ {/* 跨行写法:`<input` 与 class 分两行 —— 逐行正则会漏(S4 必须整段扫) */}
13
+ <input
14
+ type="text"
15
+ class="my-cell-input"
16
+ placeholder="跨行写法"
17
+ />
18
+ <a class="my-op-link" onClick={() => remove}>删除</a>
19
+ <a href="#help" class="my-help-link">帮助</a>
20
+ <div style="height: 100vh" />
21
+ <AppPanel
22
+ title="列表"
23
+ toolbar={
24
+ <>
25
+ <AppInput />
26
+ <AppSearchBox />
27
+ <AppSelect />
28
+ <AppTextarea />
29
+ </>
30
+ }
31
+ />
32
+ <AppShell.Split sidebar={{ width: 200 }} />
33
+ <FDataGrid valueField="a" textField="b" rowOption={{ height: 32 }} />
34
+ <span>{row.raw}</span>
35
+ </AppShell.Body>
36
+ </AppShell>
37
+ )
38
+ }
@@ -0,0 +1,4 @@
1
+ // 故意违规样本:路由级页面未使用 AppShell(S1)
2
+ export default function NoShellPage() {
3
+ return <div class="my-legacy-layout">没有骨架</div>
4
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * 护栏自测:**故意违规的样本必须被打回,且必须携带 correction(唯一改法 + 出处)**。
3
+ *
4
+ * 为什么用 `node:test` 而不是 vitest:三条护栏是零依赖的 Node 脚本,
5
+ * `node --test` 内置即可运行,消费方与本仓都不需要额外安装测试框架
6
+ * (组件级的 DOM 契约测试另行引入 vitest + jsdom)。
7
+ *
8
+ * 运行:`pnpm --filter @manohub/app-kit test:unit`
9
+ */
10
+ import { test } from 'node:test'
11
+ import assert from 'node:assert/strict'
12
+ import { cpSync, mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
13
+ import { spawnSync } from 'node:child_process'
14
+ import { tmpdir } from 'node:os'
15
+ import { fileURLToPath } from 'node:url'
16
+ import { dirname, join } from 'node:path'
17
+
18
+ const HERE = dirname(fileURLToPath(import.meta.url))
19
+ const LINT = join(HERE, '..')
20
+ const AUDITS = ['style-audit.mjs', 'component-audit.mjs', 'structure-audit.mjs']
21
+
22
+ /** 造一个临时消费仓:<tmp>/app/src/... + appkit-guardrails.config.json */
23
+ function makeConsumer(fixtureName) {
24
+ const root = mkdtempSync(join(tmpdir(), 'appkit-guardrails-'))
25
+ mkdirSync(join(root, 'app'), { recursive: true })
26
+ cpSync(join(HERE, 'fixtures', fixtureName), join(root, 'app', 'src'), { recursive: true })
27
+ writeFileSync(
28
+ join(root, 'appkit-guardrails.config.json'),
29
+ JSON.stringify({
30
+ apps: [{ dir: 'app', name: 'fixture-app', prefixes: ['other-'], pending: false }],
31
+ srcGlobs: ['src/**/*.ts', 'src/**/*.tsx'],
32
+ styleGlobs: ['src/**/*.css'],
33
+ ignore: ['node_modules', 'dist'],
34
+ }),
35
+ )
36
+ return root
37
+ }
38
+
39
+ function runAudit(script, cwd) {
40
+ const res = spawnSync(process.execPath, [join(LINT, script), '--cwd=' + cwd], { encoding: 'utf8' })
41
+ return { code: res.status, out: (res.stdout ?? '') + (res.stderr ?? '') }
42
+ }
43
+
44
+ test('glob:嵌套路径必须匹配(防「只扫顶层文件」的静默漏扫)', async () => {
45
+ const { globToRegExp } = await import('../shared.mjs')
46
+ const src = globToRegExp('src/**/*.tsx')
47
+ assert.ok(src.test('src/main.tsx'), '顶层文件应匹配')
48
+ assert.ok(src.test('src/views/page.tsx'), '一级嵌套应匹配')
49
+ assert.ok(src.test('src/a/b/c/page.tsx'), '多级嵌套应匹配')
50
+ assert.ok(!src.test('src/page.ts'), '扩展名不符不应匹配')
51
+ const css = globToRegExp('src/**/*.css')
52
+ assert.ok(css.test('src/features/x/y.css'), '深层 css 应匹配')
53
+ })
54
+
55
+ test('违规样本:三条护栏都必须报错,且每条违规都带 correction', () => {
56
+ const root = makeConsumer('violations-src')
57
+ try {
58
+ const expectations = {
59
+ 'style-audit.mjs': ['style/prefix', 'style/visual', 'style/rem', 'style/important', 'style/bare-element', 'style/ak-selector', 'style/token'],
60
+ 'component-audit.mjs': ['api/farris-import', 'api/legacy-prop'],
61
+ 'structure-audit.mjs': [
62
+ 'structure/no-shell',
63
+ 'structure/header-title',
64
+ 'structure/body-mode',
65
+ 'structure/native-control',
66
+ 'structure/viewport-height',
67
+ 'structure/self-header',
68
+ 'structure/toolbar-fields',
69
+ 'structure/self-border',
70
+ ],
71
+ }
72
+ for (const script of AUDITS) {
73
+ const { code, out } = runAudit(script, root)
74
+ assert.equal(code, 1, `${script} 应当失败(有违规)`)
75
+ for (const rule of expectations[script]) {
76
+ assert.ok(out.includes(rule), `${script} 未报出规则 ${rule}`)
77
+ }
78
+ assert.ok(out.includes('改法:'), `${script} 的违规必须带唯一改法`)
79
+ assert.ok(out.includes('出处:CONTRACT.md'), `${script} 的违规必须带规范出处`)
80
+ }
81
+ } finally {
82
+ rmSync(root, { recursive: true, force: true })
83
+ }
84
+ })
85
+
86
+ test('S4:原生按钮、「无 href 的锚点」、跨行写法的原生输入都要打回,带 href 的真链接不误报', () => {
87
+ const root = makeConsumer('violations-src')
88
+ try {
89
+ const { out } = runAudit('structure-audit.mjs', root)
90
+ const hits = out.split('\n').filter((line) => line.includes('structure/native-control'))
91
+ // 三条:按钮、无 href 锚点、以及 `<input` 与 class= 分两行的输入框
92
+ // (第三条是防回归:S4 曾经是逐行正则,跨行写法能整条逃过)
93
+ assert.equal(hits.length, 3, `按钮 / 锚点 / 跨行输入各应命中一条,实际 ${hits.length} 条`)
94
+ assert.ok(!out.includes('my-help-link'), '带 href 的锚点是真链接,不应被判为原生控件')
95
+ assert.ok(
96
+ hits.some((line) => line.includes('bad-page.tsx:13')),
97
+ `跨行输入的命中行号应指向 <input 那一行,实际:${hits.join(' | ')}`,
98
+ )
99
+ } finally {
100
+ rmSync(root, { recursive: true, force: true })
101
+ }
102
+ })
103
+
104
+ test('合规样本:三条护栏必须全绿(不得误报)', () => {
105
+ const root = makeConsumer('clean-src')
106
+ try {
107
+ for (const script of AUDITS) {
108
+ const { code, out } = runAudit(script, root)
109
+ assert.equal(code, 0, `${script} 不应报错,实际输出:\n${out}`)
110
+ }
111
+ } finally {
112
+ rmSync(root, { recursive: true, force: true })
113
+ }
114
+ })
115
+
116
+ test('pending 应用:照常报告但不计入退出码', () => {
117
+ const root = makeConsumer('violations-src')
118
+ try {
119
+ writeFileSync(
120
+ join(root, 'appkit-guardrails.config.json'),
121
+ JSON.stringify({
122
+ apps: [{ dir: 'app', name: 'fixture-app', prefixes: ['other-'], pending: true }],
123
+ ignore: ['node_modules', 'dist'],
124
+ }),
125
+ )
126
+ for (const script of AUDITS) {
127
+ const { code, out } = runAudit(script, root)
128
+ assert.equal(code, 0, `${script}:pending 应用不应影响退出码`)
129
+ assert.ok(out.includes('存量挂起'), `${script}:应提示存在存量挂起`)
130
+ }
131
+ } finally {
132
+ rmSync(root, { recursive: true, force: true })
133
+ }
134
+ })
135
+
136
+ test('--changed:非 git 环境下不裁剪(宁可多检查)', () => {
137
+ const root = makeConsumer('violations-src')
138
+ try {
139
+ const { code } = runAudit('style-audit.mjs', root)
140
+ assert.equal(code, 1)
141
+ } finally {
142
+ rmSync(root, { recursive: true, force: true })
143
+ }
144
+ })