File size: 6,439 Bytes
7c1820c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// ---------------------------------------------------------------------------
// 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 (
    <svg className="shell-nav-icon" viewBox="0 0 16 16" aria-hidden="true">
      <ellipse cx="8" cy="3.6" rx="5.3" ry="2.1" />
      <path d="M2.7 3.6v8.8c0 1.16 2.37 2.1 5.3 2.1s5.3-.94 5.3-2.1V3.6" />
      <path d="M2.7 8c0 1.16 2.37 2.1 5.3 2.1S13.3 9.16 13.3 8" />
    </svg>
  );
}

/**
 * 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 (
    <div className="shell-db-head">
      <span className={dbChipClass(icon)} aria-hidden="true">
        {/* The database's own mark when it has one, the cylinder when it does not β€”
            the same pair the rail row draws, so the header and the nav agree. Both
            paint in the chip's ink (the stylesheet's two overrides): `FolderMark`
            would otherwise stroke its pastel `-deep`, which is measured against the
            WHITE rail and disappears on its own tone.

            WAVE 23 C13 β€” `glyph` overrides only the FALLBACK, never a chosen mark, and
            that ordering is the contract. Automation is not a database and must not wear
            the cylinder; but it CAN wear a `nav_meta` icon (the rail already draws one,
            Shell:1026), and a header that ignored it would be the one surface where the
            rail and the frame disagreed about the same row. So: chosen icon > caller's
            glyph > the cylinder. */}
        {icon ? <FolderMark icon={icon} size={16} /> : (glyph ?? <DbIcon />)}
      </span>
      {/* An `h1`, not a styled span: this is the first time the work surface has NAMED itself,
          and the name of the thing you are looking at is what a heading is for. Every other
          full-pane surface in this shell (`shell-placeholder`) already uses one, and they never
          render together β€” so the page gains a heading rather than a second one. */}
      <h1 className="shell-db-name">{label}</h1>
      {aside ?? null}
    </div>
  );
}