@godxjp/ui 31.11.1 → 31.13.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/agent/START-HERE.md +4 -4
- package/agent/components/Timeline.json +7 -1
- package/agent/components.json +7 -1
- package/agent/index.json +4 -4
- package/agent/llms.txt +5 -5
- package/agent/tokens.json +12 -0
- package/dist/components/data-display/timeline.d.ts +13 -1
- package/dist/components/data-display/timeline.js +2 -2
- package/dist/contracts/measurement.json +1 -1
- package/dist/props/registry.d.ts +1 -1
- package/dist/props/registry.js +1 -1
- package/dist/styles/data-display-layout.css +6 -2
- package/dist/styles/layers.json +1 -1
- package/dist/tokens/components/data-display.css +3 -0
- package/docs/CONSUMER-RULES.md +1 -1
- package/docs/DESIGN-AUTHORITY.md +6 -0
- package/docs/data-display/timeline.tsx +13 -0
- package/package.json +2 -2
- package/scripts/ui-audit.mjs +129 -0
- package/scripts/visual-audit-rules.mjs +17 -0
- package/scripts/visual-audit.mjs +31 -1
package/agent/START-HERE.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
You are about to write code against a design system you did not author. This file is the whole
|
|
4
4
|
contract. Read it before you write JSX.
|
|
5
5
|
|
|
6
|
-
**This catalog describes `@godxjp/ui` 31.
|
|
6
|
+
**This catalog describes `@godxjp/ui` 31.13.0.** If the project you are editing has a different
|
|
7
7
|
version in its `package.json`, read the pinned catalog for THAT version instead
|
|
8
8
|
(`…/v<their-version>/agent/…`). A catalog newer than the installed package describes props that do
|
|
9
9
|
not exist yet; older, and it hides props that do. Neither failure announces itself.
|
|
@@ -56,10 +56,10 @@ Then ask it: `search_components`, `get_component`, `get_tokens`, `get_rule`, `li
|
|
|
56
56
|
its `importPath`, and its examples. Fetch only the handful you picked in step 1.
|
|
57
57
|
3. `rules.json` — 50 cardinal rules. The ones about raw HTML and hardcoded colour are not
|
|
58
58
|
style advice.
|
|
59
|
-
4. `tokens.json` —
|
|
59
|
+
4. `tokens.json` — 2118 design tokens, each tagged with its `tier`. **If you were handed a
|
|
60
60
|
brand, read the 211 `foundation` entries first** — `--primary`, `--background`,
|
|
61
61
|
`--radius`, `--font-size-base` are the handful everything else derives from. The
|
|
62
|
-
|
|
62
|
+
1803 `component` entries are per-part knobs; reach for one only when a role is
|
|
63
63
|
right everywhere except one component.
|
|
64
64
|
5. `anti-ai-tells.json` — 26 shapes that make generated UI look generated, each with the
|
|
65
65
|
fix. Read before you reach for a gradient hero or a wall of coloured chips.
|
|
@@ -145,7 +145,7 @@ has stopped following the brand.
|
|
|
145
145
|
|---|---|---|---|
|
|
146
146
|
| `foundation` | 211 | the seeds — `--primary`, `--background`, `--foreground`, `--radius`, `--font-size-base`, `--shadow-color`. Everything below is derived from these | **yes — this is the main road.** Handed a brand colour, this is where it goes: `:root { --primary: <H> <S>% <L>%; }` (HSL components, no `hsl()` wrapper) |
|
|
147
147
|
| `semantic` | 104 | named roles that follow the seeds — `--ring`, `--text-link`, `--primary-hover`, `--overlay-background` | only when the seed is right and ONE role must differ. That role then stops following a later brand change |
|
|
148
|
-
| `component` |
|
|
148
|
+
| `component` | 1803 | per-part knobs, `--{component}-{part}-{property}` | rarely. Most are declared `initial` with the real default at the call site — deliberate, so a scoped override re-resolves instead of freezing at `:root` |
|
|
149
149
|
|
|
150
150
|
A token whose `value` is `initial` is not empty and not broken: `initial` is the guaranteed-invalid
|
|
151
151
|
value, so the real default is computed where the element paints it. Set it and yours wins.
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
"description": "Rail glyph strategy (default 'icon'). 'icon' = legacy look (Plane for current, CheckCircle2 otherwise). 'ordinal' = every glyph is its 1-based step number; status sets colour only. 'status' = glyph by status (done → check, current → filled dot, pending → step number).",
|
|
15
15
|
"name": "variant",
|
|
16
16
|
"type": "\"icon\" | \"ordinal\" | \"status\""
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"description": "Item spacing (default 'default'). Sets `--timeline-item-gap` — the space between items and between the rail and the body. 'default' = `--space-stack-md` (16px); 'compact' = `--space-stack-sm` (8px, retune with `--timeline-item-gap-compact`) for a dense activity log / issue history. antd's Timeline has no density prop (one fixed `itemPaddingBottom`, 20px), so this is a godx extension.",
|
|
20
|
+
"name": "density",
|
|
21
|
+
"type": "\"default\" | \"compact\""
|
|
17
22
|
}
|
|
18
23
|
],
|
|
19
24
|
"related": [
|
|
@@ -26,10 +31,11 @@
|
|
|
26
31
|
"storyPath": "data-display/Timeline.stories.tsx",
|
|
27
32
|
"tagline": "Vertical event list with an icon rail. Current item gets a highlighted glyph. `variant` switches the rail to numbered (ordinal) or status-driven glyphs, and each item carries a 3-state `status` (done/current/pending) plus an optional per-item `icon`. The whole progress column is ONE hue: done dot, current dot and travelled line all paint `--primary`, exactly as `Steps` paints `finish` and `process`.",
|
|
28
33
|
"usage": [
|
|
29
|
-
"DO pass an array of `TimelineItem` objects to `items`. Each item is `{ title, location?, time?, note?, current?, status?, icon? }`. All fields except `title` are optional. The only other
|
|
34
|
+
"DO pass an array of `TimelineItem` objects to `items`. Each item is `{ title, location?, time?, note?, current?, status?, icon? }`. All fields except `title` are optional. The only other props are `variant` and `density`.",
|
|
30
35
|
"DO mark the in-progress event with `current: true` (or `status: 'current'`). In the default `variant='icon'`, the current item renders a `Plane` icon and every other item a `CheckCircle2`. Use `variant='ordinal'` for a numbered route stepper (1,2,3…) or `variant='status'` for a done→check / current→dot / pending→number tracker. Pass `status: 'done' | 'current' | 'pending'` per item to drive colour and (in the status variant) the glyph; pass `icon` (a LucideIcon) to override the glyph for a single item.",
|
|
31
36
|
"DO pass `ReactNode` to `title`, `location`, `time`, and `note` — you can embed formatted text, `<Badge>`, `<Badge>`, or `<span>` inside those fields. Use `formatDate` to pre-format timestamps before passing them as `time`.",
|
|
32
37
|
"DO NOT hand-roll a vertical event list with divs, icons, and connector lines — that is exactly what Timeline ships. Do not apply extra padding or wrapping outside the component; it manages its own rail and spacing internally.",
|
|
38
|
+
"DO pass `density=\"compact\"` when the timeline is a long, dense log (issue history, audit trail) and the default 16px between items reads too airy — it drops the item gap to `--space-stack-sm` (8px). Never tighten it with your own padding/margin on the items.",
|
|
33
39
|
"DO NOT use Timeline for user-facing wizard progress (steps the user must complete in order) — use `Steps` for that. Timeline is read-only historical/status display; it has no interactive state, no `onClick`, and no concept of 'go to step'.",
|
|
34
40
|
"DO wrap Timeline in `<CardContent>` when placing it inside a `Card` — bare `Card` has no inner padding, so the rail will render flush against the card edge without `CardContent`.",
|
|
35
41
|
"THE PROGRESS COLUMN IS ONE HUE (gh#731). The done dot, the current dot and the travelled line all default to `hsl(var(--primary))` — the same contract `Steps` uses for `finish` + `process`, and antd's (only `error` leaves the primary hue). The current item is told apart by a ring (`--timeline-dot-current-ring-width`, `--steps-dot-process-ring-width`'s value), by `aria-current=\"step\"` + a localized sr-only prefix, and by a different glyph — never by a second colour role. Re-theme `--primary` and the whole rail follows. Measured light: done 2.18:1 → 6.31:1 against the card, glyph 6.31:1 on the fill; dark 7.22:1 → 9.84:1, glyph 10.72:1.",
|
package/agent/components.json
CHANGED
|
@@ -4974,6 +4974,11 @@
|
|
|
4974
4974
|
"description": "Rail glyph strategy (default 'icon'). 'icon' = legacy look (Plane for current, CheckCircle2 otherwise). 'ordinal' = every glyph is its 1-based step number; status sets colour only. 'status' = glyph by status (done → check, current → filled dot, pending → step number).",
|
|
4975
4975
|
"name": "variant",
|
|
4976
4976
|
"type": "\"icon\" | \"ordinal\" | \"status\""
|
|
4977
|
+
},
|
|
4978
|
+
{
|
|
4979
|
+
"description": "Item spacing (default 'default'). Sets `--timeline-item-gap` — the space between items and between the rail and the body. 'default' = `--space-stack-md` (16px); 'compact' = `--space-stack-sm` (8px, retune with `--timeline-item-gap-compact`) for a dense activity log / issue history. antd's Timeline has no density prop (one fixed `itemPaddingBottom`, 20px), so this is a godx extension.",
|
|
4980
|
+
"name": "density",
|
|
4981
|
+
"type": "\"default\" | \"compact\""
|
|
4977
4982
|
}
|
|
4978
4983
|
],
|
|
4979
4984
|
"related": [
|
|
@@ -4986,10 +4991,11 @@
|
|
|
4986
4991
|
"storyPath": "data-display/Timeline.stories.tsx",
|
|
4987
4992
|
"tagline": "Vertical event list with an icon rail. Current item gets a highlighted glyph. `variant` switches the rail to numbered (ordinal) or status-driven glyphs, and each item carries a 3-state `status` (done/current/pending) plus an optional per-item `icon`. The whole progress column is ONE hue: done dot, current dot and travelled line all paint `--primary`, exactly as `Steps` paints `finish` and `process`.",
|
|
4988
4993
|
"usage": [
|
|
4989
|
-
"DO pass an array of `TimelineItem` objects to `items`. Each item is `{ title, location?, time?, note?, current?, status?, icon? }`. All fields except `title` are optional. The only other
|
|
4994
|
+
"DO pass an array of `TimelineItem` objects to `items`. Each item is `{ title, location?, time?, note?, current?, status?, icon? }`. All fields except `title` are optional. The only other props are `variant` and `density`.",
|
|
4990
4995
|
"DO mark the in-progress event with `current: true` (or `status: 'current'`). In the default `variant='icon'`, the current item renders a `Plane` icon and every other item a `CheckCircle2`. Use `variant='ordinal'` for a numbered route stepper (1,2,3…) or `variant='status'` for a done→check / current→dot / pending→number tracker. Pass `status: 'done' | 'current' | 'pending'` per item to drive colour and (in the status variant) the glyph; pass `icon` (a LucideIcon) to override the glyph for a single item.",
|
|
4991
4996
|
"DO pass `ReactNode` to `title`, `location`, `time`, and `note` — you can embed formatted text, `<Badge>`, `<Badge>`, or `<span>` inside those fields. Use `formatDate` to pre-format timestamps before passing them as `time`.",
|
|
4992
4997
|
"DO NOT hand-roll a vertical event list with divs, icons, and connector lines — that is exactly what Timeline ships. Do not apply extra padding or wrapping outside the component; it manages its own rail and spacing internally.",
|
|
4998
|
+
"DO pass `density=\"compact\"` when the timeline is a long, dense log (issue history, audit trail) and the default 16px between items reads too airy — it drops the item gap to `--space-stack-sm` (8px). Never tighten it with your own padding/margin on the items.",
|
|
4993
4999
|
"DO NOT use Timeline for user-facing wizard progress (steps the user must complete in order) — use `Steps` for that. Timeline is read-only historical/status display; it has no interactive state, no `onClick`, and no concept of 'go to step'.",
|
|
4994
5000
|
"DO wrap Timeline in `<CardContent>` when placing it inside a `Card` — bare `Card` has no inner padding, so the rail will render flush against the card edge without `CardContent`.",
|
|
4995
5001
|
"THE PROGRESS COLUMN IS ONE HUE (gh#731). The done dot, the current dot and the travelled line all default to `hsl(var(--primary))` — the same contract `Steps` uses for `finish` + `process`, and antd's (only `error` leaves the primary hue). The current item is told apart by a ring (`--timeline-dot-current-ring-width`, `--steps-dot-process-ring-width`'s value), by `aria-current=\"step\"` + a localized sr-only prefix, and by a different glyph — never by a second colour role. Re-theme `--primary` and the whole rail follows. Measured light: done 2.18:1 → 6.31:1 against the card, glyph 6.31:1 on the fill; dark 7.22:1 → 9.84:1, glyph 10.72:1.",
|
package/agent/index.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"components": 178,
|
|
5
5
|
"patterns": 21,
|
|
6
6
|
"rules": 50,
|
|
7
|
-
"tokens":
|
|
7
|
+
"tokens": 2118,
|
|
8
8
|
"vocabulary": 14
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
@@ -48,19 +48,19 @@
|
|
|
48
48
|
"note": "Pin to the tag that matches the @godxjp/ui version you installed. A catalog newer than your package describes props you do not have; older, and it hides props you do.",
|
|
49
49
|
"read": {
|
|
50
50
|
"live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
|
|
51
|
-
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v31.
|
|
51
|
+
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v31.13.0/agent/index.json"
|
|
52
52
|
},
|
|
53
53
|
"source": "mcp/src/data — the same data @godxjp/ui-mcp serves — plus the foundation and semantic token tiers, read from src/tokens/*.css",
|
|
54
54
|
"start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
|
|
55
55
|
"tokenTiers": {
|
|
56
56
|
"component": "per-part knobs, --{component}-{part}-{property}; usually leave these alone",
|
|
57
57
|
"counts": {
|
|
58
|
-
"component":
|
|
58
|
+
"component": 1803,
|
|
59
59
|
"foundation": 211,
|
|
60
60
|
"semantic": 104
|
|
61
61
|
},
|
|
62
62
|
"foundation": "the seeds a consumer is invited to set — --primary, --background, --radius",
|
|
63
63
|
"semantic": "named roles that follow the seeds — --ring, --text-link, --overlay-background"
|
|
64
64
|
},
|
|
65
|
-
"version": "31.
|
|
65
|
+
"version": "31.13.0"
|
|
66
66
|
}
|
package/agent/llms.txt
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @godxjp/ui
|
|
2
2
|
|
|
3
|
-
> A Japanese-enterprise React design system: 178 components,
|
|
4
|
-
> 50 cardinal rules. This file is the entry point for AI agents. Catalog version 31.
|
|
3
|
+
> A Japanese-enterprise React design system: 178 components, 2118 design tokens,
|
|
4
|
+
> 50 cardinal rules. This file is the entry point for AI agents. Catalog version 31.13.0.
|
|
5
5
|
|
|
6
6
|
If your client can run a process, do not read these files — run the MCP server instead
|
|
7
|
-
(`npx @godxjp/ui-mcp@31.
|
|
7
|
+
(`npx @godxjp/ui-mcp@31.13.0`). It is searchable and version-locked. These files exist for agents
|
|
8
8
|
that can only fetch URLs.
|
|
9
9
|
|
|
10
10
|
## Start
|
|
@@ -18,7 +18,7 @@ that can only fetch URLs.
|
|
|
18
18
|
- [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 48 KB — all 178 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
|
|
19
19
|
- [components/<Name>.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json): one file per component (1 KB–36 KB, median 6 KB). Read the index, then fetch only the ones you chose — this is the selective route, and the reason you do not need the blob.
|
|
20
20
|
- [components.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json): 1.2 MB — every entry in one file. Most URL fetchers truncate a response this size without saying so; prefer the per-component files.
|
|
21
|
-
- [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 211 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 104 `semantic` roles,
|
|
21
|
+
- [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 211 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 104 `semantic` roles, 1803 `component` knobs.
|
|
22
22
|
- [vocabulary.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json): the controlled prop vocabulary — which prop name means what, across every component.
|
|
23
23
|
- [rules.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json): 50 cardinal rules.
|
|
24
24
|
- [anti-ai-tells.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/anti-ai-tells.json): 26 shapes that make generated UI look generated, each with its fix.
|
|
@@ -26,7 +26,7 @@ that can only fetch URLs.
|
|
|
26
26
|
## Pinning
|
|
27
27
|
|
|
28
28
|
Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
|
|
29
|
-
the tag: `.../godx-jp/godxjp-ui/v31.
|
|
29
|
+
the tag: `.../godx-jp/godxjp-ui/v31.13.0/agent/...`. A catalog that does not match the installed
|
|
30
30
|
package describes props that are absent, or hides props that are present, and says nothing either way.
|
|
31
31
|
|
|
32
32
|
Pinned catalogs only exist for releases whose tag actually contains `agent/`. If `…/v<version>/agent/index.json` returns 404, that release predates this catalog: read `…/main/…` instead and compare `index.json` → `version` against the package you have, so you at least know which way it drifted.
|
package/agent/tokens.json
CHANGED
|
@@ -5675,6 +5675,18 @@
|
|
|
5675
5675
|
"tier": "component",
|
|
5676
5676
|
"value": "initial"
|
|
5677
5677
|
},
|
|
5678
|
+
{
|
|
5679
|
+
"description": "Space between items and between rail and body. `initial` so `Timeline density=\"compact\"` can retune it on the root; default = var(--space-stack-md), compact = --timeline-item-gap-compact (default var(--space-stack-sm)), resolved at the call site.",
|
|
5680
|
+
"name": "--timeline-item-gap",
|
|
5681
|
+
"tier": "component",
|
|
5682
|
+
"value": "initial"
|
|
5683
|
+
},
|
|
5684
|
+
{
|
|
5685
|
+
"description": "Data-display component tokens — small-by-design text knobs (rule #45/#46).",
|
|
5686
|
+
"name": "--timeline-item-gap-compact",
|
|
5687
|
+
"tier": "component",
|
|
5688
|
+
"value": "initial"
|
|
5689
|
+
},
|
|
5678
5690
|
{
|
|
5679
5691
|
"description": "QR codes stay scanner-safe in light and dark application themes. Consumers may override these component tokens only when the resulting pair retains strong contrast.",
|
|
5680
5692
|
"name": "--qr-code-foreground",
|
|
@@ -2,6 +2,11 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
import { type LucideIcon } from "lucide-react";
|
|
3
3
|
export type TimelineStatus = "done" | "current" | "pending";
|
|
4
4
|
export type TimelineVariant = "icon" | "ordinal" | "status";
|
|
5
|
+
/**
|
|
6
|
+
* A Timeline-local subset of the density vocabulary — it has no `comfortable` step, the same shape
|
|
7
|
+
* as `ListRowDensity`.
|
|
8
|
+
*/
|
|
9
|
+
export type TimelineDensity = "default" | "compact";
|
|
5
10
|
export type TimelineItem = {
|
|
6
11
|
title: ReactNode;
|
|
7
12
|
location?: ReactNode;
|
|
@@ -21,5 +26,12 @@ export type TimelineProps = {
|
|
|
21
26
|
* CheckCircle2 otherwise).
|
|
22
27
|
*/
|
|
23
28
|
variant?: TimelineVariant;
|
|
29
|
+
/**
|
|
30
|
+
* Space between items (and between the rail and the body): `--timeline-item-gap`. `default` is
|
|
31
|
+
* `--space-stack-md` (16px, today's look); `compact` is `--space-stack-sm` (8px) for a dense
|
|
32
|
+
* activity log or issue history. antd's Timeline has no density/size prop — its item spacing is
|
|
33
|
+
* one fixed token (`itemPaddingBottom`, 20px) — so this is a godx extension, not an antd port.
|
|
34
|
+
*/
|
|
35
|
+
density?: TimelineDensity;
|
|
24
36
|
};
|
|
25
|
-
export declare function Timeline({ items, variant }: TimelineProps): import("react").JSX.Element;
|
|
37
|
+
export declare function Timeline({ items, variant, density }: TimelineProps): import("react").JSX.Element;
|
|
@@ -16,9 +16,9 @@ function resolveStatus(item) {
|
|
|
16
16
|
}
|
|
17
17
|
return "done";
|
|
18
18
|
}
|
|
19
|
-
function Timeline({ items, variant = "icon" }) {
|
|
19
|
+
function Timeline({ items, variant = "icon", density = "default" }) {
|
|
20
20
|
const { t } = useTranslation();
|
|
21
|
-
return /* @__PURE__ */ jsx("ol", { className: "ui-timeline", "data-variant": variant, children: items.map((item, index) => {
|
|
21
|
+
return /* @__PURE__ */ jsx("ol", { className: "ui-timeline", "data-variant": variant, "data-density": density, children: items.map((item, index) => {
|
|
22
22
|
const status = resolveStatus(item);
|
|
23
23
|
const isCurrent = status === "current";
|
|
24
24
|
const ordinal = index + 1;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "31.
|
|
3
|
+
"version": "31.13.0",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -2996,7 +2996,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
2996
2996
|
readonly TimelineProp: {
|
|
2997
2997
|
readonly group: "data-display";
|
|
2998
2998
|
readonly file: "components/data-display/timeline.tsx";
|
|
2999
|
-
readonly vocabulary: readonly ["ClassNameProp"];
|
|
2999
|
+
readonly vocabulary: readonly ["ClassNameProp", "DensityProp"];
|
|
3000
3000
|
};
|
|
3001
3001
|
readonly TimelineGridColumnProp: {
|
|
3002
3002
|
readonly group: "data-display";
|
package/dist/props/registry.js
CHANGED
|
@@ -3528,7 +3528,7 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
3528
3528
|
TimelineProp: {
|
|
3529
3529
|
group: "data-display",
|
|
3530
3530
|
file: "components/data-display/timeline.tsx",
|
|
3531
|
-
vocabulary: ["ClassNameProp"]
|
|
3531
|
+
vocabulary: ["ClassNameProp", "DensityProp"]
|
|
3532
3532
|
},
|
|
3533
3533
|
TimelineGridColumnProp: {
|
|
3534
3534
|
group: "data-display",
|
|
@@ -472,14 +472,18 @@
|
|
|
472
472
|
flex-direction: column;
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
.ui-timeline[data-density="compact"] {
|
|
476
|
+
--timeline-item-gap: var(--timeline-item-gap-compact, var(--space-stack-sm));
|
|
477
|
+
}
|
|
478
|
+
|
|
475
479
|
.ui-timeline-item {
|
|
476
480
|
display: grid;
|
|
477
481
|
grid-template-columns: 1.5rem minmax(0, 1fr);
|
|
478
|
-
column-gap: var(--space-stack-md);
|
|
482
|
+
column-gap: var(--timeline-item-gap, var(--space-stack-md));
|
|
479
483
|
}
|
|
480
484
|
|
|
481
485
|
.ui-timeline-body {
|
|
482
|
-
padding-bottom: var(--space-stack-md);
|
|
486
|
+
padding-bottom: var(--timeline-item-gap, var(--space-stack-md));
|
|
483
487
|
}
|
|
484
488
|
|
|
485
489
|
.ui-timeline-item:last-child .ui-timeline-body {
|
package/dist/styles/layers.json
CHANGED
package/docs/CONSUMER-RULES.md
CHANGED
|
@@ -6,7 +6,7 @@ Read this once; the audit enforces it. Everything else in `docs/` is for contrib
|
|
|
6
6
|
2. **Mount `<AppProvider defaultLocale="…">` once, at the root.** Without it the kit's OWN strings (dialog close/cancel/delete, counters, picker labels) fall to the resting locale `vi` and pickers to UTC — measured in a Japanese app as Vietnamese buttons, silently (gh#1005). A development build now warns once when a kit component renders outside it; `syncI18nLocale(locale, fallback)` is the escape for an app that truly cannot mount the provider. Every page is `<PageContainer title subtitle extra footer>`; its sections are spaced by the page. Group items inside a section with `<Flex direction="col" gap>` or `<ResponsiveGrid>`.
|
|
7
7
|
3. No Tailwind layout on your own elements: no `flex`, `grid`, `gap-*`, `p-*`, `m-*`, `space-*`. Rows are `<Flex>` (default row), stacks are `<Flex direction="col">`, grids are `<ResponsiveGrid>`.
|
|
8
8
|
4. No hand-rolled surfaces: no `rounded-* border bg-*` divs. A box is `Card`, a pill is `Badge`, a person is `Avatar`, a row is `ListRow`, a label/value pair is `Descriptions`, an empty area is `EmptyState`, a read-only sample of a colour the USER chose is `Swatch`. A LIST of those rows is `<Flex as="ul" marker="none" direction="col" gap="none">` with `<ListRow as="li">` children — `marker="none"` keeps the `<ul>`, the `<li>` semantics and the gap token while dropping the bullet and the indent. Never a raw `<ul>`/`<ol>` (it carries no gap token), never `<div role="list">` + `<div role="listitem">`, and never a wrapper around each row: the divider is `:not(:last-child)` among siblings, so a row alone in its own wrapper is always the last one and EVERY divider disappears silently. A bulleted prose list is the same `<Flex as="ul">` without `marker`.
|
|
9
|
-
5. Real controls only: `Button`, `Input`, `Select`, `Textarea`, `Checkbox`… never raw `<button>`/`<input>`; a labelled control lives in `<FormField label>`. A Select outside a form takes `width="auto"`. **A disabled control's reason is visible text, never a tooltip** — see below. **Fields live in a `<Form layout="horizontal" labelWidth controlWidth>`** — never a group of `FormField`s without one, never a hand-rolled `<Flex>` row of fields (a row that belongs together is `<SpaceCompact>` or `<Form columns>`); size each control for its content with `controlWidth` (GOV.UK text-input width: a port ~7rem, a short enum ~10rem); three or more fields is a page, not a Dialog. The audit rules are `formfield-needs-form`, `dialog-form-too-big` and `select-width-hint`. **A case the kit cannot express is an issue, not a hand-roll:** open it on godx-jp/godxjp-ui, use the nearest valid composition, and mark it `// TODO(godxjp-ui#<n>)` (gh#998).
|
|
9
|
+
5. Real controls only: `Button`, `Input`, `Select`, `Textarea`, `Checkbox`… never raw `<button>`/`<input>`; a labelled control lives in `<FormField label>`. A Select outside a form takes `width="auto"`. **A disabled control's reason is visible text, never a tooltip** — see below. **Fields live in a `<Form layout="horizontal" labelWidth controlWidth>`** — never a group of `FormField`s without one, never a hand-rolled `<Flex>` row of fields (a row that belongs together is `<SpaceCompact>` or `<Form columns>`); size each control for its content with `controlWidth` (GOV.UK text-input width: a port ~7rem, a short enum ~10rem); three or more fields is a page, not a Dialog. The audit rules are `formfield-needs-form`, `dialog-form-too-big` and `select-width-hint`. **A case the kit cannot express is an issue, not a hand-roll:** open it on godx-jp/godxjp-ui, use the nearest valid composition, and mark it `// TODO(godxjp-ui#<n>)` (gh#998). **One action row, one Button size:** every `Button` under one parent takes the same `size` — a missing size is `default`, and `icon-sm` pairs with `sm`, `icon-xs` with `xs`, `icon` with default. Never `size="sm"` beside a default Button. The audit rules are `mixed-button-size` (static) and `mixed-button-height` (visual-audit, within 0.5px) (gh#1084).
|
|
10
10
|
6. Text is `<Text>` / `<Heading>` with `tone`, `size`, `weight`, `truncate`, `mono` — not `className="text-muted-foreground font-semibold"`. A STANDALONE glyph is `<Icon as={Lock} size="sm" tone="muted" />` from `@godxjp/ui/general`: a bare lucide icon carries its own `width="24" height="24"` and only four places re-size it for you (`Button`, a `DropdownMenuItem`, a `TopbarItem`, a `ListRow` leading slot), so everywhere else — in a `Text`, a table cell, an `<a>` — it draws at 24px beside 14px type. `size-4` and `w-[16px]` are closed to you by rules 3 and 8, and `size={16}` hard-codes a number the theme owns; `Icon` puts the glyph on the `--icon-size-*` scale (`2xs…4xl`) instead, and is `aria-hidden` unless you give it a `label` (then it is `role="img"` with that name). The audit rule is `lucide-icon-needs-size`.
|
|
11
11
|
7. Colours are semantic tokens (`tone="destructive"`, `bg-primary`), never palette names, hex, or `bg-black` / `text-white`.
|
|
12
12
|
8. Sizes come from props (`size`, `width`, `columns`), never `w-[240px]` / `max-h-[420px]`.
|
package/docs/DESIGN-AUTHORITY.md
CHANGED
|
@@ -399,6 +399,12 @@ bottomEnd | none` (`TablePaginationPositionProp`, default `['bottomEnd']`), the
|
|
|
399
399
|
would rule every existing tree, including the picker-sized and `size="xs"` outlines where a rule
|
|
400
400
|
every 24px is noise rather than structure. A tree used as page navigation opts in.
|
|
401
401
|
|
|
402
|
+
- **`Timeline density` has no antd counterpart.** antd's Timeline spaces items with one fixed
|
|
403
|
+
token (`itemPaddingBottom`, 20px) and offers no size or density prop. godx adds
|
|
404
|
+
`density?: "default" | "compact"` — the ListRow-shaped subset of `DensityProp` — driving one
|
|
405
|
+
token, `--timeline-item-gap`: `default` keeps `--space-stack-md` (16px), `compact` is
|
|
406
|
+
`--space-stack-sm` (8px), the step nearer antd's 20px of the two compact candidates (xs is 4px).
|
|
407
|
+
|
|
402
408
|
- **`RangeTimeline` nested rows take `Tree`'s spelling, not antd's (gh#724).** antd has no Gantt;
|
|
403
409
|
the nearest names are `Tree expandedKeys` / `defaultExpandedKeys` / `onExpand` and `Table
|
|
404
410
|
expandable.expandedRowKeys` / `onExpandedRowsChange`. This library already mapped the Tree ones
|
|
@@ -126,6 +126,19 @@ export default function Demo() {
|
|
|
126
126
|
</CardContent>
|
|
127
127
|
</Card>
|
|
128
128
|
|
|
129
|
+
<Card>
|
|
130
|
+
<CardHeader>
|
|
131
|
+
<CardTitle level={2}>density="compact"</CardTitle>
|
|
132
|
+
<CardDescription>
|
|
133
|
+
Item gap drops to --space-stack-sm (8px) for a long log such as an issue history. antd
|
|
134
|
+
has no Timeline density; this is a godx extension.
|
|
135
|
+
</CardDescription>
|
|
136
|
+
</CardHeader>
|
|
137
|
+
<CardContent>
|
|
138
|
+
<Timeline variant="status" density="compact" items={approvalItems} />
|
|
139
|
+
</CardContent>
|
|
140
|
+
</Card>
|
|
141
|
+
|
|
129
142
|
<Card>
|
|
130
143
|
<CardHeader>
|
|
131
144
|
<CardTitle level={2}>配送ルート進捗</CardTitle>
|
package/package.json
CHANGED
package/scripts/ui-audit.mjs
CHANGED
|
@@ -891,6 +891,30 @@ const RULES = [
|
|
|
891
891
|
message:
|
|
892
892
|
'Hand-rolled overflow-wrap:anywhere ([overflow-wrap:anywhere] / wrap-anywhere) — use <Text break="anywhere"> for an email, code or id in a table cell or Descriptions value. It also releases the cell\'s inherited nowrap, so drop the break-words / whitespace-normal that came with it. Not whitespace="pre-wrap": its break-word does not shrink a table column (gh#927).',
|
|
893
893
|
},
|
|
894
|
+
{
|
|
895
|
+
id: "mixed-button-size",
|
|
896
|
+
replacement: "one Button size per row",
|
|
897
|
+
scope: "consumer",
|
|
898
|
+
severity: "error",
|
|
899
|
+
/*
|
|
900
|
+
* ONE ROW, ONE BUTTON SIZE. A consumer shipped 「AIスコアを編集」 at `size="sm"` beside 裁定する at
|
|
901
|
+
* the default size in one action row: two heights, two paddings, two type sizes side by side,
|
|
902
|
+
* and the owner saw it before any audit did. Sibling <Button>s under one JSX parent must share
|
|
903
|
+
* a size. A missing `size` is `default`; an `icon-*` size belongs to its text size's family
|
|
904
|
+
* (`icon-sm` = `sm`, `icon-xs` = `xs`, `icon` = default), so `xs` beside `icon-sm` still fails.
|
|
905
|
+
*
|
|
906
|
+
* "Siblings" looks through what renders no box of its own: fragments, `{cond && …}`,
|
|
907
|
+
* ternaries, `.map(…)` and a Tooltip / TooltipTrigger around a button. Any other element is a
|
|
908
|
+
* new parent — FormActions, CardFooter, ModalFooter, Space, Flex, PageHeader `actions={…}` are
|
|
909
|
+
* all just parents here. A `size={expr}` that is not a string literal is unknown and skipped;
|
|
910
|
+
* a button kept in a variable (`{editButton}`) is invisible to a static scan — the runtime
|
|
911
|
+
* `mixed-button-height` check in visual-audit.mjs catches that one.
|
|
912
|
+
*/
|
|
913
|
+
spansElement: true,
|
|
914
|
+
matches: mixedButtonSizeMatches,
|
|
915
|
+
message:
|
|
916
|
+
"Buttons in one row mix sizes — every Button under one parent takes the same `size` (a missing size is `default`; `icon-sm` pairs with `sm`, `icon-xs` with `xs`, `icon` with default). Pick the row's size and apply it to every button in it (docs/CONSUMER-RULES.md §5).",
|
|
917
|
+
},
|
|
894
918
|
];
|
|
895
919
|
|
|
896
920
|
/**
|
|
@@ -1518,6 +1542,111 @@ function jsxOpeningEnd(source, start) {
|
|
|
1518
1542
|
return source.length;
|
|
1519
1543
|
}
|
|
1520
1544
|
|
|
1545
|
+
/** Transparent wrappers: they render no row of their own, so the buttons inside stay siblings. */
|
|
1546
|
+
const BUTTON_ROW_TRANSPARENT = new Set([
|
|
1547
|
+
"",
|
|
1548
|
+
"Fragment",
|
|
1549
|
+
"React.Fragment",
|
|
1550
|
+
"Tooltip",
|
|
1551
|
+
"TooltipTrigger",
|
|
1552
|
+
]);
|
|
1553
|
+
|
|
1554
|
+
/** A Button's `size` literal from its OPENING tag's own attributes (not a nested element's). */
|
|
1555
|
+
function buttonSizeOf(openTag) {
|
|
1556
|
+
// Blank every `{…}` body so `icon={<Plus size="sm" />}` cannot answer for the Button.
|
|
1557
|
+
let top = "";
|
|
1558
|
+
let depth = 0;
|
|
1559
|
+
let quote = "";
|
|
1560
|
+
for (let i = 0; i < openTag.length; i++) {
|
|
1561
|
+
const ch = openTag[i];
|
|
1562
|
+
if (quote) {
|
|
1563
|
+
if (ch === "\\") i++;
|
|
1564
|
+
else if (ch === quote) quote = "";
|
|
1565
|
+
top += depth ? " " : ch;
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
1569
|
+
if (ch === "{") depth++;
|
|
1570
|
+
top += depth ? (ch === "{" && depth === 1 ? "{" : " ") : ch;
|
|
1571
|
+
if (ch === "}") depth--;
|
|
1572
|
+
}
|
|
1573
|
+
const at = /\bsize\s*=\s*/.exec(top);
|
|
1574
|
+
if (!at) return "default";
|
|
1575
|
+
const rest = openTag.slice(at.index + at[0].length);
|
|
1576
|
+
const lit = /^(?:"([^"]*)"|'([^']*)'|\{\s*(?:"([^"]*)"|'([^']*)'|`([^`$]*)`)\s*\})/.exec(rest);
|
|
1577
|
+
return lit ? (lit[1] ?? lit[2] ?? lit[3] ?? lit[4] ?? lit[5]) : null;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/** `icon-sm` → `sm`, `icon` → `default`, `md` → `default` (Button maps md to the default size). */
|
|
1581
|
+
const buttonSizeFamily = (size) => {
|
|
1582
|
+
const bare = size.replace(/^icon-?/, "");
|
|
1583
|
+
return bare === "" || bare === "md" ? "default" : bare;
|
|
1584
|
+
};
|
|
1585
|
+
|
|
1586
|
+
/**
|
|
1587
|
+
* `mixed-button-size`: group every <Button> by its nearest non-transparent JSX parent and yield
|
|
1588
|
+
* one match per group whose size families differ. A tag stack over the whole file; an element in
|
|
1589
|
+
* an attribute (`actions={<>…</>}`) gets the attribute's owner as its parent, kept apart from the
|
|
1590
|
+
* owner's children. A `<` glued to an identifier outside JSX children is a TS generic.
|
|
1591
|
+
*/
|
|
1592
|
+
function* mixedButtonSizeMatches(source) {
|
|
1593
|
+
const stack = []; // { tag, id, attr }
|
|
1594
|
+
const pending = []; // opening tags whose attributes are still being scanned
|
|
1595
|
+
const groups = new Map();
|
|
1596
|
+
let nextId = 0;
|
|
1597
|
+
const settle = (upTo) => {
|
|
1598
|
+
while (pending.length && pending.at(-1).end < upTo) {
|
|
1599
|
+
const p = pending.pop();
|
|
1600
|
+
const i = stack.lastIndexOf(p.frame);
|
|
1601
|
+
if (i >= 0) stack.length = i;
|
|
1602
|
+
if (!p.selfClose) stack.push({ tag: p.frame.tag, id: nextId++, attr: false });
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
const tagRe = /<(\/?)(?:([A-Za-z][\w.]*)(?=[\s/>])|(?=>))/g;
|
|
1606
|
+
for (const m of source.matchAll(tagRe)) {
|
|
1607
|
+
settle(m.index);
|
|
1608
|
+
const [, close, tag = ""] = m;
|
|
1609
|
+
// `useState<Issue>(` — a `<` glued to an identifier, `)` or `]` is a generic, unless it sits
|
|
1610
|
+
// in JSX children, where `Total<b>` is text followed by a tag.
|
|
1611
|
+
const glued = /[\w$)\].]/.test(source[m.index - 1] ?? "");
|
|
1612
|
+
if (!close && glued && (stack.length === 0 || stack.at(-1).attr)) continue;
|
|
1613
|
+
if (close) {
|
|
1614
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
1615
|
+
if (!stack[i].attr && stack[i].tag === tag) {
|
|
1616
|
+
stack.length = i;
|
|
1617
|
+
break;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
const end = jsxOpeningEnd(source, m.index);
|
|
1623
|
+
if (end >= source.length) continue;
|
|
1624
|
+
const openTag = source.slice(m.index, end + 1);
|
|
1625
|
+
if (tag === "Button") {
|
|
1626
|
+
const parent = stack.findLast((f) => f.attr || !BUTTON_ROW_TRANSPARENT.has(f.tag));
|
|
1627
|
+
const size = buttonSizeOf(openTag);
|
|
1628
|
+
if (parent && size !== null) {
|
|
1629
|
+
(groups.get(parent.id) ?? groups.set(parent.id, []).get(parent.id)).push({
|
|
1630
|
+
size,
|
|
1631
|
+
index: m.index,
|
|
1632
|
+
openTag,
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
const frame = { tag, id: nextId++, attr: true };
|
|
1637
|
+
stack.push(frame);
|
|
1638
|
+
pending.push({ end, frame, selfClose: /\/\s*>$/.test(openTag) });
|
|
1639
|
+
}
|
|
1640
|
+
for (const group of groups.values()) {
|
|
1641
|
+
const families = new Set(group.map((b) => buttonSizeFamily(b.size)));
|
|
1642
|
+
if (group.length < 2 || families.size < 2) continue;
|
|
1643
|
+
yield {
|
|
1644
|
+
0: `sizes ${group.map((b) => b.size).join(" + ")}: ${group[0].openTag}`,
|
|
1645
|
+
index: group[0].index,
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1521
1650
|
// Structural: a <Card> (without p-0) whose first child is body content rather than a Card
|
|
1522
1651
|
// sub-component sits FLUSH (no padding). Per-line regexes can't see across lines, so this is a
|
|
1523
1652
|
// whole-file pass. The body must be wrapped in <CardContent> (titles in <CardHeader>).
|
|
@@ -43,6 +43,13 @@ export const VISUAL_RULES = [
|
|
|
43
43
|
standard: "@godxjp/ui control tier (--control-height) · Nielsen consistency heuristic",
|
|
44
44
|
fix: "Every control in one row (Flex row, PageContainer extra, footer) must share --control-height. Replace the odd one with the real primitive (Avatar/Button/Badge) instead of a hand-rolled pill; never restyle a control's height.",
|
|
45
45
|
},
|
|
46
|
+
{
|
|
47
|
+
id: "mixed-button-height",
|
|
48
|
+
severity: "error",
|
|
49
|
+
category: "layout",
|
|
50
|
+
standard: "@godxjp/ui Button size (one size per row) · Nielsen consistency heuristic",
|
|
51
|
+
fix: "Every Button in one flex row renders at one height (within 0.5px): give them all the same `size` (icon-sm pairs with sm, icon-xs with xs, icon with default). This is the runtime twin of the static `mixed-button-size` rule and catches rows assembled from variables.",
|
|
52
|
+
},
|
|
46
53
|
{
|
|
47
54
|
id: "sibling-card-gap",
|
|
48
55
|
severity: "error",
|
|
@@ -185,6 +192,16 @@ export function controlHeightMismatches(rows) {
|
|
|
185
192
|
return rows.filter((r) => new Set(r.heights).size > 1);
|
|
186
193
|
}
|
|
187
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Flex rows whose Buttons render at different heights (spread > 0.5px). Heights are NOT rounded:
|
|
197
|
+
* 28 vs 28.4 is the same size, 24 vs 28 is `xs` beside `sm`. @param {{name:string, heights:number[]}[]} rows
|
|
198
|
+
*/
|
|
199
|
+
export function mixedButtonHeights(rows, tolerance = 0.5) {
|
|
200
|
+
return rows.filter(
|
|
201
|
+
(r) => r.heights.length > 1 && Math.max(...r.heights) - Math.min(...r.heights) > tolerance,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
188
205
|
/** Adjacent card pairs closer than the minimum gap. @param {{gap:number}[]} pairs */
|
|
189
206
|
export function tightCardPairs(pairs, min = 8) {
|
|
190
207
|
return pairs.filter((p) => p.gap < min);
|
package/scripts/visual-audit.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
alertControlIssues,
|
|
14
14
|
missingCssLayers,
|
|
15
15
|
controlHeightMismatches,
|
|
16
|
+
mixedButtonHeights,
|
|
16
17
|
tightCardPairs,
|
|
17
18
|
starvedRowTexts,
|
|
18
19
|
} from "./visual-audit-rules.mjs";
|
|
@@ -261,6 +262,27 @@ function collectInPage() {
|
|
|
261
262
|
heights: controls.map((c) => Math.round(c.getBoundingClientRect().height)),
|
|
262
263
|
});
|
|
263
264
|
}
|
|
265
|
+
// Button rows — every flex row (any container, not only .ui-flex: CardFooter, a dialog footer,
|
|
266
|
+
// PageHeader actions) whose Buttons render at more than one height. A child counts when it IS a
|
|
267
|
+
// Button or directly wraps one (a Tooltip trigger span). `h-auto` opts a Button out of the
|
|
268
|
+
// control tier on purpose (a two-line list item), so it is not a row member.
|
|
269
|
+
const buttonRows = [];
|
|
270
|
+
const isRowButton = (el) => el.matches("[data-slot=button]") && !el.classList.contains("h-auto");
|
|
271
|
+
for (const row of document.querySelectorAll("body *")) {
|
|
272
|
+
const cs = getComputedStyle(row);
|
|
273
|
+
if (!cs.display.includes("flex") || !cs.flexDirection.startsWith("row") || !visible(row))
|
|
274
|
+
continue;
|
|
275
|
+
const buttons = [...row.children]
|
|
276
|
+
.filter(visible)
|
|
277
|
+
.map((c) => (isRowButton(c) ? c : c.querySelector(":scope > [data-slot=button]")))
|
|
278
|
+
.filter((b) => b && isRowButton(b) && visible(b));
|
|
279
|
+
if (buttons.length < 2) continue;
|
|
280
|
+
buttonRows.push({
|
|
281
|
+
name: (row.className || row.tagName).toString().slice(0, 40),
|
|
282
|
+
heights: buttons.map((b) => b.getBoundingClientRect().height),
|
|
283
|
+
sizes: buttons.map((b) => b.getAttribute("data-size") || "?"),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
264
286
|
// Sibling cards — the gap between consecutive cards in one parent.
|
|
265
287
|
const cardPairs = [];
|
|
266
288
|
const seen = new Set();
|
|
@@ -295,7 +317,7 @@ function collectInPage() {
|
|
|
295
317
|
});
|
|
296
318
|
}
|
|
297
319
|
}
|
|
298
|
-
return { accents, targets, text, alerts, layers, rows, cardPairs, rowTexts };
|
|
320
|
+
return { accents, targets, text, alerts, layers, rows, buttonRows, cardPairs, rowTexts };
|
|
299
321
|
}
|
|
300
322
|
/* c8 ignore stop */
|
|
301
323
|
|
|
@@ -395,6 +417,14 @@ async function audit(targets, { chromium }) {
|
|
|
395
417
|
`row "${r.name}" mixes control heights ${[...new Set(r.heights)].join("/")}px`,
|
|
396
418
|
),
|
|
397
419
|
);
|
|
420
|
+
for (const r of mixedButtonHeights(m.buttonRows || []))
|
|
421
|
+
findings.push(
|
|
422
|
+
buildFinding(
|
|
423
|
+
"mixed-button-height",
|
|
424
|
+
url,
|
|
425
|
+
`row "${r.name}" mixes Button heights ${r.heights.map((h) => +h.toFixed(1)).join("/")}px (sizes ${r.sizes.join("/")})`,
|
|
426
|
+
),
|
|
427
|
+
);
|
|
398
428
|
const tight = tightCardPairs(m.cardPairs || []);
|
|
399
429
|
if (tight.length)
|
|
400
430
|
findings.push(
|