sync: update from private repo (dfadcd5f)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-23 06:38:48 +00:00
parent 6a2f2cc736
commit 29ccaf1e92
377 changed files with 31028 additions and 8994 deletions
+210
View File
@@ -0,0 +1,210 @@
/**
* ファイルブラウザのツールバー部品群(スペース / タスク共有)。
* いずれも状態を持たない純プレゼンテーショナル。drag 状態だけ FileDropzone が内部に持つ。
*
* - FileActions … 「追加」ボタン + 隠し input + 再読み込み
* - FileSelectionBar … すべて選択 + 選択件数 + 削除(複数選択)
* - FileDropzone … 子をラップし、ドラッグ&ドロップでアップロードを受ける
*/
import { useEffect, useRef, useState } from 'react';
export { filesToBase64 } from '../../lib/fileBase64';
const ICON_BTN =
'w-8 h-8 flex items-center justify-center rounded-md border border-hairline bg-canvas text-slate-500 hover:text-slate-900 hover:bg-surface transition-colors';
export function FileActions({
idPrefix,
canUpload,
onUploadFiles,
onRefresh,
isRefreshing,
isUploading,
}: {
idPrefix: string;
canUpload: boolean;
onUploadFiles: (files: File[]) => void;
onRefresh: () => void;
isRefreshing?: boolean;
isUploading?: boolean;
}) {
const inputRef = useRef<HTMLInputElement>(null);
return (
<div className="flex shrink-0 items-center gap-1.5">
{canUpload && (
<>
<input
ref={inputRef}
type="file"
multiple
data-testid={`${idPrefix}-files-upload-input`}
className="hidden"
onChange={e => {
const files = Array.from(e.target.files ?? []);
e.target.value = '';
onUploadFiles(files);
}}
/>
<button
type="button"
data-testid={`${idPrefix}-files-upload-btn`}
onClick={() => inputRef.current?.click()}
disabled={isUploading}
className="inline-flex h-8 items-center gap-1 rounded-md border border-hairline bg-canvas px-2.5 text-2xs font-medium text-slate-600 transition-colors hover:bg-surface hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 disabled:opacity-50"
>
<svg className="h-3.5 w-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<path d="M8 3.5v9M3.5 8h9" />
</svg>
</button>
</>
)}
<button
type="button"
onClick={onRefresh}
disabled={isRefreshing}
className={`${ICON_BTN} disabled:opacity-50`}
title="再読み込み"
aria-label="再読み込み"
>
<svg className={`h-3.5 w-3.5 ${isRefreshing ? 'animate-spin' : ''}`} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<path d="M2 8a6 6 0 0110.5-4M14 8a6 6 0 01-10.5 4" />
<path d="M12 2v3h-3M4 14v-3h3" />
</svg>
</button>
</div>
);
}
export function FileSelectionBar({
idPrefix,
allSelected,
onToggleSelectAll,
selectedCount,
onDeleteSelected,
onDownloadSelected,
isDeleting,
isDownloading,
}: {
idPrefix: string;
allSelected: boolean;
onToggleSelectAll: () => void;
selectedCount: number;
onDeleteSelected: () => void;
onDownloadSelected: () => void;
isDeleting?: boolean;
isDownloading?: boolean;
}) {
return (
<div className="flex flex-wrap items-center gap-2 text-2xs text-slate-500">
<label className="inline-flex cursor-pointer items-center gap-1.5 select-none">
<input
type="checkbox"
data-testid={`${idPrefix}-files-select-all`}
checked={allSelected}
onChange={onToggleSelectAll}
className="h-3.5 w-3.5 rounded border-hairline accent-[var(--brand-primary)]"
/>
</label>
<span data-testid={`${idPrefix}-files-selected-count`} className="text-slate-400">
{selectedCount}
</span>
{selectedCount > 0 && (
<div className="ml-auto flex items-center gap-1.5">
<button
type="button"
data-testid={`${idPrefix}-files-download-selected`}
onClick={onDownloadSelected}
disabled={isDownloading}
className="inline-flex h-7 items-center gap-1 rounded-md border border-hairline bg-canvas px-2.5 text-2xs font-medium text-slate-600 transition-colors hover:bg-surface hover:text-slate-900 disabled:opacity-50"
>
<svg className="h-3.5 w-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M8 2v8M4.5 6.5L8 10l3.5-3.5M2.5 13h11" />
</svg>
</button>
<button
type="button"
data-testid={`${idPrefix}-files-delete-selected`}
onClick={onDeleteSelected}
disabled={isDeleting}
className="inline-flex h-7 items-center gap-1 rounded-md border border-red-200 bg-red-50 px-2.5 text-2xs font-bold text-red-700 transition-colors hover:bg-red-100 disabled:opacity-50 dark:border-red-500/30 dark:bg-red-500/15 dark:text-red-300"
>
<svg className="h-3.5 w-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 4h10M6.5 4V2.5h3V4M5 4l.5 9h5l.5-9" />
</svg>
</button>
</div>
)}
</div>
);
}
export function FileDropzone({
idPrefix,
enabled,
isUploading,
onDropFiles,
onRejectFolder,
children,
}: {
idPrefix: string;
/** false のときドロップを受け付けない(オーバーレイも出さない)。 */
enabled: boolean;
isUploading?: boolean;
onDropFiles: (files: File[]) => void;
/** フォルダがドロップされたときのコールバック(未対応メッセージ表示用)。 */
onRejectFolder?: () => void;
children: React.ReactNode;
}) {
const [dragDepth, setDragDepth] = useState(0);
// ウィンドウ外への drag 離脱や drop 取りこぼしで dragDepth が戻らずオーバーレイが
// 残るのを防ぐ。window レベルの dragend/drop で必ず 0 に戻す。
useEffect(() => {
const reset = () => setDragDepth(0);
window.addEventListener('dragend', reset);
window.addEventListener('drop', reset);
return () => {
window.removeEventListener('dragend', reset);
window.removeEventListener('drop', reset);
};
}, []);
// testid と要素は常に出し、enabled=falseread-only)のときは DnD ハンドラと
// オーバーレイだけ無効化する(既存 e2e の dropzone testid を温存する)。
return (
<div
data-testid={`${idPrefix}-files-dropzone`}
className="relative min-h-[6rem] rounded-lg"
onDragEnter={enabled ? (e => { e.preventDefault(); setDragDepth(d => d + 1); }) : undefined}
onDragOver={enabled ? (e => { e.preventDefault(); }) : undefined}
onDragLeave={enabled ? (e => { e.preventDefault(); if (!e.relatedTarget) { setDragDepth(0); } else { setDragDepth(d => Math.max(0, d - 1)); } }) : undefined}
onDrop={enabled ? (e => {
e.preventDefault();
setDragDepth(0);
const hasDirectory = Array.from(e.dataTransfer.items ?? []).some(
it => it.webkitGetAsEntry?.()?.isDirectory,
);
const files = Array.from(e.dataTransfer.files);
if (hasDirectory) { onRejectFolder?.(); return; }
if (files.length === 0) return;
onDropFiles(files);
}) : undefined}
>
{enabled && dragDepth > 0 && (
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-lg border-2 border-dashed border-slate-400 bg-canvas/85 text-sm font-medium text-slate-600">
</div>
)}
{enabled && isUploading && (
<div className="pointer-events-none absolute right-1 top-1 z-10 rounded bg-slate-800/80 px-2 py-0.5 text-2xs text-white">
</div>
)}
{children}
</div>
);
}