mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-03 16:41:27 +01:00
keys: Ctrl moves between tiles, Ctrl-Q jots from anywhere, the plan gets a screen of its own
Alt never reached the app, so the tile keys are Ctrl-h/j/k/l and Ctrl-Shift-h/j/k/l, taken before vim sees them, and h/l walk the tiles while reading. Ctrl-Q lowers the scratchpad from the editor straight into the last note, and raises it back to the document, still writing. The drawer sits in the porthole's opening with a full rim. The board, the agenda, the calendar and a card's back live on a plan screen that S goes to and comes back from, so documents and views never share the screen. The calendar fits its tile instead of scrolling.
This commit is contained in:
1 parent
b1107013ee
commit
650fc40fa7
26 files changed
+340
-221
No files matched your search
+7
-2
@@ -246,8 +246,8 @@
|
||||
[[...keysOf('tree', 'down'), ...keysOf('tree', 'up')], 'browse, l opens, L opens beside'],
|
||||
[keysOf('global', 'space'), 'switch space, Tab cycles'],
|
||||
[keysOf('global', 'overview'), 'overview of every space'],
|
||||
[keysOf('global', 'plan'), 'board, agenda and calendar'],
|
||||
[[...keysOf('global', 'board'), ...keysOf('global', 'agenda'), ...keysOf('global', 'calendar')], 'one of them, in a tile'],
|
||||
[keysOf('global', 'plan'), 'the plan: board, agenda, calendar'],
|
||||
[[...keysOf('global', 'board'), ...keysOf('global', 'agenda'), ...keysOf('global', 'calendar')], 'one of them, on the plan'],
|
||||
[keysOf('tree', 'capture'), 'capture a card to the inbox'],
|
||||
[keysOf('global', 'find'), 'find in every space'],
|
||||
[keysOf('tree', 'search'), 'search this space'],
|
||||
@@ -293,6 +293,7 @@
|
||||
class="panes switch"
|
||||
class:reading={workspace.tiles.all.some((tile) => !tile.empty)}
|
||||
class:full={workspace.fullscreen}
|
||||
class:plan={workspace.tiles.planning}
|
||||
class:focus-doc={workspace.focus !== 'list'}
|
||||
style="--dir: {workspace.switchDirection}"
|
||||
>
|
||||
@@ -433,6 +434,10 @@
|
||||
height: calc(var(--h) * 100% - var(--gap));
|
||||
}
|
||||
|
||||
/* The plan screen: the views alone, with no list beside them. */
|
||||
.panes.plan { grid-template-columns: 1fr; }
|
||||
.panes.plan .window.list { display: none; }
|
||||
|
||||
/* `f`: the focused pane fills the porthole — the list, or one tile. */
|
||||
.panes.full { grid-template-columns: 1fr; }
|
||||
.panes.full.focus-doc .window.list,
|
||||
|
||||
@@ -83,14 +83,18 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* A tile's window is its frame, so it only fills it. */
|
||||
/*
|
||||
* A tile's window is its frame, so it only fills it — and the month is
|
||||
* fitted into what the tile leaves it, never scrolled: the weeks share the
|
||||
* height, up to a row a pointer still hits comfortably.
|
||||
*/
|
||||
.sheet {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 20px 24px;
|
||||
overflow: hidden;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
header { display: flex; align-items: center; gap: 12px; }
|
||||
@@ -99,21 +103,26 @@
|
||||
header .title { color: var(--light); }
|
||||
|
||||
.grid {
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 4px;
|
||||
grid-template-rows: auto;
|
||||
grid-auto-rows: minmax(0, 40px);
|
||||
gap: 3px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.weekday { text-align: center; font-size: 10px; color: var(--muted); }
|
||||
.grid > li { min-height: 0; }
|
||||
|
||||
.day {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 100%;
|
||||
aspect-ratio: 1.4;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-s);
|
||||
@@ -126,12 +135,13 @@
|
||||
.day.written { color: var(--fg); }
|
||||
.day.today { border-color: var(--rim); }
|
||||
.day.at { background: var(--light); color: var(--ink); }
|
||||
.dot, .many { position: absolute; bottom: 4px; }
|
||||
.dot, .many { position: absolute; bottom: 3px; }
|
||||
.dot { width: 5px; height: 5px; border-radius: 999px; background: var(--light); }
|
||||
.many { font-size: 9px; color: var(--light); }
|
||||
.day.at :is(.dot) { background: var(--ink); }
|
||||
.day.at .many { color: var(--ink); }
|
||||
|
||||
.chosen { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
||||
.chosen h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -155,6 +155,7 @@
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { api } from './api'
|
||||
import { hooks } from './commands'
|
||||
import { claim } from './modes'
|
||||
import { editorTheme, highlighting, languages } from './editorTheme'
|
||||
import { embedSnippet } from './markdown'
|
||||
import { workspace, type Tile } from './store.svelte'
|
||||
@@ -180,9 +181,11 @@
|
||||
|
||||
/*
|
||||
* Esc, before vim sees it: in normal mode, or with no vim, it leaves, as
|
||||
* `:q` does — unless it is closing the completions.
|
||||
* `:q` does — unless it is closing the completions. The Ctrl keys heard
|
||||
* everywhere — between the tiles, the scratchpad — go before vim too.
|
||||
*/
|
||||
function onkeydown(event: KeyboardEvent) {
|
||||
if (claim(event)) return
|
||||
if (event.key !== 'Escape' || (workspace.vimEnabled && mode !== 'normal')) return
|
||||
if (view && completionStatus(view.state) !== null) return
|
||||
event.preventDefault()
|
||||
@@ -317,6 +320,7 @@
|
||||
parent: host!,
|
||||
})
|
||||
|
||||
if (writing?.append) view.dispatch({ selection: { anchor: text.length } })
|
||||
if (writing || workspace.tile === tile) view.focus()
|
||||
|
||||
const cm = workspace.vimEnabled ? getCM(view) : null
|
||||
@@ -333,7 +337,10 @@
|
||||
// A fresh entry starts in insert mode, and so does an empty short text,
|
||||
// which is there to be written; anything else starts in normal mode,
|
||||
// the way opening a file in vim does.
|
||||
if (tile?.enterInsert || (writing && !text.trim())) {
|
||||
if (writing?.append && text.trim()) {
|
||||
Vim.handleKey(cm, 'G', 'workspace')
|
||||
Vim.handleKey(cm, 'A', 'workspace')
|
||||
} else if (tile?.enterInsert || (writing && !text.trim())) {
|
||||
if (tile) tile.enterInsert = false
|
||||
Vim.handleKey(cm, 'i', 'workspace')
|
||||
}
|
||||
|
||||
+18
-13
@@ -83,6 +83,7 @@
|
||||
<footer class="label">
|
||||
<span>writing — it saves itself</span>
|
||||
<span><span class="kbd">esc</span> back to the notes</span>
|
||||
<span><span class="kbd">⌃ Q</span> back to where you were</span>
|
||||
<span><span class="kbd">:keep</span> make it a document</span>
|
||||
</footer>
|
||||
{:else}
|
||||
@@ -92,31 +93,35 @@
|
||||
|
||||
<style>
|
||||
/*
|
||||
* Hung from under the bar across the frame's opening, as the desktop's
|
||||
* scratchpad slides down from under its bar: hull, a rim, the lower corners
|
||||
* rounded, and the light along its lower edge to say it has the keys.
|
||||
* The upper part of the porthole, come down over what is in it: the same
|
||||
* opening under the bar, the same rounded corners, and the focused
|
||||
* window's lit rim, since it has the keys while it is down. It is
|
||||
* uncovered from the top, as the desktop's scratchpad comes from under its
|
||||
* bar.
|
||||
*/
|
||||
.drawer {
|
||||
--pane-bg: color-mix(in oklab, var(--surface) 40%, var(--hull));
|
||||
position: absolute;
|
||||
top: calc(var(--bar-h) + var(--frame));
|
||||
left: calc(var(--frame) + 24px);
|
||||
right: calc(var(--rail-w) + 24px);
|
||||
top: var(--bar-h);
|
||||
left: var(--frame);
|
||||
right: var(--rail-w);
|
||||
z-index: 25;
|
||||
height: min(62vh, 640px);
|
||||
height: min(62vh, 640px, calc(100% - var(--bar-h) - var(--status-h)));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 14px 20px 12px;
|
||||
background: var(--hull);
|
||||
border: 1px solid var(--surface-alt);
|
||||
border-top: 0;
|
||||
border-bottom-color: var(--light-dim);
|
||||
border-radius: 0 0 var(--frame-radius) var(--frame-radius);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--frame-radius);
|
||||
background:
|
||||
linear-gradient(var(--pane-bg), var(--pane-bg)) padding-box,
|
||||
linear-gradient(135deg, var(--light), var(--light-dim)) border-box;
|
||||
box-shadow: 0 18px 40px color-mix(in oklab, var(--ink) 70%, transparent);
|
||||
animation: lower var(--t-slow) var(--ease-snap) both;
|
||||
}
|
||||
@keyframes lower {
|
||||
from { transform: translateY(-104%); }
|
||||
from { clip-path: inset(0 0 100% 0 round var(--frame-radius)); }
|
||||
to { clip-path: inset(0 0 0 0 round var(--frame-radius)); }
|
||||
}
|
||||
|
||||
header { display: flex; align-items: center; gap: 10px; }
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
actions(index)
|
||||
}}
|
||||
class:active={index === workspace.tree.cursor}
|
||||
class:open={workspace.tiles.all.some((tile) => tile.open?.id === node.id)}
|
||||
class:open={workspace.tiles.every.some((tile) => tile.open?.id === node.id)}
|
||||
class:marked={workspace.tree.isMarked(node.id)}
|
||||
class:nested
|
||||
data-index={index}
|
||||
|
||||
@@ -7,7 +7,13 @@
|
||||
const { pending = '' }: { pending?: string } = $props()
|
||||
|
||||
const mode = $derived(
|
||||
workspace.editing ? (workspace.vimEnabled ? workspace.vimMode.toUpperCase() : 'EDIT') : 'BROWSE',
|
||||
workspace.editing
|
||||
? workspace.vimEnabled
|
||||
? workspace.vimMode.toUpperCase()
|
||||
: 'EDIT'
|
||||
: workspace.tiles.planning
|
||||
? 'PLAN'
|
||||
: 'BROWSE',
|
||||
)
|
||||
|
||||
// The path is the file name here: work/einarbeiten/01-was-ist-eebus.
|
||||
|
||||
+25
-13
@@ -82,7 +82,7 @@ function focusPane(pane: 'list' | 'document') {
|
||||
}
|
||||
|
||||
/*
|
||||
* Alt + h/j/k/l: the focus goes to the tile that way, out of the editor
|
||||
* Ctrl + h/j/k/l: the focus goes to the tile that way, out of the editor
|
||||
* too; left of the leftmost tile is the list. With Shift the focused tile
|
||||
* goes that way instead, swapping places with the one it lands on.
|
||||
*/
|
||||
@@ -93,7 +93,8 @@ function tileKey(direction: Direction, shift: boolean) {
|
||||
}
|
||||
const next = neighbour(dwindle(workspace.tiles.all.length), workspace.tiles.focused, direction)
|
||||
if (next < 0) {
|
||||
if (direction === 'h' && !shift) focusPane('list')
|
||||
// The plan screen has no list beside it.
|
||||
if (direction === 'h' && !shift && !workspace.tiles.planning) focusPane('list')
|
||||
return
|
||||
}
|
||||
if (!shift) return void focusTile(next)
|
||||
@@ -103,15 +104,24 @@ function tileKey(direction: Direction, shift: boolean) {
|
||||
workspace.tiles.focused = next
|
||||
}
|
||||
|
||||
/** The drawer up, and the keys back where they were: in the editor, when a document was being written. */
|
||||
async function raise() {
|
||||
await workspace.scratch.close()
|
||||
if (!workspace.scratch.open && workspace.editing && workspace.pane === 'document') hooks.focusEditor()
|
||||
}
|
||||
|
||||
export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
|
||||
always: {
|
||||
save: () => {
|
||||
if (workspace.drafts.writing) guard(() => workspace.drafts.saveWriting())
|
||||
else if (workspace.open) guard(() => workspace.drafts.save())
|
||||
},
|
||||
close: () => void runCommand('close'),
|
||||
focus: (key) => tileKey(key.slice(-1) as Direction, false),
|
||||
swap: (key) => tileKey(key.slice(-1) as Direction, true),
|
||||
// Under the drawer the tiles are out of sight, so the keys are the note's.
|
||||
focus: (key) => (workspace.scratch.open ? false : tileKey(key.slice(-1) as Direction, false)),
|
||||
swap: (key) => (workspace.scratch.open ? false : tileKey(key.slice(-1).toLowerCase() as Direction, true)),
|
||||
// Ctrl-Q: down with the last note being written, and up again back to
|
||||
// what was being done — the document still in the mode it was left in.
|
||||
scratch: () => guard(() => (workspace.scratch.open ? raise() : workspace.scratch.jot())),
|
||||
},
|
||||
|
||||
sheet: {
|
||||
@@ -318,7 +328,7 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
|
||||
},
|
||||
line: () => line(':'),
|
||||
help: () => void runCommand('help'),
|
||||
raise: () => void workspace.scratch.close(),
|
||||
raise: () => guard(raise),
|
||||
nothing: () => {},
|
||||
},
|
||||
|
||||
@@ -355,6 +365,8 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
|
||||
open: (key) => {
|
||||
const enter = key === 'Enter' || key === 'Shift-Enter'
|
||||
if (enter && reading() && hooks.fileAction()) return
|
||||
// Reading, `l` is the tile to the right, as `h` is the one to the left.
|
||||
if (!enter && workspace.pane === 'document' && workspace.open) return tileKey('l', false)
|
||||
// Shift-Enter, like `L`: beside what is open, in a tile of its own.
|
||||
if (key === 'Shift-Enter' && !reading()) return guard(() => workspace.tiles.openBeside())
|
||||
const shown = workspace.tiles.all.findIndex((tile) => tile.open && tile.open.id === workspace.tree.selected?.id)
|
||||
@@ -364,10 +376,10 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
|
||||
beside: () => {
|
||||
if (!reading()) guard(() => workspace.tiles.openBeside())
|
||||
},
|
||||
// Leftwards at every depth: from the document to the list, from the
|
||||
// list up a level.
|
||||
// Leftwards at every depth: from a document to the tile left of it, from
|
||||
// the leftmost to the list, from the list up a level.
|
||||
left: () => {
|
||||
if (workspace.open && workspace.pane === 'document') focusPane('list')
|
||||
if (workspace.open && workspace.pane === 'document') tileKey('h', false)
|
||||
else guard(() => workspace.tree.up())
|
||||
},
|
||||
sideways: (key) => {
|
||||
@@ -516,7 +528,7 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
|
||||
open: () => void workspace.guard(() => workspace.diary.openCalendarDay()).then(() => hooks.focusEditor()),
|
||||
},
|
||||
|
||||
/** What every view shares: `q` closes its tile, back to the one before. */
|
||||
/** What every view shares: `q` closes a card's back, and from the others goes back to the documents. */
|
||||
views: {
|
||||
close: () => void runCommand('close'),
|
||||
},
|
||||
@@ -549,9 +561,9 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
|
||||
agenda: () => void runCommand('agenda'),
|
||||
today: () => void runCommand('today'),
|
||||
calendar: () => void runCommand('cal'),
|
||||
// `S`, as the desktop's special workspace is bound: the board, the
|
||||
// agenda and the calendar, put up and away again. `b`, `t` and `C` only
|
||||
// ever go to their tile.
|
||||
// `S`, as the desktop's special workspace is bound: the plan screen —
|
||||
// the board, the agenda and the calendar — and back to the documents.
|
||||
// `b`, `t` and `C` go there too, to their own view.
|
||||
plan: () => void runCommand('plan'),
|
||||
line: () => line(':'),
|
||||
help: () => void runCommand('help'),
|
||||
|
||||
@@ -591,14 +591,14 @@ export const commands: CommandSpec[] = [
|
||||
{
|
||||
name: 'board',
|
||||
group: 'board',
|
||||
help: "this space's board, in a tile — the last one shown when this space has none",
|
||||
help: "this space's board, on the plan screen — the last one shown when this space has none",
|
||||
run: () => workspace.guard(() => workspace.tiles.show('board')),
|
||||
},
|
||||
{
|
||||
name: 'plan',
|
||||
aliases: ['special'],
|
||||
group: 'board',
|
||||
help: 'the board, the agenda and the calendar, tiled ahead of what is open — S puts them up, and every view away again; 1 … 9 on the board picks the board',
|
||||
help: 'the plan screen: the board, the agenda and the calendar, in place of the documents — S goes there and back again; 1 … 9 on the board picks the board',
|
||||
run: () => workspace.guard(() => workspace.tiles.togglePlan()),
|
||||
},
|
||||
{
|
||||
@@ -874,7 +874,7 @@ export const commands: CommandSpec[] = [
|
||||
name: 'close',
|
||||
aliases: ['clo'],
|
||||
group: 'tiles',
|
||||
help: 'close the focused tile — Alt-q; asks when its draft could not be written',
|
||||
help: 'close the focused tile — q while reading; on the plan screen, back to the documents; asks when its draft could not be written',
|
||||
run: () =>
|
||||
workspace.guard(async () => {
|
||||
await workspace.drafts.flushTile()
|
||||
|
||||
+11
-11
@@ -46,7 +46,7 @@ export interface Binding {
|
||||
context: Context
|
||||
/**
|
||||
* What is pressed, as `keyName` spells it: `j`, `G`, `Enter`, `Space`,
|
||||
* `Shift-Tab`, `Ctrl-d`, `Alt-h`, `1`, `Shift-1`, a two-key `gg`, or `*`
|
||||
* `Shift-Tab`, `Ctrl-d`, `Ctrl-H`, `1`, `Shift-1`, a two-key `gg`, or `*`
|
||||
* for any key without Ctrl, Alt or Meta. A binding without keys only
|
||||
* describes — a mouse gesture, the editor's own keymap.
|
||||
*/
|
||||
@@ -78,12 +78,11 @@ export const bindings = [
|
||||
{ context: 'global', keys: ['Tab', 'Shift-Tab'], action: 'cycle', label: 'Tab · Shift-Tab', help: 'next / previous space' },
|
||||
{ context: 'global', keys: ['w'], action: 'overview', help: 'overview of every space — hjkl, then Enter' },
|
||||
{ context: 'global', keys: ['f'], action: 'fullscreen', help: 'fullscreen the focused pane or tile; again or Esc to leave' },
|
||||
{ context: 'always', keys: ['Alt-h', 'Alt-j', 'Alt-k', 'Alt-l'], action: 'focus', typing: true, label: 'Alt-h/j/k/l', help: 'focus the tile that way, from the editor too — left of the leftmost is the list', essential: true },
|
||||
{ context: 'always', keys: ['Alt-Shift-h', 'Alt-Shift-j', 'Alt-Shift-k', 'Alt-Shift-l'], action: 'swap', typing: true, label: 'Alt-Shift-h/j/k/l', help: 'move the focused tile that way' },
|
||||
{ context: 'always', keys: ['Ctrl-h', 'Ctrl-j', 'Ctrl-k', 'Ctrl-l'], action: 'focus', typing: true, label: 'Ctrl-h/j/k/l', help: 'focus the tile that way, from the editor too — left of the leftmost is the list; h / l do the same while reading', essential: true },
|
||||
{ context: 'always', keys: ['Ctrl-H', 'Ctrl-J', 'Ctrl-K', 'Ctrl-L'], action: 'swap', typing: true, label: 'Ctrl-Shift-h/j/k/l', help: 'move the focused tile that way' },
|
||||
{ context: 'tree', keys: ['L'], action: 'beside', label: 'L · Shift-Enter', help: 'open the row in a new tile beside the others' },
|
||||
{ context: 'global', keys: ['gt'], action: 'nextTile', label: 'gt / gT', help: 'next / previous tile — a view too' },
|
||||
{ context: 'global', keys: ['gT'], action: 'previousTile' },
|
||||
{ context: 'always', keys: ['Alt-q'], action: 'close', typing: true, help: 'close the focused tile — :close; :only closes the others' },
|
||||
{ context: 'tree', keys: ['j', 'ArrowDown'], action: 'down', label: 'j / k · ↓ ↑', help: 'move down / up the list — scroll the document when it has focus', essential: true },
|
||||
{ context: 'tree', keys: ['k', 'ArrowUp'], action: 'up' },
|
||||
{ context: 'tree', keys: ['gg'], action: 'top', label: 'gg / G', help: 'first / last row, or top / bottom of the document — a first g, d, ] or [ waits a second in the status line' },
|
||||
@@ -109,14 +108,14 @@ export const bindings = [
|
||||
{ context: 'tree', keys: ['za'], action: 'files', help: "a document's own files, folded open under it in the list — again folds them away; p drops marked files in there" },
|
||||
{ context: 'tree', keys: ['M'], action: 'markAll', help: 'pick up the whole folder — again puts it all back' },
|
||||
{ context: 'tree', label: 'Ctrl / Shift-click', help: 'pick a row up · every row up to it — drag them onto a folder' },
|
||||
{ context: 'global', keys: ['S'], action: 'plan', help: 'the board, the agenda and the calendar, tiled ahead of what is open with the board focused — S again puts every view away', essential: true },
|
||||
{ context: 'global', keys: ['t'], action: 'agenda', help: 'agenda — late, due this week, in hand, on every board — in a tile of its own', essential: true },
|
||||
{ context: 'global', keys: ['S'], action: 'plan', help: 'the plan screen: the board, the agenda and the calendar, in place of the documents and the list — S again goes back to the documents as they were', essential: true },
|
||||
{ context: 'global', keys: ['t'], action: 'agenda', help: 'agenda — late, due this week, in hand, on every board — on the plan screen', essential: true },
|
||||
{ context: 'global', keys: ['T'], action: 'today', help: "today's diary entry, started if there is none — from templates/day when the diary has one", essential: true },
|
||||
{ context: 'global', keys: ['C'], action: 'calendar', help: 'the diary by month, in a tile: h/l a day, j/k a week, H/L a month, . today, Enter opens or starts the day beside it' },
|
||||
{ context: 'global', keys: ['C'], action: 'calendar', help: 'the diary by month, on the plan screen: h/l a day, j/k a week, H/L a month, . today, Enter opens or starts the day among the documents' },
|
||||
{ context: 'tree', keys: ['[d'], action: 'previousEntry', label: '[d / ]d', help: 'the diary entry before / after the one open, by date' },
|
||||
{ context: 'tree', keys: [']d'], action: 'nextEntry' },
|
||||
{ context: 'global', keys: ['Ctrl-p'], action: 'find', label: 'Ctrl-P', help: 'find anything, in every space — Enter opens the result lit beside them, n goes on', essential: true },
|
||||
{ context: 'global', keys: ['b'], action: 'board', help: "the board, in a tile: this space's, or the last one shown — 1 … 9 there shows another's" },
|
||||
{ context: 'global', keys: ['b'], action: 'board', help: "the board, on the plan screen: this space's, or the last one shown — 1 … 9 there shows another's" },
|
||||
{ context: 'tree', keys: ['c'], action: 'capture', help: "capture a card into this space's inbox, without leaving here — reading a document, a card about it; Tab on the line makes it a note, or a line in today's entry", essential: true },
|
||||
{ context: 'tree', keys: ['q'], action: 'close', label: 'Esc · q', help: 'close the document, and its tile when there are several — Esc first puts down what m picked up', essential: true },
|
||||
{ context: 'tree', keys: ['Escape'], action: 'escape' },
|
||||
@@ -136,12 +135,13 @@ export const bindings = [
|
||||
// --- the workspace around the panes ------------------------------------------
|
||||
{ context: 'workspace', keys: ['Escape'], action: 'escape', typing: true },
|
||||
{ context: 'workspace', keys: ['Q'], action: 'scratch', section: 'scratch', help: 'lower the scratchpad over whatever is on screen, or raise it again — :scratch <text> takes a note without it', essential: true },
|
||||
{ context: 'always', keys: ['Ctrl-q'], action: 'scratch', typing: true, section: 'scratch', label: 'Ctrl-Q', help: 'from anywhere, the editor too: the scratchpad down with the last note being written, and again back to where you were, still writing — Firefox quits on it instead', essential: true },
|
||||
// While a document is being written, a key that lands outside the editor
|
||||
// hands the focus back to it instead of walking the list unseen.
|
||||
{ context: 'writing', keys: ['*'], action: 'refocus' },
|
||||
|
||||
// --- what the views share: the board, the agenda, the calendar, a card's back
|
||||
{ context: 'views', keys: ['q', 'Escape'], action: 'close', label: 'q · Esc', caps: ['q'], hint: 'close', help: "in a view — the board, the agenda, the calendar, a card's back — close its tile; the focus goes back to the tile before" },
|
||||
{ context: 'views', keys: ['q', 'Escape'], action: 'close', label: 'q · Esc', caps: ['q'], hint: 'close', help: "on the plan screen: from a card's back to the board, from the board, the agenda or the calendar back to the documents" },
|
||||
|
||||
// --- on a board ---------------------------------------------------------------
|
||||
{ context: 'board', keys: ['h', 'ArrowLeft'], action: 'left', label: 'h / l', caps: ['h', 'j', 'k', 'l'], hint: 'move', help: 'to the list left / right — right of the last, to the tile beside' },
|
||||
@@ -156,7 +156,7 @@ export const bindings = [
|
||||
{ context: 'board', keys: ['L'], action: 'sendRight' },
|
||||
{ context: 'board', keys: ['Space'], action: 'done', hint: 'done', help: 'into the done list, or back out to the inbox' },
|
||||
{ context: 'board', keys: ['Enter'], action: 'turn', hint: 'turn over', help: 'turn the card over, in a tile beside: its labels, description and checklist' },
|
||||
{ context: 'board', keys: ['gf'], action: 'notes', help: "the card's notes — a document, made on first use; q comes back" },
|
||||
{ context: 'board', keys: ['gf'], action: 'notes', help: "the card's notes — a document, made on first use, opened among the documents; S comes back" },
|
||||
{ context: 'board', keys: ['o', 'c'], action: 'new', label: 'o · c', caps: ['o'], hint: 'new', help: 'new card on this list' },
|
||||
{ context: 'board', keys: ['D'], action: 'due', hint: 'due', help: 'due date: fri, tm, +3d, 10-31 …' },
|
||||
{ context: 'board', keys: ['R'], action: 'rename', hint: 'rename', help: 'rename the card, or the list when it is empty' },
|
||||
@@ -183,7 +183,7 @@ export const bindings = [
|
||||
{ context: 'card', keys: ['D'], action: 'due', hint: 'due' },
|
||||
{ context: 'card', keys: ['H'], action: 'sendLeft', label: 'H / L', caps: ['H', 'L'], hint: 'send', help: 'send the card to the list left / right' },
|
||||
{ context: 'card', keys: ['L'], action: 'sendRight' },
|
||||
{ context: 'card', keys: ['gf'], action: 'notes', hint: 'notes', help: "the card's notes — a document, made on first use; q comes back" },
|
||||
{ context: 'card', keys: ['gf'], action: 'notes', hint: 'notes', help: "the card's notes — a document, made on first use, opened among the documents; S comes back" },
|
||||
{ context: 'card', keys: [':'], action: 'line' },
|
||||
|
||||
// --- the scratchpad, while it is down --------------------------------------------
|
||||
|
||||
+18
-2
@@ -3,7 +3,7 @@
|
||||
* contexts in keymap.ts, the topmost first. A key goes down the stack until
|
||||
* a context takes it.
|
||||
*
|
||||
* always Ctrl-S, Alt-h/j/k/l — heard in the editor too
|
||||
* always Ctrl-S, Ctrl-Q, Ctrl-h/j/k/l — heard in the editor too
|
||||
* a sheet help, info, trash …; nothing under one hears a key
|
||||
* drawer the scratchpad, while it is down
|
||||
* workspace Esc out of fullscreen, Q
|
||||
@@ -52,7 +52,7 @@ const lone = new Set(['Shift', 'Control', 'Alt', 'AltGraph', 'Meta', 'CapsLock',
|
||||
|
||||
/**
|
||||
* A key as the table spells it: `j`, `G`, `Enter`, `Shift-Tab`, `Ctrl-d`,
|
||||
* `Alt-Shift-h`, `1`, `Shift-1`. Digits, and letters under Alt, are read
|
||||
* `Ctrl-H`, `1`, `Shift-1`. Digits, and letters under Alt, are read
|
||||
* from `event.code`, so they are the same key on every layout — and on a
|
||||
* Mac, where Alt turns h into ˙. Shift is only spelled out where the key
|
||||
* itself does not already say it. A modifier on its own is no key at all.
|
||||
@@ -134,3 +134,19 @@ export function dispatch(event: KeyboardEvent, pending: string, line: boolean, t
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* A Ctrl key heard in every context, taken before the editor sees it: vim
|
||||
* has uses of its own for Ctrl-h and Ctrl-k, and what it handles never
|
||||
* reaches `dispatch`. Ctrl-S is left to the editor, which saves its own
|
||||
* text. True when it was taken.
|
||||
*/
|
||||
export function claim(event: KeyboardEvent): boolean {
|
||||
const name = keyName(event)
|
||||
if (!name?.startsWith('Ctrl-')) return false
|
||||
const binding = find('always', name)
|
||||
if (!binding?.typing || binding.action === 'save' || !run(binding, name, event)) return false
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return true
|
||||
}
|
||||
@@ -334,7 +334,7 @@ export class Cards {
|
||||
/** The cards about each document open in a tile, read again. */
|
||||
async #reloadCardsAbout() {
|
||||
await Promise.all(
|
||||
this.ws.tiles.all.map(async (tile) => {
|
||||
this.ws.tiles.every.map(async (tile) => {
|
||||
const id = tile.open?.kind === 'document' ? tile.open.id : null
|
||||
if (id === null) return
|
||||
const cards = await api.nodeCards(id).catch(() => null)
|
||||
|
||||
@@ -89,7 +89,7 @@ export class Drafts {
|
||||
/** Back online: what is waiting goes now rather than at the next retry. */
|
||||
watchConnection() {
|
||||
window.addEventListener('online', () => {
|
||||
for (const tile of this.ws.tiles.all) {
|
||||
for (const tile of this.ws.tiles.every) {
|
||||
if (!tile.dirty) continue
|
||||
tile.stopAutosave()
|
||||
void this.#writeBack(tile)
|
||||
@@ -107,7 +107,7 @@ export class Drafts {
|
||||
* unsaved work without a word.
|
||||
*/
|
||||
async flush() {
|
||||
for (const tile of this.ws.tiles.all) await this.flushTile(tile)
|
||||
for (const tile of this.ws.tiles.every) await this.flushTile(tile)
|
||||
}
|
||||
|
||||
/** The same for one tile only, as it is about to show something else. */
|
||||
@@ -173,7 +173,7 @@ export class Drafts {
|
||||
* then refused, and `:e!` takes the rewritten text.
|
||||
*/
|
||||
async reread() {
|
||||
for (const tile of this.ws.tiles.all) {
|
||||
for (const tile of this.ws.tiles.every) {
|
||||
if (!tile.open || tile.editing || tile.dirty) continue
|
||||
const node = await api.getNode(tile.open.id).catch(() => null)
|
||||
if (!node) continue
|
||||
@@ -236,7 +236,7 @@ export class Drafts {
|
||||
|
||||
/** Every tile showing `id` reads it again, after it was written somewhere else. */
|
||||
async rereadNode(id: number) {
|
||||
for (const tile of this.ws.tiles.all) {
|
||||
for (const tile of this.ws.tiles.every) {
|
||||
if (tile.open?.id !== id || tile.dirty) continue
|
||||
const node = await api.getNode(id)
|
||||
tile.open = node
|
||||
|
||||
@@ -43,6 +43,18 @@ export class Scratchpad {
|
||||
this.open = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Ctrl-Q, from anywhere: the drawer down with the note written last being
|
||||
* written on, at its end — or, when it is down, up again.
|
||||
*/
|
||||
async jot() {
|
||||
if (this.open) return this.close()
|
||||
await this.toggle()
|
||||
this.cursor = 0
|
||||
this.editNote()
|
||||
if (this.ws.drafts.writing) this.ws.drafts.writing.append = true
|
||||
}
|
||||
|
||||
/** `o`: a new note at the top, written straight away. */
|
||||
newNote() {
|
||||
this.#writeNote(null, '')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isContainer, type WorkspaceNode } from '../api'
|
||||
import { formatDay } from '../util'
|
||||
import type { Workspace } from '../store.svelte'
|
||||
import type { Pane, Workspace } from '../store.svelte'
|
||||
|
||||
/**
|
||||
* What a tile shows: a node of the tree — a document or a file — or one of
|
||||
@@ -68,9 +68,10 @@ export class Tile {
|
||||
}
|
||||
|
||||
/**
|
||||
* The tiles on screen and which has the focus: opened, closed, walked, and
|
||||
* the views — the board, the agenda, the calendar, a card's back — put up
|
||||
* among them.
|
||||
* The tiles on screen and which has the focus: opened, closed, walked. There
|
||||
* are two screens of them, and one is shown at a time: the documents, beside
|
||||
* the list, and the plan — the board, the agenda, the calendar, a card's
|
||||
* back — which `S` goes to and comes back from.
|
||||
*/
|
||||
export class Tiles {
|
||||
readonly ws: Workspace
|
||||
@@ -91,9 +92,19 @@ export class Tiles {
|
||||
return this.all[this.focused] ?? this.all[0]
|
||||
}
|
||||
|
||||
/** Every tile showing `id`. */
|
||||
/** The plan screen is on show, and the documents are put away behind it. */
|
||||
planning = $state(false)
|
||||
/** The screen not on show, as it was left. */
|
||||
#away = $state<{ all: Tile[]; focused: number; trail: Tile[]; pane: Pane }>({ all: [], focused: 0, trail: [], pane: 'list' })
|
||||
|
||||
/** The tiles of both screens: a draft behind the plan is still written. */
|
||||
get every(): Tile[] {
|
||||
return [...this.all, ...this.#away.all]
|
||||
}
|
||||
|
||||
/** Every tile showing `id`, on either screen. */
|
||||
showing(id: number): Tile[] {
|
||||
return this.all.filter((tile) => tile.open?.id === id)
|
||||
return this.every.filter((tile) => tile.open?.id === id)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -158,8 +169,13 @@ export class Tiles {
|
||||
if (this.current.empty) this.ws.pane = 'list'
|
||||
}
|
||||
|
||||
/** `:close`: the focused tile goes, or with only one, what it shows. */
|
||||
/**
|
||||
* `:close`: the focused tile goes, or with only one, what it shows. On the
|
||||
* plan only a card's back goes; closing the board, the agenda or the
|
||||
* calendar goes back to the documents.
|
||||
*/
|
||||
close() {
|
||||
if (this.planning && this.current.kind !== 'card') return this.leavePlan()
|
||||
this.ws.drafts.leaveCard()
|
||||
this.current.clear()
|
||||
this.prune()
|
||||
@@ -201,46 +217,60 @@ export class Tiles {
|
||||
|
||||
/*
|
||||
* The views — the board, the agenda, the calendar, a card's back — are
|
||||
* tiles, opened beside what is on screen. Each is read again whenever it is
|
||||
* gone to: it is looked at in passing, and what it says should be what is
|
||||
* true now.
|
||||
* the tiles of the plan screen. Each is read again whenever it is gone to:
|
||||
* it is looked at in passing, and what it says should be what is true now.
|
||||
*/
|
||||
|
||||
/** `b`, `t`, `C`, Enter on a card: to that view's tile, opening it first when it is not up. */
|
||||
/** The views the plan screen holds, in the order they are tiled. */
|
||||
static readonly PLAN: View[] = ['board', 'agenda', 'calendar']
|
||||
|
||||
/** The other screen on show, and this one put away as it is. */
|
||||
swap() {
|
||||
const away = this.#away
|
||||
this.#away = { all: this.all, focused: this.focused, trail: this.trail, pane: this.ws.pane }
|
||||
this.all = away.all.length > 0 ? away.all : [new Tile()]
|
||||
this.focused = Math.min(away.focused, this.all.length - 1)
|
||||
this.trail = away.trail
|
||||
this.ws.pane = away.pane
|
||||
this.planning = !this.planning
|
||||
}
|
||||
|
||||
/** To the plan screen, with every view on it, from wherever the documents were. */
|
||||
enterPlan() {
|
||||
if (this.planning) return
|
||||
this.ws.drafts.leaveCard()
|
||||
this.swap()
|
||||
const cards = this.all.filter((tile) => tile.kind === 'card')
|
||||
this.all = [...Tiles.PLAN.map((kind) => this.view(kind) ?? new Tile(kind)), ...cards]
|
||||
this.focused = Math.min(this.focused, this.all.length - 1)
|
||||
this.ws.pane = 'document'
|
||||
}
|
||||
|
||||
/** Back to the documents, as they were left. */
|
||||
leavePlan() {
|
||||
if (!this.planning) return
|
||||
this.ws.drafts.leaveCard()
|
||||
this.swap()
|
||||
// What was closed behind the plan — deleted, moved away — is gone from here too.
|
||||
this.prune()
|
||||
if (!this.current.empty) this.focusOn(this.focused)
|
||||
}
|
||||
|
||||
/** `b`, `t`, `C`, Enter on a card: to the plan screen, and that view on it focused. */
|
||||
async show(kind: View) {
|
||||
await this.loadView(kind)
|
||||
if (!this.view(kind)) {
|
||||
// The one empty tile there can be takes it; otherwise it goes beside.
|
||||
if (this.current.empty) this.current.kind = kind
|
||||
else this.all.push(new Tile(kind))
|
||||
}
|
||||
// Coming to the plan, every view on it is read again, not only this one.
|
||||
const kinds = this.planning ? [kind] : Tiles.PLAN
|
||||
await Promise.all(kinds.map((each) => this.loadView(each)))
|
||||
this.enterPlan()
|
||||
if (!this.view(kind)) this.all.push(new Tile(kind))
|
||||
this.focusOn(this.all.indexOf(this.view(kind)!))
|
||||
if (kind === 'board' && this.ws.cards.boardSpaceId === null) this.ws.say('no space has a board — :set board turns one on', 'error')
|
||||
}
|
||||
|
||||
/** The views `S` puts up together, in the order they are tiled. */
|
||||
static readonly PLAN: View[] = ['board', 'agenda', 'calendar']
|
||||
|
||||
/**
|
||||
* `S`: the board, the agenda and the calendar, tiled ahead of the
|
||||
* documents with the board focused — or, when all three are up, every view
|
||||
* put away again.
|
||||
*/
|
||||
/** `S`: the plan screen with the board focused, or back to the documents. */
|
||||
async togglePlan() {
|
||||
if (Tiles.PLAN.every((kind) => this.view(kind))) {
|
||||
for (const tile of this.all) if (tile.kind !== 'node') tile.clear()
|
||||
this.prune()
|
||||
if (!this.current.empty) this.focusOn(this.focused)
|
||||
return
|
||||
}
|
||||
await Promise.all(Tiles.PLAN.map((kind) => this.loadView(kind)))
|
||||
const current = this.current
|
||||
const missing = Tiles.PLAN.filter((kind) => !this.view(kind)).map((kind) => new Tile(kind))
|
||||
this.all = [...missing, ...this.all]
|
||||
this.focused = this.all.indexOf(current)
|
||||
this.prune()
|
||||
this.focusOn(this.all.indexOf(this.view('board')!))
|
||||
if (this.ws.cards.boardSpaceId === null) this.ws.say('no space has a board — :set board turns one on', 'error')
|
||||
if (this.planning) return this.leavePlan()
|
||||
await this.show('board')
|
||||
}
|
||||
|
||||
/** Read again what a view shows. */
|
||||
@@ -253,7 +283,7 @@ export class Tiles {
|
||||
}
|
||||
}
|
||||
|
||||
/** Every view on screen read again, after a space switch brought them. */
|
||||
/** Every view on the plan read again, after a space switch. */
|
||||
async loadViews() {
|
||||
if (this.view('board') || this.view('card')) await this.ws.cards.loadBoard()
|
||||
if (this.view('calendar')) await this.loadView('calendar')
|
||||
|
||||
@@ -5,9 +5,10 @@ import type { Workspace, Pane } from '../store.svelte'
|
||||
|
||||
/**
|
||||
* Where a space was left: the folder, the tiles, the row, the pane. A tile
|
||||
* is the id of the node it shows, or the name of the view. `openId` is the
|
||||
* focused tile's, all a spot held before there were tiles, and `open` the
|
||||
* documents alone, all it held before there were views.
|
||||
* is the id of the node it shows; the name of a view is what a spot held
|
||||
* while views were tiled among the documents, and is passed over now.
|
||||
* `openId` is the focused tile's, all a spot held before there were tiles,
|
||||
* and `open` the documents alone, all it held before there were views.
|
||||
*/
|
||||
type Spot = {
|
||||
parentId: number
|
||||
@@ -144,47 +145,49 @@ export class Tree {
|
||||
if (start) await this.enterSpace(start.id)
|
||||
}
|
||||
|
||||
/** `views` false: the views are leaving with the focus, and are not left behind. */
|
||||
#rememberSpot(views = true) {
|
||||
/** Where this space is left, its documents on screen. */
|
||||
#rememberSpot() {
|
||||
if (this.spaceId === null || this.parentId === null) return
|
||||
// A card's back is the card under the board's cursor, which is not kept.
|
||||
const kept = this.ws.tiles.all.filter((tile) => !tile.empty && (tile.kind === 'node' || (views && tile.kind !== 'card')))
|
||||
const kept = this.ws.tiles.all.filter((tile) => tile.open)
|
||||
this.#spots[this.spaceId] = {
|
||||
parentId: this.parentId,
|
||||
openId: this.ws.open?.id ?? null,
|
||||
cursor: this.cursor,
|
||||
pane: this.ws.pane,
|
||||
tiles: kept.map((tile) => (tile.kind === 'node' ? tile.open!.id : tile.kind)),
|
||||
tiles: kept.map((tile) => tile.open!.id),
|
||||
focused: Math.max(kept.indexOf(this.ws.tile), 0),
|
||||
}
|
||||
localStorage.setItem('diary:spots', JSON.stringify(this.#spots))
|
||||
}
|
||||
|
||||
async enterSpace(id: number) {
|
||||
const leaving = this.spaceId !== null && this.spaceId !== id
|
||||
/*
|
||||
* The views look at every board and the whole diary, so with one of them
|
||||
* focused they come along — moved, not copied — as the special workspace
|
||||
* once did: a digit on the board shows that space's board, and the
|
||||
* agenda stays beside it.
|
||||
* The views look at every board and the whole diary, so the plan screen
|
||||
* stays as it is — a digit on the board shows that space's board — and
|
||||
* it is the documents behind it that are this space's.
|
||||
*/
|
||||
let carried: Tile[] = []
|
||||
let carriedFocus: Tile | undefined
|
||||
const planning = this.ws.tiles.planning
|
||||
if (planning) this.ws.tiles.swap()
|
||||
try {
|
||||
await this.#enterSpace(id)
|
||||
} finally {
|
||||
if (planning) this.ws.tiles.swap()
|
||||
}
|
||||
await this.ws.tiles.loadViews()
|
||||
}
|
||||
|
||||
async #enterSpace(id: number) {
|
||||
const leaving = this.spaceId !== null && this.spaceId !== id
|
||||
if (leaving) {
|
||||
await this.ws.drafts.flush()
|
||||
const carry = this.ws.focus !== 'list' && this.ws.focus !== 'node'
|
||||
this.#rememberSpot(!carry)
|
||||
this.#rememberSpot()
|
||||
const from = this.spaces.findIndex((s) => s.id === this.spaceId)
|
||||
const to = this.spaces.findIndex((s) => s.id === id)
|
||||
this.ws.switchDirection = to >= from ? 1 : -1
|
||||
if (carry) {
|
||||
carried = this.ws.tiles.all.filter((tile) => tile.kind !== 'node' && tile.kind !== 'card')
|
||||
carriedFocus = this.ws.tile.kind === 'card' ? (this.ws.tiles.view('board') ?? carried[0]) : this.ws.tile
|
||||
}
|
||||
// Each space has tiles of its own; these come back with this one.
|
||||
for (const tile of this.ws.tiles.all) tile.stopAutosave()
|
||||
this.ws.tiles.replace(carried, carriedFocus)
|
||||
this.ws.pane = carried.length > 0 ? 'document' : 'list'
|
||||
this.ws.tiles.replace([])
|
||||
this.ws.pane = 'list'
|
||||
}
|
||||
this.spaceId = id
|
||||
localStorage.setItem('diary:space', String(id))
|
||||
@@ -204,12 +207,7 @@ export class Tree {
|
||||
spot.tiles ?? spot.open ?? (spot.openId !== null && this.nodes.some((n) => n.id === spot.openId) ? [spot.openId] : [])
|
||||
const restored: Tile[] = []
|
||||
for (const entry of entries) {
|
||||
if (typeof entry === 'string') {
|
||||
let tile = this.ws.tiles.view(entry)
|
||||
if (!tile) this.ws.tiles.all.push((tile = new Tile(entry)))
|
||||
restored.push(tile)
|
||||
continue
|
||||
}
|
||||
if (typeof entry === 'string') continue
|
||||
const empty = this.ws.tiles.all.findIndex((tile) => tile.empty)
|
||||
if (empty >= 0) this.ws.tiles.focused = empty
|
||||
else {
|
||||
@@ -224,25 +222,23 @@ export class Tree {
|
||||
}
|
||||
}
|
||||
this.ws.tiles.prune()
|
||||
const back = carriedFocus ?? restored[Math.min(spot.focused ?? 0, restored.length - 1)]
|
||||
const back = restored[Math.min(spot.focused ?? 0, restored.length - 1)]
|
||||
if (back && this.ws.tiles.all.includes(back)) {
|
||||
this.ws.tiles.focusOn(this.ws.tiles.all.indexOf(back))
|
||||
if (!carriedFocus) this.ws.pane = spot.pane
|
||||
this.ws.pane = spot.pane
|
||||
} else {
|
||||
this.cursor = Math.min(spot.cursor, Math.max(this.nodes.length - 1, 0))
|
||||
}
|
||||
}
|
||||
this.ws.tiles.trail = []
|
||||
await this.ws.tiles.loadViews()
|
||||
}
|
||||
|
||||
/** Show the children of `id`, which must be a space or a folder. */
|
||||
async goTo(id: number, keep?: number) {
|
||||
await this.ws.drafts.flush()
|
||||
const folder = await api.getNode(id)
|
||||
// Walking into another space by a link leaves this one where it was; the
|
||||
// views stay on screen, so they go along rather than stay behind too.
|
||||
if (this.spaceId !== null && folder.path[0]?.id !== this.spaceId) this.#rememberSpot(false)
|
||||
// Walking into another space by a link leaves this one where it was.
|
||||
if (this.spaceId !== null && folder.path[0]?.id !== this.spaceId) this.#rememberSpot()
|
||||
this.parentId = id
|
||||
this.parentSealed = folder.sealed
|
||||
this.query = ''
|
||||
@@ -347,7 +343,14 @@ export class Tree {
|
||||
}
|
||||
|
||||
async openNode(id: number, edit = false) {
|
||||
if (this.ws.open?.id !== id) await this.ws.drafts.flushTile()
|
||||
/*
|
||||
* The plan screen holds the views alone: what is opened from it — a
|
||||
* card's notes, a day of the calendar, a search — opens on the documents'
|
||||
* screen, beside what is there, and `S` goes back.
|
||||
*/
|
||||
const fromPlan = this.ws.tiles.planning
|
||||
if (fromPlan) this.ws.tiles.leavePlan()
|
||||
else if (this.ws.open?.id !== id) await this.ws.drafts.flushTile()
|
||||
|
||||
const node = await api.getNode(id)
|
||||
if (isContainer(node)) {
|
||||
@@ -362,10 +365,7 @@ export class Tree {
|
||||
if (edit && node.kind === 'document') this.ws.editing = true
|
||||
return
|
||||
}
|
||||
// A view is never put away by what is opened from it — a card's notes,
|
||||
// a day of the calendar, a search from the board: that opens beside it,
|
||||
// and closing it goes back there.
|
||||
if (this.ws.tile.kind !== 'node') this.ws.tiles.add()
|
||||
if (fromPlan && !this.ws.tile.empty) this.ws.tiles.add()
|
||||
|
||||
this.ws.open = node
|
||||
this.ws.draft = { name: node.name, body: node.body, created_at: node.created_at }
|
||||
@@ -549,7 +549,7 @@ export class Tree {
|
||||
async deleteSpace(id: number) {
|
||||
const { removed } = await api.deleteNode(id)
|
||||
this.lastDeleted = id
|
||||
for (const tile of this.ws.tiles.all) tile.clear()
|
||||
for (const tile of this.ws.tiles.every) if (tile.kind === 'node') tile.clear()
|
||||
this.ws.tiles.prune()
|
||||
this.ws.cards.rawBoard = null
|
||||
this.ws.cards.boardSpaceId = null
|
||||
@@ -585,7 +585,7 @@ export class Tree {
|
||||
*/
|
||||
async moveNodes(nodes: NodeSummary[], parent: number, where = 'here') {
|
||||
if (nodes.length === 0) return this.ws.say('nothing marked — press m on a row first', 'error')
|
||||
const leaving = this.ws.tiles.all.filter((tile) => nodes.some((n) => n.id === tile.open?.id))
|
||||
const leaving = this.ws.tiles.every.filter((tile) => nodes.some((n) => n.id === tile.open?.id))
|
||||
for (const tile of leaving) {
|
||||
await this.ws.drafts.flushTile(tile)
|
||||
tile.clear()
|
||||
|
||||
@@ -51,6 +51,6 @@ export class Vault {
|
||||
failed > 0 || shared > 0 ? 'error' : undefined,
|
||||
)
|
||||
await this.ws.tree.refresh()
|
||||
for (const tile of this.ws.tiles.all) if (tile.open?.kind === 'file') tile.open = await api.getNode(tile.open.id)
|
||||
for (const tile of this.ws.tiles.every) if (tile.open?.kind === 'file') tile.open = await api.getNode(tile.open.id)
|
||||
}
|
||||
}
|
||||
@@ -160,7 +160,7 @@ export class Workspace {
|
||||
// the draft included, so pick up where it stopped rather than start over.
|
||||
if (this.tree.spaces.length > 0) {
|
||||
await this.tree.refresh()
|
||||
for (const tile of this.tiles.all) {
|
||||
for (const tile of this.tiles.every) {
|
||||
if (tile.open && tile.dirty) await this.guard(() => this.drafts.save({ quiet: true, tile }))
|
||||
}
|
||||
if (this.drafts.writing?.dirty) await this.guard(() => this.drafts.saveWriting())
|
||||
@@ -178,7 +178,8 @@ export class Workspace {
|
||||
this.vault.status = null
|
||||
this.tree.spaces = []
|
||||
this.tree.nodes = []
|
||||
for (const tile of this.tiles.all) tile.clear()
|
||||
if (this.tiles.planning) this.tiles.leavePlan()
|
||||
for (const tile of this.tiles.every) tile.clear()
|
||||
this.tiles.prune()
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ export class Writing {
|
||||
dirty = $state(false)
|
||||
/** The text was found in this browser, newer than what the server has. */
|
||||
readonly recovered: boolean
|
||||
/** Opened to be typed into straight away: the editor starts in insert mode, at the end. */
|
||||
append = false
|
||||
|
||||
#write: (text: string) => Promise<void>
|
||||
#fail: (error: unknown, retry: number) => void
|
||||
|
||||
Reference in new issue
Block a user