diff --git a/frontend/e2e/files-plugin.spec.js b/frontend/e2e/files-plugin.spec.js
index a79a1ff..f8c5182 100644
--- a/frontend/e2e/files-plugin.spec.js
+++ b/frontend/e2e/files-plugin.spec.js
@@ -129,6 +129,34 @@ test.describe('G: Files Plugin', () => {
await expect(page.locator('[data-file-name="bad"]')).toHaveCount(0);
});
+ test('files explorer uses rich file icons by file type', async ({ page }) => {
+ await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
+ await openFilesTool(page);
+ await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
+
+ async function createFile(action, name) {
+ await page.locator(`[data-files-action="${action}"]`).click();
+ await page.locator('[data-files-create-input]').fill(name);
+ await page.locator('[data-files-create-confirm]').click();
+ await expect(page.locator(`[data-file-name="${name}"]`)).toBeVisible();
+ }
+
+ await createFile('new-markdown', 'IconNote.md');
+ await createFile('new-text', 'Photo.png');
+ await createFile('new-text', 'Manual.pdf');
+ await createFile('new-text', 'Script.js');
+
+ const iconFor = (name) => page.locator(`[data-file-name="${name}"] .files-item-icon`);
+ await expect(iconFor('Notes')).toHaveAttribute('data-file-icon', 'folder');
+ await expect(iconFor('project-only.txt')).toHaveAttribute('data-file-icon', 'text');
+ await expect(iconFor('IconNote.md')).toHaveAttribute('data-file-icon', 'markdown');
+ await expect(iconFor('Photo.png')).toHaveAttribute('data-file-icon', 'image');
+ await expect(iconFor('Manual.pdf')).toHaveAttribute('data-file-icon', 'pdf');
+ await expect(iconFor('Script.js')).toHaveAttribute('data-file-icon', 'code');
+ await expect(iconFor('Manual.pdf')).toHaveAttribute('aria-label', 'PDF file');
+ await expect(iconFor('Photo.png')).toHaveAttribute('title', 'Image file');
+ });
+
test('files explorer restores an item from trash metadata', async ({ page }) => {
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
await openFilesTool(page);
diff --git a/frontend/src/lib/test/wails-mock.js b/frontend/src/lib/test/wails-mock.js
index 07e8be8..cad8b4e 100644
--- a/frontend/src/lib/test/wails-mock.js
+++ b/frontend/src/lib/test/wails-mock.js
@@ -679,6 +679,41 @@
return '(' + function () {
var SVG = '';
var FOLDER_SVG = '';
+ var FILE_SVGS = {
+ folder: FOLDER_SVG,
+ generic: SVG,
+ markdown: '',
+ text: '',
+ image: '',
+ pdf: '',
+ code: ''
+ };
+ var ICON_EXTENSIONS = {
+ md: 'markdown', markdown: 'markdown',
+ txt: 'text', text: 'text', log: 'text', rtf: 'text',
+ jpg: 'image', jpeg: 'image', png: 'image', gif: 'image', webp: 'image', svg: 'image',
+ pdf: 'pdf',
+ js: 'code', jsx: 'code', mjs: 'code', cjs: 'code', ts: 'code', tsx: 'code',
+ py: 'code', go: 'code', rs: 'code', css: 'code', html: 'code', json: 'code'
+ };
+ function fileIconCategory(item) {
+ if (item.type === 'folder') return 'folder';
+ return ICON_EXTENSIONS[(item.extension || ext(item.name)).toLowerCase()] || 'generic';
+ }
+ function fileIconLabel(category) {
+ return {
+ folder: 'Folder',
+ markdown: 'Markdown file',
+ text: 'Text file',
+ image: 'Image file',
+ pdf: 'PDF file',
+ code: 'Code file',
+ generic: 'File'
+ }[category] || 'File';
+ }
+ function fileIcon(item) {
+ return FILE_SVGS[fileIconCategory(item)] || FILE_SVGS.generic;
+ }
function e(tag, attrs, children) {
var node = document.createElement(tag);
attrs = attrs || {};
@@ -838,6 +873,8 @@
list.appendChild(e('div', { className: 'files-header' }, [e('span', {}, ['Name']), e('span', {}, ['Type']), e('span', {}, ['Size']), e('span', {}, ['Modified']), e('span', {}, ['Actions'])]));
var shown = visible();
shown.forEach(function (item) {
+ var iconCategory = fileIconCategory(item);
+ var iconLabel = fileIconLabel(iconCategory);
var row = e('div', {
className: 'files-item' + (selected[item.relativePath] ? ' selected' : ''),
'data-file-name': item.name,
@@ -852,7 +889,7 @@
ev.dataTransfer.effectAllowed = 'move';
}
}, []);
- row.appendChild(e('span', { className: 'files-namecell' }, [e('span', { className: 'files-item-icon', innerHTML: item.type === 'folder' ? FOLDER_SVG : SVG }, []), e('span', { className: 'files-item-name' }, [item.name])]));
+ row.appendChild(e('span', { className: 'files-namecell' }, [e('span', { className: 'files-item-icon', 'data-file-icon': iconCategory, title: iconLabel, 'aria-label': iconLabel, innerHTML: fileIcon(item) }, []), e('span', { className: 'files-item-name' }, [item.name])]));
row.appendChild(e('span', { className: 'files-item-meta' }, [item.type === 'folder' ? 'folder' : (item.extension || ext(item.name) || 'file')]));
row.appendChild(e('span', { className: 'files-item-meta' }, [item.size ? String(item.size) : '']));
row.appendChild(e('span', { className: 'files-item-meta' }, [formatDate(item.modifiedAt)]));