console files: one-row toolbar, more rows on screen

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
This commit is contained in:
phantomptrandClaude Sonnet 5.5 committed 2026-10-17 12:18:29 -07:00
1 parent f7daeec308
commit 4fcb9e913a
46 files changed
+276 -130

No files matched your search

+88
View File
@@ -1213,3 +1213,91 @@ button {
height: 1.2rem;
margin-right: 0.7rem;
}
/* ---- Files: one toolbar row, more rows on screen ---- */
.cw-files-screen .cw-h1 {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
.cw-toolbar-one {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 0.2rem;
}
.cw-toolbar-one .cw-pathbar {
flex: 1;
margin: 0 0.8rem 0 0;
}
.cw-tools {
flex: none;
display: flex;
align-items: center;
}
.cw-tool {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.6rem;
height: 2.4rem;
margin-left: 0.3rem;
color: inherit;
background: var(--color-surface-3);
}
.cw-tool svg {
width: 1.3rem;
height: 1.3rem;
}
.cw-tool[aria-pressed="true"] {
background: var(--color-accent-soft);
color: var(--color-accent);
}
.cw-tool.is-pinned svg {
fill: currentColor;
}
.cw-tool:disabled {
opacity: 0.45;
cursor: default;
}
.cw-tool-label {
display: none;
position: absolute;
top: calc(100% + 0.25rem);
right: 0;
z-index: 5;
padding: 0.25rem 0.6rem;
border-radius: var(--radius-control);
background: var(--color-surface-3);
border: 1px solid var(--color-border);
color: var(--color-text);
font-size: 0.9rem;
white-space: nowrap;
pointer-events: none;
}
.cw-tool:focus .cw-tool-label {
display: block;
}
.cw-files-screen .cw-status {
margin: 0 0 0.4rem;
}
.cw-files-screen .cw-file {
margin-bottom: 0.2rem;
}
.cw-files-screen .cw-file-main {
padding: 0.3rem 0.9rem;
}
.cw-files-screen .cw-file .cw-icon-button {
width: 2rem;
height: 2rem;
}
.cw-hint-tip {
margin-left: auto;
color: var(--color-text);
font-weight: 600;
}
+4 -1
View File
@@ -277,8 +277,11 @@ describe("Files listing controls", () => {
)}
</>,
);
expect(html).toContain('data-action="sortview"');
expect(html).toContain('data-action="select-all"');
expect(html).not.toContain('data-action="sortview"'); // the selection bar replaces the right side
const plain = renderToStaticMarkup(<>{FilesView(props({ onView: vi.fn(), onGoTo: vi.fn(), prefs: { favorites: [], recent: [], last: null }, onPin: vi.fn(), onUnpin: vi.fn() }))}</>);
for (const a of ["pin", "goto", "mkdir", "select", "sortview", "filter"]) expect(plain).toContain(`data-action="${a}"`);
expect(plain).toContain('data-tip="Sort and view"');
expect(html).toContain("700 items · 1 selected (10 B)");
expect(html).toContain('data-action="more-rows"');
expect(html.match(/data-entry="/g)).toHaveLength(300);
+129 -129
View File
@@ -2,6 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
ArrowDownUp,
CheckCheck,
ChevronsDown,
Ellipsis,
Filter,
FilterX,
FolderPlus,
@@ -9,8 +11,10 @@ import {
ListX,
RotateCw,
Square,
SquareCheck,
Star,
TextCursorInput,
type LucideIcon,
} from "lucide-react";
import { formatBytes } from "../../lib/format";
import { ApiError, call } from "../api";
@@ -21,10 +25,8 @@ import { useBackLevel } from "../backHistory";
import { onNavAction, topModal } from "../nav";
import type { SectionContext } from "../sections";
import { t } from "../strings";
import { ActionsButton } from "../ui/ActionsButton";
import { ConfirmDialog } from "../ui/ConfirmDialog";
import { anchorOf, focusLost, restoreFocus, type FocusAnchor } from "../ui/restoreFocus";
import { SelectToggle } from "../ui/SelectToggle";
import { ActionsSheet, type SheetAction } from "./ActionsSheet";
import { DirList, ROW_WINDOW, viewKind } from "./DirList";
import { canEject, DriveList, isImage, isSystemVolume, placeLabel } from "./DriveList";
@@ -455,28 +457,42 @@ function TasksLine({ tasks }: { tasks: Job[] }) {
);
}
/** ☆ / ★: pin the folder shown (it then shows under Pinned in the drive list), or unpin it. */
function PinButton({
path,
pinned,
onPin,
onUnpin,
/**
* One toolbar button: an icon, named by aria-label; its name shows next to it
* while it has focus (CSS) and in the hint bar (data-tip).
*/
function ToolButton({
icon: Icon,
label,
action,
onClick,
pressed,
disabled,
className,
}: {
path: string;
pinned: boolean;
onPin: (path: string) => void;
onUnpin: (path: string) => void;
icon: LucideIcon;
label: string;
action: string;
onClick: () => void;
pressed?: boolean;
disabled?: boolean;
className?: string;
}) {
return (
<button
type="button"
className={`cw-focusable cw-button cw-pin${pinned ? " is-pinned" : ""}`}
data-action="pin"
aria-pressed={pinned}
onClick={() => (pinned ? onUnpin(path) : onPin(path))}
className={`cw-focusable cw-tool${className ? ` ${className}` : ""}`}
data-action={action}
data-tip={label}
aria-label={label}
aria-pressed={pressed}
disabled={disabled}
onClick={onClick}
>
<Star aria-hidden />
<span>{t(pinned ? "cw_files_pinned_on" : "cw_files_pin")}</span>
<Icon aria-hidden />
<span className="cw-tool-label" aria-hidden>
{label}
</span>
</button>
);
}
@@ -498,6 +514,16 @@ export function FilesView(p: FilesViewProps) {
const view = p.view ?? DEFAULT_VIEW;
const capped = p.dir.more && p.dir.entries.length >= LOAD_ALL_CAP;
const st = p.status;
const pinned = p.path != null && p.prefs ? isPinned(p.prefs, p.path) : false;
const loadAllButton = (
<ToolButton
icon={ChevronsDown}
label={t("cw_files_load_all")}
action="load-all"
disabled={p.dir.loading || capped}
onClick={() => p.onLoadAll?.()}
/>
);
return (
<div className="cw-files-screen">
<h1 className="cw-h1">{t("cw_nav_files")}</h1>
@@ -539,123 +565,97 @@ export function FilesView(p: FilesViewProps) {
/>
) : (
<>
<PathBar key={p.path} path={p.path} volumes={p.volumes ?? []} onOpen={p.onOpen} onUp={p.onUp} />
<div className="cw-toolbar">
{p.prefs && p.onPin && p.onUnpin ? (
<PinButton path={p.path} pinned={isPinned(p.prefs, p.path)} onPin={p.onPin} onUnpin={p.onUnpin} />
) : null}
{p.onGoTo ? (
<button
type="button"
className="cw-focusable cw-button"
data-action="goto"
onClick={() => p.onAsk({ kind: "goto" })}
>
<TextCursorInput aria-hidden />
<span>{t("cw_files_goto")}</span>
</button>
) : null}
<button
type="button"
className="cw-focusable cw-button"
data-action="mkdir"
onClick={() => p.onAsk({ kind: "mkdir" })}
>
<FolderPlus aria-hidden />
<span>{t("cw_files_new_folder")}</span>
</button>
<SelectToggle on={p.selecting} onToggle={p.onToggleSelecting} />
{p.selecting && p.selected.length ? (
<>
<span className="cw-tag">{t("cw_files_selected", { n: p.selected.length })}</span>
<ActionsButton onPress={() => p.onAsk({ kind: "sheet", names: p.selected })} />
</>
) : null}
{vol ? (
<span className="cw-dim cw-toolbar-free">{t("cw_files_free", { free: formatBytes(vol.free_bytes) })}</span>
) : null}
</div>
{p.onView ? (
<div className="cw-toolbar cw-listbar">
<button
type="button"
className="cw-focusable cw-button"
data-action="sortview"
onClick={() => p.onAsk({ kind: "viewmenu" })}
>
<ArrowDownUp aria-hidden />
<span>{t("cw_files_sort_view")}</span>
</button>
<button
type="button"
className="cw-focusable cw-button"
data-action="filter"
aria-pressed={view.filter !== ""}
onClick={() => p.onAsk({ kind: "filter" })}
>
<Filter aria-hidden />
<span>{view.filter ? t("cw_files_filter_active", { q: view.filter }) : t("cw_files_filter")}</span>
</button>
{view.filter ? (
<button
type="button"
className="cw-focusable cw-icon-button"
data-action="filter-clear"
aria-label={t("cw_files_filter_clear")}
title={t("cw_files_filter_clear")}
onClick={() => p.onView?.({ filter: "" })}
>
<FilterX aria-hidden />
</button>
) : null}
{p.dir.more && p.onLoadAll ? (
<button
type="button"
className="cw-focusable cw-button"
data-action="load-all"
disabled={p.dir.loading || capped}
onClick={p.onLoadAll}
>
<span>{p.dir.loading ? t("cw_home_loading") : t("cw_files_load_all")}</span>
</button>
) : null}
<div className="cw-toolbar-one">
<PathBar key={p.path} path={p.path} volumes={p.volumes ?? []} onOpen={p.onOpen} onUp={p.onUp} />
<div className="cw-tools" role="toolbar" aria-label={t("cw_files_tools")}>
{p.selecting ? (
<>
<button
type="button"
className="cw-focusable cw-button"
data-action="select-all"
onClick={p.onSelectAll}
>
<CheckCheck aria-hidden />
<span>{t("cw_files_select_all")}</span>
</button>
<button
type="button"
className="cw-focusable cw-button"
data-action="select-none"
onClick={p.onSelectNone}
>
<ListX aria-hidden />
<span>{t("cw_files_select_none")}</span>
</button>
<button
type="button"
className="cw-focusable cw-button"
data-action="select-invert"
onClick={p.onSelectInvert}
>
<ListChecks aria-hidden />
<span>{t("cw_files_select_invert")}</span>
</button>
<ToolButton icon={CheckCheck} label={t("cw_files_select_all")} action="select-all" onClick={() => p.onSelectAll?.()} />
<ToolButton icon={ListX} label={t("cw_files_select_none")} action="select-none" onClick={() => p.onSelectNone?.()} />
<ToolButton icon={ListChecks} label={t("cw_files_select_invert")} action="select-invert" onClick={() => p.onSelectInvert?.()} />
{p.selected.length ? (
<ToolButton
icon={Ellipsis}
label={t("cw_actions")}
action="selection-actions"
onClick={() => p.onAsk({ kind: "sheet", names: p.selected })}
/>
) : null}
{p.dir.more && p.onLoadAll ? loadAllButton : null}
<ToolButton
icon={SquareCheck}
label={t("cw_select")}
action="select"
pressed
onClick={p.onToggleSelecting}
/>
</>
) : null}
) : (
<>
{p.prefs && p.onPin && p.onUnpin ? (
<ToolButton
icon={Star}
label={t(pinned ? "cw_files_pinned_on" : "cw_files_pin")}
action="pin"
pressed={pinned}
className={pinned ? "is-pinned" : undefined}
onClick={() => (pinned ? p.onUnpin?.(p.path as string) : p.onPin?.(p.path as string))}
/>
) : null}
{p.onGoTo ? (
<ToolButton
icon={TextCursorInput}
label={t("cw_files_goto")}
action="goto"
onClick={() => p.onAsk({ kind: "goto" })}
/>
) : null}
<ToolButton
icon={FolderPlus}
label={t("cw_files_new_folder")}
action="mkdir"
onClick={() => p.onAsk({ kind: "mkdir" })}
/>
<ToolButton icon={SquareCheck} label={t("cw_select")} action="select" onClick={p.onToggleSelecting} />
{p.onView ? (
<>
<ToolButton
icon={ArrowDownUp}
label={t("cw_files_sort_view")}
action="sortview"
onClick={() => p.onAsk({ kind: "viewmenu" })}
/>
<ToolButton
icon={Filter}
label={view.filter ? t("cw_files_filter_active", { q: view.filter }) : t("cw_files_filter")}
action="filter"
pressed={view.filter !== ""}
onClick={() => p.onAsk({ kind: "filter" })}
/>
{view.filter ? (
<ToolButton
icon={FilterX}
label={t("cw_files_filter_clear")}
action="filter-clear"
onClick={() => p.onView?.({ filter: "" })}
/>
) : null}
{p.dir.more && p.onLoadAll ? loadAllButton : null}
</>
) : null}
</>
)}
</div>
) : null}
</div>
{st && p.dir.loaded ? (
<p className="cw-dim cw-status" role="status" data-status="">
{statusText(st)}
{capped ? ` · ${t("cw_files_capped", { n: p.dir.entries.length })}` : ""}
{[
statusText(st),
capped ? t("cw_files_capped", { n: p.dir.entries.length }) : null,
vol ? t("cw_files_free", { free: formatBytes(vol.free_bytes) }) : null,
]
.filter(Boolean)
.join(" · ")}
</p>
) : null}
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}، بدون احتساب المجلدات",
"cw_files_tasks": "المهام الجارية: {n}",
"cw_files_too_large": "كبير جدًا للمعاينة.",
"cw_files_tools": "أدوات المجلد",
"cw_files_truncated": "يُعرض أول 1 MiB فقط.",
"cw_files_unmount": "إلغاء التركيب",
"cw_files_unmount_body": "ستتوقف اللعبة التي تعمل من هذه الصورة، وستغيب ملفاتها حتى يُعاد تركيب الصورة.",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}, ফোল্ডার গণনা হয়নি",
"cw_files_tasks": "চলমান কাজ: {n}",
"cw_files_too_large": "প্রিভিউ করার জন্য খুব বড়।",
"cw_files_tools": "ফোল্ডারের টুল",
"cw_files_truncated": "প্রথম 1 MiB দেখানো হচ্ছে।",
"cw_files_unmount": "আনমাউন্ট করুন",
"cw_files_unmount_body": "এই ইমেজ থেকে চলা গেম বন্ধ হয়ে যাবে, এবং ইমেজটি আবার মাউন্ট না হওয়া পর্যন্ত এর ফাইলগুলো থাকবে না।",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}, Ordner nicht mitgezählt",
"cw_files_tasks": "Laufende Aufgaben: {n}",
"cw_files_too_large": "Zu groß für die Vorschau.",
"cw_files_tools": "Ordnerwerkzeuge",
"cw_files_truncated": "Die ersten 1 MiB werden angezeigt.",
"cw_files_unmount": "Aushängen",
"cw_files_unmount_body": "Ein Spiel, das von diesem Image läuft, wird beendet, und seine Dateien fehlen, bis das Image wieder eingebunden ist.",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}, folders not counted",
"cw_files_tasks": "Tasks running: {n}",
"cw_files_too_large": "Too large to preview.",
"cw_files_tools": "Folder tools",
"cw_files_truncated": "Showing the first 1 MiB.",
"cw_files_unmount": "Unmount",
"cw_files_unmount_body": "A game running from this image will stop, and its files are gone until the image is mounted again.",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}, sin contar carpetas",
"cw_files_tasks": "Tareas en curso: {n}",
"cw_files_too_large": "Demasiado grande para la vista previa.",
"cw_files_tools": "Herramientas de la carpeta",
"cw_files_truncated": "Se muestra el primer 1 MiB.",
"cw_files_unmount": "Desmontar",
"cw_files_unmount_body": "Un juego que se ejecute desde esta imagen se detendrá, y sus archivos no estarán disponibles hasta que se vuelva a montar la imagen.",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}، پوشه‌ها محاسبه نشده‌اند",
"cw_files_tasks": "کارهای در حال اجرا: {n}",
"cw_files_too_large": "برای پیش‌نمایش بیش از حد بزرگ است.",
"cw_files_tools": "ابزارهای پوشه",
"cw_files_truncated": "فقط ۱ مبی‌بایت اول نمایش داده می‌شود.",
"cw_files_unmount": "پیاده کردن",
"cw_files_unmount_body": "بازی‌ای که از این ایمیج اجرا می‌شود متوقف می‌شود و فایل‌هایش تا سوار شدن دوبارهٔ ایمیج در دسترس نیستند.",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}, dossiers non comptés",
"cw_files_tasks": "Tâches en cours : {n}",
"cw_files_too_large": "Trop volumineux pour l'aperçu.",
"cw_files_tools": "Outils du dossier",
"cw_files_truncated": "Affichage du premier Mio.",
"cw_files_unmount": "Démonter",
"cw_files_unmount_body": "Un jeu lancé depuis cette image s'arrêtera, et ses fichiers seront absents jusqu'à ce que l'image soit de nouveau montée.",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}, फ़ोल्डर गिने नहीं गए",
"cw_files_tasks": "चल रहे कार्य: {n}",
"cw_files_too_large": "पूर्वावलोकन के लिए बहुत बड़ा है।",
"cw_files_tools": "फ़ोल्डर के टूल",
"cw_files_truncated": "पहला 1 MiB दिखाया जा रहा है।",
"cw_files_unmount": "अनमाउंट करें",
"cw_files_unmount_body": "इस इमेज से चल रहा गेम रुक जाएगा, और इमेज दोबारा माउंट होने तक उसकी फ़ाइलें उपलब्ध नहीं रहेंगी।",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}, a mappák nincsenek beleszámítva",
"cw_files_tasks": "Futó feladatok: {n}",
"cw_files_too_large": "Túl nagy az előnézethez.",
"cw_files_tools": "Mappaeszközök",
"cw_files_truncated": "Az első 1 MiB látható.",
"cw_files_unmount": "Leválasztás",
"cw_files_unmount_body": "Az erről a képfájlról futó játék leáll, és a fájljai addig nem érhetők el, amíg a képfájlt újra nem csatolják.",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}, folder tidak dihitung",
"cw_files_tasks": "Tugas berjalan: {n}",
"cw_files_too_large": "Terlalu besar untuk pratinjau.",
"cw_files_tools": "Alat folder",
"cw_files_truncated": "Menampilkan 1 MiB pertama.",
"cw_files_unmount": "Lepas",
"cw_files_unmount_body": "Game yang berjalan dari image ini akan berhenti, dan filenya hilang sampai image dipasang lagi.",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}, cartelle escluse",
"cw_files_tasks": "Attività in corso: {n}",
"cw_files_too_large": "Troppo grande per l'anteprima.",
"cw_files_tools": "Strumenti della cartella",
"cw_files_truncated": "Viene mostrato il primo MiB.",
"cw_files_unmount": "Smonta",
"cw_files_unmount_body": "Un gioco avviato da questa immagine si fermerà, e i suoi file non ci saranno finché l'immagine non verrà montata di nuovo.",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}、フォルダは含まず",
"cw_files_tasks": "実行中のタスク: {n}",
"cw_files_too_large": "大きすぎてプレビューできません。",
"cw_files_tools": "フォルダのツール",
"cw_files_truncated": "先頭 1 MiB を表示しています。",
"cw_files_unmount": "マウント解除",
"cw_files_unmount_body": "このイメージから起動しているゲームは停止し、イメージを再びマウントするまでそのファイルは使えなくなります。",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}, 폴더는 제외",
"cw_files_tasks": "실행 중인 작업: {n}",
"cw_files_too_large": "너무 커서 미리 볼 수 없습니다.",
"cw_files_tools": "폴더 도구",
"cw_files_truncated": "처음 1 MiB를 표시합니다.",
"cw_files_unmount": "마운트 해제",
"cw_files_unmount_body": "이 이미지에서 실행 중인 게임은 멈추고, 이미지를 다시 마운트할 때까지 그 파일을 쓸 수 없습니다.",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}, foldery nie wliczone",
"cw_files_tasks": "Trwające zadania: {n}",
"cw_files_too_large": "Za duży, by pokazać podgląd.",
"cw_files_tools": "Narzędzia folderu",
"cw_files_truncated": "Pokazano pierwszy 1 MiB.",
"cw_files_unmount": "Odmontuj",
"cw_files_unmount_body": "Gra uruchomiona z tego obrazu zatrzyma się, a jej pliki znikną, dopóki obraz nie zostanie ponownie zamontowany.",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}, pastas não contadas",
"cw_files_tasks": "Tarefas em andamento: {n}",
"cw_files_too_large": "Grande demais para visualizar.",
"cw_files_tools": "Ferramentas da pasta",
"cw_files_truncated": "Mostrando o primeiro 1 MiB.",
"cw_files_unmount": "Desmontar",
"cw_files_unmount_body": "Um jogo rodando a partir desta imagem vai parar, e os arquivos dele somem até a imagem ser montada de novo.",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}, папки не учтены",
"cw_files_tasks": "Выполняется задач: {n}",
"cw_files_too_large": "Слишком большой для просмотра.",
"cw_files_tools": "Инструменты папки",
"cw_files_truncated": "Показан первый 1 МиБ.",
"cw_files_unmount": "Отключить",
"cw_files_unmount_body": "Игра, запущенная из этого образа, остановится, а её файлы будут недоступны, пока образ не подключат снова.",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size} ไม่นับโฟลเดอร์",
"cw_files_tasks": "งานที่กำลังทำ: {n}",
"cw_files_too_large": "ใหญ่เกินกว่าจะแสดงตัวอย่าง",
"cw_files_tools": "เครื่องมือโฟลเดอร์",
"cw_files_truncated": "แสดง 1 MiB แรก",
"cw_files_unmount": "ยกเลิกการเมานต์",
"cw_files_unmount_body": "เกมที่รันจากอิมเมจนี้จะหยุดทำงาน และไฟล์ของเกมจะหายไปจนกว่าจะเมานต์อิมเมจอีกครั้ง",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}, klasörler sayılmadı",
"cw_files_tasks": "Çalışan görevler: {n}",
"cw_files_too_large": "Önizleme için çok büyük.",
"cw_files_tools": "Klasör araçları",
"cw_files_truncated": "İlk 1 MiB gösteriliyor.",
"cw_files_unmount": "Bağlantıyı kes",
"cw_files_unmount_body": "Bu imajdan çalışan bir oyun durur ve imaj yeniden bağlanana kadar dosyaları erişilemez olur.",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size}, chưa tính thư mục",
"cw_files_tasks": "Tác vụ đang chạy: {n}",
"cw_files_too_large": "Quá lớn để xem trước.",
"cw_files_tools": "Công cụ thư mục",
"cw_files_truncated": "Đang hiện 1 MiB đầu tiên.",
"cw_files_unmount": "Tháo gắn",
"cw_files_unmount_body": "Trò chơi đang chạy từ ảnh đĩa này sẽ dừng, và các tệp của nó sẽ không còn cho đến khi ảnh đĩa được gắn lại.",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size},不含文件夹",
"cw_files_tasks": "运行中的任务:{n}",
"cw_files_too_large": "文件太大,无法预览。",
"cw_files_tools": "文件夹工具",
"cw_files_truncated": "仅显示前 1 MiB。",
"cw_files_unmount": "卸载",
"cw_files_unmount_body": "从此镜像运行的游戏会停止,在镜像重新挂载之前其文件将不可用。",
+1
View File
@@ -143,6 +143,7 @@
"cw_files_status_size_dirs": "{size},不含資料夾",
"cw_files_tasks": "執行中的工作:{n}",
"cw_files_too_large": "檔案太大,無法預覽。",
"cw_files_tools": "資料夾工具",
"cw_files_truncated": "只顯示前 1 MiB。",
"cw_files_unmount": "卸載",
"cw_files_unmount_body": "從此映像檔執行的遊戲會停止,在映像檔重新掛載之前其檔案將無法使用。",
+13
View File
@@ -1,3 +1,4 @@
import { useEffect, useState } from "react";
import type { InputMap } from "../nav";
import { barHints, type ScreenId } from "../help/hints";
import { t } from "../strings";
@@ -23,6 +24,13 @@ export function Glyphs({ glyphs }: { glyphs: string[] }) {
*/
export function HintBar({ map, screen }: { map: InputMap; screen: ScreenId }) {
const hints = barHints(map, screen);
// The focused toolbar button's name (icon-only buttons carry it in data-tip).
const [tip, setTip] = useState("");
useEffect(() => {
const onIn = (e: Event) => setTip((e.target as Element | null)?.closest?.("[data-tip]")?.getAttribute("data-tip") ?? "");
document.addEventListener("focusin", onIn);
return () => document.removeEventListener("focusin", onIn);
}, []);
if (!hints.length) return null;
return (
<footer className="cw-hints" aria-label={t("cw_hints_label")} data-screen={screen}>
@@ -32,6 +40,11 @@ export function HintBar({ map, screen }: { map: InputMap; screen: ScreenId }) {
{t(h.labelKey)}
</span>
))}
{tip ? (
<span className="cw-hint-tip" aria-hidden>
{tip}
</span>
) : null}
</footer>
);
}
+1
View File
@@ -4128,6 +4128,7 @@ c2c_test_deep_bad: "لم يصل الإرسال التجريبي بحجم 4 KiB:
"cw_files_status_selected": "تم تحديد {n} ({size})",
"cw_files_status_size_dirs": "{size}، بدون احتساب المجلدات",
"cw_files_none_match": "لا شيء يطابق التصفية.",
"cw_files_tools": "أدوات المجلد",
"cw_files_selected": "تم تحديد {n}",
"cw_files_items": "{n} عناصر",
"cw_files_free": "{free} متاحة",
+1
View File
@@ -4128,6 +4128,7 @@ c2c_test_deep_bad: "4 KiB-এর পরীক্ষামূলক পাঠা
"cw_files_status_selected": "{n}টি বাছাই করা ({size})",
"cw_files_status_size_dirs": "{size}, ফোল্ডার গণনা হয়নি",
"cw_files_none_match": "ফিল্টারের সাথে কিছু মেলেনি।",
"cw_files_tools": "ফোল্ডারের টুল",
"cw_files_selected": "{n}টি নির্বাচিত",
"cw_files_items": "{n}টি আইটেম",
"cw_files_free": "{free} খালি",
+1
View File
@@ -4136,6 +4136,7 @@ c2c_test_deep_bad: "Die 4-KiB-Testsendung ist nicht angekommen: {detail}",
"cw_files_status_selected": "{n} ausgewählt ({size})",
"cw_files_status_size_dirs": "{size}, Ordner nicht mitgezählt",
"cw_files_none_match": "Nichts passt zum Filter.",
"cw_files_tools": "Ordnerwerkzeuge",
"cw_files_selected": "{n} ausgewählt",
"cw_files_items": "{n} Elemente",
"cw_files_free": "{free} frei",
+1
View File
@@ -4451,6 +4451,7 @@ c2c_test_deep_bad: "The 4 KiB test send did not arrive: {detail}",
"cw_files_status_selected": "{n} selected ({size})",
"cw_files_status_size_dirs": "{size}, folders not counted",
"cw_files_none_match": "Nothing matches the filter.",
"cw_files_tools": "Folder tools",
"cw_files_selected": "{n} selected",
"cw_files_items": "{n} items",
"cw_files_free": "{free} free",
+1
View File
@@ -4078,6 +4078,7 @@ c2c_test_deep_bad: "El envío de prueba de 4 KiB no llegó: {detail}",
"cw_files_status_selected": "{n} seleccionados ({size})",
"cw_files_status_size_dirs": "{size}, sin contar carpetas",
"cw_files_none_match": "Nada coincide con el filtro.",
"cw_files_tools": "Herramientas de la carpeta",
"cw_files_selected": "{n} seleccionados",
"cw_files_items": "{n} elementos",
"cw_files_free": "{free} libres",
+1
View File
@@ -4429,6 +4429,7 @@ c2c_test_deep_bad: "ارسال آزمایشی ۴ KiB نرسید: {detail}",
"cw_files_status_selected": "{n} انتخاب شده ({size})",
"cw_files_status_size_dirs": "{size}، پوشه‌ها محاسبه نشده‌اند",
"cw_files_none_match": "هیچ موردی با فیلتر مطابقت ندارد.",
"cw_files_tools": "ابزارهای پوشه",
"cw_files_selected": "{n} انتخاب شده",
"cw_files_items": "{n} مورد",
"cw_files_free": "{free} آزاد",
+1
View File
@@ -4146,6 +4146,7 @@ c2c_test_deep_bad: "L’envoi test de 4 Kio n’est pas arrivé : {detail}",
"cw_files_status_selected": "{n} sélectionnés ({size})",
"cw_files_status_size_dirs": "{size}, dossiers non comptés",
"cw_files_none_match": "Aucun résultat pour ce filtre.",
"cw_files_tools": "Outils du dossier",
"cw_files_selected": "{n} sélectionné(s)",
"cw_files_items": "{n} éléments",
"cw_files_free": "{free} libres",
+1
View File
@@ -4144,6 +4144,7 @@ c2c_test_deep_bad: "4 KiB का परीक्षण नहीं पहुँ
"cw_files_status_selected": "{n} चुने गए ({size})",
"cw_files_status_size_dirs": "{size}, फ़ोल्डर गिने नहीं गए",
"cw_files_none_match": "फ़िल्टर से कुछ मेल नहीं खाता।",
"cw_files_tools": "फ़ोल्डर के टूल",
"cw_files_selected": "{n} चुने गए",
"cw_files_items": "{n} आइटम",
"cw_files_free": "{free} खाली",
+1
View File
@@ -4443,6 +4443,7 @@ c2c_test_deep_bad: "A 4 KiB-os tesztküldés nem érkezett meg: {detail}",
"cw_files_status_selected": "{n} kijelölve ({size})",
"cw_files_status_size_dirs": "{size}, a mappák nincsenek beleszámítva",
"cw_files_none_match": "Nincs a szűrőnek megfelelő elem.",
"cw_files_tools": "Mappaeszközök",
"cw_files_selected": "{n} kijelölve",
"cw_files_items": "{n} elem",
"cw_files_free": "{free} szabad",
+1
View File
@@ -4137,6 +4137,7 @@ c2c_test_deep_bad: "Kiriman uji 4 KiB tidak tiba: {detail}",
"cw_files_status_selected": "{n} dipilih ({size})",
"cw_files_status_size_dirs": "{size}, folder tidak dihitung",
"cw_files_none_match": "Tidak ada yang cocok dengan filter.",
"cw_files_tools": "Alat folder",
"cw_files_selected": "{n} dipilih",
"cw_files_items": "{n} item",
"cw_files_free": "{free} kosong",
+1
View File
@@ -4078,6 +4078,7 @@ c2c_test_deep_bad: "L’invio di prova di 4 KiB non è arrivato: {detail}",
"cw_files_status_selected": "{n} selezionati ({size})",
"cw_files_status_size_dirs": "{size}, cartelle escluse",
"cw_files_none_match": "Nessun risultato per il filtro.",
"cw_files_tools": "Strumenti della cartella",
"cw_files_selected": "{n} selezionati",
"cw_files_items": "{n} elementi",
"cw_files_free": "{free} liberi",
+1
View File
@@ -4149,6 +4149,7 @@ c2c_test_deep_bad: "4 KiB のテスト送信が届きませんでした: {detail
"cw_files_status_selected": "{n} 件選択 ({size})",
"cw_files_status_size_dirs": "{size}、フォルダは含まず",
"cw_files_none_match": "絞り込みに一致するものがありません。",
"cw_files_tools": "フォルダのツール",
"cw_files_selected": "{n} 件選択中",
"cw_files_items": "{n} 件",
"cw_files_free": "空き {free}",
+1
View File
@@ -4147,6 +4147,7 @@ c2c_test_deep_bad: "4 KiB 테스트 전송이 도착하지 않았습니다: {det
"cw_files_status_selected": "{n}개 선택됨 ({size})",
"cw_files_status_size_dirs": "{size}, 폴더는 제외",
"cw_files_none_match": "필터와 일치하는 항목이 없습니다.",
"cw_files_tools": "폴더 도구",
"cw_files_selected": "{n}개 선택됨",
"cw_files_items": "{n}개 항목",
"cw_files_free": "{free} 남음",
+1
View File
@@ -4383,6 +4383,7 @@ c2c_test_deep_bad: "Testowa wysyłka 4 KiB nie dotarła: {detail}",
"cw_files_status_selected": "Zaznaczono: {n} ({size})",
"cw_files_status_size_dirs": "{size}, foldery nie wliczone",
"cw_files_none_match": "Nic nie pasuje do filtra.",
"cw_files_tools": "Narzędzia folderu",
"cw_files_selected": "Zaznaczono: {n}",
"cw_files_items": "Elementy: {n}",
"cw_files_free": "Wolne: {free}",
+1
View File
@@ -4145,6 +4145,7 @@ c2c_test_deep_bad: "O envio de teste de 4 KiB não chegou: {detail}",
"cw_files_status_selected": "{n} selecionados ({size})",
"cw_files_status_size_dirs": "{size}, pastas não contadas",
"cw_files_none_match": "Nada corresponde ao filtro.",
"cw_files_tools": "Ferramentas da pasta",
"cw_files_selected": "{n} selecionados",
"cw_files_items": "{n} itens",
"cw_files_free": "{free} livres",
+1
View File
@@ -4078,6 +4078,7 @@ c2c_test_deep_bad: "Тестовая отправка 4 КиБ не дошла:
"cw_files_status_selected": "Выбрано: {n} ({size})",
"cw_files_status_size_dirs": "{size}, папки не учтены",
"cw_files_none_match": "Ничего не подходит под фильтр.",
"cw_files_tools": "Инструменты папки",
"cw_files_selected": "Выбрано: {n}",
"cw_files_items": "Элементов: {n}",
"cw_files_free": "Свободно: {free}",
+1
View File
@@ -4137,6 +4137,7 @@ c2c_test_deep_bad: "การส่งทดสอบ 4 KiB ไปไม่ถ
"cw_files_status_selected": "เลือกแล้ว {n} ({size})",
"cw_files_status_size_dirs": "{size} ไม่นับโฟลเดอร์",
"cw_files_none_match": "ไม่มีรายการที่ตรงกับตัวกรอง",
"cw_files_tools": "เครื่องมือโฟลเดอร์",
"cw_files_selected": "เลือกแล้ว {n}",
"cw_files_items": "{n} รายการ",
"cw_files_free": "ว่าง {free}",
+1
View File
@@ -4135,6 +4135,7 @@ c2c_test_deep_bad: "4 KiB'lik test gönderimi ulaşmadı: {detail}",
"cw_files_status_selected": "{n} seçili ({size})",
"cw_files_status_size_dirs": "{size}, klasörler sayılmadı",
"cw_files_none_match": "Filtreyle eşleşen yok.",
"cw_files_tools": "Klasör araçları",
"cw_files_selected": "{n} seçili",
"cw_files_items": "{n} öğe",
"cw_files_free": "{free} boş",
+1
View File
@@ -4130,6 +4130,7 @@ c2c_test_deep_bad: "Lượt gửi thử 4 KiB không tới: {detail}",
"cw_files_status_selected": "Đã chọn {n} ({size})",
"cw_files_status_size_dirs": "{size}, chưa tính thư mục",
"cw_files_none_match": "Không có mục nào khớp bộ lọc.",
"cw_files_tools": "Công cụ thư mục",
"cw_files_selected": "Đã chọn {n}",
"cw_files_items": "{n} mục",
"cw_files_free": "Còn trống {free}",
+1
View File
@@ -4084,6 +4084,7 @@ c2c_test_deep_bad: "4 KiB 测试发送未到达:{detail}",
"cw_files_status_selected": "已选 {n} 项({size})",
"cw_files_status_size_dirs": "{size},不含文件夹",
"cw_files_none_match": "没有符合筛选的项目。",
"cw_files_tools": "文件夹工具",
"cw_files_selected": "已选 {n} 项",
"cw_files_items": "{n} 项",
"cw_files_free": "可用 {free}",
+1
View File
@@ -4078,6 +4078,7 @@ c2c_test_deep_bad: "4 KiB 測試傳送未抵達:{detail}",
"cw_files_status_selected": "已選 {n} 項({size})",
"cw_files_status_size_dirs": "{size},不含資料夾",
"cw_files_none_match": "沒有符合篩選的項目。",
"cw_files_tools": "資料夾工具",
"cw_files_selected": "已選取 {n} 項",
"cw_files_items": "{n} 項",
"cw_files_free": "可用 {free}",