Compare commits

...
Author SHA1 Message Date
Nariman Jelveh f805d7bf37 fix(gui): Escape calls off a path lookup still running
Escape closed the path field, but the lookup behind it carried on: the
tab still went to the path once it answered, or alerted that it wasn't
found, and a path typed into the reopened field was ignored until then.
2026-11-03 17:44:36 -07:00
Nariman Jelveh 48a5c7d7bb fix(gui): a typed / or .. reaches the root folder
The root can't be stat'ed, so the path field reported "/" as not found,
even though Up goes there. Like Shared, it is now opened without a
lookup.
2026-11-03 17:42:51 -07:00
Nariman Jelveh 910fe2d334 fix(gui): phone keyboards leave a typed path as typed
iOS autocorrect rewrote folder names in the path field as they were
typed. Turn it off, as the GUI's other name fields do, and label the
keyboard's Enter key Go.
2026-11-03 17:40:32 -07:00
Nariman Jelveh c5c559514e fix(gui): space the path field from the Files toolbar buttons
Its border ran up against the sort button, a few pixels from the icon,
where the toolbar's other items keep 10px apart.
2026-11-03 17:40:04 -07:00
Nariman Jelveh cf514a4515 fix(gui): opening the path field no longer moves a narrow Files header
On phones and in narrow lists the field was shorter than the crumb row
it replaced and dropped its divider, so the buttons and the listing
jumped up as it opened. It now takes a crumb's height and keeps the
divider.
2026-11-03 17:39:46 -07:00
Nariman Jelveh 7b8106757e fix(gui): arrow keys go on from rows the Files tab selects for you
Selecting a typed file, or the items just uploaded or pasted, left the
keyboard's starting row on whatever was selected before, so Down jumped
next to that row instead of the new selection.
2026-11-03 17:38:48 -07:00
Nariman Jelveh 382e6274fe fix(gui): leave Enter and Escape to an IME composing in the path field
Picking a candidate with Enter while typing a name through an input
method went to the typed path, and Escape to cancel the composition
closed the field.
2026-11-03 17:38:09 -07:00
Nariman Jelveh 3d19201a2d fix(gui): look a typed path up once, however often Enter is pressed
A second Enter (or a held key) during the lookup started another one,
and for a path to nothing stacked a second alert on the first.
2026-11-03 17:37:23 -07:00
Nariman Jelveh 492e339a74 fix(gui): closing the path alert with Escape gives the path field back
UIAlert only settles when a button is clicked, so an alert closed with
Escape left the path field waiting forever: it no longer closed on blur
and never got the focus back. Wait for the alert window to close
instead.
2026-11-03 17:36:36 -07:00
Nariman Jelveh 87b6b34545 feat(gui): type a path into the dashboard Files path bar
Clicking the path bar anywhere but on a crumb, or typing /, swaps the
breadcrumbs for a text field. Enter opens the folder typed (or a file's
folder with the file selected); ~ is home and relative paths start from
the folder on screen. Escape or clicking away puts the crumbs back, and
a path to nothing shows an alert and keeps the field open.
2026-11-03 17:25:12 -07:00
6 changed files with 573 additions and 0 deletions

No files matched your search

+152
View File
@@ -39,6 +39,7 @@ import { openPublishWebsiteDialog, openPublishWorkerDialog } from './UIPublishDi
import { dedupedName } from '../../helpers/dedupedName.js';
import { compareEntries, compareNames } from './sortEntries.js';
import { middleTruncate } from './middleTruncate.js';
import { resolveTypedPath } from './typedPath.js';
import { isEntryVisible, isHiddenName, showHiddenFiles } from './hiddenFiles.js';
import { icons } from '../../helpers/actionIcons.js';
@@ -163,6 +164,7 @@ const TabFiles = {
<img draggable="false" class="path-btn path-btn-forward path-btn-disabled" tabindex="-1" role="button" aria-disabled="true" aria-label="${i18n('window_click_to_go_forward')}" src="${html_encode(window.icons['arrow-right-thin.svg'])}" title="${i18n('window_click_to_go_forward')}">
<img draggable="false" class="path-btn path-btn-up path-btn-disabled" tabindex="-1" role="button" aria-disabled="true" aria-label="${i18n('window_click_to_go_up')}" src="${html_encode(window.icons['arrow-up-thin.svg'])}" title="${i18n('window_click_to_go_up')}">
</div>
<input type="text" class="path-editor" spellcheck="false" autocomplete="off" autocapitalize="off" autocorrect="off" enterkeyhint="go" aria-label="${i18n('path')}">
<div class="path-breadcrumbs"></div>
<div class="path-actions">
<button class="path-action-btn select-mode-btn" title="${i18n('select')}" aria-pressed="false">${icons.select}</button>
@@ -836,6 +838,142 @@ const TabFiles = {
if ( crumbs ) crumbs.scrollLeft = crumbs.scrollWidth;
},
/**
* Lets the path bar be typed into: a click on it anywhere but on a crumb
* turns it into a text field, as in an Explorer window.
*
* @returns {void}
*/
bindPathEditor () {
this.$el_window.on('click', '.files-tab .path-breadcrumbs', (e) => {
if ( $(e.target).closest('.dirname').length ) return;
this.openPathEditor();
});
this.$el_window.on('keydown', '.files-tab .path-editor', (e) => {
// Enter and Escape mid-composition belong to the IME; jQuery's
// event doesn't carry isComposing, the browser's does.
if ( e.originalEvent?.isComposing || e.keyCode === 229 ) return;
if ( e.key === 'Enter' ) {
e.preventDefault();
e.stopPropagation();
this.submitPathEditor();
} else if ( e.key === 'Escape' ) {
e.preventDefault();
e.stopPropagation();
this.closePathEditor({ refocus: true });
}
});
this.$el_window.on('input', '.files-tab .path-editor', (e) => {
e.currentTarget.removeAttribute('aria-invalid');
});
this.$el_window.on('focusout', '.files-tab .path-editor', () => {
if ( ! this.pathLookup ) this.closePathEditor();
});
},
/**
* Swaps the breadcrumbs for a text field to type a path into.
*
* @param {string} [initial] - Text to start from, with the caret after
* it; by default the current path, selected
* @returns {void}
*/
openPathEditor (initial) {
const $path = this.$el_window?.find('.files-tab .header .path');
const input = $path?.find('.path-editor')[0];
if ( !input || !this.currentPath ) return;
input.value = initial ?? this.currentPath;
input.removeAttribute('aria-invalid');
$path.addClass('path-editing');
input.focus();
if ( initial === undefined ) input.select();
else input.setSelectionRange(input.value.length, input.value.length);
},
/**
* Puts the breadcrumbs back in place of the path field.
*
* @param {{ refocus?: boolean }} [options] - refocus hands the keyboard
* back to the listing
* @returns {void}
*/
closePathEditor ({ refocus = false } = {}) {
const $path = this.$el_window?.find('.files-tab .header .path');
if ( ! $path?.hasClass('path-editing') ) return;
$path.removeClass('path-editing');
// A lookup still running for the closed field is called off.
this.pathLookup = null;
// Hidden, the crumbs lost their scroll position.
this.revealCurrentCrumb();
if ( refocus ) this.focusListing();
},
/**
* Goes where the path field says: into a folder, or to a file's folder
* with the file selected. A path to nothing leaves the field open.
*
* @returns {Promise<void>}
*/
async submitPathEditor () {
// A second Enter during the lookup would look it up again, and a
// path to nothing would get a second alert.
if ( this.pathLookup ) return;
const input = this.$el_window.find('.files-tab .path-editor')[0];
const typed = input.value.trim();
const target = resolveTypedPath(typed, {
currentPath: this.currentPath,
homePath: window.home_path,
sharedPath: window.shared_path,
});
if ( target === undefined || target === this.currentPath ) {
this.closePathEditor({ refocus: true });
return;
}
let folder = target;
let file;
// Neither Shared nor the root can be stat'ed, and both are folders.
if ( target !== window.shared_path && target !== '/' ) {
// The stat and the alert take the focus from the field, which
// would close it.
const lookup = this.pathLookup = {};
try {
let entry;
try {
entry = await puter.fs.stat({ path: target, consistency: 'eventual' });
} catch {
entry = undefined;
}
if ( this.pathLookup !== lookup ) return;
if ( ! entry ) {
input.setAttribute('aria-invalid', 'true');
// Settled on close: Escape closes the alert without
// settling what UIAlert returns.
await new Promise(resolve => UIAlert({
message: i18n('path_not_found', [typed], false),
window_options: { on_close: resolve },
}));
input.focus();
input.select();
return;
}
if ( ! entry.is_dir ) {
file = entry.path ?? target;
folder = path.dirname(file);
}
} finally {
if ( this.pathLookup === lookup ) this.pathLookup = null;
}
}
this.closePathEditor({ refocus: true });
if ( folder !== this.currentPath ) {
this.pushNavHistory(folder);
await this.renderDirectory(folder);
}
if ( file ) this.selectRowsWhere(row => row.getAttribute('data-path') === file);
},
/**
* Checks if the Dashboard Files tab is currently active and visible.
*
@@ -1253,6 +1391,14 @@ const TabFiles = {
return false;
}
// No name holds a slash, so typing one starts a path to go to.
if ( e.key === '/' && !e.ctrlKey && !e.metaKey && !e.altKey && _this.typeSearchTerm === '' ) {
e.preventDefault();
e.stopImmediatePropagation();
_this.openPathEditor('/');
return false;
}
// Type-to-select: letter/number keys search items by name
if ( !e.ctrlKey && !e.metaKey && e.key.length === 1 ) {
e.preventDefault();
@@ -1414,6 +1560,8 @@ const TabFiles = {
this.click();
});
this.bindPathEditor();
const el_window_navbar_back_btn = document.querySelector(`.path-btn-back`);
const el_window_navbar_forward_btn = document.querySelector(`.path-btn-forward`);
const el_window_navbar_up_btn = document.querySelector(`.path-btn-up`);
@@ -5886,6 +6034,10 @@ const TabFiles = {
this.$el_window.find('.files-tab .files .row.selected').removeClass('selected');
rows.addClass('selected');
// The arrow keys and Shift+click go on from here, not from a row
// selected before.
window.active_element = rows[0];
window.latest_selected_item = rows[0];
this.updateFooterStats();
rows[0].scrollIntoView({ block: 'nearest' });
},
+248
View File
@@ -1426,3 +1426,251 @@ describe('TabFiles listSharedView', () => {
expect(rows[1].holder_team).toEqual(team);
});
});
describe('TabFiles path field', () => {
let UIAlert;
/** The tab's real header and an empty listing, its listeners bound. */
const mountHeader = (currentPath = '/alice/Documents') => {
// Drag and drop is jQuery UI's, which isn't loaded here.
$.fn.droppable ??= function () {
return this;
};
$.fn.draggable ??= function () {
return this;
};
document.body.innerHTML = `<div class="window">${TabFiles.html()}</div>`;
TabFiles.$el_window = $('.window');
TabFiles.currentPath = currentPath;
TabFiles.renderingDirectory = false;
TabFiles.trashedNames ??= new Map();
$('.path-breadcrumbs').html(TabFiles.renderPath(currentPath));
TabFiles.bindPathEditor();
};
const editor = () => $('.path-editor')[0];
const editing = () => $('.header .path').hasClass('path-editing');
const keydown = (el, key) => el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
beforeEach(async () => {
({ default: UIAlert } = await import('../UIAlert.js'));
UIAlert.mockClear();
window.user = { username: 'alice', directories: {} };
window.home_path = '/alice';
window.trash_path = TRASH;
window.shared_path = 'puter://shared';
window.byte_format = bytes => `${bytes} B`;
window.dashboard_nav_history = ['/alice/Documents'];
window.dashboard_nav_history_current_position = 0;
Element.prototype.scrollIntoView ??= () => {};
window.puter = { fs: {
readdir: vi.fn(async () => []),
stat: vi.fn(async ({ path }) => {
if ( path === '/alice/Pictures' ) return { path, is_dir: true };
if ( path === '/alice/Pictures/cat.png' ) return { path, is_dir: false };
throw { code: 'subject_does_not_exist' };
}),
} };
});
afterEach(() => {
TabFiles.$el_window.off();
$(document).off('keydown.tabfiles');
vi.restoreAllMocks();
});
it('opens with the current path selected on a click past the crumbs', () => {
mountHeader();
$('.path-breadcrumbs')[0].click();
expect(editing()).toBe(true);
expect(document.activeElement).toBe(editor());
expect(editor().value).toBe('/alice/Documents');
expect(editor().selectionStart).toBe(0);
expect(editor().selectionEnd).toBe('/alice/Documents'.length);
});
it('keeps a phone keyboard from rewriting the path, and offers Go', () => {
mountHeader();
expect(editor().getAttribute('autocorrect')).toBe('off');
expect(editor().getAttribute('autocapitalize')).toBe('off');
expect(editor().getAttribute('enterkeyhint')).toBe('go');
});
it('stays closed on a click on a crumb', () => {
mountHeader();
vi.spyOn(TabFiles, 'renderDirectory').mockResolvedValue();
$('.path-breadcrumbs .dirname').first()[0].click();
expect(editing()).toBe(false);
});
it('opens on a typed slash, holding just the slash', () => {
mountHeader();
vi.spyOn(TabFiles, 'isDashboardFilesActive').mockReturnValue(true);
TabFiles.typeSearchTerm = '';
TabFiles.setupKeyboardShortcuts();
document.body.dispatchEvent(new KeyboardEvent('keydown', { key: '/', which: 191, bubbles: true }));
expect(editing()).toBe(true);
expect(editor().value).toBe('/');
});
it('goes into the folder typed, and Back returns', async () => {
mountHeader();
TabFiles.openPathEditor();
editor().value = '../Pictures';
await TabFiles.submitPathEditor();
expect(TabFiles.currentPath).toBe('/alice/Pictures');
expect(window.dashboard_nav_history).toEqual(['/alice/Documents', '/alice/Pictures']);
expect(editing()).toBe(false);
});
it('goes to the root, which can\'t be looked up, from / or ..', async () => {
for ( const typed of ['/', '..'] ) {
mountHeader('/alice');
TabFiles.openPathEditor();
editor().value = typed;
await TabFiles.submitPathEditor();
expect(TabFiles.currentPath).toBe('/');
}
expect(window.puter.fs.stat).not.toHaveBeenCalled();
expect(UIAlert).not.toHaveBeenCalled();
});
it('opens a typed file\'s folder with the file selected', async () => {
mountHeader();
window.puter.fs.readdir = vi.fn(async () => [{ name: 'cat.png', path: '/alice/Pictures/cat.png', is_dir: false, uid: 'u-cat', size: 1, modified: 0 }]);
vi.spyOn(TabFiles, 'renderItem').mockImplementation(async (file) => {
$('.files').append(`<div class="row item" data-uid="${file.uid}" data-path="${file.path}"></div>`);
});
TabFiles.openPathEditor();
editor().value = '~/Pictures/cat.png';
await TabFiles.submitPathEditor();
expect(TabFiles.currentPath).toBe('/alice/Pictures');
expect($('.files .row.selected').attr('data-path')).toBe('/alice/Pictures/cat.png');
});
it('moves the arrow keys\' starting row to a typed file in the folder on screen', async () => {
mountHeader('/alice/Pictures');
$('.files').html(['a.png', 'cat.png', 'z.png'].map(name => `<div class="row item" data-name="${name}" data-path="/alice/Pictures/${name}"></div>`).join(''));
const [first, cat] = $('.files .row').toArray();
first.classList.add('selected');
window.latest_selected_item = first;
TabFiles.openPathEditor();
editor().value = 'cat.png';
await TabFiles.submitPathEditor();
expect($('.files .row.selected').toArray()).toEqual([cat]);
expect(window.latest_selected_item).toBe(cat);
expect(window.active_element).toBe(cat);
});
it('stays open, marked, on a path to nothing', async () => {
mountHeader();
const render = vi.spyOn(TabFiles, 'renderDirectory');
TabFiles.openPathEditor();
editor().value = '/alice/Gone';
// The alert takes the focus, which must not close the field.
UIAlert.mockImplementation(async ({ window_options }) => {
editor().blur();
window_options.on_close();
return true;
});
await TabFiles.submitPathEditor();
expect(UIAlert.mock.calls[0][0].message).toContain('/alice/Gone');
expect(render).not.toHaveBeenCalled();
expect(editing()).toBe(true);
expect(editor().getAttribute('aria-invalid')).toBe('true');
expect(document.activeElement).toBe(editor());
});
it('takes the field back when the alert is closed without its button', async () => {
mountHeader();
TabFiles.openPathEditor();
editor().value = '/alice/Gone';
// Escape removes the alert, and what UIAlert returns never settles.
UIAlert.mockImplementation(({ window_options }) => {
editor().blur();
window_options.on_close();
return new Promise(() => {});
});
await TabFiles.submitPathEditor();
expect(document.activeElement).toBe(editor());
editor().blur();
expect(editing()).toBe(false);
});
it('looks a path up once, however often Enter is pressed meanwhile', async () => {
mountHeader();
TabFiles.openPathEditor();
editor().value = '/alice/Gone';
UIAlert.mockImplementation(async ({ window_options }) => window_options.on_close());
await Promise.all([TabFiles.submitPathEditor(), TabFiles.submitPathEditor()]);
expect(window.puter.fs.stat).toHaveBeenCalledTimes(1);
expect(UIAlert).toHaveBeenCalledTimes(1);
});
it('calls off a lookup on Escape, and takes a new path at once', async () => {
mountHeader();
const render = vi.spyOn(TabFiles, 'renderDirectory').mockResolvedValue();
let settle;
window.puter.fs.stat.mockImplementationOnce(() => new Promise(resolve => {
settle = resolve;
}));
TabFiles.openPathEditor();
editor().value = '/alice/Gone';
const calledOff = TabFiles.submitPathEditor();
keydown(editor(), 'Escape');
TabFiles.openPathEditor();
editor().value = '/alice/Pictures';
await TabFiles.submitPathEditor();
settle(undefined);
await calledOff;
expect(render.mock.calls).toEqual([['/alice/Pictures']]);
expect(UIAlert).not.toHaveBeenCalled();
});
it('leaves Enter and Escape to an IME composing a name', () => {
mountHeader();
const submit = vi.spyOn(TabFiles, 'submitPathEditor').mockResolvedValue();
TabFiles.openPathEditor();
for ( const key of ['Enter', 'Escape'] ) {
editor().dispatchEvent(new KeyboardEvent('keydown', { key, isComposing: true, bubbles: true }));
editor().dispatchEvent(new KeyboardEvent('keydown', { key: 'Process', keyCode: 229, bubbles: true }));
}
expect(submit).not.toHaveBeenCalled();
expect(editing()).toBe(true);
});
it('gives the crumbs back on Escape or when it loses the focus', () => {
mountHeader();
TabFiles.openPathEditor();
keydown(editor(), 'Escape');
expect(editing()).toBe(false);
TabFiles.openPathEditor();
editor().blur();
expect(editing()).toBe(false);
});
});
+39
View File
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import path from '../../lib/path.js';
/**
* Where a path typed into the Files tab's path bar leads: `~` is the home
* folder, and a relative path starts from the folder on screen.
*
* @param {string} typed
* @param {{ currentPath: string, homePath: string, sharedPath: string }} places
* @returns {string | undefined} An absolute path, the Shared view's path, or
* undefined when nothing was typed
*/
export function resolveTypedPath (typed, { currentPath, homePath, sharedPath }) {
const text = typed.trim();
if ( text === '' ) return undefined;
if ( text.replace(/\/+$/, '') === sharedPath ) return sharedPath;
if ( text === '~' || text.startsWith('~/') ) return path.resolve(homePath, `.${text.slice(1)}`);
// The Shared view is a query, not a folder a relative path can start from.
const base = currentPath?.startsWith('/') ? currentPath : homePath;
return path.resolve(base, text);
}
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { describe, expect, it } from 'vitest';
import { resolveTypedPath } from './typedPath.js';
const places = { currentPath: '/alice/Documents', homePath: '/alice', sharedPath: 'puter://shared' };
describe('resolveTypedPath', () => {
it('takes an absolute path as typed, without the trailing slash', () => {
expect(resolveTypedPath('/bob/Public/', places)).toBe('/bob/Public');
expect(resolveTypedPath('/', places)).toBe('/');
});
it('ignores the space around a pasted path', () => {
expect(resolveTypedPath(' /alice/Pictures \n', places)).toBe('/alice/Pictures');
});
it('reads ~ as the home folder', () => {
expect(resolveTypedPath('~', places)).toBe('/alice');
expect(resolveTypedPath('~/Desktop', places)).toBe('/alice/Desktop');
});
it('starts a relative path from the folder on screen', () => {
expect(resolveTypedPath('Work/2026', places)).toBe('/alice/Documents/Work/2026');
expect(resolveTypedPath('../Videos', places)).toBe('/alice/Videos');
expect(resolveTypedPath('/alice/./Documents/../Public', places)).toBe('/alice/Public');
});
it('starts a relative path from home in the Shared view', () => {
expect(resolveTypedPath('Desktop', { ...places, currentPath: 'puter://shared' })).toBe('/alice/Desktop');
});
it('keeps the Shared view\'s path', () => {
expect(resolveTypedPath('puter://shared/', places)).toBe('puter://shared');
});
it('leads nowhere when nothing was typed', () => {
expect(resolveTypedPath(' ', places)).toBeUndefined();
});
it('a ~ inside a name is just a character', () => {
expect(resolveTypedPath('~notes', places)).toBe('/alice/Documents/~notes');
});
});
+72
View File
@@ -4421,6 +4421,47 @@ body.myapps-reordering .myapps-tile {
overflow-x: auto;
white-space: nowrap;
scrollbar-width: none;
/* The row's empty space is the bar's, so a click there can open the
path for typing (see openPathEditor). */
flex: 1 1 auto;
align-self: stretch;
cursor: text;
}
/* Keeps a click target past the last crumb when a deep path fills the row. */
.dashboard-section-files .header .path-breadcrumbs:not(:empty)::after {
content: '';
flex: 0 0 24px;
align-self: stretch;
}
.dashboard-section-files .header .path-editor {
display: none;
flex: 1 1 auto;
min-width: 0;
height: 28px;
/* Clear of the action buttons by their own 10px gap */
margin: 0 10px;
padding: 0 8px;
box-sizing: border-box;
font-size: 13px;
border: 1px solid var(--select-color);
border-radius: 6px;
background: var(--dashboard-input-background);
color: var(--dashboard-text);
outline: none;
}
.dashboard-section-files .header .path-editor[aria-invalid="true"] {
border-color: var(--dashboard-danger-text);
}
.dashboard-section-files .header .path.path-editing .path-editor {
display: block;
}
.dashboard-section-files .header .path.path-editing .path-breadcrumbs {
display: none;
}
.dashboard-section-files .header .path-breadcrumbs::-webkit-scrollbar {
@@ -6096,6 +6137,26 @@ body.myapps-reordering .myapps-tile {
border-bottom: 1px solid var(--dashboard-border);
}
/* Sized as a crumb is, so the header doesn't jump as the field opens */
.dashboard-section-files .header .path-editor {
order: 1;
flex: 0 1 100%;
margin: 0 0 8px 0;
height: auto;
padding: 3px 8px;
line-height: inherit;
}
/* The crumbs' divider, kept while the field stands in for them */
.dashboard-section-files .header .path.path-editing::after {
content: '';
order: 1;
flex: 0 1 100%;
height: 1px;
margin-bottom: 8px;
background: var(--dashboard-border);
}
.dashboard-section-files .header .path-nav-buttons {
order: 2;
border-right: none;
@@ -6280,6 +6341,17 @@ body.myapps-reordering .myapps-tile {
min-height: 40px;
}
.dashboard-section-files .header .path-editor {
margin-left: 50px;
height: 40px;
/* Under 16px, iOS zooms the page in on focus. */
font-size: 16px;
}
.dashboard-section-files .header .path.path-editing::after {
margin-left: 50px;
}
/* Finger-sized crumbs, in that row */
@media (pointer: coarse) {
.dashboard-section-files .header .path .dirname {
+1
View File
@@ -283,6 +283,7 @@ const en = {
items_selected: '{{items}} selected',
folder_empty: 'No files in this directory.',
folder_open_failed: 'This folder couldn\'t be opened.',
path_not_found: 'Couldn\'t find "%%". Check the path and try again.',
reserved_name_not_allowed: 'The name "{{name}}" is not allowed, because it is a reserved name. Please choose another name.',
upload_some_failed: 'Some uploads failed:',
upload_to_trash_not_allowed: 'Uploading to trash is not allowed!',