diff --git a/src/gui/src/UI/Dashboard/TabFiles.js b/src/gui/src/UI/Dashboard/TabFiles.js
index dd6237ba2..163d419b1 100644
--- a/src/gui/src/UI/Dashboard/TabFiles.js
+++ b/src/gui/src/UI/Dashboard/TabFiles.js
@@ -48,6 +48,7 @@ const icons = {
restore: ``,
list: ``,
grid: ``,
+ gridSmall: ``,
sort: ``,
select: ``,
done: ``,
@@ -195,7 +196,7 @@ const TabFiles = {
$row.find('.item-modified').text(window.timeago.format(file.modified * 1000));
}
if (
- _this.currentView === 'grid' &&
+ _this.isGridView() &&
typeof file.thumbnail === 'string' &&
file.thumbnail.length > 0
) {
@@ -536,17 +537,7 @@ const TabFiles = {
this.initNativeFileDrop();
// Apply initial view mode from persisted preferences
-
- const $filesContainer = this.$el_window.find('.files-tab .files');
- const $tabContent = this.$el_window.find('.files-tab');
- if ( this.currentView === 'grid' ) {
- $filesContainer.addClass('files-grid-view');
- $tabContent.addClass('files-grid-mode');
- this.$el_window.find('.view-toggle-btn').html(icons.list);
- } else {
- $filesContainer.addClass('files-list-view');
- this.$el_window.find('.view-toggle-btn').html(icons.grid);
- }
+ this.applyViewMode();
// Check for initial file path from URL routing
if ( window.dashboard_initial_file_path ) {
@@ -1270,9 +1261,9 @@ const TabFiles = {
fileInput.click();
};
- // View toggle button
- document.querySelector('.view-toggle-btn').onclick = () => {
- this.toggleView();
+ // View button (shows dropdown menu: list / compact grid / grid)
+ document.querySelector('.view-toggle-btn').onclick = (e) => {
+ this.showViewMenu(e);
};
// Sort button (shows dropdown menu)
@@ -1583,7 +1574,7 @@ const TabFiles = {
$name.text(fullName);
}
});
- } else if ( this.currentView === 'grid' ) {
+ } else if ( this.isGridView() ) {
// Apply middle-truncation in grid view
$filesTab.find('.files.files-grid-view .row .item-name').each(function () {
const $name = $(this);
@@ -2516,13 +2507,13 @@ const TabFiles = {
const $clone = $(el_item).clone();
// Wrap in container structure so CSS selectors match
- const viewClass = _this.currentView === 'grid' ? 'files-grid-view' : 'files-list-view';
+ const viewClass = _this.viewClass();
const $wrapper = $(`
`);
$wrapper.find('.files').append($clone);
// In grid view, set fixed width since the grid auto-fill
// doesn't work without a proper parent width context
- if ( _this.currentView === 'grid' ) {
+ if ( _this.isGridView() ) {
$clone.css('width', $(el_item).outerWidth());
$wrapper.find('.files').css('display', 'block');
}
@@ -2556,7 +2547,7 @@ const TabFiles = {
ui.helper.addClass('selected');
// Clone other selected items with proper container structure
- const viewClass = _this.currentView === 'grid' ? 'files-grid-view' : 'files-list-view';
+ const viewClass = _this.viewClass();
$(el_item).siblings('.row.selected').each(function () {
const $clone = $(this).clone();
const $wrapper = $(``);
@@ -2987,32 +2978,71 @@ const TabFiles = {
},
/**
- * Toggles between list and grid view modes.
+ * Whether the current view mode is one of the grid variants.
*
- * Persists the preference to storage.
+ * Both 'grid' and 'grid-sm' share the files-grid-view container class
+ * (and all grid behavior); 'grid-sm' adds a size-modifier class on top.
*
+ * @returns {boolean}
+ */
+ isGridView () {
+ return this.currentView === 'grid' || this.currentView === 'grid-sm';
+ },
+
+ /**
+ * Container classes for the current view mode, e.g. for drag-ghost wrappers.
+ *
+ * @returns {string} Space-separated class list for the .files container
+ */
+ viewClass () {
+ if ( this.currentView === 'grid-sm' ) return 'files-grid-view files-grid-view-sm';
+ if ( this.currentView === 'grid' ) return 'files-grid-view';
+ return 'files-list-view';
+ },
+
+ /**
+ * Displays the view mode selection menu (list / compact grid / grid).
+ *
+ * @param {MouseEvent} e - The click event from the view button
* @returns {void}
*/
- toggleView () {
- const $filesContainer = this.$el_window.find('.files-tab .files');
- const $toggleBtn = this.$el_window.find('.view-toggle-btn');
- const $tabContent = this.$el_window.find('.files-tab');
+ showViewMenu (e) {
+ const _this = this;
- if ( this.currentView === 'list' ) {
- this.currentView = 'grid';
- $filesContainer.removeClass('files-list-view').addClass('files-grid-view');
- $tabContent.addClass('files-grid-mode');
- $toggleBtn.html(icons.list);
- $toggleBtn.attr('title', 'Switch to list view');
- } else {
- this.currentView = 'list';
- $filesContainer.removeClass('files-grid-view').addClass('files-list-view');
- $tabContent.removeClass('files-grid-mode');
- $toggleBtn.html(icons.grid);
- $toggleBtn.attr('title', 'Switch to grid view');
- }
+ const viewOptions = [
+ { mode: 'list', label: 'List' },
+ { mode: 'grid-sm', label: 'Compact Grid' },
+ { mode: 'grid', label: 'Grid' },
+ ];
- puter.kv.set('view_mode', this.currentView);
+ const items = viewOptions.map(opt => {
+ return {
+ html: `${opt.label}`,
+ checked: _this.currentView === opt.mode,
+ onClick: () => {
+ _this.setView(opt.mode);
+ },
+ };
+ });
+
+ UIContextMenu({
+ items: items,
+ position: { left: e.pageX, top: e.pageY },
+ });
+ },
+
+ /**
+ * Switches to the given view mode and persists the preference.
+ *
+ * @param {string} mode - 'list', 'grid', or 'grid-sm'
+ * @returns {void}
+ */
+ setView (mode) {
+ if ( this.currentView === mode ) return;
+ this.currentView = mode;
+ this.applyViewMode();
+
+ puter.kv.set('view_mode', mode);
// Refresh content to update icons for the new view mode
if ( this.currentPath ) {
@@ -3020,6 +3050,32 @@ const TabFiles = {
}
},
+ /**
+ * Applies the current view mode's classes and button icon to the DOM.
+ *
+ * @returns {void}
+ */
+ applyViewMode () {
+ const $filesContainer = this.$el_window.find('.files-tab .files');
+ const $toggleBtn = this.$el_window.find('.view-toggle-btn');
+ const $tabContent = this.$el_window.find('.files-tab');
+
+ $filesContainer.removeClass('files-list-view files-grid-view files-grid-view-sm');
+ if ( this.isGridView() ) {
+ $filesContainer.addClass(this.viewClass());
+ $tabContent.addClass('files-grid-mode');
+ } else {
+ $filesContainer.addClass('files-list-view');
+ $tabContent.removeClass('files-grid-mode');
+ }
+
+ // The button shows the active view's icon and opens the view menu
+ const btnIcon = this.currentView === 'grid' ? icons.grid
+ : this.currentView === 'grid-sm' ? icons.gridSmall
+ : icons.list;
+ $toggleBtn.html(btnIcon);
+ },
+
/**
* Toggles select mode for mobile multi-file selection.
*
diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css
index bb120a918..db70537b3 100644
--- a/src/gui/src/css/dashboard.css
+++ b/src/gui/src/css/dashboard.css
@@ -2263,6 +2263,41 @@ body.myapps-reordering .myapps-tile {
text-align: center;
}
+/* --- Compact grid view ---
+ The container carries both files-grid-view and files-grid-view-sm, so all
+ grid rules above apply; this block only shrinks tile/icon/badge sizing. */
+
+.dashboard-section-files .files-tab .files.files-grid-view.files-grid-view-sm {
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ gap: 8px;
+}
+
+.dashboard-section-files .files-tab .files.files-grid-view.files-grid-view-sm .row {
+ padding: 8px;
+ gap: 6px;
+}
+
+.dashboard-section-files .files-tab .files.files-grid-view.files-grid-view-sm .row .item-icon {
+ width: 80px;
+ height: 80px;
+ border-radius: 6px;
+}
+
+.dashboard-section-files .files-tab .files.files-grid-view.files-grid-view-sm .row .item-icon svg {
+ width: 40px;
+ height: 40px;
+}
+
+.dashboard-section-files .files-tab .files.files-grid-view.files-grid-view-sm .row .item-name {
+ font-size: 12px;
+}
+
+.dashboard-section-files .files-tab .files.files-grid-view.files-grid-view-sm .row img.item-badge {
+ width: 16px !important;
+ height: 16px !important;
+ margin: 3px;
+}
+
.dashboard-section-files .files-tab.files-grid-mode .header .columns {
display: none;
}