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 では成功も照会中も表現できない。
);
}