Files
narl-workspace/web/src/lib/StatusBar.svelte
T
nvrl a27d5de9c7 phone app: installable, opens offline, takes the share sheet onto the scratchpad, back lets go of one thing
A manifest with a 192, a 512 and a maskable icon, and a hand-written service
worker built beside the app by a small vite plugin: it keeps index.html, the
logo, the entry with its imports and the latin fonts, keeps each hashed asset
once fetched, asks the server for pages first and answers from the cache when
it is down, and never touches /api. The CSP names worker-src and manifest-src.

The share target posts to /share, which the worker catches: the text waits in
a cache and becomes a scratchpad note once someone is logged in, with the
drawer down on it. On a touch screen one guard entry in the history makes back
let go of one thing at a time — a prompt, the line, a sheet, the drawer, the
editor, a tile, then a folder up — and leave only at a space's root. The bar
and the band grow by the safe-area insets. The login no longer says the server
is unreachable once it answers again with the session gone.
2026-10-22 20:10:13 +02:00

92 lines
3.2 KiB
Svelte

<script lang="ts">
import Rail from './Rail.svelte'
import { workspace } from './store.svelte'
import { isTouchDevice, slugPath } from './util'
/** A key waiting for its second half, like vim's `showcmd`. */
const { pending = '' }: { pending?: string } = $props()
const mode = $derived(
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.
const file = $derived(
workspace.open ? slugPath(workspace.open.path) : `${workspace.tree.here || '~'}/`,
)
const lines = $derived(workspace.editing ? workspace.draft.body.split('\n').length : null)
</script>
<footer class="status">
<span
class="mode"
class:editing={workspace.editing}
class:insert={mode === 'INSERT' || mode === 'EDIT'}
class:visual={mode.startsWith('VISUAL')}
>
{mode}
</span>
<span class="file">{file}{#if workspace.dirty}<span class="dirty"> [+]</span>{/if}</span>
<span class="msg" class:alarm={workspace.flash?.kind === 'error'}>{workspace.flash?.text ?? ''}</span>
{#if pending}<span class="pending" aria-live="polite">{pending}</span>{/if}
{#if workspace.tiles.all.length > 1}<span class="flag" title=":ls lists the tiles">tile {workspace.tiles.focused + 1}/{workspace.tiles.all.length}</span>{/if}
{#if workspace.fullscreen}<span class="flag">full</span>{/if}
{#if workspace.vault.status?.unlocked}<span class="flag vault" title="the vault is open — :vault lock closes it">vault open</span>{/if}
{#if lines !== null}<span class="readout">{lines}<span class="unit">L</span></span>{/if}
<span class="hint">
{isTouchDevice ? 'tap : for commands' : '? help · : command · / search'}
</span>
<div class="tools"><Rail dock /></div>
</footer>
<style>
.mode.insert { background: var(--light); color: var(--ink); }
.mode.visual { background: var(--light-dim); color: var(--ink); }
.file { flex: none; color: var(--subtle); }
.dirty { color: var(--light); }
.msg { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--fg); }
.msg.alarm { color: var(--alarm); }
.pending { flex: none; color: var(--light); font-family: var(--font-mono); }
.flag { color: var(--light); letter-spacing: 0.18em; text-transform: uppercase; }
.tools { display: none; }
/*
* On a phone the band is the dock. The mode only shows while writing, and
* messages float just above the band as a toast, so they never squeeze the
* doors.
*/
@media (max-width: 767px) {
.status { padding: 0 6px var(--inset-bottom); gap: 6px; overflow: visible; }
.hint, .file, .readout, .flag, .pending { display: none; }
.mode:not(.editing) { display: none; }
.tools { display: flex; flex: 1; height: 100%; }
.msg {
position: absolute;
left: 50%;
bottom: calc(100% + 8px);
max-width: calc(100% - 24px);
padding: 6px 12px;
border: 1px solid var(--overlay);
border-radius: var(--radius-s);
background: var(--surface);
box-shadow: 0 6px 24px rgb(0 0 0 / 0.45);
transform: translateX(-50%);
z-index: 20;
}
.msg:empty { display: none; }
}
</style>