@godxjp/ui 23.1.0 → 23.2.1
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/components/data-display/card.d.ts +45 -2
- package/dist/components/data-display/card.js +4 -2
- package/dist/components/data-display/feature-list.d.ts +26 -0
- package/dist/components/data-display/feature-list.js +29 -0
- package/dist/components/data-display/index.d.ts +6 -0
- package/dist/components/data-display/index.js +6 -0
- package/dist/components/data-display/swatch.d.ts +24 -0
- package/dist/components/data-display/swatch.js +19 -0
- package/dist/components/data-display/thumbnail.d.ts +37 -0
- package/dist/components/data-display/thumbnail.js +20 -0
- package/dist/components/feedback/index.d.ts +2 -2
- package/dist/components/feedback/index.js +19 -1
- package/dist/components/feedback/skeleton.d.ts +33 -3
- package/dist/components/feedback/skeleton.js +154 -3
- package/dist/components/layout/flex.d.ts +1 -1
- package/dist/components/layout/flex.js +17 -5
- package/dist/components/layout/sidebar.js +9 -2
- package/dist/components/navigation/tabs-scroll.d.ts +27 -0
- package/dist/components/navigation/tabs-scroll.js +36 -1
- package/dist/components/navigation/tabs.d.ts +2 -2
- package/dist/components/navigation/tabs.js +44 -12
- package/dist/i18n/messages/en.json +5 -0
- package/dist/i18n/messages/ja.json +5 -0
- package/dist/i18n/messages/vi.json +5 -0
- package/dist/props/components/data-display.prop.d.ts +93 -0
- package/dist/props/components/feedback.prop.d.ts +85 -1
- package/dist/props/components/layout.prop.d.ts +72 -1
- package/dist/props/components/navigation.prop.d.ts +72 -1
- package/dist/props/registry.d.ts +118 -0
- package/dist/props/registry.js +134 -0
- package/dist/styles/alert-layout.css +139 -0
- package/dist/styles/card-layout.css +48 -0
- package/dist/styles/data-display-layout.css +88 -0
- package/dist/styles/navigation-layout.css +76 -3
- package/dist/styles/shell-layout.css +18 -0
- package/dist/tokens/components/card.css +2 -0
- package/dist/tokens/components/data-display.css +23 -0
- package/dist/tokens/components/feedback.css +18 -0
- package/dist/tokens/components/navigation.css +2 -0
- package/dist/tokens/foundation.css +18 -16
- package/docs/CONSUMER-RULES.md +1 -1
- package/docs/DESIGN-AUTHORITY.md +2 -1
- package/docs/FRAME-COVERAGE-REPORT.md +29 -3
- package/docs/assets/shot-landscape.svg +12 -0
- package/docs/assets/shot-portrait.svg +10 -0
- package/docs/data-display/card/index.tsx +40 -0
- package/docs/data-display/feature-list.tsx +159 -0
- package/docs/data-display/swatch.tsx +107 -0
- package/docs/data-display/thumbnail.tsx +128 -0
- package/docs/feedback/skeleton.tsx +57 -0
- package/docs/layout/flex.tsx +29 -0
- package/docs/layout/sidebar.tsx +66 -10
- package/docs/navigation/tabs.tsx +59 -0
- package/package.json +4 -7
- package/scripts/_agent-setup.mjs +164 -28
- package/scripts/consumer-rule.md +16 -0
- package/scripts/postinstall.mjs +9 -0
- package/scripts/ui-audit.mjs +70 -28
package/docs/navigation/tabs.tsx
CHANGED
|
@@ -625,6 +625,65 @@ export default function Demo() {
|
|
|
625
625
|
/>
|
|
626
626
|
</CardContent>
|
|
627
627
|
</Card>
|
|
628
|
+
|
|
629
|
+
<Card>
|
|
630
|
+
<CardHeader>
|
|
631
|
+
<CardTitle level={2}>indicator · animated</CardTitle>
|
|
632
|
+
<CardDescription>
|
|
633
|
+
Ant Design の indicator
|
|
634
|
+
は下線バーの長さと寄せ方を決める。size="full"(既定)はタブ全幅、size="label"
|
|
635
|
+
はタブ自身の左右パディングを引いた分、つまりラベルの幅。align
|
|
636
|
+
はバーが短いときだけ効く。animated は inkBar(下線のクロスフェード・既定 ON)と
|
|
637
|
+
tabPane(パネルのフェードイン・既定 OFF)の二つのスイッチで、prefers-reduced-motion
|
|
638
|
+
では両方とも止まる。
|
|
639
|
+
</CardDescription>
|
|
640
|
+
</CardHeader>
|
|
641
|
+
<CardContent>
|
|
642
|
+
<Flex direction="col" gap="lg">
|
|
643
|
+
<Tabs
|
|
644
|
+
id="antd-indicator-full"
|
|
645
|
+
defaultValue="pending"
|
|
646
|
+
variant="line"
|
|
647
|
+
items={journalItems}
|
|
648
|
+
/>
|
|
649
|
+
<Tabs
|
|
650
|
+
id="antd-indicator-label"
|
|
651
|
+
defaultValue="pending"
|
|
652
|
+
variant="line"
|
|
653
|
+
indicator={{ size: "label" }}
|
|
654
|
+
items={journalItems}
|
|
655
|
+
/>
|
|
656
|
+
<Tabs
|
|
657
|
+
id="antd-indicator-label-start"
|
|
658
|
+
defaultValue="pending"
|
|
659
|
+
variant="line"
|
|
660
|
+
indicator={{ size: "label", align: "start" }}
|
|
661
|
+
items={journalItems}
|
|
662
|
+
/>
|
|
663
|
+
<Tabs
|
|
664
|
+
id="antd-indicator-label-end"
|
|
665
|
+
defaultValue="pending"
|
|
666
|
+
variant="line"
|
|
667
|
+
indicator={{ size: "label", align: "end" }}
|
|
668
|
+
items={journalItems}
|
|
669
|
+
/>
|
|
670
|
+
<Tabs
|
|
671
|
+
id="antd-animated-off"
|
|
672
|
+
defaultValue="pending"
|
|
673
|
+
variant="line"
|
|
674
|
+
animated={false}
|
|
675
|
+
items={journalItems}
|
|
676
|
+
/>
|
|
677
|
+
<Tabs
|
|
678
|
+
id="antd-animated-pane"
|
|
679
|
+
defaultValue="pending"
|
|
680
|
+
variant="line"
|
|
681
|
+
animated={{ tabPane: true }}
|
|
682
|
+
items={journalItems}
|
|
683
|
+
/>
|
|
684
|
+
</Flex>
|
|
685
|
+
</CardContent>
|
|
686
|
+
</Card>
|
|
628
687
|
</Flex>
|
|
629
688
|
</PageContainer>
|
|
630
689
|
);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/ui",
|
|
3
|
-
"version": "23.1
|
|
4
|
-
"godxUiMcp": "23.1
|
|
3
|
+
"version": "23.2.1",
|
|
4
|
+
"godxUiMcp": "23.2.1",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -261,10 +261,6 @@
|
|
|
261
261
|
"types": "./dist/components/data-entry/textarea.d.ts",
|
|
262
262
|
"import": "./dist/components/data-entry/textarea.js"
|
|
263
263
|
},
|
|
264
|
-
"./ui/time-input": {
|
|
265
|
-
"types": "./dist/components/data-entry/time-input.d.ts",
|
|
266
|
-
"import": "./dist/components/data-entry/time-input.js"
|
|
267
|
-
},
|
|
268
264
|
"./ui/dialog": {
|
|
269
265
|
"types": "./dist/components/feedback/dialog.d.ts",
|
|
270
266
|
"import": "./dist/components/feedback/dialog.js"
|
|
@@ -336,7 +332,7 @@
|
|
|
336
332
|
"verify": "pnpm typecheck && pnpm lint && pnpm format && pnpm build && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:no-external-assets && pnpm check:token-scale-bypass && pnpm check:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:frame-coverage && pnpm test",
|
|
337
333
|
"verify:static": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:token-scale-bypass && pnpm check:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:mcp-prop-sync && pnpm check:contrast && pnpm check:visual-audit && pnpm test",
|
|
338
334
|
"verify:ci": "pnpm verify:ci:static && pnpm check:frame-contracts && pnpm test",
|
|
339
|
-
"verify:ci:static": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:registry && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:no-external-assets && pnpm check:token-scale-bypass && pnpm check:no-antd-runtime && pnpm check:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:gate-coverage && pnpm check:mcp-prop-sync && pnpm check:catalog-contradictions && pnpm check:catalog-snippets",
|
|
335
|
+
"verify:ci:static": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:registry && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:no-external-assets && pnpm check:token-scale-bypass && pnpm check:no-antd-runtime && pnpm check:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-catalog-completeness && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:gate-coverage && pnpm check:mcp-prop-sync && pnpm check:catalog-contradictions && pnpm check:catalog-snippets",
|
|
340
336
|
"verify:browser": "pnpm check:contrast && pnpm check:text-ink-clip && pnpm check:font-fallback-metrics && pnpm check:visual-audit",
|
|
341
337
|
"verify:release": "pnpm verify:static && pnpm check:frame-contracts && pnpm check:frame-coverage",
|
|
342
338
|
"verify:publish-tree": "pnpm build && pnpm check:packed-public-contract && pnpm check:use-client && pnpm check:dist-tokens-resolve",
|
|
@@ -347,6 +343,7 @@
|
|
|
347
343
|
"check:mcp-sync": "node scripts/check-mcp-sync.mjs",
|
|
348
344
|
"check:mcp-catalog-coverage": "node scripts/check-mcp-catalog-coverage.mjs",
|
|
349
345
|
"check:mcp-orphans": "node scripts/check-mcp-orphans.mjs",
|
|
346
|
+
"check:mcp-catalog-completeness": "node scripts/check-mcp-catalog-completeness.mjs",
|
|
350
347
|
"check:catalog-contradictions": "node scripts/check-catalog-contradictions.mjs",
|
|
351
348
|
"check:mcp-prop-sync": "node scripts/check-mcp-prop-sync.mjs",
|
|
352
349
|
"check:doc-prop-existence": "node scripts/check-doc-prop-existence.mjs",
|
package/scripts/_agent-setup.mjs
CHANGED
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
* init-agent-kit.mjs (explicit, full kit). Every writer is IDEMPOTENT and
|
|
4
4
|
* NON-DESTRUCTIVE: it only creates a missing file or ADDS a missing key, never
|
|
5
5
|
* overwrites existing config.
|
|
6
|
+
*
|
|
7
|
+
* That sentence was a claim, not a guarantee, until gh#541: `readJson` returned `null` for BOTH
|
|
8
|
+
* "no file" and "file I cannot parse", so `readJson(path) ?? {}` read a consumer's malformed
|
|
9
|
+
* `.mcp.json` as an empty one and wrote over it — their other MCP servers went with it, silently.
|
|
10
|
+
* The guarantee is now structural: a file that exists but cannot be read, parsed, or recognised is
|
|
11
|
+
* NEVER written to. We leave a `.godxjp-ui-suggested` sidecar next to it and say so.
|
|
6
12
|
*/
|
|
7
13
|
import { createHash } from "node:crypto";
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { dirname, join } from "node:path";
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { basename, dirname, join } from "node:path";
|
|
10
16
|
import { fileURLToPath } from "node:url";
|
|
11
17
|
|
|
12
18
|
/** The godxjp-ui MCP server — pulled on demand via npx (no extra dependency to ship). */
|
|
@@ -20,6 +26,14 @@ export const MCP_KEY = "godx-ui";
|
|
|
20
26
|
export const AUDIT_HOOK_CMD = "node node_modules/@godxjp/ui/scripts/audit-hook.mjs";
|
|
21
27
|
export const PRIMER_CMD = "cat .claude/godxjp-ui-workflow.md";
|
|
22
28
|
|
|
29
|
+
/** What `.claude/settings.json` would get, used only for the `.godxjp-ui-suggested` sidecar. */
|
|
30
|
+
const SUGGESTED_HOOKS = {
|
|
31
|
+
PostToolUse: [
|
|
32
|
+
{ matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: AUDIT_HOOK_CMD }] },
|
|
33
|
+
],
|
|
34
|
+
SessionStart: [{ hooks: [{ type: "command", command: PRIMER_CMD }] }],
|
|
35
|
+
};
|
|
36
|
+
|
|
23
37
|
/** The per-session workflow mandate the SessionStart hook injects into the agent. */
|
|
24
38
|
export const KIT_VERSION = readJson(join(SELF_ROOT, "package.json"))?.version ?? "0.0.0";
|
|
25
39
|
|
|
@@ -145,12 +159,65 @@ Full guide: \`.claude/godxjp-ui-workflow.md\`.
|
|
|
145
159
|
<!-- godxjp-ui:end -->
|
|
146
160
|
`;
|
|
147
161
|
|
|
148
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Read a JSON file and say WHICH failure happened. Collapsing "absent" and "corrupt" into one
|
|
164
|
+
* `null` is the whole of gh#541 — see the module header.
|
|
165
|
+
*
|
|
166
|
+
* @returns {{state:"ok",json:object}|{state:"missing"}|{state:"unreadable"}|{state:"invalid-json"}|{state:"wrong-shape"}}
|
|
167
|
+
*/
|
|
168
|
+
function readJsonFile(path) {
|
|
169
|
+
let raw;
|
|
170
|
+
try {
|
|
171
|
+
raw = readFileSync(path, "utf8");
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return error.code === "ENOENT" ? { state: "missing" } : { state: "unreadable" };
|
|
174
|
+
}
|
|
175
|
+
let json;
|
|
149
176
|
try {
|
|
150
|
-
|
|
177
|
+
json = JSON.parse(raw);
|
|
151
178
|
} catch {
|
|
152
|
-
return
|
|
179
|
+
return { state: "invalid-json" };
|
|
153
180
|
}
|
|
181
|
+
// `JSON.parse` happily returns `null`, `[]` or `"text"`. Merging keys into any of those and
|
|
182
|
+
// writing the result back is the same data loss by a different door, so it is a refusal too.
|
|
183
|
+
if (json === null || typeof json !== "object" || Array.isArray(json))
|
|
184
|
+
return { state: "wrong-shape" };
|
|
185
|
+
return { state: "ok", json };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The sentence that goes in the refusal, so a consumer knows which of the three it hit. */
|
|
189
|
+
const READ_FAILURE = {
|
|
190
|
+
"invalid-json": "not valid JSON",
|
|
191
|
+
"wrong-shape": "JSON, but not an object",
|
|
192
|
+
unreadable: "unreadable",
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/** Unusable-as-absent — only for the read-only callers, which never write anything back. */
|
|
196
|
+
function readJson(path) {
|
|
197
|
+
const read = readJsonFile(path);
|
|
198
|
+
return read.state === "ok" ? read.json : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Write through a temp file + rename. `writeFileSync` is NOT atomic: an interrupt mid-write (^C
|
|
203
|
+
* during `npm install`, a full disk, the OOM killer) leaves a truncated file behind, which the
|
|
204
|
+
* next run reads as malformed. That is how the two halves of gh#541 fed each other — one bad
|
|
205
|
+
* write manufactured the precondition for the overwrite.
|
|
206
|
+
*/
|
|
207
|
+
function writeFileAtomic(path, data) {
|
|
208
|
+
const tmp = `${path}.godxjp-ui-tmp`;
|
|
209
|
+
writeFileSync(tmp, data);
|
|
210
|
+
renameSync(tmp, path);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Refuse to touch `path`, leave what we WOULD have written beside it, and return the sentence the
|
|
215
|
+
* caller prints. A diff the consumer can read beats a config we guessed at.
|
|
216
|
+
*/
|
|
217
|
+
function refuseAndSuggest(path, data, reason) {
|
|
218
|
+
const suggested = `${path}.godxjp-ui-suggested`;
|
|
219
|
+
writeFileAtomic(suggested, data);
|
|
220
|
+
return `left untouched (${reason}) — see ${basename(suggested)}`;
|
|
154
221
|
}
|
|
155
222
|
|
|
156
223
|
/** Ensure `.mcp.json` registers the godx-ui MCP server. Returns 'created' | 'added' | 'present'. */
|
|
@@ -164,22 +231,61 @@ function readJson(path) {
|
|
|
164
231
|
* agent reading whatever guidance shipped the day the package was FIRST installed. The library
|
|
165
232
|
* moved; the instructions for using it did not.
|
|
166
233
|
*/
|
|
234
|
+
/**
|
|
235
|
+
* Replace the delimited managed region, or REFUSE.
|
|
236
|
+
*
|
|
237
|
+
* The old shape set `tail = ""` when the closing marker was missing and returned
|
|
238
|
+
* `current.slice(0, i) + next` — so a file whose end marker had been deleted (or truncated away by
|
|
239
|
+
* a half-finished write) lost everything after the opening marker. Whatever the consumer wrote
|
|
240
|
+
* below our block was simply gone (gh#541).
|
|
241
|
+
*
|
|
242
|
+
* There is no safe way to guess where a region ends, so we do not guess. Two refusals, both
|
|
243
|
+
* returning `null` for the caller to report:
|
|
244
|
+
* • the opening marker appears more than once — which region is ours is ambiguous;
|
|
245
|
+
* • the closing marker is missing — the region has no end.
|
|
246
|
+
* A closing marker that appears more than once is NOT a refusal: the first one after the opening
|
|
247
|
+
* marker is the region's end, and everything past it is preserved either way.
|
|
248
|
+
*
|
|
249
|
+
* @returns {string|null} the rewritten file, or `null` to say "do not write".
|
|
250
|
+
*/
|
|
167
251
|
export function refreshBlock(current, next, startMarker, endMarker) {
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
252
|
+
const first = current.indexOf(startMarker);
|
|
253
|
+
if (first < 0) return current.replace(/\s*$/, "") + "\n\n" + next;
|
|
254
|
+
if (current.indexOf(startMarker, first + startMarker.length) >= 0) return null;
|
|
255
|
+
if (!endMarker) return null;
|
|
256
|
+
const j = current.indexOf(endMarker, first);
|
|
257
|
+
if (j < 0) return null;
|
|
258
|
+
return current.slice(0, first) + next + current.slice(j + endMarker.length);
|
|
173
259
|
}
|
|
174
260
|
|
|
175
261
|
export function ensureMcpJson(root) {
|
|
176
262
|
const path = join(root, ".mcp.json");
|
|
177
|
-
const
|
|
263
|
+
const read = readJsonFile(path);
|
|
264
|
+
if (read.state !== "ok" && read.state !== "missing") {
|
|
265
|
+
return refuseAndSuggest(
|
|
266
|
+
path,
|
|
267
|
+
JSON.stringify({ mcpServers: { [MCP_KEY]: MCP_SERVER } }, null, 2) + "\n",
|
|
268
|
+
READ_FAILURE[read.state],
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const json = read.state === "ok" ? read.json : {};
|
|
272
|
+
if (
|
|
273
|
+
json.mcpServers !== undefined &&
|
|
274
|
+
(json.mcpServers === null ||
|
|
275
|
+
typeof json.mcpServers !== "object" ||
|
|
276
|
+
Array.isArray(json.mcpServers))
|
|
277
|
+
) {
|
|
278
|
+
return refuseAndSuggest(
|
|
279
|
+
path,
|
|
280
|
+
JSON.stringify({ mcpServers: { [MCP_KEY]: MCP_SERVER } }, null, 2) + "\n",
|
|
281
|
+
"`mcpServers` is not an object",
|
|
282
|
+
);
|
|
283
|
+
}
|
|
178
284
|
json.mcpServers = json.mcpServers ?? {};
|
|
179
285
|
if (json.mcpServers[MCP_KEY]) return "present";
|
|
180
|
-
const created =
|
|
286
|
+
const created = read.state === "missing";
|
|
181
287
|
json.mcpServers[MCP_KEY] = MCP_SERVER;
|
|
182
|
-
|
|
288
|
+
writeFileAtomic(path, JSON.stringify(json, null, 2) + "\n");
|
|
183
289
|
return created ? "created" : "added";
|
|
184
290
|
}
|
|
185
291
|
|
|
@@ -187,7 +293,31 @@ export function ensureMcpJson(root) {
|
|
|
187
293
|
export function ensureClaudeHooks(root) {
|
|
188
294
|
const path = join(root, ".claude", "settings.json");
|
|
189
295
|
mkdirSync(dirname(path), { recursive: true });
|
|
190
|
-
const
|
|
296
|
+
const read = readJsonFile(path);
|
|
297
|
+
if (read.state !== "ok" && read.state !== "missing") {
|
|
298
|
+
// Same guard as ensureMcpJson, and the reason it is here rather than only there: this file
|
|
299
|
+
// holds the consumer's OWN hooks. Rewriting it from `{}` silently unhooks their tooling.
|
|
300
|
+
return [
|
|
301
|
+
refuseAndSuggest(
|
|
302
|
+
path,
|
|
303
|
+
JSON.stringify({ hooks: SUGGESTED_HOOKS }, null, 2) + "\n",
|
|
304
|
+
READ_FAILURE[read.state],
|
|
305
|
+
),
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
const json = read.state === "ok" ? read.json : {};
|
|
309
|
+
if (
|
|
310
|
+
json.hooks !== undefined &&
|
|
311
|
+
(json.hooks === null || typeof json.hooks !== "object" || Array.isArray(json.hooks))
|
|
312
|
+
) {
|
|
313
|
+
return [
|
|
314
|
+
refuseAndSuggest(
|
|
315
|
+
path,
|
|
316
|
+
JSON.stringify({ hooks: SUGGESTED_HOOKS }, null, 2) + "\n",
|
|
317
|
+
"`hooks` is not an object",
|
|
318
|
+
),
|
|
319
|
+
];
|
|
320
|
+
}
|
|
191
321
|
json.hooks = json.hooks ?? {};
|
|
192
322
|
const added = [];
|
|
193
323
|
|
|
@@ -209,7 +339,7 @@ export function ensureClaudeHooks(root) {
|
|
|
209
339
|
added.push("SessionStart:workflow-primer");
|
|
210
340
|
}
|
|
211
341
|
|
|
212
|
-
|
|
342
|
+
writeFileAtomic(path, JSON.stringify(json, null, 2) + "\n");
|
|
213
343
|
return added;
|
|
214
344
|
}
|
|
215
345
|
|
|
@@ -225,10 +355,10 @@ export function writeWorkflowMd(root) {
|
|
|
225
355
|
if (existsSync(path)) {
|
|
226
356
|
const cur = readFileSync(path, "utf8");
|
|
227
357
|
if (cur.trim() === WORKFLOW_MD.trim()) return false;
|
|
228
|
-
|
|
358
|
+
writeFileAtomic(path, WORKFLOW_MD);
|
|
229
359
|
return "refreshed";
|
|
230
360
|
}
|
|
231
|
-
|
|
361
|
+
writeFileAtomic(path, WORKFLOW_MD);
|
|
232
362
|
return true;
|
|
233
363
|
}
|
|
234
364
|
|
|
@@ -248,7 +378,8 @@ function blockIsCurrent(existing, block) {
|
|
|
248
378
|
const wanted = stampedDigest(block);
|
|
249
379
|
const have = stampedDigest(existing);
|
|
250
380
|
if (wanted && have) return wanted === have;
|
|
251
|
-
const region = (text) =>
|
|
381
|
+
const region = (text) =>
|
|
382
|
+
text.slice(text.indexOf("<!-- godxjp-ui:start"), text.indexOf("<!-- godxjp-ui:end -->"));
|
|
252
383
|
return region(existing).trim() === region(block).trim();
|
|
253
384
|
}
|
|
254
385
|
|
|
@@ -264,17 +395,22 @@ export function ensureClaudeMd(root) {
|
|
|
264
395
|
// reached nobody, and the block still read as current. A file written before digest stamping
|
|
265
396
|
// has no digest at all, so fall back to comparing the rendered body.
|
|
266
397
|
if (blockIsCurrent(existing, CLAUDE_MD_BLOCK)) return "present";
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
398
|
+
const next = refreshBlock(
|
|
399
|
+
existing,
|
|
400
|
+
CLAUDE_MD_BLOCK,
|
|
401
|
+
"<!-- godxjp-ui:start",
|
|
402
|
+
"<!-- godxjp-ui:end -->",
|
|
270
403
|
);
|
|
404
|
+
if (next === null)
|
|
405
|
+
return refuseAndSuggest(path, CLAUDE_MD_BLOCK, "godxjp-ui markers are broken");
|
|
406
|
+
writeFileAtomic(path, next);
|
|
271
407
|
return "refreshed";
|
|
272
408
|
}
|
|
273
409
|
if (existing == null) {
|
|
274
|
-
|
|
410
|
+
writeFileAtomic(path, CLAUDE_MD_BLOCK);
|
|
275
411
|
return "created";
|
|
276
412
|
}
|
|
277
|
-
|
|
413
|
+
writeFileAtomic(path, existing.replace(/\s*$/, "") + "\n\n" + CLAUDE_MD_BLOCK);
|
|
278
414
|
return "appended";
|
|
279
415
|
}
|
|
280
416
|
|
|
@@ -323,8 +459,8 @@ export function refreshGuineaPigSkill(root) {
|
|
|
323
459
|
const current = readFileSync(target, "utf8");
|
|
324
460
|
const marker = "\n---\n\n# 8. ";
|
|
325
461
|
const i = current.indexOf(marker);
|
|
326
|
-
|
|
327
|
-
|
|
462
|
+
writeFileAtomic(target, base.replace(/\s*$/, "") + "\n" + (i < 0 ? "" : current.slice(i)));
|
|
463
|
+
writeFileAtomic(optin, `${stamp}\n`);
|
|
328
464
|
return true;
|
|
329
465
|
}
|
|
330
466
|
|
|
@@ -370,7 +506,7 @@ export function ensureConsumerRules(root) {
|
|
|
370
506
|
return false;
|
|
371
507
|
}
|
|
372
508
|
mkdirSync(dir, { recursive: true });
|
|
373
|
-
|
|
509
|
+
writeFileAtomic(target, next);
|
|
374
510
|
|
|
375
511
|
// Prettier and this writer were fighting over the same file: the body holds aligned markdown
|
|
376
512
|
// tables, Prettier reformats them, the digest changes, the next install writes it back, and
|
|
@@ -383,7 +519,7 @@ export function ensureConsumerRules(root) {
|
|
|
383
519
|
(line) => !cur.includes(line),
|
|
384
520
|
);
|
|
385
521
|
if (owned.length) {
|
|
386
|
-
|
|
522
|
+
writeFileAtomic(
|
|
387
523
|
ignoreFile,
|
|
388
524
|
`${cur.replace(/\s*$/, "")}\n\n# Owned by @godxjp/ui — rewritten on every install, never hand-formatted.\n${owned.join("\n")}\n`,
|
|
389
525
|
);
|
|
@@ -396,7 +532,7 @@ export function ensureConsumerRules(root) {
|
|
|
396
532
|
if (existsSync(index)) {
|
|
397
533
|
const cur = readFileSync(index, "utf8");
|
|
398
534
|
if (!cur.includes(".ai/rules/godxjp-ui.md")) {
|
|
399
|
-
|
|
535
|
+
writeFileAtomic(
|
|
400
536
|
index,
|
|
401
537
|
cur.replace(/\s*$/, "") + `\n| ${uiDir}/** | .ai/rules/godxjp-ui.md |\n`,
|
|
402
538
|
);
|
package/scripts/consumer-rule.md
CHANGED
|
@@ -9,6 +9,22 @@
|
|
|
9
9
|
`docs/CONSUMER-RULES.md` (10 luật) và, với kho chuột bạch, ở
|
|
10
10
|
`.claude/skills/godx-ui-guinea-pig/SKILL.md`.
|
|
11
11
|
|
|
12
|
+
## Nạp style: HAI lối vào, và lối thứ hai không chở font
|
|
13
|
+
|
|
14
|
+
```css
|
|
15
|
+
@import "@godxjp/ui/styles"; /* mọi layer + Noto Sans JP / M PLUS 2 đóng gói sẵn */
|
|
16
|
+
@import "@godxjp/ui/styles/core"; /* CÙNG các layer ấy, KHÔNG một @font-face nào */
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Chọn `core` khi kho tự lo mặt chữ, hoặc khi không muốn chở font: `@fontsource` cắt
|
|
20
|
+
Noto Sans JP thành hàng trăm lát `unicode-range`, và trình duyệt chỉ biết cần lát nào
|
|
21
|
+
SAU khi đã dựng bố cục — một consumer đo được **737 lát / 13 MB**, gấp bảy lần toàn bộ
|
|
22
|
+
JavaScript của họ, cộng ~8 vòng tải mỗi lần chuyển màn.
|
|
23
|
+
|
|
24
|
+
`core` không phải cherry-pick: nó là một trong hai lối vào được hỗ trợ, và thứ tự layer
|
|
25
|
+
vẫn nguyên vẹn. Cherry-pick từng layer riêng lẻ thì vẫn cấm — đó là thứ làm vỡ hợp đồng
|
|
26
|
+
thứ tự, không phải việc chọn lối vào.
|
|
27
|
+
|
|
12
28
|
## Bố cục chuẩn của platform: BA CỘT, và ba cột là BA PHẠM VI
|
|
13
29
|
|
|
14
30
|
Vỏ mặc định của mọi app trên platform là ba cột, dựng bằng một `AppShell`:
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -20,13 +20,22 @@ if (skip) process.exit(0); // silent: CI / opt-out / self-install / no consumer
|
|
|
20
20
|
|
|
21
21
|
try {
|
|
22
22
|
const r = ensureMcpJson(root);
|
|
23
|
+
// A refusal is a full sentence, not one of the three status words — say it on its own line
|
|
24
|
+
// rather than folding it into "MCP in .mcp.json (…)", where it would read as a success.
|
|
25
|
+
if (r.startsWith("left untouched")) {
|
|
26
|
+
console.log(`\n @godxjp/ui → .mcp.json ${r}\n`);
|
|
27
|
+
}
|
|
23
28
|
// The mandate is plain text the agent reads every turn (CLAUDE.md block + workflow file). It
|
|
24
29
|
// changes nothing in the dev loop, so it is installed by default: an agent that never saw the
|
|
25
30
|
// but no mandate). Only the hooks — which DO change the loop — stay behind `init-agent`.
|
|
26
31
|
const md = ensureClaudeMd(root);
|
|
32
|
+
if (md.startsWith("left untouched")) {
|
|
33
|
+
console.log(` @godxjp/ui → CLAUDE.md ${md}\n`);
|
|
34
|
+
}
|
|
27
35
|
const wf = writeWorkflowMd(root);
|
|
28
36
|
const skill = refreshGuineaPigSkill(root);
|
|
29
37
|
const rules = ensureConsumerRules(root);
|
|
38
|
+
if (r.startsWith("left untouched") || md.startsWith("left untouched")) process.exit(0); // already reported
|
|
30
39
|
if (r === "present" && md === "present" && !wf && !skill && !rules) process.exit(0); // current — stay quiet
|
|
31
40
|
console.log(
|
|
32
41
|
`\n @godxjp/ui → MCP in .mcp.json (${r}); workflow mandate in CLAUDE.md (${md}).\n` +
|
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
|
|
@@ -173,7 +216,7 @@ const RULES = [
|
|
|
173
216
|
return /\brounded(?:-(?:full|sm|md|lg|xl|2xl))?\b/.test(cls) && /\b(?:border|bg-)/.test(cls);
|
|
174
217
|
},
|
|
175
218
|
message:
|
|
176
|
-
"Hand-rolled surface (rounded + border/bg) — use Card, Badge, Avatar, ListRow, Descriptions or EmptyState so height, padding and radius come from the tokens (docs/CONSUMER-RULES.md §4).",
|
|
219
|
+
"Hand-rolled surface (rounded + border/bg) — use Card, Badge, Avatar, ListRow, Descriptions or EmptyState so height, padding and radius come from the tokens (docs/CONSUMER-RULES.md §4). A read-only sample of a colour a USER chose is Swatch, which takes that value as a prop.",
|
|
177
220
|
},
|
|
178
221
|
{
|
|
179
222
|
id: "no-space-xy",
|
|
@@ -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
|
}
|
|
@@ -871,8 +911,7 @@ function staleOwnedRules() {
|
|
|
871
911
|
`This file is written by @godxjp/ui and says version ${stamped}, but the installed package ` +
|
|
872
912
|
`is ${installed}. Its rules describe a different library than the one you are building ` +
|
|
873
913
|
"against — most likely because `ignore-scripts=true` kept our postinstall from running.",
|
|
874
|
-
replacement:
|
|
875
|
-
"INIT_CWD=\"$PWD\" node node_modules/@godxjp/ui/scripts/postinstall.mjs",
|
|
914
|
+
replacement: 'INIT_CWD="$PWD" node node_modules/@godxjp/ui/scripts/postinstall.mjs',
|
|
876
915
|
snippet: `<!-- godxjp-ui:version ${stamped} --> vs installed ${installed}`,
|
|
877
916
|
};
|
|
878
917
|
}
|
|
@@ -881,12 +920,15 @@ const findings = [];
|
|
|
881
920
|
const stale = staleOwnedRules();
|
|
882
921
|
if (stale) findings.push(stale);
|
|
883
922
|
let filesScanned = 0;
|
|
923
|
+
/** What was actually OPENED. The summary used to name the selection instead (gh#542). */
|
|
924
|
+
const scannedFiles = [];
|
|
884
925
|
for (const dir of SCAN_DIRS) {
|
|
885
926
|
for (const file of walk(isAbsolute(dir) ? dir : join(CWD, dir))) {
|
|
886
927
|
const rel = relative(CWD, file);
|
|
887
928
|
// Framework test support is executable fixture markup, not a shipped product screen.
|
|
888
929
|
if (SELF && !args.includes("--consumer") && rel.startsWith("src/test/")) continue;
|
|
889
930
|
filesScanned += 1;
|
|
931
|
+
scannedFiles.push(rel);
|
|
890
932
|
// A primitive implements native controls; asking Input to render Input recurses.
|
|
891
933
|
// Consumer applications and executable docs still receive these composition checks.
|
|
892
934
|
const fileRules =
|
|
@@ -903,7 +945,7 @@ for (const dir of SCAN_DIRS) {
|
|
|
903
945
|
/** Both opt-outs, by line index: the per-line markers and the reason-carrying block. */
|
|
904
946
|
const suppressed = (ruleId, i) =>
|
|
905
947
|
isSuppressed(ruleId, origLines[i], origLines[i - 1]) || inDisabledBlock(ruleId, i);
|
|
906
|
-
const isJsx = file.endsWith(".tsx");
|
|
948
|
+
const isJsx = file.endsWith(".tsx") || file.endsWith(".jsx");
|
|
907
949
|
// This compiler output intentionally resolves CSS variables to email-safe literals.
|
|
908
950
|
// gen-email-tokens.mjs --check verifies it against its canonical token sources.
|
|
909
951
|
const compiledEmailTokens =
|
|
@@ -980,7 +1022,7 @@ for (const dir of SCAN_DIRS) {
|
|
|
980
1022
|
let depth = 0;
|
|
981
1023
|
const tag = /<(\/?)Card(Content|Header|Cover|Footer|Bar)?\b/g;
|
|
982
1024
|
tag.lastIndex = from;
|
|
983
|
-
for (let m; (m = tag.exec(scanContent));
|
|
1025
|
+
for (let m; (m = tag.exec(scanContent));) {
|
|
984
1026
|
const [, closing, slot] = m;
|
|
985
1027
|
if (!slot) {
|
|
986
1028
|
if (closing) {
|
|
@@ -1119,7 +1161,7 @@ if (filesScanned === 0) {
|
|
|
1119
1161
|
console.log(` ${C.dim}${f.snippet}${C.reset}`);
|
|
1120
1162
|
}
|
|
1121
1163
|
console.log(
|
|
1122
|
-
`\ngodxjp-ui audit: ${C.red}${errors.length} error(s)${C.reset}, ${C.yellow}${warnings.length} warning(s)${C.reset} across ${
|
|
1164
|
+
`\ngodxjp-ui audit: ${C.red}${errors.length} error(s)${C.reset}, ${C.yellow}${warnings.length} warning(s)${C.reset} across ${scannedFiles.join(", ")}.`,
|
|
1123
1165
|
);
|
|
1124
1166
|
if (errors.length === 0 && warnings.length === 0) {
|
|
1125
1167
|
console.log("✓ No UI-standardization violations found.");
|