mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
Improve design files grouping (#1082)
Add a modified-date grouping mode to make busy design workspaces easier to scan as generated files accumulate. The new view keeps existing batch actions and pagination available, adds localized labels, and covers date boundaries with component tests.
This commit is contained in:
parent
bb578b3dca
commit
cbb3c0e33a
21 changed files with 774 additions and 156 deletions
|
|
@ -24,9 +24,32 @@ interface Props {
|
|||
onNewSketch: () => void;
|
||||
}
|
||||
|
||||
type DesignFilesGroupMode = 'kind' | 'modified';
|
||||
type ModifiedSection = 'today' | 'yesterday' | 'previous7Days' | 'previous30Days' | 'older';
|
||||
type SortKey = 'name' | 'kind' | 'mtime';
|
||||
type SortDir = 'asc' | 'desc';
|
||||
|
||||
const MODIFIED_SECTION_ORDER: ModifiedSection[] = [
|
||||
'today',
|
||||
'yesterday',
|
||||
'previous7Days',
|
||||
'previous30Days',
|
||||
'older',
|
||||
];
|
||||
const MODIFIED_SECTION_LABEL_KEY: Record<ModifiedSection, keyof Dict> = {
|
||||
today: 'designFiles.modifiedToday',
|
||||
yesterday: 'designFiles.modifiedYesterday',
|
||||
previous7Days: 'designFiles.modifiedPrevious7Days',
|
||||
previous30Days: 'designFiles.modifiedPrevious30Days',
|
||||
older: 'designFiles.modifiedOlder',
|
||||
};
|
||||
|
||||
/**
|
||||
* 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({
|
||||
projectId,
|
||||
files,
|
||||
|
|
@ -56,7 +79,12 @@ export function DesignFilesPanel({
|
|||
const [sortDir, setSortDir] = useState<SortDir>('desc');
|
||||
const lastKeyPress = useRef<Map<string, number>>(new Map());
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [groupMode, setGroupMode] = useState<DesignFilesGroupMode>('kind');
|
||||
const [collapsedModifiedSections, setCollapsedModifiedSections] = useState<
|
||||
Set<ModifiedSection>
|
||||
>(new Set());
|
||||
const [renaming, setRenaming] = useState<{ name: string; draft: string; saving: boolean } | null>(null);
|
||||
const [dayBoundary, setDayBoundary] = useState(() => Date.now());
|
||||
|
||||
const sortedFiles = useMemo(() => {
|
||||
return [...files].sort((a, b) => {
|
||||
|
|
@ -74,7 +102,31 @@ export function DesignFilesPanel({
|
|||
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 pageFiles = useMemo(
|
||||
() =>
|
||||
sortedFiles.slice(
|
||||
safePage * effectivePageSize,
|
||||
(safePage + 1) * effectivePageSize,
|
||||
),
|
||||
[effectivePageSize, safePage, sortedFiles],
|
||||
);
|
||||
const modifiedGroups = useMemo(() => {
|
||||
const groups: Record<ModifiedSection, ProjectFile[]> = {
|
||||
today: [],
|
||||
yesterday: [],
|
||||
previous7Days: [],
|
||||
previous30Days: [],
|
||||
older: [],
|
||||
};
|
||||
const thresholds = modifiedSectionThresholds(dayBoundary);
|
||||
for (const f of pageFiles) {
|
||||
groups[modifiedSectionFor(f.mtime, thresholds)].push(f);
|
||||
}
|
||||
return groups;
|
||||
}, [dayBoundary, pageFiles]);
|
||||
const visibleModifiedSections = MODIFIED_SECTION_ORDER.filter(
|
||||
(section) => modifiedGroups[section].length > 0,
|
||||
);
|
||||
const rangeStart = safePage * effectivePageSize + 1;
|
||||
const rangeEnd = Math.min((safePage + 1) * effectivePageSize, sortedFiles.length);
|
||||
const allPageSelected = pageFiles.every((f) => selected.has(f.name));
|
||||
|
|
@ -88,6 +140,21 @@ export function DesignFilesPanel({
|
|||
if (Number.isFinite(totalPages)) setPage((p) => Math.min(p, totalPages - 1));
|
||||
}, [totalPages]);
|
||||
|
||||
useEffect(() => {
|
||||
const now = Date.now();
|
||||
const startOfTomorrow = new Date(now);
|
||||
startOfTomorrow.setHours(24, 0, 0, 0);
|
||||
const timer = window.setTimeout(
|
||||
() => setDayBoundary(Date.now()),
|
||||
Math.max(1, startOfTomorrow.getTime() - now),
|
||||
);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [dayBoundary]);
|
||||
|
||||
// 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(() => {
|
||||
setSelected((prev) => {
|
||||
if (prev.size === 0) return prev;
|
||||
|
|
@ -247,6 +314,188 @@ export function DesignFilesPanel({
|
|||
}
|
||||
}
|
||||
|
||||
function toggleModifiedSection(section: ModifiedSection) {
|
||||
setCollapsedModifiedSections((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(section)) {
|
||||
next.delete(section);
|
||||
} else {
|
||||
next.add(section);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function renderFileRow(f: ProjectFile) {
|
||||
const active = preview === f.name;
|
||||
const isHovered = hover === f.name;
|
||||
const renameState = renaming?.name === f.name ? renaming : null;
|
||||
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 df-cell-openable"
|
||||
onClick={() => setPreview(f.name)}
|
||||
onDoubleClick={() => onOpenFile(f.name)}
|
||||
>
|
||||
<span className="df-row-icon" data-kind={f.kind} aria-hidden>
|
||||
{kindGlyph(f.kind)}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className="df-cell-name df-cell-openable"
|
||||
onClick={() => {
|
||||
if (!renameState) setPreview(f.name);
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
if (!renameState) onOpenFile(f.name);
|
||||
}}
|
||||
>
|
||||
{renameState ? (
|
||||
<input
|
||||
autoFocus
|
||||
className="df-rename-input"
|
||||
value={renameState.draft}
|
||||
disabled={renameState.saving}
|
||||
onChange={(e) => setRenaming({ ...renameState, draft: e.target.value })}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onDoubleClick={(e) => e.stopPropagation()}
|
||||
onBlur={(e) => {
|
||||
if (e.currentTarget.dataset.skipRenameCommit === '1') return;
|
||||
void commitRename(f.name, renameState.draft);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
e.currentTarget.dataset.skipRenameCommit = '1';
|
||||
void commitRename(f.name, renameState.draft);
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
e.currentTarget.dataset.skipRenameCommit = '1';
|
||||
setRenaming(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="df-row-name-btn"
|
||||
onClick={() => setPreview(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 df-cell-openable"
|
||||
onClick={() => setPreview(f.name)}
|
||||
onDoubleClick={() => onOpenFile(f.name)}
|
||||
>
|
||||
<span className="df-kind-label">{kindLabel(f.kind, t)}</span>
|
||||
</td>
|
||||
<td
|
||||
className="df-cell-time df-cell-openable"
|
||||
onClick={() => setPreview(f.name)}
|
||||
onDoubleClick={() => onOpenFile(f.name)}
|
||||
>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
function renderModifiedSections() {
|
||||
return visibleModifiedSections.flatMap((section) => {
|
||||
const sectionFiles = modifiedGroups[section];
|
||||
const collapsed = collapsedModifiedSections.has(section);
|
||||
const label = t(MODIFIED_SECTION_LABEL_KEY[section]);
|
||||
return [
|
||||
<tr className="df-section-row" key={`${section}-label`}>
|
||||
<td colSpan={6}>
|
||||
<button
|
||||
type="button"
|
||||
className="df-section-toggle"
|
||||
aria-expanded={!collapsed}
|
||||
aria-label={`${collapsed ? t('designFiles.expandGroup') : t('designFiles.collapseGroup')} ${label}`}
|
||||
onClick={() => toggleModifiedSection(section)}
|
||||
>
|
||||
<Icon name={collapsed ? 'chevron-right' : 'chevron-down'} size={13} />
|
||||
<span>{label}</span>
|
||||
<span className="df-section-count">{sectionFiles.length}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>,
|
||||
...(collapsed ? [] : sectionFiles.map(renderFileRow)),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
async function handleBatchDownload() {
|
||||
const fileList = [...selected];
|
||||
if (fileList.length === 0) return;
|
||||
|
|
@ -355,6 +604,31 @@ export function DesignFilesPanel({
|
|||
<div className="df-empty">{t('designFiles.empty')}</div>
|
||||
) : (
|
||||
<>
|
||||
{files.length > 0 ? (
|
||||
<div
|
||||
className="df-group-toggle"
|
||||
role="group"
|
||||
aria-label={t('designFiles.groupBy')}
|
||||
>
|
||||
<span>{t('designFiles.groupBy')}</span>
|
||||
<button
|
||||
type="button"
|
||||
className={groupMode === 'kind' ? 'active' : ''}
|
||||
aria-pressed={groupMode === 'kind'}
|
||||
onClick={() => setGroupMode('kind')}
|
||||
>
|
||||
{t('designFiles.groupByKind')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={groupMode === 'modified' ? 'active' : ''}
|
||||
aria-pressed={groupMode === 'modified'}
|
||||
onClick={() => setGroupMode('modified')}
|
||||
>
|
||||
{t('designFiles.groupByModified')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{liveArtifacts.length > 0 ? (
|
||||
<div className="df-section" key="live-artifacts">
|
||||
<div className="df-section-label">{t('designFiles.sectionLiveArtifacts')}</div>
|
||||
|
|
@ -499,151 +773,9 @@ export function DesignFilesPanel({
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{pageFiles.map((f) => {
|
||||
const active = preview === f.name;
|
||||
const isHovered = hover === f.name;
|
||||
const renameState = renaming?.name === f.name ? renaming : null;
|
||||
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 df-cell-openable"
|
||||
onClick={() => setPreview(f.name)}
|
||||
onDoubleClick={() => onOpenFile(f.name)}
|
||||
>
|
||||
<span className="df-row-icon" data-kind={f.kind} aria-hidden>
|
||||
{kindGlyph(f.kind)}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className="df-cell-name df-cell-openable"
|
||||
onClick={() => {
|
||||
if (!renameState) setPreview(f.name);
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
if (!renameState) onOpenFile(f.name);
|
||||
}}
|
||||
>
|
||||
{renameState ? (
|
||||
<input
|
||||
autoFocus
|
||||
className="df-rename-input"
|
||||
value={renameState.draft}
|
||||
disabled={renameState.saving}
|
||||
onChange={(e) =>
|
||||
setRenaming({ ...renameState, draft: e.target.value })
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onDoubleClick={(e) => e.stopPropagation()}
|
||||
onBlur={(e) => {
|
||||
if (e.currentTarget.dataset.skipRenameCommit === '1') return;
|
||||
void commitRename(f.name, renameState.draft);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
e.currentTarget.dataset.skipRenameCommit = '1';
|
||||
void commitRename(f.name, renameState.draft);
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
e.currentTarget.dataset.skipRenameCommit = '1';
|
||||
setRenaming(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="df-row-name-btn"
|
||||
onClick={() => setPreview(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 df-cell-openable"
|
||||
onClick={() => setPreview(f.name)}
|
||||
onDoubleClick={() => onOpenFile(f.name)}
|
||||
>
|
||||
<span className="df-kind-label">{kindLabel(f.kind, t)}</span>
|
||||
</td>
|
||||
<td
|
||||
className="df-cell-time df-cell-openable"
|
||||
onClick={() => setPreview(f.name)}
|
||||
onDoubleClick={() => onOpenFile(f.name)}
|
||||
>
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
{groupMode === 'modified'
|
||||
? renderModifiedSections()
|
||||
: pageFiles.map(renderFileRow)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="df-pagination df-pagination-center">
|
||||
|
|
@ -878,6 +1010,39 @@ function kindSortPriority(kind: ProjectFileKind): number {
|
|||
return 11;
|
||||
}
|
||||
|
||||
interface ModifiedSectionThresholds {
|
||||
todayStart: number;
|
||||
yesterdayStart: number;
|
||||
previous7DaysStart: number;
|
||||
previous30DaysStart: number;
|
||||
}
|
||||
|
||||
function modifiedSectionThresholds(now: number): ModifiedSectionThresholds {
|
||||
const startOfToday = new Date(now);
|
||||
startOfToday.setHours(0, 0, 0, 0);
|
||||
return {
|
||||
todayStart: startOfToday.getTime(),
|
||||
yesterdayStart: dateDaysBefore(startOfToday, 1).getTime(),
|
||||
previous7DaysStart: dateDaysBefore(startOfToday, 7).getTime(),
|
||||
previous30DaysStart: dateDaysBefore(startOfToday, 30).getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
function modifiedSectionFor(ts: number, thresholds: ModifiedSectionThresholds): ModifiedSection {
|
||||
const { todayStart, yesterdayStart, previous7DaysStart, previous30DaysStart } = thresholds;
|
||||
if (ts >= todayStart) return 'today';
|
||||
if (ts >= yesterdayStart) return 'yesterday';
|
||||
if (ts >= previous7DaysStart) return 'previous7Days';
|
||||
if (ts >= previous30DaysStart) return 'previous30Days';
|
||||
return 'older';
|
||||
}
|
||||
|
||||
function dateDaysBefore(date: Date, days: number): Date {
|
||||
const result = new Date(date);
|
||||
result.setDate(result.getDate() - days);
|
||||
return result;
|
||||
}
|
||||
|
||||
function kindGlyph(kind: ProjectFileKind): string {
|
||||
if (kind === 'html') return '\u27E8\u27E9';
|
||||
if (kind === 'image') return '\u25A3';
|
||||
|
|
|
|||
|
|
@ -656,11 +656,21 @@ export const ar: Dict = {
|
|||
'designFiles.previewClose': 'إغلاق المعاينة',
|
||||
'designFiles.modified': 'تم التعديل {time} · {size}',
|
||||
'designFiles.weeksAgo': 'منذ {n} أسبوع',
|
||||
'designFiles.groupBy': 'التجميع حسب',
|
||||
'designFiles.groupByKind': 'النوع',
|
||||
'designFiles.groupByModified': 'تاريخ التعديل',
|
||||
'designFiles.expandGroup': 'توسيع',
|
||||
'designFiles.collapseGroup': 'طي',
|
||||
'designFiles.sectionPages': 'صفحات',
|
||||
'designFiles.sectionScripts': 'سكربتات',
|
||||
'designFiles.sectionImages': 'صور',
|
||||
'designFiles.sectionSketches': 'رسومات',
|
||||
'designFiles.sectionOther': 'أخرى',
|
||||
'designFiles.modifiedToday': 'اليوم',
|
||||
'designFiles.modifiedYesterday': 'أمس',
|
||||
'designFiles.modifiedPrevious7Days': 'آخر 7 أيام',
|
||||
'designFiles.modifiedPrevious30Days': 'آخر 30 يومًا',
|
||||
'designFiles.modifiedOlder': 'أقدم',
|
||||
'designFiles.showMore': 'عرض +{n} أخرى',
|
||||
'designFiles.kindHtml': 'صفحة HTML',
|
||||
'designFiles.kindImage': 'صورة',
|
||||
|
|
|
|||
|
|
@ -544,11 +544,21 @@ export const de: Dict = {
|
|||
'designFiles.previewClose': 'Vorschau schließen',
|
||||
'designFiles.modified': 'Geändert {time} · {size}',
|
||||
'designFiles.weeksAgo': 'vor {n} W.',
|
||||
'designFiles.groupBy': 'Gruppieren nach',
|
||||
'designFiles.groupByKind': 'Typ',
|
||||
'designFiles.groupByModified': 'Geändert',
|
||||
'designFiles.expandGroup': 'Erweitern',
|
||||
'designFiles.collapseGroup': 'Einklappen',
|
||||
'designFiles.sectionPages': 'Seiten',
|
||||
'designFiles.sectionScripts': 'Skripte',
|
||||
'designFiles.sectionImages': 'Bilder',
|
||||
'designFiles.sectionSketches': 'Sketches',
|
||||
'designFiles.sectionOther': 'Andere',
|
||||
'designFiles.modifiedToday': 'Heute',
|
||||
'designFiles.modifiedYesterday': 'Gestern',
|
||||
'designFiles.modifiedPrevious7Days': 'Letzte 7 Tage',
|
||||
'designFiles.modifiedPrevious30Days': 'Letzte 30 Tage',
|
||||
'designFiles.modifiedOlder': 'Älter',
|
||||
'designFiles.showMore': '+{n} weitere anzeigen',
|
||||
'designFiles.kindHtml': 'HTML-Seite',
|
||||
'designFiles.kindImage': 'Bild',
|
||||
|
|
|
|||
|
|
@ -723,12 +723,22 @@ export const en: Dict = {
|
|||
'designFiles.previewClose': 'Close preview',
|
||||
'designFiles.modified': 'Modified {time} · {size}',
|
||||
'designFiles.weeksAgo': '{n}w ago',
|
||||
'designFiles.groupBy': 'Group by',
|
||||
'designFiles.groupByKind': 'Kind',
|
||||
'designFiles.groupByModified': 'Modified',
|
||||
'designFiles.expandGroup': 'Expand',
|
||||
'designFiles.collapseGroup': 'Collapse',
|
||||
'designFiles.sectionPages': 'Pages',
|
||||
'designFiles.sectionScripts': 'Scripts',
|
||||
'designFiles.sectionImages': 'Images',
|
||||
'designFiles.sectionSketches': 'Sketches',
|
||||
'designFiles.sectionLiveArtifacts': 'Live artifacts',
|
||||
'designFiles.sectionOther': 'Other',
|
||||
'designFiles.modifiedToday': 'Today',
|
||||
'designFiles.modifiedYesterday': 'Yesterday',
|
||||
'designFiles.modifiedPrevious7Days': 'Previous 7 days',
|
||||
'designFiles.modifiedPrevious30Days': 'Previous 30 days',
|
||||
'designFiles.modifiedOlder': 'Older',
|
||||
'designFiles.showMore': 'Show +{n} more',
|
||||
'designFiles.kindHtml': 'HTML page',
|
||||
'designFiles.kindImage': 'Image',
|
||||
|
|
|
|||
|
|
@ -545,11 +545,21 @@ export const esES: Dict = {
|
|||
'designFiles.previewClose': 'Cerrar vista previa',
|
||||
'designFiles.modified': 'Modificado {time} · {size}',
|
||||
'designFiles.weeksAgo': 'hace {n} sem',
|
||||
'designFiles.groupBy': 'Agrupar por',
|
||||
'designFiles.groupByKind': 'Tipo',
|
||||
'designFiles.groupByModified': 'Modificado',
|
||||
'designFiles.expandGroup': 'Expandir',
|
||||
'designFiles.collapseGroup': 'Contraer',
|
||||
'designFiles.sectionPages': 'Páginas',
|
||||
'designFiles.sectionScripts': 'Scripts',
|
||||
'designFiles.sectionImages': 'Imágenes',
|
||||
'designFiles.sectionSketches': 'Bocetos',
|
||||
'designFiles.sectionOther': 'Otros',
|
||||
'designFiles.modifiedToday': 'Hoy',
|
||||
'designFiles.modifiedYesterday': 'Ayer',
|
||||
'designFiles.modifiedPrevious7Days': 'Últimos 7 días',
|
||||
'designFiles.modifiedPrevious30Days': 'Últimos 30 días',
|
||||
'designFiles.modifiedOlder': 'Anterior',
|
||||
'designFiles.showMore': 'Mostrar +{n} más',
|
||||
'designFiles.kindHtml': 'Página HTML',
|
||||
'designFiles.kindImage': 'Imagen',
|
||||
|
|
|
|||
|
|
@ -669,12 +669,22 @@ export const fa: Dict = {
|
|||
'designFiles.previewClose': 'بستن پیشنمایش',
|
||||
'designFiles.modified': 'ویرایش شده {time} · {size}',
|
||||
'designFiles.weeksAgo': '{n} هفته پیش',
|
||||
'designFiles.groupBy': 'گروهبندی بر اساس',
|
||||
'designFiles.groupByKind': 'نوع',
|
||||
'designFiles.groupByModified': 'زمان ویرایش',
|
||||
'designFiles.expandGroup': 'باز کردن',
|
||||
'designFiles.collapseGroup': 'جمع کردن',
|
||||
'designFiles.sectionPages': 'صفحات',
|
||||
'designFiles.sectionScripts': 'اسکریپتها',
|
||||
'designFiles.sectionImages': 'تصاویر',
|
||||
'designFiles.sectionSketches': 'طرحها',
|
||||
'designFiles.sectionLiveArtifacts': 'مصنوعات زنده',
|
||||
'designFiles.sectionOther': 'سایر',
|
||||
'designFiles.modifiedToday': 'امروز',
|
||||
'designFiles.modifiedYesterday': 'دیروز',
|
||||
'designFiles.modifiedPrevious7Days': '۷ روز گذشته',
|
||||
'designFiles.modifiedPrevious30Days': '۳۰ روز گذشته',
|
||||
'designFiles.modifiedOlder': 'قدیمیتر',
|
||||
'designFiles.showMore': '+{n} بیشتر',
|
||||
'designFiles.kindHtml': 'صفحه HTML',
|
||||
'designFiles.kindImage': 'تصویر',
|
||||
|
|
|
|||
|
|
@ -656,11 +656,21 @@ export const fr: Dict = {
|
|||
'designFiles.previewClose': 'Fermer l\'aperçu',
|
||||
'designFiles.modified': 'Modifié {time} · {size}',
|
||||
'designFiles.weeksAgo': 'il y a {n} sem',
|
||||
'designFiles.groupBy': 'Grouper par',
|
||||
'designFiles.groupByKind': 'Type',
|
||||
'designFiles.groupByModified': 'Modification',
|
||||
'designFiles.expandGroup': 'Développer',
|
||||
'designFiles.collapseGroup': 'Réduire',
|
||||
'designFiles.sectionPages': 'Pages',
|
||||
'designFiles.sectionScripts': 'Scripts',
|
||||
'designFiles.sectionImages': 'Images',
|
||||
'designFiles.sectionSketches': 'Croquis',
|
||||
'designFiles.sectionOther': 'Autre',
|
||||
'designFiles.modifiedToday': 'Aujourd’hui',
|
||||
'designFiles.modifiedYesterday': 'Hier',
|
||||
'designFiles.modifiedPrevious7Days': '7 derniers jours',
|
||||
'designFiles.modifiedPrevious30Days': '30 derniers jours',
|
||||
'designFiles.modifiedOlder': 'Plus ancien',
|
||||
'designFiles.showMore': 'Afficher +{n} de plus',
|
||||
'designFiles.kindHtml': 'Page HTML',
|
||||
'designFiles.kindImage': 'Image',
|
||||
|
|
|
|||
|
|
@ -656,11 +656,21 @@ export const hu: Dict = {
|
|||
'designFiles.previewClose': 'Előnézet bezárása',
|
||||
'designFiles.modified': 'Módosítva: {time} · {size}',
|
||||
'designFiles.weeksAgo': '{n} hete',
|
||||
'designFiles.groupBy': 'Csoportosítás',
|
||||
'designFiles.groupByKind': 'Típus',
|
||||
'designFiles.groupByModified': 'Módosítás',
|
||||
'designFiles.expandGroup': 'Kibontás',
|
||||
'designFiles.collapseGroup': 'Összecsukás',
|
||||
'designFiles.sectionPages': 'Oldalak',
|
||||
'designFiles.sectionScripts': 'Szkriptek',
|
||||
'designFiles.sectionImages': 'Képek',
|
||||
'designFiles.sectionSketches': 'Vázlatok',
|
||||
'designFiles.sectionOther': 'Egyéb',
|
||||
'designFiles.modifiedToday': 'Ma',
|
||||
'designFiles.modifiedYesterday': 'Tegnap',
|
||||
'designFiles.modifiedPrevious7Days': 'Előző 7 nap',
|
||||
'designFiles.modifiedPrevious30Days': 'Előző 30 nap',
|
||||
'designFiles.modifiedOlder': 'Régebbi',
|
||||
'designFiles.showMore': '+{n} további megjelenítése',
|
||||
'designFiles.kindHtml': 'HTML oldal',
|
||||
'designFiles.kindImage': 'Kép',
|
||||
|
|
|
|||
|
|
@ -756,12 +756,22 @@ export const id: Dict = {
|
|||
'designFiles.previewClose': 'Tutup pratinjau',
|
||||
'designFiles.modified': 'Diubah {time} - {size}',
|
||||
'designFiles.weeksAgo': '{n} minggu lalu',
|
||||
'designFiles.groupBy': 'Kelompokkan menurut',
|
||||
'designFiles.groupByKind': 'Jenis',
|
||||
'designFiles.groupByModified': 'Diubah',
|
||||
'designFiles.expandGroup': 'Perluas',
|
||||
'designFiles.collapseGroup': 'Ciutkan',
|
||||
'designFiles.sectionPages': 'Halaman',
|
||||
'designFiles.sectionScripts': 'Skrip',
|
||||
'designFiles.sectionImages': 'Gambar',
|
||||
'designFiles.sectionSketches': 'Sketsa',
|
||||
'designFiles.sectionLiveArtifacts': 'Live artifact',
|
||||
'designFiles.sectionOther': 'Lainnya',
|
||||
'designFiles.modifiedToday': 'Hari ini',
|
||||
'designFiles.modifiedYesterday': 'Kemarin',
|
||||
'designFiles.modifiedPrevious7Days': '7 hari sebelumnya',
|
||||
'designFiles.modifiedPrevious30Days': '30 hari sebelumnya',
|
||||
'designFiles.modifiedOlder': 'Lebih lama',
|
||||
'designFiles.showMore': 'Tampilkan +{n} lagi',
|
||||
'designFiles.kindHtml': 'Halaman HTML',
|
||||
'designFiles.kindImage': 'Gambar',
|
||||
|
|
|
|||
|
|
@ -543,11 +543,21 @@ export const ja: Dict = {
|
|||
'designFiles.previewClose': 'プレビューを閉じる',
|
||||
'designFiles.modified': '{time} に変更 · {size}',
|
||||
'designFiles.weeksAgo': '{n}週間前',
|
||||
'designFiles.groupBy': 'グループ化',
|
||||
'designFiles.groupByKind': '種類',
|
||||
'designFiles.groupByModified': '更新日時',
|
||||
'designFiles.expandGroup': '展開',
|
||||
'designFiles.collapseGroup': '折りたたむ',
|
||||
'designFiles.sectionPages': 'ページ',
|
||||
'designFiles.sectionScripts': 'スクリプト',
|
||||
'designFiles.sectionImages': '画像',
|
||||
'designFiles.sectionSketches': 'スケッチ',
|
||||
'designFiles.sectionOther': 'その他',
|
||||
'designFiles.modifiedToday': '今日',
|
||||
'designFiles.modifiedYesterday': '昨日',
|
||||
'designFiles.modifiedPrevious7Days': '過去7日間',
|
||||
'designFiles.modifiedPrevious30Days': '過去30日間',
|
||||
'designFiles.modifiedOlder': 'それ以前',
|
||||
'designFiles.showMore': 'さらに {n} 件表示',
|
||||
'designFiles.kindHtml': 'HTML ページ',
|
||||
'designFiles.kindImage': '画像',
|
||||
|
|
|
|||
|
|
@ -656,11 +656,21 @@ export const ko: Dict = {
|
|||
'designFiles.previewClose': '미리보기 닫기',
|
||||
'designFiles.modified': '{time} 수정됨 · {size}',
|
||||
'designFiles.weeksAgo': '{n}주 전',
|
||||
'designFiles.groupBy': '그룹 기준',
|
||||
'designFiles.groupByKind': '종류',
|
||||
'designFiles.groupByModified': '수정일',
|
||||
'designFiles.expandGroup': '펼치기',
|
||||
'designFiles.collapseGroup': '접기',
|
||||
'designFiles.sectionPages': '페이지',
|
||||
'designFiles.sectionScripts': '스크립트',
|
||||
'designFiles.sectionImages': '이미지',
|
||||
'designFiles.sectionSketches': '스케치',
|
||||
'designFiles.sectionOther': '기타',
|
||||
'designFiles.modifiedToday': '오늘',
|
||||
'designFiles.modifiedYesterday': '어제',
|
||||
'designFiles.modifiedPrevious7Days': '지난 7일',
|
||||
'designFiles.modifiedPrevious30Days': '지난 30일',
|
||||
'designFiles.modifiedOlder': '이전',
|
||||
'designFiles.showMore': '+{n}개 더 보기',
|
||||
'designFiles.kindHtml': 'HTML 페이지',
|
||||
'designFiles.kindImage': '이미지',
|
||||
|
|
|
|||
|
|
@ -656,11 +656,21 @@ export const pl: Dict = {
|
|||
'designFiles.previewClose': 'Zamknij podgląd',
|
||||
'designFiles.modified': 'Zmodyfikowano {time} · {size}',
|
||||
'designFiles.weeksAgo': '{n} tyg. temu',
|
||||
'designFiles.groupBy': 'Grupuj według',
|
||||
'designFiles.groupByKind': 'Typ',
|
||||
'designFiles.groupByModified': 'Modyfikacja',
|
||||
'designFiles.expandGroup': 'Rozwiń',
|
||||
'designFiles.collapseGroup': 'Zwiń',
|
||||
'designFiles.sectionPages': 'Strony',
|
||||
'designFiles.sectionScripts': 'Skrypty',
|
||||
'designFiles.sectionImages': 'Obrazy',
|
||||
'designFiles.sectionSketches': 'Szkice',
|
||||
'designFiles.sectionOther': 'Inne',
|
||||
'designFiles.modifiedToday': 'Dzisiaj',
|
||||
'designFiles.modifiedYesterday': 'Wczoraj',
|
||||
'designFiles.modifiedPrevious7Days': 'Ostatnie 7 dni',
|
||||
'designFiles.modifiedPrevious30Days': 'Ostatnie 30 dni',
|
||||
'designFiles.modifiedOlder': 'Starsze',
|
||||
'designFiles.showMore': 'Pokaż +{n} więcej',
|
||||
'designFiles.kindHtml': 'Strona HTML',
|
||||
'designFiles.kindImage': 'Obraz',
|
||||
|
|
|
|||
|
|
@ -668,12 +668,22 @@ export const ptBR: Dict = {
|
|||
'designFiles.previewClose': 'Fechar prévia',
|
||||
'designFiles.modified': 'Modificado {time} · {size}',
|
||||
'designFiles.weeksAgo': 'há {n} sem',
|
||||
'designFiles.groupBy': 'Agrupar por',
|
||||
'designFiles.groupByKind': 'Tipo',
|
||||
'designFiles.groupByModified': 'Modificado',
|
||||
'designFiles.expandGroup': 'Expandir',
|
||||
'designFiles.collapseGroup': 'Recolher',
|
||||
'designFiles.sectionPages': 'Páginas',
|
||||
'designFiles.sectionScripts': 'Scripts',
|
||||
'designFiles.sectionImages': 'Imagens',
|
||||
'designFiles.sectionSketches': 'Esboços',
|
||||
'designFiles.sectionLiveArtifacts': 'Artefatos live',
|
||||
'designFiles.sectionOther': 'Outros',
|
||||
'designFiles.modifiedToday': 'Hoje',
|
||||
'designFiles.modifiedYesterday': 'Ontem',
|
||||
'designFiles.modifiedPrevious7Days': 'Últimos 7 dias',
|
||||
'designFiles.modifiedPrevious30Days': 'Últimos 30 dias',
|
||||
'designFiles.modifiedOlder': 'Mais antigos',
|
||||
'designFiles.showMore': 'Mostrar +{n} mais',
|
||||
'designFiles.kindHtml': 'Página HTML',
|
||||
'designFiles.kindImage': 'Imagem',
|
||||
|
|
|
|||
|
|
@ -668,12 +668,22 @@ export const ru: Dict = {
|
|||
'designFiles.previewClose': 'Закрыть предпросмотр',
|
||||
'designFiles.modified': 'Изменено {time} · {size}',
|
||||
'designFiles.weeksAgo': '{n} нед. назад',
|
||||
'designFiles.groupBy': 'Группировать по',
|
||||
'designFiles.groupByKind': 'Тип',
|
||||
'designFiles.groupByModified': 'Изменено',
|
||||
'designFiles.expandGroup': 'Развернуть',
|
||||
'designFiles.collapseGroup': 'Свернуть',
|
||||
'designFiles.sectionPages': 'Страницы',
|
||||
'designFiles.sectionScripts': 'Скрипты',
|
||||
'designFiles.sectionImages': 'Изображения',
|
||||
'designFiles.sectionSketches': 'Эскизы',
|
||||
'designFiles.sectionLiveArtifacts': 'Live-артефакты',
|
||||
'designFiles.sectionOther': 'Другое',
|
||||
'designFiles.modifiedToday': 'Сегодня',
|
||||
'designFiles.modifiedYesterday': 'Вчера',
|
||||
'designFiles.modifiedPrevious7Days': 'Последние 7 дней',
|
||||
'designFiles.modifiedPrevious30Days': 'Последние 30 дней',
|
||||
'designFiles.modifiedOlder': 'Старше',
|
||||
'designFiles.showMore': 'Показать ещё +{n}',
|
||||
'designFiles.kindHtml': 'HTML страница',
|
||||
'designFiles.kindImage': 'Изображение',
|
||||
|
|
|
|||
|
|
@ -647,11 +647,21 @@ export const tr: Dict = {
|
|||
'designFiles.previewClose': 'Önizlemeyi kapat',
|
||||
'designFiles.modified': 'Düzenlendi: {time} · {size}',
|
||||
'designFiles.weeksAgo': '{n} hafta önce',
|
||||
'designFiles.groupBy': 'Grupla',
|
||||
'designFiles.groupByKind': 'Tür',
|
||||
'designFiles.groupByModified': 'Değiştirilme',
|
||||
'designFiles.expandGroup': 'Genişlet',
|
||||
'designFiles.collapseGroup': 'Daralt',
|
||||
'designFiles.sectionPages': 'Sayfalar',
|
||||
'designFiles.sectionScripts': 'Betikler',
|
||||
'designFiles.sectionImages': 'Görseller',
|
||||
'designFiles.sectionSketches': 'Taslaklar',
|
||||
'designFiles.sectionOther': 'Diğer',
|
||||
'designFiles.modifiedToday': 'Bugün',
|
||||
'designFiles.modifiedYesterday': 'Dün',
|
||||
'designFiles.modifiedPrevious7Days': 'Son 7 gün',
|
||||
'designFiles.modifiedPrevious30Days': 'Son 30 gün',
|
||||
'designFiles.modifiedOlder': 'Daha eski',
|
||||
'designFiles.showMore': '+{n} tane daha göster',
|
||||
'designFiles.kindHtml': 'HTML sayfası',
|
||||
'designFiles.kindImage': 'Görsel',
|
||||
|
|
|
|||
|
|
@ -669,12 +669,22 @@ export const uk: Dict = {
|
|||
'designFiles.previewClose': 'Закрити попередній перегляд',
|
||||
'designFiles.modified': 'Змінено {time} · {size}',
|
||||
'designFiles.weeksAgo': '{n}тижнів назад',
|
||||
'designFiles.groupBy': 'Групувати за',
|
||||
'designFiles.groupByKind': 'Тип',
|
||||
'designFiles.groupByModified': 'Змінено',
|
||||
'designFiles.expandGroup': 'Розгорнути',
|
||||
'designFiles.collapseGroup': 'Згорнути',
|
||||
'designFiles.sectionPages': 'Сторінки',
|
||||
'designFiles.sectionLiveArtifacts': 'Live-артефакти',
|
||||
'designFiles.sectionScripts': 'Скрипти',
|
||||
'designFiles.sectionImages': 'Зображення',
|
||||
'designFiles.sectionSketches': 'Ескізи',
|
||||
'designFiles.sectionOther': 'Інше',
|
||||
'designFiles.modifiedToday': 'Сьогодні',
|
||||
'designFiles.modifiedYesterday': 'Учора',
|
||||
'designFiles.modifiedPrevious7Days': 'Останні 7 днів',
|
||||
'designFiles.modifiedPrevious30Days': 'Останні 30 днів',
|
||||
'designFiles.modifiedOlder': 'Старіші',
|
||||
'designFiles.showMore': 'Показати ще +{n}',
|
||||
'designFiles.kindHtml': 'Веб-сторінка',
|
||||
'designFiles.kindLiveArtifact': 'Live-артефакт',
|
||||
|
|
|
|||
|
|
@ -712,12 +712,22 @@ export const zhCN: Dict = {
|
|||
'designFiles.previewClose': '关闭预览',
|
||||
'designFiles.modified': '修改于 {time} · {size}',
|
||||
'designFiles.weeksAgo': '{n} 周前',
|
||||
'designFiles.groupBy': '分组方式',
|
||||
'designFiles.groupByKind': '类型',
|
||||
'designFiles.groupByModified': '修改时间',
|
||||
'designFiles.expandGroup': '展开',
|
||||
'designFiles.collapseGroup': '折叠',
|
||||
'designFiles.sectionPages': '页面',
|
||||
'designFiles.sectionScripts': '脚本',
|
||||
'designFiles.sectionImages': '图片',
|
||||
'designFiles.sectionSketches': '草图',
|
||||
'designFiles.sectionLiveArtifacts': '实时制品',
|
||||
'designFiles.sectionOther': '其它',
|
||||
'designFiles.modifiedToday': '今天',
|
||||
'designFiles.modifiedYesterday': '昨天',
|
||||
'designFiles.modifiedPrevious7Days': '最近 7 天',
|
||||
'designFiles.modifiedPrevious30Days': '最近 30 天',
|
||||
'designFiles.modifiedOlder': '更早',
|
||||
'designFiles.showMore': '再显示 {n} 个',
|
||||
'designFiles.kindHtml': 'HTML 页面',
|
||||
'designFiles.kindImage': '图片',
|
||||
|
|
|
|||
|
|
@ -705,12 +705,22 @@ export const zhTW: Dict = {
|
|||
'designFiles.previewClose': '關閉預覽',
|
||||
'designFiles.modified': '修改於 {time} · {size}',
|
||||
'designFiles.weeksAgo': '{n} 週前',
|
||||
'designFiles.groupBy': '分組方式',
|
||||
'designFiles.groupByKind': '類型',
|
||||
'designFiles.groupByModified': '修改時間',
|
||||
'designFiles.expandGroup': '展開',
|
||||
'designFiles.collapseGroup': '折疊',
|
||||
'designFiles.sectionPages': '頁面',
|
||||
'designFiles.sectionScripts': '腳本',
|
||||
'designFiles.sectionImages': '圖片',
|
||||
'designFiles.sectionSketches': '草圖',
|
||||
'designFiles.sectionLiveArtifacts': '即時成品',
|
||||
'designFiles.sectionOther': '其它',
|
||||
'designFiles.modifiedToday': '今天',
|
||||
'designFiles.modifiedYesterday': '昨天',
|
||||
'designFiles.modifiedPrevious7Days': '最近 7 天',
|
||||
'designFiles.modifiedPrevious30Days': '最近 30 天',
|
||||
'designFiles.modifiedOlder': '更早',
|
||||
'designFiles.showMore': '再顯示 {n} 個',
|
||||
'designFiles.kindHtml': 'HTML 頁面',
|
||||
'designFiles.kindImage': '圖片',
|
||||
|
|
|
|||
|
|
@ -874,12 +874,22 @@ export interface Dict {
|
|||
'designFiles.previewClose': string;
|
||||
'designFiles.modified': string;
|
||||
'designFiles.weeksAgo': string;
|
||||
'designFiles.groupBy': string;
|
||||
'designFiles.groupByKind': string;
|
||||
'designFiles.groupByModified': string;
|
||||
'designFiles.expandGroup': string;
|
||||
'designFiles.collapseGroup': string;
|
||||
'designFiles.sectionPages': string;
|
||||
'designFiles.sectionScripts': string;
|
||||
'designFiles.sectionImages': string;
|
||||
'designFiles.sectionSketches': string;
|
||||
'designFiles.sectionLiveArtifacts': string;
|
||||
'designFiles.sectionOther': string;
|
||||
'designFiles.modifiedToday': string;
|
||||
'designFiles.modifiedYesterday': string;
|
||||
'designFiles.modifiedPrevious7Days': string;
|
||||
'designFiles.modifiedPrevious30Days': string;
|
||||
'designFiles.modifiedOlder': string;
|
||||
'designFiles.showMore': string;
|
||||
'designFiles.kindHtml': string;
|
||||
'designFiles.kindImage': string;
|
||||
|
|
|
|||
|
|
@ -7232,6 +7232,39 @@ button.connector-action.is-loading {
|
|||
overflow-y: auto;
|
||||
padding: 12px 0 0;
|
||||
}
|
||||
.df-group-toggle {
|
||||
margin: 0 20px 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-panel);
|
||||
color: var(--text-muted);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
.df-group-toggle > span {
|
||||
padding: 0 6px 0 4px;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
.df-group-toggle button {
|
||||
min-height: 24px;
|
||||
padding: 3px 9px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
.df-group-toggle button:hover {
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text);
|
||||
}
|
||||
.df-group-toggle button.active {
|
||||
background: var(--bg-subtle);
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
.df-section { display: flex; flex-direction: column; gap: 0; }
|
||||
.df-section + .df-section { margin-top: 6px; }
|
||||
.df-section-label {
|
||||
|
|
@ -7242,6 +7275,34 @@ button.connector-action.is-loading {
|
|||
font-weight: 600;
|
||||
padding: 12px 20px 6px;
|
||||
}
|
||||
.df-section-toggle {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.df-section-toggle:hover {
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text);
|
||||
}
|
||||
.df-section-toggle span:first-of-type {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.df-section-row td {
|
||||
padding: 10px 20px 4px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.df-section-row:first-child td {
|
||||
border-top: none;
|
||||
}
|
||||
.df-section-count {
|
||||
margin-left: 8px;
|
||||
color: var(--text-faint);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { act, cleanup, fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { DesignFilesPanel } from '../../src/components/DesignFilesPanel';
|
||||
import type { ProjectFile, ProjectFileKind } from '../../src/types';
|
||||
|
||||
|
|
@ -15,19 +16,29 @@ function extForKind(kind: ProjectFileKind): string {
|
|||
return 'bin';
|
||||
}
|
||||
|
||||
function file(overrides: Partial<ProjectFile> & Pick<ProjectFile, 'name'>): ProjectFile {
|
||||
return {
|
||||
path: overrides.name,
|
||||
type: 'file',
|
||||
size: 1024,
|
||||
mtime: Date.now(),
|
||||
kind: 'html',
|
||||
mime: 'text/html',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function generateFiles(count: number): ProjectFile[] {
|
||||
const kinds: ProjectFileKind[] = [
|
||||
'html', 'image', 'sketch', 'text', 'code', 'pdf',
|
||||
];
|
||||
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!,
|
||||
const kind = kinds[i % kinds.length]!;
|
||||
return file({
|
||||
name: `file-${i + 1}.${extForKind(kind)}`,
|
||||
kind,
|
||||
size: 1024 * (i + 1),
|
||||
mtime: Date.now() - i * 60_000,
|
||||
mime: 'text/plain',
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +79,207 @@ function getSelects(container: HTMLElement) {
|
|||
return Array.from(container.querySelectorAll<HTMLSelectElement>('select'));
|
||||
}
|
||||
|
||||
describe('DesignFilesPanel grouping', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('does not show grouping controls when only live artifacts are available', () => {
|
||||
render(
|
||||
<DesignFilesPanel
|
||||
projectId="project-1"
|
||||
files={[]}
|
||||
liveArtifacts={[
|
||||
{
|
||||
kind: 'live-artifact',
|
||||
artifactId: 'artifact-1',
|
||||
tabId: 'live:artifact-1',
|
||||
projectId: 'project-1',
|
||||
title: 'Live Preview',
|
||||
slug: 'live-preview',
|
||||
status: 'active',
|
||||
refreshStatus: 'idle',
|
||||
pinned: false,
|
||||
preview: { type: 'html', entry: 'index.html' },
|
||||
hasDocument: true,
|
||||
updatedAt: '2026-05-09T12:00:00.000Z',
|
||||
},
|
||||
]}
|
||||
onRefreshFiles={vi.fn()}
|
||||
onOpenFile={vi.fn()}
|
||||
onOpenLiveArtifact={vi.fn()}
|
||||
onRenameFile={vi.fn()}
|
||||
onDeleteFile={vi.fn()}
|
||||
onDeleteFiles={vi.fn()}
|
||||
onUpload={vi.fn()}
|
||||
onUploadFiles={vi.fn()}
|
||||
onPaste={vi.fn()}
|
||||
onNewSketch={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('group', { name: 'Group by' })).toBeNull();
|
||||
expect(screen.getByTestId('design-file-row-live:artifact-1')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('keeps the ungrouped table view as the default view', () => {
|
||||
renderPanel([
|
||||
file({ name: 'page.html', kind: 'html', mime: 'text/html' }),
|
||||
file({ name: 'chart.png', kind: 'image', mime: 'image/png' }),
|
||||
]);
|
||||
|
||||
const groupControls = screen.getByRole('group', { name: 'Group by' });
|
||||
const kindGroupButton = within(groupControls).getByRole('button', { name: 'Kind' });
|
||||
expect(kindGroupButton.getAttribute('aria-pressed')).toBe('true');
|
||||
expect(screen.getByText('Name')).toBeTruthy();
|
||||
expect(document.querySelector('.df-th-kind')?.textContent).toContain('Kind');
|
||||
expect(screen.queryByText('Today')).toBeNull();
|
||||
});
|
||||
|
||||
it('can group files by modified date and collapse a date group', () => {
|
||||
const now = new Date(2026, 4, 9, 12).getTime();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
renderPanel([
|
||||
file({ name: 'today.html', mtime: new Date(2026, 4, 9, 11).getTime() }),
|
||||
file({ name: 'yesterday.html', mtime: new Date(2026, 4, 8, 12).getTime() }),
|
||||
]);
|
||||
|
||||
expect(screen.queryByText('Today')).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
|
||||
|
||||
expect(screen.getByText('Today')).toBeTruthy();
|
||||
expect(screen.getByText('Yesterday')).toBeTruthy();
|
||||
expect(screen.getByTestId('design-file-row-today.html')).toBeTruthy();
|
||||
expect(screen.getByTestId('design-file-row-yesterday.html')).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Collapse Today/i }));
|
||||
|
||||
expect(screen.queryByTestId('design-file-row-today.html')).toBeNull();
|
||||
expect(screen.getByTestId('design-file-row-yesterday.html')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('keeps files from seven calendar days ago in the previous 7 days group', () => {
|
||||
const now = new Date(2026, 4, 9, 12).getTime();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
renderPanel([file({ name: 'week-old.html', mtime: new Date(2026, 4, 2, 12).getTime() })]);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
|
||||
|
||||
expect(screen.getByText('Previous 7 days')).toBeTruthy();
|
||||
expect(screen.queryByText('Previous 30 days')).toBeNull();
|
||||
expect(screen.getByTestId('design-file-row-week-old.html')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('keeps files at the seven calendar day boundary in the previous 7 days group', () => {
|
||||
const now = new Date(2026, 4, 9, 12).getTime();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
renderPanel([
|
||||
file({ name: 'week-boundary.html', mtime: new Date(2026, 4, 2, 0, 0, 0, 0).getTime() }),
|
||||
]);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
|
||||
|
||||
expect(screen.getByText('Previous 7 days')).toBeTruthy();
|
||||
expect(screen.queryByText('Previous 30 days')).toBeNull();
|
||||
expect(screen.getByTestId('design-file-row-week-boundary.html')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('keeps files from thirty calendar days ago in the previous 30 days group', () => {
|
||||
const now = new Date(2026, 4, 9, 12).getTime();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
renderPanel([
|
||||
file({ name: 'month-old.html', mtime: new Date(2026, 3, 9, 12).getTime() }),
|
||||
]);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
|
||||
|
||||
expect(screen.getByText('Previous 30 days')).toBeTruthy();
|
||||
expect(screen.queryByText('Older')).toBeNull();
|
||||
expect(screen.getByTestId('design-file-row-month-old.html')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('keeps files at the thirty calendar day boundary in the previous 30 days group', () => {
|
||||
const now = new Date(2026, 4, 9, 12).getTime();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
renderPanel([
|
||||
file({
|
||||
name: 'month-boundary.html',
|
||||
mtime: new Date(2026, 3, 9, 0, 0, 0, 0).getTime(),
|
||||
}),
|
||||
]);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
|
||||
|
||||
expect(screen.getByText('Previous 30 days')).toBeTruthy();
|
||||
expect(screen.queryByText('Older')).toBeNull();
|
||||
expect(screen.getByTestId('design-file-row-month-boundary.html')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('groups files older than thirty calendar days into older', () => {
|
||||
const now = new Date(2026, 4, 9, 12).getTime();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
renderPanel([file({ name: 'archive.html', mtime: new Date(2026, 3, 8, 12).getTime() })]);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
|
||||
|
||||
expect(screen.getByText('Older')).toBeTruthy();
|
||||
expect(screen.queryByText('Previous 30 days')).toBeNull();
|
||||
expect(screen.getByTestId('design-file-row-archive.html')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('groups only the current page so large file lists stay paginated', () => {
|
||||
const now = new Date(2026, 4, 9, 12).getTime();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
renderPanel(
|
||||
Array.from({ length: 31 }, (_, i) =>
|
||||
file({ name: `today-${String(i + 1).padStart(2, '0')}.html`, mtime: now - i }),
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
|
||||
|
||||
expect(screen.getByTestId('design-file-row-today-01.html')).toBeTruthy();
|
||||
expect(screen.queryByTestId('design-file-row-today-31.html')).toBeNull();
|
||||
expect(getPageInfo(document.body)).toContain('1–30 of 31');
|
||||
});
|
||||
|
||||
it('updates modified date groups when the local day changes', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2026, 4, 9, 23, 59, 50));
|
||||
|
||||
renderPanel([file({ name: 'late-edit.html', mtime: new Date(2026, 4, 9, 23, 59).getTime() })]);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
|
||||
|
||||
expect(screen.getByText('Today')).toBeTruthy();
|
||||
expect(screen.queryByText('Yesterday')).toBeNull();
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(10_001);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Yesterday')).toBeTruthy();
|
||||
expect(screen.queryByText('Today')).toBeNull();
|
||||
expect(screen.getByTestId('design-file-row-late-edit.html')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DesignFilesPanel large-list regression', () => {
|
||||
it('renders only the default page size (30) rows with 500 files', () => {
|
||||
const files = generateFiles(500);
|
||||
|
|
|
|||
Loading…
Reference in a new issue