Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,41 @@ auto-generated per-PR notes; this file is the curated, human-readable history.
as #586 (`SurfaceLifecycle` + docked right-inspector slot), #587
(side-panel registry), #588 (composition-root decomposition), and #589
(dashboard gesture/repaint extraction).
- **`SurfaceLifecycle` (`src/ui/surface-lifecycle.ts`) + a docked
`inspectorHost` slot** (#586, phase 1 of the #593 refactor umbrella).
`.main-row` (`app-shell.ts`) gains a real, shell-owned `inspectorHost` +
`inspectorResize` handle as layout siblings of `queryHost`/`dashboardHost`
— never a `position: fixed` overlay. The cell-detail drawer, rows viewer,
and Reference pane (`results.ts`/`doc-pane.ts`) all now dock into it
through the shared `SurfaceLifecycle` open/close/Escape/focus-restore
primitive and `inspector-host.ts`'s singleton-slot manager, replacing three
independent hand-rolled lifecycles (`isTopDrawer`, the `.cd-backdrop` DOM
probes/CSS, and the docs pane's own bespoke resize/keydown wiring — all
deleted). `cellDrawerPx`/`docPanePx` collapse into one `rightInspectorPx`
preference (compat read order: `rightInspectorPx` → `docPanePx` →
`cellDrawerPx` → 480px default; single canonical write, and each candidate
is validated independently so a corrupt canonical value falls through to a
real legacy one instead of yielding `NaN`). Because the inspector is now a
layout sibling rather than an overlay, its width is clamped **dock-aware** —
the old flat 92vw ceiling could starve the centre surface once the panel
took real layout space, so the ceiling now also reserves a 320px minimum for
the centre (plus the sidebar and handles) and is recomputed whenever the
panel unfolds or the window resizes, not once at construction. The clamp
only ever changes the *displayed* width; the user's persisted preference is
never narrowed by it. Docked surfaces
are now non-modal (no keyboard-owner acquisition — the pre-#586 modal cell
drawer blocked every app shortcut while open; this issue's docked model
fixes that), so `app.ts`'s Query↔Dashboard surface transition and
sign-out/connection-scope teardown now close whichever surface currently
occupies the shared dock (`closeInspector`), not just Reference. One
deliberate behavior change: since the dock holds only one occupant at a
time, opening Cell while Rows is showing now REPLACES Rows instead of
stacking a second panel on top of it (#488, the next phase, owns
tool-registry/tab persistence semantics; not in scope here). The one
surviving non-docked case — a cell-detail drawer opened inside a real
detached browser tab (`results.ts`'s Data Pane) — keeps a self-contained
modal overlay (renamed `.cell-detail-overlay`), still built on
`SurfaceLifecycle`.

### Changed
- **The project wiki moved in-repo, as tracked `.wiki/`.** The maintainer/agent
Expand Down
9 changes: 5 additions & 4 deletions src/application/app-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ import { KEYS } from '../state.js';
* `save*` method on `App` (`saveJSON`/`saveVarValues`/`saveFilterActive`/…),
* untouched by this service. */
export type PreferenceKey =
| 'theme' | 'sidebarPx' | 'editorPct' | 'sideSplitPct' | 'cellDrawerPx'
| 'theme' | 'sidebarPx' | 'editorPct' | 'sideSplitPct'
| 'sidePanel' | 'resultRowLimit'
// #313 — the documentation pane's own persisted resize width, a sibling of
// cellDrawerPx (never shared with it — see splitters.ts's 'docPane' axis).
| 'docPanePx';
// #586 — the single canonical docked right-inspector width, replacing the
// former cellDrawerPx/docPanePx pair (see splitters.ts's 'rightInspector'
// axis and state.ts's compat-read `rightInspectorPx` comment).
| 'rightInspectorPx';

/** The one state field this service reads/writes (`toggleTheme` only) — a
* plain settable property, not a signal (matches `AppState.theme`). */
Expand Down
69 changes: 53 additions & 16 deletions src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,12 +353,19 @@ export interface AppState {
sidebarPx: number;
editorPct: number;
sideSplitPct: number;
cellDrawerPx: number;
/** The docs pane's own persisted resize width (#313) — a sibling of
* `cellDrawerPx`, read/written only by the 'docPane' splitter axis
* (splitters.ts) and `attachDrawerResize`'s `stateKey: 'docPanePx'` option
* (drawer.ts); never shared with the cell-detail/rows-viewer drawer. */
docPanePx: number;
/**
* The docked right-inspector's persisted width (#586) — one browser
* preference shared by every surface the shell mounts into `inspectorHost`
* (cell detail, rows viewer, Reference), replacing the two independent
* `cellDrawerPx`/`docPanePx` prefs each surface's own overlay used to read.
* Read/written only by the `'rightInspector'` splitter axis (splitters.ts)
* and app-shell.ts's own resize handle — never a per-surface key again.
* `createState`'s load is compatibility-ordered: a real `rightInspectorPx`
* wins, else a real `docPanePx`, else a real `cellDrawerPx` (both still
* read-only, never written again), else the default — so upgrading a
* browser that already had either old preference keeps it.
*/
rightInspectorPx: number;
tabs: Signal<QueryTab[]>;
activeTabId: Signal<string>;
schema: Signal<unknown[] | null>;
Expand Down Expand Up @@ -478,6 +485,14 @@ export const KEYS = {
sidebarPx: 'asb:sidebarPx',
editorPct: 'asb:editorPct',
sideSplitPct: 'asb:sideSplitPct',
/** #586 — the single canonical right-inspector width preference. Written
* only from app-shell.ts's shared resize handle / the detached cell-detail
* overlay's own drag handle (drawer.ts). */
rightInspectorPx: 'asb:rightInspectorPx',
/** #586 — retained ONLY as compat-read sources for `rightInspectorPx`
* (`createState`'s load order below); never written again, and no longer
* `AppState` fields of their own. Their literal strings are still a
* persisted-data contract (#459) — do not rename them. */
cellDrawerPx: 'asb:cellDrawerPx',
docPanePx: 'asb:docPanePx',
sidePanel: 'asb:sidePanel',
Expand Down Expand Up @@ -618,6 +633,22 @@ export function setTabSpecDraft(
export function createState(read: StateReader = { loadJSON, loadStr }): AppState {
const num = (key: string, dflt: number, lo: number, hi: number) =>
clamp(parseFloat(read.loadStr(key, String(dflt))), lo, hi);
// #586 finding 4: the compat-read precedence below needs each candidate
// parsed and validated INDEPENDENTLY, not chained with `||` — `||`
// short-circuits on any non-empty string, so a malformed canonical value
// (e.g. a corrupted `"bad"`) both blocks a perfectly valid legacy fallback
// AND survives as `NaN` through `clamp` (`Math.min(Math.max(NaN,320),
// Infinity)` is `NaN`), which the shell then applies as a literal
// `"NaNpx"` width. Returns the first candidate that parses to a finite
// number (whatever its magnitude — the caller's own `clamp` still bounds
// it), or `480` if none does.
const firstValidPx = (...raws: string[]): number => {
for (const raw of raws) {
const n = parseInt(raw, 10);
if (Number.isFinite(n)) return n;
}
return 480;
};
const storedQueries = decodeStoredSavedQueries(read.loadJSON(KEYS.saved, []));
const initialWorkspaceName = read.loadStr(KEYS.libraryName, DEFAULT_LIBRARY_NAME);
return {
Expand All @@ -632,16 +663,22 @@ export function createState(read: StateReader = { loadJSON, loadStr }): AppState
sidebarPx: clamp(parseInt(read.loadStr(KEYS.sidebarPx, '248'), 10), 180, 420),
editorPct: num(KEYS.editorPct, 45, 15, 85),
sideSplitPct: num(KEYS.sideSplitPct, 58, 25, 85),
// Cell-detail / rows-viewer drawer width (issue #101). The 92vw upper
// bound depends on the live viewport, not this load-time default, so only
// the floor is enforced here — clampDrawerWidth (splitters.js) applies the
// full [320, 92vw] clamp whenever the drawer is opened or resized.
cellDrawerPx: clamp(parseInt(read.loadStr(KEYS.cellDrawerPx, '560'), 10), 320, Infinity),
// The docs pane's own persisted width (#313) — same floor-only load-time
// clamp as cellDrawerPx above (clampDrawerWidth applies the full
// [320, 92vw] bound whenever the pane is opened/resized against the live
// viewport).
docPanePx: clamp(parseInt(read.loadStr(KEYS.docPanePx, '420'), 10), 320, Infinity),
// The docked right-inspector's width (#586). Compat read order: a real
// rightInspectorPx wins; else a real docPanePx (a pre-#586 Reference-pane
// width); else a real cellDrawerPx (a pre-#586 cell/rows drawer width);
// else the default (matches #488's RIGHT_INSPECTOR_DEFAULT_PX) — see
// `firstValidPx` above for why each candidate is validated independently
// rather than chained with `||`. The dock-aware upper bound depends on
// the live viewport AND the sidebar/handles beside the inspector, not
// this load-time default, so only the floor is enforced here —
// `clampDockedInspectorWidth` (splitters.ts) applies the real clamp,
// via app-shell.ts's `reclampInspectorWidth`, whenever the inspector is
// opened, resized, or the viewport changes (#586 findings 2a/2b).
rightInspectorPx: clamp(firstValidPx(
read.loadStr(KEYS.rightInspectorPx, ''),
read.loadStr(KEYS.docPanePx, ''),
read.loadStr(KEYS.cellDrawerPx, ''),
), 320, Infinity),
// Reactive (signals): mutating these drives repaints via effects in
// createApp — no manual refresh() list to keep in sync. Read/write through
// `.value`. tabs/activeTabId drive renderTabs + the editor + the save button;
Expand Down
106 changes: 73 additions & 33 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,20 @@ h1, h2, h3, h4, h5, h6 {
flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0;
}
.query-host[hidden], .dashboard-host[hidden] { display: none !important; }
/* #586: the docked right-inspector slot — a shell-owned layout SIBLING of
`.query-host`/`.dashboard-host` (never a `position: fixed` overlay),
replacing three independent body-mounted overlays (the cell-detail
drawer, the rows viewer, the Reference pane). Width is set inline
(app-shell.ts, from the persisted `rightInspectorPx` preference); folded
(`[hidden]`) consumes no layout width, same `[hidden]` override reasoning
as `.query-host`/`.dashboard-host` above. `.inspector-resize` is the
shared handle between the centre surface and the host (mirrors
`.col-resize` for the sidebar). */
.inspector-host {
flex: 0 0 auto; display: flex; flex-direction: column; min-height: 0;
background: var(--bg-editor); border-left: 1px solid var(--border);
}
.inspector-host[hidden], .inspector-resize[hidden] { display: none !important; }
.sidebar {
display: flex; flex-direction: column;
background: var(--bg-side);
Expand All @@ -874,15 +888,21 @@ h1, h2, h3, h4, h5, h6 {
the first consumer. */
container-type: inline-size; container-name: sidebar;
}
.col-resize, .row-resize {
/* #586: `.inspector-resize` gets the SAME vertical-bar handle styling as
`.col-resize` (sidebar) via these grouped selectors — a DISTINCT class,
not a second class on the same element, so e2e specs' `page.locator
('.col-resize')` (the sidebar's own handle) stays unambiguous rather than
resolving to two elements (a real regression only e2e caught — happy-dom
runs no real layout/selector-strictness check). */
.col-resize, .row-resize, .inspector-resize {
position: relative; flex-shrink: 0; z-index: 1;
background: transparent;
}
.col-resize { width: 7px; cursor: col-resize; }
.col-resize, .inspector-resize { width: 7px; cursor: col-resize; }
.row-resize {
height: 7px; cursor: row-resize;
}
.col-resize::before, .row-resize::before,
.col-resize::before, .row-resize::before, .inspector-resize::before,
.schema-detail-handle::before, .cd-resize-h::before {
content: ''; position: absolute; pointer-events: none;
background: var(--border);
Expand All @@ -897,7 +917,7 @@ h1, h2, h3, h4, h5, h6 {
states share one centre line. */
transition: transform 100ms ease, background-color 100ms ease;
}
.col-resize::before, .cd-resize-h::before {
.col-resize::before, .inspector-resize::before, .cd-resize-h::before {
top: 0; bottom: 0; left: 50%;
width: 1px; transform: translateX(-50%) scaleX(1);
}
Expand All @@ -906,6 +926,7 @@ h1, h2, h3, h4, h5, h6 {
height: 1px; transform: translateY(-50%) scaleY(1);
}
.col-resize:hover::before, .col-resize.dragging::before,
.inspector-resize:hover::before, .inspector-resize.dragging::before,
.cd-resize-h:hover::before, .cd-resize-h.dragging::before {
transform: translateX(-50%) scaleX(3); background: var(--accent);
}
Expand Down Expand Up @@ -2805,18 +2826,37 @@ table.res-table.fixed td .cell-val { max-width: 100%; }
table.res-table tbody tr:hover td { background: var(--bg-hover); }
table.res-table tbody tr:hover td.idx { background: var(--bg-hover); }

/* Cell-detail drawer (click a result cell) */
.cd-backdrop { position: fixed; inset: 0; z-index: 60; background: var(--scrim); display: flex; justify-content: flex-end; }
.cd-panel {
/* width is set inline (results.js attachDrawerResize) from the persisted
cellDrawerPx pref, clamped to [320, 92vw] — see clampDrawerWidth (#101) */
height: 100%; position: relative;
/* Cell-detail / rows-viewer / Reference chrome (click a result cell; #101,
#313). #586 REWRITE: `.cd-panel`/`.docs-panel` used to each be their OWN
fixed-position overlay (`.cd-backdrop`'s flex-end wrapper for `.cd-panel`;
`.docs-panel` fixed to the viewport itself) — every docked surface now
fills the shared `.inspector-host` (app-shell.ts) as a normal flow child
instead, so both base rules below are DOCKED geometry (fill parent, no
shadow, no fixed position). `.cell-detail-overlay` (further down) restores
the OLD `.cd-backdrop` geometry for the one surviving non-docked case: a
cell-detail drawer opened inside a genuinely separate detached-tab
document (results.ts's `openCellDetail`, `opts.overlay`/`targetDoc`),
which has no shell/`inspectorHost` of its own to dock into. */
.cd-panel, .docs-panel {
width: 100%; height: 100%; min-width: 0; min-height: 0;
background: var(--bg-editor);
box-shadow: var(--shadow-drawer);
display: flex; flex-direction: column;
}
/* Left-edge drag handle that resizes the drawer (#101), straddling the panel's
border like table.res-table's .col-resize-h straddles a column's edge. */
/* The one surviving non-docked case (see the block comment above): a real
modal backdrop, restoring `.cd-panel`'s pre-#586 floating geometry —
`attachDrawerResize` (drawer.ts) still sets its width inline from the
shared `rightInspectorPx` preference, clamped to [320, 92vw]
(clampDrawerWidth, #101). */
.cell-detail-overlay { position: fixed; inset: 0; z-index: 60; background: var(--scrim); display: flex; justify-content: flex-end; }
.cell-detail-overlay .cd-panel {
width: auto; height: 100%; position: relative;
box-shadow: var(--shadow-drawer);
}
/* Left-edge drag handle that resizes the drawer (#101) — only ever appended
inside `.cell-detail-overlay .cd-panel` now (#586: every docked surface is
sized by app-shell.ts's own shared `.inspector-resize` handle instead),
straddling the panel's border like table.res-table's .col-resize-h
straddles a column's edge. */
.cd-resize-h {
position: absolute; top: 0; left: 0; margin-left: -3px; z-index: 2;
width: 6px; height: 100%;
Expand All @@ -2838,19 +2878,10 @@ table.res-table tbody tr:hover td.idx { background: var(--bg-hover); }
[data-density='compact'] table.res-table td { padding: 4px 10px; }

/* Documentation pane (#313): buildDrawerChrome's NON-modal chrome under its
own 'docs' prefix — persistent, no backdrop (unlike .cd-backdrop's cell
detail drawer), so it's positioned fixed to the viewport's right edge
itself rather than centered by a flex backdrop wrapper. Width is set
inline (attachDrawerResize's docPanePx stateKey) from the persisted
docPanePx pref, clamped to [320, 92vw] — see clampDrawerWidth (#101/#313). */
.docs-panel {
position: fixed; top: 0; right: 0; bottom: 0; z-index: 55;
height: 100%;
background: var(--bg-editor);
border-left: 1px solid var(--border);
box-shadow: var(--shadow-drawer);
display: flex; flex-direction: column;
}
own 'docs' prefix — geometry comes entirely from the shared `.cd-panel,
.docs-panel` docked rule above now (#586: this pane was already
persistent/non-modal, so unifying it with the (formerly modal) cell
drawer's docked geometry was a pure simplification, no behavior change). */
.docs-head { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
.docs-title { flex: 1; min-width: 0; }
.docs-title-text { font-weight: var(--fw-semibold); font-size: var(--text-body); color: var(--fg); }
Expand Down Expand Up @@ -3165,8 +3196,12 @@ table.res-table tbody tr:hover td.idx { background: var(--bg-hover); }
.app-header .lib-name { max-width: 100%; padding: 0 3px; font-size: var(--text-label); }
.app-header .hd-btn.user-btn { width: 26px; padding: 0 5px; }
.app-header .hd-btn.user-btn .user-short { display: none; }
/* The desktop drawer handle owns this boundary. Restore a structural line
when mobile hides that non-touch resize affordance. */
/* The desktop drawer handle owns this boundary (`.cd-resize-h`, hidden on
touch below). Restore a structural line on `.cd-panel` itself so the
surviving non-docked case (a cell-detail drawer opened inside a real
detached-tab document) still shows one — harmless/orthogonal on the
docked case too (`.inspector-host` already draws its own border-left,
so this never doubles a visible line there). */
.cd-panel { border-left: 1px solid var(--border); }

/* ---- Bottom tab nav: one full-screen panel at a time ---- */
Expand Down Expand Up @@ -3277,7 +3312,7 @@ table.res-table tbody tr:hover td.idx { background: var(--bg-hover); }

/* No draggable splitters on touch — a hidden handle can't receive mousedown,
so the splitter JS never wires up (nothing to disable in JS). */
.col-resize, .row-resize, .side-split,
.col-resize, .row-resize, .side-split, .inspector-resize,
.col-resize-h, .schema-detail-handle, .cd-resize-h { display: none !important; }

/* Drag cue off (the schema rows drop `draggable` in mobile mode — schema.js). */
Expand All @@ -3295,10 +3330,15 @@ table.res-table tbody tr:hover td.idx { background: var(--bg-hover); }
.file-menu { width: auto; max-width: calc(100vw - 24px); }
.cm-tooltip { max-width: calc(100vw - 16px); }
.save-popover { max-width: calc(100vw - 16px); }
/* Cell-detail drawer → full-width, non-resizable (its handle is hidden above). */
.cd-panel { width: 100vw !important; min-width: 0; }
/* Documentation pane (#313) → same full-width, non-resizable treatment. */
.docs-panel { width: 100vw !important; min-width: 0; }
/* #586: the docked right-inspector becomes a full-screen, non-resizable
overlay when open on mobile (its handle is hidden above) — a mechanical
port of the pre-#586 per-surface `.cd-panel`/`.docs-panel` 100vw-fixed
mobile treatment onto the one shared host, not a redesign (mobile
presentation stays out of #586's scope). `[hidden]` still wins when
folded (the base rule above), so this only ever applies while open. */
.inspector-host {
position: fixed; inset: 0; z-index: 60; width: 100vw !important; min-width: 0;
}
}

/* ── Dashboard (#149 D1 / #407 / #425) ──────────────────────────────────────
Expand Down
Loading