@godxjp/ui 28.5.0 → 28.6.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 (35) hide show
  1. package/dist/components/data-entry/field.d.ts +1 -1
  2. package/dist/components/data-entry/field.js +35 -4
  3. package/dist/components/data-entry/input.d.ts +1 -1
  4. package/dist/components/layout/index.d.ts +1 -1
  5. package/dist/components/layout/nav-list.d.ts +9 -1
  6. package/dist/components/layout/nav-list.js +22 -11
  7. package/dist/components/layout/sidebar.d.ts +16 -2
  8. package/dist/components/layout/sidebar.js +1 -0
  9. package/dist/components/ui/password-input.d.ts +1 -1
  10. package/dist/contracts/measurement.json +1 -1
  11. package/dist/lib/control-styles.d.ts +18 -3
  12. package/dist/lib/control-styles.js +1 -1
  13. package/dist/props/components/data-entry.prop.d.ts +36 -2
  14. package/dist/props/components/layout.prop.d.ts +16 -5
  15. package/dist/props/registry.d.ts +1 -1
  16. package/dist/props/registry.js +8 -1
  17. package/dist/styles/control.css +12 -0
  18. package/dist/styles/data-display-layout.css +25 -2
  19. package/dist/styles/dialog-layout.css +12 -0
  20. package/dist/styles/layout.css +1 -1
  21. package/dist/tokens/components/data-display.css +2 -1
  22. package/dist/tokens/components/feedback.css +2 -0
  23. package/docs/FRAME-COVERAGE-REPORT.md +1 -1
  24. package/docs/data-display/collapsible.tsx +5 -5
  25. package/docs/data-display/descriptions.tsx +188 -109
  26. package/docs/data-display/legend.tsx +17 -4
  27. package/docs/data-display/scroll-area.tsx +75 -5
  28. package/docs/data-display/stat-card.tsx +319 -64
  29. package/docs/data-entry/form.tsx +700 -6
  30. package/docs/data-entry/input.tsx +663 -53
  31. package/docs/data-entry/radio-group.tsx +216 -2
  32. package/docs/data-entry/switch.tsx +210 -4
  33. package/docs/data-entry/upload-crop-dialog.tsx +24 -25
  34. package/docs/layout/nav-list.tsx +556 -35
  35. package/package.json +7 -6
@@ -1,9 +1,29 @@
1
1
  import { useState } from "react";
2
2
 
3
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
4
- import { Field, RadioGroup, RadioItem } from "@godxjp/ui/data-entry";
3
+ import {
4
+ Badge,
5
+ Card,
6
+ CardContent,
7
+ CardDescription,
8
+ CardHeader,
9
+ CardTitle,
10
+ } from "@godxjp/ui/data-display";
11
+ import { Field, FormField, RadioGroup, RadioItem } from "@godxjp/ui/data-entry";
5
12
  import { Flex, PageContainer } from "@godxjp/ui/layout";
6
13
 
14
+ /** 配送時間帯 · 選択肢が2〜4個を超えたときの見え方を確かめるための実データ。 */
15
+ const DELIVERY_SLOTS = [
16
+ "08:00-10:00",
17
+ "10:00-12:00",
18
+ "12:00-14:00",
19
+ "14:00-16:00",
20
+ "16:00-18:00",
21
+ "18:00-20:00",
22
+ "20:00-21:00",
23
+ ];
24
+
25
+ const CLOSING_DAYS = ["5日", "10日", "15日", "20日", "25日", "月末"];
26
+
7
27
  /**
8
28
  * RadioGroup — 2–4 mutually-exclusive choices, all visible at once (use Select
9
29
  * when the list is long or must collapse). Data-driven via an options array.
@@ -11,6 +31,8 @@ import { Flex, PageContainer } from "@godxjp/ui/layout";
11
31
  */
12
32
  export default function Demo() {
13
33
  const [controlled, setControlled] = useState("email");
34
+ const [plan, setPlan] = useState("business");
35
+
14
36
  return (
15
37
  <PageContainer
16
38
  title="RadioGroup"
@@ -34,6 +56,198 @@ export default function Demo() {
34
56
  </CardContent>
35
57
  </Card>
36
58
 
59
+ <Card>
60
+ <CardHeader>
61
+ <CardTitle level={2}>選択肢ごとの説明 · プラン選択</CardTitle>
62
+ <CardDescription>
63
+ options[].description が各行に付く。料金や条件を選択肢の外に書くと、どの行の
64
+ 説明なのかが読み手側の推測になる。末尾の Badge は label を ReactNode にして
65
+ 入れる。Badge は押せないのでラベルのクリック判定を壊さない。
66
+ </CardDescription>
67
+ </CardHeader>
68
+ <CardContent>
69
+ <RadioGroup
70
+ name="plan"
71
+ value={plan}
72
+ onValueChange={setPlan}
73
+ options={[
74
+ {
75
+ value: "standard",
76
+ label: "スタンダード",
77
+ description: "月額 4,800 円 · 5ユーザーまで · メールサポート",
78
+ },
79
+ {
80
+ value: "business",
81
+ label: (
82
+ <>
83
+ ビジネス
84
+ <Badge as="span" tone="info">
85
+ 推奨
86
+ </Badge>
87
+ </>
88
+ ),
89
+ description: "月額 12,000 円 · 30ユーザーまで · 監査ログと権限テンプレート",
90
+ },
91
+ {
92
+ value: "enterprise",
93
+ label: "エンタープライズ",
94
+ description: "個別見積 · SAML SSO · 専任担当とSLA",
95
+ },
96
+ {
97
+ value: "legacy",
98
+ label: "ライト (新規受付終了)",
99
+ disabled: true,
100
+ description: "既存契約のみ継続。移行先はスタンダードです",
101
+ },
102
+ ]}
103
+ />
104
+ </CardContent>
105
+ </Card>
106
+
107
+ <Card>
108
+ <CardHeader>
109
+ <CardTitle level={2}>ラベルの長さ · 2文字と3行が同じ群に並ぶ</CardTitle>
110
+ <CardDescription>
111
+ 短いラベルと折り返す長いラベルが同居する。丸は1行目の中心に留まり、2行目以降は
112
+ ラベル列の中で折り返す。ここが崩れると、長い選択肢だけ丸が沈んで列が波打つ。
113
+ </CardDescription>
114
+ </CardHeader>
115
+ <CardContent>
116
+ <RadioGroup
117
+ defaultValue="short"
118
+ options={[
119
+ { value: "short", label: "税" },
120
+ { value: "mid", label: "手数料を按分する" },
121
+ {
122
+ value: "long",
123
+ label:
124
+ "取引先ごとに設定された端数処理の規則を優先し、規則が未設定の取引先についてのみ、この画面で選んだ既定の規則を適用する",
125
+ description: "規則の上書きは監査ログに残ります",
126
+ },
127
+ ]}
128
+ />
129
+ </CardContent>
130
+ </Card>
131
+
132
+ <Card>
133
+ <CardHeader>
134
+ <CardTitle level={2}>optionType=&quot;button&quot; · 箱型の選択</CardTitle>
135
+ <CardDescription>
136
+ 箱で選ばせたいときはこれが唯一の描画。カード型ラジオという別コンポーネントは
137
+ 無い。role は radiogroup / radio のままなので、矢印キーの移動も name での送信も
138
+ 変わらない。buttonStyle は選択中の塗りだけを決める。
139
+ </CardDescription>
140
+ </CardHeader>
141
+ <CardContent>
142
+ <Flex direction="col" gap="md">
143
+ <RadioGroup
144
+ defaultValue="month"
145
+ optionType="button"
146
+ options={[
147
+ { value: "day", label: "日次" },
148
+ { value: "week", label: "週次" },
149
+ { value: "month", label: "月次" },
150
+ ]}
151
+ />
152
+ <RadioGroup
153
+ defaultValue="pdf"
154
+ optionType="button"
155
+ buttonStyle="solid"
156
+ options={[
157
+ { value: "pdf", label: "PDF" },
158
+ { value: "csv", label: "CSV" },
159
+ { value: "xlsx", label: "Excel" },
160
+ { value: "print", label: "印刷", disabled: true },
161
+ ]}
162
+ />
163
+ </Flex>
164
+ </CardContent>
165
+ </Card>
166
+
167
+ <Card>
168
+ <CardHeader>
169
+ <CardTitle level={2}>選択肢が多いとき</CardTitle>
170
+ <CardDescription>
171
+ 13件を横並びにした形。全件が見えるので比較はできるが、この量になると走査に時間が
172
+ かかり、Select のほうが速い。RadioGroup は「全部見せる価値がある2〜4件」が本来の
173
+ 間合い。限界の見え方を置いておかないと、どこで乗り換えるべきか判断できない。
174
+ </CardDescription>
175
+ </CardHeader>
176
+ <CardContent>
177
+ <RadioGroup
178
+ name="delivery_slot"
179
+ defaultValue="10:00-12:00"
180
+ orientation="horizontal"
181
+ options={[
182
+ ...DELIVERY_SLOTS.map((slot) => ({ value: slot, label: slot })),
183
+ ...CLOSING_DAYS.map((day) => ({ value: day, label: `${day}締め` })),
184
+ ]}
185
+ />
186
+ </CardContent>
187
+ </Card>
188
+
189
+ <Card>
190
+ <CardHeader>
191
+ <CardTitle level={2}>選択肢が1つだけ</CardTitle>
192
+ <CardDescription>
193
+ 一度選ぶと解除できない。選択の取り消しが要るなら Checkbox、即時に効く オン・オフなら
194
+ Switch が正しい。1件のラジオを置いてしまう事故は、この見え方を 知らないまま起きる。
195
+ </CardDescription>
196
+ </CardHeader>
197
+ <CardContent>
198
+ <RadioGroup
199
+ name="verification"
200
+ options={[
201
+ {
202
+ value: "verified",
203
+ label: "本人確認済みとして登録する",
204
+ description: "解除するには管理者への申請が必要です",
205
+ },
206
+ ]}
207
+ />
208
+ </CardContent>
209
+ </Card>
210
+
211
+ <Card>
212
+ <CardHeader>
213
+ <CardTitle level={2}>FormField · 必須とエラー</CardTitle>
214
+ <CardDescription>
215
+ radiogroup は widget なので、aria-invalid / aria-errormessage / aria-required
216
+ まで含めた検証の配線を FormField がそのまま渡せる。エラー文は role=alert で
217
+ 読み上げられる。readOnly は RadioGroup に無く、変更させない群は disabled で表す。
218
+ </CardDescription>
219
+ </CardHeader>
220
+ <CardContent>
221
+ <Flex direction="col" gap="md">
222
+ <FormField id="closing-day" label="締め日" required error="締め日を選択してください">
223
+ <RadioGroup
224
+ id="closing-day"
225
+ name="closing_day"
226
+ orientation="horizontal"
227
+ options={CLOSING_DAYS.map((day) => ({ value: day, label: day }))}
228
+ />
229
+ </FormField>
230
+ <FormField
231
+ id="tax-rounding"
232
+ label="消費税の端数処理"
233
+ helper="取引先との契約に合わせて選びます"
234
+ >
235
+ <RadioGroup
236
+ id="tax-rounding"
237
+ name="tax_rounding"
238
+ defaultValue="floor"
239
+ orientation="horizontal"
240
+ options={[
241
+ { value: "round", label: "四捨五入" },
242
+ { value: "floor", label: "切り捨て" },
243
+ { value: "ceil", label: "切り上げ" },
244
+ ]}
245
+ />
246
+ </FormField>
247
+ </Flex>
248
+ </CardContent>
249
+ </Card>
250
+
37
251
  <Card>
38
252
  <CardHeader>
39
253
  <CardTitle level={2}>Controlled · named · disabled</CardTitle>
@@ -1,13 +1,38 @@
1
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
2
- import { Field, Switch } from "@godxjp/ui/data-entry";
1
+ import { useState } from "react";
2
+ import { CircleHelp } from "lucide-react";
3
+
4
+ import {
5
+ Badge,
6
+ Card,
7
+ CardContent,
8
+ CardDescription,
9
+ CardHeader,
10
+ CardTitle,
11
+ ListRow,
12
+ } from "@godxjp/ui/data-display";
13
+ import { Field, Label, Switch } from "@godxjp/ui/data-entry";
14
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@godxjp/ui/feedback";
15
+ import { Button, Icon } from "@godxjp/ui/general";
3
16
  import { Flex, PageContainer } from "@godxjp/ui/layout";
4
17
 
5
18
  /**
6
19
  * Switch — a boolean toggle. Wrap in Field (NOT FormField) for a labelled row
7
- * with the hidden form input + description. Composed only from real @godxjp/ui
8
- * components.
20
+ * with the hidden form input + description; put it in ListRow's `trailing` when
21
+ * the settings row wants the toggle at the END of the line. Composed only from
22
+ * real @godxjp/ui components.
9
23
  */
10
24
  export default function Demo() {
25
+ // 公開設定は保存に往復が要る。loading 中のトグルは値を変えずに要求だけを拒否する。
26
+ const [published, setPublished] = useState(false);
27
+ const [saving, setSaving] = useState(false);
28
+ const savePublished = (next: boolean) => {
29
+ setSaving(true);
30
+ window.setTimeout(() => {
31
+ setPublished(next);
32
+ setSaving(false);
33
+ }, 1200);
34
+ };
35
+
11
36
  return (
12
37
  <PageContainer title="Switch" subtitle="Boolean toggle · wrap in Field for a labelled row">
13
38
  <Flex direction="col" gap="lg">
@@ -35,6 +60,187 @@ export default function Demo() {
35
60
  </CardContent>
36
61
  </Card>
37
62
 
63
+ <Card>
64
+ <CardHeader>
65
+ <CardTitle level={2}>設定セクション · ListRow の trailing に置く</CardTitle>
66
+ <CardDescription>
67
+ 設定画面の1行は「見出しと説明が左、トグルが右端」。Field は制御を先頭に置くので
68
+ この並びにはならない。ListRow の trailing に Switch を入れ、title を Label htmlFor
69
+ にして紐づける。これが無いと、行ごとにトグル位置が揃わず目で追えなくなる。
70
+ </CardDescription>
71
+ </CardHeader>
72
+ <CardContent flush>
73
+ <ListRow
74
+ align="start"
75
+ overflow="wrap"
76
+ title={<Label htmlFor="set-2fa">二要素認証</Label>}
77
+ description="ログイン時にワンタイムコードの入力を求めます。"
78
+ trailing={<Switch id="set-2fa" defaultChecked />}
79
+ />
80
+ <ListRow
81
+ align="start"
82
+ overflow="wrap"
83
+ title={
84
+ <Label htmlFor="set-ocr">
85
+ レシート OCR
86
+ <Badge as="span" tone="info">
87
+ ベータ
88
+ </Badge>
89
+ </Label>
90
+ }
91
+ description="添付画像から金額と日付を読み取って仕訳の下書きを作ります。"
92
+ trailing={<Switch id="set-ocr" />}
93
+ />
94
+ <ListRow
95
+ align="start"
96
+ overflow="wrap"
97
+ title={<Label htmlFor="set-publish">取引先ポータルへの公開</Label>}
98
+ description="保存に数秒かかります。通信中は loading のまま操作を受け付けません。"
99
+ trailing={
100
+ <Switch
101
+ id="set-publish"
102
+ checked={published}
103
+ onCheckedChange={savePublished}
104
+ loading={saving}
105
+ />
106
+ }
107
+ />
108
+ <ListRow
109
+ align="start"
110
+ overflow="wrap"
111
+ title={<Label htmlFor="set-lock">会計期間のロック</Label>}
112
+ description="決算確定済みのため解除できません。管理者に依頼してください。"
113
+ trailing={<Switch id="set-lock" disabled defaultChecked />}
114
+ />
115
+ </CardContent>
116
+ </Card>
117
+
118
+ <Card>
119
+ <CardHeader>
120
+ <CardTitle level={2}>ラベルの長さ · 2文字と3行が同じ列に並ぶ</CardTitle>
121
+ <CardDescription>
122
+ 同じ設定群に2文字の見出しと3行に折り返す見出しが混ざる。つまみが1行目の中心に
123
+ 留まるかはここでしか分からない。揃っていないと、長い行だけトグルが沈んで見える。
124
+ </CardDescription>
125
+ </CardHeader>
126
+ <CardContent>
127
+ <Flex direction="col" gap="md">
128
+ <Field id="sw-len-short" label="税" description="一覧の金額を税抜で表示します">
129
+ <Switch id="sw-len-short" defaultChecked />
130
+ </Field>
131
+ <Field
132
+ id="sw-len-long"
133
+ label="請求書の発行時に、取引先の経理担当窓口へ PDF を自動送付し、送付ログを電子帳簿保存法の要件に従って7年間保管する"
134
+ description="送付先が未登録の取引先はスキップし、月次レポートに一覧で残します"
135
+ >
136
+ <Switch id="sw-len-long" />
137
+ </Field>
138
+ <Field id="sw-len-mid" label="自動保存" description="30秒ごとに下書きを保存します">
139
+ <Switch id="sw-len-mid" defaultChecked />
140
+ </Field>
141
+ </Flex>
142
+ </CardContent>
143
+ </Card>
144
+
145
+ <Card>
146
+ <CardHeader>
147
+ <CardTitle level={2}>状態の一覧</CardTitle>
148
+ <CardDescription>
149
+ オフ / オン / 無効 / 無効かつオン / 保存中 / 不正。readOnly と indeterminate は Switch
150
+ に存在しない。読み取り専用は disabled で表し、中間状態が要るなら Checkbox の
151
+ indeterminate を使う。どれが操作できる行なのか、並べないと判別できない。
152
+ </CardDescription>
153
+ </CardHeader>
154
+ <CardContent>
155
+ <Flex direction="col" gap="md">
156
+ <Field id="sw-state-off" label="オフ" description="既定値">
157
+ <Switch id="sw-state-off" />
158
+ </Field>
159
+ <Field id="sw-state-on" label="オン" description="defaultChecked">
160
+ <Switch id="sw-state-on" defaultChecked />
161
+ </Field>
162
+ <Field id="sw-state-disabled" label="無効" description="権限がありません">
163
+ <Switch id="sw-state-disabled" disabled />
164
+ </Field>
165
+ <Field
166
+ id="sw-state-disabled-on"
167
+ label="無効かつオン"
168
+ description="上位の設定で固定されています"
169
+ >
170
+ <Switch id="sw-state-disabled-on" disabled defaultChecked />
171
+ </Field>
172
+ <Field id="sw-state-loading" label="保存中" description="aria-busy / aria-disabled">
173
+ <Switch id="sw-state-loading" loading defaultChecked />
174
+ </Field>
175
+ {/* 不正状態は Field の error スロットが持つ。role=alert のメッセージを出し、
176
+ 制御に aria-invalid / aria-errormessage / aria-describedby を配線する。
177
+ description と error は競合せず、上下に積む。 */}
178
+ <Field
179
+ id="sw-state-invalid"
180
+ label="電子帳簿保存に対応する"
181
+ description="保存先を設定すると有効にできます"
182
+ error="保存先のストレージが未設定のため有効にできません。"
183
+ >
184
+ <Switch id="sw-state-invalid" />
185
+ </Field>
186
+ </Flex>
187
+ </CardContent>
188
+ </Card>
189
+
190
+ <Card>
191
+ <CardHeader>
192
+ <CardTitle level={2}>ラベル脇の補助 · labelAddon</CardTitle>
193
+ <CardDescription>
194
+ Badge のような非対話の印は label に直接置ける。押せる補助は labelAddon に渡す。 label
195
+ は制御を指す本物の &lt;label htmlFor&gt; なので、その中に入れたボタンは
196
+ 押した瞬間にトグルを反転させる。labelAddon は label の外の兄弟として行内に並ぶ。
197
+ </CardDescription>
198
+ </CardHeader>
199
+ <CardContent>
200
+ <Flex direction="col" gap="md">
201
+ {/* 非対話の addon は label の中で足りる。Label は ui-label の gap を持つ。 */}
202
+ <Field
203
+ id="sw-addon-badge"
204
+ label={
205
+ <>
206
+ 自動仕訳
207
+ <Badge as="span" tone="info">
208
+ ベータ
209
+ </Badge>
210
+ </>
211
+ }
212
+ description="学習済みの仕訳パターンから勘定科目を推定します"
213
+ >
214
+ <Switch id="sw-addon-badge" defaultChecked />
215
+ </Field>
216
+ {/* 押せる補助は labelAddon。label の外にあるので押してもトグルは動かない。 */}
217
+ <Field
218
+ id="sw-addon-help"
219
+ label="源泉徴収の自動計算"
220
+ description="報酬の支払時に源泉徴収額を自動で行に足します"
221
+ labelAddon={
222
+ <Tooltip>
223
+ <TooltipTrigger asChild>
224
+ <Button
225
+ variant="ghost"
226
+ size="icon-sm"
227
+ aria-label="源泉徴収の自動計算について"
228
+ >
229
+ <Icon as={CircleHelp} size="sm" tone="muted" />
230
+ </Button>
231
+ </TooltipTrigger>
232
+ <TooltipContent>
233
+ 税率は支払先の区分と支払額から決まります。確定値は請求書の明細に残ります。
234
+ </TooltipContent>
235
+ </Tooltip>
236
+ }
237
+ >
238
+ <Switch id="sw-addon-help" />
239
+ </Field>
240
+ </Flex>
241
+ </CardContent>
242
+ </Card>
243
+
38
244
  <Card>
39
245
  <CardHeader>
40
246
  <CardTitle level={2}>size · sm / md</CardTitle>
@@ -1,4 +1,4 @@
1
- import { useRef, useState } from "react";
1
+ import { useState } from "react";
2
2
 
3
3
  import {
4
4
  Avatar,
@@ -11,7 +11,7 @@ import {
11
11
  CardTitle,
12
12
  } from "@godxjp/ui/data-display";
13
13
  import { Upload, UploadCropDialog } from "@godxjp/ui/data-entry";
14
- import { Button, Text } from "@godxjp/ui/general";
14
+ import { Text } from "@godxjp/ui/general";
15
15
  import { Flex, PageContainer } from "@godxjp/ui/layout";
16
16
 
17
17
  /**
@@ -31,7 +31,6 @@ export default function Demo() {
31
31
  const [cropFile, setCropFile] = useState<File | null>(null);
32
32
  const [avatar, setAvatar] = useState<string | null>(null);
33
33
  const [lastResult, setLastResult] = useState<string | null>(null);
34
- const pickerRef = useRef<HTMLInputElement>(null);
35
34
 
36
35
  return (
37
36
  <PageContainer title="UploadCropDialog" subtitle="切り抜きだけを単体で使う場合">
@@ -46,31 +45,31 @@ export default function Demo() {
46
45
  </CardHeader>
47
46
  <CardContent>
48
47
  <Flex direction="col" gap="md">
49
- <Flex direction="row" gap="md" align="center" wrap>
50
- <Avatar size="lg">
51
- {avatar ? <AvatarImage src={avatar} alt="切り抜いたプロフィール写真" /> : null}
52
- <AvatarFallback>田</AvatarFallback>
53
- </Avatar>
54
- <Button onClick={() => pickerRef.current?.click()}>写真を選ぶ</Button>
55
- </Flex>
48
+ <Avatar size="lg">
49
+ {avatar ? <AvatarImage src={avatar} alt="切り抜いたプロフィール写真" /> : null}
50
+ <AvatarFallback>田</AvatarFallback>
51
+ </Avatar>
56
52
 
57
- {/* The picker is the CONSUMER's, not the dialog's — that is the whole point of using
58
- this component standalone. It is hidden because the Button above is the affordance;
59
- a visible raw control beside a godx Button would be two controls for one job. */}
60
- <input
61
- ref={pickerRef}
62
- type="file"
53
+ {/* The picker is the PAGE's, not the dialog's — that is the whole point of using this
54
+ component standalone. It is `Upload variant="button"` rather than a raw
55
+ <input type="file">: rule §3 forbids a raw control in an example, because a
56
+ consumer copies what they see. `onUpload` hands back the File, which is exactly
57
+ what UploadCropDialog wants. */}
58
+ <Upload
59
+ variant="button"
63
60
  accept="image/*"
64
- className="sr-only"
65
- aria-hidden
66
- tabIndex={-1}
67
- onChange={(event) => {
68
- const file = event.target.files?.[0] ?? null;
69
- setCropFile(file);
70
- // Clear the input so re-picking the SAME file fires change again.
71
- event.target.value = "";
61
+ maxCount={1}
62
+ onValueChange={(items) => {
63
+ /* No onUpload here on purpose: this page wants the FILE, not an upload. The
64
+ catalog states it outright — "without onUpload the File object sits in
65
+ item.file until you manually process it", which is exactly the standalone
66
+ crop case. */
67
+ const picked = items[0]?.file ?? null;
68
+ if (picked) setCropFile(picked);
72
69
  }}
73
- />
70
+ >
71
+ 写真を選ぶ
72
+ </Upload>
74
73
 
75
74
  <Text size="sm" tone="muted" aria-live="polite">
76
75
  {lastResult ?? "まだ切り抜いていません。"}