@godxjp/ui 23.2.0 → 23.3.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/dist/app/timezones.js +2 -1
- package/dist/components/charts/chart-cartesian.js +2 -4
- package/dist/components/charts/chart-frame.js +2 -1
- package/dist/components/data-entry/date-picker.js +12 -14
- package/dist/components/data-entry/number-input.js +2 -1
- package/dist/components/feedback/index.d.ts +2 -2
- package/dist/components/feedback/index.js +2 -0
- package/dist/components/feedback/skeleton.d.ts +11 -2
- package/dist/components/feedback/skeleton.js +8 -0
- package/dist/components/general/button.js +2 -1
- package/dist/components/layout/error-surface.js +3 -2
- package/dist/components/ui/toggle.js +2 -1
- package/dist/i18n/translate.js +3 -2
- package/dist/lib/datetime/picker-format.js +2 -1
- package/dist/lib/format.js +3 -2
- package/dist/lib/intl-cache.d.ts +32 -0
- package/dist/lib/intl-cache.js +29 -0
- package/dist/props/components/data-entry.prop.d.ts +11 -0
- package/dist/props/components/feedback.prop.d.ts +19 -0
- package/dist/props/registry.d.ts +5 -0
- package/dist/props/registry.js +5 -0
- package/dist/styles/alert-layout.css +22 -0
- package/dist/styles/card-layout.css +4 -0
- package/dist/styles/form-layout.css +11 -1
- package/dist/tokens/axes.css +5 -0
- package/dist/tokens/foundation.css +4 -4
- package/docs/DESIGN-AUTHORITY.md +44 -20
- package/package.json +21 -5
- package/scripts/_agent-setup.mjs +196 -29
- package/scripts/guinea-pig-skill.md +14 -0
- package/scripts/postinstall.mjs +9 -0
- package/scripts/ui-audit.mjs +135 -32
package/scripts/ui-audit.mjs
CHANGED
|
@@ -32,29 +32,72 @@ const dirArgs = args.filter((a) => !a.startsWith("--") && a !== "json");
|
|
|
32
32
|
* local gate or in CI then covers every edit path, including the ones nobody thought of.
|
|
33
33
|
*/
|
|
34
34
|
function changedFiles() {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const run = (a) =>
|
|
35
|
+
// A git command that FAILED used to be indistinguishable from one that found nothing: `run`
|
|
36
|
+
// returned `stdout ?? ""`, so a shallow clone with no `origin/main`, or a directory that is not
|
|
37
|
+
// a repository at all, produced an empty list and a clean, green, zero-exit run (gh#542).
|
|
38
|
+
const run = (a) => {
|
|
39
|
+
const r = spawnSync("git", a, { encoding: "utf8" });
|
|
40
|
+
return r.status === 0 ? (r.stdout ?? "") : null;
|
|
41
|
+
};
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
const mergeBase = run(["merge-base", "HEAD", "origin/main"]);
|
|
44
|
+
if (mergeBase === null) {
|
|
45
|
+
return {
|
|
46
|
+
error:
|
|
47
|
+
"ui-audit --changed could not resolve `git merge-base HEAD origin/main`. " +
|
|
48
|
+
"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 origin/main (a shallow clone may need " +
|
|
50
|
+
"`git fetch --unshallow`), or pass the directories to scan instead of `--changed`.",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const parts = [
|
|
55
|
+
run(["diff", "--name-only", "--diff-filter=ACMR", mergeBase.trim(), "--"]),
|
|
56
|
+
run(["diff", "--name-only", "--diff-filter=ACMR", "--cached"]),
|
|
57
|
+
run(["ls-files", "--others", "--exclude-standard"]),
|
|
52
58
|
];
|
|
59
|
+
if (parts.some((out) => out === null)) {
|
|
60
|
+
return {
|
|
61
|
+
error:
|
|
62
|
+
"ui-audit --changed: a `git diff`/`git ls-files` call failed; refusing to report a clean run.",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
files: [
|
|
68
|
+
...new Set(
|
|
69
|
+
parts
|
|
70
|
+
.join("\n")
|
|
71
|
+
.split("\n")
|
|
72
|
+
.map((f) => f.trim())
|
|
73
|
+
// SCANNABLE, not a second hand-written list. Selecting `.jsx` here while `walk()` below
|
|
74
|
+
// accepted only `.tsx`/`.ts` meant a changed `.jsx` was picked, counted in the summary
|
|
75
|
+
// line as scanned, and then dropped by the walker without being opened — and if it was
|
|
76
|
+
// the only change, the run exited 0 saying "no .tsx/.jsx changed" (gh#542).
|
|
77
|
+
.filter((f) => SCANNABLE.test(f) && existsSync(join(CWD, f))),
|
|
78
|
+
),
|
|
79
|
+
],
|
|
80
|
+
};
|
|
53
81
|
}
|
|
54
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The ONE extension set. `changedFiles()` selects with it and `walk()` admits with it, so the
|
|
85
|
+
* selector and the walker cannot drift apart again — that drift was gh#542.
|
|
86
|
+
*/
|
|
87
|
+
const SCANNABLE = /\.(tsx|jsx|ts)$/;
|
|
88
|
+
|
|
55
89
|
const CHANGED = args.includes("--changed");
|
|
90
|
+
const changed = CHANGED ? changedFiles() : null;
|
|
91
|
+
if (changed?.error) {
|
|
92
|
+
if (asJson) {
|
|
93
|
+
process.stdout.write(JSON.stringify({ error: changed.error }, null, 2) + "\n");
|
|
94
|
+
} else {
|
|
95
|
+
console.error(changed.error);
|
|
96
|
+
}
|
|
97
|
+
process.exit(2);
|
|
98
|
+
}
|
|
56
99
|
const SCAN_DIRS = CHANGED
|
|
57
|
-
?
|
|
100
|
+
? changed.files
|
|
58
101
|
: dirArgs.length
|
|
59
102
|
? dirArgs
|
|
60
103
|
: SELF
|
|
@@ -728,7 +771,7 @@ function walk(dir, acc = []) {
|
|
|
728
771
|
// Accept a FILE path directly (the per-file editor hook passes one), not just a directory.
|
|
729
772
|
try {
|
|
730
773
|
if (statSync(dir).isFile()) {
|
|
731
|
-
if (
|
|
774
|
+
if (SCANNABLE.test(dir)) acc.push(dir);
|
|
732
775
|
return acc;
|
|
733
776
|
}
|
|
734
777
|
} catch {
|
|
@@ -747,10 +790,7 @@ function walk(dir, acc = []) {
|
|
|
747
790
|
// Test/story dirs are not product UI — never hold them to the UI-standardization rules.
|
|
748
791
|
if (name === "__tests__" || name === "node_modules") continue;
|
|
749
792
|
walk(full, acc);
|
|
750
|
-
} else if (
|
|
751
|
-
(name.endsWith(".tsx") || name.endsWith(".ts")) &&
|
|
752
|
-
!/\.(test|spec|stories)\.tsx?$/.test(name)
|
|
753
|
-
) {
|
|
793
|
+
} else if (SCANNABLE.test(name) && !/\.(test|spec|stories)\.[jt]sx?$/.test(name)) {
|
|
754
794
|
acc.push(full);
|
|
755
795
|
}
|
|
756
796
|
}
|
|
@@ -851,16 +891,47 @@ function staleOwnedRules() {
|
|
|
851
891
|
const target = join(CWD, ".ai", "rules", "godxjp-ui.md");
|
|
852
892
|
if (!existsSync(target)) return null;
|
|
853
893
|
|
|
854
|
-
|
|
894
|
+
// FOUR states, not two. `!stamped || !installed || stamped === installed` collapsed three very
|
|
895
|
+
// different situations into one silence, and only ONE of them is genuinely fine:
|
|
896
|
+
//
|
|
897
|
+
// no rule file, consumer never opted into the agent kit -> silent, and it MUST stay silent.
|
|
898
|
+
// Turning this into a finding would make a UI audit into a tool that nags every consumer to
|
|
899
|
+
// install an agent kit they did not ask for. (Handled by the existsSync above.)
|
|
900
|
+
// rule file present but carrying NO stamp -> compatibility UNKNOWN, say so.
|
|
901
|
+
// installed version unreadable -> compatibility UNKNOWN, say so.
|
|
902
|
+
// stamp != installed -> stale, the original finding.
|
|
903
|
+
const contents = readFileSync(target, "utf8");
|
|
904
|
+
const stamped = /<!-- godxjp-ui:version ([^\s]+) -->/.exec(contents)?.[1];
|
|
905
|
+
|
|
855
906
|
let installed;
|
|
856
907
|
try {
|
|
857
908
|
installed = JSON.parse(
|
|
858
909
|
readFileSync(join(CWD, "node_modules", "@godxjp", "ui", "package.json"), "utf8"),
|
|
859
910
|
).version;
|
|
860
911
|
} catch {
|
|
861
|
-
|
|
912
|
+
installed = undefined;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (!stamped || !installed) {
|
|
916
|
+
// A file the package OWNS, whose provenance cannot be established. That is not the same as
|
|
917
|
+
// "up to date", and reporting it as such is the whole class of bug this function exists for.
|
|
918
|
+
return {
|
|
919
|
+
file: ".ai/rules/godxjp-ui.md",
|
|
920
|
+
line: 1,
|
|
921
|
+
rule: "owned-rules-unknown",
|
|
922
|
+
severity: "warn",
|
|
923
|
+
message:
|
|
924
|
+
`This file is written by @godxjp/ui, but its version cannot be established ` +
|
|
925
|
+
`(${!stamped ? "the file carries no `<!-- godxjp-ui:version … -->` stamp" : "the installed package version could not be read"}). ` +
|
|
926
|
+
`An agent may be reading guidance from a different major. Refresh it with ` +
|
|
927
|
+
`\`npx @godxjp/ui init-agent\`, or delete the file if this project does not use the agent kit.`,
|
|
928
|
+
replacement: null,
|
|
929
|
+
standard: null,
|
|
930
|
+
snippet: (stamped ?? "(no stamp)") + " vs " + (installed ?? "(package version unreadable)"),
|
|
931
|
+
};
|
|
862
932
|
}
|
|
863
|
-
|
|
933
|
+
|
|
934
|
+
if (stamped === installed) return null;
|
|
864
935
|
|
|
865
936
|
return {
|
|
866
937
|
file: ".ai/rules/godxjp-ui.md",
|
|
@@ -880,12 +951,15 @@ const findings = [];
|
|
|
880
951
|
const stale = staleOwnedRules();
|
|
881
952
|
if (stale) findings.push(stale);
|
|
882
953
|
let filesScanned = 0;
|
|
954
|
+
/** What was actually OPENED. The summary used to name the selection instead (gh#542). */
|
|
955
|
+
const scannedFiles = [];
|
|
883
956
|
for (const dir of SCAN_DIRS) {
|
|
884
957
|
for (const file of walk(isAbsolute(dir) ? dir : join(CWD, dir))) {
|
|
885
958
|
const rel = relative(CWD, file);
|
|
886
959
|
// Framework test support is executable fixture markup, not a shipped product screen.
|
|
887
960
|
if (SELF && !args.includes("--consumer") && rel.startsWith("src/test/")) continue;
|
|
888
961
|
filesScanned += 1;
|
|
962
|
+
scannedFiles.push(rel);
|
|
889
963
|
// A primitive implements native controls; asking Input to render Input recurses.
|
|
890
964
|
// Consumer applications and executable docs still receive these composition checks.
|
|
891
965
|
const fileRules =
|
|
@@ -902,7 +976,7 @@ for (const dir of SCAN_DIRS) {
|
|
|
902
976
|
/** Both opt-outs, by line index: the per-line markers and the reason-carrying block. */
|
|
903
977
|
const suppressed = (ruleId, i) =>
|
|
904
978
|
isSuppressed(ruleId, origLines[i], origLines[i - 1]) || inDisabledBlock(ruleId, i);
|
|
905
|
-
const isJsx = file.endsWith(".tsx");
|
|
979
|
+
const isJsx = file.endsWith(".tsx") || file.endsWith(".jsx");
|
|
906
980
|
// This compiler output intentionally resolves CSS variables to email-safe literals.
|
|
907
981
|
// gen-email-tokens.mjs --check verifies it against its canonical token sources.
|
|
908
982
|
const compiledEmailTokens =
|
|
@@ -1069,11 +1143,37 @@ const warnings = findings.filter((f) => f.severity === "warn");
|
|
|
1069
1143
|
// "✓ No UI-standardization violations found." and exited 0 having read nothing at all.
|
|
1070
1144
|
// …except under `--changed`, where "this branch touched no .tsx" is a clean run, not a
|
|
1071
1145
|
// misconfigured path. Failing there would make the gate unusable on every backend-only commit.
|
|
1072
|
-
if
|
|
1073
|
-
|
|
1146
|
+
// "This branch touched no UI file" is a clean run — but ONLY if there is nothing else to say.
|
|
1147
|
+
//
|
|
1148
|
+
// `staleOwnedRules()` runs before the scan loop and pushes its finding into `findings`, and this
|
|
1149
|
+
// branch used to exit 0 regardless: a consumer whose package-owned rules were four majors out of
|
|
1150
|
+
// date got "✓ no .tsx/.jsx changed" and a clean exit on any commit that happened not to touch a
|
|
1151
|
+
// component. Under `--format json` it was worse — the exit came BEFORE anything was written, so
|
|
1152
|
+
// stdout was EMPTY and a CI step reading it could not tell that from a pass.
|
|
1153
|
+
//
|
|
1154
|
+
// Reproduced: rules stamped 19.6.0 against an installed 23.3.0, one README.md changed →
|
|
1155
|
+
// `✓ no .tsx/.jsx changed`, exit 0, empty JSON, while `ui-audit src --format json` on the same
|
|
1156
|
+
// tree at the same moment reported `owned-rules-stale`.
|
|
1157
|
+
//
|
|
1158
|
+
// Same defect as the `.jsx` one this file just fixed, one screen further down: a gate declaring
|
|
1159
|
+
// itself clean while holding a finding.
|
|
1160
|
+
/** `--changed` legitimately opened no file. NOT the same thing as a misconfigured scan path. */
|
|
1161
|
+
const changedNoFiles = CHANGED && filesScanned === 0;
|
|
1162
|
+
|
|
1163
|
+
if (changedNoFiles && findings.length === 0) {
|
|
1164
|
+
if (asJson) {
|
|
1165
|
+
// ALWAYS emit a valid document on the JSON path. This branch used to exit before writing
|
|
1166
|
+
// anything, so a CI step parsing stdout got an empty string and a zero exit.
|
|
1167
|
+
process.stdout.write(
|
|
1168
|
+
JSON.stringify({ summary: { errors: 0, warnings: 0 }, findings: [] }, null, 2) + "\n",
|
|
1169
|
+
);
|
|
1170
|
+
} else if (!quiet) {
|
|
1171
|
+
console.log("✓ ui-audit --changed: no .tsx/.jsx changed on this branch.");
|
|
1172
|
+
}
|
|
1074
1173
|
process.exit(0);
|
|
1075
1174
|
}
|
|
1076
|
-
|
|
1175
|
+
|
|
1176
|
+
if (filesScanned === 0 && !CHANGED) {
|
|
1077
1177
|
const message =
|
|
1078
1178
|
`ui-audit scanned 0 files — none of [${SCAN_DIRS.join(", ")}] exists (or all were filtered). ` +
|
|
1079
1179
|
`Pass the directories to scan, e.g. \`node scripts/ui-audit.mjs src docs\`. ` +
|
|
@@ -1088,7 +1188,7 @@ if (filesScanned === 0) {
|
|
|
1088
1188
|
process.exitCode = 2;
|
|
1089
1189
|
}
|
|
1090
1190
|
|
|
1091
|
-
if (filesScanned === 0) {
|
|
1191
|
+
if (filesScanned === 0 && !changedNoFiles) {
|
|
1092
1192
|
// already reported above
|
|
1093
1193
|
} else if (asJson) {
|
|
1094
1194
|
process.stdout.write(
|
|
@@ -1118,7 +1218,10 @@ if (filesScanned === 0) {
|
|
|
1118
1218
|
console.log(` ${C.dim}${f.snippet}${C.reset}`);
|
|
1119
1219
|
}
|
|
1120
1220
|
console.log(
|
|
1121
|
-
`\ngodxjp-ui audit: ${C.red}${errors.length} error(s)${C.reset}, ${C.yellow}${warnings.length} warning(s)${C.reset}
|
|
1221
|
+
`\ngodxjp-ui audit: ${C.red}${errors.length} error(s)${C.reset}, ${C.yellow}${warnings.length} warning(s)${C.reset}` +
|
|
1222
|
+
(scannedFiles.length > 0
|
|
1223
|
+
? ` across ${scannedFiles.join(", ")}.`
|
|
1224
|
+
: " — no UI file changed on this branch, but the findings above are not about a file."),
|
|
1122
1225
|
);
|
|
1123
1226
|
if (errors.length === 0 && warnings.length === 0) {
|
|
1124
1227
|
console.log("✓ No UI-standardization violations found.");
|
|
@@ -1126,4 +1229,4 @@ if (filesScanned === 0) {
|
|
|
1126
1229
|
}
|
|
1127
1230
|
|
|
1128
1231
|
// See the note above --rules: exitCode, so a large JSON report drains fully.
|
|
1129
|
-
if (filesScanned > 0) process.exitCode = errors.length > 0 ? 1 : 0;
|
|
1232
|
+
if (filesScanned > 0 || changedNoFiles) process.exitCode = errors.length > 0 ? 1 : 0;
|