diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 88a51850..7a85a064 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -78,16 +78,29 @@ export default tseslint.config( "no-restricted-imports": [ "error", { + // NOTE the String.raw: these are REGEX sources, and in a plain "..." literal `\.` is an + // unknown escape that collapses to a bare `.` — the any-char wildcard. The first cut of + // this rule shipped that way and matched far less than it read like (E10 review round 1). patterns: [ { // `../X` (exactly one level up = a feature component). A `regex` rather than a glob: // eslint matches `patterns` gitignore-style, where `../*` also swallows `../../lib/x`. - regex: "^\.\./(?!\.\./)", + regex: String.raw`^\.\./(?!\.\./)`, message: "components/ui/ is domain-free: a platform component may not import a feature component. Invert the dependency (take a prop/render-prop) or keep the component outside ui/.", }, { - regex: "^\.\./\.\./lib/api/", + // The same file by its OTHER spelling. `../../components/Feed` resolves exactly where + // `../Feed` does, so the rule above is only half the boundary; `ui/` is carved out + // because a sibling primitive is the one thing this path may legitimately name. + regex: String.raw`^\.\./\.\./components/(?!ui/)`, + message: + "components/ui/ is domain-free: a platform component may not import a feature component. Invert the dependency (take a prop/render-prop) or keep the component outside ui/.", + }, + { + // `(/|$)` so the BARREL counts too: `../../lib/api` is the spelling every feature + // component uses, and a `/`-anchored pattern let exactly that one through. + regex: String.raw`^\.\./\.\./lib/api(/|$)`, message: "components/ui/ is domain-free: a platform component may not call a domain API slice. Take the data as props.", }, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9a0f239b..7fdb2b6e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -59,9 +59,6 @@ export default function App() { const { t, i18n } = useTranslation(); const { page, channelView, ytSearch } = useNavigation(); const { setPage, enterYtSearch } = useNavigationActions(); - // The `global` key scope, bound once for the whole app: `h` opens the cheat sheet, Mod+K the - // jump-to palette (both rendered at the bottom of this tree). - useGlobalKeymap(); // An emailed verification link is confirmed HERE, above the signed-in/signed-out branch below, so // it works in a browser that already holds a session. It used to be handled inside Welcome, which // that branch never renders when signed in — the token was silently dropped. Signed out, the @@ -140,6 +137,11 @@ export default function App() { refetchInterval: (query) => (query.state.data ? 60_000 : false), }); + // The `global` key scope: `h` opens the cheat sheet, Mod+K the jump-to palette. Bound only once + // signed in, because that is where both overlays are mounted (bottom of this tree) — see + // useGlobalKeymap on why binding it above the signed-out branch is worse than not binding it. + useGlobalKeymap(!!meQuery.data); + // Pin this tab to whatever account it first loaded as. Without this, a tab that never picked // an account (it was riding the session's default) would silently swap identity when ANOTHER // tab changes the default — e.g. that other tab adds/switches an account. Once pinned, this diff --git a/frontend/src/components/Messages.tsx b/frontend/src/components/Messages.tsx index b61695bc..687b7661 100644 --- a/frontend/src/components/Messages.tsx +++ b/frontend/src/components/Messages.tsx @@ -214,6 +214,9 @@ function Directory({ onPick, onBack }: { onPick: (u: MessageUser) => void; onBac