import { useState } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display"; import { Form, Upload, type UploadFileItem, type UploadRequestContext, } from "@godxjp/ui/data-entry"; import { Button, Text } from "@godxjp/ui/general"; import { Flex, PageContainer } from "@godxjp/ui/layout"; import { FilePlus2, Plus } from "lucide-react"; /** * Upload — drag-and-drop / button / avatar / picture file uploader (6 variants). * Wire onUpload to your media-service; call collectUploadCommitActions on submit. * UploadCropDialog is embedded internally by variant="avatar-crop" — not a public import. * Composed only from real @godxjp/ui components. */ /** Simulated transport: progress, abort, and retry remain observable in the preview. */ async function demoUpload(file: File, _item: UploadFileItem, context: UploadRequestContext) { for (let percent = 0; percent <= 100; percent += 20) { await new Promise((resolve) => setTimeout(resolve, 180)); context.signal.throwIfAborted(); context.onProgress(percent); } if (file.name.startsWith("fail")) throw new Error("アップロードに失敗しました。再試行できます。"); return { mediaId: crypto.randomUUID() }; } export default function Demo() { const [submittedFiles, setSubmittedFiles] = useState([]); const [dropzoneItems, setDropzoneItems] = useState([]); const [buttonItems, setButtonItems] = useState([]); const [triggerIconItems, setTriggerIconItems] = useState([]); const [pictureCardItems, setPictureCardItems] = useState([]); const [pictureItem, setPictureItem] = useState([]); const [avatarItem, setAvatarItem] = useState([]); const [avatarCropItem, setAvatarCropItem] = useState([]); /** A test-result drawer: one screenshot, one machine-readable result, one log, one archive. */ const mixedItems: UploadFileItem[] = [ { uid: "mixed-screenshot", name: "screenshot.png", size: 184 * 1024, mimeType: "image/png", status: "done", previewUrl: "data:image/svg+xml;utf8,", }, // Server media whose thumbnail has not been generated yet: still an image, still marked as one. { uid: "mixed-original", name: "photo-original.png", size: 4 * 1024 * 1024, status: "done" }, { uid: "mixed-result", name: "result.json", size: 12 * 1024, status: "done" }, { uid: "mixed-log", name: "run.txt", size: 3 * 1024, status: "done" }, { uid: "mixed-bundle", name: "evidence.zip", size: 2 * 1024 * 1024, status: "done" }, { uid: "mixed-spec", name: "spec.pdf", size: 640 * 1024, status: "done" }, { uid: "mixed-dump", name: "device-dump", size: 96 * 1024, status: "done" }, ]; const failedItems: UploadFileItem[] = [ { uid: "failed-invoice", name: "invoice-too-large.pdf", size: 25 * 1024 * 1024, mimeType: "application/pdf", status: "error", error: "20 MB 以下のファイルを選択してください", }, ]; return ( Multipart form 選択・ドロップ・貼り付けしたファイルをフォームでまとめて送信します。
setSubmittedFiles([])} onSubmit={(event) => { event.preventDefault(); const data = new FormData(event.currentTarget); setSubmittedFiles( data .getAll("attachments[]") .filter((value): value is File => value instanceof File) .map((value) => value.name), ); }} > {submittedFiles.join("、")}
Dropzone ドラッグ&ドロップエリア · PDF・Excel などの書類添付に使用。 List type · 添付ファイル一覧 listType は variant と独立した軸です。dropzone のまま listType="picture" にすると、画像はサムネイル、それ以外はファイル種別のアイコンで並びます。 {}} onDownload={() => {}} /> Error and recovery 失敗したファイルは再試行できます。処理中のファイルはキャンセルできます。 {}} removable /> Disabled · populated 権限不足時も既存ファイル名を保ったまま操作を無効化する。 {}} disabled /> Button コンパクトなボタン形式 · CSVインポートなどの管理画面向け。 CSVをインポート triggerIcon · 「新規作成」がアップロードのとき variant="button" のトリガーが描くグリフを差し替える。コンポーネントを渡す(triggerIcon={Plus}、 要素ではない)。triggerVariant は強弱しか変えられないので、実体はアップロードでも「新規作成」として並ぶボタンは これまで矢印を背負うしかなかった。クラスと余白と aria-hidden はライブラリ側が持ったままなので、差し替えても大きさは --upload-row-icon-size のまま、読み上げにも出ない。 資料を追加 {}} aria-label="請求書を添付" /> {}}> 既定のグリフ Picture Card 96×96 サムネイルグリッド · 商品ギャラリーや画像一覧に使用。 Picture 単一画像プレビュー · コンテンツのカバー画像選択に使用。 Avatar 円形の単一画像ピッカー · ユーザーアイコン変更に使用。 Avatar Crop アバター選択後にトリミングダイアログを開く。UploadCropDialog は 内部モーダルとして自動で埋め込まれるため、個別インポート不要。
); }