@godxjp/ui 28.5.0 → 28.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/components/data-display/data-table.d.ts +10 -2
  2. package/dist/components/data-display/data-table.js +12 -5
  3. package/dist/components/data-display/scroll-area.js +16 -2
  4. package/dist/components/data-display/service-launcher-card.d.ts +25 -0
  5. package/dist/components/data-display/table.d.ts +38 -1
  6. package/dist/components/data-display/table.js +57 -39
  7. package/dist/components/data-entry/field.d.ts +1 -1
  8. package/dist/components/data-entry/field.js +35 -4
  9. package/dist/components/data-entry/form-field.js +29 -12
  10. package/dist/components/data-entry/input.d.ts +1 -1
  11. package/dist/components/layout/index.d.ts +1 -1
  12. package/dist/components/layout/nav-list.d.ts +9 -1
  13. package/dist/components/layout/nav-list.js +22 -11
  14. package/dist/components/layout/sidebar.d.ts +16 -2
  15. package/dist/components/layout/sidebar.js +1 -0
  16. package/dist/components/navigation/app-setting-picker.js +20 -3
  17. package/dist/components/ui/password-input.d.ts +1 -1
  18. package/dist/components/ui/table.js +1 -0
  19. package/dist/contracts/measurement.json +1 -1
  20. package/dist/i18n/messages/en.json +5 -1
  21. package/dist/i18n/messages/ja.json +5 -1
  22. package/dist/i18n/messages/vi.json +5 -1
  23. package/dist/lib/control-styles.d.ts +18 -3
  24. package/dist/lib/control-styles.js +1 -1
  25. package/dist/lib/hooks.d.ts +23 -0
  26. package/dist/lib/hooks.js +27 -0
  27. package/dist/props/components/data-display.prop.d.ts +14 -0
  28. package/dist/props/components/data-entry.prop.d.ts +36 -2
  29. package/dist/props/components/layout.prop.d.ts +16 -5
  30. package/dist/props/registry.d.ts +2 -2
  31. package/dist/props/registry.js +10 -2
  32. package/dist/styles/control.css +12 -0
  33. package/dist/styles/data-display-layout.css +25 -2
  34. package/dist/styles/dialog-layout.css +12 -0
  35. package/dist/styles/form-layout.css +22 -0
  36. package/dist/styles/layout.css +1 -2
  37. package/dist/styles/navigation-layout.css +18 -19
  38. package/dist/tokens/components/data-display.css +2 -1
  39. package/dist/tokens/components/feedback.css +2 -0
  40. package/dist/tokens/components/form.css +2 -0
  41. package/dist/tokens/components/navigation.css +2 -0
  42. package/docs/COMPOSITION-VS-COMPONENT.md +3 -0
  43. package/docs/DESIGN-AUTHORITY.md +25 -5
  44. package/docs/FRAME-COVERAGE-REPORT.md +1 -1
  45. package/docs/data-display/collapsible.tsx +5 -5
  46. package/docs/data-display/descriptions.tsx +188 -109
  47. package/docs/data-display/legend.tsx +17 -4
  48. package/docs/data-display/scroll-area.tsx +106 -5
  49. package/docs/data-display/service-launcher-card.tsx +275 -115
  50. package/docs/data-display/stat-card.tsx +319 -64
  51. package/docs/data-entry/form.tsx +700 -6
  52. package/docs/data-entry/input.tsx +663 -53
  53. package/docs/data-entry/radio-group.tsx +216 -2
  54. package/docs/data-entry/switch.tsx +210 -4
  55. package/docs/data-entry/upload-crop-dialog.tsx +24 -25
  56. package/docs/layout/nav-list.tsx +556 -35
  57. package/package.json +9 -7
  58. package/scripts/visual-audit.mjs +29 -1
@@ -14,6 +14,7 @@ import {
14
14
  InputOTP,
15
15
  InputOTPGroup,
16
16
  InputOTPSlot,
17
+ NumberInput,
17
18
  PasswordInput,
18
19
  Rating,
19
20
  RadioGroup,
@@ -83,6 +84,29 @@ const ACCOUNT_TREE = [
83
84
  },
84
85
  ];
85
86
 
87
+ /** 都道府県 — 住所の複合行の先頭。value は ISO 3166-2:JP のサブディビジョンコード。 */
88
+ const PREFECTURES = [
89
+ { value: "JP-13", label: "東京都" },
90
+ { value: "JP-14", label: "神奈川県" },
91
+ { value: "JP-27", label: "大阪府" },
92
+ { value: "JP-23", label: "愛知県" },
93
+ { value: "JP-01", label: "北海道" },
94
+ ];
95
+
96
+ /** 生年月日の 年/月/日 3連 Select — 日本の業務フォームで最も多い複合フィールド。 */
97
+ const BIRTH_YEARS = Array.from({ length: 60 }, (_, index) => {
98
+ const year = 2006 - index;
99
+ return { value: String(year), label: `${year}年` };
100
+ });
101
+ const MONTHS = Array.from({ length: 12 }, (_, index) => ({
102
+ value: String(index + 1),
103
+ label: `${index + 1}月`,
104
+ }));
105
+ const DAYS = Array.from({ length: 31 }, (_, index) => ({
106
+ value: String(index + 1),
107
+ label: `${index + 1}日`,
108
+ }));
109
+
86
110
  /**
87
111
  * Form — the comprehensive catalogue. One page proving (1) EVERY data-entry field type wired in a
88
112
  * FormField, (2) every Form LAYOUT (vertical / horizontal / inline / multi-column / responsive
@@ -115,6 +139,22 @@ export default function Demo() {
115
139
  const [files, setFiles] = useState<UploadFileItem[]>([]);
116
140
  const [period2, setPeriod2] = useState("monthly");
117
141
 
142
+ // ── 複合フィールド(1ラベル × 複数入力)の状態 ─────────────────────────────
143
+ const [lastName, setLastName] = useState("山田");
144
+ const [firstName, setFirstName] = useState("太郎");
145
+ const [kanaLast, setKanaLast] = useState("ヤマダ");
146
+ const [kanaFirst, setKanaFirst] = useState("タロウ");
147
+ const [zip1, setZip1] = useState("100");
148
+ const [zip2, setZip2] = useState("0005");
149
+ const [prefecture, setPrefecture] = useState("JP-13");
150
+ const [birthYear, setBirthYear] = useState("1988");
151
+ const [birthMonth, setBirthMonth] = useState("4");
152
+ const [birthDay, setBirthDay] = useState("");
153
+
154
+ // ── Textarea 系の状態(文字数カウンタ・自動伸長) ──────────────────────────
155
+ const [remarks, setRemarks] = useState("初回取引のため与信枠は50万円で開始する。");
156
+ const [thread, setThread] = useState("");
157
+
118
158
  return (
119
159
  <PageContainer
120
160
  title="Form"
@@ -154,8 +194,334 @@ export default function Demo() {
154
194
  </CardContent>
155
195
  </Card>
156
196
 
157
- {/* ════════════════════ 1. EVERY FIELD TYPE ════════════════════ */}
158
- <Heading level={2}>1. 全フィールド型 · すべての入力コントロール</Heading>
197
+ {/* ════════════════════ 1. COMPOUND FIELDS ════════════════════ */}
198
+ <Heading level={2}>1. 複合フィールド · 1つのラベルに複数の入力欄</Heading>
199
+ <Text tone="muted" size="sm">
200
+ 日本の業務フォームでは氏名・郵便番号・電話番号・生年月日がいずれも「1項目 ×
201
+ 複数入力欄」になる。FormField は直下の 1 要素にしか属性を注入できないので、直下が Flex
202
+ のときはラベルを FieldNameContext で配り、Flex 自身を role=group にする。これが無いと中の
203
+ Input / Select は全て無名になり、axe の label · button-name が一気に赤くなる(gh#303)。
204
+ </Text>
205
+
206
+ <Card>
207
+ <CardHeader>
208
+ <CardTitle level={2}>氏名 · フリガナ(1ラベル × 2入力)</CardTitle>
209
+ <CardDescription>
210
+ 「氏名」は 1 項目・2 欄。Flex でくるんでも姓と名の両方にラベルが届く。ただし各欄の
211
+ aria-label を省くと両方が「氏名」と読み上げられ、どちらが姓か分からなくなる。
212
+ 複合フィールドで最初に壊れるのがここ。data-field は各 Input 自身の id から解決され、
213
+ cmp-name-last / cmp-name-first として別々に自動テストから掴める。
214
+ </CardDescription>
215
+ </CardHeader>
216
+ <CardContent>
217
+ <Form layout="horizontal" labelWidth="9rem">
218
+ <FormField id="cmp-name" label="氏名" required helper="戸籍上の表記で入力">
219
+ <Flex direction="row" gap="sm" align="center">
220
+ <Input
221
+ id="cmp-name-last"
222
+ aria-label="姓"
223
+ value={lastName}
224
+ onValueChange={setLastName}
225
+ placeholder="山田"
226
+ />
227
+ <Input
228
+ id="cmp-name-first"
229
+ aria-label="名"
230
+ value={firstName}
231
+ onValueChange={setFirstName}
232
+ placeholder="太郎"
233
+ />
234
+ </Flex>
235
+ </FormField>
236
+ <FormField id="cmp-kana" label="フリガナ" required helper="全角カタカナ">
237
+ <Flex direction="row" gap="sm" align="center">
238
+ <Input
239
+ id="cmp-kana-last"
240
+ aria-label="セイ"
241
+ value={kanaLast}
242
+ onValueChange={setKanaLast}
243
+ placeholder="ヤマダ"
244
+ />
245
+ <Input
246
+ id="cmp-kana-first"
247
+ aria-label="メイ"
248
+ value={kanaFirst}
249
+ onValueChange={setKanaFirst}
250
+ placeholder="タロウ"
251
+ />
252
+ </Flex>
253
+ </FormField>
254
+ </Form>
255
+ </CardContent>
256
+ </Card>
257
+
258
+ <Card>
259
+ <CardHeader>
260
+ <CardTitle level={2}>郵便番号 · 住所(狭い欄と広い欄が同じ行に並ぶ)</CardTitle>
261
+ <CardDescription>
262
+ 郵便番号は 3桁 + 4桁 の固定幅、住所は残り全部。幅を Input
263
+ に持たせる方法は無いので、狭い欄だけを Flex width で囲み shrink={false}
264
+ で潰れないようにする。区切りの「-」は Text aria-hidden で、読み上げには混ぜない。
265
+ 住所行は都道府県 Select(width=auto)+市区町村+番地で、grow が効かないと番地欄が
266
+ 3文字幅に潰れるのが見える。
267
+ </CardDescription>
268
+ </CardHeader>
269
+ <CardContent>
270
+ <Form layout="horizontal" labelWidth="9rem">
271
+ <FormField
272
+ id="cmp-zip"
273
+ label="郵便番号"
274
+ required
275
+ helper="ハイフンなしの7桁でも可"
276
+ labelAddon={
277
+ <Button type="button" variant="link" size="xs">
278
+ 住所を自動入力
279
+ </Button>
280
+ }
281
+ >
282
+ <Flex direction="row" gap="sm" align="center">
283
+ <Text aria-hidden tone="muted">
284
+ 〒
285
+ </Text>
286
+ <Flex width="5.5rem" shrink={false}>
287
+ <Input
288
+ id="cmp-zip-head"
289
+ aria-label="郵便番号 上3桁"
290
+ inputMode="numeric"
291
+ maxLength={3}
292
+ value={zip1}
293
+ onValueChange={setZip1}
294
+ placeholder="100"
295
+ />
296
+ </Flex>
297
+ <Text aria-hidden tone="muted">
298
+ -
299
+ </Text>
300
+ <Flex width="6.5rem" shrink={false}>
301
+ <Input
302
+ id="cmp-zip-tail"
303
+ aria-label="郵便番号 下4桁"
304
+ inputMode="numeric"
305
+ maxLength={4}
306
+ value={zip2}
307
+ onValueChange={setZip2}
308
+ placeholder="0005"
309
+ />
310
+ </Flex>
311
+ </Flex>
312
+ </FormField>
313
+ <FormField id="cmp-address" label="住所" required>
314
+ <Flex direction="row" gap="sm" align="center" wrap>
315
+ <Flex width="9rem" shrink={false}>
316
+ <Select
317
+ id="cmp-address-pref"
318
+ aria-label="都道府県"
319
+ name="prefecture"
320
+ value={prefecture}
321
+ onValueChange={setPrefecture}
322
+ options={PREFECTURES}
323
+ placeholder="都道府県"
324
+ />
325
+ </Flex>
326
+ <Flex width="11rem" shrink={false}>
327
+ <Input
328
+ id="cmp-address-city"
329
+ aria-label="市区町村"
330
+ defaultValue="千代田区丸の内"
331
+ />
332
+ </Flex>
333
+ <Flex grow>
334
+ <Input
335
+ id="cmp-address-street"
336
+ aria-label="番地・建物名"
337
+ defaultValue="1-9-1 グラントウキョウノースタワー 18F"
338
+ />
339
+ </Flex>
340
+ </Flex>
341
+ </FormField>
342
+ </Form>
343
+ </CardContent>
344
+ </Card>
345
+
346
+ <Card>
347
+ <CardHeader>
348
+ <CardTitle level={2}>電話番号 3分割 · 生年月日 年/月/日 Select</CardTitle>
349
+ <CardDescription>
350
+ 桁数の決まった 3 分割と、型の違うコントロールを 1 ラベル下に混ぜる場合。生年月日は
351
+ Select × 3(年は60件、月日は固定)で、どれか一つでも未選択なら値として不完全になる。
352
+ その「部分入力」をどう検証するかが複合フィールドの本題。DatePicker
353
+ で足りる画面ではこのパターンを使わない。
354
+ </CardDescription>
355
+ </CardHeader>
356
+ <CardContent>
357
+ <Form layout="horizontal" labelWidth="9rem">
358
+ <FormField id="cmp-tel" label="電話番号" helper="市外局番から">
359
+ <Flex direction="row" gap="sm" align="center">
360
+ <Flex width="5rem" shrink={false}>
361
+ <Input
362
+ id="cmp-tel-area"
363
+ aria-label="市外局番"
364
+ inputMode="numeric"
365
+ maxLength={4}
366
+ defaultValue="03"
367
+ />
368
+ </Flex>
369
+ <Text aria-hidden tone="muted">
370
+ -
371
+ </Text>
372
+ <Flex width="5rem" shrink={false}>
373
+ <Input
374
+ id="cmp-tel-city"
375
+ aria-label="市内局番"
376
+ inputMode="numeric"
377
+ maxLength={4}
378
+ defaultValue="6273"
379
+ />
380
+ </Flex>
381
+ <Text aria-hidden tone="muted">
382
+ -
383
+ </Text>
384
+ <Flex width="5rem" shrink={false}>
385
+ <Input
386
+ id="cmp-tel-line"
387
+ aria-label="加入者番号"
388
+ inputMode="numeric"
389
+ maxLength={4}
390
+ defaultValue="0001"
391
+ />
392
+ </Flex>
393
+ <Flex width="6rem" shrink={false}>
394
+ <NumberInput
395
+ id="cmp-tel-ext"
396
+ aria-label="内線番号"
397
+ controls={false}
398
+ min={0}
399
+ max={9999}
400
+ defaultValue={281}
401
+ />
402
+ </Flex>
403
+ </Flex>
404
+ </FormField>
405
+ <FormField
406
+ id="cmp-birth"
407
+ label="生年月日"
408
+ required
409
+ error={birthDay === "" ? "日を選択してください。" : undefined}
410
+ >
411
+ <Flex direction="row" gap="sm" align="center" wrap>
412
+ <Flex width="8rem" shrink={false}>
413
+ <Select
414
+ id="cmp-birth-year"
415
+ aria-label="生年"
416
+ name="birth_year"
417
+ value={birthYear}
418
+ onValueChange={setBirthYear}
419
+ options={BIRTH_YEARS}
420
+ placeholder="年"
421
+ showSearch
422
+ />
423
+ </Flex>
424
+ <Flex width="6rem" shrink={false}>
425
+ <Select
426
+ id="cmp-birth-month"
427
+ aria-label="生月"
428
+ name="birth_month"
429
+ value={birthMonth}
430
+ onValueChange={setBirthMonth}
431
+ options={MONTHS}
432
+ placeholder="月"
433
+ />
434
+ </Flex>
435
+ <Flex width="6rem" shrink={false}>
436
+ <Select
437
+ id="cmp-birth-day"
438
+ aria-label="生日"
439
+ name="birth_day"
440
+ value={birthDay}
441
+ onValueChange={setBirthDay}
442
+ options={DAYS}
443
+ placeholder="日"
444
+ status={birthDay === "" ? "error" : undefined}
445
+ />
446
+ </Flex>
447
+ </Flex>
448
+ </FormField>
449
+ </Form>
450
+ </CardContent>
451
+ </Card>
452
+
453
+ <Card>
454
+ <CardHeader>
455
+ <CardTitle level={2}>複合フィールドのエラー · どちらの欄が悪いのか</CardTitle>
456
+ <CardDescription>
457
+ FormField のエラーは 1 行しか出ない。欄が 3 つある行で「形式が正しくありません」
458
+ だけ出しても、利用者はどこを直せばいいか分からない。行のメッセージは FormField の
459
+ error に、犯人の特定は各コントロールの status="error" に分担させる。status
460
+ は塗るだけ(aria-invalid は error の側)なので二重に読み上げられない。
461
+ </CardDescription>
462
+ </CardHeader>
463
+ <CardContent>
464
+ <Form layout="horizontal" labelWidth="9rem">
465
+ <FormField
466
+ id="cmp-err-zip"
467
+ label="郵便番号"
468
+ required
469
+ error="下4桁が数字ではありません。"
470
+ >
471
+ <Flex direction="row" gap="sm" align="center">
472
+ <Flex width="5.5rem" shrink={false}>
473
+ <Input id="cmp-err-zip-head" aria-label="郵便番号 上3桁" defaultValue="100" />
474
+ </Flex>
475
+ <Text aria-hidden tone="muted">
476
+ -
477
+ </Text>
478
+ <Flex width="6.5rem" shrink={false}>
479
+ <Input
480
+ id="cmp-err-zip-tail"
481
+ aria-label="郵便番号 下4桁"
482
+ status="error"
483
+ defaultValue="00O5"
484
+ />
485
+ </Flex>
486
+ </Flex>
487
+ </FormField>
488
+ <FormField
489
+ id="cmp-warn-name"
490
+ label="氏名"
491
+ validateStatus="warning"
492
+ hasFeedback
493
+ helper="フリガナと姓が一致しません。登録は可能です。"
494
+ >
495
+ <Flex direction="row" gap="sm" align="center">
496
+ <Input id="cmp-warn-name-last" aria-label="姓" defaultValue="山田" />
497
+ <Input id="cmp-warn-name-first" aria-label="名" defaultValue="太郎" />
498
+ </Flex>
499
+ </FormField>
500
+ <FormField id="cmp-ok-tel" label="電話番号" validateStatus="success" hasFeedback>
501
+ <Flex direction="row" gap="sm" align="center">
502
+ <Flex width="5rem" shrink={false}>
503
+ <Input id="cmp-ok-tel-area" aria-label="市外局番" defaultValue="03" />
504
+ </Flex>
505
+ <Text aria-hidden tone="muted">
506
+ -
507
+ </Text>
508
+ <Flex width="5rem" shrink={false}>
509
+ <Input id="cmp-ok-tel-city" aria-label="市内局番" defaultValue="6273" />
510
+ </Flex>
511
+ <Text aria-hidden tone="muted">
512
+ -
513
+ </Text>
514
+ <Flex width="5rem" shrink={false}>
515
+ <Input id="cmp-ok-tel-line" aria-label="加入者番号" defaultValue="0001" />
516
+ </Flex>
517
+ </Flex>
518
+ </FormField>
519
+ </Form>
520
+ </CardContent>
521
+ </Card>
522
+
523
+ {/* ════════════════════ 2. EVERY FIELD TYPE ════════════════════ */}
524
+ <Heading level={2}>2. 全フィールド型 · すべての入力コントロール</Heading>
159
525
  <Text tone="muted" size="sm">
160
526
  ライブラリが提供するデータ入力コンポーネントを 1 つずつ FormField
161
527
  でラップ。ラベル・ヘルパー・必須マークを付与し、aria-* は自動配線される。
@@ -207,6 +573,62 @@ export default function Demo() {
207
573
  </CardContent>
208
574
  </Card>
209
575
 
576
+ <Card>
577
+ <CardHeader>
578
+ <CardTitle level={2}>Textarea · 固定行数 / 文字数カウンタ / 自動伸長</CardTitle>
579
+ <CardDescription>
580
+ 複数行欄は 3 種類しかない。行数固定(rows)・上限付きカウンタ(count)・内容に合わせて
581
+ 伸びる(autoGrow minRows/maxRows)。カウンタはコードポイント単位で数え、超過を
582
+ 報告するだけで値を切らない。IME 変換中に切ると日本語が途中で壊れるため。autoGrow は
583
+ CSS で測るので貼り付け・IME 確定・プログラムからの代入でも追従し、maxRows
584
+ を超えたらページを押し広げずに内部スクロールする。
585
+ </CardDescription>
586
+ </CardHeader>
587
+ <CardContent>
588
+ <Form>
589
+ <FormField id="ta-remarks" label="備考" helper="取引条件の申し送り事項">
590
+ <Textarea
591
+ id="ta-remarks"
592
+ rows={3}
593
+ value={remarks}
594
+ onValueChange={setRemarks}
595
+ count={{ max: 200, show: true }}
596
+ allowClear
597
+ placeholder="例: 初回取引のため与信枠は50万円で開始する。"
598
+ />
599
+ </FormField>
600
+ <FormField
601
+ id="ta-thread"
602
+ label="社内コメント"
603
+ helper="1行から始まり、8行まで伸びてから内部スクロールに切り替わる"
604
+ >
605
+ <Textarea
606
+ id="ta-thread"
607
+ autoGrow
608
+ minRows={1}
609
+ maxRows={8}
610
+ value={thread}
611
+ onValueChange={setThread}
612
+ placeholder="承認者への申し送りを入力…"
613
+ />
614
+ </FormField>
615
+ <FormField id="ta-template" label="請求メール定型文(read-only)">
616
+ <Textarea
617
+ id="ta-template"
618
+ rows={4}
619
+ readOnly
620
+ defaultValue={
621
+ "いつもお世話になっております。\n2026年1月度のご請求書を添付いたします。\nお支払期限は2026年2月28日です。\nご不明点はご返信ください。"
622
+ }
623
+ />
624
+ </FormField>
625
+ <FormField id="ta-disabled" label="監査コメント(権限なし · disabled)">
626
+ <Textarea id="ta-disabled" rows={2} disabled defaultValue="監査部門のみ編集可能" />
627
+ </FormField>
628
+ </Form>
629
+ </CardContent>
630
+ </Card>
631
+
210
632
  <Card>
211
633
  <CardHeader>
212
634
  <CardTitle level={2}>Responsive collapse contract · every breakpoint</CardTitle>
@@ -553,8 +975,8 @@ export default function Demo() {
553
975
  </CardContent>
554
976
  </Card>
555
977
 
556
- {/* ════════════════════ 2. EVERY LAYOUT ════════════════════ */}
557
- <Heading level={2}>2. レイアウト · vertical / horizontal / inline / columns</Heading>
978
+ {/* ════════════════════ 3. EVERY LAYOUT ════════════════════ */}
979
+ <Heading level={2}>3. レイアウト · vertical / horizontal / inline / columns / 混在</Heading>
558
980
 
559
981
  <Card>
560
982
  <CardHeader>
@@ -677,8 +1099,170 @@ export default function Demo() {
677
1099
  </CardContent>
678
1100
  </Card>
679
1101
 
680
- {/* ════════════════════ 3. EVERY STATE ════════════════════ */}
681
- <Heading level={2}>3. 状態 · pristine / filled / error / disabled / read-only</Heading>
1102
+ <Card>
1103
+ <CardHeader>
1104
+ <CardTitle level={2}>ラベル長 · 2文字のラベルと3行に折り返すラベルが同じ列に</CardTitle>
1105
+ <CardDescription>
1106
+ ラベル列の幅は1つしか無いので、「氏名」と「適格請求書発行事業者登録番号…」は必ず
1107
+ 同じ列に入る。長い方が折り返したとき、1行の入力欄がラベルの1行目に揃うか、
1108
+ ブロックの中央に落ちるかでフォーム全体の読みやすさが決まる。labelAlign は end と start
1109
+ の両方を並べて比較できるようにした。
1110
+ </CardDescription>
1111
+ </CardHeader>
1112
+ <CardContent>
1113
+ <Flex direction="col" gap="lg">
1114
+ <Form layout="horizontal" labelWidth="14rem" labelAlign="end" collapseBelow={false}>
1115
+ <FormField id="len-e-name" label="氏名" required>
1116
+ <Input id="len-e-name" defaultValue="山田 太郎" />
1117
+ </FormField>
1118
+ <FormField
1119
+ id="len-e-invoice"
1120
+ label="適格請求書発行事業者 登録番号(インボイス制度・国税庁公表サイトと突合)"
1121
+ required
1122
+ helper="T + 13桁"
1123
+ >
1124
+ <Input id="len-e-invoice" defaultValue="T1234567890123" />
1125
+ </FormField>
1126
+ <FormField id="len-e-note" label="略称">
1127
+ <Input id="len-e-note" defaultValue="ゴドー商事" />
1128
+ </FormField>
1129
+ </Form>
1130
+ <Form layout="horizontal" labelWidth="14rem" labelAlign="start" collapseBelow={false}>
1131
+ <FormField id="len-s-name" label="氏名" required>
1132
+ <Input id="len-s-name" defaultValue="山田 太郎" />
1133
+ </FormField>
1134
+ <FormField
1135
+ id="len-s-invoice"
1136
+ label="適格請求書発行事業者 登録番号(インボイス制度・国税庁公表サイトと突合)"
1137
+ required
1138
+ helper="T + 13桁"
1139
+ >
1140
+ <Input id="len-s-invoice" defaultValue="T1234567890123" />
1141
+ </FormField>
1142
+ <FormField id="len-s-note" label="略称">
1143
+ <Input id="len-s-note" defaultValue="ゴドー商事" />
1144
+ </FormField>
1145
+ </Form>
1146
+ </Flex>
1147
+ </CardContent>
1148
+ </Card>
1149
+
1150
+ <Card>
1151
+ <CardHeader>
1152
+ <CardTitle level={2}>混在レイアウト · 1つのフォームに horizontal と vertical</CardTitle>
1153
+ <CardDescription>
1154
+ 実画面はどれか1つでは足りない。短い項目は horizontal
1155
+ でラベル列に揃え、複合行と自由記述だけは幅いっぱいの vertical に落とす。layout は Form
1156
+ に1度書き、はみ出す行だけ FormField 側で上書きする。ここを className
1157
+ で殴ると、密度変更やテナントの再テーマでその行だけ取り残される。
1158
+ </CardDescription>
1159
+ </CardHeader>
1160
+ <CardContent>
1161
+ <Form layout="horizontal" labelWidth="9rem">
1162
+ <FormField id="mix-code" label="取引先コード" required helper="発番後は変更不可">
1163
+ <Input id="mix-code" defaultValue="BTY-0012" />
1164
+ </FormField>
1165
+ <FormField id="mix-name" label="取引先名" required>
1166
+ <Input id="mix-name" defaultValue="株式会社ゴドー商事" />
1167
+ </FormField>
1168
+ <FormField id="mix-pay" label="支払条件">
1169
+ <Select
1170
+ id="mix-pay"
1171
+ name="payment_terms"
1172
+ defaultValue="eom30"
1173
+ options={[
1174
+ { value: "eom30", label: "月末締め翌月末払い" },
1175
+ { value: "eom60", label: "月末締め翌々月末払い" },
1176
+ { value: "prepaid", label: "前払い" },
1177
+ ]}
1178
+ />
1179
+ </FormField>
1180
+ {/* 複合行: ラベル列に収めると3欄が潰れるので、この行だけ vertical で幅を取る。 */}
1181
+ <FormField id="mix-addr" label="請求書送付先" layout="vertical">
1182
+ <Flex direction="row" gap="sm" align="center" wrap>
1183
+ <Flex width="9rem" shrink={false}>
1184
+ <Select
1185
+ id="mix-addr-pref"
1186
+ aria-label="都道府県"
1187
+ defaultValue="JP-13"
1188
+ options={PREFECTURES}
1189
+ />
1190
+ </Flex>
1191
+ <Flex width="11rem" shrink={false}>
1192
+ <Input id="mix-addr-city" aria-label="市区町村" defaultValue="千代田区丸の内" />
1193
+ </Flex>
1194
+ <Flex grow>
1195
+ <Input id="mix-addr-street" aria-label="番地・建物名" defaultValue="1-9-1" />
1196
+ </Flex>
1197
+ </Flex>
1198
+ </FormField>
1199
+ {/* 自由記述も vertical。9rem のラベル列の右に押し込むと1行が短すぎる。 */}
1200
+ <FormField
1201
+ id="mix-memo"
1202
+ label="社内申し送り"
1203
+ layout="vertical"
1204
+ helper="与信・取引条件の背景を残す"
1205
+ >
1206
+ <Textarea
1207
+ id="mix-memo"
1208
+ autoGrow
1209
+ minRows={3}
1210
+ maxRows={10}
1211
+ defaultValue="代表者の交代に伴い、2026年4月から請求先部署が経理部に変更。"
1212
+ />
1213
+ </FormField>
1214
+ <FormField id="mix-owner" label="担当者">
1215
+ <Input id="mix-owner" defaultValue="佐藤 花子" />
1216
+ </FormField>
1217
+ </Form>
1218
+ </CardContent>
1219
+ </Card>
1220
+
1221
+ <Card>
1222
+ <CardHeader>
1223
+ <CardTitle level={2}>
1224
+ 幅 · 狭い項目と広い項目を同じ行に(controlWidth / colSpan)
1225
+ </CardTitle>
1226
+ <CardDescription>
1227
+ 郵便番号の欄が住所と同じ幅で伸びていると、7桁しか入らない欄だと見て分からない。
1228
+ controlWidth はラベル行の幅を保ったままコントロールだけを狭める(FormField 自体を
1229
+ width で縛ると、ラベルとエラー行まで一緒に縮む)。columns グリッドでは広い項目を
1230
+ colSpan で跨がせ、スマホ幅では 1 列に畳んで全ての欄が全幅に戻る。
1231
+ </CardDescription>
1232
+ </CardHeader>
1233
+ <CardContent>
1234
+ <Flex direction="col" gap="lg">
1235
+ <Form layout="horizontal" labelWidth="9rem">
1236
+ <FormField id="w-zip" label="郵便番号" controlWidth="10rem" helper="7桁">
1237
+ <Input id="w-zip" inputMode="numeric" defaultValue="1000005" />
1238
+ </FormField>
1239
+ <FormField id="w-qty" label="数量" controlWidth="7rem">
1240
+ <NumberInput id="w-qty" defaultValue={12} min={1} max={999} />
1241
+ </FormField>
1242
+ <FormField id="w-rate" label="適用税率" controlWidth="7rem">
1243
+ <NumberInput id="w-rate" defaultValue={10} min={0} max={100} suffix="%" />
1244
+ </FormField>
1245
+ <FormField id="w-addr" label="住所">
1246
+ <Input id="w-addr" defaultValue="東京都千代田区丸の内1-9-1" />
1247
+ </FormField>
1248
+ </Form>
1249
+ <Form columns={2}>
1250
+ <FormField id="w-g-zip" label="郵便番号" controlWidth="10rem">
1251
+ <Input id="w-g-zip" inputMode="numeric" defaultValue="1000005" />
1252
+ </FormField>
1253
+ <FormField id="w-g-pref" label="都道府県" controlWidth="10rem">
1254
+ <Select id="w-g-pref" defaultValue="JP-13" options={PREFECTURES} />
1255
+ </FormField>
1256
+ <FormField id="w-g-addr" label="住所(2列を跨ぐ)" colSpan={2}>
1257
+ <Input id="w-g-addr" defaultValue="東京都千代田区丸の内1-9-1 18F" />
1258
+ </FormField>
1259
+ </Form>
1260
+ </Flex>
1261
+ </CardContent>
1262
+ </Card>
1263
+
1264
+ {/* ════════════════════ 4. EVERY STATE ════════════════════ */}
1265
+ <Heading level={2}>4. 状態 · pristine / filled / error / disabled / read-only</Heading>
682
1266
 
683
1267
  <Card>
684
1268
  <CardHeader>
@@ -709,6 +1293,116 @@ export default function Demo() {
709
1293
  </CardContent>
710
1294
  </Card>
711
1295
 
1296
+ <Card>
1297
+ <CardHeader>
1298
+ <CardTitle level={2}>ヘルパーの位置 · 答える前に要る注記は label の下へ</CardTitle>
1299
+ <CardDescription>
1300
+ 書式や単位の注記は、入力した後に読んでも遅い。helperPlacement="before"
1301
+ はラベルと入力欄の間に置くだけで、id も aria-describedby も変わらない。ラベルを
1302
+ ReactNode にして2行目を足すと string ラベルの aria フォールバックを失い、labelAddon
1303
+ に押し込むとラベル行が2段に膨らむ。どちらも同じ見た目で壊れ方だけが違う。
1304
+ </CardDescription>
1305
+ </CardHeader>
1306
+ <CardContent>
1307
+ <Form layout="horizontal" labelWidth="9rem">
1308
+ <FormField
1309
+ id="hp-before"
1310
+ label="振込予定日"
1311
+ helper="YYYY/MM/DD · 銀行営業日のみ指定できます"
1312
+ helperPlacement="before"
1313
+ required
1314
+ >
1315
+ <Input id="hp-before" defaultValue="2026/02/27" />
1316
+ </FormField>
1317
+ <FormField
1318
+ id="hp-after"
1319
+ label="振込手数料"
1320
+ helper="当社負担の場合は0を入力"
1321
+ controlWidth="9rem"
1322
+ >
1323
+ <NumberInput id="hp-after" defaultValue={330} min={0} prefix="¥" />
1324
+ </FormField>
1325
+ </Form>
1326
+ </CardContent>
1327
+ </Card>
1328
+
1329
+ <Card>
1330
+ <CardHeader>
1331
+ <CardTitle level={2}>staticText · 編集できない値を同じフォームの同じ列に</CardTitle>
1332
+ <CardDescription>
1333
+ 発番済みのコードや作成者は入力欄ですらない。readOnly の Input
1334
+ で見せると「押せば直せそう」に見え、別の Descriptions
1335
+ ブロックに逃がすとラベル列の幅と行間を手で合わせ直すことになる。staticText
1336
+ は制御要素を持たない FormField で、同じ Form の layout · labelAlign · 行間を
1337
+ そのまま引き継ぐ。
1338
+ </CardDescription>
1339
+ </CardHeader>
1340
+ <CardContent>
1341
+ <Form layout="horizontal" labelWidth="9rem" requiredMark="optional">
1342
+ <FormField id="st-code" label="伝票番号" staticText="INV-2026-0001" />
1343
+ <FormField id="st-created" label="作成日時" staticText="2026年1月15日 09:32" />
1344
+ <FormField id="st-author" label="作成者" staticText="佐藤 花子(経理部)" />
1345
+ <FormField id="st-status" label="ステータス" staticText="承認待ち" />
1346
+ <FormField id="st-owner" label="承認者" required>
1347
+ <Select
1348
+ id="st-owner"
1349
+ name="approver"
1350
+ placeholder="選択してください"
1351
+ options={[
1352
+ { value: "tanaka", label: "田中 一郎(経理部長)" },
1353
+ { value: "suzuki", label: "鈴木 次郎(管理本部長)" },
1354
+ ]}
1355
+ />
1356
+ </FormField>
1357
+ <FormField id="st-memo" label="承認コメント" layout="vertical">
1358
+ <Textarea id="st-memo" autoGrow minRows={2} maxRows={6} />
1359
+ </FormField>
1360
+ </Form>
1361
+ </CardContent>
1362
+ </Card>
1363
+
1364
+ <Card>
1365
+ <CardHeader>
1366
+ <CardTitle level={2}>フォーム全体の無効化 · 送信中と閲覧権限</CardTitle>
1367
+ <CardDescription>
1368
+ 送信中や閲覧のみの権限では、フィールドを1つずつ disabled にして回る必要はない。Form の
1369
+ disabled が中のコントロール(ネイティブ要素まで)を まとめて止める。requiredMark を
1370
+ false にすると全項目必須の申請フォームでアスタリスクの列が消え、"optional"
1371
+ にすると逆に任意項目だけに印が付く。
1372
+ </CardDescription>
1373
+ </CardHeader>
1374
+ <CardContent>
1375
+ <Flex direction="col" gap="lg">
1376
+ <Form layout="horizontal" labelWidth="9rem" disabled>
1377
+ <FormField id="dis-name" label="取引先名" required>
1378
+ <Input id="dis-name" defaultValue="株式会社ゴドー商事" />
1379
+ </FormField>
1380
+ <FormField id="dis-pay" label="支払条件" required>
1381
+ <Select
1382
+ id="dis-pay"
1383
+ defaultValue="eom30"
1384
+ options={[{ value: "eom30", label: "月末締め翌月末払い" }]}
1385
+ />
1386
+ </FormField>
1387
+ <FormField id="dis-memo" label="社内申し送り" layout="vertical">
1388
+ <Textarea id="dis-memo" rows={2} defaultValue="送信中は編集できません。" />
1389
+ </FormField>
1390
+ </Form>
1391
+ <Form layout="horizontal" labelWidth="9rem" requiredMark={false}>
1392
+ <FormField id="nm-name" label="申請者" required>
1393
+ <Input id="nm-name" defaultValue="山田 太郎" />
1394
+ </FormField>
1395
+ <FormField id="nm-dept" label="所属部署" required>
1396
+ <Input id="nm-dept" defaultValue="営業第一部" />
1397
+ </FormField>
1398
+ <FormField id="nm-note" label="備考">
1399
+ <Input id="nm-note" placeholder="全項目必須のため印を出さない" />
1400
+ </FormField>
1401
+ </Form>
1402
+ </Flex>
1403
+ </CardContent>
1404
+ </Card>
1405
+
712
1406
  <Card>
713
1407
  <CardHeader>
714
1408
  <CardTitle level={2}>