mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix/Bug#772-DesignFilesSortButton-DesignFilesTableNowSortableColumns (#804)
* Disclaimer: Changes made using OpenCode with Big Pickle, an AI code assistant.
1. Removed the ↑ button from DesignFilesPanel.tsx (was a "back" button that
closed the preview pane — confusingly placed in the header as if it were for
sorting).
2. Converted the file list to a single <table> with sortable columns:
- Replaced the section-based grouping (Pages, Sketches, Scripts, Images,
Other) with a flat, sortable table
- Added column headers: Name, Kind, Modified — all clickable to toggle
sort direction
- Default sort is by modified time descending (same as previous behavior)
- Sort indicator arrows (↑/↓) show the active sort column and direction
- Live artifacts remain as a separate section above the table
3. Added i18n keys designFiles.colName, designFiles.colKind, designFiles.
colModified to all 17 locale files and the type definitions.
4. Updated CSS with table layout styles (.df-table, .df-file-row, column
width classes, sortable header styles).
Files modified:
- apps/web/src/components/DesignFilesPanel.tsx
- apps/web/src/index.css
- apps/web/src/i18n/types.ts
- apps/web/src/i18n/locales/en.ts
- apps/web/src/i18n/locales/*.ts (all 16 other locale files)
* Updated to preserve keyboard access to sorting
* Fixed keyboard to focus/activate/launch file from Design Files list. Single space bar will show preview, double spare bar will open the file as a tab
* Top pagination bar (above the table):
- "Show" dropdown with options 15, 30 (default), 45, 60, All
- Page range indicator (1–20 of 45)
- Previous / Next buttons
Bottom pagination bar (below the table):
- Previous / Next buttons
- "Go to page" dropdown listing all page numbers
- Same page range indicator
Implementation details:
- All controls use native <select> and <button> elements — fully keyboard
accessible (Tab, arrow keys, Enter/Space)
- Page resets to 0 when page size changes
- safePage clamps to valid bounds when file count changes (e.g. after delete)
- "All" sets page size to total file count (effectively one page)
- Prev/Next buttons show disabled state at boundaries with reduced opacity
* All 46 test files, all 385 tests pass. Here's what the regression test covers:
┌────────────────────┬──────────────────────────────────────────────────────┐
│Test │What it verifies │
├────────────────────┼──────────────────────────────────────────────────────┤
│default page size │500 files → only 30 .df-file-row elements in DOM │
├────────────────────┼──────────────────────────────────────────────────────┤
│page size All │changing per-page to "All" shows all 500 rows │
├────────────────────┼──────────────────────────────────────────────────────┤
│page size 60 │changing to 60 shows 60 rows │
├────────────────────┼──────────────────────────────────────────────────────┤
│Next navigation │clicking Next advances page and shows file-31 (sorted │
│ │by mtime desc) │
├────────────────────┼──────────────────────────────────────────────────────┤
│Prev/Next disabled │Prev disabled on page 0, Next disabled on last page │
│states │ │
├────────────────────┼──────────────────────────────────────────────────────┤
│jump to page │bottom dropdown jumps to page 3 (shows file-91) │
├────────────────────┼──────────────────────────────────────────────────────┤
│page info text │1–30 of 500 → after Next → 31–60 of 500 │
├────────────────────┼──────────────────────────────────────────────────────┤
│render time │renders 500 files in under 2s │
└────────────────────┴──────────────────────────────────────────────────────┘
* Fixed i18n for DesignFiles, and Fixed DesignFilesPanel Test
* Fixed - P3 — .df-thead rule defined but never applied
* Fixed keyboard use for file navigation, focus and button usage
* Fix i18n for x of y in design files pagination
* Fixed SafePage clamping
* Fixed dupe file total count
* Fixed x of y i18n
* Fixed DeleteSelected i18n and missing from Test
* fix effective pagesize issue, and change duplicate file kind to a filesize
* Readded page/everything selection and i18n
* Fixed i18n issues
* Resolved indonesian i18n issue with cloudflare keys
* Fixed unrelated cloudflare i18n issues as requested in Pull Request by reviewer
* Fix e2e test: click filename button instead of row for preview
The DesignFilesPanel was refactored from <button> rows to a <tr> with
a nested <button> for the filename. The e2e test was still clicking
the <tr> which has no onClick handler, so the preview never appeared.
* Remove duplicate formatSize helper, reuse humanBytes instead
This commit is contained in:
parent
644a7daf2d
commit
0b039777b9
22 changed files with 929 additions and 295 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useMemo, useRef, useState, useTransition } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useT } from '../i18n';
|
import { useT } from '../i18n';
|
||||||
import type { Dict } from '../i18n/types';
|
import type { Dict } from '../i18n/types';
|
||||||
import { projectFileUrl } from '../providers/registry';
|
import { projectFileUrl } from '../providers/registry';
|
||||||
|
|
@ -23,26 +23,9 @@ interface Props {
|
||||||
onNewSketch: () => void;
|
onNewSketch: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Section = 'pages' | 'scripts' | 'images' | 'sketches' | 'other';
|
type SortKey = 'name' | 'kind' | 'mtime';
|
||||||
|
type SortDir = 'asc' | 'desc';
|
||||||
|
|
||||||
const SECTION_LABEL_KEY: Record<Section, keyof Dict> = {
|
|
||||||
pages: 'designFiles.sectionPages',
|
|
||||||
scripts: 'designFiles.sectionScripts',
|
|
||||||
images: 'designFiles.sectionImages',
|
|
||||||
sketches: 'designFiles.sectionSketches',
|
|
||||||
other: 'designFiles.sectionOther',
|
|
||||||
};
|
|
||||||
|
|
||||||
const SECTION_ORDER: Section[] = ['pages', 'sketches', 'scripts', 'images', 'other'];
|
|
||||||
const INITIAL_SECTION_FILE_LIMIT = 30;
|
|
||||||
const SECTION_FILE_LIMIT_INCREMENT = 200;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Full-panel browser for a project's `.od/projects/<id>/` folder. Mirrors
|
|
||||||
* Claude Design's "Design Files" surface: grouped sections, hover-revealed
|
|
||||||
* row menu, drop-files footer, and (when a row is selected) a right-side
|
|
||||||
* preview pane. Triggered as a sticky first tab in FileWorkspace.
|
|
||||||
*/
|
|
||||||
export function DesignFilesPanel({
|
export function DesignFilesPanel({
|
||||||
projectId,
|
projectId,
|
||||||
files,
|
files,
|
||||||
|
|
@ -66,30 +49,42 @@ export function DesignFilesPanel({
|
||||||
const MENU_ESTIMATED_HEIGHT = 115;
|
const MENU_ESTIMATED_HEIGHT = 115;
|
||||||
const MENU_SAFE_PADDING = 8;
|
const MENU_SAFE_PADDING = 8;
|
||||||
const [preview, setPreview] = useState<string | null>(null);
|
const [preview, setPreview] = useState<string | null>(null);
|
||||||
const [sectionLimits, setSectionLimits] = useState<Partial<Record<Section, number>>>({});
|
|
||||||
const [isSectionExpansionPending, startSectionExpansion] = useTransition();
|
|
||||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||||
|
const [sortKey, setSortKey] = useState<SortKey>('mtime');
|
||||||
|
const [sortDir, setSortDir] = useState<SortDir>('desc');
|
||||||
|
const lastKeyPress = useRef<Map<string, number>>(new Map());
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
const grouped = useMemo(() => {
|
const sortedFiles = useMemo(() => {
|
||||||
const groups: Record<Section, ProjectFile[]> = {
|
return [...files].sort((a, b) => {
|
||||||
pages: [],
|
let cmp: number;
|
||||||
sketches: [],
|
if (sortKey === 'name') cmp = a.name.localeCompare(b.name);
|
||||||
scripts: [],
|
else if (sortKey === 'kind') cmp = kindSortPriority(a.kind) - kindSortPriority(b.kind);
|
||||||
images: [],
|
else cmp = a.mtime - b.mtime;
|
||||||
other: [],
|
return sortDir === 'asc' ? cmp : -cmp;
|
||||||
};
|
});
|
||||||
const sorted = [...files].sort((a, b) => b.mtime - a.mtime);
|
}, [files, sortKey, sortDir]);
|
||||||
for (const f of sorted) {
|
|
||||||
groups[sectionFor(f)].push(f);
|
const [page, setPage] = useState(0);
|
||||||
}
|
const [pageSize, setPageSize] = useState<number | 'all'>(30);
|
||||||
return groups;
|
|
||||||
}, [files]);
|
const effectivePageSize = pageSize === 'all' ? Math.max(1, sortedFiles.length) : pageSize;
|
||||||
|
const totalPages = Math.max(1, Math.ceil(sortedFiles.length / effectivePageSize));
|
||||||
|
const safePage = Math.min(page, totalPages - 1);
|
||||||
|
const pageFiles = sortedFiles.slice(safePage * effectivePageSize, (safePage + 1) * effectivePageSize);
|
||||||
|
const rangeStart = safePage * effectivePageSize + 1;
|
||||||
|
const rangeEnd = Math.min((safePage + 1) * effectivePageSize, sortedFiles.length);
|
||||||
|
const allPageSelected = pageFiles.every((f) => selected.has(f.name));
|
||||||
|
const somePageSelected = !allPageSelected && pageFiles.some((f) => selected.has(f.name));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(0);
|
||||||
|
}, [pageSize]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Number.isFinite(totalPages)) setPage((p) => Math.min(p, totalPages - 1));
|
||||||
|
}, [totalPages]);
|
||||||
|
|
||||||
// Prune selections that no longer exist in the current file list
|
|
||||||
// (e.g. after a refresh or delete within the same project).
|
|
||||||
// Cross-project leaks are handled by the parent remounting this
|
|
||||||
// component via key={projectId}.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelected((prev) => {
|
setSelected((prev) => {
|
||||||
if (prev.size === 0) return prev;
|
if (prev.size === 0) return prev;
|
||||||
|
|
@ -111,7 +106,6 @@ export function DesignFilesPanel({
|
||||||
[preview, files],
|
[preview, files],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Close the row menu on outside click / escape.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!menuPos) return;
|
if (!menuPos) return;
|
||||||
const close = () => setMenuPos(null);
|
const close = () => setMenuPos(null);
|
||||||
|
|
@ -135,6 +129,17 @@ export function DesignFilesPanel({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSort(key: SortKey) {
|
||||||
|
return () => {
|
||||||
|
if (sortKey === key) {
|
||||||
|
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
|
||||||
|
} else {
|
||||||
|
setSortKey(key);
|
||||||
|
setSortDir('asc');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSelect(name: string) {
|
function toggleSelect(name: string) {
|
||||||
setSelected((prev) => {
|
setSelected((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
|
|
@ -147,20 +152,49 @@ export function DesignFilesPanel({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectAllInSection(sectionFiles: ProjectFile[]) {
|
function toggleSelectPage() {
|
||||||
setSelected((prev) => {
|
setSelected((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
for (const f of sectionFiles) next.add(f.name);
|
if (allPageSelected) {
|
||||||
|
for (const f of pageFiles) next.delete(f.name);
|
||||||
|
} else {
|
||||||
|
for (const f of pageFiles) next.add(f.name);
|
||||||
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSection(sectionFiles: ProjectFile[]) {
|
function selectAllFiles() {
|
||||||
setSelected((prev) => {
|
setSelected(new Set(sortedFiles.map((f) => f.name)));
|
||||||
const next = new Set(prev);
|
}
|
||||||
for (const f of sectionFiles) next.delete(f.name);
|
|
||||||
return next;
|
function clearSelection() {
|
||||||
});
|
setSelected(new Set());
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMenuFor(name: string, el: HTMLElement) {
|
||||||
|
const rect = el.closest('.df-row-menu')?.getBoundingClientRect();
|
||||||
|
if (!rect) return;
|
||||||
|
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const spaceBelow = viewportHeight - rect.bottom;
|
||||||
|
const spaceAbove = rect.top;
|
||||||
|
|
||||||
|
let top: number;
|
||||||
|
if (spaceBelow >= MENU_ESTIMATED_HEIGHT + MENU_SAFE_PADDING) {
|
||||||
|
top = rect.bottom + 4;
|
||||||
|
} else if (spaceAbove >= MENU_ESTIMATED_HEIGHT + MENU_SAFE_PADDING) {
|
||||||
|
top = rect.top - MENU_ESTIMATED_HEIGHT - 4;
|
||||||
|
} else {
|
||||||
|
top = Math.max(
|
||||||
|
MENU_SAFE_PADDING,
|
||||||
|
viewportHeight - MENU_ESTIMATED_HEIGHT - MENU_SAFE_PADDING,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const left = Math.max(MENU_SAFE_PADDING, rect.right - 160);
|
||||||
|
|
||||||
|
setMenuPos({ name, top, left });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleBatchDelete() {
|
async function handleBatchDelete() {
|
||||||
|
|
@ -228,15 +262,6 @@ export function DesignFilesPanel({
|
||||||
<div className={`df-panel ${preview ? '' : 'no-preview'}`}>
|
<div className={`df-panel ${preview ? '' : 'no-preview'}`}>
|
||||||
<div className="df-main">
|
<div className="df-main">
|
||||||
<div className="df-head">
|
<div className="df-head">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="icon-only"
|
|
||||||
onClick={() => setPreview(null)}
|
|
||||||
title={t('designFiles.up')}
|
|
||||||
aria-label={t('designFiles.back')}
|
|
||||||
>
|
|
||||||
↑
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="icon-only"
|
className="icon-only"
|
||||||
|
|
@ -329,154 +354,249 @@ export function DesignFilesPanel({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{SECTION_ORDER.filter((s) => grouped[s].length > 0).map((section) => {
|
{sortedFiles.length > 0 ? (
|
||||||
const sectionFiles = grouped[section];
|
<>
|
||||||
const visibleLimit = sectionLimits[section] ?? INITIAL_SECTION_FILE_LIMIT;
|
<div className="df-pagination df-pagination-start">
|
||||||
const visibleFiles = sectionFiles.slice(0, visibleLimit);
|
<label>
|
||||||
const hiddenCount = sectionFiles.length - visibleFiles.length;
|
{t('designFiles.perPage')}:
|
||||||
return (
|
<select
|
||||||
<div className="df-section" key={section}>
|
value={pageSize === 'all' ? 'all' : pageSize}
|
||||||
<div className="df-section-label">
|
onChange={(e) => {
|
||||||
{t(SECTION_LABEL_KEY[section])}
|
const val = e.target.value;
|
||||||
<span className="df-section-count">{sectionFiles.length}</span>
|
setPageSize(val === 'all' ? 'all' : Number(val));
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="df-select-all"
|
|
||||||
title={t('designFiles.selectAll')}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
selectAllInSection(sectionFiles);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('designFiles.selectAll')}
|
|
||||||
</button>
|
|
||||||
{sectionFiles.some((f) => selected.has(f.name)) ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="df-select-all"
|
|
||||||
title={t('designFiles.clearSelection')}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
clearSection(sectionFiles);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('designFiles.clearSelection')}
|
<option value={15}>15</option>
|
||||||
|
<option value={30}>30</option>
|
||||||
|
<option value={45}>45</option>
|
||||||
|
<option value={60}>60</option>
|
||||||
|
<option value="all">{t('designFiles.all')}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<span className="df-page-info">
|
||||||
|
{t('designFiles.pageInfo', { start: rangeStart, end: rangeEnd, total: sortedFiles.length })}
|
||||||
|
</span>
|
||||||
|
<div className="df-select-bar">
|
||||||
|
<button type="button" className="df-select-all" onClick={toggleSelectPage}>
|
||||||
|
{t('designFiles.selectPage')}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
{selected.size < sortedFiles.length ? (
|
||||||
</div>
|
<button type="button" className="df-select-all" onClick={selectAllFiles}>
|
||||||
{visibleFiles.map((f) => {
|
{t('designFiles.selectAll', { n: sortedFiles.length })}
|
||||||
const active = preview === f.name;
|
</button>
|
||||||
const isHovered = hover === f.name;
|
) : null}
|
||||||
return (
|
{selected.size > 0 ? (
|
||||||
|
<button type="button" className="df-select-all" onClick={clearSelection}>
|
||||||
|
{t('designFiles.clearSelection')}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="df-pagination-right">
|
||||||
<button
|
<button
|
||||||
key={f.name}
|
|
||||||
type="button"
|
type="button"
|
||||||
data-testid={`design-file-row-${f.name}`}
|
className="df-page-btn"
|
||||||
className={`df-row ${active ? 'active' : ''} ${selected.has(f.name) ? 'selected' : ''}`}
|
disabled={safePage <= 0}
|
||||||
onMouseEnter={() => setHover(f.name)}
|
onClick={() => setPage(Math.max(0, safePage - 1))}
|
||||||
onMouseLeave={() => setHover((c) => (c === f.name ? null : c))}
|
|
||||||
onClick={() => setPreview(f.name)}
|
|
||||||
onDoubleClick={() => onOpenFile(f.name)}
|
|
||||||
>
|
>
|
||||||
<span
|
{t('designFiles.prev')}
|
||||||
className="df-row-check"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
toggleSelect(f.name);
|
|
||||||
}}
|
|
||||||
role="checkbox"
|
|
||||||
aria-checked={selected.has(f.name)}
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
toggleSelect(f.name);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selected.has(f.name) ? '☑' : '☐'}
|
|
||||||
</span>
|
|
||||||
<span className="df-row-icon" data-kind={f.kind} aria-hidden>
|
|
||||||
{kindGlyph(f.kind)}
|
|
||||||
</span>
|
|
||||||
<span className="df-row-name-wrap">
|
|
||||||
<span className="df-row-name">{f.name}</span>
|
|
||||||
<span className="df-row-sub">{kindLabel(f.kind, t)}</span>
|
|
||||||
</span>
|
|
||||||
<span className="df-row-time">{relativeTime(f.mtime, t)}</span>
|
|
||||||
<span
|
|
||||||
data-testid={`design-file-menu-${f.name}`}
|
|
||||||
className="df-row-menu"
|
|
||||||
style={isHovered || active ? { opacity: 1 } : undefined}
|
|
||||||
role="button"
|
|
||||||
aria-label={t('designFiles.rowMenu')}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const rect = (e.target as HTMLElement)
|
|
||||||
.closest('.df-row-menu')
|
|
||||||
?.getBoundingClientRect();
|
|
||||||
if (!rect) return;
|
|
||||||
|
|
||||||
const viewportHeight = window.innerHeight;
|
|
||||||
const spaceBelow = viewportHeight - rect.bottom;
|
|
||||||
const spaceAbove = rect.top;
|
|
||||||
|
|
||||||
let top: number;
|
|
||||||
if (spaceBelow >= MENU_ESTIMATED_HEIGHT + MENU_SAFE_PADDING) {
|
|
||||||
top = rect.bottom + 4;
|
|
||||||
} else if (spaceAbove >= MENU_ESTIMATED_HEIGHT + MENU_SAFE_PADDING) {
|
|
||||||
top = rect.top - MENU_ESTIMATED_HEIGHT - 4;
|
|
||||||
} else {
|
|
||||||
top = Math.max(
|
|
||||||
MENU_SAFE_PADDING,
|
|
||||||
viewportHeight - MENU_ESTIMATED_HEIGHT - MENU_SAFE_PADDING,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const left = Math.max(MENU_SAFE_PADDING, rect.right - 160);
|
|
||||||
|
|
||||||
setMenuPos({
|
|
||||||
name: f.name,
|
|
||||||
top,
|
|
||||||
left,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
⋯
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
<button
|
||||||
})}
|
type="button"
|
||||||
{hiddenCount > 0 ? (
|
className="df-page-btn"
|
||||||
|
disabled={safePage >= totalPages - 1}
|
||||||
|
onClick={() => setPage(Math.min(totalPages - 1, safePage + 1))}
|
||||||
|
>
|
||||||
|
{t('designFiles.next')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table className="df-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="df-th-check">
|
||||||
|
<span
|
||||||
|
className="df-row-check"
|
||||||
|
onClick={toggleSelectPage}
|
||||||
|
role="checkbox"
|
||||||
|
aria-checked={allPageSelected}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleSelectPage();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) (el as HTMLElement).ariaChecked = allPageSelected ? 'true' : somePageSelected ? 'mixed' : 'false';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{allPageSelected ? '\u2611' : somePageSelected ? '\u25A3' : '\u2610'}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th className="df-th-icon" />
|
||||||
|
<th
|
||||||
|
className="df-th-name df-th-sortable"
|
||||||
|
aria-sort={sortKey === 'name' ? (sortDir === 'asc' ? 'ascending' : 'descending') : 'none'}
|
||||||
|
>
|
||||||
|
<button type="button" className="df-th-btn" onClick={toggleSort('name')}>
|
||||||
|
{t('designFiles.colName')}
|
||||||
|
{sortKey === 'name' ? <span className="df-sort-arrow">{sortDir === 'asc' ? ' \u2191' : ' \u2193'}</span> : null}
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="df-th-kind df-th-sortable"
|
||||||
|
aria-sort={sortKey === 'kind' ? (sortDir === 'asc' ? 'ascending' : 'descending') : 'none'}
|
||||||
|
>
|
||||||
|
<button type="button" className="df-th-btn" onClick={toggleSort('kind')}>
|
||||||
|
{t('designFiles.colKind')}
|
||||||
|
{sortKey === 'kind' ? <span className="df-sort-arrow">{sortDir === 'asc' ? ' \u2191' : ' \u2193'}</span> : null}
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="df-th-time df-th-sortable"
|
||||||
|
aria-sort={sortKey === 'mtime' ? (sortDir === 'asc' ? 'ascending' : 'descending') : 'none'}
|
||||||
|
>
|
||||||
|
<button type="button" className="df-th-btn" onClick={toggleSort('mtime')}>
|
||||||
|
{t('designFiles.colModified')}
|
||||||
|
{sortKey === 'mtime' ? <span className="df-sort-arrow">{sortDir === 'asc' ? ' \u2191' : ' \u2193'}</span> : null}
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
<th className="df-th-menu" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{pageFiles.map((f) => {
|
||||||
|
const active = preview === f.name;
|
||||||
|
const isHovered = hover === f.name;
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={f.name}
|
||||||
|
data-testid={`design-file-row-${f.name}`}
|
||||||
|
className={`df-file-row ${active ? 'active' : ''} ${selected.has(f.name) ? 'selected' : ''}`}
|
||||||
|
onMouseEnter={() => setHover(f.name)}
|
||||||
|
onMouseLeave={() => setHover((c) => (c === f.name ? null : c))}
|
||||||
|
>
|
||||||
|
<td className="df-cell-check">
|
||||||
|
<span
|
||||||
|
className="df-row-check"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleSelect(f.name);
|
||||||
|
}}
|
||||||
|
role="checkbox"
|
||||||
|
aria-checked={selected.has(f.name)}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleSelect(f.name);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selected.has(f.name) ? '\u2611' : '\u2610'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="df-cell-icon">
|
||||||
|
<span className="df-row-icon" data-kind={f.kind} aria-hidden>
|
||||||
|
{kindGlyph(f.kind)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="df-cell-name">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="df-row-name-btn"
|
||||||
|
onClick={() => setPreview(f.name)}
|
||||||
|
onDoubleClick={() => onOpenFile(f.name)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
const now = Date.now();
|
||||||
|
const last = lastKeyPress.current.get(f.name) ?? 0;
|
||||||
|
if (now - last < 300) {
|
||||||
|
lastKeyPress.current.delete(f.name);
|
||||||
|
onOpenFile(f.name);
|
||||||
|
} else {
|
||||||
|
lastKeyPress.current.set(f.name, now);
|
||||||
|
setPreview(f.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="df-row-name-wrap">
|
||||||
|
<span className="df-row-name">{f.name}</span>
|
||||||
|
<span className="df-row-sub">{humanBytes(f.size)}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td className="df-cell-kind">
|
||||||
|
<span className="df-kind-label">{kindLabel(f.kind, t)}</span>
|
||||||
|
</td>
|
||||||
|
<td className="df-cell-time">{relativeTime(f.mtime, t)}</td>
|
||||||
|
<td className="df-cell-menu">
|
||||||
|
<span
|
||||||
|
data-testid={`design-file-menu-${f.name}`}
|
||||||
|
className="df-row-menu"
|
||||||
|
style={isHovered || active ? { opacity: 1 } : undefined}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={t('designFiles.rowMenu')}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openMenuFor(f.name, e.target as HTMLElement);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
openMenuFor(f.name, e.currentTarget as HTMLElement);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
⋯
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className="df-pagination df-pagination-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="df-section-more"
|
className="df-page-btn"
|
||||||
disabled={isSectionExpansionPending}
|
disabled={safePage <= 0}
|
||||||
aria-busy={isSectionExpansionPending}
|
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||||
onClick={() =>
|
|
||||||
startSectionExpansion(() => {
|
|
||||||
setSectionLimits((curr) => ({
|
|
||||||
...curr,
|
|
||||||
[section]: Math.min(
|
|
||||||
sectionFiles.length,
|
|
||||||
visibleLimit + SECTION_FILE_LIMIT_INCREMENT,
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Icon name={isSectionExpansionPending ? 'spinner' : 'plus'} size={12} />
|
{t('designFiles.prev')}
|
||||||
<span>
|
|
||||||
{t('designFiles.showMore', {
|
|
||||||
n: Math.min(hiddenCount, SECTION_FILE_LIMIT_INCREMENT),
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
<label>
|
||||||
</div>
|
{t('designFiles.jumpToPage')}:
|
||||||
);
|
<select
|
||||||
})}
|
value={safePage}
|
||||||
|
onChange={(e) => setPage(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
{Array.from({ length: totalPages }, (_, i) => (
|
||||||
|
<option key={i} value={i}>
|
||||||
|
{i + 1}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="df-page-btn"
|
||||||
|
disabled={safePage >= totalPages - 1}
|
||||||
|
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||||
|
>
|
||||||
|
{t('designFiles.next')}
|
||||||
|
</button>
|
||||||
|
<span className="df-page-info">
|
||||||
|
{t('designFiles.pageInfo', { start: rangeStart, end: rangeEnd, total: sortedFiles.length })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
|
@ -649,31 +769,32 @@ function DfPreview({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sectionFor(file: ProjectFile): Section {
|
function kindSortPriority(kind: ProjectFileKind): number {
|
||||||
if (file.kind === 'html' || file.kind === 'text') return 'pages';
|
if (kind === 'html') return 0;
|
||||||
if (file.kind === 'sketch') return 'sketches';
|
if (kind === 'text') return 1;
|
||||||
if (file.kind === 'code') return 'scripts';
|
if (kind === 'code') return 2;
|
||||||
if (file.kind === 'image') return 'images';
|
if (kind === 'sketch') return 3;
|
||||||
if (
|
if (kind === 'image') return 4;
|
||||||
file.kind === 'pdf' ||
|
if (kind === 'document') return 5;
|
||||||
file.kind === 'document' ||
|
if (kind === 'pdf') return 6;
|
||||||
file.kind === 'presentation' ||
|
if (kind === 'presentation') return 7;
|
||||||
file.kind === 'spreadsheet'
|
if (kind === 'spreadsheet') return 8;
|
||||||
) return 'pages';
|
if (kind === 'video') return 9;
|
||||||
return 'other';
|
if (kind === 'audio') return 10;
|
||||||
|
return 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
function kindGlyph(kind: ProjectFileKind): string {
|
function kindGlyph(kind: ProjectFileKind): string {
|
||||||
if (kind === 'html') return '⟨⟩';
|
if (kind === 'html') return '\u27E8\u27E9';
|
||||||
if (kind === 'image') return '▣';
|
if (kind === 'image') return '\u25A3';
|
||||||
if (kind === 'sketch') return '✎';
|
if (kind === 'sketch') return '\u270E';
|
||||||
if (kind === 'text') return '¶';
|
if (kind === 'text') return '\u00B6';
|
||||||
if (kind === 'code') return '{}';
|
if (kind === 'code') return '\u007B\u007D';
|
||||||
if (kind === 'pdf') return 'PDF';
|
if (kind === 'pdf') return 'PDF';
|
||||||
if (kind === 'document') return 'DOC';
|
if (kind === 'document') return 'DOC';
|
||||||
if (kind === 'presentation') return 'PPT';
|
if (kind === 'presentation') return 'PPT';
|
||||||
if (kind === 'spreadsheet') return 'XLS';
|
if (kind === 'spreadsheet') return 'XLS';
|
||||||
return '·';
|
return '\u00B7';
|
||||||
}
|
}
|
||||||
|
|
||||||
function kindLabel(kind: ProjectFileKind, t: TranslateFn): string {
|
function kindLabel(kind: ProjectFileKind, t: TranslateFn): string {
|
||||||
|
|
|
||||||
|
|
@ -599,6 +599,7 @@ export const ar: Dict = {
|
||||||
'لا يوجد شيء هنا بعد. اسحب الملفات أدناه، أو أنشئ رسماً / الصق نصاً.',
|
'لا يوجد شيء هنا بعد. اسحب الملفات أدناه، أو أنشئ رسماً / الصق نصاً.',
|
||||||
'designFiles.refresh': 'تحديث',
|
'designFiles.refresh': 'تحديث',
|
||||||
'designFiles.delete': 'حذف',
|
'designFiles.delete': 'حذف',
|
||||||
|
'designFiles.deleteSelected': 'حذف {n}',
|
||||||
'designFiles.searchPlaceholder': 'بحث في الملفات...',
|
'designFiles.searchPlaceholder': 'بحث في الملفات...',
|
||||||
'designFiles.up': 'للأعلى',
|
'designFiles.up': 'للأعلى',
|
||||||
'designFiles.back': 'رجوع',
|
'designFiles.back': 'رجوع',
|
||||||
|
|
@ -606,10 +607,10 @@ export const ar: Dict = {
|
||||||
'designFiles.rowMenu': 'قائمة الصف',
|
'designFiles.rowMenu': 'قائمة الصف',
|
||||||
'designFiles.openInTab': 'فتح في علامة تبويب',
|
'designFiles.openInTab': 'فتح في علامة تبويب',
|
||||||
'designFiles.download': 'تحميل',
|
'designFiles.download': 'تحميل',
|
||||||
'designFiles.downloadSelected': 'Download {n} as ZIP',
|
'designFiles.downloadSelected': 'تنزيل {n} كـ ZIP',
|
||||||
'designFiles.deleteSelected': 'حذف {n}',
|
'designFiles.clearSelection': 'مسح التحديد',
|
||||||
'designFiles.clearSelection': 'Clear',
|
'designFiles.selectPage': 'تحديد الكل في الصفحة',
|
||||||
'designFiles.selectAll': 'Select all',
|
'designFiles.selectAll': 'تحديد الكل',
|
||||||
'designFiles.dropTitle': '⤓ أسقط الملفات هنا',
|
'designFiles.dropTitle': '⤓ أسقط الملفات هنا',
|
||||||
'designFiles.dropDesc':
|
'designFiles.dropDesc':
|
||||||
'الصور، المستندات، المراجع، أو المجلدات - سيستخدمها الوكيل كسياق.',
|
'الصور، المستندات، المراجع، أو المجلدات - سيستخدمها الوكيل كسياق.',
|
||||||
|
|
@ -637,6 +638,15 @@ export const ar: Dict = {
|
||||||
'designFiles.kindPresentation': 'عرض تقديمي',
|
'designFiles.kindPresentation': 'عرض تقديمي',
|
||||||
'designFiles.kindSpreadsheet': 'جدول بيانات',
|
'designFiles.kindSpreadsheet': 'جدول بيانات',
|
||||||
'designFiles.kindBinary': 'ثنائي',
|
'designFiles.kindBinary': 'ثنائي',
|
||||||
|
'designFiles.colName': 'الاسم',
|
||||||
|
'designFiles.colKind': 'النوع',
|
||||||
|
'designFiles.colModified': 'آخر تعديل',
|
||||||
|
'designFiles.perPage': 'عرض',
|
||||||
|
'designFiles.all': 'الكل',
|
||||||
|
'designFiles.prev': 'السابق',
|
||||||
|
'designFiles.next': 'التالي',
|
||||||
|
'designFiles.jumpToPage': 'انتقل إلى الصفحة',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} من {total}',
|
||||||
'quickSwitcher.placeholder': 'فتح ملف…',
|
'quickSwitcher.placeholder': 'فتح ملف…',
|
||||||
'quickSwitcher.empty': 'لا توجد ملفات في هذا المشروع',
|
'quickSwitcher.empty': 'لا توجد ملفات في هذا المشروع',
|
||||||
'quickSwitcher.noMatches': 'لا توجد نتائج',
|
'quickSwitcher.noMatches': 'لا توجد نتائج',
|
||||||
|
|
|
||||||
|
|
@ -494,10 +494,11 @@ export const de: Dict = {
|
||||||
'designFiles.rowMenu': 'Zeilenmenü',
|
'designFiles.rowMenu': 'Zeilenmenü',
|
||||||
'designFiles.openInTab': 'In Tab öffnen',
|
'designFiles.openInTab': 'In Tab öffnen',
|
||||||
'designFiles.download': 'Herunterladen',
|
'designFiles.download': 'Herunterladen',
|
||||||
'designFiles.downloadSelected': 'Download {n} as ZIP',
|
'designFiles.downloadSelected': '{n} als ZIP herunterladen',
|
||||||
'designFiles.deleteSelected': '{n} löschen',
|
'designFiles.deleteSelected': '{n} löschen',
|
||||||
'designFiles.clearSelection': 'Clear',
|
'designFiles.clearSelection': 'Auswahl aufheben',
|
||||||
'designFiles.selectAll': 'Select all',
|
'designFiles.selectPage': 'Alle auf dieser Seite auswählen',
|
||||||
|
'designFiles.selectAll': 'Alle auswählen',
|
||||||
'designFiles.dropTitle': '⤓ Dateien hier ablegen',
|
'designFiles.dropTitle': '⤓ Dateien hier ablegen',
|
||||||
'designFiles.dropDesc':
|
'designFiles.dropDesc':
|
||||||
'Bilder, Docs, Referenzen oder Ordner — der Agent nutzt sie als Kontext.',
|
'Bilder, Docs, Referenzen oder Ordner — der Agent nutzt sie als Kontext.',
|
||||||
|
|
@ -525,6 +526,15 @@ export const de: Dict = {
|
||||||
'designFiles.kindPresentation': 'Präsentation',
|
'designFiles.kindPresentation': 'Präsentation',
|
||||||
'designFiles.kindSpreadsheet': 'Tabellenblatt',
|
'designFiles.kindSpreadsheet': 'Tabellenblatt',
|
||||||
'designFiles.kindBinary': 'Binärdatei',
|
'designFiles.kindBinary': 'Binärdatei',
|
||||||
|
'designFiles.colName': 'Name',
|
||||||
|
'designFiles.colKind': 'Art',
|
||||||
|
'designFiles.colModified': 'Geändert',
|
||||||
|
'designFiles.perPage': 'Anzeigen',
|
||||||
|
'designFiles.all': 'Alle',
|
||||||
|
'designFiles.prev': 'Zurück',
|
||||||
|
'designFiles.next': 'Weiter',
|
||||||
|
'designFiles.jumpToPage': 'Gehe zu Seite',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} von {total}',
|
||||||
'quickSwitcher.placeholder': 'Datei öffnen…',
|
'quickSwitcher.placeholder': 'Datei öffnen…',
|
||||||
'quickSwitcher.empty': 'Keine Dateien in diesem Projekt',
|
'quickSwitcher.empty': 'Keine Dateien in diesem Projekt',
|
||||||
'quickSwitcher.noMatches': 'Keine Treffer',
|
'quickSwitcher.noMatches': 'Keine Treffer',
|
||||||
|
|
|
||||||
|
|
@ -620,7 +620,8 @@ export const en: Dict = {
|
||||||
'designFiles.downloadSelected': 'Download {n} as ZIP',
|
'designFiles.downloadSelected': 'Download {n} as ZIP',
|
||||||
'designFiles.deleteSelected': 'Delete {n}',
|
'designFiles.deleteSelected': 'Delete {n}',
|
||||||
'designFiles.clearSelection': 'Clear',
|
'designFiles.clearSelection': 'Clear',
|
||||||
'designFiles.selectAll': 'Select all',
|
'designFiles.selectPage': 'Select all on page',
|
||||||
|
'designFiles.selectAll': 'Select everything',
|
||||||
'designFiles.dropTitle': '⤓ Drop files here',
|
'designFiles.dropTitle': '⤓ Drop files here',
|
||||||
'designFiles.dropDesc':
|
'designFiles.dropDesc':
|
||||||
'Images, docs, references, or folders — the agent will use them as context.',
|
'Images, docs, references, or folders — the agent will use them as context.',
|
||||||
|
|
@ -650,6 +651,15 @@ export const en: Dict = {
|
||||||
'designFiles.kindSpreadsheet': 'Spreadsheet',
|
'designFiles.kindSpreadsheet': 'Spreadsheet',
|
||||||
'designFiles.kindLiveArtifact': 'Live artifact',
|
'designFiles.kindLiveArtifact': 'Live artifact',
|
||||||
'designFiles.kindBinary': 'Binary',
|
'designFiles.kindBinary': 'Binary',
|
||||||
|
'designFiles.colName': 'Name',
|
||||||
|
'designFiles.colKind': 'Kind',
|
||||||
|
'designFiles.colModified': 'Modified',
|
||||||
|
'designFiles.perPage': 'Show',
|
||||||
|
'designFiles.all': 'All',
|
||||||
|
'designFiles.prev': 'Previous',
|
||||||
|
'designFiles.next': 'Next',
|
||||||
|
'designFiles.jumpToPage': 'Go to page',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} of {total}',
|
||||||
'quickSwitcher.placeholder': 'Open file…',
|
'quickSwitcher.placeholder': 'Open file…',
|
||||||
'quickSwitcher.empty': 'No files in this project',
|
'quickSwitcher.empty': 'No files in this project',
|
||||||
'quickSwitcher.noMatches': 'No matches',
|
'quickSwitcher.noMatches': 'No matches',
|
||||||
|
|
|
||||||
|
|
@ -495,10 +495,11 @@ export const esES: Dict = {
|
||||||
'designFiles.rowMenu': 'Menú de la fila',
|
'designFiles.rowMenu': 'Menú de la fila',
|
||||||
'designFiles.openInTab': 'Abrir en pestaña',
|
'designFiles.openInTab': 'Abrir en pestaña',
|
||||||
'designFiles.download': 'Descargar',
|
'designFiles.download': 'Descargar',
|
||||||
'designFiles.downloadSelected': 'Download {n} as ZIP',
|
'designFiles.downloadSelected': 'Descargar {n} como ZIP',
|
||||||
'designFiles.deleteSelected': 'Eliminar {n}',
|
'designFiles.deleteSelected': 'Eliminar {n}',
|
||||||
'designFiles.clearSelection': 'Clear',
|
'designFiles.clearSelection': 'Limpiar selección',
|
||||||
'designFiles.selectAll': 'Select all',
|
'designFiles.selectPage': 'Seleccionar todo en la página',
|
||||||
|
'designFiles.selectAll': 'Seleccionar todo',
|
||||||
'designFiles.dropTitle': '⤓ Suelta archivos aquí',
|
'designFiles.dropTitle': '⤓ Suelta archivos aquí',
|
||||||
'designFiles.dropDesc':
|
'designFiles.dropDesc':
|
||||||
'Imágenes, documentos, referencias o carpetas: el agente los usará como contexto.',
|
'Imágenes, documentos, referencias o carpetas: el agente los usará como contexto.',
|
||||||
|
|
@ -526,6 +527,15 @@ export const esES: Dict = {
|
||||||
'designFiles.kindPresentation': 'Presentación',
|
'designFiles.kindPresentation': 'Presentación',
|
||||||
'designFiles.kindSpreadsheet': 'Hoja de cálculo',
|
'designFiles.kindSpreadsheet': 'Hoja de cálculo',
|
||||||
'designFiles.kindBinary': 'Binario',
|
'designFiles.kindBinary': 'Binario',
|
||||||
|
'designFiles.colName': 'Nombre',
|
||||||
|
'designFiles.colKind': 'Tipo',
|
||||||
|
'designFiles.colModified': 'Modificado',
|
||||||
|
'designFiles.perPage': 'Mostrar',
|
||||||
|
'designFiles.all': 'Todos',
|
||||||
|
'designFiles.prev': 'Anterior',
|
||||||
|
'designFiles.next': 'Siguiente',
|
||||||
|
'designFiles.jumpToPage': 'Ir a la página',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} de {total}',
|
||||||
'quickSwitcher.placeholder': 'Abrir archivo…',
|
'quickSwitcher.placeholder': 'Abrir archivo…',
|
||||||
'quickSwitcher.empty': 'No hay archivos en este proyecto',
|
'quickSwitcher.empty': 'No hay archivos en este proyecto',
|
||||||
'quickSwitcher.noMatches': 'Sin resultados',
|
'quickSwitcher.noMatches': 'Sin resultados',
|
||||||
|
|
|
||||||
|
|
@ -610,6 +610,7 @@ export const fa: Dict = {
|
||||||
'هنوز هیچ چیزی اینجا نیست. فایلها را رها کنید، یا یک طرح ایجاد کنید / متن بچسبانید.',
|
'هنوز هیچ چیزی اینجا نیست. فایلها را رها کنید، یا یک طرح ایجاد کنید / متن بچسبانید.',
|
||||||
'designFiles.refresh': 'بازنشانی',
|
'designFiles.refresh': 'بازنشانی',
|
||||||
'designFiles.delete': 'حذف',
|
'designFiles.delete': 'حذف',
|
||||||
|
'designFiles.deleteSelected': 'حذف {n}',
|
||||||
'designFiles.searchPlaceholder': 'جستجوی فایلها…',
|
'designFiles.searchPlaceholder': 'جستجوی فایلها…',
|
||||||
'designFiles.up': 'بالا',
|
'designFiles.up': 'بالا',
|
||||||
'designFiles.back': 'بازگشت',
|
'designFiles.back': 'بازگشت',
|
||||||
|
|
@ -617,10 +618,10 @@ export const fa: Dict = {
|
||||||
'designFiles.rowMenu': 'منوی ردیف',
|
'designFiles.rowMenu': 'منوی ردیف',
|
||||||
'designFiles.openInTab': 'باز کردن در تب',
|
'designFiles.openInTab': 'باز کردن در تب',
|
||||||
'designFiles.download': 'دانلود',
|
'designFiles.download': 'دانلود',
|
||||||
'designFiles.downloadSelected': 'Download {n} as ZIP',
|
'designFiles.downloadSelected': 'دانلود {n} به صورت ZIP',
|
||||||
'designFiles.deleteSelected': 'حذف {n}',
|
'designFiles.clearSelection': 'پاک کردن انتخاب',
|
||||||
'designFiles.clearSelection': 'Clear',
|
'designFiles.selectPage': 'انتخاب همه در صفحه',
|
||||||
'designFiles.selectAll': 'Select all',
|
'designFiles.selectAll': 'انتخاب همه',
|
||||||
'designFiles.dropTitle': '⤓ فایلها را اینجا رها کنید',
|
'designFiles.dropTitle': '⤓ فایلها را اینجا رها کنید',
|
||||||
'designFiles.dropDesc':
|
'designFiles.dropDesc':
|
||||||
'تصاویر، اسناد، مراجع یا پوشهها — عامل از آنها به عنوان زمینه استفاده خواهد کرد.',
|
'تصاویر، اسناد، مراجع یا پوشهها — عامل از آنها به عنوان زمینه استفاده خواهد کرد.',
|
||||||
|
|
@ -650,6 +651,15 @@ export const fa: Dict = {
|
||||||
'designFiles.kindSpreadsheet': 'صفحه گسترده',
|
'designFiles.kindSpreadsheet': 'صفحه گسترده',
|
||||||
'designFiles.kindLiveArtifact': 'مصنوع زنده',
|
'designFiles.kindLiveArtifact': 'مصنوع زنده',
|
||||||
'designFiles.kindBinary': 'باینری',
|
'designFiles.kindBinary': 'باینری',
|
||||||
|
'designFiles.colName': 'نام',
|
||||||
|
'designFiles.colKind': 'نوع',
|
||||||
|
'designFiles.colModified': 'تغییر یافته',
|
||||||
|
'designFiles.perPage': 'نمایش',
|
||||||
|
'designFiles.all': 'همه',
|
||||||
|
'designFiles.prev': 'قبلی',
|
||||||
|
'designFiles.next': 'بعدی',
|
||||||
|
'designFiles.jumpToPage': 'برو به صفحه',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} از {total}',
|
||||||
'quickSwitcher.placeholder': 'باز کردن فایل…',
|
'quickSwitcher.placeholder': 'باز کردن فایل…',
|
||||||
'quickSwitcher.empty': 'هیچ فایلی در این پروژه نیست',
|
'quickSwitcher.empty': 'هیچ فایلی در این پروژه نیست',
|
||||||
'quickSwitcher.noMatches': 'بدون نتیجه',
|
'quickSwitcher.noMatches': 'بدون نتیجه',
|
||||||
|
|
|
||||||
|
|
@ -606,10 +606,11 @@ export const fr: Dict = {
|
||||||
'designFiles.rowMenu': 'Menu de ligne',
|
'designFiles.rowMenu': 'Menu de ligne',
|
||||||
'designFiles.openInTab': 'Ouvrir dans un onglet',
|
'designFiles.openInTab': 'Ouvrir dans un onglet',
|
||||||
'designFiles.download': 'Télécharger',
|
'designFiles.download': 'Télécharger',
|
||||||
'designFiles.downloadSelected': 'Download {n} as ZIP',
|
'designFiles.downloadSelected': 'Télécharger {n} sélectionnés en ZIP',
|
||||||
|
'designFiles.clearSelection': 'Effacer la sélection',
|
||||||
|
'designFiles.selectPage': 'Tout sélectionner sur la page',
|
||||||
|
'designFiles.selectAll': 'Tout sélectionner',
|
||||||
'designFiles.deleteSelected': 'Supprimer {n}',
|
'designFiles.deleteSelected': 'Supprimer {n}',
|
||||||
'designFiles.clearSelection': 'Clear',
|
|
||||||
'designFiles.selectAll': 'Select all',
|
|
||||||
'designFiles.dropTitle': '⤓ Déposez les fichiers ici',
|
'designFiles.dropTitle': '⤓ Déposez les fichiers ici',
|
||||||
'designFiles.dropDesc':
|
'designFiles.dropDesc':
|
||||||
'Images, documents, références ou dossiers — l\'agent les utilisera comme contexte.',
|
'Images, documents, références ou dossiers — l\'agent les utilisera comme contexte.',
|
||||||
|
|
@ -637,6 +638,15 @@ export const fr: Dict = {
|
||||||
'designFiles.kindPresentation': 'Présentation',
|
'designFiles.kindPresentation': 'Présentation',
|
||||||
'designFiles.kindSpreadsheet': 'Tableur',
|
'designFiles.kindSpreadsheet': 'Tableur',
|
||||||
'designFiles.kindBinary': 'Binaire',
|
'designFiles.kindBinary': 'Binaire',
|
||||||
|
'designFiles.colName': 'Nom',
|
||||||
|
'designFiles.colKind': 'Type',
|
||||||
|
'designFiles.colModified': 'Modifié le',
|
||||||
|
'designFiles.perPage': 'Afficher',
|
||||||
|
'designFiles.all': 'Tout',
|
||||||
|
'designFiles.prev': 'Précédent',
|
||||||
|
'designFiles.next': 'Suivant',
|
||||||
|
'designFiles.jumpToPage': 'Aller à la page',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} sur {total}',
|
||||||
'quickSwitcher.placeholder': 'Ouvrir un fichier…',
|
'quickSwitcher.placeholder': 'Ouvrir un fichier…',
|
||||||
'quickSwitcher.empty': 'Aucun fichier dans ce projet',
|
'quickSwitcher.empty': 'Aucun fichier dans ce projet',
|
||||||
'quickSwitcher.noMatches': 'Aucun résultat',
|
'quickSwitcher.noMatches': 'Aucun résultat',
|
||||||
|
|
|
||||||
|
|
@ -606,10 +606,11 @@ export const hu: Dict = {
|
||||||
'designFiles.rowMenu': 'Sor menü',
|
'designFiles.rowMenu': 'Sor menü',
|
||||||
'designFiles.openInTab': 'Megnyitás lapon',
|
'designFiles.openInTab': 'Megnyitás lapon',
|
||||||
'designFiles.download': 'Letöltés',
|
'designFiles.download': 'Letöltés',
|
||||||
'designFiles.downloadSelected': 'Download {n} as ZIP',
|
'designFiles.downloadSelected': '{n} fájl letöltése ZIP-ként',
|
||||||
|
'designFiles.clearSelection': 'Kijelölés törlése',
|
||||||
|
'designFiles.selectPage': 'Összes kijelölése az oldalon',
|
||||||
|
'designFiles.selectAll': 'Összes kijelölése',
|
||||||
'designFiles.deleteSelected': '{n} törlése',
|
'designFiles.deleteSelected': '{n} törlése',
|
||||||
'designFiles.clearSelection': 'Clear',
|
|
||||||
'designFiles.selectAll': 'Select all',
|
|
||||||
'designFiles.dropTitle': '⤓ Húzd ide a fájlokat',
|
'designFiles.dropTitle': '⤓ Húzd ide a fájlokat',
|
||||||
'designFiles.dropDesc':
|
'designFiles.dropDesc':
|
||||||
'Képek, dokumentumok, hivatkozások vagy mappák — az ügynök kontextusként használja őket.',
|
'Képek, dokumentumok, hivatkozások vagy mappák — az ügynök kontextusként használja őket.',
|
||||||
|
|
@ -637,6 +638,15 @@ export const hu: Dict = {
|
||||||
'designFiles.kindPresentation': 'Prezentáció',
|
'designFiles.kindPresentation': 'Prezentáció',
|
||||||
'designFiles.kindSpreadsheet': 'Táblázat',
|
'designFiles.kindSpreadsheet': 'Táblázat',
|
||||||
'designFiles.kindBinary': 'Bináris',
|
'designFiles.kindBinary': 'Bináris',
|
||||||
|
'designFiles.colName': 'Név',
|
||||||
|
'designFiles.colKind': 'Típus',
|
||||||
|
'designFiles.colModified': 'Módosítva',
|
||||||
|
'designFiles.perPage': 'Mutat',
|
||||||
|
'designFiles.all': 'Összes',
|
||||||
|
'designFiles.prev': 'Előző',
|
||||||
|
'designFiles.next': 'Következő',
|
||||||
|
'designFiles.jumpToPage': 'Ugrás oldalra',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} / {total}',
|
||||||
'quickSwitcher.placeholder': 'Fájl megnyitása…',
|
'quickSwitcher.placeholder': 'Fájl megnyitása…',
|
||||||
'quickSwitcher.empty': 'Nincsenek fájlok ebben a projektben',
|
'quickSwitcher.empty': 'Nincsenek fájlok ebben a projektben',
|
||||||
'quickSwitcher.noMatches': 'Nincs találat',
|
'quickSwitcher.noMatches': 'Nincs találat',
|
||||||
|
|
|
||||||
|
|
@ -708,6 +708,7 @@ export const id: Dict = {
|
||||||
'designFiles.downloadSelected': 'Unduh {n} sebagai ZIP',
|
'designFiles.downloadSelected': 'Unduh {n} sebagai ZIP',
|
||||||
'designFiles.deleteSelected': 'Hapus {n}',
|
'designFiles.deleteSelected': 'Hapus {n}',
|
||||||
'designFiles.clearSelection': 'Bersihkan',
|
'designFiles.clearSelection': 'Bersihkan',
|
||||||
|
'designFiles.selectPage': 'Pilih semua di halaman',
|
||||||
'designFiles.selectAll': 'Pilih semua',
|
'designFiles.selectAll': 'Pilih semua',
|
||||||
'designFiles.dropTitle': 'Lepaskan file di sini',
|
'designFiles.dropTitle': 'Lepaskan file di sini',
|
||||||
'designFiles.dropDesc': 'Upload file desain, gambar, dokumen, atau aset lain ke proyek ini.',
|
'designFiles.dropDesc': 'Upload file desain, gambar, dokumen, atau aset lain ke proyek ini.',
|
||||||
|
|
@ -737,6 +738,15 @@ export const id: Dict = {
|
||||||
'designFiles.kindSpreadsheet': 'Spreadsheet',
|
'designFiles.kindSpreadsheet': 'Spreadsheet',
|
||||||
'designFiles.kindLiveArtifact': 'Live artifact',
|
'designFiles.kindLiveArtifact': 'Live artifact',
|
||||||
'designFiles.kindBinary': 'Biner',
|
'designFiles.kindBinary': 'Biner',
|
||||||
|
'designFiles.colName': 'Nama',
|
||||||
|
'designFiles.colKind': 'Jenis',
|
||||||
|
'designFiles.colModified': 'Diubah',
|
||||||
|
'designFiles.perPage': 'Tampilkan',
|
||||||
|
'designFiles.all': 'Semua',
|
||||||
|
'designFiles.prev': 'Sebelumnya',
|
||||||
|
'designFiles.next': 'Berikutnya',
|
||||||
|
'designFiles.jumpToPage': 'Lompat ke halaman',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} dari {total}',
|
||||||
|
|
||||||
'quickSwitcher.placeholder': 'Buka file...',
|
'quickSwitcher.placeholder': 'Buka file...',
|
||||||
'quickSwitcher.empty': 'Tidak ada file di proyek ini',
|
'quickSwitcher.empty': 'Tidak ada file di proyek ini',
|
||||||
|
|
@ -913,15 +923,15 @@ export const id: Dict = {
|
||||||
'fileViewer.deployToProvider': 'Deploy ke {provider}',
|
'fileViewer.deployToProvider': 'Deploy ke {provider}',
|
||||||
'fileViewer.redeployToProvider': 'Deploy ulang ke {provider}',
|
'fileViewer.redeployToProvider': 'Deploy ulang ke {provider}',
|
||||||
'fileViewer.deployingToProvider': 'Deploying ke {provider}...',
|
'fileViewer.deployingToProvider': 'Deploying ke {provider}...',
|
||||||
'fileViewer.cloudflareApiToken': 'Cloudflare API token',
|
'fileViewer.cloudflareApiToken': 'Token API Cloudflare',
|
||||||
'fileViewer.cloudflareApiTokenGetLink': 'Dapatkan Cloudflare API token',
|
'fileViewer.cloudflareApiTokenGetLink': 'Dapatkan token API Cloudflare',
|
||||||
'fileViewer.cloudflareApiTokenPlaceholder': 'Tempelkan Cloudflare API token Anda',
|
'fileViewer.cloudflareApiTokenPlaceholder': 'Tempelkan token API Cloudflare Anda',
|
||||||
'fileViewer.cloudflareApiTokenReuseHint': 'Cloudflare API token yang tersimpan akan digunakan. Masukkan token baru untuk menggantinya.',
|
'fileViewer.cloudflareApiTokenReuseHint': 'Token API Cloudflare yang tersimpan akan digunakan. Masukkan token baru untuk menggantinya.',
|
||||||
'fileViewer.cloudflareApiTokenRequired': 'Masukkan dan simpan Cloudflare API token terlebih dahulu.',
|
'fileViewer.cloudflareApiTokenRequired': 'Masukkan dan simpan token API Cloudflare terlebih dahulu.',
|
||||||
'fileViewer.cloudflareApiTokenScopeHint': 'Pages Edit is required for deploys. Zone Read is required to list domains. DNS Edit is only needed when binding a custom domain.',
|
'fileViewer.cloudflareApiTokenScopeHint': 'Token memerlukan Akun: Cloudflare Pages: Edit ditambah akses baca akun.',
|
||||||
'fileViewer.cloudflareAccountId': 'Account ID',
|
'fileViewer.cloudflareAccountId': 'ID Akun',
|
||||||
'fileViewer.cloudflareAccountIdHint': 'Wajib. Temukan account ID di dashboard Cloudflare.',
|
'fileViewer.cloudflareAccountIdHint': 'Wajib. Temukan ID akun di dasbor Cloudflare.',
|
||||||
'fileViewer.cloudflareAccountIdRequired': 'Masukkan dan simpan Cloudflare Account ID terlebih dahulu.',
|
'fileViewer.cloudflareAccountIdRequired': 'Masukkan dan simpan ID Akun Cloudflare terlebih dahulu.',
|
||||||
'fileViewer.cloudflareZoneLabel': 'Domain',
|
'fileViewer.cloudflareZoneLabel': 'Domain',
|
||||||
'fileViewer.cloudflareZonePlaceholder': 'Save Cloudflare settings to load domains',
|
'fileViewer.cloudflareZonePlaceholder': 'Save Cloudflare settings to load domains',
|
||||||
'fileViewer.cloudflareZoneRequired': 'Select a Cloudflare domain first.',
|
'fileViewer.cloudflareZoneRequired': 'Select a Cloudflare domain first.',
|
||||||
|
|
|
||||||
|
|
@ -493,10 +493,11 @@ export const ja: Dict = {
|
||||||
'designFiles.rowMenu': '行メニュー',
|
'designFiles.rowMenu': '行メニュー',
|
||||||
'designFiles.openInTab': 'タブで開く',
|
'designFiles.openInTab': 'タブで開く',
|
||||||
'designFiles.download': 'ダウンロード',
|
'designFiles.download': 'ダウンロード',
|
||||||
'designFiles.downloadSelected': 'Download {n} as ZIP',
|
'designFiles.downloadSelected': '{n}件をZIPとしてダウンロード',
|
||||||
'designFiles.deleteSelected': '{n} 件を削除',
|
'designFiles.deleteSelected': '{n} 件を削除',
|
||||||
'designFiles.clearSelection': 'Clear',
|
'designFiles.clearSelection': '選択をクリア',
|
||||||
'designFiles.selectAll': 'Select all',
|
'designFiles.selectPage': 'このページをすべて選択',
|
||||||
|
'designFiles.selectAll': 'すべて選択',
|
||||||
'designFiles.dropTitle': '⤓ ファイルをここにドロップ',
|
'designFiles.dropTitle': '⤓ ファイルをここにドロップ',
|
||||||
'designFiles.dropDesc':
|
'designFiles.dropDesc':
|
||||||
'画像、ドキュメント、参考資料、フォルダー — エージェントがコンテキストとして使用します。',
|
'画像、ドキュメント、参考資料、フォルダー — エージェントがコンテキストとして使用します。',
|
||||||
|
|
@ -524,6 +525,15 @@ export const ja: Dict = {
|
||||||
'designFiles.kindPresentation': 'プレゼンテーション',
|
'designFiles.kindPresentation': 'プレゼンテーション',
|
||||||
'designFiles.kindSpreadsheet': 'スプレッドシート',
|
'designFiles.kindSpreadsheet': 'スプレッドシート',
|
||||||
'designFiles.kindBinary': 'バイナリ',
|
'designFiles.kindBinary': 'バイナリ',
|
||||||
|
'designFiles.colName': '名前',
|
||||||
|
'designFiles.colKind': '種類',
|
||||||
|
'designFiles.colModified': '更新日',
|
||||||
|
'designFiles.perPage': '表示',
|
||||||
|
'designFiles.all': 'すべて',
|
||||||
|
'designFiles.prev': '前へ',
|
||||||
|
'designFiles.next': '次へ',
|
||||||
|
'designFiles.jumpToPage': 'ページにジャンプ',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} / {total}',
|
||||||
'quickSwitcher.placeholder': 'ファイルを開く…',
|
'quickSwitcher.placeholder': 'ファイルを開く…',
|
||||||
'quickSwitcher.empty': 'このプロジェクトにファイルがありません',
|
'quickSwitcher.empty': 'このプロジェクトにファイルがありません',
|
||||||
'quickSwitcher.noMatches': '一致なし',
|
'quickSwitcher.noMatches': '一致なし',
|
||||||
|
|
|
||||||
|
|
@ -606,10 +606,11 @@ export const ko: Dict = {
|
||||||
'designFiles.rowMenu': '항목 메뉴',
|
'designFiles.rowMenu': '항목 메뉴',
|
||||||
'designFiles.openInTab': '탭에서 열기',
|
'designFiles.openInTab': '탭에서 열기',
|
||||||
'designFiles.download': '다운로드',
|
'designFiles.download': '다운로드',
|
||||||
'designFiles.downloadSelected': 'Download {n} as ZIP',
|
'designFiles.downloadSelected': '{n}개를 ZIP으로 다운로드',
|
||||||
|
'designFiles.clearSelection': '선택 해제',
|
||||||
|
'designFiles.selectPage': '페이지에서 모두 선택',
|
||||||
|
'designFiles.selectAll': '모두 선택',
|
||||||
'designFiles.deleteSelected': '{n}개 삭제',
|
'designFiles.deleteSelected': '{n}개 삭제',
|
||||||
'designFiles.clearSelection': 'Clear',
|
|
||||||
'designFiles.selectAll': 'Select all',
|
|
||||||
'designFiles.dropTitle': '⤓ 여기에 파일을 놓으세요',
|
'designFiles.dropTitle': '⤓ 여기에 파일을 놓으세요',
|
||||||
'designFiles.dropDesc':
|
'designFiles.dropDesc':
|
||||||
'이미지, 문서, 참조 자료, 폴더 등을 놓아주세요. 에이전트가 문맥으로 활용합니다.',
|
'이미지, 문서, 참조 자료, 폴더 등을 놓아주세요. 에이전트가 문맥으로 활용합니다.',
|
||||||
|
|
@ -637,6 +638,15 @@ export const ko: Dict = {
|
||||||
'designFiles.kindPresentation': '프레젠테이션',
|
'designFiles.kindPresentation': '프레젠테이션',
|
||||||
'designFiles.kindSpreadsheet': '스프레드시트',
|
'designFiles.kindSpreadsheet': '스프레드시트',
|
||||||
'designFiles.kindBinary': '바이너리 파일',
|
'designFiles.kindBinary': '바이너리 파일',
|
||||||
|
'designFiles.colName': '이름',
|
||||||
|
'designFiles.colKind': '종류',
|
||||||
|
'designFiles.colModified': '수정일',
|
||||||
|
'designFiles.perPage': '표시',
|
||||||
|
'designFiles.all': '모두',
|
||||||
|
'designFiles.prev': '이전',
|
||||||
|
'designFiles.next': '다음',
|
||||||
|
'designFiles.jumpToPage': '페이지로 이동',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} / {total}',
|
||||||
'quickSwitcher.placeholder': '파일 열기…',
|
'quickSwitcher.placeholder': '파일 열기…',
|
||||||
'quickSwitcher.empty': '이 프로젝트에 파일이 없습니다',
|
'quickSwitcher.empty': '이 프로젝트에 파일이 없습니다',
|
||||||
'quickSwitcher.noMatches': '일치 항목 없음',
|
'quickSwitcher.noMatches': '일치 항목 없음',
|
||||||
|
|
|
||||||
|
|
@ -606,10 +606,11 @@ export const pl: Dict = {
|
||||||
'designFiles.rowMenu': 'Menu wiersza',
|
'designFiles.rowMenu': 'Menu wiersza',
|
||||||
'designFiles.openInTab': 'Otwórz w karcie',
|
'designFiles.openInTab': 'Otwórz w karcie',
|
||||||
'designFiles.download': 'Pobierz',
|
'designFiles.download': 'Pobierz',
|
||||||
'designFiles.downloadSelected': 'Download {n} as ZIP',
|
'designFiles.downloadSelected': 'Pobierz {n} jako ZIP',
|
||||||
|
'designFiles.clearSelection': 'Wyczyść zaznaczenie',
|
||||||
|
'designFiles.selectPage': 'Zaznacz wszystko na stronie',
|
||||||
|
'designFiles.selectAll': 'Zaznacz wszystko',
|
||||||
'designFiles.deleteSelected': 'Usuń {n}',
|
'designFiles.deleteSelected': 'Usuń {n}',
|
||||||
'designFiles.clearSelection': 'Clear',
|
|
||||||
'designFiles.selectAll': 'Select all',
|
|
||||||
'designFiles.dropTitle': '⤓ Upuść pliki tutaj',
|
'designFiles.dropTitle': '⤓ Upuść pliki tutaj',
|
||||||
'designFiles.dropDesc':
|
'designFiles.dropDesc':
|
||||||
'Obrazy, dokumenty, referencje lub foldery — agent użyje ich jako kontekstu.',
|
'Obrazy, dokumenty, referencje lub foldery — agent użyje ich jako kontekstu.',
|
||||||
|
|
@ -637,6 +638,15 @@ export const pl: Dict = {
|
||||||
'designFiles.kindPresentation': 'Prezentacja',
|
'designFiles.kindPresentation': 'Prezentacja',
|
||||||
'designFiles.kindSpreadsheet': 'Arkusz kalkulacyjny',
|
'designFiles.kindSpreadsheet': 'Arkusz kalkulacyjny',
|
||||||
'designFiles.kindBinary': 'Plik binarny',
|
'designFiles.kindBinary': 'Plik binarny',
|
||||||
|
'designFiles.colName': 'Nazwa',
|
||||||
|
'designFiles.colKind': 'Rodzaj',
|
||||||
|
'designFiles.colModified': 'Zmodyfikowano',
|
||||||
|
'designFiles.perPage': 'Pokaż',
|
||||||
|
'designFiles.all': 'Wszystkie',
|
||||||
|
'designFiles.prev': 'Poprzednia',
|
||||||
|
'designFiles.next': 'Następna',
|
||||||
|
'designFiles.jumpToPage': 'Przejdź do strony',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} z {total}',
|
||||||
'quickSwitcher.placeholder': 'Otwórz plik…',
|
'quickSwitcher.placeholder': 'Otwórz plik…',
|
||||||
'quickSwitcher.empty': 'Brak plików w tym projekcie',
|
'quickSwitcher.empty': 'Brak plików w tym projekcie',
|
||||||
'quickSwitcher.noMatches': 'Brak wyników',
|
'quickSwitcher.noMatches': 'Brak wyników',
|
||||||
|
|
|
||||||
|
|
@ -616,10 +616,11 @@ export const ptBR: Dict = {
|
||||||
'designFiles.rowMenu': 'Menu da linha',
|
'designFiles.rowMenu': 'Menu da linha',
|
||||||
'designFiles.openInTab': 'Abrir em aba',
|
'designFiles.openInTab': 'Abrir em aba',
|
||||||
'designFiles.download': 'Baixar',
|
'designFiles.download': 'Baixar',
|
||||||
'designFiles.downloadSelected': 'Download {n} as ZIP',
|
'designFiles.downloadSelected': 'Baixar {n} selecionados como ZIP',
|
||||||
|
'designFiles.clearSelection': 'Limpar seleção',
|
||||||
|
'designFiles.selectPage': 'Selecionar tudo na página',
|
||||||
|
'designFiles.selectAll': 'Selecionar tudo',
|
||||||
'designFiles.deleteSelected': 'Excluir {n}',
|
'designFiles.deleteSelected': 'Excluir {n}',
|
||||||
'designFiles.clearSelection': 'Clear',
|
|
||||||
'designFiles.selectAll': 'Select all',
|
|
||||||
'designFiles.dropTitle': '⤓ Solte arquivos aqui',
|
'designFiles.dropTitle': '⤓ Solte arquivos aqui',
|
||||||
'designFiles.dropDesc':
|
'designFiles.dropDesc':
|
||||||
'Imagens, docs, referências ou pastas — o agente usará tudo como contexto.',
|
'Imagens, docs, referências ou pastas — o agente usará tudo como contexto.',
|
||||||
|
|
@ -649,6 +650,15 @@ export const ptBR: Dict = {
|
||||||
'designFiles.kindSpreadsheet': 'Planilha',
|
'designFiles.kindSpreadsheet': 'Planilha',
|
||||||
'designFiles.kindLiveArtifact': 'Artefato live',
|
'designFiles.kindLiveArtifact': 'Artefato live',
|
||||||
'designFiles.kindBinary': 'Binário',
|
'designFiles.kindBinary': 'Binário',
|
||||||
|
'designFiles.colName': 'Nome',
|
||||||
|
'designFiles.colKind': 'Tipo',
|
||||||
|
'designFiles.colModified': 'Modificado',
|
||||||
|
'designFiles.perPage': 'Mostrar',
|
||||||
|
'designFiles.all': 'Todos',
|
||||||
|
'designFiles.prev': 'Anterior',
|
||||||
|
'designFiles.next': 'Próximo',
|
||||||
|
'designFiles.jumpToPage': 'Ir para a página',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} de {total}',
|
||||||
'quickSwitcher.placeholder': 'Abrir arquivo…',
|
'quickSwitcher.placeholder': 'Abrir arquivo…',
|
||||||
'quickSwitcher.empty': 'Nenhum arquivo neste projeto',
|
'quickSwitcher.empty': 'Nenhum arquivo neste projeto',
|
||||||
'quickSwitcher.noMatches': 'Sem resultados',
|
'quickSwitcher.noMatches': 'Sem resultados',
|
||||||
|
|
|
||||||
|
|
@ -616,10 +616,11 @@ export const ru: Dict = {
|
||||||
'designFiles.rowMenu': 'Меню строки',
|
'designFiles.rowMenu': 'Меню строки',
|
||||||
'designFiles.openInTab': 'Открыть во вкладке',
|
'designFiles.openInTab': 'Открыть во вкладке',
|
||||||
'designFiles.download': 'Скачать',
|
'designFiles.download': 'Скачать',
|
||||||
'designFiles.downloadSelected': 'Download {n} as ZIP',
|
'designFiles.downloadSelected': 'Скачать {n} как ZIP',
|
||||||
|
'designFiles.clearSelection': 'Очистить выделение',
|
||||||
|
'designFiles.selectPage': 'Выбрать всё на странице',
|
||||||
|
'designFiles.selectAll': 'Выбрать всё',
|
||||||
'designFiles.deleteSelected': 'Удалить {n}',
|
'designFiles.deleteSelected': 'Удалить {n}',
|
||||||
'designFiles.clearSelection': 'Clear',
|
|
||||||
'designFiles.selectAll': 'Select all',
|
|
||||||
'designFiles.dropTitle': '⤓ Перетащите файлы сюда',
|
'designFiles.dropTitle': '⤓ Перетащите файлы сюда',
|
||||||
'designFiles.dropDesc':
|
'designFiles.dropDesc':
|
||||||
'Изображения, документы, референсы или папки — агент будет использовать их как контекст.',
|
'Изображения, документы, референсы или папки — агент будет использовать их как контекст.',
|
||||||
|
|
@ -649,6 +650,15 @@ export const ru: Dict = {
|
||||||
'designFiles.kindSpreadsheet': 'Таблица',
|
'designFiles.kindSpreadsheet': 'Таблица',
|
||||||
'designFiles.kindLiveArtifact': 'Live-артефакт',
|
'designFiles.kindLiveArtifact': 'Live-артефакт',
|
||||||
'designFiles.kindBinary': 'Бинарный',
|
'designFiles.kindBinary': 'Бинарный',
|
||||||
|
'designFiles.colName': 'Имя',
|
||||||
|
'designFiles.colKind': 'Тип',
|
||||||
|
'designFiles.colModified': 'Изменён',
|
||||||
|
'designFiles.perPage': 'Показать',
|
||||||
|
'designFiles.all': 'Все',
|
||||||
|
'designFiles.prev': 'Назад',
|
||||||
|
'designFiles.next': 'Вперёд',
|
||||||
|
'designFiles.jumpToPage': 'Перейти на страницу',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} из {total}',
|
||||||
'quickSwitcher.placeholder': 'Открыть файл…',
|
'quickSwitcher.placeholder': 'Открыть файл…',
|
||||||
'quickSwitcher.empty': 'В проекте нет файлов',
|
'quickSwitcher.empty': 'В проекте нет файлов',
|
||||||
'quickSwitcher.noMatches': 'Нет совпадений',
|
'quickSwitcher.noMatches': 'Нет совпадений',
|
||||||
|
|
|
||||||
|
|
@ -597,10 +597,11 @@ export const tr: Dict = {
|
||||||
'designFiles.rowMenu': 'Sıra menüsü',
|
'designFiles.rowMenu': 'Sıra menüsü',
|
||||||
'designFiles.openInTab': 'Sekmede aç',
|
'designFiles.openInTab': 'Sekmede aç',
|
||||||
'designFiles.download': 'İndir',
|
'designFiles.download': 'İndir',
|
||||||
'designFiles.downloadSelected': 'Download {n} as ZIP',
|
'designFiles.downloadSelected': '{n} dosyayı ZIP olarak indir',
|
||||||
|
'designFiles.clearSelection': 'Seçimi temizle',
|
||||||
|
'designFiles.selectPage': 'Sayfadaki tümünü seç',
|
||||||
|
'designFiles.selectAll': 'Tümünü seç',
|
||||||
'designFiles.deleteSelected': '{n} sil',
|
'designFiles.deleteSelected': '{n} sil',
|
||||||
'designFiles.clearSelection': 'Clear',
|
|
||||||
'designFiles.selectAll': 'Select all',
|
|
||||||
'designFiles.dropTitle': '⤓ Dosyaları buraya sürükleyin',
|
'designFiles.dropTitle': '⤓ Dosyaları buraya sürükleyin',
|
||||||
'designFiles.dropDesc':
|
'designFiles.dropDesc':
|
||||||
'Görseller, dokümanlar, referanslar, veya klasörler — ajan onları bağlam olarak kullanacak.',
|
'Görseller, dokümanlar, referanslar, veya klasörler — ajan onları bağlam olarak kullanacak.',
|
||||||
|
|
@ -628,6 +629,15 @@ export const tr: Dict = {
|
||||||
'designFiles.kindPresentation': 'Sunum',
|
'designFiles.kindPresentation': 'Sunum',
|
||||||
'designFiles.kindSpreadsheet': 'Elektronik tablo',
|
'designFiles.kindSpreadsheet': 'Elektronik tablo',
|
||||||
'designFiles.kindBinary': 'Binary',
|
'designFiles.kindBinary': 'Binary',
|
||||||
|
'designFiles.colName': 'Ad',
|
||||||
|
'designFiles.colKind': 'Tür',
|
||||||
|
'designFiles.colModified': 'Değiştirilme',
|
||||||
|
'designFiles.perPage': 'Göster',
|
||||||
|
'designFiles.all': 'Tümü',
|
||||||
|
'designFiles.prev': 'Önceki',
|
||||||
|
'designFiles.next': 'Sonraki',
|
||||||
|
'designFiles.jumpToPage': 'Sayfaya git',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} / {total}',
|
||||||
'quickSwitcher.placeholder': 'Dosya aç…',
|
'quickSwitcher.placeholder': 'Dosya aç…',
|
||||||
'quickSwitcher.empty': 'Bu projede dosya yok',
|
'quickSwitcher.empty': 'Bu projede dosya yok',
|
||||||
'quickSwitcher.noMatches': 'Eşleşme yok',
|
'quickSwitcher.noMatches': 'Eşleşme yok',
|
||||||
|
|
|
||||||
|
|
@ -617,10 +617,11 @@ export const uk: Dict = {
|
||||||
'designFiles.rowMenu': 'Меню рядка',
|
'designFiles.rowMenu': 'Меню рядка',
|
||||||
'designFiles.openInTab': 'Відкрити на вкладці',
|
'designFiles.openInTab': 'Відкрити на вкладці',
|
||||||
'designFiles.download': 'Завантажити',
|
'designFiles.download': 'Завантажити',
|
||||||
'designFiles.downloadSelected': 'Download {n} as ZIP',
|
'designFiles.downloadSelected': 'Завантажити {n} як ZIP',
|
||||||
|
'designFiles.clearSelection': 'Очистити виділення',
|
||||||
|
'designFiles.selectPage': 'Вибрати все на сторінці',
|
||||||
|
'designFiles.selectAll': 'Вибрати все',
|
||||||
'designFiles.deleteSelected': 'Видалити {n}',
|
'designFiles.deleteSelected': 'Видалити {n}',
|
||||||
'designFiles.clearSelection': 'Clear',
|
|
||||||
'designFiles.selectAll': 'Select all',
|
|
||||||
'designFiles.dropTitle': '⤓ Перенесіть файли сюди',
|
'designFiles.dropTitle': '⤓ Перенесіть файли сюди',
|
||||||
'designFiles.dropDesc':
|
'designFiles.dropDesc':
|
||||||
'Зображення, документи, посилання або папки — агент використовуватиме їх як контекст.',
|
'Зображення, документи, посилання або папки — агент використовуватиме їх як контекст.',
|
||||||
|
|
@ -650,6 +651,15 @@ export const uk: Dict = {
|
||||||
'designFiles.kindPresentation': 'Презентація',
|
'designFiles.kindPresentation': 'Презентація',
|
||||||
'designFiles.kindSpreadsheet': 'Електронна таблиця',
|
'designFiles.kindSpreadsheet': 'Електронна таблиця',
|
||||||
'designFiles.kindBinary': 'Двійковий',
|
'designFiles.kindBinary': 'Двійковий',
|
||||||
|
'designFiles.colName': 'Назва',
|
||||||
|
'designFiles.colKind': 'Тип',
|
||||||
|
'designFiles.colModified': 'Змінено',
|
||||||
|
'designFiles.perPage': 'Показати',
|
||||||
|
'designFiles.all': 'Усі',
|
||||||
|
'designFiles.prev': 'Назад',
|
||||||
|
'designFiles.next': 'Далі',
|
||||||
|
'designFiles.jumpToPage': 'Перейти на сторінку',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} із {total}',
|
||||||
'liveArtifact.refresh.button': 'Оновити',
|
'liveArtifact.refresh.button': 'Оновити',
|
||||||
'liveArtifact.refresh.buttonTitle': 'Оновити live-артефакт',
|
'liveArtifact.refresh.buttonTitle': 'Оновити live-артефакт',
|
||||||
'liveArtifact.refresh.loadingTitle': 'Оновлення live-артефакту…',
|
'liveArtifact.refresh.loadingTitle': 'Оновлення live-артефакту…',
|
||||||
|
|
|
||||||
|
|
@ -610,6 +610,7 @@ export const zhCN: Dict = {
|
||||||
'designFiles.downloadSelected': '下载选中的 {n} 个文件为 ZIP',
|
'designFiles.downloadSelected': '下载选中的 {n} 个文件为 ZIP',
|
||||||
'designFiles.deleteSelected': '删除 {n} 个',
|
'designFiles.deleteSelected': '删除 {n} 个',
|
||||||
'designFiles.clearSelection': '取消选择',
|
'designFiles.clearSelection': '取消选择',
|
||||||
|
'designFiles.selectPage': '全选此页',
|
||||||
'designFiles.selectAll': '全选',
|
'designFiles.selectAll': '全选',
|
||||||
'designFiles.dropTitle': '⤓ 把文件拖到这里',
|
'designFiles.dropTitle': '⤓ 把文件拖到这里',
|
||||||
'designFiles.dropDesc': '图片、文档、参考资料或文件夹 — 智能体都会用作上下文。',
|
'designFiles.dropDesc': '图片、文档、参考资料或文件夹 — 智能体都会用作上下文。',
|
||||||
|
|
@ -639,6 +640,15 @@ export const zhCN: Dict = {
|
||||||
'designFiles.kindSpreadsheet': '电子表格',
|
'designFiles.kindSpreadsheet': '电子表格',
|
||||||
'designFiles.kindLiveArtifact': '实时制品',
|
'designFiles.kindLiveArtifact': '实时制品',
|
||||||
'designFiles.kindBinary': '二进制',
|
'designFiles.kindBinary': '二进制',
|
||||||
|
'designFiles.colName': '名称',
|
||||||
|
'designFiles.colKind': '类型',
|
||||||
|
'designFiles.colModified': '修改时间',
|
||||||
|
'designFiles.perPage': '显示',
|
||||||
|
'designFiles.all': '全部',
|
||||||
|
'designFiles.prev': '上一页',
|
||||||
|
'designFiles.next': '下一页',
|
||||||
|
'designFiles.jumpToPage': '跳转到页面',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} / {total}',
|
||||||
'quickSwitcher.placeholder': '打开文件…',
|
'quickSwitcher.placeholder': '打开文件…',
|
||||||
'quickSwitcher.empty': '此项目中没有文件',
|
'quickSwitcher.empty': '此项目中没有文件',
|
||||||
'quickSwitcher.noMatches': '无匹配项',
|
'quickSwitcher.noMatches': '无匹配项',
|
||||||
|
|
@ -800,19 +810,19 @@ export const zhCN: Dict = {
|
||||||
'fileViewer.vercelToken': 'Vercel token',
|
'fileViewer.vercelToken': 'Vercel token',
|
||||||
'fileViewer.vercelTokenGetLink': '获取 Vercel token',
|
'fileViewer.vercelTokenGetLink': '获取 Vercel token',
|
||||||
'fileViewer.vercelTokenPlaceholder': '粘贴你的 Vercel token',
|
'fileViewer.vercelTokenPlaceholder': '粘贴你的 Vercel token',
|
||||||
'fileViewer.vercelTokenReuseHint': '将使用已保存的 token。输入新 token 可替换。',
|
'fileViewer.vercelTokenReuseHint': '将使用已保存的令牌。输入新令牌可替换。',
|
||||||
'fileViewer.vercelTokenRequired': '请先输入并保存 Vercel token。',
|
'fileViewer.vercelTokenRequired': '请先输入并保存 Vercel 令牌。',
|
||||||
'fileViewer.cloudflareApiToken': 'Cloudflare API token',
|
'fileViewer.cloudflareApiToken': 'Cloudflare API 令牌',
|
||||||
'fileViewer.cloudflareApiTokenGetLink': '获取 Cloudflare API token',
|
'fileViewer.cloudflareApiTokenGetLink': '获取 Cloudflare API 令牌',
|
||||||
'fileViewer.cloudflareApiTokenPlaceholder': '粘贴你的 Cloudflare API token',
|
'fileViewer.cloudflareApiTokenPlaceholder': '粘贴你的 Cloudflare API 令牌',
|
||||||
'fileViewer.cloudflareApiTokenReuseHint': '将使用已保存的 Cloudflare API token。输入新 token 可替换。',
|
'fileViewer.cloudflareApiTokenReuseHint': '将使用已保存的 Cloudflare API 令牌。输入新令牌可替换。',
|
||||||
'fileViewer.cloudflareApiTokenRequired': '请先输入并保存 Cloudflare API token。',
|
'fileViewer.cloudflareApiTokenRequired': '请先输入并保存 Cloudflare API 令牌。',
|
||||||
'fileViewer.cloudflareApiTokenScopeHint': 'Pages Edit 是部署必需权限;列出域名需要 Zone Read;只有绑定自定义域名时才需要 DNS Edit。',
|
'fileViewer.cloudflareApiTokenScopeHint': '令牌需要 Account: Cloudflare Pages: Edit 权限,以及账号读取权限。',
|
||||||
'fileViewer.vercelTeamId': 'Team ID',
|
'fileViewer.vercelTeamId': '团队 ID',
|
||||||
'fileViewer.vercelTeamSlug': 'Team slug',
|
'fileViewer.vercelTeamSlug': '团队标识',
|
||||||
'fileViewer.cloudflareAccountId': 'Account ID',
|
'fileViewer.cloudflareAccountId': '账户 ID',
|
||||||
'fileViewer.cloudflareAccountIdHint': '必填。可在 Cloudflare 控制台中找到账号 ID。',
|
'fileViewer.cloudflareAccountIdHint': '必填。可在 Cloudflare 控制台中找到账户 ID。',
|
||||||
'fileViewer.cloudflareAccountIdRequired': '请先输入并保存 Cloudflare Account ID。',
|
'fileViewer.cloudflareAccountIdRequired': '请先输入并保存 Cloudflare 账户 ID。',
|
||||||
'fileViewer.cloudflareZoneLabel': 'Domain',
|
'fileViewer.cloudflareZoneLabel': 'Domain',
|
||||||
'fileViewer.cloudflareZonePlaceholder': 'Save Cloudflare settings to load domains',
|
'fileViewer.cloudflareZonePlaceholder': 'Save Cloudflare settings to load domains',
|
||||||
'fileViewer.cloudflareZoneRequired': 'Select a Cloudflare domain first.',
|
'fileViewer.cloudflareZoneRequired': 'Select a Cloudflare domain first.',
|
||||||
|
|
|
||||||
|
|
@ -610,6 +610,7 @@ export const zhTW: Dict = {
|
||||||
'designFiles.downloadSelected': '下載選中的 {n} 個檔案為 ZIP',
|
'designFiles.downloadSelected': '下載選中的 {n} 個檔案為 ZIP',
|
||||||
'designFiles.deleteSelected': '刪除 {n} 個',
|
'designFiles.deleteSelected': '刪除 {n} 個',
|
||||||
'designFiles.clearSelection': '取消選擇',
|
'designFiles.clearSelection': '取消選擇',
|
||||||
|
'designFiles.selectPage': '全選此頁',
|
||||||
'designFiles.selectAll': '全選',
|
'designFiles.selectAll': '全選',
|
||||||
'designFiles.dropTitle': '⤓ 把檔案拖到這裡',
|
'designFiles.dropTitle': '⤓ 把檔案拖到這裡',
|
||||||
'designFiles.dropDesc': '圖片、文件、參考資料或資料夾 — 智慧體都會用作上下文。',
|
'designFiles.dropDesc': '圖片、文件、參考資料或資料夾 — 智慧體都會用作上下文。',
|
||||||
|
|
@ -639,6 +640,15 @@ export const zhTW: Dict = {
|
||||||
'designFiles.kindSpreadsheet': '試算表',
|
'designFiles.kindSpreadsheet': '試算表',
|
||||||
'designFiles.kindLiveArtifact': '即時成品',
|
'designFiles.kindLiveArtifact': '即時成品',
|
||||||
'designFiles.kindBinary': '二進位',
|
'designFiles.kindBinary': '二進位',
|
||||||
|
'designFiles.colName': '名稱',
|
||||||
|
'designFiles.colKind': '類型',
|
||||||
|
'designFiles.colModified': '修改時間',
|
||||||
|
'designFiles.perPage': '顯示',
|
||||||
|
'designFiles.all': '全部',
|
||||||
|
'designFiles.prev': '上一頁',
|
||||||
|
'designFiles.next': '下一頁',
|
||||||
|
'designFiles.jumpToPage': '跳轉到頁面',
|
||||||
|
'designFiles.pageInfo': '{start}–{end} / {total}',
|
||||||
'quickSwitcher.placeholder': '開啟檔案…',
|
'quickSwitcher.placeholder': '開啟檔案…',
|
||||||
'quickSwitcher.empty': '此專案中沒有檔案',
|
'quickSwitcher.empty': '此專案中沒有檔案',
|
||||||
'quickSwitcher.noMatches': '無符合項目',
|
'quickSwitcher.noMatches': '無符合項目',
|
||||||
|
|
@ -800,19 +810,19 @@ export const zhTW: Dict = {
|
||||||
'fileViewer.vercelToken': 'Vercel token',
|
'fileViewer.vercelToken': 'Vercel token',
|
||||||
'fileViewer.vercelTokenGetLink': '取得 Vercel token',
|
'fileViewer.vercelTokenGetLink': '取得 Vercel token',
|
||||||
'fileViewer.vercelTokenPlaceholder': '貼上你的 Vercel token',
|
'fileViewer.vercelTokenPlaceholder': '貼上你的 Vercel token',
|
||||||
'fileViewer.vercelTokenReuseHint': '將使用已儲存的 token。輸入新 token 可替換。',
|
'fileViewer.vercelTokenReuseHint': '將使用已儲存的代碼。輸入新代碼可替換。',
|
||||||
'fileViewer.vercelTokenRequired': '請先輸入並儲存 Vercel token。',
|
'fileViewer.vercelTokenRequired': '請先輸入並儲存 Vercel 代碼。',
|
||||||
'fileViewer.cloudflareApiToken': 'Cloudflare API token',
|
'fileViewer.cloudflareApiToken': 'Cloudflare API 代碼',
|
||||||
'fileViewer.cloudflareApiTokenGetLink': '取得 Cloudflare API token',
|
'fileViewer.cloudflareApiTokenGetLink': '取得 Cloudflare API 代碼',
|
||||||
'fileViewer.cloudflareApiTokenPlaceholder': '貼上你的 Cloudflare API token',
|
'fileViewer.cloudflareApiTokenPlaceholder': '貼上你的 Cloudflare API 代碼',
|
||||||
'fileViewer.cloudflareApiTokenReuseHint': '將使用已儲存的 Cloudflare API token。輸入新 token 可替換。',
|
'fileViewer.cloudflareApiTokenReuseHint': '將使用已儲存的 Cloudflare API 代碼。輸入新代碼可替換。',
|
||||||
'fileViewer.cloudflareApiTokenRequired': '請先輸入並儲存 Cloudflare API token。',
|
'fileViewer.cloudflareApiTokenRequired': '請先輸入並儲存 Cloudflare API 代碼。',
|
||||||
'fileViewer.cloudflareApiTokenScopeHint': 'Pages Edit 是部署必需權限;列出網域需要 Zone Read;只有綁定自訂網域時才需要 DNS Edit。',
|
'fileViewer.cloudflareApiTokenScopeHint': '代碼需要 Account: Cloudflare Pages: Edit 權限,以及帳號讀取權限。',
|
||||||
'fileViewer.vercelTeamId': 'Team ID',
|
'fileViewer.vercelTeamId': '團隊 ID',
|
||||||
'fileViewer.vercelTeamSlug': 'Team slug',
|
'fileViewer.vercelTeamSlug': '團隊標記',
|
||||||
'fileViewer.cloudflareAccountId': 'Account ID',
|
'fileViewer.cloudflareAccountId': '帳戶 ID',
|
||||||
'fileViewer.cloudflareAccountIdHint': '必填。可在 Cloudflare 控制台中找到帳號 ID。',
|
'fileViewer.cloudflareAccountIdHint': '必填。可在 Cloudflare 控制台中找到帳戶 ID。',
|
||||||
'fileViewer.cloudflareAccountIdRequired': '請先輸入並儲存 Cloudflare Account ID。',
|
'fileViewer.cloudflareAccountIdRequired': '請先輸入並儲存 Cloudflare 帳戶 ID。',
|
||||||
'fileViewer.cloudflareZoneLabel': 'Domain',
|
'fileViewer.cloudflareZoneLabel': 'Domain',
|
||||||
'fileViewer.cloudflareZonePlaceholder': 'Save Cloudflare settings to load domains',
|
'fileViewer.cloudflareZonePlaceholder': 'Save Cloudflare settings to load domains',
|
||||||
'fileViewer.cloudflareZoneRequired': 'Select a Cloudflare domain first.',
|
'fileViewer.cloudflareZoneRequired': 'Select a Cloudflare domain first.',
|
||||||
|
|
|
||||||
|
|
@ -770,6 +770,7 @@ export interface Dict {
|
||||||
'designFiles.downloadSelected': string;
|
'designFiles.downloadSelected': string;
|
||||||
'designFiles.deleteSelected': string;
|
'designFiles.deleteSelected': string;
|
||||||
'designFiles.clearSelection': string;
|
'designFiles.clearSelection': string;
|
||||||
|
'designFiles.selectPage': string;
|
||||||
'designFiles.selectAll': string;
|
'designFiles.selectAll': string;
|
||||||
'designFiles.dropTitle': string;
|
'designFiles.dropTitle': string;
|
||||||
'designFiles.dropDesc': string;
|
'designFiles.dropDesc': string;
|
||||||
|
|
@ -799,6 +800,15 @@ export interface Dict {
|
||||||
'designFiles.kindSpreadsheet': string;
|
'designFiles.kindSpreadsheet': string;
|
||||||
'designFiles.kindLiveArtifact': string;
|
'designFiles.kindLiveArtifact': string;
|
||||||
'designFiles.kindBinary': string;
|
'designFiles.kindBinary': string;
|
||||||
|
'designFiles.colName': string;
|
||||||
|
'designFiles.colKind': string;
|
||||||
|
'designFiles.colModified': string;
|
||||||
|
'designFiles.perPage': string;
|
||||||
|
'designFiles.all': string;
|
||||||
|
'designFiles.prev': string;
|
||||||
|
'designFiles.next': string;
|
||||||
|
'designFiles.jumpToPage': string;
|
||||||
|
'designFiles.pageInfo': string;
|
||||||
'quickSwitcher.placeholder': string;
|
'quickSwitcher.placeholder': string;
|
||||||
'quickSwitcher.empty': string;
|
'quickSwitcher.empty': string;
|
||||||
'quickSwitcher.noMatches': string;
|
'quickSwitcher.noMatches': string;
|
||||||
|
|
|
||||||
|
|
@ -7037,6 +7037,179 @@ button.connector-action.is-loading {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
/* --- file list table --- */
|
||||||
|
.df-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: auto;
|
||||||
|
}
|
||||||
|
.df-table thead { border-bottom: 1px solid var(--border); }
|
||||||
|
.df-th-btn {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
width: 100%;
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
text-transform: inherit;
|
||||||
|
}
|
||||||
|
.df-th-btn:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.df-th-sortable {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 12px 20px 6px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: color 120ms ease;
|
||||||
|
}
|
||||||
|
.df-th-sortable:hover { color: var(--text); }
|
||||||
|
.df-th-check, .df-cell-check { width: 28px; padding: 10px 0 10px 20px; }
|
||||||
|
.df-th-icon, .df-cell-icon { width: 36px; padding: 10px 0; }
|
||||||
|
.df-th-name { width: auto; }
|
||||||
|
.df-th-kind { width: 110px; }
|
||||||
|
.df-th-time { width: 100px; }
|
||||||
|
.df-th-menu, .df-cell-menu { width: 32px; padding: 10px 20px 10px 0; }
|
||||||
|
.df-sort-arrow { font-size: 10px; }
|
||||||
|
|
||||||
|
.df-file-row {
|
||||||
|
transition: background 120ms ease;
|
||||||
|
}
|
||||||
|
.df-file-row:hover { background: var(--bg-subtle); }
|
||||||
|
.df-file-row.active { background: var(--blue-bg); }
|
||||||
|
.df-file-row.active .df-row-name { color: var(--text-strong); }
|
||||||
|
.df-file-row.selected { background: var(--blue-bg); }
|
||||||
|
|
||||||
|
.df-cell-check {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.df-cell-icon {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.df-cell-name {
|
||||||
|
padding: 10px 12px 10px 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.df-row-name-btn {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.df-row-name-btn:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.df-cell-kind {
|
||||||
|
padding: 10px 12px 10px 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.df-cell-time {
|
||||||
|
padding: 10px 12px 10px 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.df-cell-menu {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.df-kind-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- pagination --- */
|
||||||
|
.df-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.df-pagination-start {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.df-pagination-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.df-pagination-end {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.df-pagination-right {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.df-pagination label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.df-pagination select {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.df-pagination select:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.df-page-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
transition: background 120ms ease, color 120ms ease;
|
||||||
|
}
|
||||||
|
.df-page-btn:hover:not(:disabled) {
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.df-page-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.df-page-btn:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.df-page-info {
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 11.5px;
|
||||||
|
}
|
||||||
|
|
||||||
.df-row {
|
.df-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 28px 36px 1fr auto auto;
|
grid-template-columns: 28px 36px 1fr auto auto;
|
||||||
|
|
@ -7078,16 +7251,20 @@ button.connector-action.is-loading {
|
||||||
.df-row-check:hover { background: var(--border); color: var(--text); }
|
.df-row-check:hover { background: var(--border); color: var(--text); }
|
||||||
.df-row-check[aria-checked="true"] { color: var(--accent-strong); }
|
.df-row-check[aria-checked="true"] { color: var(--accent-strong); }
|
||||||
|
|
||||||
|
.df-select-bar {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
.df-select-all {
|
.df-select-all {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-muted);
|
color: var(--text-faint);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-left: auto;
|
padding: 2px 6px;
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background 120ms ease, color 120ms ease;
|
transition: background 120ms ease, color 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
@ -7145,6 +7322,7 @@ button.connector-action.is-loading {
|
||||||
transition: opacity 120ms ease;
|
transition: opacity 120ms ease;
|
||||||
}
|
}
|
||||||
.df-row:hover .df-row-menu { opacity: 1; }
|
.df-row:hover .df-row-menu { opacity: 1; }
|
||||||
|
.df-row-menu:focus { opacity: 1; }
|
||||||
.df-row-menu:hover { background: var(--border); color: var(--text); }
|
.df-row-menu:hover { background: var(--border); color: var(--text); }
|
||||||
.df-section-more {
|
.df-section-more {
|
||||||
margin: 6px 20px 10px;
|
margin: 6px 20px 10px;
|
||||||
|
|
|
||||||
154
apps/web/tests/components/DesignFilesPanel.test.tsx
Normal file
154
apps/web/tests/components/DesignFilesPanel.test.tsx
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
|
import { fireEvent, render } from '@testing-library/react';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { DesignFilesPanel } from '../../src/components/DesignFilesPanel';
|
||||||
|
import type { ProjectFile, ProjectFileKind } from '../../src/types';
|
||||||
|
|
||||||
|
function extForKind(kind: ProjectFileKind): string {
|
||||||
|
if (kind === 'html') return 'html';
|
||||||
|
if (kind === 'image') return 'png';
|
||||||
|
if (kind === 'sketch') return 'svg';
|
||||||
|
if (kind === 'text') return 'txt';
|
||||||
|
if (kind === 'code') return 'ts';
|
||||||
|
if (kind === 'pdf') return 'pdf';
|
||||||
|
return 'bin';
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateFiles(count: number): ProjectFile[] {
|
||||||
|
const kinds: ProjectFileKind[] = [
|
||||||
|
'html', 'image', 'sketch', 'text', 'code', 'pdf',
|
||||||
|
];
|
||||||
|
return Array.from({ length: count }, (_, i) => {
|
||||||
|
const kind = kinds[i % kinds.length];
|
||||||
|
return {
|
||||||
|
name: `file-${i + 1}.${extForKind(kind!)}`,
|
||||||
|
kind: kind!,
|
||||||
|
size: 1024 * (i + 1),
|
||||||
|
mtime: Date.now() - i * 60_000,
|
||||||
|
mime: 'text/plain',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPanel(files: ProjectFile[]) {
|
||||||
|
return render(
|
||||||
|
<DesignFilesPanel
|
||||||
|
projectId="test-project"
|
||||||
|
files={files}
|
||||||
|
liveArtifacts={[]}
|
||||||
|
onRefreshFiles={vi.fn()}
|
||||||
|
onOpenFile={vi.fn()}
|
||||||
|
onOpenLiveArtifact={vi.fn()}
|
||||||
|
onDeleteFile={vi.fn()}
|
||||||
|
onDeleteFiles={vi.fn()}
|
||||||
|
onUpload={vi.fn()}
|
||||||
|
onUploadFiles={vi.fn()}
|
||||||
|
onPaste={vi.fn()}
|
||||||
|
onNewSketch={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageInfo(container: HTMLElement): string {
|
||||||
|
const el = container.querySelector('.df-page-info');
|
||||||
|
return el?.textContent?.trim() ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** page-btn order: top-Prev=0, top-Next=1, bottom-Prev=2, bottom-Next=3 */
|
||||||
|
function getPageBtns(container: HTMLElement) {
|
||||||
|
return Array.from(container.querySelectorAll<HTMLButtonElement>('.df-page-btn'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelects(container: HTMLElement) {
|
||||||
|
return Array.from(container.querySelectorAll<HTMLSelectElement>('select'));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('DesignFilesPanel large-list regression', () => {
|
||||||
|
it('renders only the default page size (30) rows with 500 files', () => {
|
||||||
|
const files = generateFiles(500);
|
||||||
|
const { container } = renderPanel(files);
|
||||||
|
expect(container.querySelectorAll('.df-file-row').length).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows all 500 rows when page size is set to All', () => {
|
||||||
|
const files = generateFiles(500);
|
||||||
|
const { container } = renderPanel(files);
|
||||||
|
|
||||||
|
const selects = getSelects(container);
|
||||||
|
fireEvent.change(selects[0]!, { target: { value: 'all' } });
|
||||||
|
|
||||||
|
expect(container.querySelectorAll('.df-file-row').length).toBe(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows 60 rows when page size is changed to 60', () => {
|
||||||
|
const files = generateFiles(500);
|
||||||
|
const { container } = renderPanel(files);
|
||||||
|
|
||||||
|
const selects = getSelects(container);
|
||||||
|
fireEvent.change(selects[0]!, { target: { value: '60' } });
|
||||||
|
|
||||||
|
expect(container.querySelectorAll('.df-file-row').length).toBe(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigates pages with Next button and updates row content', () => {
|
||||||
|
const files = generateFiles(500);
|
||||||
|
const { container } = renderPanel(files);
|
||||||
|
|
||||||
|
expect(container.querySelectorAll('.df-file-row').length).toBe(30);
|
||||||
|
expect(container.querySelector('.df-file-row')!.textContent).toContain('file-1');
|
||||||
|
|
||||||
|
const btns = getPageBtns(container);
|
||||||
|
fireEvent.click(btns[1]!);
|
||||||
|
|
||||||
|
expect(container.querySelectorAll('.df-file-row').length).toBe(30);
|
||||||
|
expect(container.querySelector('.df-file-row')!.textContent).toContain('file-31');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows disabled Previous on first page and Next on last page', () => {
|
||||||
|
const files = generateFiles(45);
|
||||||
|
const { container } = renderPanel(files);
|
||||||
|
|
||||||
|
const btns = getPageBtns(container);
|
||||||
|
expect(btns[0]!.disabled).toBe(true);
|
||||||
|
expect(btns[1]!.disabled).toBe(false);
|
||||||
|
|
||||||
|
fireEvent.click(btns[1]!);
|
||||||
|
const btns2 = getPageBtns(container);
|
||||||
|
expect(btns2[0]!.disabled).toBe(false);
|
||||||
|
|
||||||
|
fireEvent.click(getPageBtns(container)[1]!);
|
||||||
|
fireEvent.click(getPageBtns(container)[1]!);
|
||||||
|
expect(getPageBtns(container)[1]!.disabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('jumps to a specific page via page dropdown at bottom', () => {
|
||||||
|
const files = generateFiles(200);
|
||||||
|
const { container } = renderPanel(files);
|
||||||
|
|
||||||
|
const selects = getSelects(container);
|
||||||
|
fireEvent.change(selects[1]!, { target: { value: '3' } });
|
||||||
|
|
||||||
|
expect(container.querySelector('.df-file-row')!.textContent).toContain('file-91');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates page info text when navigating', () => {
|
||||||
|
const files = generateFiles(500);
|
||||||
|
const { container } = renderPanel(files);
|
||||||
|
|
||||||
|
expect(getPageInfo(container)).toContain('1–30 of 500');
|
||||||
|
|
||||||
|
const btns = getPageBtns(container);
|
||||||
|
fireEvent.click(btns[1]!);
|
||||||
|
|
||||||
|
expect(getPageInfo(container)).toContain('31–60 of 500');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders 500 files within a reasonable time', () => {
|
||||||
|
const files = generateFiles(500);
|
||||||
|
const start = performance.now();
|
||||||
|
renderPanel(files);
|
||||||
|
const elapsed = performance.now() - start;
|
||||||
|
expect(elapsed).toBeLessThan(2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1157,12 +1157,13 @@ async function runDesignFilesUploadFlow(
|
||||||
hasText: 'moodboard.png',
|
hasText: 'moodboard.png',
|
||||||
});
|
});
|
||||||
await expect(fileRow).toBeVisible();
|
await expect(fileRow).toBeVisible();
|
||||||
await fileRow.click();
|
const nameBtn = fileRow.getByRole('button').first();
|
||||||
|
await nameBtn.click();
|
||||||
const preview = page.getByTestId('design-file-preview');
|
const preview = page.getByTestId('design-file-preview');
|
||||||
await expect(preview).toBeVisible();
|
await expect(preview).toBeVisible();
|
||||||
await expect(preview.getByText(/moodboard\.png/i)).toBeVisible();
|
await expect(preview.getByText(/moodboard\.png/i)).toBeVisible();
|
||||||
|
|
||||||
await fileRow.dblclick();
|
await nameBtn.dblclick();
|
||||||
await expect(page.getByRole('tab', { name: /moodboard\.png/i })).toBeVisible();
|
await expect(page.getByRole('tab', { name: /moodboard\.png/i })).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue