- A library past the list's cap is searched and sorted on the server; the count says "the newest". - Each folder and each search opens on its first page (DataTable pageScope, useCardPager scope). - A file dropped where the page takes none never reaches the browser (which would unload the app). - The viewer's arrows wait while a layer opened from it is on top, and scroll a wide text line; a text shows its first 5000 lines and says where the rest is. - Confirms name the file (no more "your downloads"); the asking menu rows end in "…". - The tray names a running step beside its bar, shows a pause mark for paused work, polls slowly while only paused work waits, and forgets a finished file that was deleted. - An unchanged rename sends nothing; a paste opens the download dialog only with nothing on top; the link box never replaces what was typed. - Honest states: folders that failed to load, a file no longer there, the storage figure; the rail says "nothing downloaded" only when there is nothing. - Focus after deleting the open folder and when the format form opens; the format name's label. - ui/Meter (one danger line for both quota bars) and ui/RenameField in the platform layer. - Copy fixes, stale comments and the speculative hub key; a contract test for the list's parameters; e2e: the Files page and the paste intake.
55 lines
2.7 KiB
TypeScript
55 lines
2.7 KiB
TypeScript
import { ensureNavId, peekNavId } from "./history";
|
|
import { anyBlockingLayer } from "./layerStack";
|
|
import { createStore } from "./store";
|
|
|
|
// Downloading a link is an action you start anywhere: the Files page's link box, a link pasted onto
|
|
// any page, the command palette. They all hand the link to ONE app-wide download dialog
|
|
// (components/DownloadIntake), so none of them mounts a dialog of its own.
|
|
|
|
const pending = createStore<string | null>(null);
|
|
|
|
/** An http(s) link, alone — what a paste or the link box may download; anything else is not one. */
|
|
export function asDownloadLink(text: string | null | undefined): string | null {
|
|
const s = text?.trim() ?? "";
|
|
return /^https?:\/\/\S+$/i.test(s) ? s : null;
|
|
}
|
|
|
|
/** Whether a paste here is the page's to take: not typed into a field, and nothing on top that owns
|
|
* the keyboard — a dialog, a player, an open menu (the reader is busy with that, and a link pasted
|
|
* at it is not a request to download). The layer stack knows them all; a player marks no
|
|
* `aria-modal`, so the markup alone would miss it. */
|
|
export function pasteIsForThePage(target: EventTarget | null): boolean {
|
|
const el = target instanceof Element ? target : null;
|
|
if (el?.closest("input, textarea, select, [contenteditable]:not([contenteditable='false'])"))
|
|
return false;
|
|
return !anyBlockingLayer() && !document.querySelector("[aria-modal='true']");
|
|
}
|
|
|
|
/** Ask to download `url`: the app's download dialog opens on it. */
|
|
export const requestDownload = (url: string): void => pending.set(url);
|
|
|
|
/** The link the dialog is open on (null = closed). DownloadIntake only. */
|
|
export const usePendingDownload = (): string | null => pending.useValue();
|
|
|
|
/** The dialog closed, downloading or not. */
|
|
export const clearPendingDownload = (): void => pending.set(null);
|
|
|
|
// The palette's "Download from link…" takes the reader to the Files page's link box. A token for the
|
|
// history entry the navigation made (as lib/homeIntent's): Files is a lazy module, often not mounted
|
|
// when it is asked, and a request that outlived its entry must not grab the focus on a later visit.
|
|
const linkBox = createStore<number | null>(null);
|
|
|
|
/** Ask the Files page to focus its link box on THIS entry — `setPage("files", requestLinkBox)`. */
|
|
export const requestLinkBox = (): void => linkBox.set(ensureNavId());
|
|
|
|
/** The pending request (null = none) — the render subscription that wakes the link box. */
|
|
export const useLinkBoxRequest = (): number | null => linkBox.useValue();
|
|
|
|
/** Take the request, clearing it; true only on the entry it was made for. */
|
|
export const takeLinkBox = (): boolean => {
|
|
const entry = linkBox.get();
|
|
if (entry === null) return false;
|
|
linkBox.set(null);
|
|
return entry === peekNavId();
|
|
};
|