@q1travel/design-system 0.30.12 → 0.30.13

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/README.md CHANGED
@@ -71,7 +71,7 @@ root.render(
71
71
  | --- | --- |
72
72
  | `npx q1-design ls` | 列出包内提供的组件、hook 与应用模板 |
73
73
  | `npx q1-design doc <组件名>` | 打印组件的公共 API;`doc app-base` 查看应用基础样式模板 |
74
- | `npx q1-design lint [路径...]` | 按 `Q1-DS-*` 规则检查样式用法,默认扫描 `src` |
74
+ | `npx q1-design lint [路径...]` | 按 `Q1-DS-*` 规则检查样式和组件枚举字面量,默认扫描 `src` |
75
75
  | `npx q1-design lint --diff <base-ref>` | 只检查相对 `base-ref` 有改动的文件 |
76
76
  | `npx q1-design overrides [路径...]` | 统计对组件外观的 `className` 覆写 |
77
77
  | `npx q1-design sync` | 将设计契约与 AI 编码指南同步为项目内的只读缓存 |
@@ -0,0 +1,42 @@
1
+ export const COMPONENT_ENUM_RULE = 'Q1-DS-002';
2
+
3
+ // Match the CLI's import-based scan; only literal JSX attributes are checked.
4
+ export function lintComponentEnums(source, filename, loadSchema) {
5
+ const imports = new Map();
6
+ const importPattern = /import\s*\{([^{}]*?)\}\s*from\s*["']@q1travel\/design-system["']/gs;
7
+ for (const match of source.matchAll(importPattern)) {
8
+ for (const part of match[1].split(',')) {
9
+ if (/^\s*type\s/.test(part)) continue;
10
+ const [component, local = component] = part.trim().split(/\s+as\s+/);
11
+ if (/^[A-Z][A-Za-z0-9_]*$/.test(local)) imports.set(local, component);
12
+ }
13
+ }
14
+ for (const match of source.matchAll(/import\s*\*\s*as\s+(\w+)\s*from\s*["']@q1travel\/design-system["']/g)) {
15
+ imports.set(`${match[1]}.*`, '*');
16
+ }
17
+ if (!imports.size) return [];
18
+ const hits = [];
19
+ const tagPattern = /<([A-Z][\w.]*)\b((?:"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\}|[^>"'{}])*)>/g;
20
+ for (const tag of source.matchAll(tagPattern)) {
21
+ const [namespace, member] = tag[1].split('.');
22
+ const component = member && imports.has(`${namespace}.*`) ? member : imports.get(tag[1]);
23
+ if (!component) continue;
24
+ const attributes = /(?:^|\s)(variant|size)\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*(?:"([^"]*)"|'([^']*)')\s*\})/g;
25
+ for (const attribute of tag[2].matchAll(attributes)) {
26
+ let value = attribute[2] ?? attribute[3] ?? attribute[4] ?? attribute[5];
27
+ if (attribute[2] !== undefined || attribute[3] !== undefined) {
28
+ value = value.replace(/&#(x[\da-f]+|\d+);/gi, (_, code) => String.fromCodePoint(code[0].toLowerCase() === 'x' ? parseInt(code.slice(1), 16) : Number(code)));
29
+ }
30
+ const allowed = loadSchema()[component]?.[attribute[1]];
31
+ if (allowed && !allowed.includes(value)) {
32
+ const offset = tag.index + tag[0].indexOf(tag[2]) + attribute.index;
33
+ hits.push({
34
+ ruleId: COMPONENT_ENUM_RULE,
35
+ lineNumber: source.slice(0, offset + attribute[0].search(/\S/)).split('\n').length,
36
+ lineContent: `error ${component}.${attribute[1]}=${JSON.stringify(value)}; 允许值: ${allowed.join(' | ')}`,
37
+ });
38
+ }
39
+ }
40
+ }
41
+ return hits;
42
+ }
package/bin/q1-design.mjs CHANGED
@@ -22,6 +22,8 @@ import { createHash } from 'node:crypto';
22
22
  import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
23
23
  import { fileURLToPath, pathToFileURL } from 'node:url';
24
24
 
25
+ import { COMPONENT_ENUM_RULE, lintComponentEnums } from './lint-component-enums.mjs';
26
+
25
27
  const PACKAGE_NAME = '@q1travel/design-system';
26
28
  const SKILL_RELATIVE_DIR = '.claude/skills/product-design';
27
29
  const MANIFEST_FILE_NAME = '.q1-design-manifest.json';
@@ -644,6 +646,17 @@ const LINT_RULES = [
644
646
  function collectLintHits(inputPaths, baseDir) {
645
647
  const targetFiles = collectLintTargetFiles(inputPaths, baseDir);
646
648
  const hits = [];
649
+ let enumSchema;
650
+ const loadEnumSchema = () => {
651
+ if (!enumSchema) {
652
+ const relativePath = 'dist-pkg/component-enums.json';
653
+ const packageRoot = resolveInstalledPackageRoot(baseDir) ?? baseDir;
654
+ const schemaPath = join(packageRoot, relativePath);
655
+ if (!existsSync(schemaPath)) throw new Error(`[q1-design] 找不到 ${relativePath},无法检查组件枚举;请安装 ${PACKAGE_NAME} 或先运行 npm run build:lib。`);
656
+ enumSchema = JSON.parse(readFileSync(schemaPath, 'utf8'));
657
+ }
658
+ return enumSchema;
659
+ };
647
660
 
648
661
  for (const absoluteFilePath of targetFiles) {
649
662
  const fileContent = readFileSync(absoluteFilePath, 'utf8');
@@ -653,6 +666,14 @@ function collectLintHits(inputPaths, baseDir) {
653
666
  isTokens: isTokensPath(absoluteFilePath, baseDir),
654
667
  };
655
668
 
669
+ if (!fileFacts.isCssFile && !hasFileDisablePragma(fileContent, COMPONENT_ENUM_RULE)) {
670
+ const isSuppressed = createSuppressionTracker(COMPONENT_ENUM_RULE);
671
+ const suppressedLines = fileContent.split('\n').map((line) => isSuppressed(line));
672
+ for (const hit of lintComponentEnums(fileContent, absoluteFilePath, loadEnumSchema)) {
673
+ if (!suppressedLines[hit.lineNumber - 1]) hits.push({ ...hit, filePath: relativeFilePath });
674
+ }
675
+ }
676
+
656
677
  const activeRules = LINT_RULES.map((rule) => ({
657
678
  rule,
658
679
  applicable: rule.appliesToFile(fileFacts) && !hasFileDisablePragma(fileContent, rule.ruleId),
@@ -717,7 +738,7 @@ function runLint(inputPaths, consumerRootDir, diffBaseRef) {
717
738
 
718
739
  for (const hit of hits) {
719
740
  console.log(
720
- `${hit.ruleId} ${hit.filePath}:${hit.lineNumber} ${truncateLine(hit.lineContent)}`,
741
+ `${hit.ruleId} ${hit.filePath}:${hit.lineNumber} ${hit.ruleId === COMPONENT_ENUM_RULE ? hit.lineContent : truncateLine(hit.lineContent)}`,
721
742
  );
722
743
  }
723
744
  console.log(`[q1-design] 共 ${hits.length} 处命中`);
@@ -1109,7 +1130,12 @@ if (isDirectRun) {
1109
1130
  }
1110
1131
  args.splice(diffIndex, 2);
1111
1132
  }
1112
- runLint(args.length > 0 ? args : ['src'], consumerRootDir, diffBaseRef);
1133
+ try {
1134
+ runLint(args.length > 0 ? args : ['src'], consumerRootDir, diffBaseRef);
1135
+ } catch (error) {
1136
+ console.error(error.message);
1137
+ process.exit(1);
1138
+ }
1113
1139
  } else if (subcommand === 'eval') {
1114
1140
  runEval();
1115
1141
  } else if (subcommand === 'overrides') {
@@ -112,7 +112,7 @@ Timeline / TimelineItem (ol/li, 8px token dot or Avatar), Marker (quiet actor/ti
112
112
 
113
113
  Accepted 2026-10-07: public `ToggleGroup` names a group of pressable controls with single or multiple selection. Its `items` API exposes `value`, `label`, optional `icon`, `disabled`, and `ariaLabel`; icon-only items require an icon and `ariaLabel`. `type="single"` accepts string `value` / `defaultValue` and `onValueChange`; `allowEmpty` defaults to false. `type="multiple"` accepts string arrays. Controlled values remain caller-owned; uncontrolled single selection falls back to the first enabled item when its value is absent or unknown and empty selection is disallowed. Disabling an existing selected item retains its pressed state and moves only the tabstop. No existing Switch consumer is migrated.
114
114
 
115
- Native buttons expose `aria-pressed` within `role="group"`. Roving tabindex and Arrow / Home / End move focus without selecting; Enter / Space activate through native clicks. Disabled items are skipped. Button-aligned sm32 / md40 / lg48 sizes use existing tokens, quiet selected background and primary text without a selected ring, shared focus shadow, and the shared coarse-pointer 44px TouchTarget with reserved neighbor space. The Tier A keyboard contract lives on `/components/toggle-group`; Switch retains radio semantics and Tabs retains panel semantics.
115
+ Native buttons expose `aria-pressed` within `role="group"`. Roving tabindex and Arrow / Home / End move focus without selecting; Enter / Space activate through native clicks. Disabled items are skipped. Button-aligned sm32 / md40 / lg48 sizes use existing tokens, quiet selected background and primary text with the shared `selection-recipe.css` inset border-strong selection edge (bg-subtle in light, bg-raised in dark); keyboard focus replaces that edge with the shared focus shadow, and the shared coarse-pointer 44px TouchTarget with reserved neighbor space. The Tier A keyboard contract lives on `/components/toggle-group`; Switch retains radio semantics and Tabs retains panel semantics.
116
116
  ## Checkbox task completion circle
117
117
  - Native base: `src/components/ui/ControlOption.tsx`; circle-only readonly/warning module: `src/components/ui/CircleCheckbox.tsx`; independent style: `src/styles/checkbox-circle.css`, imported at the top of global.css and included by build-style.mjs. Square defaults remain unchanged.
118
118
  - Documentation: `src/pages/CheckboxPage.tsx`, `CheckboxCircleDemo.tsx`, `src/styles/checkbox-circle-specimen.css`; circle completion is restricted to tasks/checklists and row completion treatment stays consumer-owned.
@@ -150,3 +150,7 @@ The announcement region wraps only the icon, the `q1-sr-only` 「错误:」 pr
150
150
  ## Cascader 层级选择
151
151
 
152
152
  `Cascader`(`src/components/ui/Cascader.tsx`)负责字段及触发器;`cascader/` 的 Types、model、window、Panel 分别负责受控边界、路径与独立选择、测量窗口、关联 listbox 与搜索。复用 Popover/useOverlay、RegionErrorState 和 Spinner;样式来自既有 surface、text、control、space、radius 与 focus token。窄屏同一弹层单列钻取,亮暗继承 anchor。完整约束、可选省略、公开面与回滚见 [Cascader](./cascader.md)。文档入口 `/components/cascader`,不迁移既有选择器。
153
+
154
+ ## ThemeSwitcher selection appearance
155
+
156
+ Accepted 2026-11-02 (#761): ThemeSwitcher keeps a single outer group border. Options have no border; selection uses `--q1-color-bg-selected-neutral` and primary text, with no inset selection edge. Unselected options keep secondary text and the container background. Light and dark selected and unselected text meet 4.5:1 contrast; the selected background differs from the container. Native labeled radios retain their keyboard behavior, hover changes enabled option text to primary, and keyboard focus uses the shared focus shadow. Public props, class names, DOM structure, exports, and tokens are unchanged.
@@ -0,0 +1,284 @@
1
+ {
2
+ "Badge": {
3
+ "size": [
4
+ "sm",
5
+ "md",
6
+ "lg"
7
+ ]
8
+ },
9
+ "Book": {
10
+ "variant": [
11
+ "stripe",
12
+ "simple"
13
+ ]
14
+ },
15
+ "Browser": {
16
+ "variant": [
17
+ "light",
18
+ "dark"
19
+ ]
20
+ },
21
+ "Button": {
22
+ "variant": [
23
+ "primary",
24
+ "secondary",
25
+ "ghost",
26
+ "warning",
27
+ "danger"
28
+ ],
29
+ "size": [
30
+ "sm",
31
+ "md",
32
+ "lg"
33
+ ]
34
+ },
35
+ "ButtonLink": {
36
+ "variant": [
37
+ "primary",
38
+ "secondary",
39
+ "ghost",
40
+ "warning",
41
+ "danger"
42
+ ],
43
+ "size": [
44
+ "sm",
45
+ "md",
46
+ "lg"
47
+ ]
48
+ },
49
+ "CalendarGrid": {
50
+ "size": [
51
+ "large",
52
+ "small"
53
+ ]
54
+ },
55
+ "Checkbox": {
56
+ "size": [
57
+ "sm",
58
+ "md"
59
+ ]
60
+ },
61
+ "CodeBlock": {
62
+ "variant": [
63
+ "default",
64
+ "flush"
65
+ ]
66
+ },
67
+ "Combobox": {
68
+ "size": [
69
+ "sm",
70
+ "md",
71
+ "lg"
72
+ ]
73
+ },
74
+ "DotsMenu": {
75
+ "size": [
76
+ "sm",
77
+ "md",
78
+ "lg"
79
+ ]
80
+ },
81
+ "ErrorMessage": {
82
+ "size": [
83
+ "sm",
84
+ "md",
85
+ "lg"
86
+ ]
87
+ },
88
+ "FilterChip": {
89
+ "size": [
90
+ "sm",
91
+ "md",
92
+ "lg"
93
+ ]
94
+ },
95
+ "Gauge": {
96
+ "size": [
97
+ "xs",
98
+ "sm",
99
+ "md",
100
+ "lg"
101
+ ]
102
+ },
103
+ "IconButton": {
104
+ "variant": [
105
+ "primary",
106
+ "secondary",
107
+ "ghost",
108
+ "warning",
109
+ "danger"
110
+ ],
111
+ "size": [
112
+ "sm",
113
+ "md",
114
+ "lg"
115
+ ]
116
+ },
117
+ "Input": {
118
+ "size": [
119
+ "xs",
120
+ "sm",
121
+ "md",
122
+ "lg"
123
+ ]
124
+ },
125
+ "InputOTP": {
126
+ "size": [
127
+ "md",
128
+ "lg"
129
+ ]
130
+ },
131
+ "Marker": {
132
+ "variant": [
133
+ "default",
134
+ "separator",
135
+ "unread"
136
+ ]
137
+ },
138
+ "Menu": {
139
+ "size": [
140
+ "sm",
141
+ "md",
142
+ "lg"
143
+ ]
144
+ },
145
+ "Navigation": {
146
+ "variant": [
147
+ "default",
148
+ "line"
149
+ ]
150
+ },
151
+ "Note": {
152
+ "size": [
153
+ "small",
154
+ "default",
155
+ "large"
156
+ ]
157
+ },
158
+ "PageFeedback": {
159
+ "variant": [
160
+ "rating",
161
+ "helpful"
162
+ ]
163
+ },
164
+ "Pill": {
165
+ "size": [
166
+ "sm",
167
+ "md",
168
+ "lg"
169
+ ]
170
+ },
171
+ "ProductLockup": {
172
+ "size": [
173
+ "sm",
174
+ "md"
175
+ ]
176
+ },
177
+ "ProjectBanner": {
178
+ "variant": [
179
+ "default",
180
+ "success",
181
+ "warning",
182
+ "error"
183
+ ]
184
+ },
185
+ "RangeField": {
186
+ "size": [
187
+ "large",
188
+ "small"
189
+ ]
190
+ },
191
+ "Select": {
192
+ "size": [
193
+ "xs",
194
+ "sm",
195
+ "md",
196
+ "lg"
197
+ ]
198
+ },
199
+ "Sheet": {
200
+ "size": [
201
+ "sm",
202
+ "md",
203
+ "lg",
204
+ "xl"
205
+ ]
206
+ },
207
+ "SidebarDrawer": {
208
+ "variant": [
209
+ "panel",
210
+ "plain"
211
+ ]
212
+ },
213
+ "Spinner": {
214
+ "size": [
215
+ "sm",
216
+ "md",
217
+ "lg",
218
+ "xl"
219
+ ]
220
+ },
221
+ "SplitButton": {
222
+ "variant": [
223
+ "default",
224
+ "secondary"
225
+ ],
226
+ "size": [
227
+ "sm",
228
+ "md",
229
+ "lg"
230
+ ]
231
+ },
232
+ "StatusPage": {
233
+ "size": [
234
+ "page",
235
+ "region"
236
+ ]
237
+ },
238
+ "Switch": {
239
+ "variant": [
240
+ "segmented",
241
+ "cards"
242
+ ],
243
+ "size": [
244
+ "sm",
245
+ "md",
246
+ "lg"
247
+ ]
248
+ },
249
+ "Tabs": {
250
+ "variant": [
251
+ "default",
252
+ "secondary",
253
+ "line"
254
+ ]
255
+ },
256
+ "Textarea": {
257
+ "size": [
258
+ "xs",
259
+ "sm",
260
+ "md",
261
+ "lg"
262
+ ]
263
+ },
264
+ "TextWithCopyButton": {
265
+ "size": [
266
+ "default",
267
+ "small"
268
+ ]
269
+ },
270
+ "Toggle": {
271
+ "size": [
272
+ "sm",
273
+ "md",
274
+ "lg"
275
+ ]
276
+ },
277
+ "ToggleGroup": {
278
+ "size": [
279
+ "sm",
280
+ "md",
281
+ "lg"
282
+ ]
283
+ }
284
+ }
@@ -14283,7 +14283,7 @@ function Gd(e) {
14283
14283
  ),
14284
14284
  e.mode === "multiple" && J.length ? /* @__PURE__ */ n("div", { className: "q1-cascader__tags", role: "list", "aria-label": "已选项", children: J.map((ce) => /* @__PURE__ */ _("span", { className: "q1-cascader__tag", role: "listitem", children: [
14285
14285
  /* @__PURE__ */ n("span", { children: U(ce) }),
14286
- w ? /* @__PURE__ */ n("button", { type: "button", className: "q1-cascader__remove", disabled: !F, "aria-label": `移除 ${ne(ce)}`, onClick: () => Me(ce), children: /* @__PURE__ */ n(Ue, { size: 16, strokeWidth: oe, "aria-hidden": "true" }) }) : null
14286
+ w ? /* @__PURE__ */ n("button", { type: "button", className: "q1-cascader__remove", disabled: !F, "aria-label": `移除 ${ne(ce)}`, onClick: () => Me(ce), children: /* @__PURE__ */ n(Ue, { size: 12, strokeWidth: oe, "aria-hidden": "true" }) }) : null
14287
14287
  ] }, ce)) }) : null,
14288
14288
  w && J.length ? /* @__PURE__ */ n("button", { type: "button", className: "q1-cascader__remove", disabled: !F, "aria-label": "全部清除", onClick: () => Me(), children: /* @__PURE__ */ n(Ue, { size: 16, strokeWidth: oe, "aria-hidden": "true" }) }) : null
14289
14289
  ] }),
@@ -2894,6 +2894,7 @@
2894
2894
  }
2895
2895
 
2896
2896
  .banner__close {
2897
+ font: inherit;
2897
2898
  box-sizing: border-box;
2898
2899
  display: inline-flex;
2899
2900
  align-items: center;
@@ -3095,6 +3096,7 @@ a.breadcrumb__link:hover {
3095
3096
  }
3096
3097
 
3097
3098
  .breadcrumb__toggle {
3099
+ font: inherit;
3098
3100
  display: inline-flex;
3099
3101
  align-items: center;
3100
3102
  justify-content: center;
@@ -3388,6 +3390,7 @@ a.breadcrumb__link:hover {
3388
3390
  可见焦点环——它可聚焦,chrome 整体不再 aria-hidden 正是为了让它可达。
3389
3391
  点击目标 24×24(WCAG 2.2 最小目标),图标仍 12px 居中——负 margin 抵消撑高,不改地址栏高度。 */
3390
3392
  .browser__address-copy {
3393
+ font: inherit;
3391
3394
  flex-shrink: 0;
3392
3395
  display: grid;
3393
3396
  place-items: center;
@@ -4338,6 +4341,7 @@ a[href]:has(> .book):focus-visible {
4338
4341
  }
4339
4342
 
4340
4343
  .cal-field__trigger {
4344
+ font: inherit;
4341
4345
  display: inline-flex;
4342
4346
  align-items: center;
4343
4347
  gap: var(--q1-space-2);
@@ -4360,6 +4364,7 @@ a[href]:has(> .book):focus-visible {
4360
4364
 
4361
4365
  .cal-field__clear,
4362
4366
  .cal-field__caret {
4367
+ font: inherit;
4363
4368
  display: grid;
4364
4369
  place-items: center;
4365
4370
  min-height: var(--q1-space-6);
@@ -4479,6 +4484,7 @@ a[href]:has(> .book):focus-visible {
4479
4484
  }
4480
4485
 
4481
4486
  .calendar__nav-btn {
4487
+ font: inherit;
4482
4488
  display: grid;
4483
4489
  place-items: center;
4484
4490
  width: 28px;
@@ -4540,6 +4546,7 @@ a[href]:has(> .book):focus-visible {
4540
4546
  }
4541
4547
 
4542
4548
  .calendar__day {
4549
+ font: inherit;
4543
4550
  position: relative;
4544
4551
  display: grid;
4545
4552
  place-items: center;
@@ -4601,6 +4608,7 @@ a[href]:has(> .book):focus-visible {
4601
4608
  }
4602
4609
 
4603
4610
  .cal-period__trigger {
4611
+ font: inherit;
4604
4612
  display: inline-flex;
4605
4613
  align-items: center;
4606
4614
  gap: var(--q1-space-2);
@@ -4638,6 +4646,7 @@ a[href]:has(> .book):focus-visible {
4638
4646
  }
4639
4647
 
4640
4648
  .cal-period__option {
4649
+ font: inherit;
4641
4650
  width: 100%;
4642
4651
  text-align: left;
4643
4652
  padding: var(--q1-space-2) var(--q1-space-3);
@@ -5062,6 +5071,7 @@ a[href]:has(> .book):focus-visible {
5062
5071
  }
5063
5072
 
5064
5073
  .snippet__copy {
5074
+ font: inherit;
5065
5075
  position: absolute;
5066
5076
  top: 50%;
5067
5077
  right: var(--q1-space-1);
@@ -5335,6 +5345,7 @@ a[href]:has(> .book):focus-visible {
5335
5345
 
5336
5346
  /* 行动按钮。 */
5337
5347
  .note__action {
5348
+ font: inherit;
5338
5349
  flex-shrink: 0;
5339
5350
  display: inline-flex;
5340
5351
  align-items: center;
@@ -6417,6 +6428,7 @@ a.q1-card:focus-visible {
6417
6428
  }
6418
6429
 
6419
6430
  .twc__copy {
6431
+ font: inherit;
6420
6432
  display: grid;
6421
6433
  place-items: center;
6422
6434
  width: 24px;
@@ -6532,6 +6544,7 @@ a.q1-card:focus-visible {
6532
6544
 
6533
6545
  /* 清除按钮:视觉走字段家族的通用 X;Esc 仍是即时键盘路径。 */
6534
6546
  .search-input__clear {
6547
+ font: inherit;
6535
6548
  position: absolute;
6536
6549
  top: 50%;
6537
6550
  right: var(--q1-space-2);
@@ -6741,6 +6754,7 @@ a.q1-card:focus-visible {
6741
6754
 
6742
6755
  /* 药丸触发器:h32 / font 14/500 / 全圆角;颜色走家族 panel+border。 */
6743
6756
  .show-more__trigger {
6757
+ font: inherit;
6744
6758
  display: inline-flex;
6745
6759
  align-items: center;
6746
6760
  gap: var(--q1-space-1);
@@ -7537,6 +7551,7 @@ a.q1-card:focus-visible {
7537
7551
  min-height: 0;
7538
7552
  }
7539
7553
  .scroller-buttons__control {
7554
+ font: inherit;
7540
7555
  flex: none;
7541
7556
  display: grid;
7542
7557
  place-items: center;
@@ -7601,9 +7616,8 @@ a.q1-card:focus-visible {
7601
7616
  place-items: center;
7602
7617
  }
7603
7618
  .theme-switcher__option:has(input:checked) {
7604
- background: var(--q1-color-bg-subtle);
7619
+ background: var(--q1-color-bg-selected-neutral);
7605
7620
  color: var(--q1-color-text-primary);
7606
- box-shadow: inset 0 0 0 1px var(--q1-color-gray-400);
7607
7621
  }
7608
7622
  .theme-switcher__option:has(input:focus-visible) {
7609
7623
  outline: none;
@@ -7960,6 +7974,7 @@ a.q1-card:focus-visible {
7960
7974
  box-shadow: var(--q1-shadow-focus);
7961
7975
  }
7962
7976
  .q1-toast__close {
7977
+ font: inherit;
7963
7978
  flex: none;
7964
7979
  display: grid;
7965
7980
  place-items: center;
@@ -8268,6 +8283,7 @@ a.q1-card:focus-visible {
8268
8283
  display: inline-block;
8269
8284
  }
8270
8285
  .dots-menu__trigger {
8286
+ font: inherit;
8271
8287
  display: grid;
8272
8288
  place-items: center;
8273
8289
  border: 0;
@@ -8573,6 +8589,7 @@ a.q1-card:focus-visible {
8573
8589
  outline: none;
8574
8590
  }
8575
8591
  .combobox__clear {
8592
+ font: inherit;
8576
8593
  flex: none;
8577
8594
  display: grid;
8578
8595
  place-items: center;
@@ -8887,6 +8904,7 @@ a.q1-card:focus-visible {
8887
8904
  }
8888
8905
  /* 遮罩颜色与 Modal 同源,见 overlay-surfaces.css:浅色为白纱,暗色压暗。 */
8889
8906
  .search-dialog__backdrop {
8907
+ font: inherit;
8890
8908
  position: absolute;
8891
8909
  inset: 0;
8892
8910
  border: 0;
@@ -8938,6 +8956,7 @@ a.q1-card:focus-visible {
8938
8956
  }
8939
8957
  /* 关闭入口以 Esc 键帽呈现:键盘优先、视觉更轻;粗指针下由 TouchTarget 扩展到 44px 命中区。 */
8940
8958
  .search-dialog__close {
8959
+ font: inherit;
8941
8960
  position: relative;
8942
8961
  display: inline-flex;
8943
8962
  flex: none;
@@ -9171,6 +9190,7 @@ a.q1-card:focus-visible {
9171
9190
  padding: var(--q1-space-4);
9172
9191
  }
9173
9192
  .modal__backdrop {
9193
+ font: inherit;
9174
9194
  position: absolute;
9175
9195
  inset: 0;
9176
9196
  border: 0;
@@ -9354,6 +9374,7 @@ a.q1-card:focus-visible {
9354
9374
  align-items: center;
9355
9375
  }
9356
9376
  .drawer__backdrop {
9377
+ font: inherit;
9357
9378
  position: absolute;
9358
9379
  inset: 0;
9359
9380
  border: 0;
@@ -9373,6 +9394,7 @@ a.q1-card:focus-visible {
9373
9394
  /* 抓手把是真按钮(点击/Enter/Space 关闭,下滑手势保留给触屏)——原 role=button+tabIndex=-1
9374
9395
  是读屏能发现却无法激活的矛盾信号。清按钮默认样式,触控目标保持至少 44px。 */
9375
9396
  .drawer__handle {
9397
+ font: inherit;
9376
9398
  display: grid;
9377
9399
  place-items: center;
9378
9400
  width: 100%;
@@ -9688,6 +9710,7 @@ a.q1-card:focus-visible {
9688
9710
  color: var(--q1-color-text-primary);
9689
9711
  }
9690
9712
  .sheet__close {
9713
+ font: inherit;
9691
9714
  flex: none;
9692
9715
  display: grid;
9693
9716
  place-items: center;
@@ -10557,6 +10580,7 @@ a.q1-navigation__item[aria-disabled='true'] {
10557
10580
 
10558
10581
  .q1-context-switcher__popup[role='dialog'] input[type='search'],
10559
10582
  .q1-context-switcher__popup--stacked input[type='search'] {
10583
+ font: inherit;
10560
10584
  width: 100%;
10561
10585
  min-width: 0;
10562
10586
  min-height: var(--q1-space-8);
@@ -11491,7 +11515,7 @@ a.q1-navigation__item[aria-disabled='true'] {
11491
11515
 
11492
11516
  .q1-cascader { display: grid; gap: var(--q1-space-2); min-inline-size: 0; }
11493
11517
  .q1-cascader__label { color: var(--q1-color-text-primary); font-size: var(--q1-font-size-label-14); line-height: var(--q1-line-height-label-14); }
11494
- .q1-cascader__control { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: var(--q1-space-2); border: calc(var(--q1-space-1) / 4) solid var(--q1-color-border); border-radius: var(--q1-radius-2); background: var(--q1-color-bg-sunken); }
11518
+ .q1-cascader__control { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: var(--q1-space-2); border: calc(var(--q1-space-1) / 4) solid var(--q1-color-border-control); border-radius: var(--q1-radius-2); background: var(--q1-color-bg-sunken); }
11495
11519
  .q1-cascader__trigger { display: flex; align-items: center; justify-content: space-between; flex: 1; gap: var(--q1-space-2); min-inline-size: 0; min-block-size: var(--q1-control-height-default); padding: var(--q1-space-2) var(--q1-space-3); border: none; background: none; border-radius: var(--q1-radius-2); color: var(--q1-color-text-primary); font: inherit; font-size: var(--q1-font-size-label-14); line-height: var(--q1-line-height-label-14); cursor: pointer; text-align: start; overflow-wrap: anywhere; }
11496
11520
  .q1-cascader__trigger svg, .q1-cascader__option svg { flex: none; }
11497
11521
  .q1-cascader__control:focus-within { box-shadow: var(--q1-shadow-focus-input); }
@@ -11500,25 +11524,26 @@ a.q1-navigation__item[aria-disabled='true'] {
11500
11524
  .q1-cascader__trigger:focus-visible { outline: none; }
11501
11525
  .q1-cascader__placeholder, .q1-cascader__path { color: var(--q1-color-text-muted); }
11502
11526
  .q1-cascader__control[data-disabled], .q1-cascader__trigger[aria-disabled], .q1-cascader__option[aria-disabled] { color: var(--q1-color-text-disabled); cursor: default; }
11527
+ .q1-cascader__control[data-disabled]:not([data-invalid]) { border-color: var(--q1-color-border); }
11503
11528
  .q1-cascader__control[data-disabled] .q1-cascader__trigger { color: inherit; }
11504
11529
  .q1-cascader__control[data-tags] { align-items: start; flex-wrap: nowrap; padding-inline-end: calc(var(--q1-control-height-default) + var(--q1-space-3)); }
11505
11530
  .q1-cascader__control[data-tags] .q1-cascader__trigger { position: absolute; inset: 0; inline-size: 100%; padding: 0; }
11506
11531
  .q1-cascader__control[data-tags][data-clearable] { padding-inline-end: calc(var(--q1-control-height-default) * 2 + var(--q1-space-3)); }
11507
11532
  .q1-cascader__control[data-clearable]:not([data-tags]) .q1-cascader__trigger { margin-inline-end: var(--q1-control-height-default); }
11508
- .q1-cascader__control[data-tags] .q1-cascader__trigger > svg { position: absolute; inset-block-start: var(--q1-space-2); inset-inline-end: var(--q1-space-2); }
11533
+ .q1-cascader__control[data-tags] .q1-cascader__trigger > svg { position: absolute; inset-block-start: calc((var(--q1-control-height-default) - var(--q1-space-4)) / 2); inset-inline-end: var(--q1-space-2); }
11509
11534
  .q1-cascader__control[data-tags][data-clearable] .q1-cascader__trigger > svg { inset-inline-end: calc(var(--q1-control-height-default) + var(--q1-space-2)); }
11510
11535
  .q1-cascader__remove { position: relative; z-index: 1; pointer-events: auto; }
11511
11536
  .q1-cascader__control > .q1-cascader__remove { position: absolute; inset-block-start: 0; inset-inline-end: 0; }
11512
- .q1-cascader__tags { position: relative; pointer-events: none; min-inline-size: 0; flex: 1; display: flex; flex-wrap: wrap; gap: var(--q1-space-2); padding: var(--q1-space-2); }
11513
- .q1-cascader__tag { max-inline-size: 100%; min-inline-size: 0; display: inline-flex; align-items: center; gap: var(--q1-space-2); background: var(--q1-color-bg-selected); border-radius: var(--q1-radius-2); padding-inline-start: var(--q1-space-2); overflow-wrap: anywhere; }
11537
+ .q1-cascader__tags { position: relative; pointer-events: none; min-inline-size: 0; flex: 1; display: flex; flex-wrap: wrap; gap: var(--q1-space-1); padding: var(--q1-space-2); }
11538
+ .q1-cascader__tag { font-size: var(--q1-font-size-label-14); line-height: var(--q1-line-height-label-14); max-inline-size: 100%; min-inline-size: 0; display: inline-flex; align-items: center; gap: var(--q1-space-1); background: var(--q1-color-bg-hover); border-radius: var(--q1-radius-1); padding-inline-start: var(--q1-space-2); overflow-wrap: anywhere; }
11514
11539
  .q1-cascader__tag > span { min-inline-size: 0; }
11515
- .q1-cascader__tag .q1-cascader__remove { pointer-events: auto; flex: none; }
11540
+ .q1-cascader__tag .q1-cascader__remove { pointer-events: auto; flex: none; min-block-size: var(--q1-space-6); min-inline-size: var(--q1-space-6); padding: 0; border-radius: var(--q1-radius-1); color: inherit; }
11516
11541
  .q1-cascader__option [data-selection-area] { display: inline-flex; align-items: center; justify-content: center; flex: none; align-self: stretch; inline-size: var(--q1-control-height-default); margin: calc(var(--q1-space-2) * -1) 0 calc(var(--q1-space-2) * -1) calc(var(--q1-space-3) * -1); }
11517
11542
  .q1-cascader__remove, .q1-cascader__back { display: inline-flex; align-items: center; justify-content: center; min-block-size: var(--q1-control-height-default); min-inline-size: var(--q1-control-height-default); padding: var(--q1-space-2); border: none; border-radius: var(--q1-radius-2); background: none; color: var(--q1-color-text-secondary); font: inherit; cursor: pointer; }
11518
11543
  .q1-cascader__remove:focus-visible, .q1-cascader__back:focus-visible, .q1-cascader__search:focus-visible, .q1-cascader__list:focus-visible { outline: none; box-shadow: var(--q1-shadow-focus-input); }
11519
11544
  .q1-cascader__popup.popover-panel { max-inline-size: calc(100vw - var(--q1-space-6)); padding: var(--q1-space-2); border-radius: var(--q1-radius-3); background: var(--q1-color-bg-overlay); }
11520
11545
  .q1-cascader__panel { display: grid; gap: var(--q1-space-2); min-inline-size: 0; }
11521
- .q1-cascader__search { box-sizing: border-box; inline-size: 100%; min-block-size: var(--q1-control-height-default); padding: var(--q1-space-2) var(--q1-space-3); border: calc(var(--q1-space-1) / 4) solid var(--q1-color-border); border-radius: var(--q1-radius-2); background: var(--q1-color-bg-sunken); color: var(--q1-color-text-primary); font: inherit; font-size: var(--q1-font-size-label-14); }
11546
+ .q1-cascader__search { box-sizing: border-box; inline-size: 100%; min-block-size: var(--q1-control-height-default); padding: var(--q1-space-2) var(--q1-space-3); border: calc(var(--q1-space-1) / 4) solid var(--q1-color-border-control); border-radius: var(--q1-radius-2); background: var(--q1-color-bg-sunken); color: var(--q1-color-text-primary); font: inherit; font-size: var(--q1-font-size-label-14); }
11522
11547
  .q1-cascader__columns { display: flex; overflow-x: auto; min-inline-size: 0; max-inline-size: calc(100vw - var(--q1-space-6) * 2); }
11523
11548
  .q1-cascader__column { flex: 0 0 auto; inline-size: calc(var(--q1-space-10) * 6); min-inline-size: 0; max-inline-size: 100%; }
11524
11549
  .q1-cascader__column[hidden] { display: none; }
@@ -11544,7 +11569,7 @@ a.q1-navigation__item[aria-disabled='true'] {
11544
11569
  .q1-cascader__control[data-clearable]:not([data-tags]) .q1-cascader__trigger { margin-inline-end: var(--q1-touch-target-min); }
11545
11570
  .q1-cascader__control[data-tags] .q1-cascader__trigger > svg { inset-block-start: calc((var(--q1-touch-target-min) - var(--q1-space-4)) / 2); }
11546
11571
  .q1-cascader__control[data-tags][data-clearable] .q1-cascader__trigger > svg { inset-inline-end: calc(var(--q1-touch-target-min) + var(--q1-space-2)); }
11547
- .q1-cascader__remove { min-inline-size: var(--q1-touch-target-min); }
11572
+ .q1-cascader__remove, .q1-cascader__tag .q1-cascader__remove { min-inline-size: var(--q1-touch-target-min); min-block-size: var(--q1-touch-target-min); }
11548
11573
  }
11549
11574
 
11550
11575
  /* 未知总量以往返移动表示持续等待;减弱动态时保留居中填充段,不伪装完成比例。 */
@@ -12474,7 +12499,7 @@ a.q1-navigation__item[aria-disabled='true'] {
12474
12499
 
12475
12500
  .multi-combobox { min-width: 0; }
12476
12501
  .multi-combobox__control { box-sizing: border-box; position: relative; display: flex; align-items: center; gap: var(--q1-space-2); min-height: var(--q1-control-height-default); padding: var(--q1-space-1) var(--q1-space-3); border: calc(var(--q1-space-1) / 4) solid var(--q1-color-border-control); border-radius: var(--q1-radius-2); background: var(--q1-color-bg-sunken); }
12477
- .multi-combobox__trigger { position: absolute; inset: 0; width: 100%; border: 0; border-radius: inherit; background: transparent; cursor: pointer; }
12502
+ .multi-combobox__trigger { font: inherit; position: absolute; inset: 0; width: 100%; border: 0; border-radius: inherit; background: transparent; cursor: pointer; }
12478
12503
  .multi-combobox__trigger:focus { outline: none; }
12479
12504
  .multi-combobox__control:has(.multi-combobox__trigger:focus-visible) { box-shadow: var(--q1-shadow-focus); }
12480
12505
  .multi-combobox__control[data-invalid] { border-color: var(--q1-color-status-danger); box-shadow: 0 0 0 calc(var(--q1-space-1) / 4) var(--q1-color-status-danger), 0 0 0 var(--q1-space-1) var(--q1-color-red-100); }
@@ -12484,7 +12509,7 @@ a.q1-navigation__item[aria-disabled='true'] {
12484
12509
  .multi-combobox__values { display: flex; flex: 1; flex-wrap: wrap; align-items: center; gap: var(--q1-space-1); min-width: 0; pointer-events: none; font-size: var(--q1-font-size-label-14); line-height: var(--q1-line-height-label-14); }
12485
12510
  .multi-combobox__tag { position: relative; display: inline-flex; align-items: center; gap: var(--q1-space-1); max-width: 100%; padding-inline-start: var(--q1-space-2); border-radius: var(--q1-radius-1); background: var(--q1-color-bg-hover); color: var(--q1-color-text-primary); }
12486
12511
  .multi-combobox__tag > span { min-width: 0; overflow-wrap: anywhere; }
12487
- .multi-combobox__remove { position: relative; display: inline-flex; align-items: center; justify-content: center; flex: none; min-width: var(--q1-space-6); min-height: var(--q1-space-6); padding: 0; border: 0; border-radius: var(--q1-radius-1); background: transparent; color: inherit; pointer-events: auto; cursor: pointer; }
12512
+ .multi-combobox__remove { font: inherit; position: relative; display: inline-flex; align-items: center; justify-content: center; flex: none; min-width: var(--q1-space-6); min-height: var(--q1-space-6); padding: 0; border: 0; border-radius: var(--q1-radius-1); background: transparent; color: inherit; pointer-events: auto; cursor: pointer; }
12488
12513
  .multi-combobox__remove:disabled { cursor: default; }
12489
12514
  .multi-combobox__remove:focus-visible { outline: none; box-shadow: var(--q1-shadow-focus); }
12490
12515
  .multi-combobox__placeholder { color: var(--q1-color-text-muted); }
@@ -12560,6 +12585,7 @@ a.q1-navigation__item[aria-disabled='true'] {
12560
12585
  .command-palette__hint,
12561
12586
  .command-palette__close,
12562
12587
  .command-palette__kbd {
12588
+ font: inherit;
12563
12589
  border-radius: var(--q1-radius-1);
12564
12590
  background: color-mix(in srgb, var(--q1-color-text-primary) 8%, transparent);
12565
12591
  color: var(--q1-color-text-secondary);
@@ -12583,6 +12609,7 @@ a.q1-navigation__item[aria-disabled='true'] {
12583
12609
  }
12584
12610
 
12585
12611
  .command-palette__backdrop {
12612
+ font: inherit;
12586
12613
  position: absolute;
12587
12614
  inset: 0;
12588
12615
  border: 0;
@@ -13073,6 +13100,7 @@ a.q1-navigation__item[aria-disabled='true'] {
13073
13100
  }
13074
13101
 
13075
13102
  .copy-button {
13103
+ font: inherit;
13076
13104
  display: grid;
13077
13105
  place-items: center;
13078
13106
  width: var(--q1-space-8);
@@ -13197,6 +13225,7 @@ a.q1-navigation__item[aria-disabled='true'] {
13197
13225
  }
13198
13226
 
13199
13227
  .clearable__clear {
13228
+ font: inherit;
13200
13229
  display: grid;
13201
13230
  place-items: center;
13202
13231
  width: var(--q1-space-6);
@@ -13290,6 +13319,7 @@ a.q1-navigation__item[aria-disabled='true'] {
13290
13319
  }
13291
13320
 
13292
13321
  .codeblock__tab {
13322
+ font: inherit;
13293
13323
  min-height: var(--q1-space-6);
13294
13324
  padding: calc(var(--q1-space-1) / 2) var(--q1-space-3);
13295
13325
  border: none;
@@ -13486,6 +13516,7 @@ a.q1-navigation__item[aria-disabled='true'] {
13486
13516
  }
13487
13517
 
13488
13518
  .description__hint {
13519
+ font: inherit;
13489
13520
  display: inline-grid;
13490
13521
  place-items: center;
13491
13522
  width: var(--q1-space-6);
@@ -13639,6 +13670,7 @@ a.q1-navigation__item[aria-disabled='true'] {
13639
13670
  触屏下 TouchTarget 再扩到 --q1-touch-target-min。 */
13640
13671
  .info-tip__trigger,
13641
13672
  .info-tip__close {
13673
+ font: inherit;
13642
13674
  position: relative;
13643
13675
  display: inline-grid;
13644
13676
  place-items: center;
@@ -13959,6 +13991,7 @@ a.q1-navigation__item[aria-disabled='true'] {
13959
13991
  }
13960
13992
 
13961
13993
  .corner-remove-button {
13994
+ font: inherit;
13962
13995
  position: absolute;
13963
13996
  top: 0;
13964
13997
  right: 0;
@@ -14881,7 +14914,7 @@ a.q1-navigation__item[aria-disabled='true'] {
14881
14914
  .composer textarea::placeholder { color: var(--q1-color-text-muted); }
14882
14915
  .composer__tools { display: flex; align-items: center; gap: var(--q1-space-3); padding: var(--q1-space-2); }
14883
14916
  .composer__tools > .button { margin-left: auto; }
14884
- .composer__icon { position: relative; --touch-target-border-width: 0px; display: grid; place-items: center; border: 0; background: transparent; color: var(--q1-color-text-secondary); width: var(--q1-space-7); height: var(--q1-space-7); border-radius: var(--q1-radius-2); }
14917
+ .composer__icon { font: inherit; position: relative; --touch-target-border-width: 0px; display: grid; place-items: center; border: 0; background: transparent; color: var(--q1-color-text-secondary); width: var(--q1-space-7); height: var(--q1-space-7); border-radius: var(--q1-radius-2); }
14885
14918
  .composer__icon:hover { background: var(--q1-color-bg-hover); }
14886
14919
  .composer__icon:focus-visible { outline: none; box-shadow: var(--q1-shadow-focus); }
14887
14920
  .composer__attachments { display: flex; flex-wrap: wrap; gap: var(--q1-space-2); padding: var(--q1-space-2); }
@@ -14909,7 +14942,7 @@ a.q1-navigation__item[aria-disabled='true'] {
14909
14942
  .attachment__retry { position: relative; --touch-target-border-width: 0px; border: 0; padding: 0; background: transparent; color: var(--q1-color-text-primary); font: inherit; font-weight: var(--q1-font-weight-medium); border-radius: var(--q1-radius-1); cursor: pointer; }
14910
14943
  .attachment__retry:hover { text-decoration: underline; }
14911
14944
  .attachment__retry:focus-visible { outline: none; box-shadow: var(--q1-shadow-focus); }
14912
- .attachment__remove { position: relative; --touch-target-border-width: 0px; border: 0; background: transparent; color: var(--q1-color-text-muted); display: grid; place-items: center; width: var(--q1-space-6); height: var(--q1-space-6); border-radius: var(--q1-radius-1); flex-shrink: 0; }
14945
+ .attachment__remove { font: inherit; position: relative; --touch-target-border-width: 0px; border: 0; background: transparent; color: var(--q1-color-text-muted); display: grid; place-items: center; width: var(--q1-space-6); height: var(--q1-space-6); border-radius: var(--q1-radius-1); flex-shrink: 0; }
14913
14946
  .attachment__remove:hover { background: var(--q1-color-bg-hover); }
14914
14947
  .attachment__remove:focus-visible { outline: none; box-shadow: var(--q1-shadow-focus); }
14915
14948
  @media (pointer: coarse), (any-pointer: coarse) { .attachment { gap: var(--q1-space-4); padding: var(--q1-space-3); } }
@@ -15001,6 +15034,7 @@ a.q1-navigation__item[aria-disabled='true'] {
15001
15034
  }
15002
15035
 
15003
15036
  .feedback__emotion {
15037
+ font: inherit;
15004
15038
  display: grid;
15005
15039
  place-items: center;
15006
15040
  width: var(--q1-space-8);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@q1travel/design-system",
3
- "version": "0.30.12",
3
+ "version": "0.30.13",
4
4
  "description": "React components, design tokens, and interface contracts for q1travel products.",
5
5
  "keywords": [
6
6
  "react",
@@ -55,15 +55,15 @@
55
55
  "preview": "vite preview --host 127.0.0.1",
56
56
  "check:shell": "npm run sync:contracts && node scripts/check-shell.mjs && node scripts/check-token-package.mjs && npm run check:release-provenance",
57
57
  "check:browser:strictmode": "node scripts/check-browser.mjs --strictmode-ref",
58
- "check:browser": "node --test scripts/devtools-connection.test.mjs scripts/browser-exit-deadline.test.mjs scripts/chromium-teardown.test.mjs scripts/vite-server-lifecycle.test.mjs scripts/chromium-launch.test.mjs scripts/chromium-recovery.test.mjs scripts/chromium-ownership.test.mjs scripts/cdp-recovery.test.mjs && npm run check:browser:strictmode && node scripts/check-browser.mjs --note && node scripts/check-browser.mjs --combobox-panel && node scripts/check-browser.mjs && node scripts/check-clearable-input-browser.mjs && node scripts/check-calendar-browser.mjs && node scripts/check-banner-browser.mjs && node scripts/check-button-link-browser.mjs && node scripts/check-button-browser.mjs && node scripts/check-trailing-icon-padding-browser.mjs && node scripts/check-overlay-theme-browser.mjs && node scripts/check-browser-component-browser.mjs",
59
- "check:package-api": "node scripts/check-package-api.mjs && node --test scripts/check-public-type-global-augmentations.test.mjs && node --test scripts/check-combobox-required.test.mjs scripts/check-multi-combobox-contract.test.mjs && node --test scripts/check-q1-design-lint.test.mjs scripts/check-q1-design-bin-entry.test.mjs && node --test scripts/check-json-view-contract.test.mjs && node --test scripts/check-verification-code-input.test.mjs && node --test scripts/check-bare-field-error.test.mjs scripts/check-label-hidden-contract.test.mjs scripts/check-required-indicator-contract.test.mjs scripts/check-spinner-inherit-contract.test.mjs && node --test scripts/check-controls-contract.test.mjs && node --test scripts/check-pagination-contract.test.mjs && node --test scripts/check-table-core.test.mjs scripts/check-table-interaction.test.mjs scripts/check-table-layout.test.mjs && node --test scripts/check-contrast-contract.test.mjs && node --test scripts/check-optical-alignment-contract.test.mjs && node --test scripts/check-overlay-coordination.test.mjs scripts/check-sheet-contract.test.mjs && node --test scripts/check-choicebox-contract.test.mjs && node --test scripts/check-radio-group-contract.test.mjs && node --test scripts/check-clearable-input-contract.test.mjs && node --test scripts/check-card-contract.test.mjs && node --test scripts/check-book-contract.test.mjs && node --test scripts/check-breadcrumbs-contract.test.mjs && node --test scripts/check-description-contract.test.mjs && node --test scripts/check-error-message-contract.test.mjs && node --test scripts/check-show-more-contract.test.mjs && node --test scripts/check-load-more-button-contract.test.mjs && node --test scripts/check-banner-contract.test.mjs && node --test scripts/check-project-banner-contract.test.mjs && node --test scripts/check-note-contract.test.mjs && node --test scripts/check-feedback-contract.test.mjs && node --test scripts/check-page-feedback-contract.test.mjs && node --test scripts/check-drawer-gesture.test.mjs && node --test scripts/check-checkbox-circle-contract.test.mjs && node --test scripts/check-chat-contract.test.mjs && node --test scripts/check-calendar-single-contract.test.mjs && node --test scripts/check-hover-card-contract.test.mjs && node --test scripts/check-bare-field-error.test.mjs && node --test scripts/check-controls-contract.test.mjs scripts/check-control-size-contract.test.mjs && node --test scripts/check-composer-dots-menu-contract.test.mjs && node --test scripts/check-filter-option-panel-contract.test.mjs && npm run check:release-diff",
58
+ "check:browser": "node --test scripts/cascader-browser-lifecycle.test.mjs scripts/devtools-connection.test.mjs scripts/browser-exit-deadline.test.mjs scripts/chromium-teardown.test.mjs scripts/vite-server-lifecycle.test.mjs scripts/chromium-launch.test.mjs scripts/chromium-recovery.test.mjs scripts/chromium-ownership.test.mjs scripts/cdp-recovery.test.mjs && npm run check:browser:strictmode && node scripts/check-browser.mjs --note && node scripts/check-browser.mjs --combobox-panel && node scripts/check-browser.mjs && node scripts/check-clearable-input-browser.mjs && node scripts/check-calendar-browser.mjs && node scripts/check-banner-browser.mjs && node scripts/check-button-link-browser.mjs && node scripts/check-button-browser.mjs && node scripts/check-trailing-icon-padding-browser.mjs && node scripts/check-overlay-theme-browser.mjs && node scripts/check-browser-component-browser.mjs",
59
+ "check:package-api": "node scripts/check-package-api.mjs && node --test scripts/check-public-type-global-augmentations.test.mjs && node --test scripts/check-combobox-required.test.mjs scripts/check-multi-combobox-contract.test.mjs && node --test scripts/check-q1-design-lint.test.mjs scripts/check-q1-design-component-enums.test.mjs scripts/check-q1-design-bin-entry.test.mjs && node --test scripts/check-json-view-contract.test.mjs && node --test scripts/check-verification-code-input.test.mjs && node --test scripts/check-bare-field-error.test.mjs scripts/check-label-hidden-contract.test.mjs scripts/check-required-indicator-contract.test.mjs scripts/check-spinner-inherit-contract.test.mjs && node --test scripts/check-controls-contract.test.mjs && node --test scripts/check-pagination-contract.test.mjs && node --test scripts/check-table-core.test.mjs scripts/check-table-interaction.test.mjs scripts/check-table-layout.test.mjs && node --test scripts/check-contrast-contract.test.mjs && node --test scripts/check-optical-alignment-contract.test.mjs && node --test scripts/check-overlay-coordination.test.mjs scripts/check-sheet-contract.test.mjs && node --test scripts/check-choicebox-contract.test.mjs && node --test scripts/check-radio-group-contract.test.mjs && node --test scripts/check-clearable-input-contract.test.mjs && node --test scripts/check-card-contract.test.mjs && node --test scripts/check-book-contract.test.mjs && node --test scripts/check-breadcrumbs-contract.test.mjs && node --test scripts/check-description-contract.test.mjs && node --test scripts/check-error-message-contract.test.mjs && node --test scripts/check-show-more-contract.test.mjs && node --test scripts/check-load-more-button-contract.test.mjs && node --test scripts/check-banner-contract.test.mjs && node --test scripts/check-project-banner-contract.test.mjs && node --test scripts/check-note-contract.test.mjs && node --test scripts/check-feedback-contract.test.mjs && node --test scripts/check-page-feedback-contract.test.mjs && node --test scripts/check-drawer-gesture.test.mjs && node --test scripts/check-checkbox-circle-contract.test.mjs && node --test scripts/check-chat-contract.test.mjs && node --test scripts/check-calendar-single-contract.test.mjs && node --test scripts/check-hover-card-contract.test.mjs && node --test scripts/check-bare-field-error.test.mjs && node --test scripts/check-controls-contract.test.mjs scripts/check-control-size-contract.test.mjs && node --test scripts/check-composer-dots-menu-contract.test.mjs && node --test scripts/check-filter-option-panel-contract.test.mjs && npm run check:release-diff",
60
60
  "check:release-diff": "node --test scripts/check-release-artifact-diff.test.mjs && node scripts/check-release-artifact-diff.mjs",
61
61
  "check:release-provenance": "node --test scripts/check-release-provenance.test.mjs && node scripts/check-release-provenance.mjs",
62
62
  "check:release-sync": "node --test scripts/release-sync.test.mjs scripts/release-preview-version.test.mjs",
63
63
  "release:verify": "node scripts/release-sync.mjs --verify-only",
64
64
  "release:sync": "node scripts/release-sync.mjs",
65
65
  "check": "npm run check:release-sync && npm run build && npm run check:shell && npm run check:browser && npm run build:pkg",
66
- "build:lib": "vite build --config vite.lib.config.ts",
66
+ "build:lib": "vite build --config vite.lib.config.ts && node scripts/build-component-enums.mjs",
67
67
  "build:style": "node scripts/build-style.mjs",
68
68
  "build:pkg": "npm run build:lib && npm run build:style && npm run check:package-api",
69
69
  "prepack": "npm run build:pkg"
package/tooling/README.md CHANGED
@@ -6,4 +6,10 @@
6
6
  - `evals/fixtures.json` —— eval fixture 索引。
7
7
  - `evals/<fixture>/{before,after}/` —— 每条规则的前后态样例,用来测 skill / linter 是否产出预期改动。
8
8
 
9
- **现状**:`q1-design lint` 已实现 `Q1-DS-001`(`--ds-*`)/`Q1-DS-401`(`transition: all`)/`Q1-DS-404`(裸 duration/easing)/`Q1-DS-501`(裸颜色字面量)四条纯机械规则;`q1-design eval` 已实现,对每个 fixture 校验 before 命中且 after 零命中。`q1-design overrides` 统计消费方对组件的 className 覆写并按「重画外观/仅布局」分类——它不是 pass/fail 规则,因为覆写布局是正当用法,拦它的规则最后会被整体关掉。其余 `kind=deterministic` 规则(含判断成分)与全部 `kind=judgment` 规则待后续批次。见 `../.agents/skills/product-design/references/coverage-gaps.md`。
9
+ **现状**:`q1-design lint` 已实现 `Q1-DS-001`(`--ds-*`)/`Q1-DS-401`(`transition: all`)/`Q1-DS-404`(裸 duration/easing)/`Q1-DS-501`(裸颜色字面量)与 `Q1-DS-002`(组件 `variant` / `size` 非法字符串字面量)五条纯机械规则;`q1-design eval` 已实现,对每个 fixture 校验 before 命中且 after 零命中。`q1-design overrides` 统计消费方对组件的 className 覆写并按「重画外观/仅布局」分类——它不是 pass/fail 规则,因为覆写布局是正当用法,拦它的规则最后会被整体关掉。其余 `kind=deterministic` 规则(含判断成分)与全部 `kind=judgment` 规则待后续批次。见 `../.agents/skills/product-design/references/coverage-gaps.md`。
10
+
11
+ `Q1-DS-002` 为 error:仅检查直接从 `@q1travel/design-system` 导入的组件(含命名别名、命名空间),不追踪消费方再导出或运行时赋值;局部同名组件不受影响。允许值来自随包 `dist-pkg/design-system.d.ts` 中组件 props 的有限字符串联合,覆盖 `Button` 及其他声明了此类 `variant` / `size` 的导出组件。`variant="quiet"`、`variant={'quiet'}` 会检查,`variant={value}` 与条件表达式交给消费方类型检查。诊断包含文件、行号、组件、属性、非法值和允许值;命中退出 1,支持现有文件、区间和下一行 pragma。检测到相关字面量却缺少类型声明时退出 1 并提示修复,不能静默跳过。
12
+
13
+ CLI 通过运行时依赖 TypeScript 解析 JSX 与词法绑定,避免把注释、字符串或被遮蔽的同名组件误当成包组件;不依赖消费方开发依赖或本仓源码。
14
+
15
+ 在本仓直跑组件规则或对应 CLI 测试前,运行 `npm run build:lib` 生成类型声明;仅扫描 CSS 不要求该产物。
@@ -1,5 +1,5 @@
1
1
  {
2
- "note": "q1travel 产品设计确定性规则清单。每条带稳定 ID + canonical 出处。kind=deterministic 的可由 linter 静态强制;kind=judgment 的留给 product-design skill / Review。linter CLI 与 eval runner 已实现:`q1-design lint`(Q1-DS-001/401/501)与 `q1-design eval`(bin/q1-design.mjs);覆盖缺口见 ../../../.agents/skills/product-design/references/coverage-gaps.md。",
2
+ "note": "q1travel 产品设计确定性规则清单。每条带稳定 ID + canonical 出处。kind=deterministic 的可由 linter 静态强制;kind=judgment 的留给 product-design skill / Review。linter CLI 与 eval runner 已实现:`q1-design lint`(Q1-DS-001/002/401/404/501)与 `q1-design eval`(bin/q1-design.mjs);覆盖缺口见 ../../../.agents/skills/product-design/references/coverage-gaps.md。",
3
3
  "version": 0,
4
4
  "idScheme": {
5
5
  "0xx": "token 与事实源",
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "rules": [
13
13
  { "id": "Q1-DS-001", "title": "新代码用 --q1-*,不用 --ds-*", "source": "contracts/token-naming.md", "severity": "error", "kind": "deterministic", "detect": "新增/改动代码里出现 --ds-* 声明或作为新决策源名", "fix": "改用 --q1-* canonical token" },
14
+ { "id": "Q1-DS-002", "title": "组件枚举属性仅使用公开值", "source": "dist-pkg/design-system.d.ts", "severity": "error", "kind": "deterministic", "detect": "直接从包导入的组件,其 variant / size 字符串字面量不在 props 有限联合中", "fix": "使用诊断列出的公开允许值,动态表达式交给类型检查" },
14
15
  { "id": "Q1-DS-101", "title": "导航用原生 link、动作用原生 button", "source": "contracts/interface-guidelines.md#interactions", "severity": "error", "kind": "judgment", "detect": "用 div/span + onClick 承载导航或动作", "fix": "导航用 <a href>,动作用 <button>" },
15
16
  { "id": "Q1-DS-102", "title": "命中区 ≥24px(触摸 44px)", "source": "contracts/interface-guidelines.md#interactions", "severity": "warn", "kind": "judgment", "detect": "可点击元素视觉/命中区小于 24px 且未扩大命中区", "fix": "min-width/height + 负 margin 扩大命中区,不撑高容器" },
16
17
  { "id": "Q1-DS-301", "title": "icon-only 控件必须有可访问名", "source": "contracts/interface-guidelines.md#content-and-accessibility", "severity": "error", "kind": "judgment", "detect": "只含图标的 button/link 无 aria-label / 可访问名", "fix": "补 aria-label 或视觉隐藏文本" },