Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,8 @@ read or change the production filesystem.
It reads the console document from that URL and serves its download fixture over
loopback HTTP, verifying the ZIP bytes saved by Chromium. API writes stay mocked;
native attachments bypass Playwright response interception.

`node scripts/files-change-ui-audit.js URL KEY` also covers selected Trash
actions and readable labels, managed/read-only controls, unlock/relock, blocked
keyboard shortcuts, and the shared Kit menu’s disabled styles, click guards and
keyboard focus. Its file changes stay in synthetic in-memory fixtures.
63 changes: 48 additions & 15 deletions docker/setup/client/files.js

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions docker/setup/client/kit.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,14 +316,15 @@ const Kit = (() => {
// ---------------------------------------------------------------- menus --
const menuItems = (items) => items.map((item, i) => item.sep
? html`<hr class="tc-menu-sep">`
: html`<button class="${cx('tc-menu-item', item.danger && 'tc-menu-item--danger')}" role="${item.radio ? 'menuitemradio' : item.checkbox ? 'menuitemcheckbox' : 'menuitem'}"${item.radio || item.checkbox ? html` aria-checked="${String(Boolean(item.checked))}"` : ''} type="button" data-menu-index="${i}"${item.disabled ? raw(' disabled') : ''}>${item.radio || item.checkbox ? icon('check', 'tc-menu-check') : icon(item.icon)}<span class="tc-menu-item-text">${item.description ? html`<strong>${item.label}</strong><span class="tc-menu-item-description">${item.description}</span>` : item.label}</span>${item.shortcut ? html`<span class="tc-menu-item-shortcut">${item.shortcut}</span>` : ''}</button>`);
: html`<button class="${cx('tc-menu-item', item.danger && 'tc-menu-item--danger')}" role="${item.radio ? 'menuitemradio' : item.checkbox ? 'menuitemcheckbox' : 'menuitem'}"${item.radio || item.checkbox ? html` aria-checked="${String(Boolean(item.checked))}"` : ''} type="button" data-menu-index="${i}"${item.disabled ? raw(' disabled aria-disabled="true"') : ''}>${item.radio || item.checkbox ? icon('check', 'tc-menu-check') : icon(item.icon)}<span class="tc-menu-item-text">${item.description ? html`<strong>${item.label}</strong><span class="tc-menu-item-description">${item.description}</span>` : item.label}</span>${item.shortcut ? html`<span class="tc-menu-item-shortcut">${item.shortcut}</span>` : ''}</button>`);

const wireMenu = (layer, items) => {
const buttons = () => [...layer.panel.querySelectorAll('.tc-menu-item:not([disabled])')];
const buttons = () => [...layer.panel.querySelectorAll('.tc-menu-item:not([disabled]):not([aria-disabled="true"])')];
layer.panel.addEventListener('click', (e) => {
const button = e.target.closest('[data-menu-index]');
if (button) {
const item = items[Number(button.getAttribute('data-menu-index'))];
if (button.disabled || button.getAttribute('aria-disabled') === 'true' || item?.disabled) return;
layer.close('select');
if (item && item.run) item.run();
return;
Expand Down Expand Up @@ -358,16 +359,17 @@ const Kit = (() => {
const titleId = 'menu-title-' + Math.random().toString(36).slice(2, 8);
const layer = open({
kind: 'bottom',
panel: { tag: 'aside', class: 'tc-sheet tc-sheet--bottom', role: 'menu', 'aria-labelledby': titleId },
panel: { tag: 'aside', class: 'tc-sheet tc-sheet--bottom tc-sheet--menu', role: 'menu', 'aria-labelledby': titleId },
returnTo: trigger,
render: () => html`
<div class="tc-sheet-head tc-menu-head">${head && head.tile ? head.tile : ''}<div class="tc-menu-head-text"><h2 class="tc-sheet-title" id="${titleId}">${(head && head.title) || label}</h2>${head && head.sub ? html`<span class="tc-small tc-muted">${head.sub}</span>` : ''}</div></div>
<div class="tc-sheet-actions">${status || ''}${menuItems(items)}${footer ? html`<div class="tc-menu-foot">${footer}</div>` : ''}</div>
<div class="tc-sheet-actions">${status || ''}${menuItems(items)}</div>
${footer ? html`<div class="tc-menu-foot">${footer}</div>` : ''}
<div class="tc-sheet-cancel"><button class="tc-btn tc-btn--block tc-btn--lg" type="button" data-menu-cancel>Cancel</button></div>`,
backdrop: 'close',
});
wireMenu(layer, items);
const first = layer.panel.querySelector('.tc-menu-item');
const first = layer.panel.querySelector('.tc-menu-item:not([disabled]):not([aria-disabled="true"])');
if (first) first.focus({ preventScroll: true });
return layer;
}
Expand Down Expand Up @@ -397,7 +399,7 @@ const Kit = (() => {
layer.panel.style.left = left + 'px';
layer.panel.style.top = topPx + 'px';
wireMenu(layer, items);
const first = layer.panel.querySelector('.tc-menu-item');
const first = layer.panel.querySelector('.tc-menu-item:not([disabled]):not([aria-disabled="true"])');
if (first) first.focus({ preventScroll: true });
return layer;
};
Expand Down
36 changes: 22 additions & 14 deletions docker/setup/console.css
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,11 @@ html.tc-locked { overflow: hidden; }
.tc-sheet-actions .tc-menu-item { min-height: 40px; font-size: 16px; padding: 4px 8px; }
.tc-sheet-actions .tc-menu-sep { margin: 6px 8px; }
.tc-sheet-cancel { padding: 8px 16px calc(16px + env(safe-area-inset-bottom, 0px)); }
.tc-sheet--menu { overflow: hidden; }
.tc-sheet--menu > .tc-menu-head, .tc-sheet--menu > .tc-sheet-cancel, .tc-sheet--menu > .tc-menu-foot { flex: none; }
.tc-sheet--menu > .tc-sheet-actions { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.tc-sheet--menu > .tc-sheet-actions > * { flex: none; }
.tc-sheet--menu > .tc-menu-foot { margin: 0 20px; padding: 8px 0; overflow-wrap: anywhere; }
.tc-sheet .tc-sheet-foot-note { margin-right: auto; font-size: 12px; color: var(--fg-muted); display: inline-flex; gap: 6px; align-items: center; }
.tc-layer[data-closing] > * { animation: tc-fade-out 140ms ease-in forwards !important; }

Expand Down Expand Up @@ -2292,14 +2297,15 @@ button.tc-dv-hunk { width: 100%; border-left: 0; border-right: 0; cursor: pointe
.tc-fl-row > .tc-fmenu { opacity: 0; justify-self: end; }
.tc-fl-row:hover > .tc-fmenu, .tc-fl-row[aria-selected="true"] > .tc-fmenu, .tc-fl-row[data-hover] > .tc-fmenu { opacity: 1; }

/* Selection replaces the column header with what you can do to it. */
.tc-fl-selbar { display: grid; grid-template-columns: 32px minmax(0, 1fr) auto; align-items: center; column-gap: 12px; min-height: 44px; padding: 0 8px 0 10px; border-bottom: 1px solid color-mix(in srgb, var(--accent) 30%, var(--border));
/* Selection summary and actions share a row, wrapping as groups when needed. */
.tc-fl-selbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; min-height: 44px; padding: 8px 10px; border-bottom: 1px solid color-mix(in srgb, var(--accent) 30%, var(--border));
background: color-mix(in srgb, var(--accent) 6%, var(--surface)); border-radius: calc(var(--radius-xl) - 1px) calc(var(--radius-xl) - 1px) 0 0; }
[data-theme="dark"] .tc-fl-selbar { background: color-mix(in srgb, var(--accent) 14%, var(--surface)); }
.tc-fl-selbar > div:nth-child(2) { display: flex; align-items: baseline; gap: 8px; font-size: 13px; }
.tc-fl-selbar > div:nth-child(2) strong { font-weight: 500; }
.tc-fl-selbar > div:nth-child(2) span { color: var(--fg-muted); font-size: 12.5px; }
.tc-fl-selbar > div:last-child { display: flex; align-items: center; gap: 4px; }
.tc-fl-selbar > .tc-checkbox { flex: none; }
.tc-files-selection-summary { display: flex; flex: 1; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; min-width: 0; font-size: 13px; }
.tc-files-selection-summary strong { font-weight: 500; white-space: nowrap; }
.tc-files-selection-summary span { color: var(--fg-muted); font-size: 12.5px; }
.tc-files-selection-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 4px; max-width: 100%; margin-left: auto; }

/* Footer of the list: counts, hidden items, scan time. */
.tc-fl-foot { display: flex; align-items: center; gap: 8px; min-height: 40px; padding: 0 12px 0 16px; border-top: 1px solid var(--border); font-size: 12px; color: var(--fg-muted);
Expand Down Expand Up @@ -2520,8 +2526,6 @@ button.tc-dv-hunk { width: 100%; border-left: 0; border-right: 0; cursor: pointe

/* ------------------------------------------------------- menus, etc --- */
.tc-ctx { position: absolute; z-index: 40; width: 260px; }
.tc-menu-item[aria-disabled="true"] { color: var(--fg-muted); cursor: default; }
.tc-menu-item[aria-disabled="true"] > .tc-icon { opacity: 0.5; }
.tc-menu-item .tc-menu-lock { margin-left: auto; width: 12px; height: 12px; color: var(--fg-subtle); }
.tc-menu-item .tc-kbd { margin-left: auto; height: 18px; min-width: 18px; font-size: 11px; background: transparent; border: 0; box-shadow: none; color: var(--fg-muted); padding: 0 2px; letter-spacing: 0.04em; }
.tc-menu-item--danger .tc-kbd { color: color-mix(in srgb, var(--danger-text) 70%, transparent); }
Expand Down Expand Up @@ -2581,6 +2585,7 @@ button.tc-dv-hunk { width: 100%; border-left: 0; border-right: 0; cursor: pointe
.tc-prow-file[aria-selected="true"] { background: color-mix(in srgb, var(--accent) 8%, transparent); }
.tc-prow-file .tc-checkbox { width: 22px; height: 22px; margin: 0; border-radius: 6px; }
.tc-prow-file .tc-checkbox:checked { background-size: 14px; }
#files-browser .tc-prow-file > .tc-btn { min-width: 44px; min-height: 44px; }
.tc-pselbar { flex: none; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; padding: 8px 8px 26px; border-top: 1px solid var(--border); background: color-mix(in srgb, var(--bg) 86%, transparent); -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px); }
.tc-pselbar button { display: flex; flex-direction: column; align-items: center; gap: 4px; height: 52px; justify-content: center; border: 0; border-radius: 10px; background: none; color: var(--fg); font: inherit; font-size: 12px; }
.tc-pselbar button .tc-icon { width: 20px; height: 20px; }
Expand Down Expand Up @@ -2725,7 +2730,15 @@ button.tc-dv-hunk { width: 100%; border-left: 0; border-right: 0; cursor: pointe
.tc-files-crumbs > button { flex: none; }
.tc-files-crumbs > button:last-child { overflow: hidden; text-overflow: ellipsis; }
.tc-files-phone-body { padding: 0 16px; }
.tc-files-phone-selection { position: fixed; bottom: 0; left: 0; right: 0; z-index: var(--z-sticky); display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid var(--border); background: var(--surface-raised); box-shadow: var(--shadow-popup); font-size: 14px; }
.tc-files-phone-selection { position: fixed; bottom: 0; left: 0; right: 0; z-index: var(--z-sticky); display: flex; flex-direction: column; gap: 4px; padding: 8px 8px calc(8px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid var(--border); background: var(--surface-raised); box-shadow: var(--shadow-popup); font-size: 14px; }
.tc-files-phone-selection-head { display: flex; align-items: center; justify-content: space-between; min-height: 44px; }
.tc-files-phone-selection-head strong { font-size: 12px; font-weight: 500; }
#files-browser .tc-files-phone-selection-head .tc-btn { min-height: 44px; }
.tc-files-phone-actions.tc-pselbar { grid-template-columns: repeat(auto-fit,minmax(60px,1fr)); padding: 0; border: 0; background: none; -webkit-backdrop-filter: none; backdrop-filter: none; }
#files-browser .tc-files-phone-actions > .tc-btn { width: 100%; min-width: 0; height: 52px; padding: 0 4px; box-shadow: none; }
#files-browser .tc-files-phone-actions > .tc-btn .tc-icon { flex: none; }
#files-browser .tc-files-phone-actions > .tc-btn:disabled { color: var(--fg-muted); }
@media (max-width:639.98px) { body[data-console-route="files"]:has(.tc-files-phone-selection) .tc-toaster { bottom: calc(132px + env(safe-area-inset-bottom, 0px)); } }
.tc-files-detail-sheet { max-height: 90dvh; overflow-y: auto; }
.tc-files-detail-sheet .tc-fd-head { padding-top: 24px; }
@media (min-width: 640px) and (max-width: 1023.98px) { #files-browser .tc-fsearch { flex-basis: 180px; } #files-browser .tc-fl--projects { --fl-cols: 32px minmax(0, 1fr) 150px 80px 72px 32px; } }
Expand Down Expand Up @@ -3036,8 +3049,6 @@ body[data-console-route="files"]:has(.tc-layer) .tc-toaster { visibility: hidden
.tc-files-archive-preview li { display: flex; align-items: center; gap: 8px; font-size: 12px; }
.tc-files-archive-preview li .tc-icon { width: 15px; height: 15px; color: var(--fg-muted); }
.tc-files-archive-preview code { overflow-wrap: anywhere; }
#files-browser .tc-fl-selbar { grid-template-columns: 24px minmax(100px,1fr); padding: 8px 10px; row-gap: 8px; }
#files-browser .tc-fl-selbar > div:last-child { grid-column: 1 / -1; flex-wrap: wrap; }
.tc-storage-data-row:has(a) { grid-template-columns: minmax(0,1fr) auto auto; gap: 12px; }
@media(max-width:639.98px) {
:is(.tc-files-zip,.tc-files-extract,.tc-files-clean,.tc-files-delete) .tc-fdlg-head { padding: 14px 16px 4px; }
Expand All @@ -3051,9 +3062,6 @@ body[data-console-route="files"]:has(.tc-layer) .tc-toaster { visibility: hidden
.tc-files-extract .tc-input, .tc-files-delete .tc-input { min-height: 44px; font-size: 16px; }
.tc-files-delete .tc-risk { grid-template-columns: 18px minmax(0,1fr); padding: 12px; gap: 8px; }
.tc-files-delete .tc-risk > .tc-btn { grid-column: 2; justify-self: start; }
#files-browser .tc-files-phone-selection { flex-wrap: wrap; padding: 8px; gap: 4px; }
#files-browser .tc-files-phone-selection > strong { flex-basis: 100%; font-size: 12px; }
#files-browser .tc-files-phone-selection .tc-btn { padding-inline: 8px; }
.tc-storage-data-row:has(a) { grid-template-columns: minmax(0,1fr) auto; }
.tc-storage-data-row > a { grid-column: 1 / -1; justify-self: start; }
}
Expand Down
1 change: 1 addition & 0 deletions docker/setup/design/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@
.tc-menu-item .tc-kbd, .tc-menu-item-shortcut { margin-left: auto; font-size: 12px; color: var(--fg-muted); letter-spacing: 0.1em; }
.tc-menu-item--danger { color: var(--danger-text); }
.tc-menu-item--danger > .tc-icon { color: currentColor; }
.tc-menu-item:disabled, .tc-menu-item[aria-disabled="true"] { color: var(--fg-muted); opacity: var(--opacity-disabled); cursor: not-allowed; background: transparent; }
.tc-menu-sep { height: 1px; margin: 4px 0; background: var(--border); border: 0; }
.tc-menu-label { padding: 6px 8px 2px; font-size: 12px; font-weight: 500; color: var(--fg-muted); }

Expand Down
11 changes: 8 additions & 3 deletions docs/files.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ Trash is per filesystem, merged into one view. Workspace items use
Restore keeps any conflicting name by adding `(restored)`. Items expire after
seven days; disk pressure at 90% removes the oldest until usage is below 85%,
while preserving the first ten minutes for Undo. Empty Trash requires a fresh
confirmation. Other mounts have no implicit Trash.
confirmation. Other mounts have no implicit Trash. Select items for **Restore**
or **Remove selected…**; on a phone, use **Select items** in View. Trash selection
offers recovery and removal instead of file-editing or transfer actions.

Upload files, a folder, clipboard files or a desktop drop. At most three files
upload together in ordered 8 MiB chunks. Received data stays under a private
Expand All @@ -81,8 +83,11 @@ except on the line being edited. A stale save offers Discard mine, Save as a
copy, Overwrite theirs and Compare. Saves use a synced temporary file, preserve
the mode and publish by rename. The console never executes the edited file.

Managed folders begin locked. **Edit anyway** unlocks only the named managed
root until you leave it; **Lock again** ends that permission immediately.
Managed folders begin locked. Editing actions are muted while previews and
downloads stay available; the menu explains the restriction. **Edit anyway**
unlocks only the named managed root until you leave it; **Lock again** ends that
permission immediately. Phone selections and keyboard shortcuts respect the
same lock.
Sign-ins and keys have separate consequences. Process checks fail closed when
activity cannot be established. For a supervised T3 process, **Stop T3 Code
first** pauses it for the change and verifies that it restarts. Other processes
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/files-polish/phone-managed-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/files-polish/phone-trash-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/files-polish/trash-selection-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading