feat(files): folders managed in place, a grid per folder, drop to upload, open on one file

- Own folders are made, renamed and deleted from the rail (right click, long press,
  Shift+F10) or the "…" beside an open folder's name; any folder's retention is a
  submenu (7/30/90 days, until deleted). Deleting confirms where its files go.
- List or grid per folder, sharing one sort; the grid is the one media card.
- A file dropped anywhere on the page opens Upload with it, bound for the folder
  on screen.
- A link (?item=) or the upload notice opens Files on the file: its page turned
  to, the keyboard on it, a ring round it.
- An in-place name field left by Enter or Escape hands the focus back to what
  replaced it (the row, the entry, the "…") instead of dropping it on <body>.
- The table's saved page size is per account.
This commit is contained in:
2026-10-11 15:59:12 +02:00
parent 85aa917b38
commit d22c64f4dd
23 changed files with 1569 additions and 175 deletions
+3 -1
View File
@@ -17,6 +17,8 @@ export function useCardPager<T>(
/** Follow a row the caller just moved: each new `seq` turns to the page holding the row `match`
* finds — the same contract as `DataTable`'s `reveal`, for the card paint of the same list. */
reveal?: { seq: number; match: (row: T) => boolean } | null,
/** The size picker's name — what the cards ARE (the channel manager's are "cards"). */
sizeLabel?: string,
) {
const { t } = useTranslation();
const [size, setSize] = useState(() => parseCardSize(readAccount(sizeKey, null)));
@@ -58,7 +60,7 @@ export function useCardPager<T>(
writeAccount(sizeKey, s);
setPage(0);
},
sizeLabel: t("channels.cardsPerPage"),
sizeLabel: sizeLabel ?? t("channels.cardsPerPage"),
},
/** Call when the caller's own filters change — a different list starts at page one. */
resetPage: useCallback(() => setPage(0), []),