Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
49e3fb3
Return focus to the grid when the cell dock closes
broisnischal Sep 25, 2026
77f7fad
Space previews a cell, and dialogs show what is focused
broisnischal Sep 25, 2026
358eeb6
Stop soft wrap hanging the cell editor on a half-megabyte line
broisnischal Sep 25, 2026
3b5d243
Put the table view shortcuts on one modifier
broisnischal Sep 25, 2026
c2d21cc
Never leave the window stuck on a black screen
broisnischal Sep 25, 2026
9bccf0e
Seed Alt+A with the column the cursor is on
broisnischal Sep 25, 2026
4e39989
Load a cell into the dock instead of talking about it
broisnischal Sep 25, 2026
e851a00
Drop the byte formatter that went with the removed toast
broisnischal Sep 25, 2026
789d7dd
Search the database as you type
broisnischal Sep 25, 2026
b13c5c0
Make Shift+Space focus reliably, and Ctrl+F reach the database search
broisnischal Sep 25, 2026
6fb8386
Cover the whole branch in the changeset
broisnischal Sep 25, 2026
e61a1f1
Draw the sub-view in the chosen table style, and let the gutter go
broisnischal Sep 25, 2026
039c898
Add the sub-view style and gutter toggle to the changeset
broisnischal Sep 25, 2026
9a05804
Show a splash while the app boots instead of a black rectangle
broisnischal Sep 25, 2026
bb76197
Show the mark while the app boots, and stop timing the reveal
broisnischal Sep 25, 2026
2d20084
Follow the cursor sideways in the related-rows dock, and cut its rend…
broisnischal Sep 25, 2026
897f5d9
Hold the preview bar together at any width
broisnischal Sep 25, 2026
5fae67a
Stop stringifying the cell value on every keystroke that misses
broisnischal Sep 25, 2026
3900b0f
Put the app mark on the splash, inline rather than fetched
broisnischal Sep 25, 2026
0c1f3ff
Let line numbers be turned on while soft wrap is
broisnischal Sep 25, 2026
2144dc5
Trim the changeset to one line per change
broisnischal Sep 25, 2026
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
24 changes: 24 additions & 0 deletions .changeset/grid-and-cell-dock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
### New Features
- Space previews the focused cell, Shift+Space opens it with the caret already in the editor
- Line numbers in the cell editor toggle with Alt+L
- Stroke's mark and name show while the app starts

### Bug Fixes
- Soft wrap no longer freezes the app on a cell held on one very long line
- Space no longer types a space into the cell it was meant to preview
- Escape from the cell editor puts focus back on the cell it came from
- The window no longer starts on a black screen
- Confirm dialogs show which button is focused
- Related rows follow the cursor sideways across foreign key columns
- Related rows draw in the table style you selected
- The Load button on a cell updates the preview below it
- A value past the size cap opens in the preview instead of a message about it
- Ctrl+F focuses the search box in Find in database
- The cell editor bar keeps its buttons on screen at any width

### Changes
- Filter, sort, columns and reset move to Alt+A, Alt+S, Alt+C and Alt+R
- Alt+A opens the filter on the column the cursor is already on
- Find in database searches as you type, and Escape clears it
- Soft wrap and line numbers are remembered from one cell to the next
- Opening and scrolling related rows is faster
54 changes: 53 additions & 1 deletion index.html

Large diffs are not rendered by default.

18 changes: 10 additions & 8 deletions src/lib/app-reveal.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,16 @@ export function isRevealed() {
export function revealApp() {
if (revealed || typeof document === 'undefined') return
revealed = true
// Show first: a hidden WebKitGTK window never fires requestAnimationFrame,
// so the fade below would wait forever. Then one frame so whatever the
// caller just set (overlay, modal) is in the DOM before the fade starts.
void showWindow().finally(() => {
requestAnimationFrame(() => {
document.documentElement.style.opacity = '1'
})
})
// Mark first, and synchronously. This used to wait on the show IPC to come
// back and then on a requestAnimationFrame - a frame a hidden window never
// produces, so when the show did not land the page stayed hidden and what
// finally uncovered it was a timer, seconds later. Nothing here waits now:
// the attribute is set, the CSS swaps the splash for the app, and the window
// is asked to show itself afterwards. Marking before the window appears is
// the right order anyway - the first thing on screen is the finished app
// rather than a page fading in.
document.documentElement.dataset.revealed = ''
void showWindow()
}

/** Never leave a blank window: reveal regardless if no screen claimed it. */
Expand Down
226 changes: 190 additions & 36 deletions src/lib/components/CellEditorPanel.svelte

Large diffs are not rendered by default.

37 changes: 35 additions & 2 deletions src/lib/components/CodeEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
placeholder = '',
ariaLabel = '',
keys = [],
gutter = true,
} = $props()

/** @type {HTMLDivElement | null} */
Expand All @@ -50,11 +51,37 @@
let view = null

const wrapC = new Compartment()
const gutterC = new Compartment()
const readOnlyC = new Compartment()
const langC = new Compartment()

/**
* Longest logical line, without splitting the string into an array - a
* `split('\n')` on a multi-megabyte value allocates a second copy of it.
*/
function longestLine(/** @type {string} */ text) {
let max = 0
let at = 0
for (;;) {
const nl = text.indexOf('\n', at)
if (nl === -1) return Math.max(max, text.length - at)
if (nl - at > max) max = nl - at
at = nl + 1
}
}

/**
* Past this, no language and so no parse or highlight. A jsonb column holding
* a file comes through as one line of a few hundred thousand characters, and
* handing that to the JSON parser costs more than the colour is worth. VS
* Code draws the same line at 20,000 characters
* (`editor.maxTokenizationLineLength`), for the same reason.
*/
const MAX_TOKENIZE_LINE = 20_000

/** JSON by its first character, markup by an early tag; everything else plain. */
function languageFor(/** @type {string} */ text) {
if (longestLine(text) > MAX_TOKENIZE_LINE) return []
if (/^\s*[[{]/.test(text)) return json()
if (/<[A-Za-z!/]/.test(text.slice(0, 2000))) return html()
return []
Expand Down Expand Up @@ -530,9 +557,11 @@
return EditorState.create({
doc,
extensions: [
lineNumbers(),
// Reconfigurable: the panel hides the gutter on request, and hiding it
// means dropping the fold gutter with it - a fold arrow column with no
// numbers beside it is a stripe nothing explains.
gutterC.of(gutter ? [lineNumbers(), foldGutter({ openText: '▾', closedText: '▸' })] : []),
codeFolding(),
foldGutter({ openText: '▾', closedText: '▸' }),
highlightActiveLine(),
highlightActiveLineGutter(),
drawSelection(),
Expand Down Expand Up @@ -586,6 +615,10 @@
if (view && next !== view.state.doc.toString()) view.setState(freshState(next))
})
$effect(() => { const w = wrap; view?.dispatch({ effects: wrapC.reconfigure(w ? EditorView.lineWrapping : []) }) })
$effect(() => {
const g = gutter
view?.dispatch({ effects: gutterC.reconfigure(g ? [lineNumbers(), foldGutter({ openText: '▾', closedText: '▸' })] : []) })
})
$effect(() => { const r = readOnly; view?.dispatch({ effects: readOnlyC.reconfigure(EditorState.readOnly.of(r)) }) })

export function focus() { view?.focus() }
Expand Down
24 changes: 19 additions & 5 deletions src/lib/components/ConfirmDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,24 @@
let contentEl = $state(null)

/**
* Hold focus on the dialog itself instead of letting it land on Cancel, which
* is the first focusable child. With Cancel focused, Enter would click it -
* the exact opposite of the confirm-on-Enter below. Tab still reaches both
* buttons, and Escape still cancels.
* Focus the confirm button, not Cancel and not the dialog box.
*
* Cancel is the first focusable child, so the default would put focus on it
* and Enter would cancel - the opposite of the confirm-on-Enter below.
* Parking focus on the box avoided that but left nothing on screen looking
* focused, so the dialog opened with no visible answer to "where am I".
*
* The confirm button is the default action and already says so with its ↵, so
* it is the honest place for focus: Enter now activates it the ordinary way
* (onkeydown below steps aside for a focused button, so it fires once), and
* Escape still cancels.
* @param {Event} e
*/
function onOpenAutoFocus(e) {
if (!confirmOnEnter) return
e.preventDefault()
try { contentEl?.focus({ preventScroll: true }) } catch { /* not focusable */ }
const target = contentEl?.querySelector('[data-confirm-action]') ?? contentEl
try { /** @type {HTMLElement|null} */ (target)?.focus({ preventScroll: true }) } catch { /* not focusable */ }
}

/**
Expand Down Expand Up @@ -133,10 +141,16 @@
other, and it showed - one carried an icon and a boxed kbd chip, the
other an ✕ that said nothing "Cancel" did not already say. -->
<Button variant="outline" onclick={cancel}>{cancelLabel}</Button>
<!-- focus: as well as focus-visible. onOpenAutoFocus sets focus in code and
WebKitGTK does not always count that as :focus-visible, which is the
only state the shared button styles draw a ring for - so the dialog
opened with its default action focused and nothing showing it. -->
<Button
variant={variant === 'destructive' ? 'destructive' : 'default'}
{disabled}
onclick={confirm}
data-confirm-action
class="focus:outline-2 focus:outline-offset-0 focus:outline-ring"
>
{#if confirmIcon}<Icon name={confirmIcon} class="size-3.5 shrink-0" />{/if}
{confirmLabel}
Expand Down
Loading
Loading