@godxjp/ui 30.5.3 → 30.7.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 +5 -5
- package/agent/components/RecordPicker.json +6 -0
- package/agent/components/Skeleton.json +0 -4
- package/agent/components/SkeletonDetail.json +31 -0
- package/agent/components/SkeletonStat.json +30 -0
- package/agent/components-index.json +10 -0
- package/agent/components.json +67 -4
- package/agent/index.json +6 -6
- package/agent/llms.txt +6 -6
- package/agent/tokens.json +6 -0
- package/dist/components/data-entry/record-picker.d.ts +2 -1
- package/dist/components/data-entry/record-picker.js +295 -108
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +3 -1
- package/dist/i18n/messages/ja.json +3 -1
- package/dist/i18n/messages/vi.json +3 -1
- package/dist/props/components/data-entry.prop.d.ts +16 -0
- package/dist/styles/data-entry-layout.css +18 -0
- package/dist/tokens/components/data-entry.css +2 -0
- package/docs/FRAME-COVERAGE-REPORT.md +6 -2
- package/package.json +2 -2
- package/scripts/ui-audit.mjs +111 -8
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` 30.
|
|
6
|
+
**This catalog describes `@godxjp/ui` 30.7.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.
|
|
@@ -47,7 +47,7 @@ Then ask it: `search_components`, `get_component`, `get_tokens`, `get_rule`, `li
|
|
|
47
47
|
task is a task** — "build a settings page", "confirm a destructive delete", "a list page with
|
|
48
48
|
filters" — start HERE, not at the components. Then fetch `patterns/<name>.json` for complete,
|
|
49
49
|
copy-paste-ready code. A component index answers "does X exist"; it cannot answer "build Y".
|
|
50
|
-
1. `components-index.json` — 46 KB, all
|
|
50
|
+
1. `components-index.json` — 46 KB, all 175 components as name + group +
|
|
51
51
|
tagline. Read this when you already know the SHAPE you need. Each entry may carry `absorbed`:
|
|
52
52
|
names that **do not exist** and map to it — `Combobox`, `Autocomplete`, `CountrySelect` and
|
|
53
53
|
`SearchSelect` are all `Select`. If you are about to hand-roll something, search this field
|
|
@@ -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` — 2074 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
|
+
1760 `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` | 103 | 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` | 1760 | 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.
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
"name": "mode",
|
|
11
11
|
"type": "\"single\" | \"multiple\""
|
|
12
12
|
},
|
|
13
|
+
{
|
|
14
|
+
"defaultValue": "\"auto\"",
|
|
15
|
+
"description": "TWO ENTRY POINTS AT ONCE, instead of \"dropdown OR dialog\" (gh#944). `auto` keeps the original behaviour: `threshold` picks one. `inline` opens both — a typeable field with suggestions REGARDLESS of `count`, and a 「検索」 button beside it that always shows and opens the dialog with filters + pagination; the half-typed text carries into the dialog as its initial query. Reach for it when `count > threshold` is almost always true: a consumer measured customer projects at hundreds-to-thousands of records, so the dialog-only branch was the one users met most, and there they lost the ability to type a key they already knew. The threshold answers \"is the set big\"; the real question is \"does this person already know what they want\" — `inline` stops forcing a choice. NOTE: the forwarded ref lands on the <input> under `inline` and on the trigger <button> otherwise.",
|
|
16
|
+
"name": "shape",
|
|
17
|
+
"type": "\"auto\" | \"inline\""
|
|
18
|
+
},
|
|
13
19
|
{
|
|
14
20
|
"defaultValue": "10",
|
|
15
21
|
"description": "The dropdown/dialog switch. A dropdown is the right control for eight people and the wrong one for eight hundred; this is where that line is drawn, once, by a service rather than per screen.",
|
|
@@ -38,10 +38,6 @@
|
|
|
38
38
|
31
|
|
39
39
|
],
|
|
40
40
|
"storyPath": "feedback/Skeleton.stories.tsx",
|
|
41
|
-
"subParts": [
|
|
42
|
-
"SkeletonDetail",
|
|
43
|
-
"SkeletonStat"
|
|
44
|
-
],
|
|
45
41
|
"tagline": "Base pulsing skeleton block, and the namespace the shaped presets hang off (Skeleton.Avatar / .Button / .Input / .Node / .Image / .Article).",
|
|
46
42
|
"usage": [
|
|
47
43
|
"DO use Skeleton for a custom block when SkeletonRows/Table/Stat/Article do not match the final layout.",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"example": "import { SkeletonDetail } from \"@godxjp/ui/feedback\";\n\n<SkeletonDetail />",
|
|
3
|
+
"group": "feedback",
|
|
4
|
+
"importPath": "@godxjp/ui/feedback",
|
|
5
|
+
"name": "SkeletonDetail",
|
|
6
|
+
"props": [],
|
|
7
|
+
"related": [
|
|
8
|
+
"SkeletonStat — the same propless treatment for a KPI tile.",
|
|
9
|
+
"SkeletonRows — flat list lines, not label/value pairs.",
|
|
10
|
+
"SkeletonTable — tabular placeholder with a header band.",
|
|
11
|
+
"Skeleton — the bare block this preset is built from; reach for it when you need a shape this one does not draw.",
|
|
12
|
+
"Descriptions — the component this placeholder is standing in for."
|
|
13
|
+
],
|
|
14
|
+
"rules": [
|
|
15
|
+
3,
|
|
16
|
+
31
|
|
17
|
+
],
|
|
18
|
+
"storyPath": "feedback/Skeleton.stories.tsx",
|
|
19
|
+
"tagline": "Fixed-shape placeholder for a RECORD page — title, subtitle, then a bordered box of six label/value pairs. Takes no props: the shape IS the contract, so every detail screen loads identically.",
|
|
20
|
+
"usage": [
|
|
21
|
+
"DO use it while a single record's data resolves — a detail page, a drawer showing one entity, a Descriptions block.",
|
|
22
|
+
"DO leave it propless. It has no `rows` knob on purpose: a record page that loads with a different number of lines each time reads as a layout bug, and `Skeleton` is there for a bespoke shape.",
|
|
23
|
+
"DON'T use it for a list or a table — SkeletonRows draws flat lines, SkeletonTable draws a header band.",
|
|
24
|
+
"Import from `@godxjp/ui/feedback` (canonical). `@godxjp/ui/admin` re-exports it for admin pages."
|
|
25
|
+
],
|
|
26
|
+
"useCases": [
|
|
27
|
+
"Detail page body before the record arrives",
|
|
28
|
+
"Drawer or sheet opened on a row while its full entity loads",
|
|
29
|
+
"Descriptions block placeholder"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"example": "import { SkeletonStat } from \"@godxjp/ui/feedback\";\nimport { ResponsiveGrid } from \"@godxjp/ui/layout\";\n\n<ResponsiveGrid columns={{ base: 1, sm: 2, lg: 4 }} gap=\"md\">\n {Array.from({ length: 4 }).map((_, i) => (\n <SkeletonStat key={i} />\n ))}\n</ResponsiveGrid>",
|
|
3
|
+
"group": "feedback",
|
|
4
|
+
"importPath": "@godxjp/ui/feedback",
|
|
5
|
+
"name": "SkeletonStat",
|
|
6
|
+
"props": [],
|
|
7
|
+
"related": [
|
|
8
|
+
"StatCard — the loaded component this stands in for; match the grid, not just the size.",
|
|
9
|
+
"SkeletonDetail — the same propless treatment for a record page.",
|
|
10
|
+
"SkeletonRows — flat list lines.",
|
|
11
|
+
"Skeleton — the bare block this preset is built from."
|
|
12
|
+
],
|
|
13
|
+
"rules": [
|
|
14
|
+
3,
|
|
15
|
+
31
|
|
16
|
+
],
|
|
17
|
+
"storyPath": "feedback/Skeleton.stories.tsx",
|
|
18
|
+
"tagline": "Fixed-shape placeholder for ONE KPI tile — label, big value, caption. Takes no props, so a row of tiles loads at a uniform height and nothing reflows when the numbers arrive.",
|
|
19
|
+
"usage": [
|
|
20
|
+
"DO render one per StatCard while the figures load, inside the same grid the loaded tiles use.",
|
|
21
|
+
"DO leave it propless — it mirrors StatCard's three-line rhythm exactly, which is what stops the row jumping on hydration.",
|
|
22
|
+
"DON'T wrap it in a Card yourself if the loaded state uses StatCard; put it where the StatCard goes so the surface is identical.",
|
|
23
|
+
"Import from `@godxjp/ui/feedback` (canonical). `@godxjp/ui/admin` re-exports it for admin pages."
|
|
24
|
+
],
|
|
25
|
+
"useCases": [
|
|
26
|
+
"Dashboard KPI row before the metrics query resolves",
|
|
27
|
+
"A single figure on a detail page loading separately from the record",
|
|
28
|
+
"Placeholder inside a ResponsiveGrid of StatCards"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -607,6 +607,16 @@
|
|
|
607
607
|
"name": "Skeleton",
|
|
608
608
|
"tagline": "Base pulsing skeleton block, and the namespace the shaped presets hang off (Skeleton.Avatar / .Button / .Input / .Node / .Image / .Article)."
|
|
609
609
|
},
|
|
610
|
+
{
|
|
611
|
+
"group": "feedback",
|
|
612
|
+
"name": "SkeletonDetail",
|
|
613
|
+
"tagline": "Fixed-shape placeholder for a RECORD page — title, subtitle, then a bordered box of six label/value pairs. Takes no props: the shape IS the contract, so every detail screen loads identically."
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
"group": "feedback",
|
|
617
|
+
"name": "SkeletonStat",
|
|
618
|
+
"tagline": "Fixed-shape placeholder for ONE KPI tile — label, big value, caption. Takes no props, so a row of tiles loads at a uniform height and nothing reflows when the numbers arrive."
|
|
619
|
+
},
|
|
610
620
|
{
|
|
611
621
|
"group": "feedback",
|
|
612
622
|
"name": "SkeletonArticle",
|
package/agent/components.json
CHANGED
|
@@ -5996,6 +5996,12 @@
|
|
|
5996
5996
|
"name": "mode",
|
|
5997
5997
|
"type": "\"single\" | \"multiple\""
|
|
5998
5998
|
},
|
|
5999
|
+
{
|
|
6000
|
+
"defaultValue": "\"auto\"",
|
|
6001
|
+
"description": "TWO ENTRY POINTS AT ONCE, instead of \"dropdown OR dialog\" (gh#944). `auto` keeps the original behaviour: `threshold` picks one. `inline` opens both — a typeable field with suggestions REGARDLESS of `count`, and a 「検索」 button beside it that always shows and opens the dialog with filters + pagination; the half-typed text carries into the dialog as its initial query. Reach for it when `count > threshold` is almost always true: a consumer measured customer projects at hundreds-to-thousands of records, so the dialog-only branch was the one users met most, and there they lost the ability to type a key they already knew. The threshold answers \"is the set big\"; the real question is \"does this person already know what they want\" — `inline` stops forcing a choice. NOTE: the forwarded ref lands on the <input> under `inline` and on the trigger <button> otherwise.",
|
|
6002
|
+
"name": "shape",
|
|
6003
|
+
"type": "\"auto\" | \"inline\""
|
|
6004
|
+
},
|
|
5999
6005
|
{
|
|
6000
6006
|
"defaultValue": "10",
|
|
6001
6007
|
"description": "The dropdown/dialog switch. A dropdown is the right control for eight people and the wrong one for eight hundred; this is where that line is drawn, once, by a service rather than per screen.",
|
|
@@ -11474,10 +11480,6 @@
|
|
|
11474
11480
|
31
|
|
11475
11481
|
],
|
|
11476
11482
|
"storyPath": "feedback/Skeleton.stories.tsx",
|
|
11477
|
-
"subParts": [
|
|
11478
|
-
"SkeletonDetail",
|
|
11479
|
-
"SkeletonStat"
|
|
11480
|
-
],
|
|
11481
11483
|
"tagline": "Base pulsing skeleton block, and the namespace the shaped presets hang off (Skeleton.Avatar / .Button / .Input / .Node / .Image / .Article).",
|
|
11482
11484
|
"usage": [
|
|
11483
11485
|
"DO use Skeleton for a custom block when SkeletonRows/Table/Stat/Article do not match the final layout.",
|
|
@@ -11491,6 +11493,67 @@
|
|
|
11491
11493
|
"Inline metadata placeholder"
|
|
11492
11494
|
]
|
|
11493
11495
|
},
|
|
11496
|
+
{
|
|
11497
|
+
"example": "import { SkeletonDetail } from \"@godxjp/ui/feedback\";\n\n<SkeletonDetail />",
|
|
11498
|
+
"group": "feedback",
|
|
11499
|
+
"importPath": "@godxjp/ui/feedback",
|
|
11500
|
+
"name": "SkeletonDetail",
|
|
11501
|
+
"props": [],
|
|
11502
|
+
"related": [
|
|
11503
|
+
"SkeletonStat — the same propless treatment for a KPI tile.",
|
|
11504
|
+
"SkeletonRows — flat list lines, not label/value pairs.",
|
|
11505
|
+
"SkeletonTable — tabular placeholder with a header band.",
|
|
11506
|
+
"Skeleton — the bare block this preset is built from; reach for it when you need a shape this one does not draw.",
|
|
11507
|
+
"Descriptions — the component this placeholder is standing in for."
|
|
11508
|
+
],
|
|
11509
|
+
"rules": [
|
|
11510
|
+
3,
|
|
11511
|
+
31
|
|
11512
|
+
],
|
|
11513
|
+
"storyPath": "feedback/Skeleton.stories.tsx",
|
|
11514
|
+
"tagline": "Fixed-shape placeholder for a RECORD page — title, subtitle, then a bordered box of six label/value pairs. Takes no props: the shape IS the contract, so every detail screen loads identically.",
|
|
11515
|
+
"usage": [
|
|
11516
|
+
"DO use it while a single record's data resolves — a detail page, a drawer showing one entity, a Descriptions block.",
|
|
11517
|
+
"DO leave it propless. It has no `rows` knob on purpose: a record page that loads with a different number of lines each time reads as a layout bug, and `Skeleton` is there for a bespoke shape.",
|
|
11518
|
+
"DON'T use it for a list or a table — SkeletonRows draws flat lines, SkeletonTable draws a header band.",
|
|
11519
|
+
"Import from `@godxjp/ui/feedback` (canonical). `@godxjp/ui/admin` re-exports it for admin pages."
|
|
11520
|
+
],
|
|
11521
|
+
"useCases": [
|
|
11522
|
+
"Detail page body before the record arrives",
|
|
11523
|
+
"Drawer or sheet opened on a row while its full entity loads",
|
|
11524
|
+
"Descriptions block placeholder"
|
|
11525
|
+
]
|
|
11526
|
+
},
|
|
11527
|
+
{
|
|
11528
|
+
"example": "import { SkeletonStat } from \"@godxjp/ui/feedback\";\nimport { ResponsiveGrid } from \"@godxjp/ui/layout\";\n\n<ResponsiveGrid columns={{ base: 1, sm: 2, lg: 4 }} gap=\"md\">\n {Array.from({ length: 4 }).map((_, i) => (\n <SkeletonStat key={i} />\n ))}\n</ResponsiveGrid>",
|
|
11529
|
+
"group": "feedback",
|
|
11530
|
+
"importPath": "@godxjp/ui/feedback",
|
|
11531
|
+
"name": "SkeletonStat",
|
|
11532
|
+
"props": [],
|
|
11533
|
+
"related": [
|
|
11534
|
+
"StatCard — the loaded component this stands in for; match the grid, not just the size.",
|
|
11535
|
+
"SkeletonDetail — the same propless treatment for a record page.",
|
|
11536
|
+
"SkeletonRows — flat list lines.",
|
|
11537
|
+
"Skeleton — the bare block this preset is built from."
|
|
11538
|
+
],
|
|
11539
|
+
"rules": [
|
|
11540
|
+
3,
|
|
11541
|
+
31
|
|
11542
|
+
],
|
|
11543
|
+
"storyPath": "feedback/Skeleton.stories.tsx",
|
|
11544
|
+
"tagline": "Fixed-shape placeholder for ONE KPI tile — label, big value, caption. Takes no props, so a row of tiles loads at a uniform height and nothing reflows when the numbers arrive.",
|
|
11545
|
+
"usage": [
|
|
11546
|
+
"DO render one per StatCard while the figures load, inside the same grid the loaded tiles use.",
|
|
11547
|
+
"DO leave it propless — it mirrors StatCard's three-line rhythm exactly, which is what stops the row jumping on hydration.",
|
|
11548
|
+
"DON'T wrap it in a Card yourself if the loaded state uses StatCard; put it where the StatCard goes so the surface is identical.",
|
|
11549
|
+
"Import from `@godxjp/ui/feedback` (canonical). `@godxjp/ui/admin` re-exports it for admin pages."
|
|
11550
|
+
],
|
|
11551
|
+
"useCases": [
|
|
11552
|
+
"Dashboard KPI row before the metrics query resolves",
|
|
11553
|
+
"A single figure on a detail page loading separately from the record",
|
|
11554
|
+
"Placeholder inside a ResponsiveGrid of StatCards"
|
|
11555
|
+
]
|
|
11556
|
+
},
|
|
11494
11557
|
{
|
|
11495
11558
|
"example": "import { SkeletonArticle } from \"@godxjp/ui/feedback\";\n\n<SkeletonArticle avatar active paragraph={{ rows: 3 }} />",
|
|
11496
11559
|
"group": "feedback",
|
package/agent/index.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"counts": {
|
|
3
3
|
"anti-ai-tells": 26,
|
|
4
|
-
"components":
|
|
4
|
+
"components": 175,
|
|
5
5
|
"patterns": 21,
|
|
6
6
|
"rules": 50,
|
|
7
|
-
"tokens":
|
|
7
|
+
"tokens": 2074,
|
|
8
8
|
"vocabulary": 14
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
{
|
|
12
12
|
"file": "components-index.json",
|
|
13
|
-
"note": "46 KB — name + group + tagline for all
|
|
13
|
+
"note": "46 KB — name + group + tagline for all 175. FETCH THIS FIRST, then fetch only the components you chose.",
|
|
14
14
|
"url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json"
|
|
15
15
|
},
|
|
16
16
|
{
|
|
@@ -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/v30.
|
|
51
|
+
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v30.7.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": 1760,
|
|
59
59
|
"foundation": 211,
|
|
60
60
|
"semantic": 103
|
|
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": "30.
|
|
65
|
+
"version": "30.7.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:
|
|
4
|
-
> 50 cardinal rules. This file is the entry point for AI agents. Catalog version 30.
|
|
3
|
+
> A Japanese-enterprise React design system: 175 components, 2074 design tokens,
|
|
4
|
+
> 50 cardinal rules. This file is the entry point for AI agents. Catalog version 30.7.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@30.
|
|
7
|
+
(`npx @godxjp/ui-mcp@30.7.0`). It is searchable and version-locked. These files exist for agents
|
|
8
8
|
that can only fetch URLs.
|
|
9
9
|
|
|
10
10
|
## Start
|
|
@@ -15,10 +15,10 @@ that can only fetch URLs.
|
|
|
15
15
|
## Catalog
|
|
16
16
|
|
|
17
17
|
- [patterns-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/patterns-index.json): 21 whole-task patterns (name, tagline, tags). Start here when the task is a TASK — "build a settings page" — then fetch `patterns/<name>.json` for complete code.
|
|
18
|
-
- [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 46 KB — all
|
|
18
|
+
- [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 46 KB — all 175 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–34 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), 103 `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), 103 `semantic` roles, 1760 `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/v30.
|
|
29
|
+
the tag: `.../godx-jp/godxjp-ui/v30.7.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
|
@@ -5891,6 +5891,12 @@
|
|
|
5891
5891
|
"tier": "component",
|
|
5892
5892
|
"value": "22rem"
|
|
5893
5893
|
},
|
|
5894
|
+
{
|
|
5895
|
+
"description": "Danh sách gợi ý dưới ô inline (gh#944) — ngắn hơn danh sách trong Dialog, vì nó nằm TRONG dòng chảy của form chứ không phải trong một lớp phủ riêng.",
|
|
5896
|
+
"name": "--record-picker-suggest-max-block-size",
|
|
5897
|
+
"tier": "component",
|
|
5898
|
+
"value": "14rem"
|
|
5899
|
+
},
|
|
5894
5900
|
{
|
|
5895
5901
|
"description": "Data-entry component tokens — small-by-design text knobs (rule #45/#46).",
|
|
5896
5902
|
"name": "--password-strength-score-font-size",
|
|
@@ -35,6 +35,7 @@ export type { RecordPickerProp, RecordPickerProp as RecordPickerProps };
|
|
|
35
35
|
*/
|
|
36
36
|
export declare const RecordPicker: React.ForwardRefExoticComponent<Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "defaultValue" | "onChange" | "value"> & {
|
|
37
37
|
mode?: "single" | "multiple";
|
|
38
|
+
shape?: "auto" | "inline";
|
|
38
39
|
value?: string | string[] | null;
|
|
39
40
|
defaultValue?: string | string[] | null;
|
|
40
41
|
onValueChange?: (value: string | string[] | null) => void;
|
|
@@ -60,4 +61,4 @@ export declare const RecordPicker: React.ForwardRefExoticComponent<Omit<React.Bu
|
|
|
60
61
|
dialogTitle?: string;
|
|
61
62
|
size?: "xs" | "sm" | "md" | "lg";
|
|
62
63
|
"data-field"?: string;
|
|
63
|
-
} & React.RefAttributes<HTMLButtonElement>>;
|
|
64
|
+
} & React.RefAttributes<HTMLButtonElement | HTMLInputElement>>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Check, ChevronDown, X } from "lucide-react";
|
|
3
|
+
import { Check, ChevronDown, Search, X } from "lucide-react";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
6
6
|
import { cn } from "../../lib/utils.js";
|
|
@@ -24,6 +24,7 @@ import { Select } from "./select.js";
|
|
|
24
24
|
const RecordPicker = React.forwardRef(
|
|
25
25
|
function RecordPicker2({
|
|
26
26
|
mode = "single",
|
|
27
|
+
shape = "auto",
|
|
27
28
|
value,
|
|
28
29
|
defaultValue,
|
|
29
30
|
onValueChange,
|
|
@@ -71,6 +72,29 @@ const RecordPicker = React.forwardRef(
|
|
|
71
72
|
() => options ? normalizeSelectOptions(options) : [],
|
|
72
73
|
[options]
|
|
73
74
|
);
|
|
75
|
+
if (shape === "inline") {
|
|
76
|
+
return /* @__PURE__ */ jsx(
|
|
77
|
+
InlinePicker,
|
|
78
|
+
{
|
|
79
|
+
ref,
|
|
80
|
+
isMultiple,
|
|
81
|
+
selected,
|
|
82
|
+
commit,
|
|
83
|
+
staticOptions,
|
|
84
|
+
loadOptions,
|
|
85
|
+
filters,
|
|
86
|
+
selectedOptions,
|
|
87
|
+
emptyOption,
|
|
88
|
+
placeholder,
|
|
89
|
+
dialogTitle,
|
|
90
|
+
disabled,
|
|
91
|
+
size,
|
|
92
|
+
className,
|
|
93
|
+
"data-field": fieldName,
|
|
94
|
+
...props
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
}
|
|
74
98
|
const total = count ?? (options ? staticOptions.length : void 0);
|
|
75
99
|
const asDialog = total === void 0 ? Boolean(loadOptions) : total > threshold;
|
|
76
100
|
if (!asDialog) {
|
|
@@ -117,6 +141,7 @@ const RecordPicker = React.forwardRef(
|
|
|
117
141
|
);
|
|
118
142
|
}
|
|
119
143
|
);
|
|
144
|
+
const INLINE_SUGGESTION_LIMIT = 8;
|
|
120
145
|
function toArray(v) {
|
|
121
146
|
if (v === null || v === void 0) return [];
|
|
122
147
|
return Array.isArray(v) ? v : [v];
|
|
@@ -125,6 +150,155 @@ function withEmptyOption(options, emptyOption) {
|
|
|
125
150
|
if (!emptyOption) return options;
|
|
126
151
|
return [{ value: emptyOption.value, label: emptyOption.label }, ...options ?? []];
|
|
127
152
|
}
|
|
153
|
+
const InlinePicker = React.forwardRef(function InlinePicker2({
|
|
154
|
+
isMultiple,
|
|
155
|
+
selected,
|
|
156
|
+
commit,
|
|
157
|
+
staticOptions,
|
|
158
|
+
loadOptions,
|
|
159
|
+
filters,
|
|
160
|
+
selectedOptions,
|
|
161
|
+
emptyOption,
|
|
162
|
+
placeholder,
|
|
163
|
+
dialogTitle,
|
|
164
|
+
disabled,
|
|
165
|
+
size,
|
|
166
|
+
className,
|
|
167
|
+
id,
|
|
168
|
+
"data-field": fieldName,
|
|
169
|
+
"aria-describedby": describedBy
|
|
170
|
+
}, ref) {
|
|
171
|
+
const { t } = useTranslation();
|
|
172
|
+
const [query, setQuery] = React.useState("");
|
|
173
|
+
const [rows, setRows] = React.useState(staticOptions);
|
|
174
|
+
const [openSuggest, setOpenSuggest] = React.useState(false);
|
|
175
|
+
const [dialogQuery, setDialogQuery] = React.useState(null);
|
|
176
|
+
const labelCache = React.useRef(/* @__PURE__ */ new Map());
|
|
177
|
+
for (const o of [...selectedOptions ?? [], ...staticOptions, ...rows]) {
|
|
178
|
+
labelCache.current.set(o.value, o);
|
|
179
|
+
}
|
|
180
|
+
React.useEffect(() => {
|
|
181
|
+
if (!loadOptions || query.trim() === "") return;
|
|
182
|
+
const id2 = setTimeout(() => {
|
|
183
|
+
void loadOptions({ query, filters: {} }).then((r) => setRows(r.options)).catch(() => void 0);
|
|
184
|
+
}, 250);
|
|
185
|
+
return () => clearTimeout(id2);
|
|
186
|
+
}, [query, loadOptions]);
|
|
187
|
+
const suggestions = React.useMemo(() => {
|
|
188
|
+
const q = query.trim().toLowerCase();
|
|
189
|
+
const base = loadOptions ? rows : staticOptions;
|
|
190
|
+
return (q ? base.filter((o) => (o.label + (o.sublabel ?? "")).toLowerCase().includes(q)) : base).slice(0, INLINE_SUGGESTION_LIMIT);
|
|
191
|
+
}, [query, rows, staticOptions, loadOptions]);
|
|
192
|
+
const chosen = selected.map((v) => labelCache.current.get(v) ?? { value: v, label: v });
|
|
193
|
+
const pick = (v) => {
|
|
194
|
+
commit(isMultiple ? [...selected, v] : [v]);
|
|
195
|
+
setQuery("");
|
|
196
|
+
setOpenSuggest(false);
|
|
197
|
+
};
|
|
198
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
199
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "col", gap: 1, className: cn("ui-record-picker-inline", className), children: [
|
|
200
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "row", gap: "xs", align: "center", children: [
|
|
201
|
+
/* @__PURE__ */ jsx(
|
|
202
|
+
Input,
|
|
203
|
+
{
|
|
204
|
+
ref,
|
|
205
|
+
type: "search",
|
|
206
|
+
value: query,
|
|
207
|
+
disabled,
|
|
208
|
+
placeholder: placeholder ?? t("dataEntry.recordPicker.placeholder"),
|
|
209
|
+
onChange: (e) => {
|
|
210
|
+
setQuery(e.target.value);
|
|
211
|
+
setOpenSuggest(true);
|
|
212
|
+
},
|
|
213
|
+
onFocus: () => setOpenSuggest(true),
|
|
214
|
+
className: "ui-record-picker-inline-input",
|
|
215
|
+
id,
|
|
216
|
+
"data-field": fieldName,
|
|
217
|
+
"aria-describedby": describedBy
|
|
218
|
+
}
|
|
219
|
+
),
|
|
220
|
+
/* @__PURE__ */ jsxs(
|
|
221
|
+
Button,
|
|
222
|
+
{
|
|
223
|
+
type: "button",
|
|
224
|
+
variant: "outline",
|
|
225
|
+
size: size === "lg" ? "lg" : "default",
|
|
226
|
+
disabled,
|
|
227
|
+
"aria-haspopup": "dialog",
|
|
228
|
+
onClick: () => setDialogQuery(query),
|
|
229
|
+
children: [
|
|
230
|
+
/* @__PURE__ */ jsx(Search, { "aria-hidden": "true" }),
|
|
231
|
+
t("dataEntry.recordPicker.openSearch")
|
|
232
|
+
]
|
|
233
|
+
}
|
|
234
|
+
),
|
|
235
|
+
chosen.length > 0 && !disabled ? /* @__PURE__ */ jsx(
|
|
236
|
+
Button,
|
|
237
|
+
{
|
|
238
|
+
variant: "ghost",
|
|
239
|
+
size: "icon-sm",
|
|
240
|
+
"aria-label": t("dataEntry.recordPicker.clear"),
|
|
241
|
+
onClick: () => commit([]),
|
|
242
|
+
children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
|
|
243
|
+
}
|
|
244
|
+
) : null
|
|
245
|
+
] }),
|
|
246
|
+
chosen.length > 0 ? /* @__PURE__ */ jsx(Flex, { direction: "row", gap: "xs", wrap: true, align: "center", children: chosen.map((c) => /* @__PURE__ */ jsxs(Badge, { variant: "secondary", as: "span", children: [
|
|
247
|
+
c.icon,
|
|
248
|
+
c.label,
|
|
249
|
+
c.sublabel ? ` ${c.sublabel}` : ""
|
|
250
|
+
] }, c.value)) }) : null,
|
|
251
|
+
openSuggest && suggestions.length > 0 ? /* @__PURE__ */ jsx(
|
|
252
|
+
Command,
|
|
253
|
+
{
|
|
254
|
+
shouldFilter: false,
|
|
255
|
+
className: "ui-record-picker-suggest",
|
|
256
|
+
"aria-label": t("dataEntry.recordPicker.suggestions"),
|
|
257
|
+
children: suggestions.map((o) => /* @__PURE__ */ jsxs(
|
|
258
|
+
"button",
|
|
259
|
+
{
|
|
260
|
+
type: "button",
|
|
261
|
+
role: "option",
|
|
262
|
+
"aria-selected": selected.includes(o.value),
|
|
263
|
+
"data-picked": selected.includes(o.value) ? "" : void 0,
|
|
264
|
+
className: "ui-record-picker-option",
|
|
265
|
+
onClick: () => pick(o.value),
|
|
266
|
+
children: [
|
|
267
|
+
/* @__PURE__ */ jsx("span", { className: "ui-record-picker-tick", "aria-hidden": "true", children: selected.includes(o.value) ? /* @__PURE__ */ jsx(Check, {}) : null }),
|
|
268
|
+
/* @__PURE__ */ jsxs("span", { className: "ui-record-picker-option-label", children: [
|
|
269
|
+
o.label,
|
|
270
|
+
o.sublabel ? /* @__PURE__ */ jsx(Text, { as: "span", size: "2xs", tone: "muted", children: o.sublabel }) : null
|
|
271
|
+
] })
|
|
272
|
+
]
|
|
273
|
+
},
|
|
274
|
+
o.value
|
|
275
|
+
))
|
|
276
|
+
}
|
|
277
|
+
) : null
|
|
278
|
+
] }),
|
|
279
|
+
dialogQuery !== null ? /* @__PURE__ */ jsx(
|
|
280
|
+
DialogPicker,
|
|
281
|
+
{
|
|
282
|
+
isMultiple,
|
|
283
|
+
selected,
|
|
284
|
+
commit: (next) => {
|
|
285
|
+
commit(next);
|
|
286
|
+
setDialogQuery(null);
|
|
287
|
+
setQuery("");
|
|
288
|
+
},
|
|
289
|
+
staticOptions,
|
|
290
|
+
loadOptions,
|
|
291
|
+
filters,
|
|
292
|
+
selectedOptions,
|
|
293
|
+
emptyOption,
|
|
294
|
+
dialogTitle,
|
|
295
|
+
initialQuery: dialogQuery,
|
|
296
|
+
autoOpen: true,
|
|
297
|
+
onDismiss: () => setDialogQuery(null)
|
|
298
|
+
}
|
|
299
|
+
) : null
|
|
300
|
+
] });
|
|
301
|
+
});
|
|
128
302
|
const DialogPicker = React.forwardRef(function DialogPicker2({
|
|
129
303
|
isMultiple,
|
|
130
304
|
selected,
|
|
@@ -139,11 +313,14 @@ const DialogPicker = React.forwardRef(function DialogPicker2({
|
|
|
139
313
|
disabled,
|
|
140
314
|
size,
|
|
141
315
|
className,
|
|
316
|
+
initialQuery,
|
|
317
|
+
autoOpen = false,
|
|
318
|
+
onDismiss,
|
|
142
319
|
...props
|
|
143
320
|
}, ref) {
|
|
144
321
|
const { t } = useTranslation();
|
|
145
|
-
const [open, setOpen] = React.useState(
|
|
146
|
-
const [query, setQuery] = React.useState("");
|
|
322
|
+
const [open, setOpen] = React.useState(autoOpen);
|
|
323
|
+
const [query, setQuery] = React.useState(initialQuery ?? "");
|
|
147
324
|
const [filterValues, setFilterValues] = React.useState({});
|
|
148
325
|
const [rows, setRows] = React.useState(staticOptions);
|
|
149
326
|
const [status, setStatus] = React.useState("idle");
|
|
@@ -207,7 +384,7 @@ const DialogPicker = React.forwardRef(function DialogPicker2({
|
|
|
207
384
|
};
|
|
208
385
|
const chips = selected.map((v) => labels.get(v) ?? { value: v, label: v });
|
|
209
386
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
210
|
-
/* @__PURE__ */ jsxs(Flex, { direction: "row", gap: "xs", align: "center", className: "ui-record-picker-row", children: [
|
|
387
|
+
autoOpen ? null : /* @__PURE__ */ jsxs(Flex, { direction: "row", gap: "xs", align: "center", className: "ui-record-picker-row", children: [
|
|
211
388
|
/* @__PURE__ */ jsxs(
|
|
212
389
|
Button,
|
|
213
390
|
{
|
|
@@ -240,118 +417,128 @@ const DialogPicker = React.forwardRef(function DialogPicker2({
|
|
|
240
417
|
}
|
|
241
418
|
) : null
|
|
242
419
|
] }),
|
|
243
|
-
/* @__PURE__ */ jsx(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
),
|
|
257
|
-
filters?.length ? /* @__PURE__ */ jsx(
|
|
258
|
-
Flex,
|
|
259
|
-
{
|
|
260
|
-
direction: "row",
|
|
261
|
-
gap: "sm",
|
|
262
|
-
wrap: true,
|
|
263
|
-
role: "group",
|
|
264
|
-
"aria-label": t("dataEntry.recordPicker.filters"),
|
|
265
|
-
children: filters.map((f) => /* @__PURE__ */ jsx(
|
|
266
|
-
Select,
|
|
420
|
+
/* @__PURE__ */ jsx(
|
|
421
|
+
Dialog,
|
|
422
|
+
{
|
|
423
|
+
open,
|
|
424
|
+
onOpenChange: (next) => {
|
|
425
|
+
setOpen(next);
|
|
426
|
+
if (!next) onDismiss?.();
|
|
427
|
+
},
|
|
428
|
+
children: /* @__PURE__ */ jsxs(DialogContent, { className: "ui-record-picker-dialog", children: [
|
|
429
|
+
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: dialogTitle ?? t("dataEntry.recordPicker.dialogTitle") }) }),
|
|
430
|
+
/* @__PURE__ */ jsx(DialogBody, { children: /* @__PURE__ */ jsxs(Flex, { direction: "col", gap: "md", children: [
|
|
431
|
+
/* @__PURE__ */ jsx(
|
|
432
|
+
Input,
|
|
267
433
|
{
|
|
268
|
-
|
|
269
|
-
"
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
434
|
+
autoFocus: true,
|
|
435
|
+
type: "search",
|
|
436
|
+
value: query,
|
|
437
|
+
onChange: (e) => setQuery(e.target.value),
|
|
438
|
+
"aria-label": t("dataEntry.recordPicker.search"),
|
|
439
|
+
placeholder: t("dataEntry.recordPicker.searchPlaceholder")
|
|
440
|
+
}
|
|
441
|
+
),
|
|
442
|
+
filters?.length ? /* @__PURE__ */ jsx(
|
|
443
|
+
Flex,
|
|
444
|
+
{
|
|
445
|
+
direction: "row",
|
|
446
|
+
gap: "sm",
|
|
447
|
+
wrap: true,
|
|
448
|
+
role: "group",
|
|
449
|
+
"aria-label": t("dataEntry.recordPicker.filters"),
|
|
450
|
+
children: filters.map((f) => /* @__PURE__ */ jsx(
|
|
451
|
+
Select,
|
|
452
|
+
{
|
|
453
|
+
size: "sm",
|
|
454
|
+
"aria-label": f.label,
|
|
455
|
+
placeholder: f.label,
|
|
456
|
+
value: filterValues[f.name] ?? "",
|
|
457
|
+
onValueChange: (next) => setFilterValues((prev) => ({ ...prev, [f.name]: next })),
|
|
458
|
+
options: [
|
|
459
|
+
{ value: "", label: t("dataEntry.recordPicker.allFilter") },
|
|
460
|
+
...f.options
|
|
461
|
+
]
|
|
462
|
+
},
|
|
463
|
+
f.name
|
|
464
|
+
))
|
|
465
|
+
}
|
|
466
|
+
) : null,
|
|
467
|
+
/* @__PURE__ */ jsx(Command, { shouldFilter: false, split: isMultiple, className: "ui-record-picker-list", children: status === "error" ? /* @__PURE__ */ jsx(
|
|
468
|
+
EmptyState,
|
|
469
|
+
{
|
|
470
|
+
variant: "compact",
|
|
471
|
+
tone: "destructive",
|
|
472
|
+
title: t("dataEntry.recordPicker.error"),
|
|
473
|
+
action: /* @__PURE__ */ jsx(
|
|
474
|
+
Button,
|
|
475
|
+
{
|
|
476
|
+
variant: "outline",
|
|
477
|
+
size: "sm",
|
|
478
|
+
onClick: () => void load(query, filterValues),
|
|
479
|
+
children: t("dataEntry.recordPicker.more")
|
|
480
|
+
}
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
) : status === "loading" ? /* @__PURE__ */ jsx("div", { role: "status", className: "ui-record-picker-status", children: t("dataEntry.recordPicker.loading") }) : visible.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { variant: "compact", title: t("dataEntry.recordPicker.empty") }) : groups.map(([heading, items]) => {
|
|
484
|
+
const rendered = items.map((o) => {
|
|
485
|
+
const picked = (isMultiple ? draft : selected).includes(o.value);
|
|
486
|
+
return /* @__PURE__ */ jsxs(
|
|
487
|
+
"button",
|
|
488
|
+
{
|
|
489
|
+
type: "button",
|
|
490
|
+
role: "option",
|
|
491
|
+
"aria-selected": picked,
|
|
492
|
+
"data-picked": picked ? "" : void 0,
|
|
493
|
+
disabled: o.disabled,
|
|
494
|
+
className: "ui-record-picker-option",
|
|
495
|
+
onClick: () => toggle(o.value),
|
|
496
|
+
children: [
|
|
497
|
+
/* @__PURE__ */ jsx("span", { className: "ui-record-picker-tick", "aria-hidden": "true", children: picked ? /* @__PURE__ */ jsx(Check, {}) : null }),
|
|
498
|
+
o.icon,
|
|
499
|
+
/* @__PURE__ */ jsxs("span", { className: "ui-record-picker-option-label", children: [
|
|
500
|
+
o.label,
|
|
501
|
+
o.sublabel ? /* @__PURE__ */ jsx(Text, { as: "span", size: "2xs", tone: "muted", children: o.sublabel }) : null
|
|
502
|
+
] })
|
|
503
|
+
]
|
|
504
|
+
},
|
|
505
|
+
o.value
|
|
506
|
+
);
|
|
507
|
+
});
|
|
508
|
+
return heading ? /* @__PURE__ */ jsx(CommandGroup, { heading, children: rendered }, heading) : /* @__PURE__ */ jsx(React.Fragment, { children: rendered }, "__ungrouped");
|
|
509
|
+
}) }),
|
|
510
|
+
cursor !== void 0 && status !== "loading" ? /* @__PURE__ */ jsx(
|
|
289
511
|
Button,
|
|
290
512
|
{
|
|
291
513
|
variant: "outline",
|
|
292
514
|
size: "sm",
|
|
293
|
-
onClick: () => void load(query, filterValues),
|
|
515
|
+
onClick: () => void load(query, filterValues, cursor),
|
|
294
516
|
children: t("dataEntry.recordPicker.more")
|
|
295
517
|
}
|
|
296
|
-
)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
"
|
|
518
|
+
) : null
|
|
519
|
+
] }) }),
|
|
520
|
+
isMultiple ? /* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
521
|
+
/* @__PURE__ */ jsx(Text, { as: "span", size: "xs", tone: "muted", className: "me-auto", children: t("dataEntry.recordPicker.selected", { count: draft.length }) }),
|
|
522
|
+
/* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setDraft([]), children: [
|
|
523
|
+
/* @__PURE__ */ jsx(X, { "aria-hidden": "true" }),
|
|
524
|
+
t("dataEntry.recordPicker.clear")
|
|
525
|
+
] }),
|
|
526
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => setOpen(false), children: t("dataEntry.recordPicker.cancel") }),
|
|
527
|
+
/* @__PURE__ */ jsx(
|
|
528
|
+
Button,
|
|
303
529
|
{
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
o.sublabel ? /* @__PURE__ */ jsx(Text, { as: "span", size: "2xs", tone: "muted", children: o.sublabel }) : null
|
|
317
|
-
] })
|
|
318
|
-
]
|
|
319
|
-
},
|
|
320
|
-
o.value
|
|
321
|
-
);
|
|
322
|
-
});
|
|
323
|
-
return heading ? /* @__PURE__ */ jsx(CommandGroup, { heading, children: rendered }, heading) : /* @__PURE__ */ jsx(React.Fragment, { children: rendered }, "__ungrouped");
|
|
324
|
-
}) }),
|
|
325
|
-
cursor !== void 0 && status !== "loading" ? /* @__PURE__ */ jsx(
|
|
326
|
-
Button,
|
|
327
|
-
{
|
|
328
|
-
variant: "outline",
|
|
329
|
-
size: "sm",
|
|
330
|
-
onClick: () => void load(query, filterValues, cursor),
|
|
331
|
-
children: t("dataEntry.recordPicker.more")
|
|
332
|
-
}
|
|
333
|
-
) : null
|
|
334
|
-
] }) }),
|
|
335
|
-
isMultiple ? /* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
336
|
-
/* @__PURE__ */ jsx(Text, { as: "span", size: "xs", tone: "muted", className: "me-auto", children: t("dataEntry.recordPicker.selected", { count: draft.length }) }),
|
|
337
|
-
/* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setDraft([]), children: [
|
|
338
|
-
/* @__PURE__ */ jsx(X, { "aria-hidden": "true" }),
|
|
339
|
-
t("dataEntry.recordPicker.clear")
|
|
340
|
-
] }),
|
|
341
|
-
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => setOpen(false), children: t("dataEntry.recordPicker.cancel") }),
|
|
342
|
-
/* @__PURE__ */ jsx(
|
|
343
|
-
Button,
|
|
344
|
-
{
|
|
345
|
-
size: "sm",
|
|
346
|
-
onClick: () => {
|
|
347
|
-
commit(draft);
|
|
348
|
-
setOpen(false);
|
|
349
|
-
},
|
|
350
|
-
children: t("dataEntry.recordPicker.confirm")
|
|
351
|
-
}
|
|
352
|
-
)
|
|
353
|
-
] }) : null
|
|
354
|
-
] }) })
|
|
530
|
+
size: "sm",
|
|
531
|
+
onClick: () => {
|
|
532
|
+
commit(draft);
|
|
533
|
+
setOpen(false);
|
|
534
|
+
},
|
|
535
|
+
children: t("dataEntry.recordPicker.confirm")
|
|
536
|
+
}
|
|
537
|
+
)
|
|
538
|
+
] }) : null
|
|
539
|
+
] })
|
|
540
|
+
}
|
|
541
|
+
)
|
|
355
542
|
] });
|
|
356
543
|
});
|
|
357
544
|
export {
|
|
@@ -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": "30.
|
|
3
|
+
"version": "30.7.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,
|
|
@@ -1148,6 +1148,22 @@ export type RecordPickerFilterProp = {
|
|
|
1148
1148
|
export type RecordPickerProp = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "value" | "defaultValue" | "onChange"> & {
|
|
1149
1149
|
/** `single` (default) or `multiple`. Decides the value shape AND whether the dialog confirms. */
|
|
1150
1150
|
mode?: "single" | "multiple";
|
|
1151
|
+
/**
|
|
1152
|
+
* HAI LỐI VÀO CÙNG LÚC, thay vì "dropdown HOẶC Dialog" (gh#944).
|
|
1153
|
+
*
|
|
1154
|
+
* `auto` (mặc định) giữ nguyên hành vi cũ: `threshold` quyết một trong hai.
|
|
1155
|
+
*
|
|
1156
|
+
* `inline` mở cả hai cùng lúc — ô gõ được có gợi ý ngay bên dưới BẤT KỂ `count`, và một nút
|
|
1157
|
+
* 「検索」 luôn hiện bên cạnh mở Dialog có filter + phân trang. Chữ đang gõ dở mang sang Dialog
|
|
1158
|
+
* làm từ khoá ban đầu.
|
|
1159
|
+
*
|
|
1160
|
+
* Vì sao nó tồn tại: consumer đo được dự án khách có vài trăm tới vài nghìn bản ghi, nên
|
|
1161
|
+
* `count > threshold` gần như LUÔN đúng — tức nhánh "chỉ Dialog" là nhánh người dùng gặp
|
|
1162
|
+
* thường xuyên nhất, và ở đó họ mất hẳn khả năng gõ. Ngưỡng trả lời đúng câu hỏi "tập lớn hay
|
|
1163
|
+
* nhỏ" nhưng câu hỏi thật là "người này đã biết mình tìm gì chưa": biết rồi thì gõ nhanh hơn
|
|
1164
|
+
* mở modal, chưa biết thì cần filter. `inline` không bắt chọn.
|
|
1165
|
+
*/
|
|
1166
|
+
shape?: "auto" | "inline";
|
|
1151
1167
|
value?: string | string[] | null;
|
|
1152
1168
|
defaultValue?: string | string[] | null;
|
|
1153
1169
|
/** Receives the shape you passed in: an array for `multiple`, a single value (or null) else. */
|
|
@@ -846,3 +846,21 @@
|
|
|
846
846
|
min-inline-size: 0;
|
|
847
847
|
}
|
|
848
848
|
}
|
|
849
|
+
|
|
850
|
+
@layer components {
|
|
851
|
+
|
|
852
|
+
.ui-record-picker-inline {
|
|
853
|
+
inline-size: 100%;
|
|
854
|
+
}
|
|
855
|
+
.ui-record-picker-inline-input {
|
|
856
|
+
flex: 1 1 auto;
|
|
857
|
+
min-inline-size: 0;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.ui-record-picker-suggest {
|
|
861
|
+
max-block-size: var(--record-picker-suggest-max-block-size);
|
|
862
|
+
overflow-y: auto;
|
|
863
|
+
border: var(--stroke-hairline) solid hsl(var(--border));
|
|
864
|
+
border-radius: var(--radius-md);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
> Generated by `scripts/frame-coverage.mjs` (issue #163). Do not edit by hand — run `pnpm check:frame-coverage`.
|
|
4
4
|
> Standard: [FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md). A missing dimension is **UNTESTED**, never a pass.
|
|
5
5
|
|
|
6
|
-
- Public components: **
|
|
7
|
-
- With ≥1 frame: **152** · zero-frame: **
|
|
6
|
+
- Public components: **175**
|
|
7
|
+
- With ≥1 frame: **152** · zero-frame: **23**
|
|
8
8
|
- Fully covered (every contract axis declared): **0**
|
|
9
9
|
|
|
10
10
|
## UNTESTED — no `/frame/**` route
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
- [ ] `ServiceCatalogCta`
|
|
23
23
|
- [ ] `SkeletonRows`
|
|
24
24
|
- [ ] `OverlayPortalProvider`
|
|
25
|
+
- [ ] `SkeletonDetail`
|
|
26
|
+
- [ ] `SkeletonStat`
|
|
25
27
|
- [ ] `SkeletonArticle`
|
|
26
28
|
- [ ] `SkeletonAvatar`
|
|
27
29
|
- [ ] `SkeletonButton`
|
|
@@ -154,6 +156,8 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
|
|
|
154
156
|
| Avatar | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
|
|
155
157
|
| Separator | ✓ | · | N/A | N/A | · | · | · | · | · | · |
|
|
156
158
|
| Skeleton | ✓ | N/A | · | N/A | · | · | · | · | · | · |
|
|
159
|
+
| SkeletonDetail | **✗** | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
160
|
+
| SkeletonStat | **✗** | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
157
161
|
| SkeletonArticle | **✗** | N/A | · | N/A | · | · | · | · | · | · |
|
|
158
162
|
| SkeletonAvatar | **✗** | ✓ | N/A | N/A | · | · | · | · | · | · |
|
|
159
163
|
| SkeletonButton | **✗** | ✓ | N/A | N/A | · | · | · | · | · | · |
|
package/package.json
CHANGED
package/scripts/ui-audit.mjs
CHANGED
|
@@ -40,17 +40,53 @@ function changedFiles() {
|
|
|
40
40
|
return r.status === 0 ? (r.stdout ?? "") : null;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
/*
|
|
44
|
+
* `origin/main` WAS HARD-CODED, AND MOST REPOS THAT RUN THIS DO NOT DEFAULT TO main (gh#948).
|
|
45
|
+
*
|
|
46
|
+
* In a repo whose default branch is `dev`, `origin/main` usually still EXISTS — stale, or
|
|
47
|
+
* forked long ago — so `merge-base` resolves happily to an ancient commit and the "what this
|
|
48
|
+
* branch changed" set quietly becomes "everything since that commit". Measured in
|
|
49
|
+
* godx-corebooks: a fresh branch off `origin/dev` with ZERO edits audited ~200 files and
|
|
50
|
+
* reported 1114 errors. Nothing was wrong with the branch; the question was.
|
|
51
|
+
*
|
|
52
|
+
* That is the worst shape a scope bug can take — not a crash, a plausible number. It blocked
|
|
53
|
+
* corebooks#114 (the ratchet gate) and #86, because a ratchet cannot start from a baseline
|
|
54
|
+
* nobody believes.
|
|
55
|
+
*
|
|
56
|
+
* Order: an explicit `--base=<ref>` wins; otherwise `origin/HEAD`, which is the symbolic ref
|
|
57
|
+
* git keeps for the remote's OWN default branch and is therefore right in every repo without
|
|
58
|
+
* naming one; `origin/main` stays last so nothing that works today stops working.
|
|
59
|
+
*
|
|
60
|
+
* Fail-closed is unchanged (gh#542): if no candidate resolves, this returns an error rather
|
|
61
|
+
* than an empty file list. "Could not work out the base" must never read as "nothing changed".
|
|
62
|
+
*/
|
|
63
|
+
const explicitBase = args.find((a) => a.startsWith("--base="))?.slice("--base=".length);
|
|
64
|
+
const candidates = explicitBase ? [explicitBase] : ["origin/HEAD", "origin/main"];
|
|
65
|
+
|
|
66
|
+
let mergeBase = null;
|
|
67
|
+
let baseUsed = null;
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
const out = run(["merge-base", "HEAD", candidate]);
|
|
70
|
+
if (out !== null) {
|
|
71
|
+
mergeBase = out;
|
|
72
|
+
baseUsed = candidate;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
44
77
|
if (mergeBase === null) {
|
|
45
78
|
return {
|
|
46
79
|
error:
|
|
47
|
-
|
|
80
|
+
`ui-audit --changed could not resolve a base (tried: ${candidates.join(", ")}). ` +
|
|
48
81
|
"Without a base there is no such thing as \u201cwhat this branch changed\u201d, and reporting a " +
|
|
49
|
-
"clean audit from that is not a result. Fetch
|
|
50
|
-
"`git fetch --unshallow`), or pass the directories to scan instead of
|
|
82
|
+
"clean audit from that is not a result. Fetch the default branch (a shallow clone may need " +
|
|
83
|
+
"`git fetch --unshallow`), pass `--base=<ref>`, or pass the directories to scan instead of " +
|
|
84
|
+
"`--changed`.",
|
|
51
85
|
};
|
|
52
86
|
}
|
|
53
87
|
|
|
88
|
+
|
|
89
|
+
|
|
54
90
|
const parts = [
|
|
55
91
|
run(["diff", "--name-only", "--diff-filter=ACMR", mergeBase.trim(), "--"]),
|
|
56
92
|
run(["diff", "--name-only", "--diff-filter=ACMR", "--cached"]),
|
|
@@ -64,6 +100,10 @@ function changedFiles() {
|
|
|
64
100
|
}
|
|
65
101
|
|
|
66
102
|
return {
|
|
103
|
+
// Đi kèm danh sách file, và được in ra: một base SAI không làm chương trình chết, nó cho ra
|
|
104
|
+
// một con số hợp lý (1114 lỗi trên nhánh chưa sửa gì). Cách duy nhất để người đọc phát hiện
|
|
105
|
+
// là thấy nó đã so với cái gì.
|
|
106
|
+
base: baseUsed,
|
|
67
107
|
files: [
|
|
68
108
|
...new Set(
|
|
69
109
|
parts
|
|
@@ -96,13 +136,55 @@ if (changed?.error) {
|
|
|
96
136
|
}
|
|
97
137
|
process.exit(2);
|
|
98
138
|
}
|
|
139
|
+
/*
|
|
140
|
+
* ONE TERRITORY, READ BY BOTH PATHS (gh#949).
|
|
141
|
+
*
|
|
142
|
+
* The scan roots used to be spelled inline here, so only the DEFAULT path knew them and
|
|
143
|
+
* `--changed` scanned whatever git happened to name. On the same commit, same working tree:
|
|
144
|
+
*
|
|
145
|
+
* pnpm run audit -> 0 error
|
|
146
|
+
* node scripts/ui-audit.mjs --changed -> 3 error
|
|
147
|
+
*
|
|
148
|
+
* All three were in `mcp/src/data/components.ts`, and all three were the catalog's own PROSE —
|
|
149
|
+
* `Text`'s description quotes `<span className="text-[13px]">` as the anti-example the component
|
|
150
|
+
* exists to replace. A guard that reports its own teaching material is a guard people switch off.
|
|
151
|
+
*
|
|
152
|
+
* The fix is NOT to strip backtick spans from the corpus, which was the first idea: in a `.tsx`
|
|
153
|
+
* a backtick is a TEMPLATE LITERAL, i.e. real code — `className={`text-[13px]`}` appears in this
|
|
154
|
+
* repo today, and blinding the audit to it would trade three false positives for real misses.
|
|
155
|
+
*
|
|
156
|
+
* Nor is it a per-directory exclusion. The audit's subject is UI CODE, and in SELF mode this
|
|
157
|
+
* repo's UI code is `src/` and `docs/` — `mcp/src/data` renders nothing and holds no JSX (0 `.tsx`
|
|
158
|
+
* files, measured). So the roots are the territory, and `--changed` now narrows to it rather than
|
|
159
|
+
* having its own opinion. Add a root here and BOTH paths gain it; that is the property the split
|
|
160
|
+
* spelling could not have.
|
|
161
|
+
*
|
|
162
|
+
* CONSUMER mode is deliberately NOT narrowed. There the roots are a GUESS at a Laravel layout,
|
|
163
|
+
* and an app keeping its components somewhere else would get "nothing scanned" from a filter that
|
|
164
|
+
* is only right about this repo. `--changed` seeing the real files is strictly better there.
|
|
165
|
+
*/
|
|
166
|
+
const SELF_SCAN_ROOTS = ["src", "docs"];
|
|
167
|
+
const CONSUMER_SCAN_ROOTS = [
|
|
168
|
+
"resources/js/components",
|
|
169
|
+
"resources/js/pages",
|
|
170
|
+
"resources/js/layouts",
|
|
171
|
+
];
|
|
172
|
+
const withinSelfRoots = (file) =>
|
|
173
|
+
SELF_SCAN_ROOTS.some((root) => file === root || file.startsWith(`${root}/`));
|
|
174
|
+
|
|
175
|
+
/** Named so the summary can say how many files `--changed` handed over and the roots declined. */
|
|
176
|
+
const changedOutsideRoots =
|
|
177
|
+
CHANGED && SELF && !dirArgs.length ? changed.files.filter((f) => !withinSelfRoots(f)) : [];
|
|
178
|
+
|
|
99
179
|
const SCAN_DIRS = CHANGED
|
|
100
|
-
?
|
|
180
|
+
? SELF && !dirArgs.length
|
|
181
|
+
? changed.files.filter(withinSelfRoots)
|
|
182
|
+
: changed.files
|
|
101
183
|
: dirArgs.length
|
|
102
184
|
? dirArgs
|
|
103
185
|
: SELF
|
|
104
|
-
?
|
|
105
|
-
:
|
|
186
|
+
? SELF_SCAN_ROOTS
|
|
187
|
+
: CONSUMER_SCAN_ROOTS;
|
|
106
188
|
|
|
107
189
|
const PALETTE =
|
|
108
190
|
"red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|gray|grey|slate|zinc|neutral|stone";
|
|
@@ -1850,7 +1932,15 @@ if (changedNoFiles && findings.length === 0) {
|
|
|
1850
1932
|
JSON.stringify({ summary: { errors: 0, warnings: 0 }, findings: [] }, null, 2) + "\n",
|
|
1851
1933
|
);
|
|
1852
1934
|
} else if (!quiet) {
|
|
1853
|
-
|
|
1935
|
+
// "Nothing changed" and "everything that changed is outside the territory" are two
|
|
1936
|
+
// different facts, and reading the first when the second is true is how someone concludes
|
|
1937
|
+
// their file was audited (gh#949).
|
|
1938
|
+
console.log(
|
|
1939
|
+
changedOutsideRoots.length > 0
|
|
1940
|
+
? `✓ ui-audit --changed: nhánh này không đổi file .tsx/.jsx nào trong ` +
|
|
1941
|
+
`[${SELF_SCAN_ROOTS.join(", ")}] (${changedOutsideRoots.length} file đổi ở ngoài đó).`
|
|
1942
|
+
: "✓ ui-audit --changed: no .tsx/.jsx changed on this branch.",
|
|
1943
|
+
);
|
|
1854
1944
|
}
|
|
1855
1945
|
process.exit(0);
|
|
1856
1946
|
}
|
|
@@ -1899,6 +1989,19 @@ if (filesScanned === 0 && !changedNoFiles) {
|
|
|
1899
1989
|
if (f.standard) console.log(` ${C.dim}standard: ${f.standard}${C.reset}`);
|
|
1900
1990
|
console.log(` ${C.dim}${f.snippet}${C.reset}`);
|
|
1901
1991
|
}
|
|
1992
|
+
// Dưới `--changed`, NÓI RA base đã so. Một base sai không làm chương trình chết — nó cho ra một
|
|
1993
|
+
// con số hợp lý, và đó là thứ duy nhất người đọc có thể dùng để nghi ngờ (gh#948).
|
|
1994
|
+
if (CHANGED && changed?.base) {
|
|
1995
|
+
console.log(`${C.dim}so với: ${changed.base}${C.reset}`);
|
|
1996
|
+
}
|
|
1997
|
+
// Bỏ file trong im lặng là cách một gate thu hẹp dần mà không ai thấy (cùng lý do với dòng
|
|
1998
|
+
// `so với:` ngay trên). Nói ra số file và lãnh thổ đã dùng.
|
|
1999
|
+
if (changedOutsideRoots.length > 0 && !quiet) {
|
|
2000
|
+
console.log(
|
|
2001
|
+
`${C.dim}ngoài lãnh thổ [${SELF_SCAN_ROOTS.join(", ")}], không quét: ` +
|
|
2002
|
+
`${changedOutsideRoots.length} file${C.reset}`,
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
1902
2005
|
console.log(
|
|
1903
2006
|
`\ngodxjp-ui audit: ${C.red}${errors.length} error(s)${C.reset}, ${C.yellow}${warnings.length} warning(s)${C.reset}` +
|
|
1904
2007
|
(scannedFiles.length > 0
|