- The Downloads module, its rail icon and badge are gone. A bookmark, the remembered page and an old
history entry that name it land on Files (urlState.resolvePage); `g d` goes to Files too.
- Users page: a Storage tab — the disk in numbers, every user's use against their own limit with
their file count, every user's downloads; aligned tables, one menu per row (edit / reset quota,
open source, copy, the owner's quota). The admin storage rows carry quota + file count, the admin
job list its owner.
- Settings: a Downloads tab — the download formats in one list (duplicate a built-in, edit or delete
your own) and the site sign-ins, moved from Account.
- "View" on an added download, a clip or a converting upload opens the tray on that row (unfolded,
back if closed; a finished one opens as a file; under a player it opens without taking focus).
- The download dialog opens on the format used last.
- DataTable: `pager={false}` for a short list, `cardRow` to give a lone "…" its own card line.
- Welcome and README speak of Files; a new Files screenshot.
106 lines
4.8 KiB
TypeScript
106 lines
4.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { readSources } from "../../lib/rawSources.testkit";
|
|
|
|
// docs/DESIGN.md "Text stays selectable": a click surface whose text is a title or a name (a card, a
|
|
// row, a person, a channel, a playlist, a log line) is `<Pressable selectable`, so its words select and
|
|
// the click that ends selecting them does nothing. A Pressable that is NOT selectable is a control whose
|
|
// text is its own label or that has no text at all (a menu row, an option, a tab, a nav item, a
|
|
// picture, a switch). Which one a new surface is cannot be read from the code, so every plain one is
|
|
// pinned here with the reason: a new Pressable, or one that lost its flag, fails this test until
|
|
// someone decides.
|
|
|
|
const files = readSources(__dirname, [
|
|
"../**/*.tsx",
|
|
"!../**/*.test.*",
|
|
"!../design-lab/**",
|
|
]) as Record<string, string>;
|
|
|
|
/** file → [how many plain Pressables, why they are not titles]. */
|
|
const PLAIN: Record<string, [number, string]> = {
|
|
"DownloadTray.tsx": [
|
|
2,
|
|
"the folded pill (its summary is its label); a failure's reason, which opens its details",
|
|
],
|
|
"FileCard.tsx": [1, "a card's picture (named by aria-label; its title is the caption)"],
|
|
"FilesRail.tsx": [2, "the load-error retry; the New-folder command"],
|
|
"Home.tsx": [
|
|
2,
|
|
"the hero's picture and a shelf tile's picture (each named by aria-label; its title sits outside)",
|
|
],
|
|
"LanguageSwitcher.tsx": [1, "an option row"],
|
|
"NavSidebar.tsx": [4, "the account trigger and its menu rows"],
|
|
"PaletteLauncher.tsx": [2, "launcher controls"],
|
|
"PlaylistsRail.tsx": [1, "the load-error retry"],
|
|
"PlexCollectionEditor.tsx": [1, "a checkbox row in a picker"],
|
|
"PlexInfo.tsx": [1, "the rating filter (a number)"],
|
|
"PlexPlayerSettings.tsx": [1, "the clock's date-style options"],
|
|
"PlexPlaylistAdd.tsx": [1, "a checkbox row in a picker"],
|
|
"PlexPlaylistView.tsx": [1, "a show's poster stack (aria-hidden)"],
|
|
"PlexSearchBox.tsx": [1, "a listbox option"],
|
|
"plexCards.tsx": [1, "a card's picture (named by aria-label; its title is the caption)"],
|
|
"Scheduler.tsx": [1, "the interval editor trigger"],
|
|
"SettingsPanel.tsx": [1, "a colour swatch"],
|
|
"SyncStatus.tsx": [1, "the status pill trigger"],
|
|
"TagManager.tsx": [1, "a suggestion in a picker"],
|
|
"UploadDialog.tsx": [1, "the drop zone"],
|
|
"VideoEditor.tsx": [2, "timeline glyphs"],
|
|
"Welcome.tsx": [1, "a screenshot (zoom)"],
|
|
"playlistColumns.tsx": [1, "the thumbnail"],
|
|
"plexDetailUi.tsx": [2, "a switch row and the poster"],
|
|
"plexViews.tsx": [1, "the rating filter (a number)"],
|
|
"ui/ActionMenu.tsx": [1, "a menu row"],
|
|
"ui/BackToTop.tsx": [1, "a control"],
|
|
"ui/CommandPalette.tsx": [1, "a command row"],
|
|
"ui/NavRail.tsx": [
|
|
2,
|
|
"the action rows (About, Sign out) and the group row that opens its panel; a page row is a link",
|
|
],
|
|
"ui/PanelGroup.tsx": [2, "a group's collapse toggle and its label"],
|
|
"ui/SidePanel.tsx": [2, "panel controls"],
|
|
"ui/tableHeader.tsx": [1, "a column's sort header"],
|
|
};
|
|
|
|
function scan(): { plain: Record<string, number>; selectable: number } {
|
|
const plain: Record<string, number> = {};
|
|
let selectable = 0;
|
|
for (const [path, code] of Object.entries(files)) {
|
|
// Glob keys are relative to this folder: `./x.tsx` is ui/, `../x.tsx` is components/.
|
|
const rel = path.startsWith("./") ? `ui/${path.slice(2)}` : path.replace(/^\.\.\//, "");
|
|
for (const m of code.matchAll(/<Pressable\b/g)) {
|
|
if (/\sselectable\b(?!=)/.test(openingTag(code, m.index))) selectable++;
|
|
else plain[rel] = (plain[rel] ?? 0) + 1;
|
|
}
|
|
}
|
|
return { plain, selectable };
|
|
}
|
|
|
|
/** The opening tag starting at `from`: up to its `>` outside any `{…}` expression, quoted string or
|
|
* line comment (an arrow's `=>` sits inside braces, a class like `[&>svg]` inside quotes). */
|
|
function openingTag(code: string, from: number): string {
|
|
let depth = 0;
|
|
let quote: string | null = null;
|
|
for (let i = from + 1; i < code.length; i++) {
|
|
const c = code[i];
|
|
if (quote) {
|
|
if (c === quote) quote = null;
|
|
} else if (c === "/" && code[i + 1] === "/") {
|
|
i = code.indexOf("\n", i); // a comment between the props: its apostrophes are not quotes
|
|
if (i < 0) break;
|
|
} else if (c === '"' || c === "'" || c === "`") quote = c;
|
|
else if (c === "{") depth++;
|
|
else if (c === "}") depth--;
|
|
else if (c === ">" && depth === 0) return code.slice(from, i + 1);
|
|
}
|
|
return code.slice(from);
|
|
}
|
|
|
|
describe("every Pressable decides whether its text is a title", () => {
|
|
it("finds the selectable ones, and no plain one outside the pinned list", () => {
|
|
const { plain, selectable } = scan();
|
|
// A scan that stopped matching would be green over nothing.
|
|
expect(selectable).toBeGreaterThan(15);
|
|
const pinned = Object.fromEntries(Object.entries(PLAIN).map(([f, [n]]) => [f, n]));
|
|
expect(plain).toEqual(pinned);
|
|
});
|
|
});
|