// --------------------------------------------------------------------------- // shell/dbFrame.tsx — THE DATABASE FRAME, extracted so a SECOND surface can wear it. // // ⭐ WHY THIS FILE EXISTS. The owner's instruction for the Query module is one // sentence: *"the query module should exactly BE looking like the database module, // COMPLETELY, with all the views etc. The only difference is that user isn't the one // creating the view, it's the AI that they prompt in the AI assistant module."* // // "Exactly" and "completely" are only satisfiable by mounting THE SAME frame and THE // SAME grid — `shell-db-frame` + `DbHead` + `OverlayProvider` + `CustomerGrid`. A // hand-rolled view list beside it would be a second, thinner answer to a question this // product already answers. So the header and the topic→scope map moved OUT of // `Shell.tsx` (where they were private) and into here, and both call sites import them. // // ⛔ `gridScopeFor` IS THE WHOLE REASON THIS IS NOT JUST A COMPONENT MOVE. A registry // key and a grid scope are DIFFERENT SPELLINGS of the same database — `product_data` is // the key, `product` is the scope; `customer_data` is the key, `customer` is the scope; // a `ut_` key is its own scope. That map lived inline in one branch of `Shell.tsx`, and // the Query module's saved rows carry the KEY (`routes_query.BUILTIN_SCOPES = // ('customer_data', 'product_data')`). So a second inline copy — or worse, passing the // key straight through — is not a tidiness question: // · `CustomerGrid`'s VIEW_OPEN listener guards on `detail.topic !== scope`, i.e. the // SCOPE spelling, and drops anything else SILENTLY (CustomerGrid.tsx:3151); // · so an emit carrying `customer_data` at a grid mounted as `customer` never selects // the view, and the click "worked". // One question, one normaliser ([[one-question-two-normalizers]]). // --------------------------------------------------------------------------- import type { ReactNode } from "react"; import { FolderMark } from "../customer-grid/icons"; import type { SurfaceScope } from "../customer-grid/apiBridge"; import type { FolderIcon } from "../customer-grid/types"; import { dbChipClass } from "./nav"; /** The cylinder every database wears when it has never been given a mark. */ export function DbIcon() { return ( ); } /** * A nav/registry KEY → the scope `CustomerGrid` is mounted with. * * ⛔ THE ONE PLACE THIS MAP IS WRITTEN. See the header: the two spellings are silently * interchangeable-looking and are not interchangeable, and the failure they produce is a * dropped event rather than an error. * * ⚠ `cohort` is a legal `SurfaceScope` and is deliberately NOT a case here: the `#/cohort` * route left with its registry row in wave 16 (cohorts project as LOCKED VIEWS in the * Customer rail), so no nav key resolves to it. The scope stays reachable server-side; it * simply is not something a nav row names any more. */ export function gridScopeFor(key: string): SurfaceScope { if (key === "product_data") return "product"; if (key.startsWith("ut_")) return key as `ut_${string}`; return "customer"; } /** * WAVE 20 item 4 (R8 / C-ADDROW) — THE UNIVERSAL DATABASE HEADER. * * One header, identical on every database (`reference/Airtable 6.png`): a chip in * a bold colour carrying the database's mark, then its name. It replaces * `shell-ut-bar`, which existed on USER TABLES only — so the two built-in * databases had no header at all, and the one place the product named the thing * you were looking at appeared or vanished depending on where the table came * from. R8 makes it universal. * * The "Add record" button the old bar carried is GONE with it, deliberately: R8 * replaces it with the grid's own trailing "+" row (S3's half of C-ADDROW), on * user databases only — a connector-backed table's rows are read-synced, and a * "+" that must refuse is a fake affordance. * * The chip is decorative and says so: the name beside it is real text, so a * second announcement of the same fact is noise to a screen reader. * * ⭐ `aside` (added with this file's extraction) is the ONE addition, and it changes * nothing for the existing caller, which passes none. The Query module needs a control in * this row — you cannot look at a database without saying WHICH — and putting it here is * what keeps that surface the same frame rather than a frame with a strip bolted above it. */ export function DbHead({ label, icon, glyph, aside }: { label: string; icon?: FolderIcon; glyph?: ReactNode; aside?: ReactNode; }) { return (