@q1travel/design-system 0.10.0 → 0.12.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.
package/README.md CHANGED
@@ -8,8 +8,8 @@ q1travel 产品家族的设计权威上游仓,三件事集于一处:`@q1trav
8
8
 
9
9
  ## Sources
10
10
 
11
- - Source contracts(设计契约源,canonical):`contracts/design.md`、`contracts/design.dark.md`、`contracts/token-naming.md`、`contracts/icon-system.md`、`contracts/button-api-compatibility.md`、`contracts/interface-guidelines.md`、`contracts/design-engineering-principles.md`、`contracts/implementation-map.md`、`contracts/overlay-foundation.md`,以及迁移契约 `contracts/status-chip-badge-migration.md`、`contracts/pagination-migration.md`、`contracts/table-migration.md`、`contracts/overlay-shadow-migration.md`。
12
- - Generated mirrors(生成镜像,勿手改):`public/design.md`、`public/design.dark.md`,由 `scripts/sync-design-contracts.mjs` 从契约源复制。
11
+ - Source contracts(设计契约源,canonical):`contracts/design.md`、`contracts/design.dark.md`、`contracts/token-naming.md`、`contracts/icon-system.md`、`contracts/button-api-compatibility.md`、`contracts/interface-guidelines.md`、`contracts/design-engineering-principles.md`、`contracts/implementation-map.md`、`contracts/overlay-foundation.md`,以及迁移契约 `contracts/status-chip-badge-migration.md`、`contracts/pagination-migration.md`、`contracts/table-migration.md`、`contracts/overlay-shadow-migration.md`。独立 HTML 文档的有界 class 词汇表源为 `contracts/design.css`。
12
+ - Generated mirrors(生成镜像,勿手改):`public/design.md`、`public/design.dark.md` 由 `scripts/sync-design-contracts.mjs` 从契约源逐字节复制;`public/design.css` 由 `scripts/build-design-css.mjs` 把两份契约 frontmatter 的取值注入 `contracts/design.css` 的类词汇后生成,因而自足、可被独立 HTML 页面直接加载。
13
13
  - Runtime tokens:`src/styles/tokens.css`(canonical `--q1-*`,仅 `:root` 与 `:root[data-theme='dark']` 两个扁平 selector)。
14
14
  - 发布台账:`contracts/releases/`(仅存于仓库,不进 npm 包)。
15
15
 
@@ -58,6 +58,8 @@ docs/ review/ 架构骨架、agent 规范、历史决策记录
58
58
  ## Documentation & AI Guidelines
59
59
 
60
60
  - **AI Agent 必须遵循根目录 `AGENTS.md`**(Claude Code 与 Codex 共用的 canonical 入口:命令速查、红线约束、工作闭环与知识索引);`CLAUDE.md` 仅为其薄包装。
61
- - `docs/architecture.md` — 双构建管线、契约生成与下游消费模型、发布溯源链路。
61
+ - `docs/architecture.md` — 双构建管线、契约生成与下游消费模型、文档站托管与公开契约的缓存策略(含 Cloudflare Cache Rule 与 Purge 运维备忘)、发布溯源链路。
62
62
  - `docs/adr/template.md` — 新 ADR 从此模板开始;早期已接受决策存于 `review/decision-log/`。
63
- - 下游消费方式:安装 `@q1travel/design-system` 后 import 组件与 `style.css` / `tokens.css`;`npx q1-design sync` 把包内 skill 与契约物化为只读缓存,`q1-design verify` 校验缓存未被改动,`q1-design lint` 执行 `Q1-DS-*` 机械规则;`q1-design ls` 列出随包组件,`q1-design doc <组件名>` 打印其公共 API(两者现推自随包的 `.d.ts`,不另存清单)。
63
+ - `docs/token-coverage-gaps.md` — 下游有真实消费需求但本仓未导出的 `--q1-*` 能力缺口登记(待决清单,非契约)。
64
+ - `docs/research/design-md-eval/` — design.md 有/无 对照评测:固定场景、确定性检查、修正队列(研究记录,非契约)。
65
+ - 下游消费方式:安装 `@q1travel/design-system` 后 import 组件与 `style.css` / `tokens.css`;`npx q1-design sync` 把包内 skill 与契约物化为只读缓存,`q1-design verify` 校验缓存未被改动,`q1-design lint` 执行 `Q1-DS-*` 机械规则;`q1-design ls` 列出随包组件,`q1-design doc <组件名>` 打印其公共 API(两者现推自随包的 `.d.ts`,不另存清单);`q1-design overrides` 统计并分类消费方对组件外观的覆写。
package/bin/q1-design.mjs CHANGED
@@ -896,8 +896,154 @@ function runDoc(consumerRootDir, requestedName) {
896
896
  }
897
897
  }
898
898
 
899
+ // —— overrides ——
900
+ // 逃生舱口的可见化。Kumo 的做法是把这个 prop 叫 DANGEROUS_className,让每次绕过都自带
901
+ // 羞耻感。本库不照搬,因为实测数据不支持:q1travel-web 里 814 处组件使用中 223 处传了
902
+ // className(27%)。当四分之一的调用都在用它,它已经是事实上的 API——给常态路径起个
903
+ // 羞耻的名字,只会训练出对警告的免疫,那比没有警告更糟;何况改名要迁 227 处调用点。
904
+ //
905
+ // 真正缺的不是羞耻感,是**知道自己在哪里被绕过、绕过去做什么**。所以做成测量命令而不是
906
+ // pass/fail 规则:lint 只看得到 JSX,分不出「加个左边距」和「把按钮从头重画」——那要去
907
+ // 读消费方 CSS 里那个类的内容。这条命令做的正是这件事。
908
+ //
909
+ // 不进 lint 的第二个理由:覆写布局是正当的,规则拦不住也不该拦。一条会把正当用法一起
910
+ // 报红的规则,最后会被整体关掉,连同它本来能抓住的东西。
911
+
912
+ // 只动外观的属性:这些出现即意味着「在重画组件」,而不是「在摆放组件」。
913
+ const SKIN_PROPERTIES = new Set([
914
+ 'background', 'background-color', 'background-image', 'border', 'border-color',
915
+ 'border-radius', 'border-width', 'border-style', 'color', 'box-shadow',
916
+ 'font-size', 'font-weight', 'outline', 'opacity',
917
+ ]);
918
+ // 只摆位置的属性:这些是消费方的正当需求,组件本就不该管。
919
+ const BOX_PROPERTIES = new Set([
920
+ 'margin', 'margin-top', 'margin-bottom', 'margin-left', 'margin-right',
921
+ 'margin-block', 'margin-inline', 'width', 'max-width', 'min-width',
922
+ 'flex', 'flex-grow', 'flex-shrink', 'grid-area', 'grid-column', 'grid-row',
923
+ 'align-self', 'justify-self', 'order', 'position', 'top', 'left', 'right', 'bottom',
924
+ ]);
925
+
926
+ const OVERRIDE_SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
927
+
928
+ // 消费仓常在自己内部再导出一层(q1travel-web 的 src/components/ui/index.js 就是
929
+ // `export { Button } from "@q1travel/design-system"`,173 个文件走这条路,只有 64 个直接导入)。
930
+ // 只认包名会漏掉绝大多数调用点,所以额外接受调用方声明的再导出路径。
931
+ export function collectDesignSystemComponents(sourceText, reexportPaths = []) {
932
+ const names = new Set();
933
+ const sourcePatterns = [PACKAGE_NAME, ...reexportPaths].map((path) =>
934
+ path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
935
+ );
936
+ const importPattern = new RegExp(
937
+ `import\\s*\\{([^{}]*?)\\}\\s*from\\s*["'](?:${sourcePatterns.join('|')})["']`,
938
+ 'gs',
939
+ );
940
+ for (const match of sourceText.matchAll(importPattern)) {
941
+ for (const part of match[1].split(',')) {
942
+ const name = part.replace(/\btype\b/, '').trim().split(/\s+as\s+/).pop()?.trim();
943
+ if (name && /^[A-Z][A-Za-z0-9_]*$/.test(name)) names.add(name);
944
+ }
945
+ }
946
+ return names;
947
+ }
948
+
949
+ // `<Button ... className={cx('a', 'b')}>` → ['a','b']。取标签内 400 字符窗口:
950
+ // 再长的标签里出现的 className 多半属于嵌套元素,算进来会虚报。
951
+ export function collectOverrideClassNames(sourceText, componentNames) {
952
+ const found = [];
953
+ for (const name of componentNames) {
954
+ const pattern = new RegExp(`<${name}\\b[^>]{0,400}?className=(\\{[^}]*\\}|"[^"]*"|'[^']*')`, 'gs');
955
+ for (const match of sourceText.matchAll(pattern)) {
956
+ for (const token of match[1].matchAll(/[A-Za-z][\w-]*/g)) {
957
+ const value = token[0];
958
+ if (['cx', 'clsx', 'classNames', 'undefined', 'true', 'false', 'null'].includes(value)) continue;
959
+ found.push({ component: name, className: value });
960
+ }
961
+ }
962
+ }
963
+ return found;
964
+ }
965
+
966
+ export function classifyOverride(className, cssText) {
967
+ const escaped = className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
968
+ const bodies = [...cssText.matchAll(new RegExp(`\\.${escaped}\\b[^{]*\\{([^}]*)\\}`, 'gs'))];
969
+ if (bodies.length === 0) return 'undefined-in-css';
970
+ const properties = new Set();
971
+ for (const body of bodies) {
972
+ for (const declaration of body[1].split(';')) {
973
+ const [property] = declaration.split(':');
974
+ if (property) properties.add(property.trim());
975
+ }
976
+ }
977
+ for (const property of properties) if (SKIN_PROPERTIES.has(property)) return 'skin';
978
+ for (const property of properties) if (BOX_PROPERTIES.has(property)) return 'box';
979
+ return 'other';
980
+ }
981
+
982
+ function runOverrides(consumerRootDir, inputPaths) {
983
+ const targets = collectLintTargetFiles(inputPaths.length > 0 ? inputPaths : ['src'], consumerRootDir);
984
+ let cssText = '';
985
+ const sources = [];
986
+ for (const file of targets) {
987
+ const text = readFileSync(file, 'utf8');
988
+ if (extname(file) === '.css') cssText += `\n${text}`;
989
+ else if (OVERRIDE_SOURCE_EXTENSIONS.has(extname(file))) sources.push(text);
990
+ }
991
+
992
+ // 自动发现再导出出口:仓内任何 `export { X } from '@q1travel/design-system'` 的模块,
993
+ // 它的导入路径同样是设计系统的调用点。不自动发现的话,用了再导出的仓库会得到一个
994
+ // 看起来很干净、实则漏掉大半调用点的报告——那比没有报告更误导。
995
+ const reexportPaths = [];
996
+ for (const file of targets) {
997
+ if (!OVERRIDE_SOURCE_EXTENSIONS.has(extname(file))) continue;
998
+ const text = readFileSync(file, 'utf8');
999
+ if (!new RegExp(`export\\s*\\{[^{}]*\\}\\s*from\\s*["']${PACKAGE_NAME}["']`).test(text)) continue;
1000
+ const relative_ = toPosixPath(relative(consumerRootDir, file)).replace(/\/index\.(t|j)sx?$/, '').replace(/\.(t|j)sx?$/, '');
1001
+ reexportPaths.push(relative_, `@/${relative_.replace(/^src\//, '')}`, `~/${relative_.replace(/^src\//, '')}`);
1002
+ }
1003
+
1004
+ const overrides = [];
1005
+ for (const text of sources) {
1006
+ const components = collectDesignSystemComponents(text, reexportPaths);
1007
+ if (components.size === 0) continue;
1008
+ overrides.push(...collectOverrideClassNames(text, components));
1009
+ }
1010
+
1011
+ if (overrides.length === 0) {
1012
+ console.log('[q1-design] overrides: 未发现对设计系统组件的 className 覆写。');
1013
+ return;
1014
+ }
1015
+
1016
+ const buckets = { skin: 0, box: 0, other: 0, 'undefined-in-css': 0 };
1017
+ const perClass = new Map();
1018
+ for (const { component, className } of overrides) {
1019
+ const kind = classifyOverride(className, cssText);
1020
+ buckets[kind] += 1;
1021
+ const key = `${kind}|${component}|${className}`;
1022
+ perClass.set(key, (perClass.get(key) ?? 0) + 1);
1023
+ }
1024
+
1025
+ console.log(`[q1-design] overrides: 共 ${overrides.length} 处`);
1026
+ console.log(` 重画外观 (skin) ${buckets.skin}`);
1027
+ console.log(` 仅布局 (box) ${buckets.box}`);
1028
+ console.log(` 其它属性 ${buckets.other}`);
1029
+ console.log(` CSS 中未找到定义 ${buckets['undefined-in-css']}`);
1030
+ const top = [...perClass.entries()]
1031
+ .filter(([key]) => key.startsWith('skin|'))
1032
+ .sort((left, right) => right[1] - left[1])
1033
+ .slice(0, 10);
1034
+ if (top.length > 0) {
1035
+ console.log('\n重画外观的覆写 Top:');
1036
+ for (const [key, count] of top) {
1037
+ const [, component, className] = key.split('|');
1038
+ console.log(` ${String(count).padStart(3)}× <${component} className="${className}">`);
1039
+ }
1040
+ console.log('\n「重画外观」意味着组件缺一档能力,或调用点该换一档现有 variant。');
1041
+ console.log('「仅布局」是正当用法,组件本就不该管间距与排布。');
1042
+ }
1043
+ }
1044
+
899
1045
  function printUsage() {
900
- console.error('Usage: q1-design <ls|doc|sync|verify|lint|eval>');
1046
+ console.error('Usage: q1-design <ls|doc|sync|verify|lint|eval|overrides>');
901
1047
  console.error(' ls 列出随包发布的组件与 hook');
902
1048
  console.error(' doc <组件名> 打印该组件的公共 API(函数签名 + 同名前缀的类型)');
903
1049
  console.error(
@@ -907,6 +1053,9 @@ function printUsage() {
907
1053
  ' lint --diff <base-ref> [路径...] 只扫相对 base-ref 有改动的文件(存量重的仓库用它当增量门禁)',
908
1054
  );
909
1055
  console.error(' eval 对 tooling/scripts/evals 的 fixtures 跑 before/after 断言');
1056
+ console.error(
1057
+ ' overrides [路径...] 统计对设计系统组件的 className 覆写,并按「重画外观 / 仅布局」分类',
1058
+ );
910
1059
  }
911
1060
 
912
1061
  // 直跑守卫(与 scripts/ci-path-filter.mjs 同款):被 import 时只暴露纯函数,不执行 CLI,
@@ -941,6 +1090,8 @@ if (isDirectRun) {
941
1090
  runLint(args.length > 0 ? args : ['src'], consumerRootDir, diffBaseRef);
942
1091
  } else if (subcommand === 'eval') {
943
1092
  runEval();
1093
+ } else if (subcommand === 'overrides') {
1094
+ runOverrides(consumerRootDir, process.argv.slice(3));
944
1095
  } else {
945
1096
  printUsage();
946
1097
  process.exit(1);
@@ -313,13 +313,46 @@ components:
313
313
  activeTextColor: "{semantic-colors.text-primary}"
314
314
  borderColor: "{semantic-colors.border}"
315
315
  ---
316
-
317
316
  # q1travel Design System
318
317
 
319
318
  ## Overview
320
319
 
321
320
  This is the dark-theme source contract. It keeps token names aligned with the light contract and changes values only where the theme requires a different surface, text, border, or status value.
322
321
 
322
+ ## Scope and Reader
323
+
324
+ This file is the public entry point to q1travel design authority. It is written to be loaded whole, by an agent that may have no access to this repository and no other q1travel context.
325
+
326
+ It covers: which decisions are already made, which components exist, how space and hierarchy are composed, and which recognizable failures to avoid.
327
+
328
+ It does not cover: per-component API surfaces, per-surface interaction detail, or migration state. Inside this repository those live in `contracts/interface-guidelines.md` (surface rules), `contracts/implementation-map.md` (reference-to-implementation mapping), `contracts/token-naming.md` (token layers and migration), and `contracts/icon-system.md`. Where this file and those contracts disagree, the narrower contract wins and this file should be corrected.
329
+
330
+ Reference systems provide intent, never implementation source. Do not copy markup, class names, computed styles, or measurements taken from a screenshot.
331
+
332
+ ## Priority Order
333
+
334
+ When requirements conflict, protect in this order. A lower item never justifies breaking a higher one.
335
+
336
+ 1. Supplied facts: amounts, units, currencies, time zones, identifiers, and every qualifier attached to them.
337
+ 2. Accessibility contracts for the component tier in use. A Tier A keyboard model is not negotiable.
338
+ 3. Registered components and semantic tokens over new local structure.
339
+ 4. The reader's question, and the shortest honest path to its answer.
340
+ 5. q1travel authorship being unmistakable.
341
+ 6. Composition chosen for this particular material.
342
+ 7. Responsive refinement and motion detail.
343
+
344
+ ## Working Method
345
+
346
+ Four passes. Do not start at pass three.
347
+
348
+ **Frame the task.** Name the person deciding, what they decide, and the strongest supported answer. Separate observation from projection. Support two paths through the same page: a fast path that reaches the conclusion, and an audit path that exposes the evidence behind it.
349
+
350
+ **Choose composition.** Reject the obvious template for the document category. Map the material to visual variables: magnitude to length, time to sequence, alternatives to aligned rows. Use a table for exact lookup, prose for a single conclusion, a chart only where a relationship reads faster as a shape than as numbers.
351
+
352
+ **Apply the system.** Compose from the inventory below. Consume semantic tokens, not source values. Stay monochrome unless colour carries meaning that also survives without colour.
353
+
354
+ **Inspect before delivery.** Run the Verification section. Check the first viewport, the peer alignment, the theme pair, and the keyboard path before calling the work done.
355
+
323
356
  ## Token Layers
324
357
 
325
358
  - Core tokens define source design values such as color, spacing, radius, and typography.
@@ -328,6 +361,177 @@ This is the dark-theme source contract. It keeps token names aligned with the li
328
361
 
329
362
  ## Usage
330
363
 
331
- - Product code should consume semantic tokens and component mappings before using core values directly.
332
- - Theme-specific values may differ, but token names should stay stable across light and dark contracts.
333
- - Current theme resolved values are inspection output only; source values live in this contract and the runtime token layer.
364
+ - Product code consumes semantic tokens and component mappings before core values, and never literal colour values.
365
+ - New public tokens use the `--q1-*` prefix. Token names stay stable across the light and dark contracts; only values differ.
366
+ - Consume token names, never the resolved values printed by an inspector. A resolved value is inspection output, not an API.
367
+ - Every reference must resolve. `var(--x)` naming a token that does not exist fails silently: the declaration is dropped and the surface quietly loses a radius, a colour, or a border.
368
+
369
+ ## Primitives Inventory
370
+
371
+ These components ship in the package. Compose from this list before writing new structure. If a needed capability is missing from a component, extend that component rather than adding another local pattern beside it.
372
+
373
+ `q1-design ls` prints this list from the installed package, and `q1-design doc <name>` prints one component's public API. Use those when the package is installed; use this section when it is not.
374
+
375
+ **Actions.** Button, ButtonLink, IconButton, SplitButton, LoadMoreButton, ClearButton, CopyButton, Link, Toolbar.
376
+
377
+ - Button carries the action. ButtonLink navigates but reads as a button, for a page-level call to action. Link reads as text, inside a sentence. Substituting one for another makes the page read wrong.
378
+ - SplitButton is a primary action with an adjacent menu whose first entry mirrors it. Do not use it to hide unrelated actions behind a chevron.
379
+ - IconButton requires an accessible name. An icon-only control without one is not deliverable.
380
+
381
+ **Text fields.** FormField, Label, Input, ClearableInput, SearchInput, Textarea, Select, Fieldset, ErrorMessage.
382
+
383
+ - FormField owns the label, help text, and error slot. Do not assemble those three around a bare Input.
384
+ - Select is for a short, fixed list. Anything searchable, or longer than roughly seven entries, is a Combobox.
385
+
386
+ **Option fields.** ControlOption, Checkbox, Radio, Choicebox, Combobox, MultiSelect, Switch, Toggle, Slider, RangeSlider.
387
+
388
+ - Switch commits immediately. Checkbox stages a change that a submit applies. Choosing the wrong one misstates when the change takes effect.
389
+ - Toggle selects among mutually exclusive views. It is not a Switch with more entries.
390
+ - RangeSlider is two thumbs over one interval, paired with numeric inputs; Slider is a single value.
391
+
392
+ **Dates and ranges.** CalendarGrid, RangeInputs, RangeField, PeriodCombobox, addDays.
393
+
394
+ **Overlays.** Modal, ModalInset, DestructiveActionModal, Drawer, Sheet, Menu, ContextMenu, DotsMenu, Tooltip, ContextCard, SearchDialog, CommandMenu.
395
+
396
+ - Modal blocks and demands an answer. Use it for confirmation, not for a long form; a long form belongs in a Drawer or its own route.
397
+ - DestructiveActionModal is the required shape for irreversible actions. Do not rebuild that confirmation from a plain Modal.
398
+ - Sheet slides in from an edge with a focus trap; Drawer is the wider panel form. Menu is anchored and flips on collision; ContextMenu and DotsMenu share its keyboard engine.
399
+ - Tooltip carries a short label for a control. Anything a reader must read to decide belongs in the page or in a ContextCard.
400
+
401
+ **Data and evidence.** Table, Pagination, DocsPagination, ShowMore, Chart, Gauge, Progress, JsonView, Entity, Description, RelativeTimeCard.
402
+
403
+ - Table owns tabular semantics, selection, sorting, and density. Never assemble rows out of divs.
404
+ - Pagination paginates a data set with known or unknown totals. DocsPagination is previous/next reading order and is not interchangeable with it.
405
+ - Gauge is one ratio as a ring. Progress is advancement along a task, and takes segment stops.
406
+ - RelativeTimeCard reveals the absolute time behind a relative one. Prefer it to printing both.
407
+
408
+ **Status and feedback.** Banner, ProjectBanner, Note, ErrorCard, ErrorMessage, EmptyState, Badge, Pill, StatusDot, ToastProvider, useToast, Feedback, Spinner, LoadingDots, Skeleton.
409
+
410
+ - Banner announces at page scope. Note is an inline aside. ErrorMessage sits against one field. ErrorCard replaces a region that failed to load. Picking the widest one available overstates the problem.
411
+ - Toast is for non-blocking confirmation of something that already happened. It is not where an error the reader must act on belongs.
412
+ - Badge and Pill label a state drawn from a known set. Ordinary metadata is text, not a badge. StatusDot is the smallest form, and needs a text label beside it.
413
+ - Skeleton holds the shape of content that is coming. Spinner and LoadingDots say only that something is happening.
414
+
415
+ **Structure and navigation.** Card, AuthCard, Sidebar, SidebarDrawer, Navigation, NavigationSection, NavigationItem, NavigationGroup, ContextSwitcher, Breadcrumb, CollapsibleBreadcrumb, Tabs, Collapse, CollapseGroup, Separator, Scroller, ScrollerWithButtons, FileTree.
416
+
417
+ - Card is a surface you have earned, not a default wrapper. Reach for spacing and alignment first.
418
+ - Tabs switch between peer views of the same subject. They are not a way to shorten a page.
419
+ - ContextSwitcher is the two-level selection model; the consumer owns the state behind it.
420
+
421
+ **Identity and media.** Avatar, AvatarGroup, Book, Browser, BrowserDots, BrowserControls, BrowserAddress, BrowserTabs, BrowserPlaceholder, Phone, Video, ThemeSwitcher.
422
+
423
+ **Text utilities.** Code, CodeInline, CodeTokens, CodeBlock, Snippet, Kbd, MiddleTruncate, TextWithCopyButton.
424
+
425
+ - Kbd holds one key or one modifier per element.
426
+ - MiddleTruncate preserves both ends of an identifier. Do not truncate identifiers with a trailing ellipsis.
427
+
428
+ **Hooks.** useToast, useCopy, useRadio.
429
+
430
+ ## Document Stylesheet
431
+
432
+ The inventory above needs a React application. When the page is a standalone HTML document — a report, a proposal, a brief — produced outside that application, load the published stylesheet instead and compose from its class vocabulary.
433
+
434
+ ```html
435
+ <link rel="stylesheet" href="https://design.q1lx.com/design.css">
436
+ <body class="q1-doc">
437
+ <div class="q1-shell">…</div>
438
+ </body>
439
+ ```
440
+
441
+ The stylesheet is self-contained: it carries the token values it needs, so no other file has to load first. It resolves the light and dark palettes from the reader's system setting, and accepts an explicit `data-theme` of `light` or `dark` on the root element. Do not add a visible theme control.
442
+
443
+ **The vocabulary is bounded.** Treat the names below as the entire public surface. Do not invent class names, do not guess at undocumented ones, and do not write page-level `style` attributes or a second stylesheet beside it — typography, spacing, and rhythm are decided here precisely so they are not decided again per page. If something is missing, add it to the vocabulary rather than working around it.
444
+
445
+ Never load this stylesheet together with the component stylesheet that ships in the package. They serve different worlds, and loading both creates two answers to the same question.
446
+
447
+ Shell: `q1-doc`, `q1-shell`, `q1-skip-link`, `q1-header`, `q1-masthead`, `q1-footer`.
448
+
449
+ Composition: `q1-grid`, `q1-span-4`, `q1-span-6`, `q1-span-8`, `q1-span-12`, `q1-stack`, `q1-cluster`, `q1-split`, `q1-section`, `q1-reading`.
450
+
451
+ Typography: `q1-display`, `q1-title`, `q1-heading-24`, `q1-heading-20`, `q1-heading-16`, `q1-lede`, `q1-body`, `q1-label`, `q1-meta`, `q1-caption`, `q1-mono`, `q1-numeric`.
452
+
453
+ Evidence: `q1-stat-strip`, `q1-stat`, `q1-stat-label`, `q1-stat-value`, `q1-stat-detail`, `q1-table-wrap`, `q1-note`, `q1-formula`, `q1-sources`.
454
+
455
+ Comparison: `q1-bar-list`, `q1-bar`, `q1-bar-label`, `q1-bar-value`, `q1-bar-track`, `q1-bar-fill`.
456
+
457
+ Utility: `q1-sr-only`.
458
+
459
+ Rules that the classes alone do not carry:
460
+
461
+ - `q1-table-wrap` owns exactly one semantic `table`. Mark numeric headers and cells with `q1-numeric` so the column aligns and its digits share a width. `q1-numeric` belongs on table cells only; `q1-stat-value` already sets tabular figures and must not carry it.
462
+ - `q1-note` takes its meaning from `data-tone` of `info`, `success`, `warning`, or `danger`. The wording must carry that meaning too; the colour is a second cue, never the only one.
463
+ - Every `q1-bar-fill` in one list sets `--q1-bar-ratio` against the same maximum. Bars drawn without a shared scale compare nothing.
464
+ - A `q1-stat-strip` holds peer figures. Label, value, and detail are present or absent consistently across the whole strip.
465
+ - Reading prose sits in `q1-reading` or a six-to-seven column span, not across the full twelve.
466
+
467
+ ## Composition Rules
468
+
469
+ **Grid.** Twelve columns on desktop, six on tablet, four on mobile. Reading prose occupies six to seven desktop columns. Tables, charts, and major comparisons take the full twelve. Grid children set a zero minimum width so content reflows instead of overflowing.
470
+
471
+ **Spacing expresses grouping, not decoration.** Choose the step from the relationship, then keep it identical across peers.
472
+
473
+ - Within a group: `space-1` to `space-3`.
474
+ - Between groups: `space-4` to `space-6`.
475
+ - Between major sections: `space-8` to `space-10`.
476
+ - A true chapter break: `space-16`. Reserved; not a default.
477
+
478
+ Label, value, and detail spacing is identical across every peer in a set. One paragraph rhythm throughout the body.
479
+
480
+ **Hierarchy comes from type, weight, and space before it comes from surfaces.** Earn every border and every filled surface. Align every object to a shared edge, baseline, or grid line.
481
+
482
+ **Colour.** Monochrome by default. Add colour only where it encodes state, action, or a data series, and always with a second cue that is not colour. The light and dark themes are equivalent in meaning and hierarchy; neither is the primary one.
483
+
484
+ **Copy.** Sentence case headings that state the claim rather than name the section. Simplify the language, never the claim. Keep every qualifier attached to the number it qualifies.
485
+
486
+ ## DO
487
+
488
+ - Answer the reader's question in the first viewport, together with the one relationship the page turns on.
489
+ - Give the page exactly one `h1`, and descend heading levels without skipping.
490
+ - Consume semantic tokens; let the theme resolve the value.
491
+ - Use a registered component; extend it when it falls short.
492
+ - Right-align numeric columns and keep their decimal places consistent down the column.
493
+ - Let a table occupy the full width of the region that owns it.
494
+ - Keep peer values, baselines, and label positions aligned across a set.
495
+ - Name every icon-only control, and keep focus visible on every interactive element.
496
+ - Preserve uncertainty: a projection is labelled as one, and a caveat travels with its figure.
497
+ - Honour reduced-motion preferences.
498
+
499
+ ## DON'T
500
+
501
+ Do not ship any of these. Each is a recognizable generated-page default, and several are already blocked by gates in consuming repositories.
502
+
503
+ - Literal colour values in product code. Use tokens.
504
+ - A token reference with no definition behind it, or a component token with no fallback to the semantic token it replaces.
505
+ - A native `select` where the list is searchable or long.
506
+ - A hand-built Button, Badge, Table, Input, or Textarea beside the registered one.
507
+ - Reaching into another component's class names to adjust its appearance.
508
+ - All-caps eyebrows, overlines, and decorative numbered section labels.
509
+ - Decorative gradients, glows, blobs, stripes, glass effects, and shadows that carry no elevation meaning.
510
+ - A centred hero followed by a grid of equal cards.
511
+ - Repeated metric boxes standing in for a composed relationship.
512
+ - Badges or pills used for ordinary metadata.
513
+ - Nested cards, or a second border, added to repair a hierarchy that type and spacing should have carried.
514
+ - A dark rounded rectangle drawn around every chart.
515
+ - Tiny muted prose, inconsistent peer values, and misaligned baselines.
516
+ - A narrow table floating in a wide section, or a wide table compressed until words break.
517
+ - A legend where a direct label would do, or colour used without meaning.
518
+ - Bars at full width without a shared scale.
519
+ - Summary, conclusion, and recommendation sections that all restate the same sentence.
520
+ - Narration of how the page was produced.
521
+ - A visible theme switcher on a delivered page, or decorative brand marks.
522
+
523
+ ## Verification
524
+
525
+ Before delivery, confirm each of these. They are observable; none of them is a matter of taste.
526
+
527
+ - Every `var(--token)` resolves to a defined token, or carries an explicit fallback.
528
+ - No literal colour value appears outside the token definition layer.
529
+ - Every interactive element is reachable by keyboard, in a sensible order, with a visible focus style.
530
+ - Tier A components follow their WAI-ARIA APG keyboard model; Tier B controls use native semantics with an associated name; Tier C hides decorative visuals from assistive technology and names every icon-only control.
531
+ - The page carries one `h1`, ordered headings, and landmark regions.
532
+ - Text and its background meet WCAG AA, and no state is signalled by colour alone.
533
+ - Light and dark render the same hierarchy, with no missing surface or invisible border in either.
534
+ - Peer spacing, alignment, and numeric formatting are identical across each set.
535
+ - Nothing scrolls horizontally at the page level.
536
+
537
+ In this repository, `npm run check` is the full gate; a partial run is not a pass. In consuming repositories, run their design and token gates before reporting completion.