@godxjp/ui 26.0.0 → 26.2.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/components/data-entry/select.d.ts +4 -1
- package/dist/components/layout/page-container.js +7 -1
- package/dist/contracts/measurement.json +1 -1
- package/dist/styles/control.css +8 -8
- package/dist/styles/float-button-layout.css +1 -1
- package/dist/styles/focus-ring.css +1 -1
- package/dist/styles/layout.css +23 -1
- package/dist/styles/shell-layout.css +9 -5
- package/dist/tokens/components/control.css +7 -6
- package/dist/tokens/components/float-button.css +1 -1
- package/dist/tokens/components/segmented.css +1 -1
- package/dist/tokens/components/shell.css +4 -4
- package/dist/tokens/foundation.css +4 -7
- package/docs/CUSTOMER-THEMING.md +4 -0
- package/docs/layout/app-shell-states.tsx +15 -5
- package/package.json +2 -2
- package/scripts/_agent-setup.mjs +23 -3
- package/scripts/cli.mjs +63 -2
|
@@ -26,7 +26,10 @@ export type SelectCompoundProp = FieldA11yProps & {
|
|
|
26
26
|
form?: string;
|
|
27
27
|
children?: React.ReactNode;
|
|
28
28
|
};
|
|
29
|
-
export type SelectProp = SelectDataProp | SelectCompoundProp
|
|
29
|
+
export type SelectProp = SelectDataProp | (SelectCompoundProp & {
|
|
30
|
+
mode?: undefined;
|
|
31
|
+
labelInValue?: undefined;
|
|
32
|
+
});
|
|
30
33
|
/**
|
|
31
34
|
* Select — one component for every single-select. Use the compound API for full control
|
|
32
35
|
* (`<Select><SelectTrigger/><SelectContent><SelectItem/></SelectContent></Select>`), OR the
|
|
@@ -33,6 +33,12 @@ function useFooterReveal(enabled) {
|
|
|
33
33
|
function PageContainerInset({ className, children, ...props }) {
|
|
34
34
|
return /* @__PURE__ */ jsx("div", { className: cn("ui-page-container-inset", className), ...props, children });
|
|
35
35
|
}
|
|
36
|
+
function footerPadStyle(footerPad) {
|
|
37
|
+
const style = padStyle(footerPad, void 0);
|
|
38
|
+
const end = style?.paddingInlineEnd ?? style?.padding;
|
|
39
|
+
if (end === void 0) return style;
|
|
40
|
+
return { ...style, paddingInlineEnd: `calc(${end} + var(--page-footer-content-slack))` };
|
|
41
|
+
}
|
|
36
42
|
function PageContainerRoot({
|
|
37
43
|
title,
|
|
38
44
|
subtitle,
|
|
@@ -150,7 +156,7 @@ function PageContainerRoot({
|
|
|
150
156
|
),
|
|
151
157
|
toolbar != null && /* @__PURE__ */ jsx("div", { className: "ui-page-toolbar", style: padStyle(toolbarPad, void 0), children: toolbar }),
|
|
152
158
|
children != null && /* @__PURE__ */ jsx("div", { className: "ui-page-body", children }),
|
|
153
|
-
footer && /* @__PURE__ */ jsx("footer", { className: "ui-page-footer", style:
|
|
159
|
+
footer && /* @__PURE__ */ jsx("footer", { className: "ui-page-footer", style: footerPadStyle(footerPad), children: footer })
|
|
154
160
|
]
|
|
155
161
|
}
|
|
156
162
|
);
|
|
@@ -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": "26.
|
|
3
|
+
"version": "26.2.0",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
package/dist/styles/control.css
CHANGED
|
@@ -1119,7 +1119,7 @@
|
|
|
1119
1119
|
}
|
|
1120
1120
|
|
|
1121
1121
|
.ui-segmented-item:not([data-state="checked"]):not([data-disabled]):hover {
|
|
1122
|
-
background: hsl(var(--segmented-item-hover-background));
|
|
1122
|
+
background: hsl(var(--segmented-item-hover-background, var(--accent)));
|
|
1123
1123
|
color: hsl(var(--segmented-item-hover-color));
|
|
1124
1124
|
}
|
|
1125
1125
|
.ui-segmented-item:not([data-state="checked"]):not([data-disabled]):active {
|
|
@@ -2722,7 +2722,7 @@
|
|
|
2722
2722
|
}
|
|
2723
2723
|
|
|
2724
2724
|
.ui-control--filled:hover:not(:disabled):not([readonly]):not([data-disabled]) {
|
|
2725
|
-
background-color: hsl(var(--control-variant-filled-hover-background));
|
|
2725
|
+
background-color: hsl(var(--control-variant-filled-hover-background, var(--accent)));
|
|
2726
2726
|
}
|
|
2727
2727
|
|
|
2728
2728
|
.ui-control--borderless {
|
|
@@ -2939,7 +2939,7 @@
|
|
|
2939
2939
|
}
|
|
2940
2940
|
|
|
2941
2941
|
.ui-slider-dot[data-active="true"] {
|
|
2942
|
-
border-color: hsl(var(--slider-dot-active-border-color));
|
|
2942
|
+
border-color: hsl(var(--slider-dot-active-border-color, var(--primary)));
|
|
2943
2943
|
}
|
|
2944
2944
|
|
|
2945
2945
|
.ui-slider-tooltip {
|
|
@@ -3053,14 +3053,14 @@
|
|
|
3053
3053
|
}
|
|
3054
3054
|
|
|
3055
3055
|
.ui-radio-button[data-state="checked"] {
|
|
3056
|
-
color: hsl(var(--choice-button-selected-color));
|
|
3057
|
-
border-color: hsl(var(--choice-button-selected-border-color));
|
|
3056
|
+
color: hsl(var(--choice-button-selected-color, var(--primary)));
|
|
3057
|
+
border-color: hsl(var(--choice-button-selected-border-color, var(--primary)));
|
|
3058
3058
|
}
|
|
3059
3059
|
|
|
3060
3060
|
.ui-radio-button-bar[data-button-style="solid"] .ui-radio-button[data-state="checked"] {
|
|
3061
|
-
color: hsl(var(--choice-button-solid-color));
|
|
3062
|
-
background: hsl(var(--choice-button-solid-background));
|
|
3063
|
-
border-color: hsl(var(--choice-button-solid-background));
|
|
3061
|
+
color: hsl(var(--choice-button-solid-color, var(--primary-foreground)));
|
|
3062
|
+
background: hsl(var(--choice-button-solid-background, var(--primary)));
|
|
3063
|
+
border-color: hsl(var(--choice-button-solid-background, var(--primary)));
|
|
3064
3064
|
}
|
|
3065
3065
|
|
|
3066
3066
|
.ui-radio-button[data-disabled] {
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
content: "";
|
|
144
144
|
inset: 0;
|
|
145
145
|
background: conic-gradient(
|
|
146
|
-
var(--float-button-progress-color) var(--float-button-progress-offset),
|
|
146
|
+
var(--float-button-progress-color, hsl(var(--primary))) var(--float-button-progress-offset),
|
|
147
147
|
var(--float-button-progress-track-color) var(--float-button-progress-offset)
|
|
148
148
|
);
|
|
149
149
|
mask: radial-gradient(
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
.ui-otp-slot[data-active="true"],
|
|
54
54
|
.ui-otp-group[data-appearance="grouped"]:has(.ui-otp-slot[data-active="true"]) {
|
|
55
55
|
outline: var(--focus-ring-width) solid
|
|
56
|
-
hsl(var(--focus-outline-color) / var(--focus-ring-opacity, 1));
|
|
56
|
+
hsl(var(--focus-outline-color, var(--focus-ring-color, var(--ring))) / var(--focus-ring-opacity, 1));
|
|
57
57
|
outline-offset: var(--focus-ring-offset);
|
|
58
58
|
|
|
59
59
|
transition:
|
package/dist/styles/layout.css
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
+
@property --page-footer-content-slack {
|
|
4
|
+
syntax: "<length-percentage>";
|
|
5
|
+
inherits: false;
|
|
6
|
+
initial-value: 0px;
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
@layer components {
|
|
4
10
|
|
|
5
11
|
.ui-separator {
|
|
@@ -90,7 +96,14 @@
|
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
.ui-brand-glow {
|
|
93
|
-
background-image: var(
|
|
99
|
+
background-image: var(
|
|
100
|
+
--brand-glow,
|
|
101
|
+
radial-gradient(
|
|
102
|
+
ellipse var(--brand-glow-size) at var(--brand-glow-position),
|
|
103
|
+
hsl(var(--brand-glow-color, var(--primary)) / var(--brand-glow-alpha)),
|
|
104
|
+
transparent 70%
|
|
105
|
+
)
|
|
106
|
+
);
|
|
94
107
|
pointer-events: none;
|
|
95
108
|
}
|
|
96
109
|
|
|
@@ -616,6 +629,14 @@
|
|
|
616
629
|
max-inline-size: var(--page-measure-medium);
|
|
617
630
|
}
|
|
618
631
|
|
|
632
|
+
.ui-page-container[data-measure="narrow"] .ui-page-footer {
|
|
633
|
+
--page-footer-content-slack: max(0px, 100% - var(--page-measure-narrow));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.ui-page-container[data-measure="medium"] .ui-page-footer {
|
|
637
|
+
--page-footer-content-slack: max(0px, 100% - var(--page-measure-medium));
|
|
638
|
+
}
|
|
639
|
+
|
|
619
640
|
.ui-page-container--flush .ui-page-toolbar,
|
|
620
641
|
.ui-page-container--flush .ui-page-body {
|
|
621
642
|
padding-inline-start: 0;
|
|
@@ -867,6 +888,7 @@
|
|
|
867
888
|
align-items: stretch;
|
|
868
889
|
gap: var(--space-inline-sm);
|
|
869
890
|
padding-top: var(--space-stack-md);
|
|
891
|
+
padding-inline-end: calc(var(--space-page-active-x) + var(--page-footer-content-slack));
|
|
870
892
|
border-top: var(--page-footer-divider, 1px solid hsl(var(--border)));
|
|
871
893
|
}
|
|
872
894
|
|
|
@@ -291,6 +291,10 @@
|
|
|
291
291
|
max-inline-size: var(--app-shell-page-max-width);
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
.app-main :where(.ui-page-footer) {
|
|
295
|
+
--page-footer-content-slack: max(0px, 100% - var(--app-shell-page-max-width));
|
|
296
|
+
}
|
|
297
|
+
|
|
294
298
|
.ui-auth-shell {
|
|
295
299
|
--control-height: var(--auth-shell-control-height);
|
|
296
300
|
--heading-h1: var(--auth-shell-heading-size);
|
|
@@ -1430,8 +1434,8 @@
|
|
|
1430
1434
|
}
|
|
1431
1435
|
|
|
1432
1436
|
.ui-app-launcher-tile:hover {
|
|
1433
|
-
background: hsl(var(--app-launcher-tile-hover-background));
|
|
1434
|
-
color: hsl(var(--app-launcher-tile-hover-color));
|
|
1437
|
+
background: hsl(var(--app-launcher-tile-hover-background, var(--accent)));
|
|
1438
|
+
color: hsl(var(--app-launcher-tile-hover-color, var(--accent-foreground)));
|
|
1435
1439
|
}
|
|
1436
1440
|
|
|
1437
1441
|
.ui-app-launcher-tile[aria-current="page"] {
|
|
@@ -2036,14 +2040,14 @@
|
|
|
2036
2040
|
}
|
|
2037
2041
|
|
|
2038
2042
|
.ui-topbar-item:hover {
|
|
2039
|
-
background: hsl(var(--topbar-item-hover-background));
|
|
2040
|
-
color: hsl(var(--topbar-item-hover-color));
|
|
2043
|
+
background: hsl(var(--topbar-item-hover-background, var(--accent)));
|
|
2044
|
+
color: hsl(var(--topbar-item-hover-color, var(--accent-foreground)));
|
|
2041
2045
|
}
|
|
2042
2046
|
|
|
2043
2047
|
.ui-topbar-item:active,
|
|
2044
2048
|
.ui-topbar-item[data-state="open"] {
|
|
2045
2049
|
background: hsl(var(--topbar-item-active-background));
|
|
2046
|
-
color: hsl(var(--topbar-item-hover-color));
|
|
2050
|
+
color: hsl(var(--topbar-item-hover-color, var(--accent-foreground)));
|
|
2047
2051
|
}
|
|
2048
2052
|
|
|
2049
2053
|
.ui-topbar-item:disabled,
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
--number-input-touch-height: var(--band-height-2xl);
|
|
36
36
|
|
|
37
37
|
--control-variant-filled-background: var(--muted);
|
|
38
|
-
--control-variant-filled-hover-background:
|
|
38
|
+
--control-variant-filled-hover-background: initial;
|
|
39
39
|
|
|
40
40
|
--control-count-font-size: var(--font-size-sm);
|
|
41
41
|
--control-count-color: var(--muted-foreground);
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
--slider-dot-size: var(--slider-track-height);
|
|
59
59
|
--slider-dot-background: var(--background);
|
|
60
60
|
--slider-dot-border-color: var(--border);
|
|
61
|
-
--slider-dot-active-border-color:
|
|
61
|
+
--slider-dot-active-border-color: initial;
|
|
62
62
|
--slider-tooltip-background: var(--popover);
|
|
63
63
|
--slider-tooltip-color: var(--popover-foreground);
|
|
64
64
|
--slider-tooltip-font-size: var(--font-size-xs);
|
|
@@ -69,10 +69,11 @@
|
|
|
69
69
|
--choice-button-background: var(--background);
|
|
70
70
|
--choice-button-color: var(--foreground);
|
|
71
71
|
--choice-button-border-color: var(--input);
|
|
72
|
-
|
|
73
|
-
--choice-button-selected-
|
|
74
|
-
--choice-button-
|
|
75
|
-
--choice-button-solid-
|
|
72
|
+
|
|
73
|
+
--choice-button-selected-color: initial;
|
|
74
|
+
--choice-button-selected-border-color: initial;
|
|
75
|
+
--choice-button-solid-background: initial;
|
|
76
|
+
--choice-button-solid-color: initial;
|
|
76
77
|
|
|
77
78
|
--toggle-focus-ring-width: var(--stroke-lg);
|
|
78
79
|
--toggle-focus-ring-alpha: 1;
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
--float-button-progress-offset: 0turn;
|
|
34
34
|
--float-button-progress-width: var(--space-1);
|
|
35
|
-
--float-button-progress-color:
|
|
35
|
+
--float-button-progress-color: initial;
|
|
36
36
|
--float-button-progress-track-color: hsl(var(--border));
|
|
37
37
|
|
|
38
38
|
--float-button-motion-translate: var(--float-button-size);
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
--segmented-item-hover-color: var(--foreground);
|
|
18
18
|
--segmented-item-selected-color: var(--foreground);
|
|
19
19
|
|
|
20
|
-
--segmented-item-hover-background:
|
|
20
|
+
--segmented-item-hover-background: initial;
|
|
21
21
|
--segmented-item-active-background: var(--secondary);
|
|
22
22
|
|
|
23
23
|
--segmented-item-selected-background: var(--background);
|
|
@@ -110,8 +110,8 @@
|
|
|
110
110
|
--topbar-item-radius: var(--radius-sharp);
|
|
111
111
|
--topbar-item-color: var(--muted-foreground);
|
|
112
112
|
|
|
113
|
-
--topbar-item-hover-background:
|
|
114
|
-
--topbar-item-hover-color:
|
|
113
|
+
--topbar-item-hover-background: initial;
|
|
114
|
+
--topbar-item-hover-color: initial;
|
|
115
115
|
--topbar-item-active-background: var(--secondary);
|
|
116
116
|
|
|
117
117
|
--topbar-item-badge-offset-block: var(--space-1);
|
|
@@ -158,8 +158,8 @@
|
|
|
158
158
|
--app-launcher-tile-min-height: var(--control-height-comfortable);
|
|
159
159
|
--app-launcher-tile-radius: var(--radius);
|
|
160
160
|
--app-launcher-tile-color: var(--foreground);
|
|
161
|
-
--app-launcher-tile-hover-background:
|
|
162
|
-
--app-launcher-tile-hover-color:
|
|
161
|
+
--app-launcher-tile-hover-background: initial;
|
|
162
|
+
--app-launcher-tile-hover-color: initial;
|
|
163
163
|
|
|
164
164
|
--app-launcher-tile-current-background: initial;
|
|
165
165
|
--app-launcher-mark-size: 2.5rem;
|
|
@@ -100,19 +100,16 @@
|
|
|
100
100
|
|
|
101
101
|
--shadow-glow: 0 0 0 0 transparent;
|
|
102
102
|
|
|
103
|
-
--brand-glow-color:
|
|
103
|
+
--brand-glow-color: initial;
|
|
104
104
|
--brand-glow-alpha: 0.18;
|
|
105
105
|
--brand-glow-size: 60% 50%;
|
|
106
106
|
--brand-glow-position: 50% 0%;
|
|
107
|
-
--brand-glow:
|
|
108
|
-
ellipse var(--brand-glow-size) at var(--brand-glow-position),
|
|
109
|
-
hsl(var(--brand-glow-color) / var(--brand-glow-alpha)),
|
|
110
|
-
transparent 70%
|
|
111
|
-
);
|
|
107
|
+
--brand-glow: initial;
|
|
112
108
|
|
|
113
109
|
--focus-outline: 1;
|
|
114
110
|
--focus-outline-weight: var(--stroke-hairline);
|
|
115
|
-
|
|
111
|
+
|
|
112
|
+
--focus-outline-color: initial;
|
|
116
113
|
|
|
117
114
|
--control-outline-width: calc(var(--stroke-md) * var(--focus-outline));
|
|
118
115
|
--focus-outline-offset: 0px;
|
package/docs/CUSTOMER-THEMING.md
CHANGED
|
@@ -243,6 +243,9 @@ The interaction states are **derived from the `--primary` in scope, at the eleme
|
|
|
243
243
|
| `--control-outline` | **yes** — the focused field's halo (`--control-outline-alpha` stays per theme) | light `h s calc(l - 5.3)`, dark `h calc(s * 0.99) calc(l - 25.1)` |
|
|
244
244
|
| `--primary-border` | **yes** (painted by no package surface; kept for the measurement in DESIGN-AUTHORITY) | light `h calc(s * 0.68) calc(l + 16)`, dark `h calc(s * 0.467) calc(l * 0.27043)` |
|
|
245
245
|
| `--sidebar-item-active-foreground` | **yes** — defaults to the live `--primary-active` | — |
|
|
246
|
+
| `--focus-outline-color` | **yes, through `--ring`** — every keyboard-focus outline (gh#687) | `var(--focus-ring-color, var(--ring))`, read at the focused element — so it follows the `--ring` you set in the scope |
|
|
247
|
+
| Radio button bar selected (`--choice-button-*`), Slider active dot, BackTop progress, `.ui-brand-glow` | **yes** (gh#687) | `var(--primary)` / `var(--primary-foreground)` at the call site |
|
|
248
|
+
| Topbar item / AppLauncher tile / Segmented / filled-control hover | **follow `--accent`** (gh#687) | `var(--accent)` / `var(--accent-foreground)` at the call site |
|
|
246
249
|
| `--primary-foreground` | **no — set it** | the label on a filled primary; you choose it for your seed |
|
|
247
250
|
| `--ring` | **only on the element that declares `--primary`** — set it in a nested scope | `var(--primary)` on `:root` / `.dark`. `--ring` is a public role read as `hsl(var(--ring))` in consumer CSS, so it cannot become a live default; a scope below `<html>` inherits the root's |
|
|
248
251
|
| `--destructive-*`, `--control-outline-error` | no — not brand | literals |
|
|
@@ -251,6 +254,7 @@ On the package seed these produce exactly the identity kit values the tier used
|
|
|
251
254
|
|
|
252
255
|
**Rules that come with it:**
|
|
253
256
|
|
|
257
|
+
- **No token tier binds to a scoped role at `:root`.** Every knob whose default is `--primary`, `--primary-foreground`, `--ring`, `--accent` or `--accent-foreground` is `initial`, with the role as the call-site fallback — `--ring` itself is the one exception (above). `tenant-scope-freeze-687.test.ts` holds it.
|
|
254
258
|
- **Override a state only by setting its knob** (`--primary-hover: 221 90% 72%`). A set knob wins over the derived default, in its own scope and below.
|
|
255
259
|
- **Read a state through its fallback, not bare.** The four knobs are `initial` so that the default can resolve at the painting element (docs/TOKENS.md, the freeze rule). `hsl(var(--primary-hover))` on its own therefore paints nothing — use the utility (`bg-primary-hover`) or `hsl(var(--primary-hover, from hsl(var(--primary)) var(--primary-hover-channels)))`.
|
|
256
260
|
- **Label polarity.** The theme's pair steps AWAY from that theme's default label: `darken` under a light label, `lighten` under a dark one, which keeps a label that clears 4.5:1 at rest at 4.5:1 in hover and pressed for every seed (measured, `derived-seed-sweep.test.ts`). If your seed needs the OTHER polarity of label (a pale yellow with dark text in the light theme), point the pair at it too — `applyPrimaryColor()` does this automatically from the label it picks:
|
|
@@ -3,6 +3,7 @@ import { AppShell, Flex, PageContainer, Sidebar, Topbar } from "@godxjp/ui/layou
|
|
|
3
3
|
import type { SidebarSectionProp } from "@godxjp/ui/layout";
|
|
4
4
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
|
|
5
5
|
import { Button, Text } from "@godxjp/ui/general";
|
|
6
|
+
import { Input } from "@godxjp/ui/data-entry";
|
|
6
7
|
import {
|
|
7
8
|
Bell,
|
|
8
9
|
Building2,
|
|
@@ -24,11 +25,13 @@ import {
|
|
|
24
25
|
* single page:
|
|
25
26
|
*
|
|
26
27
|
* ?topbarSpan=content|full ?navRail=start|end|top|bottom ?collapsed=1 ?sidebar=none
|
|
27
|
-
* ?responsive=docked ?measure=narrow|medium ?footer=1
|
|
28
|
+
* ?responsive=docked ?measure=narrow|medium ?footer=1|row
|
|
28
29
|
*
|
|
29
30
|
* `measure` and `footer` feed `scripts/check-app-shell-page-width.mjs` (gh#672): the page is fluid
|
|
30
31
|
* in the main column at every sidebar state, and a sticky footer band always spans `.app-main`,
|
|
31
32
|
* even when a service caps content with `--app-shell-page-max-width` or the page sets `measure`.
|
|
33
|
+
* The footer's CONTENT sits in the same column as the body (gh#682), so the two shapes a footer
|
|
34
|
+
* takes are both rendered: `footer=1` an end-aligned action group, `footer=row` a full-row composer.
|
|
32
35
|
*
|
|
33
36
|
* With no query it is `topbarSpan="full"` without a rail — the state gh#474 found collapsing
|
|
34
37
|
* `main` into a 170px implicit track below 900px, with the page actions laid out past the edge.
|
|
@@ -66,12 +69,12 @@ function readState() {
|
|
|
66
69
|
responsiveNavigation:
|
|
67
70
|
params.get("responsive") === "docked" ? ("docked" as const) : ("drawer" as const),
|
|
68
71
|
measure: (["narrow", "medium"] as const).find((m) => m === params.get("measure")),
|
|
69
|
-
|
|
72
|
+
footer: (["1", "row"] as const).find((f) => f === params.get("footer")),
|
|
70
73
|
};
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
export default function Demo() {
|
|
74
|
-
const [{ topbarSpan, navRail, collapsed, hasSidebar, responsiveNavigation, measure,
|
|
77
|
+
const [{ topbarSpan, navRail, collapsed, hasSidebar, responsiveNavigation, measure, footer }] =
|
|
75
78
|
useState(readState);
|
|
76
79
|
const [activeId, setActiveId] = useState("dashboard");
|
|
77
80
|
const strip = navRail === "top" || navRail === "bottom";
|
|
@@ -121,16 +124,23 @@ export default function Demo() {
|
|
|
121
124
|
subtitle="株式会社アクメ · 2026年5月"
|
|
122
125
|
measure={measure}
|
|
123
126
|
footer={
|
|
124
|
-
|
|
127
|
+
footer === "1" ? (
|
|
125
128
|
<Flex gap="sm" align="center" justify="end">
|
|
126
129
|
<Button size="sm" variant="outline">
|
|
127
130
|
下書き保存
|
|
128
131
|
</Button>
|
|
129
132
|
<Button size="sm">コメントを送信する</Button>
|
|
130
133
|
</Flex>
|
|
134
|
+
) : footer === "row" ? (
|
|
135
|
+
<Flex grow gap="sm" align="center">
|
|
136
|
+
<Flex grow>
|
|
137
|
+
<Input aria-label="コメント" placeholder="コメントを入力" />
|
|
138
|
+
</Flex>
|
|
139
|
+
<Button size="sm">送信</Button>
|
|
140
|
+
</Flex>
|
|
131
141
|
) : undefined
|
|
132
142
|
}
|
|
133
|
-
stickyFooter={
|
|
143
|
+
stickyFooter={footer !== undefined}
|
|
134
144
|
extra={
|
|
135
145
|
<Flex gap="sm" wrap>
|
|
136
146
|
<Button size="sm" variant="outline">
|
package/package.json
CHANGED
package/scripts/_agent-setup.mjs
CHANGED
|
@@ -45,6 +45,19 @@ function mcpEntryMatches(a, b) {
|
|
|
45
45
|
return keysA.every((k) => envA[k] === envB[k]);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* True when `entry` is the shape THIS package writes (`npx @godxjp/ui-mcp@<pin>`, env at most
|
|
50
|
+
* GODX_UI_VERSION), whatever pin and version it carries. Such an entry is ours to move forward on
|
|
51
|
+
* upgrade: treating it as custom left consumers on the MCP of the release they first installed
|
|
52
|
+
* (a `@godxjp/ui-mcp@25.4.0` pin survived the upgrade to 26.x).
|
|
53
|
+
*/
|
|
54
|
+
function isPackageWrittenMcpEntry(entry) {
|
|
55
|
+
if (entry?.command !== "npx") return false;
|
|
56
|
+
const args = entry.args ?? [];
|
|
57
|
+
if (args.length !== 1 || !/^@godxjp\/ui-mcp@[^\s]+$/.test(args[0])) return false;
|
|
58
|
+
return Object.keys(entry.env ?? {}).every((key) => key === "GODX_UI_VERSION");
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
function mcpConfigMismatchMessage(root, existing, expected) {
|
|
49
62
|
const ui = readConsumerUiMetadata(root);
|
|
50
63
|
const pin = expected.args?.[0] ?? "@godxjp/ui-mcp";
|
|
@@ -347,7 +360,11 @@ export function refreshBlock(current, next, startMarker, endMarker) {
|
|
|
347
360
|
if (!endMarker) return null;
|
|
348
361
|
const j = current.indexOf(endMarker, first);
|
|
349
362
|
if (j < 0) return null;
|
|
350
|
-
|
|
363
|
+
// `next` ends in its own newline, and the old block's newline is still the first character after
|
|
364
|
+
// the end marker — keeping both grew the file by one blank line on every refresh.
|
|
365
|
+
let tail = current.slice(j + endMarker.length);
|
|
366
|
+
if (next.endsWith("\n") && tail.startsWith("\n")) tail = tail.slice(1);
|
|
367
|
+
return current.slice(0, first) + next + tail;
|
|
351
368
|
}
|
|
352
369
|
|
|
353
370
|
export function ensureMcpJson(root) {
|
|
@@ -369,10 +386,13 @@ export function ensureMcpJson(root) {
|
|
|
369
386
|
}
|
|
370
387
|
json.mcpServers = json.mcpServers ?? {};
|
|
371
388
|
if (json.mcpServers[MCP_KEY]) {
|
|
372
|
-
if (
|
|
389
|
+
if (mcpEntryMatches(json.mcpServers[MCP_KEY], expected)) return "present";
|
|
390
|
+
if (!isPackageWrittenMcpEntry(json.mcpServers[MCP_KEY])) {
|
|
373
391
|
return mcpConfigMismatchMessage(root, json.mcpServers[MCP_KEY], expected);
|
|
374
392
|
}
|
|
375
|
-
|
|
393
|
+
json.mcpServers[MCP_KEY] = expected;
|
|
394
|
+
writeFileAtomic(path, JSON.stringify(json, null, 2) + "\n");
|
|
395
|
+
return "refreshed";
|
|
376
396
|
}
|
|
377
397
|
const created = read.state === "missing";
|
|
378
398
|
json.mcpServers[MCP_KEY] = expected;
|
package/scripts/cli.mjs
CHANGED
|
@@ -18,13 +18,74 @@ const MAP = {
|
|
|
18
18
|
"visual-audit": "visual-audit.mjs",
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
// `<command> --help` is answered HERE: the scripts take free positional arguments, so a `--help`
|
|
22
|
+
// forwarded to ui-audit used to be read as nothing and ran a full audit instead of printing help.
|
|
23
|
+
const HELP = {
|
|
24
|
+
"init-agent": `godxjp-ui init-agent
|
|
25
|
+
|
|
26
|
+
Install the agent forcing-kit into the current consumer app (run it from the app root):
|
|
27
|
+
.mcp.json the godx-ui MCP server
|
|
28
|
+
.claude/settings.json a PostToolUse hook that audits every Write/Edit of a .tsx file
|
|
29
|
+
.claude/godxjp-ui-workflow.md
|
|
30
|
+
CLAUDE.md the godxjp-ui mandate block
|
|
31
|
+
Idempotent. Refuses inside the @godxjp/ui repo itself. Restart the agent afterwards.`,
|
|
32
|
+
"sync-rules": `godxjp-ui sync-rules
|
|
33
|
+
|
|
34
|
+
Refresh the package-owned agent files for the installed @godxjp/ui version: the godx-ui entry in
|
|
35
|
+
.mcp.json, the managed CLAUDE.md block, .claude/godxjp-ui-workflow.md and .ai/rules/godxjp-ui.md.
|
|
36
|
+
It is the postinstall step, run by hand — use it when the app installs with ignore-scripts=true,
|
|
37
|
+
where postinstall never runs and the rules go stale. Silent no-op in CI and when opted out.`,
|
|
38
|
+
audit: `godxjp-ui audit [dir …] [--changed] [--format json] [--quiet] [--rules]
|
|
39
|
+
|
|
40
|
+
Static UI-standardization audit over source (regex, no browser, fast). Exits non-zero on errors.
|
|
41
|
+
dir … directories or files to scan
|
|
42
|
+
--changed scan what this branch changed vs origin/main (committed, staged, untracked);
|
|
43
|
+
fails rather than reporting clean when origin/main cannot be resolved
|
|
44
|
+
--format json machine-readable findings
|
|
45
|
+
--quiet print errors only (warnings hidden)
|
|
46
|
+
--rules print the rule catalog as JSON and exit
|
|
47
|
+
Pre-commit: npx godxjp-ui audit resources/js || exit 1`,
|
|
48
|
+
"visual-audit": `godxjp-ui visual-audit [--format json] [--strict] <baseUrl> [route …]
|
|
49
|
+
|
|
50
|
+
Runtime audit (Playwright + axe-core) against an app you are ALREADY running locally.
|
|
51
|
+
baseUrl the running app, e.g. http://localhost:5173
|
|
52
|
+
route … paths appended to baseUrl (default "/")
|
|
53
|
+
--strict exit non-zero on any finding, not only errors
|
|
54
|
+
--format json machine-readable findings
|
|
55
|
+
--rules print the visual rule catalog as JSON and exit
|
|
56
|
+
Needs the playwright peer installed.`,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const USAGE = `usage: godxjp-ui <command> [args]
|
|
60
|
+
|
|
61
|
+
commands:
|
|
62
|
+
init-agent install the agent forcing-kit (MCP + auto-audit hook + CLAUDE.md mandate)
|
|
63
|
+
sync-rules refresh package-owned agent rules (postinstall, by hand)
|
|
64
|
+
audit static UI audit over source
|
|
65
|
+
visual-audit runtime audit (Playwright + axe-core) against a running app
|
|
66
|
+
|
|
67
|
+
godxjp-ui <command> --help details and flags for one command`;
|
|
68
|
+
|
|
69
|
+
const isHelp = (arg) => arg === "--help" || arg === "-h" || arg === "help";
|
|
21
70
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
71
|
+
|
|
72
|
+
if (cmd === undefined || isHelp(cmd)) {
|
|
73
|
+
const topic = isHelp(cmd) ? rest[0] : undefined;
|
|
74
|
+
console.log(topic && HELP[topic] ? HELP[topic] : USAGE);
|
|
75
|
+
process.exit(cmd === undefined ? 1 : 0);
|
|
76
|
+
}
|
|
22
77
|
const script = MAP[cmd];
|
|
23
78
|
if (!script) {
|
|
24
|
-
console.error(
|
|
79
|
+
console.error(`unknown command: ${cmd}\n\n${USAGE}`);
|
|
25
80
|
process.exit(1);
|
|
26
81
|
}
|
|
82
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
83
|
+
console.log(HELP[cmd]);
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
27
86
|
const env =
|
|
28
|
-
cmd === "sync-rules"
|
|
87
|
+
cmd === "sync-rules"
|
|
88
|
+
? { ...process.env, INIT_CWD: process.env.INIT_CWD ?? process.cwd() }
|
|
89
|
+
: process.env;
|
|
29
90
|
const r = spawnSync("node", [join(HERE, script), ...rest], { stdio: "inherit", env });
|
|
30
91
|
process.exit(r.status ?? 0);
|