@godxjp/ui 23.4.9 → 23.4.11
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/timeline.js +8 -5
- package/dist/components/data-entry/upload-crop-dialog.js +3 -2
- package/dist/components/data-entry/upload.js +3 -3
- package/dist/components/feedback/dialog.js +18 -16
- package/dist/components/layout/app-launcher.js +2 -1
- package/dist/contracts/measurement.json +12 -1
- 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/styles/control.css +7 -0
- package/docs/FRAME-COVERAGE-REPORT.md +22 -7
- package/docs/data-display/badge.tsx +1 -1
- package/docs/data-display/card/index.tsx +1 -1
- package/docs/data-display/credential-reveal.tsx +9 -6
- package/docs/data-display/popover.tsx +1 -1
- package/docs/data-entry/chat-suggestion.tsx +1 -1
- package/docs/data-entry/form-field/index.tsx +11 -8
- package/docs/data-entry/segmented.tsx +2 -2
- package/docs/feedback/dialog.tsx +21 -12
- package/docs/layout/app-launcher.tsx +2 -2
- package/package.json +5 -3
- package/scripts/ui-audit.mjs +113 -4
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useTranslation } from "../../i18n/use-translation.js";
|
|
2
4
|
import { Check, CheckCircle2, Plane } from "lucide-react";
|
|
3
|
-
const
|
|
4
|
-
done: "
|
|
5
|
-
current: "
|
|
6
|
-
pending: "
|
|
5
|
+
const SR_PREFIX_KEY = {
|
|
6
|
+
done: "dataDisplay.timeline.statusDone",
|
|
7
|
+
current: "dataDisplay.timeline.statusCurrent",
|
|
8
|
+
pending: "dataDisplay.timeline.statusPending"
|
|
7
9
|
};
|
|
8
10
|
function resolveStatus(item) {
|
|
9
11
|
if (item.status) {
|
|
@@ -15,6 +17,7 @@ function resolveStatus(item) {
|
|
|
15
17
|
return "done";
|
|
16
18
|
}
|
|
17
19
|
function Timeline({ items, variant = "icon" }) {
|
|
20
|
+
const { t } = useTranslation();
|
|
18
21
|
return /* @__PURE__ */ jsx("ol", { className: "ui-timeline", "data-variant": variant, children: items.map((item, index) => {
|
|
19
22
|
const status = resolveStatus(item);
|
|
20
23
|
const isCurrent = status === "current";
|
|
@@ -66,7 +69,7 @@ function Timeline({ items, variant = "icon" }) {
|
|
|
66
69
|
/* @__PURE__ */ jsxs("div", { className: "ui-timeline-body", children: [
|
|
67
70
|
/* @__PURE__ */ jsxs("div", { className: "ui-timeline-head", children: [
|
|
68
71
|
/* @__PURE__ */ jsxs("span", { className: "ui-timeline-title", "data-current": isCurrent ? "true" : void 0, children: [
|
|
69
|
-
/* @__PURE__ */ jsx("span", { className: "sr-only", children:
|
|
72
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: t(SR_PREFIX_KEY[status]) }),
|
|
70
73
|
item.title
|
|
71
74
|
] }),
|
|
72
75
|
item.time ? /* @__PURE__ */ jsx("span", { className: "ui-timeline-time", children: item.time }) : null
|
|
@@ -6,6 +6,7 @@ import { cn } from "../../lib/utils.js";
|
|
|
6
6
|
import { Button } from "../general/button.js";
|
|
7
7
|
import {
|
|
8
8
|
Dialog,
|
|
9
|
+
DialogBody,
|
|
9
10
|
DialogContent,
|
|
10
11
|
DialogDescription,
|
|
11
12
|
DialogFooter,
|
|
@@ -57,7 +58,7 @@ function UploadCropDialog({ open, onOpenChange, file, onConfirm }) {
|
|
|
57
58
|
/* @__PURE__ */ jsx(DialogTitle, { children: t("dataEntry.upload.cropTitle") }),
|
|
58
59
|
/* @__PURE__ */ jsx(DialogDescription, { children: t("dataEntry.upload.cropDescription") })
|
|
59
60
|
] }),
|
|
60
|
-
/* @__PURE__ */ jsxs("div", { className: "ui-stack-sm", children: [
|
|
61
|
+
/* @__PURE__ */ jsx(DialogBody, { children: /* @__PURE__ */ jsxs("div", { className: "ui-stack-sm", children: [
|
|
61
62
|
/* @__PURE__ */ jsx(
|
|
62
63
|
"div",
|
|
63
64
|
{
|
|
@@ -91,7 +92,7 @@ function UploadCropDialog({ open, onOpenChange, file, onConfirm }) {
|
|
|
91
92
|
}
|
|
92
93
|
)
|
|
93
94
|
] })
|
|
94
|
-
] }),
|
|
95
|
+
] }) }),
|
|
95
96
|
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
96
97
|
/* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: () => onOpenChange(false), children: t("common.cancel") }),
|
|
97
98
|
/* @__PURE__ */ jsx(Button, { onClick: () => void handleConfirm(), children: t("dataEntry.upload.cropConfirm") })
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from "./upload-types.js";
|
|
26
26
|
import { readDroppedFiles } from "./upload-files.js";
|
|
27
27
|
import { uploadRequest } from "./upload-request.js";
|
|
28
|
-
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../feedback/dialog.js";
|
|
28
|
+
import { Dialog, DialogBody, DialogContent, DialogHeader, DialogTitle } from "../feedback/dialog.js";
|
|
29
29
|
import { Progress } from "../data-display/progress.js";
|
|
30
30
|
import { useUploadDraft } from "./use-upload-draft.js";
|
|
31
31
|
import { collectUploadCommitActions, createUploadItem as createUploadItem2, UPLOAD_LIST_IGNORE as UPLOAD_LIST_IGNORE2 } from "./upload-types.js";
|
|
@@ -414,14 +414,14 @@ function Upload({
|
|
|
414
414
|
},
|
|
415
415
|
children: /* @__PURE__ */ jsxs(DialogContent, { "aria-describedby": void 0, children: [
|
|
416
416
|
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: previewItem?.name ?? t("dataEntry.upload.preview") }) }),
|
|
417
|
-
previewItem?.previewUrl && /* @__PURE__ */ jsx(
|
|
417
|
+
/* @__PURE__ */ jsx(DialogBody, { children: previewItem?.previewUrl && /* @__PURE__ */ jsx(
|
|
418
418
|
"img",
|
|
419
419
|
{
|
|
420
420
|
src: previewItem.previewUrl,
|
|
421
421
|
alt: previewItem.name,
|
|
422
422
|
className: "ui-upload-picture-img"
|
|
423
423
|
}
|
|
424
|
-
)
|
|
424
|
+
) })
|
|
425
425
|
] })
|
|
426
426
|
}
|
|
427
427
|
),
|
|
@@ -436,22 +436,24 @@ function AlertDialog({
|
|
|
436
436
|
description ? /* @__PURE__ */ jsx(AlertDialogDescription, { children: description }) : null
|
|
437
437
|
] })
|
|
438
438
|
] }) }),
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
439
|
+
/* @__PURE__ */ jsxs(DialogBody, { children: [
|
|
440
|
+
needsPhrase && /* @__PURE__ */ jsx(FormField, { id: inputId, label: t("common.typeToConfirm", { phrase }), children: /* @__PURE__ */ jsx(
|
|
441
|
+
Input,
|
|
442
|
+
{
|
|
443
|
+
id: inputId,
|
|
444
|
+
value: typed,
|
|
445
|
+
onChange: (e) => {
|
|
446
|
+
setTyped(e.target.value);
|
|
447
|
+
},
|
|
448
|
+
autoComplete: "off",
|
|
449
|
+
spellCheck: false,
|
|
450
|
+
placeholder: phrase,
|
|
451
|
+
"aria-required": "true",
|
|
452
|
+
disabled: busy
|
|
453
|
+
}
|
|
454
|
+
) }),
|
|
455
|
+
stepUpFailed && /* @__PURE__ */ jsx("p", { id: stepErrorId, role: "alert", className: "ui-dialog-step-up-error", children: t("feedback.alert.stepUpFailed") })
|
|
456
|
+
] }),
|
|
455
457
|
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
456
458
|
/* @__PURE__ */ jsx(DialogCancel, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", disabled: busy, children: resolvedCancel }) }),
|
|
457
459
|
/* @__PURE__ */ jsx(
|
|
@@ -7,6 +7,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "../data-display/popover
|
|
|
7
7
|
import {
|
|
8
8
|
Dialog,
|
|
9
9
|
DialogContent,
|
|
10
|
+
DialogBody,
|
|
10
11
|
DialogHeader,
|
|
11
12
|
DialogTitle,
|
|
12
13
|
DialogTrigger
|
|
@@ -217,7 +218,7 @@ function AppLauncher({
|
|
|
217
218
|
overlayClassName: "ui-app-launcher-launchpad-overlay",
|
|
218
219
|
children: [
|
|
219
220
|
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: labels.title }) }),
|
|
220
|
-
panel
|
|
221
|
+
/* @__PURE__ */ jsx(DialogBody, { children: panel })
|
|
221
222
|
]
|
|
222
223
|
}
|
|
223
224
|
)
|
|
@@ -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": "23.4.
|
|
3
|
+
"version": "23.4.11",
|
|
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,
|
|
@@ -39,6 +39,17 @@
|
|
|
39
39
|
"components": [
|
|
40
40
|
"number-input"
|
|
41
41
|
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"selector": ".ui-switch",
|
|
45
|
+
"via": ".ui-choice-input",
|
|
46
|
+
"source": "src/styles/control.css",
|
|
47
|
+
"targetMin": 24,
|
|
48
|
+
"components": [
|
|
49
|
+
"label",
|
|
50
|
+
"slider",
|
|
51
|
+
"switch"
|
|
52
|
+
]
|
|
42
53
|
}
|
|
43
54
|
]
|
|
44
55
|
},
|
package/dist/styles/control.css
CHANGED
|
@@ -500,6 +500,13 @@
|
|
|
500
500
|
transition-duration: 150ms;
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
.ui-switch > .ui-choice-input {
|
|
504
|
+
inset-block-start: 50%;
|
|
505
|
+
inset-block-end: auto;
|
|
506
|
+
block-size: max(100%, var(--touch-target-min));
|
|
507
|
+
translate: 0 -50%;
|
|
508
|
+
}
|
|
509
|
+
|
|
503
510
|
.ui-switch[data-state="checked"] {
|
|
504
511
|
background: var(--switch-checked-background, hsl(var(--primary)));
|
|
505
512
|
}
|
|
@@ -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: **
|
|
6
|
+
- Public components: **163**
|
|
7
|
+
- With ≥1 frame: **142** · zero-frame: **21**
|
|
8
8
|
- Fully covered (every contract axis declared): **0**
|
|
9
9
|
|
|
10
10
|
## UNTESTED — no `/frame/**` route
|
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
- [ ] `CardBar`
|
|
17
17
|
- [ ] `VisuallyHidden`
|
|
18
18
|
- [ ] `RangeTimeline`
|
|
19
|
+
- [ ] `Title`
|
|
20
|
+
- [ ] `Paragraph`
|
|
21
|
+
- [ ] `Link`
|
|
19
22
|
- [ ] `ServiceCatalogCta`
|
|
23
|
+
- [ ] `SkeletonRows`
|
|
20
24
|
- [ ] `OverlayPortalProvider`
|
|
21
25
|
- [ ] `SkeletonArticle`
|
|
22
26
|
- [ ] `SkeletonAvatar`
|
|
@@ -54,14 +58,19 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
|
|
|
54
58
|
| TopbarItem | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
55
59
|
| NavList | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
56
60
|
| MasterDetail | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
61
|
+
| DraggablePanel | ✓ | N/A | · | N/A | · | · | · | · | · | · |
|
|
57
62
|
| SplitPane | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
58
63
|
| ErrorSurface | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
|
|
59
64
|
| LegalDocumentShell | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
60
65
|
| Breadcrumb | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
61
|
-
| FloatButton | ✓ |
|
|
62
|
-
| Button | ✓ | ✓ | · |
|
|
63
|
-
| Text | ✓ | ✓ |
|
|
66
|
+
| FloatButton | ✓ | ✓ | · | N/A | · | · | · | · | · | · |
|
|
67
|
+
| Button | ✓ | ✓ | · | N/A | · | · | · | · | · | · |
|
|
68
|
+
| Text | ✓ | ✓ | · | N/A | · | · | · | · | · | · |
|
|
64
69
|
| Heading | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
|
|
70
|
+
| Typography | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
71
|
+
| Title | **✗** | · | · | N/A | · | · | · | · | · | · |
|
|
72
|
+
| Paragraph | **✗** | · | · | N/A | · | · | · | · | · | · |
|
|
73
|
+
| Link | **✗** | · | · | N/A | · | · | · | · | · | · |
|
|
65
74
|
| Logo | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
|
|
66
75
|
| Reveal | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
67
76
|
| Activity | ✓ | · | N/A | N/A | · | · | · | · | · | · |
|
|
@@ -102,13 +111,14 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
|
|
|
102
111
|
| Checkbox | ✓ | N/A | · | · | · | · | · | · | · | · |
|
|
103
112
|
| RadioGroup | ✓ | N/A | · | · | · | · | · | · | · | · |
|
|
104
113
|
| DatePicker | ✓ | · | · | · | · | · | · | · | · | · |
|
|
105
|
-
| Dialog | ✓ |
|
|
114
|
+
| Dialog | ✓ | ✓ | N/A | · | · | · | · | · | · | · |
|
|
106
115
|
| AlertDialog | ✓ | ✓ | · | · | · | · | · | · | · | · |
|
|
107
116
|
| AlertDialogRoot | ✓ | N/A | N/A | · | · | · | · | · | · | · |
|
|
108
117
|
| Sheet | ✓ | N/A | N/A | · | · | · | · | · | · | · |
|
|
109
118
|
| Alert | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
|
|
110
119
|
| Banner | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
|
|
111
120
|
| SkeletonTable | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
121
|
+
| SkeletonRows | **✗** | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
112
122
|
| Toaster | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
113
123
|
| Tabs | ✓ | ✓ | N/A | · | · | · | · | · | · | · |
|
|
114
124
|
| Pagination | ✓ | · | · | · | · | · | · | · | · | · |
|
|
@@ -153,7 +163,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
|
|
|
153
163
|
| HoverCard | ✓ | N/A | N/A | · | · | · | · | · | · | · |
|
|
154
164
|
| PasswordInput | ✓ | ✓ | · | · | · | · | · | · | · | · |
|
|
155
165
|
| PasswordStrength | ✓ | N/A | N/A | · | · | · | · | · | · | · |
|
|
156
|
-
| InputOTP | ✓ | · |
|
|
166
|
+
| InputOTP | ✓ | · | N/A | · | · | · | · | · | · | · |
|
|
157
167
|
| Rating | ✓ | N/A | · | · | · | · | · | · | · | · |
|
|
158
168
|
| Segmented | ✓ | ✓ | · | · | · | · | · | · | · | · |
|
|
159
169
|
| TagInput | ✓ | ✓ | · | · | · | · | · | · | · | · |
|
|
@@ -185,4 +195,9 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
|
|
|
185
195
|
| ChatBubbleList | **✗** | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
186
196
|
| ChatComposer | ✓ | · | · | · | · | · | · | · | · | · |
|
|
187
197
|
| ChatSuggestion | ✓ | N/A | N/A | · | · | · | · | · | · | · |
|
|
198
|
+
| Conversations | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
199
|
+
| Welcome | ✓ | · | N/A | N/A | · | · | · | · | · | · |
|
|
200
|
+
| Actions | ✓ | · | N/A | N/A | · | · | · | · | · | · |
|
|
201
|
+
| ThoughtChain | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
202
|
+
| Attachments | ✓ | N/A | · | N/A | · | · | · | · | · | · |
|
|
188
203
|
|
|
@@ -267,7 +267,7 @@ export default function Demo() {
|
|
|
267
267
|
<CardTitle level={2}>tabular · 数字を桁で揃える</CardTitle>
|
|
268
268
|
<CardDescription>
|
|
269
269
|
件数のチップが縦に並ぶときは tabular を。プロポーショナル数字だと 1 が 0
|
|
270
|
-
|
|
270
|
+
より狭いので、列で見たときに桁がずれてチップ幅も揺れます。既定はオフ。語を載せる
|
|
271
271
|
チップまで幅の広い数字を払う必要はありません。
|
|
272
272
|
</CardDescription>
|
|
273
273
|
</CardHeader>
|
|
@@ -128,7 +128,7 @@ export default function Demo() {
|
|
|
128
128
|
|
|
129
129
|
<Flex direction="col" gap="sm">
|
|
130
130
|
<Text as="div" weight="medium">
|
|
131
|
-
hoverable · actions
|
|
131
|
+
hoverable · actions · Ant Design の hoverable と actions
|
|
132
132
|
</Text>
|
|
133
133
|
<ResponsiveGrid columns={{ sm: 1, md: 2 }}>
|
|
134
134
|
{/*
|
|
@@ -11,6 +11,7 @@ import { Button } from "@godxjp/ui/general";
|
|
|
11
11
|
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
12
12
|
import {
|
|
13
13
|
Dialog,
|
|
14
|
+
DialogBody,
|
|
14
15
|
DialogContent,
|
|
15
16
|
DialogDescription,
|
|
16
17
|
DialogHeader,
|
|
@@ -51,12 +52,14 @@ export default function Demo() {
|
|
|
51
52
|
このキーはこの画面でのみ表示されます。安全な場所に保管してください。
|
|
52
53
|
</DialogDescription>
|
|
53
54
|
</DialogHeader>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
<DialogBody>
|
|
56
|
+
<CredentialReveal
|
|
57
|
+
label="APIキー"
|
|
58
|
+
secret={API_KEY}
|
|
59
|
+
downloadable
|
|
60
|
+
onAcknowledge={() => setOpen(false)}
|
|
61
|
+
/>
|
|
62
|
+
</DialogBody>
|
|
60
63
|
</DialogContent>
|
|
61
64
|
</Dialog>
|
|
62
65
|
</CardContent>
|
|
@@ -210,7 +210,7 @@ export default function Demo() {
|
|
|
210
210
|
<CardHeader>
|
|
211
211
|
<CardTitle level={2}>width · パネルの測り方</CardTitle>
|
|
212
212
|
<CardDescription>
|
|
213
|
-
既定の panel は --popover-width(18rem)。auto
|
|
213
|
+
既定の panel は --popover-width(18rem)。auto は中身に測らせます。自前の幅を持つもの
|
|
214
214
|
(Calendar、チャート、固定幅のプレビュー)はこれ以外に正解がありません。trigger
|
|
215
215
|
はアンカーに合わせます。className に w-*
|
|
216
216
|
を書くのは呼び出し側の定数で、どのテーマからも retune できません。flush が padding
|
|
@@ -166,7 +166,7 @@ export default function Demo() {
|
|
|
166
166
|
</CardTitle>
|
|
167
167
|
<CardDescription>
|
|
168
168
|
トリガー文字は差し替えられます。1つの入力欄に対して1つの
|
|
169
|
-
ChatSuggestion
|
|
169
|
+
ChatSuggestion。2種類を同時に見張るのではなく、用途ごとに別の欄にします。
|
|
170
170
|
</CardDescription>
|
|
171
171
|
</CardHeader>
|
|
172
172
|
<CardContent>
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
DialogHeader,
|
|
9
9
|
DialogTitle,
|
|
10
10
|
Sheet,
|
|
11
|
+
SheetBody,
|
|
11
12
|
SheetContent,
|
|
12
13
|
SheetHeader,
|
|
13
14
|
SheetTitle,
|
|
@@ -312,14 +313,16 @@ export default function Demo() {
|
|
|
312
313
|
<SheetHeader>
|
|
313
314
|
<SheetTitle>請求先設定</SheetTitle>
|
|
314
315
|
</SheetHeader>
|
|
315
|
-
<
|
|
316
|
-
<
|
|
317
|
-
<
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
<
|
|
321
|
-
|
|
322
|
-
|
|
316
|
+
<SheetBody>
|
|
317
|
+
<Form>
|
|
318
|
+
<FormField id="sheet-name" label="宛名" required>
|
|
319
|
+
<Input id="sheet-name" />
|
|
320
|
+
</FormField>
|
|
321
|
+
<FormField id="sheet-note" label="備考">
|
|
322
|
+
<Input id="sheet-note" />
|
|
323
|
+
</FormField>
|
|
324
|
+
</Form>
|
|
325
|
+
</SheetBody>
|
|
323
326
|
</SheetContent>
|
|
324
327
|
</Sheet>
|
|
325
328
|
</CardContent>
|
|
@@ -89,7 +89,7 @@ export default function Demo() {
|
|
|
89
89
|
<CardTitle level={2}>4 択 + 件数(スマホ幅では折り返す)</CardTitle>
|
|
90
90
|
<CardDescription>
|
|
91
91
|
一覧の上の状態フィルター。4 つが 1 行に収まらない幅では、トラックが 2 行目に折り返し
|
|
92
|
-
|
|
92
|
+
ます。ラベルも件数も切り詰めません。`check:segmented-wrap` がこのカードを測ります。
|
|
93
93
|
</CardDescription>
|
|
94
94
|
</CardHeader>
|
|
95
95
|
<CardContent>
|
|
@@ -112,7 +112,7 @@ export default function Demo() {
|
|
|
112
112
|
<CardDescription>
|
|
113
113
|
size は 3 段(sm / md / lg)。トラックの高さは --control-height そのもので、
|
|
114
114
|
MobileShell のようにその変数をスコープする領域では自動的にタッチ段(44px)になります。
|
|
115
|
-
vertical では 1 行ぶんが 1
|
|
115
|
+
vertical では 1 行ぶんが 1 コントロールの高さになります。横 1 行のときだけトラック
|
|
116
116
|
内側の余白を引くので、積んだときは引きません。block は幅いっぱいに広げます。
|
|
117
117
|
</CardDescription>
|
|
118
118
|
</CardHeader>
|
package/docs/feedback/dialog.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
Dialog,
|
|
4
4
|
DialogAction,
|
|
5
5
|
DialogCancel,
|
|
6
|
+
DialogBody,
|
|
6
7
|
DialogContent,
|
|
7
8
|
DialogDescription,
|
|
8
9
|
DialogFooter,
|
|
@@ -72,6 +73,7 @@ export default function Demo() {
|
|
|
72
73
|
借方・貸方科目と金額を入力して保存してください。
|
|
73
74
|
</DialogDescription>
|
|
74
75
|
</DialogHeader>
|
|
76
|
+
<DialogBody>
|
|
75
77
|
<Flex direction="col" gap="md">
|
|
76
78
|
<Flex direction="row" gap="sm" align="center">
|
|
77
79
|
<Text tone="muted" className="w-20 shrink-0">
|
|
@@ -92,6 +94,7 @@ export default function Demo() {
|
|
|
92
94
|
<Text>¥ 120,000</Text>
|
|
93
95
|
</Flex>
|
|
94
96
|
</Flex>
|
|
97
|
+
</DialogBody>
|
|
95
98
|
<DialogFooter>
|
|
96
99
|
<Button variant="outline" onClick={() => setCreateOpen(false)}>
|
|
97
100
|
キャンセル
|
|
@@ -123,6 +126,7 @@ export default function Demo() {
|
|
|
123
126
|
<DialogTitle>取引 #JE-2024-0042</DialogTitle>
|
|
124
127
|
<DialogDescription>2024年3月31日 · 月次決算仕訳</DialogDescription>
|
|
125
128
|
</DialogHeader>
|
|
129
|
+
<DialogBody>
|
|
126
130
|
<Flex direction="col" gap="sm">
|
|
127
131
|
<Flex direction="row" justify="between">
|
|
128
132
|
<Text tone="muted">借方</Text>
|
|
@@ -137,6 +141,7 @@ export default function Demo() {
|
|
|
137
141
|
<Text>3月売上計上</Text>
|
|
138
142
|
</Flex>
|
|
139
143
|
</Flex>
|
|
144
|
+
</DialogBody>
|
|
140
145
|
</DialogContent>
|
|
141
146
|
</Dialog>
|
|
142
147
|
</CardContent>
|
|
@@ -178,10 +183,12 @@ export default function Demo() {
|
|
|
178
183
|
subtitle={`見出し帯は tone="${headerTone}" で描画しています。`}
|
|
179
184
|
extra={<Badge tone={headerTone}>{headerTone}</Badge>}
|
|
180
185
|
/>
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
186
|
+
<DialogBody>
|
|
187
|
+
<Text size="sm">
|
|
188
|
+
帯の色だけが tone に追従し、見出しと本文の文字色は変わりません。extra
|
|
189
|
+
スロットは帯の右端に固定され、閉じるボタンとは重なりません。
|
|
190
|
+
</Text>
|
|
191
|
+
</DialogBody>
|
|
185
192
|
<DialogFooter>
|
|
186
193
|
<Button variant="outline" onClick={() => setToneOpen(false)}>
|
|
187
194
|
閉じる
|
|
@@ -219,14 +226,16 @@ export default function Demo() {
|
|
|
219
226
|
title="提携を終了しますか?"
|
|
220
227
|
subtitle="この操作は元に戻せません。終了理由は監査ログに残ります。"
|
|
221
228
|
/>
|
|
222
|
-
<
|
|
223
|
-
<
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
<DialogBody>
|
|
230
|
+
<FormField id="termination-reason" label="終了理由" required>
|
|
231
|
+
<Textarea
|
|
232
|
+
id="termination-reason"
|
|
233
|
+
value={reason}
|
|
234
|
+
onChange={(event) => setReason(event.target.value)}
|
|
235
|
+
placeholder="契約違反の内容を記載してください"
|
|
236
|
+
/>
|
|
237
|
+
</FormField>
|
|
238
|
+
</DialogBody>
|
|
230
239
|
<DialogFooter>
|
|
231
240
|
<DialogCancel asChild>
|
|
232
241
|
<Button variant="ghost">キャンセル</Button>
|
|
@@ -147,7 +147,7 @@ export default function Demo() {
|
|
|
147
147
|
</Card>
|
|
148
148
|
<Card>
|
|
149
149
|
<CardHeader>
|
|
150
|
-
<CardTitle level={2}>バー以外の chrome
|
|
150
|
+
<CardTitle level={2}>バー以外の chrome · icon とパネルの向き</CardTitle>
|
|
151
151
|
<CardDescription>
|
|
152
152
|
`appearance="icon"` は正方形の ghost Button。バーではない chrome(nav rail、
|
|
153
153
|
カードのヘッダ、ツールバー)のための箱で、glyph は同じです。 パネルは既定で chrome
|
|
@@ -172,7 +172,7 @@ export default function Demo() {
|
|
|
172
172
|
|
|
173
173
|
<Card>
|
|
174
174
|
<CardHeader>
|
|
175
|
-
<CardTitle level={2}>Launchpad
|
|
175
|
+
<CardTitle level={2}>Launchpad · 画面いっぱい</CardTitle>
|
|
176
176
|
<CardDescription>
|
|
177
177
|
`responsive="fullscreen"` は plaform の start bar のための形。背後のページを
|
|
178
178
|
ぼかし、その地の上にタイルが並びます(macOS Launchpad / Windows Start)。 幅に関係なく
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/ui",
|
|
3
|
-
"version": "23.4.
|
|
4
|
-
"godxUiMcp": "23.4.
|
|
3
|
+
"version": "23.4.11",
|
|
4
|
+
"godxUiMcp": "23.4.11",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -321,6 +321,7 @@
|
|
|
321
321
|
"test:coverage": "vitest run --coverage",
|
|
322
322
|
"check:example-imports": "node scripts/check-example-imports.mjs",
|
|
323
323
|
"check:frame-coverage": "node scripts/frame-coverage.mjs",
|
|
324
|
+
"check:frame-coverage-report": "node scripts/frame-coverage.mjs --check",
|
|
324
325
|
"gen:frame-coverage-ledger": "node scripts/gen-frame-coverage-ledger.mjs",
|
|
325
326
|
"check:frame-coverage-ledger": "node scripts/gen-frame-coverage-ledger.mjs --check && node scripts/check-frame-coverage-ledger.mjs",
|
|
326
327
|
"check:screen-reader-evidence": "node scripts/check-screen-reader-evidence.mjs",
|
|
@@ -330,13 +331,14 @@
|
|
|
330
331
|
"check:data-entry-touch-aria": "node scripts/check-data-entry-touch-aria.mjs",
|
|
331
332
|
"check:display-runtime-evidence": "node scripts/check-display-runtime-evidence.mjs",
|
|
332
333
|
"check:choice-hit-target": "node scripts/check-choice-hit-target.mjs",
|
|
334
|
+
"check:state-legible": "node scripts/check-state-legible.mjs",
|
|
333
335
|
"check:app-shell-narrow-grid": "node scripts/check-app-shell-narrow-grid.mjs",
|
|
334
336
|
"check:frame-contracts": "pnpm check:component-api-manifest && pnpm audit:component-cases && pnpm check:frame-coverage && pnpm check:frame-coverage-ledger && pnpm check:screen-reader-evidence && pnpm check:data-entry-owner-source && pnpm check:display-runtime-evidence",
|
|
335
337
|
"check:frame-runtime": "pnpm check:data-entry-touch-aria && pnpm check:app-shell-narrow-grid && pnpm check:choice-hit-target",
|
|
336
338
|
"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
339
|
"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
340
|
"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:measurement-contract && 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",
|
|
341
|
+
"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:measurement-contract && 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:frame-coverage-report && pnpm check:gate-coverage && pnpm check:mcp-prop-sync && pnpm check:catalog-contradictions && pnpm check:catalog-snippets",
|
|
340
342
|
"verify:browser": "pnpm check:contrast && pnpm check:text-ink-clip && pnpm check:font-fallback-metrics && pnpm check:visual-audit",
|
|
341
343
|
"verify:release": "pnpm verify:static && pnpm check:frame-contracts && pnpm check:frame-coverage",
|
|
342
344
|
"verify:publish-tree": "pnpm build && pnpm check:packed-public-contract && pnpm check:use-client && pnpm check:dist-tokens-resolve",
|
package/scripts/ui-audit.mjs
CHANGED
|
@@ -528,8 +528,21 @@ const RULES = [
|
|
|
528
528
|
{
|
|
529
529
|
id: "hand-rolled-close-glyph",
|
|
530
530
|
severity: "warn",
|
|
531
|
-
|
|
531
|
+
/*
|
|
532
|
+
* A literal close glyph as JSX text — almost always a hand-rolled dismiss that should be a slot.
|
|
533
|
+
*
|
|
534
|
+
* NOT when the glyph carries a name. The rule's own message is "a bare glyph has no accessible
|
|
535
|
+
* name", so a glyph that HAS one is the pattern this rule wants, not the one it forbids:
|
|
536
|
+
*
|
|
537
|
+
* <span aria-hidden="true">×</span>
|
|
538
|
+
* <VisuallyHidden>未実施</VisuallyHidden>
|
|
539
|
+
*
|
|
540
|
+
* That is the ○ / △ / × status scale, and it was reported against our own `Segmented` docs page
|
|
541
|
+
* — one of ten standing warnings here. Flagging it would push every consumer doing the right
|
|
542
|
+
* thing toward either a suppression comment or a worse glyph.
|
|
543
|
+
*/
|
|
532
544
|
test: />\s*[✕✖×╳]\s*</,
|
|
545
|
+
exempt: /<VisuallyHidden\b|sr-only/,
|
|
533
546
|
standard: "WAI-ARIA 1.2 (dialog) · WCAG 2.2 SC 4.1.2 · @godxjp/ui Alert/Dialog anatomy",
|
|
534
547
|
message:
|
|
535
548
|
"Don't hand-roll a ✕ close. Pass onDismiss to <Alert> (renders the × top-right with an aria-label), or use <Dialog>/<Sheet> which ship their own labelled close. A bare glyph has no accessible name.",
|
|
@@ -762,9 +775,25 @@ function isSuppressed(ruleId, sameLine, prevLine) {
|
|
|
762
775
|
|
|
763
776
|
/** A rule's own in-place escape (see the `exempt` note on RULES), on the same line or the one above.
|
|
764
777
|
* Read from the ORIGINAL lines, because the scanner blanks comments before `rule.test` runs. */
|
|
765
|
-
|
|
778
|
+
/**
|
|
779
|
+
* Also looks at the NEXT line, not just this one and the one before.
|
|
780
|
+
*
|
|
781
|
+
* An accessible name for a symbol is written AFTER the symbol, because that is the reading order:
|
|
782
|
+
*
|
|
783
|
+
* <span aria-hidden="true">×</span>
|
|
784
|
+
* <VisuallyHidden>未実施</VisuallyHidden>
|
|
785
|
+
*
|
|
786
|
+
* Checking only backwards could never see it, so `hand-rolled-close-glyph` fired on the ○/△/×
|
|
787
|
+
* status scale in our own `Segmented` docs — a glyph that is named, which is the pattern the rule
|
|
788
|
+
* wants rather than the one it forbids.
|
|
789
|
+
*/
|
|
790
|
+
function isExempt(rule, sameLine, prevLine, nextLine) {
|
|
766
791
|
if (!rule.exempt) return false;
|
|
767
|
-
return
|
|
792
|
+
return (
|
|
793
|
+
rule.exempt.test(sameLine ?? "") ||
|
|
794
|
+
rule.exempt.test(prevLine ?? "") ||
|
|
795
|
+
rule.exempt.test(nextLine ?? "")
|
|
796
|
+
);
|
|
768
797
|
}
|
|
769
798
|
|
|
770
799
|
function walk(dir, acc = []) {
|
|
@@ -857,12 +886,45 @@ function findHandCurrencyInJsxText(source, from) {
|
|
|
857
886
|
return null;
|
|
858
887
|
}
|
|
859
888
|
|
|
889
|
+
/**
|
|
890
|
+
* Is `index` inside a quoted string or a template literal, counted from the start of the file?
|
|
891
|
+
*
|
|
892
|
+
* `insideTemplateLiteral` only counts backticks, and only from an arbitrary `from` — enough for the
|
|
893
|
+
* text scan, not enough to decide whether a `<` is JSX at all. A tag written INSIDE a string is not
|
|
894
|
+
* markup, and treating it as markup sends the scan off through the rest of the file:
|
|
895
|
+
*
|
|
896
|
+
* console.warn(`[DataTable] Column "${key}" renders a <th> with no visible text…`)
|
|
897
|
+
*
|
|
898
|
+
* That `<th>` was read as an opening tag, the text scan walked past the closing backtick, skipped
|
|
899
|
+
* from quote to quote, and eventually found a currency glyph somewhere else entirely — reporting
|
|
900
|
+
* `hardcoded-currency` against a dev warning containing no currency at all. One of ten standing
|
|
901
|
+
* warnings in this repo, and the only one in `src/`.
|
|
902
|
+
*/
|
|
903
|
+
function insideStringLiteral(source, index) {
|
|
904
|
+
let quote = "";
|
|
905
|
+
for (let i = 0; i < index; i++) {
|
|
906
|
+
const char = source[i];
|
|
907
|
+
if (char === "\\") {
|
|
908
|
+
i++;
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
if (quote) {
|
|
912
|
+
if (char === quote) quote = "";
|
|
913
|
+
} else if (char === '"' || char === "'" || char === "`") {
|
|
914
|
+
quote = char;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return quote !== "";
|
|
918
|
+
}
|
|
919
|
+
|
|
860
920
|
/** Match real JSX text after a balanced opening tag, including props with comparisons. */
|
|
861
921
|
function* currencyMatches(source) {
|
|
862
922
|
for (const opening of source.matchAll(/<(?:[A-Za-z][\w.:]*\b|(?=>))/g)) {
|
|
863
923
|
const end = jsxOpeningEnd(source, opening.index);
|
|
864
924
|
if (end >= source.length) continue;
|
|
865
925
|
if (isGenericTypeBeforeCall(source, opening.index, end)) continue;
|
|
926
|
+
// A tag inside a string or template literal is prose about markup, not markup.
|
|
927
|
+
if (insideStringLiteral(source, opening.index)) continue;
|
|
866
928
|
const hit = findHandCurrencyInJsxText(source, end + 1);
|
|
867
929
|
if (hit) {
|
|
868
930
|
yield { 0: source.slice(opening.index, hit.end), index: opening.index };
|
|
@@ -923,6 +985,30 @@ const CARD_TABLE_FLUSH = new RegExp(
|
|
|
923
985
|
"g",
|
|
924
986
|
);
|
|
925
987
|
|
|
988
|
+
/*
|
|
989
|
+
* A `DialogContent` / `SheetContent` that has chrome (a header or a footer) but no BODY.
|
|
990
|
+
*
|
|
991
|
+
* The scroll lives on the body, and only on the body:
|
|
992
|
+
*
|
|
993
|
+
* [data-slot="dialog-body"] { min-height: 0; max-height: min(60vh, 32rem); overflow-y: auto }
|
|
994
|
+
* [data-slot="dialog-content"] { overflow: hidden } // no max-height, deliberately
|
|
995
|
+
*
|
|
996
|
+
* So an overlay without one cannot scroll at all: content longer than the viewport is clipped at
|
|
997
|
+
* BOTH ends, taking the footer's buttons with it, and the only way out is Escape. Reported from a
|
|
998
|
+
* consumer whose five dialogs all had the defect and none of whose reviewers could see it — it only
|
|
999
|
+
* appears on real customer data, never on demo data (gh#617).
|
|
1000
|
+
*
|
|
1001
|
+
* THE SAME SHAPE AS gh#611, and the reporter named it as such: a layout contract that EXISTS in the
|
|
1002
|
+
* stylesheet with no gate to enforce it. `Card`→`CardContent` had `card-needs-content`;
|
|
1003
|
+
* `Dialog`→`DialogBody` had nothing. `Sheet` is included for the reason gh#611 taught: when a rule
|
|
1004
|
+
* names a component, ask whether its required sibling slots are checked too — Sheet's CSS carries
|
|
1005
|
+
* the identical `:has([data-slot="sheet-body"])` pair, so it carries the identical defect.
|
|
1006
|
+
*
|
|
1007
|
+
* Chrome is the trigger, not length: a bare `<DialogContent>` holding one line needs nothing, but
|
|
1008
|
+
* once a header or footer is present the middle is a body and the middle is what scrolls.
|
|
1009
|
+
*/
|
|
1010
|
+
const OVERLAY_NEEDS_BODY = /<(Dialog|AlertDialog|Sheet)Content\b[^>]*>([\s\S]*?)<\/\1Content>/g;
|
|
1011
|
+
|
|
926
1012
|
const CARD_FLUSH = new RegExp(
|
|
927
1013
|
`<Card(?!${ATTRS}\\bp-0\\b)(?:\\s${ATTRS})?>\\s*<(?!CardContent|CardHeader|CardCover|CardFooter|CardBar|\\/Card)`,
|
|
928
1014
|
"g",
|
|
@@ -1072,7 +1158,7 @@ for (const dir of SCAN_DIRS) {
|
|
|
1072
1158
|
if (
|
|
1073
1159
|
(typeof rule.test === "function" ? rule.test(target) : rule.test.test(target)) &&
|
|
1074
1160
|
!suppressed(rule.id, i) &&
|
|
1075
|
-
!isExempt(rule, origLines[i], origLines[i - 1])
|
|
1161
|
+
!isExempt(rule, origLines[i], origLines[i - 1], origLines[i + 1])
|
|
1076
1162
|
) {
|
|
1077
1163
|
findings.push({
|
|
1078
1164
|
file: rel,
|
|
@@ -1179,6 +1265,29 @@ for (const dir of SCAN_DIRS) {
|
|
|
1179
1265
|
snippet: match[0].replace(/\s+/g, " ").slice(0, 120),
|
|
1180
1266
|
});
|
|
1181
1267
|
}
|
|
1268
|
+
for (const match of scanContent.matchAll(OVERLAY_NEEDS_BODY)) {
|
|
1269
|
+
const [, family, inner] = match;
|
|
1270
|
+
const slot = family === "Sheet" ? "Sheet" : "Dialog";
|
|
1271
|
+
const hasChrome = new RegExp(`<${slot}(?:Header|Footer)\\b`).test(inner);
|
|
1272
|
+
const hasBody = new RegExp(`<${slot}Body\\b`).test(inner);
|
|
1273
|
+
if (!hasChrome || hasBody) continue;
|
|
1274
|
+
const lineNo = scanContent.slice(0, match.index).split("\n").length;
|
|
1275
|
+
if (suppressed("dialog-needs-body", lineNo - 1)) continue;
|
|
1276
|
+
findings.push({
|
|
1277
|
+
file: rel,
|
|
1278
|
+
line: lineNo,
|
|
1279
|
+
rule: "dialog-needs-body",
|
|
1280
|
+
severity: "error",
|
|
1281
|
+
message:
|
|
1282
|
+
`<${family}Content> has a ${slot}Header/${slot}Footer but no <${slot}Body>. The scroll lives on ` +
|
|
1283
|
+
`the body — the content box is \`overflow: hidden\` with no max-height — so content taller ` +
|
|
1284
|
+
`than the viewport is clipped at BOTH ends and the footer's buttons go with it, leaving ` +
|
|
1285
|
+
`Escape as the only way out. It only shows on real data, never on demo data.`,
|
|
1286
|
+
replacement: `${slot}Body`,
|
|
1287
|
+
snippet: match[0].replace(/\s+/g, " ").slice(0, 120),
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1182
1291
|
if (
|
|
1183
1292
|
!(SELF && !args.includes("--consumer")) &&
|
|
1184
1293
|
!/<(?:Flex|ResponsiveGrid|PageContainer)\b/.test(scanContent)
|