import { useState } from "react"; import { Building2, CalendarDays, Hash, Link2, Mail, MapPin, Phone } from "lucide-react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display"; import { FormField, Input, NumberInput, PasswordInput, SearchInput } from "@godxjp/ui/data-entry"; import { Icon, Text } from "@godxjp/ui/general"; import { Flex, PageContainer, ResponsiveGrid } from "@godxjp/ui/layout"; /** * Input — styled wrapper around the native input. Always pair with FormField for * a labelled, a11y-wired field. Never a raw . Composed only from real * @godxjp/ui components. * * This page is deliberately long: a text field is trivial with a short placeholder * and an empty box, so every section below puts something in it that a real * enterprise form puts in it — a 40-character machine id, a three-line Japanese * label, an address that overflows the box, zero versus empty, Arabic. */ export default function Demo() { const [contractValue, setContractValue] = useState("制御値"); const [emptyText, setEmptyText] = useState(""); const [zeroText, setZeroText] = useState("0"); const [emptyNumber, setEmptyNumber] = useState(null); const [zeroNumber, setZeroNumber] = useState(0); const [overflow, setOverflow] = useState( "東京都港区芝公園四丁目2番8号 東京タワーフットタウン3階 株式会社ゴドー商事 経理部 請求管理課", ); const [partnerQuery, setPartnerQuery] = useState("ゴドー"); return ( サイズ · 高さラダー WHY: 一行に複数のコントロールが並ぶツールバーやフィルタ行では、高さが 1px でもずれると行が波打つ。横に並べて初めて段差が見える。size は xs / sm / md / lg の4段で、既定は md。xs は長らく Input の型にだけ無く、CSS もトークンも揃っていたのに union が塞いでいた。 同じ段をアフィックス付きでも揃える。prefix / addon は箱の高さを継承するので、段が変わっても行は揃ったまま。 状態 · 既定 / 入力済み / disabled / readOnly WHY: disabled と readOnly を取り違えると、送信されるはずの値が消える。disabled は操作もフォーカスも送信もしない。readOnly はフォーカスでき、選択・コピーでき、値はそのまま送信される。空のフィールドに Tab で入るとフォーカスリングが出る(静的な画面では再現できないので自分で試すこと)。 検証状態 · error / warning / success / validating WHY: 赤い枠だけでは「何が悪いのか」は伝わらない。error は枠と文言の両方が要る。success と validating(サーバー照会中)は Input の status には無く、FormField の validateStatus + hasFeedback が持つ。理由は非対称ではなく分担で、アイコン付きフィードバックの欄は FormField の持ち物だから。 status は Input が自分で塗る2値(error / warning)。error だけが aria-invalid も立てるので、赤い枠とスクリーンリーダーが聞く内容が一致する。 アフィックス · 箱の内側 (prefix / suffix) WHY: 単位のない数字は読み手に単位を推測させる。prefix / suffix は箱のパディングの内側に入るので、値と同じ一つの塊に見える。prefix は aria-hidden ではない(単位は意味であって装飾ではない)。 アドオン · 箱の外側 (addonBefore / addonAfter) WHY: プロトコルやドメイン接尾辞はユーザーが打つ値ではない。addon は独立した面として境界の外側に溶接され、接合側の角を閉じる。つまり「打つ場所」と「打たない場所」が形で分かれる。 } defaultValue="480" /> アイコンとクリア · 先頭・末尾は1つずつ WHY: 末尾のスロットは1つしかない。allowClear に値があるとき、クリアの✕は設定した trailingIcon を「置き換える」。両方が同時に出ることはない。これを知らずに自前の✕を重ねると二重になる。 } placeholder="取引先名で絞り込む" /> } defaultValue="2026-09-30" readOnly /> } trailingIcon={} defaultValue="keiri@godo-shoji.co.jp" /> setContractValue("")} leadingIcon={} /> } /> すぐ上の2つは値を持っているので、設定した trailingIcon ではなく✕が出ている。✕ で空にすると trailingIcon が戻る。 カウンタは超過を「報告」するだけで、値を切らない。入力中の IME 変換を途中で刈り取らないための判断。 実データの型 · メール / 電話 / 郵便番号 / URL / 機械ID WHY: type と inputMode はモバイルのキーボードと自動入力を決める。既定の文字キーボードで電話番号を打たせるのは設計の放棄。40 文字の機械 ID は箱より長いので、ここで初めて「はみ出したらどうなるか」が見える。 } defaultValue="keiri@godo-shoji.co.jp" /> } defaultValue="03-6205-3371" /> } addonBefore="〒" defaultValue="105-0011" /> } addonBefore="https://" defaultValue="www.godo-shoji.co.jp" /> 数値と金額 · 桁が動かないこと WHY: 金額を縦に並べたとき、プロポーショナル数字だと桁の位置が1行ごとにずれて比較できない。Input には tabular プロップが無いので等幅数字はクラスで足している(既知のギャップ)。刻み・クランプ・ロケール整形が要るなら Input ではなく NumberInput が正解。 Input · 等幅数字なし(桁が揃わない) Input · 等幅数字あり(桁が揃う) NumberInput · ステッパー・クランプ・Intl 整形つき 専用コンポーネントに譲る場面 WHY: Input で代用すると必ず何かが抜ける。パスワードなら表示切替・CapsLock・autocomplete 契約、検索ならデバウンスとクリアと role=searchbox。3つとも自前実装では落ちる。 {/* xs を PasswordInput でも一度は描く。size は Input の型をそのまま継いでいるので、 片方だけ描いて済ませると frame-contracts が「宣言した分岐に証拠が無い」と落ちる ── 公開した分岐は描いて証明する、というのがこのリポジトリの契約。 */} 逆に、フォーム送信で name を伴う検索欄なら SearchInput ではなく FormField + Input が正しい。SearchInput は値を送らないフィルタ用のウィジェット。 幅の振る舞い · 箱は常に親の幅を埋める WHY: Input に width プロップは無い(既知のギャップ)。箱は必ず親を 100% 埋めるので、幅を決めるのは常に外側のレイアウト。これを知らないと「なぜツールバーで巨大になるのか」が分からない。 フォームの中 · 全幅(FormField がそのまま親を埋める) ツールバーの中 · 内容に合わせて親側で詰める 短い項目と長い項目を同じ行に · 比率は親のグリッドが決める ストレス · 箱に収まらない値 WHY: 実務の住所・部署名は必ず箱より長い。Input は単一行なので折り返さず、省略記号も付けず、箱の中で横スクロールする。フォーカスを抜けると先頭に戻る。長さが問題になるなら Textarea か、値を全部見せる Descriptions に譲る判断が要る。 setOverflow("")} /> ストレス · 空とゼロは別物 WHY: 「0」と「未入力」を同じものとして扱うと、値引き0円と値引き未定の区別が消える。テキストの Input では空文字が「未入力」、"0" は入力済み。NumberInput では null が「未入力」で、0 に置き換えてはいけない。 プレースホルダーが見えている側が「未入力」。✕ で空にすると、両者は見た目でも状態でも同じになる。 ストレス · ラベルの長さが揃わない行 WHY: 日本語のラベルは2文字にも3行にもなる。ラベル行が伸びても入力欄の上端が揃うのは、行の align が start で、各セルが独立して縦に伸びるから。center にすると短いラベルの箱が下がって崩れる。 ストレス · RTL WHY: prefix / addonBefore は「開始側」であって「左」ではない。論理プロパティで書かれているので dir="rtl" で自動的に右端へ移る。物理方向のクラスを1つでも混ぜると、ここで崩れる。 } allowClear /> chrome の段 · variant WHY: 既に枠を描いている面(テーブルのセル、カードの中の小さな箱)に outlined を入れると線が二重になる。borderless はその場合の答えで、密なフォームでは filled が境界の数を減らす。 既知のギャップ このページを書いている途中で見つかった、Input 側の穴。回避策で隠さずここに書いておく。 1. size に xs 段が無い。NumberInput・Select・Button は xs を受けるのに InputProp は sm / md / lg だけ。CSS 側には .ui-control[data-size="xs"] も --control-height-xs もあるので、塞がっているのは型だけ。 2. tabular プロップが無い。Text と Badge は tabular を持つのに、金額を縦に並べる Input と NumberInput は等幅数字をクラスで足すしかない。 3. width プロップが無い。箱は常に親を 100% 埋めるので、ツールバーでは必ず外側に幅を持つ箱が要る。 4. status に success / validating が無い。FormField の validateStatus + hasFeedback で足りてはいるが、ラベルの無い裸の Input では成功も照会中も表現できない。 ); }