mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-03 16:41:27 +01:00
redesign
This commit is contained in:
1 parent
747332153c
commit
8a37ca5dfe
25 files changed
+2050
-976
No files matched your search
@@ -29,6 +29,11 @@ WORKSPACE_SECURE_COOKIE=0
|
|||||||
# <ssh-ed25519 ...>` in the app, then `sshfs -P <port> anything@host:/ mnt/`
|
# <ssh-ed25519 ...>` in the app, then `sshfs -P <port> anything@host:/ mnt/`
|
||||||
# (the username is ignored — only the key decides who gets in).
|
# (the username is ignored — only the key decides who gets in).
|
||||||
#
|
#
|
||||||
|
# On the mount, `mkdir` at the root makes a space and anywhere else a folder;
|
||||||
|
# a new `.md` file is a new document. A leading `# Heading` is the document's
|
||||||
|
# title. Editor temp, swap and backup files are kept in memory for the session
|
||||||
|
# only, and renaming one onto a `.md` name saves it into that document.
|
||||||
|
#
|
||||||
# Under Docker, bind 0.0.0.0 (not 127.0.0.1) as the app listens inside the
|
# Under Docker, bind 0.0.0.0 (not 127.0.0.1) as the app listens inside the
|
||||||
# container — the host-side address in compose.yml's `ports:` is what
|
# container — the host-side address in compose.yml's `ports:` is what
|
||||||
# actually restricts who can reach it.
|
# actually restricts who can reach it.
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ The desktop UI is modal. In the browse pane:
|
|||||||
Inside the editor the full vim keymap is live — `w`/`b`, `dd`, `ciw`, visual
|
Inside the editor the full vim keymap is live — `w`/`b`, `dd`, `ciw`, visual
|
||||||
mode, macros, `:w` to write. `:help` lists every ex command; the useful ones are
|
mode, macros, `:w` to write. `:help` lists every ex command; the useful ones are
|
||||||
`:w`, `:wq`, `:q!`, `:new [yyyy-mm-dd]`, `:date`, `:name`, `:share`, `:link`,
|
`:w`, `:wq`, `:q!`, `:new [yyyy-mm-dd]`, `:date`, `:name`, `:share`, `:link`,
|
||||||
`:upload`, `:media`, `:search`, `:backup`, `:set theme=mocha|green|amber|ice` and
|
`:upload`, `:media`, `:search`, `:backup` and
|
||||||
`:set novim`. The tree has its own: `:space [name]` switches (`:space! <name>`
|
`:set novim`. The tree has its own: `:space [name]` switches (`:space! <name>`
|
||||||
makes one, `:rmspace [name]` deletes one and everything in it), `:mkdir <name>`
|
makes one, `:rmspace [name]` deletes one and everything in it), `:mkdir <name>`
|
||||||
makes a folder, `:cd <name|..|/>` walks it and `:mv <folder|..>` moves what is
|
makes a folder, `:cd <name|..|/>` walks it and `:mv <folder|..>` moves what is
|
||||||
|
|||||||
+13
-6
@@ -300,10 +300,18 @@ pub async fn create_space(
|
|||||||
return Err(AppError::BadRequest("a space needs a name".into()));
|
return Err(AppError::BadRequest("a space needs a name".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let slug = unique_slug(&state.db, None, &slugify(name), None).await?;
|
let id = insert_space(&state.db, name, input.has_board.unwrap_or(true)).await?;
|
||||||
|
state.backup.signal();
|
||||||
|
load(&state, id).await.map(Json)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A new space, last in the switcher. Shared by the API and by `mkdir` at the
|
||||||
|
/// root of the SFTP mount, which is why it takes a pool and not the state.
|
||||||
|
pub async fn insert_space(db: &SqlitePool, name: &str, has_board: bool) -> AppResult<i64> {
|
||||||
|
let slug = unique_slug(db, None, &slugify(name), None).await?;
|
||||||
let now = crate::now();
|
let now = crate::now();
|
||||||
let position: i64 = sqlx::query("SELECT COALESCE(MAX(position), -1) + 1 AS next FROM nodes WHERE parent_id IS NULL")
|
let position: i64 = sqlx::query("SELECT COALESCE(MAX(position), -1) + 1 AS next FROM nodes WHERE parent_id IS NULL")
|
||||||
.fetch_one(&state.db)
|
.fetch_one(db)
|
||||||
.await?
|
.await?
|
||||||
.get("next");
|
.get("next");
|
||||||
|
|
||||||
@@ -315,14 +323,13 @@ pub async fn create_space(
|
|||||||
.bind(name)
|
.bind(name)
|
||||||
.bind(&slug)
|
.bind(&slug)
|
||||||
.bind(position)
|
.bind(position)
|
||||||
.bind(i64::from(input.has_board.unwrap_or(true)))
|
.bind(i64::from(has_board))
|
||||||
.bind(now)
|
.bind(now)
|
||||||
.fetch_one(&state.db)
|
.fetch_one(db)
|
||||||
.await?
|
.await?
|
||||||
.get("id");
|
.get("id");
|
||||||
|
|
||||||
state.backup.signal();
|
Ok(id)
|
||||||
load(&state, id).await.map(Json)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list(
|
pub async fn list(
|
||||||
|
|||||||
+822
-327
File diff suppressed because it is too large.
Load diff
@@ -10,6 +10,8 @@
|
|||||||
"@codemirror/language": "^6.10.8",
|
"@codemirror/language": "^6.10.8",
|
||||||
"@codemirror/state": "^6.5.1",
|
"@codemirror/state": "^6.5.1",
|
||||||
"@codemirror/view": "^6.36.2",
|
"@codemirror/view": "^6.36.2",
|
||||||
|
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||||
|
"@fontsource-variable/noto-sans": "^5.3.0",
|
||||||
"@lezer/highlight": "^1.2.1",
|
"@lezer/highlight": "^1.2.1",
|
||||||
"@replit/codemirror-vim": "^6.2.1",
|
"@replit/codemirror-vim": "^6.2.1",
|
||||||
"dompurify": "^3.2.4",
|
"dompurify": "^3.2.4",
|
||||||
@@ -100,6 +102,10 @@
|
|||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||||
|
|
||||||
|
"@fontsource-variable/jetbrains-mono": ["@fontsource-variable/jetbrains-mono@5.3.0", "", {}, "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw=="],
|
||||||
|
|
||||||
|
"@fontsource-variable/noto-sans": ["@fontsource-variable/noto-sans@5.3.0", "", {}, "sha512-e3ZuxzY9AL2psXPZlhwhnYrHUHv6PMoyK9MQ7croF0CUgURLqu9EMjW7q2FgVLwij1tjLsIHQrnwQ8aUcg4BiA=="],
|
||||||
|
|
||||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|||||||
+2
-2
@@ -4,9 +4,9 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<meta name="color-scheme" content="dark" />
|
<meta name="color-scheme" content="dark" />
|
||||||
<meta name="theme-color" content="#0b0e0d" />
|
<meta name="theme-color" content="#0c0d0b" />
|
||||||
<title>~/workspace</title>
|
<title>~/workspace</title>
|
||||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230b0e0d'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='16' fill='%2354f2a1' text-anchor='middle'%3E~%3C/text%3E%3C/svg%3E" />
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%230c0d0b'/%3E%3Ccircle cx='32' cy='32' r='15' fill='none' stroke='%23eef1ea' stroke-width='3'/%3E%3Ccircle cx='32' cy='32' r='7' fill='%23eef1ea'/%3E%3C/svg%3E" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
"@codemirror/language": "^6.10.8",
|
"@codemirror/language": "^6.10.8",
|
||||||
"@codemirror/state": "^6.5.1",
|
"@codemirror/state": "^6.5.1",
|
||||||
"@codemirror/view": "^6.36.2",
|
"@codemirror/view": "^6.36.2",
|
||||||
|
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||||
|
"@fontsource-variable/noto-sans": "^5.3.0",
|
||||||
"@lezer/highlight": "^1.2.1",
|
"@lezer/highlight": "^1.2.1",
|
||||||
"@replit/codemirror-vim": "^6.2.1",
|
"@replit/codemirror-vim": "^6.2.1",
|
||||||
"dompurify": "^3.2.4",
|
"dompurify": "^3.2.4",
|
||||||
|
|||||||
+99
-42
@@ -1,12 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { hooks, runCommand } from './lib/commands'
|
import { hooks, runCommand } from './lib/commands'
|
||||||
import { embedSnippet } from './lib/markdown'
|
import { embedSnippet } from './lib/markdown'
|
||||||
|
import Bar from './lib/Bar.svelte'
|
||||||
import Board from './lib/Board.svelte'
|
import Board from './lib/Board.svelte'
|
||||||
import CommandLine from './lib/CommandLine.svelte'
|
import CommandLine from './lib/CommandLine.svelte'
|
||||||
import DocumentPane from './lib/DocumentPane.svelte'
|
import DocumentPane from './lib/DocumentPane.svelte'
|
||||||
import Help from './lib/Help.svelte'
|
import Help from './lib/Help.svelte'
|
||||||
import Login from './lib/Login.svelte'
|
import Login from './lib/Login.svelte'
|
||||||
import MediaBrowser from './lib/MediaBrowser.svelte'
|
import MediaBrowser from './lib/MediaBrowser.svelte'
|
||||||
|
import Rail from './lib/Rail.svelte'
|
||||||
import Shared from './lib/Shared.svelte'
|
import Shared from './lib/Shared.svelte'
|
||||||
import Sidebar from './lib/Sidebar.svelte'
|
import Sidebar from './lib/Sidebar.svelte'
|
||||||
import StatusBar from './lib/StatusBar.svelte'
|
import StatusBar from './lib/StatusBar.svelte'
|
||||||
@@ -31,10 +33,6 @@
|
|||||||
|
|
||||||
if (!shareToken) void workspace.boot()
|
if (!shareToken) void workspace.boot()
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
document.documentElement.dataset.theme = workspace.theme
|
|
||||||
})
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
folderInput?.setAttribute('webkitdirectory', '')
|
folderInput?.setAttribute('webkitdirectory', '')
|
||||||
})
|
})
|
||||||
@@ -90,6 +88,18 @@
|
|||||||
await workspace.guard(() => workspace.importFolder(files))
|
await workspace.guard(() => workspace.importFolder(files))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What the empty pane offers, in the key/value shape of a status readout.
|
||||||
|
const offers: [string, string][] = [
|
||||||
|
['o', 'write a new document'],
|
||||||
|
['O', 'make a folder'],
|
||||||
|
['R', 'rename what is marked'],
|
||||||
|
['b', "this space's board"],
|
||||||
|
['j / k', 'browse, l opens'],
|
||||||
|
['h', 'up one level'],
|
||||||
|
['/', 'search this space'],
|
||||||
|
['?', 'help'],
|
||||||
|
]
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The board reads the same keys as the list, one dimension wider: h/l walk
|
* The board reads the same keys as the list, one dimension wider: h/l walk
|
||||||
* the lists, j/k the cards, and the shifted pair takes the card along. Only
|
* the lists, j/k the cards, and the shifted pair takes the card along. Only
|
||||||
@@ -361,40 +371,45 @@
|
|||||||
{#if shareToken}
|
{#if shareToken}
|
||||||
<Shared token={shareToken} shareKey={shareKey} />
|
<Shared token={shareToken} shareKey={shareKey} />
|
||||||
{:else if workspace.booting}
|
{:else if workspace.booting}
|
||||||
<div class="boot faint">booting ~/workspace…</div>
|
<div class="boot label">booting ~/workspace<span class="cursor">_</span></div>
|
||||||
{:else if !workspace.user}
|
{:else if !workspace.user}
|
||||||
<Login />
|
<Login />
|
||||||
{:else}
|
{:else}
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
<div class="shell" bind:this={shell} tabindex="-1">
|
<div class="shell" bind:this={shell} tabindex="-1">
|
||||||
{#if workspace.view === 'board'}
|
<Bar />
|
||||||
<!-- The board takes the whole width: its columns are the two panes. -->
|
<Rail />
|
||||||
<div class="panes board"><Board /></div>
|
|
||||||
{:else}
|
<!-- The opening in the frame: everything that scrolls is seen through it. -->
|
||||||
<div class="panes" class:reading={!!workspace.open}>
|
<main class="porthole">
|
||||||
<div class="list"><Sidebar /></div>
|
{#if workspace.view === 'board'}
|
||||||
<div class="entry">
|
<!-- The board takes the whole width: its columns are the two panes. -->
|
||||||
{#if workspace.open}
|
<div class="panes board"><Board /></div>
|
||||||
<DocumentPane />
|
{:else}
|
||||||
{:else}
|
<div class="panes" class:reading={!!workspace.open}>
|
||||||
<div class="placeholder faint">
|
<div class="list"><Sidebar /></div>
|
||||||
<pre>{` ┌─────────────────────────────┐
|
<div class="entry">
|
||||||
│ nothing open │
|
{#if workspace.open}
|
||||||
│ │
|
<DocumentPane />
|
||||||
│ o write a new document │
|
{:else}
|
||||||
│ O make a folder │
|
<div class="placeholder">
|
||||||
│ R rename what is marked │
|
<div class="standby resolve">
|
||||||
│ b this space's board │
|
<h2 class="label rule">nothing open</h2>
|
||||||
│ j/k browse, l opens │
|
<dl>
|
||||||
│ h up one level │
|
{#each offers as [key, what] (key)}
|
||||||
│ / search this space │
|
<dt>{key}</dt>
|
||||||
│ ? help │
|
<dd>{what}</dd>
|
||||||
└─────────────────────────────┘`}</pre>
|
{/each}
|
||||||
|
<dt>status</dt>
|
||||||
|
<dd class="light">standing by<span class="cursor">_</span></dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</main>
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
|
|
||||||
@@ -426,25 +441,63 @@
|
|||||||
|
|
||||||
.shell {
|
.shell {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
background: var(--hull);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The porthole: a rounded opening in the hull, outlined by a single rim. The
|
||||||
|
* rim is drawn on top by ::after, so the panes scroll underneath it the way
|
||||||
|
* windows sit under the frame on the desktop.
|
||||||
|
*/
|
||||||
|
.porthole {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--bar-h);
|
||||||
|
right: var(--rail-w);
|
||||||
|
bottom: var(--status-h);
|
||||||
|
left: var(--frame);
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--frame-radius);
|
||||||
|
background:
|
||||||
|
radial-gradient(120% 70% at 30% 0%, #141713 0%, transparent 60%),
|
||||||
|
var(--base);
|
||||||
|
}
|
||||||
|
.porthole::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
box-shadow: inset 0 0 0 1px var(--rim);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panes {
|
.panes {
|
||||||
flex: 1;
|
height: 100%;
|
||||||
min-height: 0;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(30ch, 22vw) 1fr;
|
grid-template-columns: minmax(32ch, 24vw) 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panes.board { grid-template-columns: 1fr; }
|
.panes.board { grid-template-columns: 1fr; }
|
||||||
|
|
||||||
.list, .entry { min-width: 0; overflow: hidden; }
|
.list, .entry { min-width: 0; min-height: 0; overflow: hidden; }
|
||||||
|
|
||||||
.placeholder { display: grid; place-items: center; height: 100%; }
|
.placeholder { display: grid; place-items: center; height: 100%; padding: 24px; }
|
||||||
.placeholder pre { margin: 0; font-size: 12px; line-height: 1.5; }
|
.standby { width: min(100%, 44ch); }
|
||||||
|
.standby h2 { margin: 0 0 18px; font-weight: 400; }
|
||||||
|
.standby dl {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 7rem 1fr;
|
||||||
|
gap: 6px 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
/* Keys keep their case: `o` and `O` are different keys. */
|
||||||
|
.standby dt { color: var(--light); letter-spacing: 0.08em; }
|
||||||
|
.standby dt:last-of-type { color: var(--muted); letter-spacing: 0.18em; text-transform: uppercase; }
|
||||||
|
.standby dd { margin: 0; color: var(--fg); }
|
||||||
|
.standby dd.light { color: var(--light); }
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -452,10 +505,13 @@
|
|||||||
z-index: 30;
|
z-index: 30;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 24px;
|
padding: calc(var(--bar-h) + 8px) 24px 24px;
|
||||||
background: color-mix(in srgb, var(--bg) 86%, transparent);
|
background: color-mix(in oklab, var(--ink) 72%, transparent);
|
||||||
|
animation: fade var(--t-base) var(--ease-glide) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes fade { from { opacity: 0; } }
|
||||||
|
|
||||||
.hidden { display: none; }
|
.hidden { display: none; }
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
@@ -463,5 +519,6 @@
|
|||||||
.panes .entry { display: none; }
|
.panes .entry { display: none; }
|
||||||
.panes.reading .list { display: none; }
|
.panes.reading .list { display: none; }
|
||||||
.panes.reading .entry { display: block; }
|
.panes.reading .entry { display: block; }
|
||||||
|
.overlay { padding: 12px; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+270
-131
@@ -1,67 +1,64 @@
|
|||||||
/* ── palette ─────────────────────────────────────────────────────────────── */
|
/* ── palette ─────────────────────────────────────────────────────────────── */
|
||||||
/* Catppuccin Mocha, accented with the palette's red. */
|
/*
|
||||||
:root, [data-theme='mocha'] {
|
* YoRHa dark, the palette of the desktop this runs on (quickshell Theme.qml,
|
||||||
--bg: #1e1e2e;
|
* hypr palette.lua) and of narl.io. A green-black ramp, sage-grey text, and
|
||||||
--bg-alt: #181825;
|
* only two colours that mean anything: `light` is focus, selection and
|
||||||
--bg-lift: #313244;
|
* whatever is live; `alarm` is something wrong. Nothing else carries hue.
|
||||||
--line: #45475a;
|
*/
|
||||||
--fg: #cdd6f4;
|
:root {
|
||||||
--fg-dim: #a6adc8;
|
--ink: #090a08;
|
||||||
--fg-faint: #6c7086;
|
--hull: #0c0d0b;
|
||||||
--accent: #f38ba8;
|
--base: #0f110e;
|
||||||
--accent-dim: #a35b70;
|
--surface: #161915;
|
||||||
--warn: #f9e2af;
|
--surface-alt: #20241f;
|
||||||
--error: #eba0ac;
|
--rim: #2a2f28;
|
||||||
--select: #4a3543;
|
--overlay: #383e35;
|
||||||
|
--outline: #545c52;
|
||||||
|
|
||||||
|
--fg: #ccd2cb;
|
||||||
|
--subtle: #9fa7a2;
|
||||||
|
--muted: #79847a;
|
||||||
|
|
||||||
|
--light: #eef1ea;
|
||||||
|
--light-dim: #8e978d;
|
||||||
|
--light-soft: #bcc1bf;
|
||||||
|
--alarm: #c8684f;
|
||||||
|
|
||||||
|
--font-sans: "Noto Sans Variable", "Noto Sans", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-mono: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, monospace;
|
||||||
|
|
||||||
|
/* The frame: a bar on top, a rail on the right, a band at the bottom, and a
|
||||||
|
rounded opening between them that the panes are seen through. */
|
||||||
|
--bar-h: 40px;
|
||||||
|
--rail-w: 48px;
|
||||||
|
--status-h: 28px;
|
||||||
|
--frame: 8px;
|
||||||
|
--frame-radius: 18px;
|
||||||
|
|
||||||
|
/* Concentric with the frame: 18 outside, 12 one step in, 6 for the small things. */
|
||||||
|
--radius-s: 6px;
|
||||||
|
--radius-m: 10px;
|
||||||
|
--radius-l: 12px;
|
||||||
|
|
||||||
|
/* Quick start, long settle, no overshoot. */
|
||||||
|
--ease-snap: cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
--ease-glide: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
--ease-in: cubic-bezier(0.55, 0, 0.9, 0.35);
|
||||||
|
--t-fast: 120ms;
|
||||||
|
--t-base: 220ms;
|
||||||
|
--t-slow: 380ms;
|
||||||
|
--t-lazy: 700ms;
|
||||||
|
|
||||||
--font: ui-monospace, "JetBrains Mono", "Fira Code", "SF Mono", Menlo, Consolas, monospace;
|
|
||||||
--radius: 2px;
|
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme='green'] {
|
@media (max-width: 767px) {
|
||||||
--bg: #0b0e0d;
|
:root {
|
||||||
--bg-alt: #101513;
|
--rail-w: var(--frame);
|
||||||
--bg-lift: #161d1a;
|
--status-h: 40px;
|
||||||
--line: #1e2b26;
|
--frame: 6px;
|
||||||
--fg: #cfe3d9;
|
--frame-radius: 14px;
|
||||||
--fg-dim: #6f8a7f;
|
}
|
||||||
--fg-faint: #3f5049;
|
|
||||||
--accent: #54f2a1;
|
|
||||||
--accent-dim: #2f9c69;
|
|
||||||
--warn: #f2c14e;
|
|
||||||
--error: #ff6b6b;
|
|
||||||
--select: #1c3a2c;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme='amber'] {
|
|
||||||
--bg: #0e0b06;
|
|
||||||
--bg-alt: #151009;
|
|
||||||
--bg-lift: #1d160c;
|
|
||||||
--line: #2e2413;
|
|
||||||
--fg: #e8d5ae;
|
|
||||||
--fg-dim: #8d7550;
|
|
||||||
--fg-faint: #4d3f28;
|
|
||||||
--accent: #ffb454;
|
|
||||||
--accent-dim: #b57a25;
|
|
||||||
--warn: #f2c14e;
|
|
||||||
--error: #ff6b6b;
|
|
||||||
--select: #3a2a10;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme='ice'] {
|
|
||||||
--bg: #080b10;
|
|
||||||
--bg-alt: #0d1219;
|
|
||||||
--bg-lift: #121a24;
|
|
||||||
--line: #1c2836;
|
|
||||||
--fg: #cddbe8;
|
|
||||||
--fg-dim: #6d8399;
|
|
||||||
--fg-faint: #3d4f60;
|
|
||||||
--accent: #7cc7ff;
|
|
||||||
--accent-dim: #3d84b8;
|
|
||||||
--warn: #f2c14e;
|
|
||||||
--error: #ff6b6b;
|
|
||||||
--select: #17324a;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── base ────────────────────────────────────────────────────────────────── */
|
/* ── base ────────────────────────────────────────────────────────────────── */
|
||||||
@@ -73,10 +70,10 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--bg);
|
background: var(--hull);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
font-family: var(--font);
|
font-family: var(--font-mono);
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -85,118 +82,260 @@ body {
|
|||||||
|
|
||||||
#app { height: 100%; }
|
#app { height: 100%; }
|
||||||
|
|
||||||
a { color: var(--accent); text-decoration: none; border-bottom: 1px solid var(--accent-dim); }
|
a {
|
||||||
a:hover { background: var(--select); }
|
color: var(--fg);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: var(--overlay);
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
text-underline-offset: 4px;
|
||||||
|
transition: color var(--t-base) var(--ease-glide), text-decoration-color var(--t-base) var(--ease-glide);
|
||||||
|
}
|
||||||
|
a:hover { color: var(--light); text-decoration-color: var(--light); }
|
||||||
|
a:focus-visible { background: var(--light); color: var(--ink); text-decoration: none; outline: none; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A button rests as a hairline and a label. Hover lifts it a step and lights
|
||||||
|
* the label; only focus inverts — the NieR cursor, light fill and ink text.
|
||||||
|
*/
|
||||||
button {
|
button {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: var(--fg-dim);
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--subtle);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid color-mix(in oklab, var(--outline) 55%, transparent);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius-s);
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color var(--t-base) var(--ease-glide),
|
||||||
|
color var(--t-base) var(--ease-glide),
|
||||||
|
border-color var(--t-base) var(--ease-glide);
|
||||||
}
|
}
|
||||||
button:hover { color: var(--accent); border-color: var(--accent-dim); }
|
button:hover { background: var(--surface-alt); color: var(--light); border-color: var(--outline); }
|
||||||
button:focus-visible, input:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }
|
button:active { transform: translateY(1px); }
|
||||||
|
button:focus-visible { background: var(--light); color: var(--ink); border-color: var(--light); outline: none; }
|
||||||
|
button:disabled { color: var(--muted); cursor: default; }
|
||||||
|
|
||||||
input, textarea {
|
input, textarea {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
background: var(--bg-alt);
|
background: var(--surface);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--rim);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius-s);
|
||||||
padding: 4px 8px;
|
padding: 6px 10px;
|
||||||
|
transition: border-color var(--t-base) var(--ease-glide);
|
||||||
}
|
}
|
||||||
input::placeholder { color: var(--fg-faint); }
|
input:focus, textarea:focus { border-color: var(--light-dim); outline: none; }
|
||||||
|
input::placeholder { color: var(--muted); }
|
||||||
|
|
||||||
::selection { background: var(--select); color: var(--fg); }
|
::selection { background: var(--light); color: var(--ink); }
|
||||||
|
|
||||||
/* thin, unobtrusive scrollbars everywhere */
|
/* thin, unobtrusive scrollbars everywhere */
|
||||||
* { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
|
* { scrollbar-width: thin; scrollbar-color: var(--overlay) transparent; }
|
||||||
*::-webkit-scrollbar { width: 8px; height: 8px; }
|
*::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||||
*::-webkit-scrollbar-thumb { background: var(--line); }
|
*::-webkit-scrollbar-thumb { background: var(--overlay); border-radius: 4px; }
|
||||||
*::-webkit-scrollbar-track { background: transparent; }
|
*::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
|
||||||
.dim { color: var(--fg-dim); }
|
.subtle { color: var(--subtle); }
|
||||||
.faint { color: var(--fg-faint); }
|
.muted { color: var(--muted); }
|
||||||
.accent { color: var(--accent); }
|
.light { color: var(--light); }
|
||||||
.error { color: var(--error); }
|
.alarm { color: var(--alarm); }
|
||||||
|
|
||||||
|
/* ── vocabulary ──────────────────────────────────────────────────────────── */
|
||||||
|
/* A caption: small, mono, upper case and spaced out. */
|
||||||
|
.label {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A caption followed by a hairline that runs to the end of the row. */
|
||||||
|
.rule {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.rule::after {
|
||||||
|
content: '';
|
||||||
|
height: 1px;
|
||||||
|
flex: 1;
|
||||||
|
background: var(--overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The square that says "this one": overlay at rest, light when it matters. */
|
||||||
|
.marker {
|
||||||
|
display: inline-block;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
flex: none;
|
||||||
|
background: var(--overlay);
|
||||||
|
transition: background-color var(--t-base) var(--ease-glide);
|
||||||
|
}
|
||||||
|
|
||||||
|
.display {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-weight: 200;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The blinking block after "standing by". */
|
||||||
|
.cursor { margin-left: 2px; animation: blink 1.1s steps(1) infinite; }
|
||||||
|
|
||||||
|
/* ── motion ──────────────────────────────────────────────────────────────── */
|
||||||
|
@keyframes resolve {
|
||||||
|
from { opacity: 0; transform: translateY(6px) scale(0.985); }
|
||||||
|
}
|
||||||
|
@keyframes rise {
|
||||||
|
from { opacity: 0; transform: translateY(12px); }
|
||||||
|
}
|
||||||
|
@keyframes blink {
|
||||||
|
50% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Things settle into place; `--i` staggers a group of them. */
|
||||||
|
.resolve {
|
||||||
|
animation: resolve var(--t-lazy) var(--ease-snap) both;
|
||||||
|
animation-delay: calc(var(--i, 0) * 80ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
animation-delay: 0ms !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ── rendered markdown ───────────────────────────────────────────────────── */
|
/* ── rendered markdown ───────────────────────────────────────────────────── */
|
||||||
.md { max-width: 78ch; }
|
/* Reading is set in the sans; anything that is code stays mono. */
|
||||||
|
.md {
|
||||||
|
max-width: 72ch;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
.md > :first-child { margin-top: 0; }
|
.md > :first-child { margin-top: 0; }
|
||||||
.md h1, .md h2, .md h3, .md h4 {
|
.md h1, .md h2, .md h3, .md h4 {
|
||||||
color: var(--accent);
|
color: var(--light);
|
||||||
font-size: 1em;
|
font-weight: 400;
|
||||||
font-weight: 700;
|
line-height: 1.3;
|
||||||
margin: 1.6em 0 0.6em;
|
margin: 1.8em 0 0.6em;
|
||||||
letter-spacing: 0.02em;
|
}
|
||||||
|
.md h1 { font-size: 1.7em; font-weight: 300; letter-spacing: 0.01em; }
|
||||||
|
.md h2 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
.md h2::after { content: ''; height: 1px; flex: 1; background: var(--overlay); }
|
||||||
|
.md h3 { font-size: 1.05em; font-weight: 500; }
|
||||||
|
.md h4 {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--subtle);
|
||||||
}
|
}
|
||||||
.md h1::before { content: '# '; color: var(--fg-faint); }
|
|
||||||
.md h2::before { content: '## '; color: var(--fg-faint); }
|
|
||||||
.md h3::before { content: '### '; color: var(--fg-faint); }
|
|
||||||
.md h4::before { content: '#### '; color: var(--fg-faint); }
|
|
||||||
.md p { margin: 0 0 1em; }
|
.md p { margin: 0 0 1em; }
|
||||||
.md ul, .md ol { margin: 0 0 1em; padding-left: 2ch; }
|
.md strong { color: var(--light); font-weight: 600; }
|
||||||
.md li { margin: 0.15em 0; }
|
.md ul, .md ol { margin: 0 0 1em; padding-left: 1.6em; }
|
||||||
.md ul { list-style: none; }
|
.md li { margin: 0.2em 0; }
|
||||||
.md ul > li::before { content: '- '; color: var(--fg-faint); margin-left: -2ch; }
|
.md li::marker { color: var(--muted); }
|
||||||
|
.md ul { list-style: none; padding-left: 1.4em; }
|
||||||
|
.md ul > li { position: relative; }
|
||||||
|
.md ul > li::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -1.1em;
|
||||||
|
top: 0.72em;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
background: var(--overlay);
|
||||||
|
}
|
||||||
|
.md ul > li:has(> input[type='checkbox'])::before { content: none; }
|
||||||
.md blockquote {
|
.md blockquote {
|
||||||
margin: 0 0 1em;
|
margin: 0 0 1em;
|
||||||
padding-left: 1.5ch;
|
padding-left: 1.2em;
|
||||||
border-left: 2px solid var(--accent-dim);
|
border-left: 1px solid var(--outline);
|
||||||
color: var(--fg-dim);
|
color: var(--subtle);
|
||||||
}
|
}
|
||||||
.md code {
|
.md code {
|
||||||
background: var(--bg-lift);
|
font-family: var(--font-mono);
|
||||||
border: 1px solid var(--line);
|
font-size: 0.86em;
|
||||||
border-radius: var(--radius);
|
background: var(--surface);
|
||||||
padding: 0 4px;
|
border: 1px solid var(--rim);
|
||||||
font-size: 0.92em;
|
border-radius: 4px;
|
||||||
|
padding: 0 5px;
|
||||||
}
|
}
|
||||||
.md pre {
|
.md pre {
|
||||||
background: var(--bg-alt);
|
font-family: var(--font-mono);
|
||||||
border: 1px solid var(--line);
|
font-size: 13px;
|
||||||
border-radius: var(--radius);
|
line-height: 1.6;
|
||||||
padding: 10px 12px;
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--rim);
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
padding: 12px 14px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
margin: 0 0 1em;
|
margin: 0 0 1em;
|
||||||
}
|
}
|
||||||
.md pre code { background: none; border: 0; padding: 0; }
|
.md pre code { background: none; border: 0; padding: 0; font-size: inherit; }
|
||||||
.md hr { border: 0; border-top: 1px dashed var(--line); margin: 2em 0; }
|
.md hr { border: 0; border-top: 1px solid var(--overlay); margin: 2.4em 0; }
|
||||||
.md img, .md video {
|
.md img, .md video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--rim);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius-s);
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 0 1em;
|
margin: 0 0 1em;
|
||||||
}
|
}
|
||||||
.md audio { width: 100%; margin: 0 0 1em; }
|
.md audio { width: 100%; margin: 0 0 1em; }
|
||||||
.md table { border-collapse: collapse; margin: 0 0 1em; display: block; overflow-x: auto; }
|
.md table {
|
||||||
.md th, .md td { border: 1px solid var(--line); padding: 3px 8px; text-align: left; }
|
border-collapse: collapse;
|
||||||
.md th { color: var(--accent); }
|
margin: 0 0 1em;
|
||||||
.md input[type='checkbox'] { accent-color: var(--accent-dim); }
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.md th, .md td { border-bottom: 1px solid var(--rim); padding: 6px 14px 6px 0; text-align: left; }
|
||||||
|
.md th {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
border-bottom-color: var(--overlay);
|
||||||
|
}
|
||||||
|
.md input[type='checkbox'] { accent-color: var(--light-dim); margin-right: 0.5em; }
|
||||||
|
|
||||||
/* ── codemirror ──────────────────────────────────────────────────────────── */
|
/* ── codemirror ──────────────────────────────────────────────────────────── */
|
||||||
.cm-editor { height: 100%; background: transparent; }
|
.cm-editor { height: 100%; background: transparent; }
|
||||||
.cm-editor.cm-focused { outline: none; }
|
.cm-editor.cm-focused { outline: none; }
|
||||||
.cm-scroller {
|
.cm-scroller {
|
||||||
font-family: var(--font) !important;
|
font-family: var(--font-mono) !important;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.6;
|
line-height: 1.65;
|
||||||
padding-bottom: 40vh;
|
padding-bottom: 40vh;
|
||||||
}
|
}
|
||||||
.cm-content { caret-color: var(--accent); }
|
.cm-content { caret-color: var(--light); }
|
||||||
.cm-gutters { background: transparent; border: 0; color: var(--fg-faint); }
|
.cm-gutters { background: transparent; border: 0; color: var(--muted); }
|
||||||
.cm-editor .cm-activeLineGutter { background: transparent; color: var(--fg-dim); }
|
.cm-editor .cm-activeLineGutter { background: transparent; color: var(--subtle); }
|
||||||
/*
|
/*
|
||||||
* CodeMirror injects its own `.cm-activeLine` rule (a light blue #cceeff44)
|
* CodeMirror injects its own `.cm-activeLine` rule (a light blue #cceeff44)
|
||||||
* into the document after this stylesheet, so an equally specific selector
|
* into the document after this stylesheet, so an equally specific selector
|
||||||
* loses and the current line renders as a bright box. Qualify it to win.
|
* loses and the current line renders as a bright box. Qualify it to win.
|
||||||
*/
|
*/
|
||||||
.cm-editor .cm-activeLine { background: color-mix(in srgb, var(--fg) 4%, transparent); }
|
.cm-editor .cm-activeLine { background: color-mix(in oklab, var(--fg) 4%, transparent); }
|
||||||
.cm-selectionBackground, .cm-content ::selection { background: var(--select) !important; }
|
.cm-selectionBackground, .cm-content ::selection { background: var(--overlay) !important; }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Insert mode: a 2px bar reads far better than the browser's hairline caret.
|
* Insert mode: a 2px bar reads far better than the browser's hairline caret.
|
||||||
@@ -207,29 +346,29 @@ input::placeholder { color: var(--fg-faint); }
|
|||||||
.cm-editor .cm-cursor,
|
.cm-editor .cm-cursor,
|
||||||
.cm-editor .cm-cursor-primary,
|
.cm-editor .cm-cursor-primary,
|
||||||
.cm-editor .cm-dropCursor {
|
.cm-editor .cm-dropCursor {
|
||||||
border-left: 2px solid var(--accent);
|
border-left: 2px solid var(--light);
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
margin-left: -1px;
|
margin-left: -1px;
|
||||||
}
|
}
|
||||||
.cm-editor .cm-cursor-secondary { border-left-color: var(--accent-dim); }
|
.cm-editor .cm-cursor-secondary { border-left-color: var(--light-dim); }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Normal mode: the vim plugin paints the character *inside* the block cursor,
|
* Normal mode: the vim plugin paints the character *inside* the block cursor,
|
||||||
* inheriting its syntax colour, so an accent-coloured glyph — a heading's `#`,
|
* inheriting its syntax colour, so a light glyph — a heading's `#`, a link —
|
||||||
* a link — disappears against the accent-coloured block. Force the glyph to the
|
* disappears against the light block. Force the glyph to ink instead, which
|
||||||
* background colour instead, which is legible whatever the token was.
|
* is legible whatever the token was: the same inversion as everywhere else.
|
||||||
*/
|
*/
|
||||||
.cm-fat-cursor,
|
.cm-fat-cursor,
|
||||||
.cm-animate-fat-cursor {
|
.cm-animate-fat-cursor {
|
||||||
background: var(--accent) !important;
|
background: var(--light) !important;
|
||||||
color: var(--bg) !important;
|
color: var(--ink) !important;
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
.cm-fat-cursor.cm-cursor-secondary { background: var(--accent-dim) !important; }
|
.cm-fat-cursor.cm-cursor-secondary { background: var(--light-dim) !important; }
|
||||||
/* Unfocused, the plugin hardcodes a pink outline; keep it on-theme. */
|
/* Unfocused, the plugin hardcodes a pink outline; keep it on-theme. */
|
||||||
.cm-editor:not(.cm-focused) .cm-fat-cursor {
|
.cm-editor:not(.cm-focused) .cm-fat-cursor {
|
||||||
background: none !important;
|
background: none !important;
|
||||||
outline: 1px solid var(--accent-dim) !important;
|
outline: 1px solid var(--light-dim) !important;
|
||||||
color: transparent !important;
|
color: transparent !important;
|
||||||
}
|
}
|
||||||
/* The panel and vim `:` prompt are themed in Editor.svelte — see the note there. */
|
/* The panel and vim `:` prompt are themed in Editor.svelte — see the note there. */
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/*
|
||||||
|
* The top of the frame, laid out like the desktop bar it is copied from: the
|
||||||
|
* mark, the spaces as workspace pills, where in the space the list is, and
|
||||||
|
* the clock. Spaces and crumbs are clickable, because a tree is the one thing
|
||||||
|
* that is genuinely faster to walk with a pointer than with `:cd`.
|
||||||
|
*/
|
||||||
|
import { hooks } from './commands'
|
||||||
|
import { workspace } from './store.svelte'
|
||||||
|
|
||||||
|
// The space is the first crumb, and it is already named by its pill.
|
||||||
|
const crumbs = $derived(workspace.path.slice(1))
|
||||||
|
|
||||||
|
let now = $state(new Date())
|
||||||
|
$effect(() => {
|
||||||
|
const timer = setInterval(() => (now = new Date()), 10_000)
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
})
|
||||||
|
const clock = $derived(
|
||||||
|
now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }),
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="bar">
|
||||||
|
<span class="mark" aria-hidden="true"></span>
|
||||||
|
<span class="divider"></span>
|
||||||
|
|
||||||
|
<nav class="spaces" aria-label="spaces">
|
||||||
|
{#each workspace.spaces as space (space.id)}
|
||||||
|
<button
|
||||||
|
class="pill"
|
||||||
|
aria-current={space.id === workspace.spaceId}
|
||||||
|
onclick={() => workspace.guard(() => workspace.enterSpace(space.id))}
|
||||||
|
>
|
||||||
|
{space.slug}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
<button class="pill add" title="new space (:space! <name>)" onclick={() => hooks.openCommandLine(':space! ')}>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<span class="divider path"></span>
|
||||||
|
|
||||||
|
<nav class="crumbs" aria-label="path">
|
||||||
|
{#if workspace.view === 'board'}
|
||||||
|
<span class="here">board</span>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="crumb"
|
||||||
|
class:here={crumbs.length === 0}
|
||||||
|
onclick={() => workspace.spaceId !== null && workspace.guard(() => workspace.goTo(workspace.spaceId!))}
|
||||||
|
>
|
||||||
|
~
|
||||||
|
</button>
|
||||||
|
{#each crumbs as crumb, index (crumb.id)}
|
||||||
|
<span class="sep">/</span>
|
||||||
|
<button
|
||||||
|
class="crumb"
|
||||||
|
class:here={index === crumbs.length - 1}
|
||||||
|
onclick={() => workspace.guard(() => workspace.goTo(crumb.id))}
|
||||||
|
>
|
||||||
|
{crumb.slug}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<span class="spacer"></span>
|
||||||
|
|
||||||
|
<span class="readout user">
|
||||||
|
<span class="key">user</span>
|
||||||
|
<span class="value">{workspace.user}</span>
|
||||||
|
</span>
|
||||||
|
<span class="divider wide"></span>
|
||||||
|
<time class="clock">{clock}</time>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bar {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 0 auto 0;
|
||||||
|
height: var(--bar-h);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 calc(var(--rail-w) + 4px) 0 calc(var(--frame) + 10px);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--subtle);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A ring with a dot in it: the favicon, and the desktop's launcher. */
|
||||||
|
.mark {
|
||||||
|
position: relative;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
flex: none;
|
||||||
|
border: 1.5px solid var(--light);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.mark::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 2px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 14px;
|
||||||
|
flex: none;
|
||||||
|
background: var(--overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spaces, .crumbs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.spaces { flex: none; max-width: 50%; }
|
||||||
|
.crumbs { flex: 0 1 auto; }
|
||||||
|
|
||||||
|
/* Workspace pills: muted at rest, light on hover, inverted when current. */
|
||||||
|
.pill {
|
||||||
|
min-width: 22px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: none;
|
||||||
|
color: var(--muted);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.pill:hover { background: transparent; color: var(--light); }
|
||||||
|
.pill[aria-current='true'] { background: var(--light); color: var(--ink); }
|
||||||
|
.pill:focus-visible { background: var(--light); color: var(--ink); }
|
||||||
|
.pill.add { color: var(--overlay); }
|
||||||
|
.pill.add:hover { color: var(--light); }
|
||||||
|
|
||||||
|
/* A crumb is a link along the path; a hairline grows under it on hover. */
|
||||||
|
.crumb {
|
||||||
|
position: relative;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 2px 0;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: none;
|
||||||
|
color: var(--subtle);
|
||||||
|
}
|
||||||
|
.crumb::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: -1px;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--light);
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: left;
|
||||||
|
transition: transform var(--t-base) var(--ease-snap);
|
||||||
|
}
|
||||||
|
.crumb:hover { background: transparent; color: var(--light); }
|
||||||
|
.crumb:hover::after { transform: scaleX(1); }
|
||||||
|
.crumb.here, .here { color: var(--light); }
|
||||||
|
.sep { color: var(--overlay); }
|
||||||
|
span.here { letter-spacing: 0.06em; text-transform: none; }
|
||||||
|
|
||||||
|
.spacer { flex: 1; }
|
||||||
|
|
||||||
|
.readout { display: flex; gap: 8px; font-variant-numeric: tabular-nums; }
|
||||||
|
.key { color: var(--muted); }
|
||||||
|
.value { color: var(--fg); text-transform: none; letter-spacing: 0.06em; }
|
||||||
|
|
||||||
|
.clock {
|
||||||
|
color: var(--light);
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.bar { padding-left: 12px; padding-right: 12px; gap: 10px; }
|
||||||
|
.crumbs, .divider.path, .user, .divider.wide { display: none; }
|
||||||
|
.spaces { max-width: none; flex: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+68
-63
@@ -5,8 +5,6 @@
|
|||||||
* this file only draws and — for a pointer — moves the two cursors.
|
* this file only draws and — for a pointer — moves the two cursors.
|
||||||
*/
|
*/
|
||||||
import type { Card } from './api'
|
import type { Card } from './api'
|
||||||
import { hooks, runCommand } from './commands'
|
|
||||||
import SpaceBar from './SpaceBar.svelte'
|
|
||||||
import { workspace } from './store.svelte'
|
import { workspace } from './store.svelte'
|
||||||
import { formatDay } from './util'
|
import { formatDay } from './util'
|
||||||
|
|
||||||
@@ -39,28 +37,22 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="board">
|
<div class="board">
|
||||||
<div class="head">
|
<div class="head label rule">
|
||||||
<span class="accent">~/{workspace.space?.slug ?? ''}/board</span>
|
<span class="marker"></span>
|
||||||
<span class="faint count">
|
<span class="name">{workspace.space?.slug ?? ''}</span>
|
||||||
|
<span>board</span>
|
||||||
|
<span class="count">
|
||||||
{workspace.board?.lists.reduce((n, list) => n + list.cards.length, 0) ?? 0} cards
|
{workspace.board?.lists.reduce((n, list) => n + list.cards.length, 0) ?? 0} cards
|
||||||
</span>
|
</span>
|
||||||
<span class="tools">
|
|
||||||
<button title="new card on this list (o)" onclick={() => hooks.openCommandLine(':card ')}>+ card</button>
|
|
||||||
<button title="new list (:list <name>)" onclick={() => hooks.openCommandLine(':list ')}>+ list</button>
|
|
||||||
<button title="back to the tree (:board)" onclick={() => runCommand('board')}>tree</button>
|
|
||||||
<button title="command line (:)" onclick={() => hooks.openCommandLine(':')}>:</button>
|
|
||||||
<button title="help (?)" onclick={() => runCommand('help')}>?</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SpaceBar />
|
|
||||||
|
|
||||||
<div class="columns" bind:this={columns}>
|
<div class="columns" bind:this={columns}>
|
||||||
{#each workspace.board?.lists ?? [] as list, listIndex (list.id)}
|
{#each workspace.board?.lists ?? [] as list, listIndex (list.id)}
|
||||||
<section class="column" class:on={listIndex === workspace.listCursor}>
|
<section class="column" class:on={listIndex === workspace.listCursor}>
|
||||||
<header>
|
<header class="label">
|
||||||
|
<span class="marker"></span>
|
||||||
<span class="name">{list.name}</span>
|
<span class="name">{list.name}</span>
|
||||||
<span class="faint">{list.cards.length}</span>
|
<span class="n">{list.cards.length}</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
@@ -75,11 +67,11 @@
|
|||||||
ondblclick={() => workspace.guard(() => workspace.openCard())}
|
ondblclick={() => workspace.guard(() => workspace.openCard())}
|
||||||
>
|
>
|
||||||
<span class="title">
|
<span class="title">
|
||||||
<span class="tick">{card.done_at === null ? '[ ]' : '[x]'}</span>
|
<span class="tick" class:ticked={card.done_at !== null}></span>
|
||||||
{card.title}
|
<span class="text">{card.title}</span>
|
||||||
</span>
|
</span>
|
||||||
{#if card.due_at !== null || card.node_id !== null}
|
{#if card.due_at !== null || card.node_id !== null}
|
||||||
<span class="meta faint">
|
<span class="meta">
|
||||||
{#if card.due_at !== null}
|
{#if card.due_at !== null}
|
||||||
<span class={urgency(card)}>due {formatDay(card.due_at)}</span>
|
<span class={urgency(card)}>due {formatDay(card.due_at)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -91,13 +83,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{:else}
|
{:else}
|
||||||
<li class="empty faint">no cards — o adds one</li>
|
<li class="empty muted">no cards — o adds one</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="empty faint">
|
<div class="empty muted">
|
||||||
no lists yet — [+] above, or :list <name>, makes a column. then o, or +, adds a card to it.
|
no lists yet — :list <name> makes a column. then o adds a card to it.
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -112,57 +104,54 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.head {
|
.head {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
flex: none;
|
flex: none;
|
||||||
|
padding: 16px 20px 10px 22px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.head .marker { background: var(--light); }
|
||||||
.count { flex: 1; text-align: right; white-space: nowrap; }
|
.head .name { color: var(--light); }
|
||||||
.tools { display: flex; gap: 2px; flex: none; }
|
|
||||||
.tools button { padding: 0 6px; line-height: 18px; }
|
|
||||||
|
|
||||||
.columns {
|
.columns {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 0;
|
gap: 10px;
|
||||||
|
padding: 4px 14px 14px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 26ch;
|
min-width: 28ch;
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
max-width: 44ch;
|
max-width: 46ch;
|
||||||
border-right: 1px solid var(--line);
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
border-radius: var(--radius-m);
|
||||||
|
background: color-mix(in oklab, var(--surface) 70%, transparent);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: border-color var(--t-base) var(--ease-glide), background-color var(--t-base) var(--ease-glide);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The list the cursor is on: its marker lights and its edge shows. */
|
||||||
|
.column.on { border-color: var(--rim); background: var(--surface); }
|
||||||
|
|
||||||
.column header {
|
.column header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
padding: 6px 10px;
|
padding: 12px 14px 8px;
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
color: var(--fg-dim);
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background: var(--bg);
|
|
||||||
}
|
}
|
||||||
|
.column header .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
/* The list the cursor is on, marked the way the sidebar marks its space. */
|
.column.on header .marker { background: var(--light); }
|
||||||
.column.on header .name { color: var(--accent); font-weight: 700; }
|
.column.on header .name { color: var(--light); }
|
||||||
.column.on { background: var(--bg-alt); }
|
.n { font-variant-numeric: tabular-nums; }
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 4px 0 24px;
|
padding: 2px 6px 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
@@ -171,31 +160,47 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 2px;
|
gap: 3px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin: 1px 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0;
|
border-radius: var(--radius-s);
|
||||||
padding: 4px 10px;
|
padding: 7px 10px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: var(--fg-dim);
|
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: none;
|
||||||
|
color: var(--fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover { background: var(--bg-lift); color: var(--fg); }
|
.card:hover { background: var(--surface-alt); color: var(--light); border-color: transparent; }
|
||||||
.card.active { background: var(--select); color: var(--fg); }
|
.card:active { transform: none; }
|
||||||
.card.done .title { text-decoration: line-through; color: var(--fg-faint); }
|
.card.active, .card:focus-visible { background: var(--light); color: var(--ink); }
|
||||||
|
.card.active :is(.meta, .today, .overdue, .doc) { color: var(--ink); }
|
||||||
|
.card.active .tick { border-color: var(--ink); }
|
||||||
|
.card.active .tick.ticked { background: var(--ink); }
|
||||||
|
.card.done .text { text-decoration: line-through; color: var(--muted); }
|
||||||
|
.card.done.active .text { color: var(--ink); }
|
||||||
|
|
||||||
.tick { color: var(--accent); }
|
.title { display: flex; align-items: baseline; gap: 9px; font-family: var(--font-sans); font-size: 14px; }
|
||||||
.title { display: block; }
|
.tick {
|
||||||
.meta { display: flex; gap: 8px; font-size: 12px; padding-left: 4ch; }
|
width: 9px;
|
||||||
.meta .today { color: var(--warn); }
|
height: 9px;
|
||||||
.meta .overdue { color: var(--error); }
|
flex: none;
|
||||||
|
border: 1px solid var(--outline);
|
||||||
|
border-radius: 2px;
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
.tick.ticked { background: var(--light-dim); border-color: var(--light-dim); }
|
||||||
|
.meta { display: flex; gap: 10px; font-size: 11px; padding-left: 18px; color: var(--muted); }
|
||||||
|
.meta .today { color: var(--light); }
|
||||||
|
.meta .overdue { color: var(--alarm); }
|
||||||
.doc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 24ch; }
|
.doc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 24ch; }
|
||||||
|
|
||||||
.empty { padding: 12px 10px; }
|
.empty { padding: 12px 10px; font-size: 12px; }
|
||||||
|
|
||||||
@media (pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
.card { padding: 8px 10px; }
|
.card { padding: 10px; }
|
||||||
.tools button { padding: 4px 10px; }
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -57,9 +57,9 @@
|
|||||||
{#each matches as spec (spec.name)}
|
{#each matches as spec (spec.name)}
|
||||||
<li>
|
<li>
|
||||||
<button onclick={() => (value = `:${spec.name} `, input?.focus())}>
|
<button onclick={() => (value = `:${spec.name} `, input?.focus())}>
|
||||||
<span class="accent">:{spec.name}</span>
|
<span class="name">:{spec.name}</span>
|
||||||
{#if spec.args}<span class="faint">{spec.args}</span>{/if}
|
{#if spec.args}<span class="muted">{spec.args}</span>{/if}
|
||||||
<span class="dim">{spec.help}</span>
|
<span class="help">{spec.help}</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -72,47 +72,60 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/*
|
||||||
|
* The command line slides up out of the bottom band, the way a popout comes
|
||||||
|
* out from behind the desktop's frame rather than fading in over it.
|
||||||
|
*/
|
||||||
.cmdline {
|
.cmdline {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: calc(var(--frame) + 12px);
|
||||||
right: 0;
|
right: calc(var(--rail-w) + 12px);
|
||||||
bottom: 0;
|
bottom: calc(var(--status-h) - 1px);
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: color-mix(in oklab, var(--hull) 97%, transparent);
|
||||||
|
border: 1px solid var(--surface-alt);
|
||||||
|
border-bottom: 0;
|
||||||
|
border-radius: var(--radius-l) var(--radius-l) 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: rise var(--t-slow) var(--ease-snap) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
form { display: flex; }
|
form { display: flex; padding: 6px; }
|
||||||
|
|
||||||
input {
|
input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 24px;
|
height: 30px;
|
||||||
background: var(--bg);
|
background: var(--surface);
|
||||||
border: 0;
|
border: 1px solid var(--rim);
|
||||||
border-top: 1px solid var(--accent-dim);
|
border-radius: var(--radius-s);
|
||||||
border-radius: 0;
|
color: var(--light);
|
||||||
color: var(--accent);
|
padding: 0 10px;
|
||||||
padding: 0 8px;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.completions {
|
.completions {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 4px 0;
|
padding: 6px 6px 0;
|
||||||
background: var(--bg-alt);
|
|
||||||
border-top: 1px solid var(--line);
|
|
||||||
max-height: 40vh;
|
max-height: 40vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.completions button {
|
.completions button {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
|
align-items: baseline;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 1px 10px;
|
padding: 3px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: none;
|
||||||
}
|
}
|
||||||
.completions button:hover { background: var(--select); }
|
.completions button:hover { background: var(--surface-alt); }
|
||||||
|
.name { color: var(--light); }
|
||||||
|
.help { color: var(--subtle); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
</style>
|
</style>
|
||||||
@@ -66,19 +66,21 @@
|
|||||||
|
|
||||||
<section class="pane">
|
<section class="pane">
|
||||||
<header>
|
<header>
|
||||||
<div class="crumb" title="{workspace.here}/{doc.slug}">
|
<div class="crumb label rule" title="{workspace.here}/{doc.slug}">
|
||||||
<span class="accent">{workspace.here}/{doc.slug}</span>
|
<span class="marker" class:live={workspace.editing}></span>
|
||||||
{#if workspace.dirty}<span class="warn">[+]</span>{/if}
|
<span class="path">{workspace.here}/{doc.slug}</span>
|
||||||
|
{#if workspace.dirty}<span class="dirty">modified</span>{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="meta">
|
<dl class="meta">
|
||||||
<span class="faint">{formatStamp(workspace.draft.created_at)} {formatWeekday(workspace.draft.created_at)}</span>
|
<dt>written</dt>
|
||||||
<span class="faint">·</span>
|
<dd>{formatStamp(workspace.draft.created_at)} {formatWeekday(workspace.draft.created_at)}</dd>
|
||||||
<span class="faint">edited {relative(doc.updated_at)}</span>
|
<dt>edited</dt>
|
||||||
<span class="faint">·</span>
|
<dd>{relative(doc.updated_at)}</dd>
|
||||||
<span class="faint">{words} words</span>
|
<dt>words</dt>
|
||||||
</div>
|
<dd>{words}</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
{#if isTouchDevice}
|
{#if isTouchDevice}
|
||||||
@@ -94,7 +96,7 @@
|
|||||||
<button class:on={doc.shared} onclick={() => runCommand(doc.shared ? 'unshare' : 'share')}>
|
<button class:on={doc.shared} onclick={() => runCommand(doc.shared ? 'unshare' : 'share')}>
|
||||||
{doc.shared ? 'unshare' : 'share'}
|
{doc.shared ? 'unshare' : 'share'}
|
||||||
</button>
|
</button>
|
||||||
<button onclick={() => runCommand('d')}>delete</button>
|
<button class="danger" onclick={() => runCommand('d')}>delete</button>
|
||||||
<button onclick={() => runCommand('q')}>close</button>
|
<button onclick={() => runCommand('q')}>close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,8 +104,9 @@
|
|||||||
|
|
||||||
{#if sharePath}
|
{#if sharePath}
|
||||||
<div class="sharebar">
|
<div class="sharebar">
|
||||||
<span class="accent">◉ public</span>
|
<span class="ring"></span>
|
||||||
<span class="faint">{sharePath}</span>
|
<span class="label public">public</span>
|
||||||
|
<span class="link">{sharePath}</span>
|
||||||
<button onclick={() => runCommand('link')}>new link</button>
|
<button onclick={() => runCommand('link')}>new link</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -111,7 +114,7 @@
|
|||||||
{#if workspace.editing}
|
{#if workspace.editing}
|
||||||
<input
|
<input
|
||||||
class="title"
|
class="title"
|
||||||
placeholder="name (optional)"
|
placeholder="Untitled"
|
||||||
bind:value={workspace.draft.name}
|
bind:value={workspace.draft.name}
|
||||||
oninput={() => workspace.touch()}
|
oninput={() => workspace.touch()}
|
||||||
onkeydown={(event) => {
|
onkeydown={(event) => {
|
||||||
@@ -124,7 +127,7 @@
|
|||||||
<div class="body">
|
<div class="body">
|
||||||
{#key doc.id}
|
{#key doc.id}
|
||||||
{#await import('./Editor.svelte')}
|
{#await import('./Editor.svelte')}
|
||||||
<div class="loading faint">loading editor…</div>
|
<div class="loading label">loading editor<span class="cursor">_</span></div>
|
||||||
{:then module}
|
{:then module}
|
||||||
{@const Editor = module.default}
|
{@const Editor = module.default}
|
||||||
<Editor />
|
<Editor />
|
||||||
@@ -141,12 +144,12 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="body reading" bind:this={reader}>
|
<div class="body reading" bind:this={reader}>
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
||||||
<article class="md" onclick={follow}>
|
<article class="md resolve" onclick={follow}>
|
||||||
{#if doc.name.trim()}<h1 class="entrytitle">{doc.name}</h1>{/if}
|
{#if doc.name.trim()}<h1 class="entrytitle">{doc.name}</h1>{/if}
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in renderMarkdown -->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in renderMarkdown -->
|
||||||
{@html html}
|
{@html html}
|
||||||
{#if !doc.body.trim()}
|
{#if !doc.body.trim()}
|
||||||
<p class="faint">this document is empty — press <span class="accent">i</span> to write.</p>
|
<p class="muted">this document is empty — press <span class="key">i</span> to write.</p>
|
||||||
{/if}
|
{/if}
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
@@ -164,74 +167,114 @@
|
|||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 10px;
|
||||||
padding: 6px 12px;
|
padding: 16px 24px 12px 28px;
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.crumb {
|
.crumb { min-width: 0; white-space: nowrap; }
|
||||||
display: flex;
|
.crumb .marker.live { background: var(--light); }
|
||||||
align-items: baseline;
|
.path {
|
||||||
gap: 6px;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
color: var(--subtle);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
.dirty { flex: none; color: var(--light); }
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; font-size: 12px; min-width: 0; }
|
.meta {
|
||||||
.warn { color: var(--warn); flex: none; }
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 8px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.meta dt { color: var(--muted); letter-spacing: 0.18em; text-transform: uppercase; }
|
||||||
|
.meta dd { margin: 0 10px 0 0; color: var(--fg); }
|
||||||
|
|
||||||
.actions { display: flex; gap: 4px; flex-wrap: wrap; flex: none; }
|
.actions { display: flex; gap: 4px; flex-wrap: wrap; flex: none; }
|
||||||
.actions .on { color: var(--accent); border-color: var(--accent-dim); }
|
.actions .on { color: var(--light); border-color: var(--light-dim); }
|
||||||
|
.actions .danger:hover { background: var(--alarm); border-color: var(--alarm); color: var(--ink); }
|
||||||
|
|
||||||
.sharebar {
|
.sharebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 12px;
|
margin: 0 24px 8px 28px;
|
||||||
background: var(--bg-lift);
|
padding: 6px 6px 6px 12px;
|
||||||
border-bottom: 1px solid var(--line);
|
border: 1px solid var(--rim);
|
||||||
|
border-radius: var(--radius-m);
|
||||||
|
background: var(--surface);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
.ring {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
flex: none;
|
||||||
|
border: 1.5px solid var(--light);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.public { color: var(--light); }
|
||||||
|
.sharebar .link { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--muted); }
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
flex: none;
|
flex: none;
|
||||||
margin: 8px 12px 0;
|
margin: 8px 24px 4px 28px;
|
||||||
|
padding: 2px 0 6px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-bottom: 1px dashed var(--line);
|
border-bottom: 1px solid var(--overlay);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
color: var(--accent);
|
color: var(--light);
|
||||||
font-weight: 700;
|
font-family: var(--font-sans);
|
||||||
padding: 2px 4px;
|
font-size: 24px;
|
||||||
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
.title:focus { border-bottom-color: var(--light-dim); }
|
||||||
|
|
||||||
.loading { padding: 16px 20px; }
|
.loading { padding: 16px 28px; }
|
||||||
|
|
||||||
.body { flex: 1; min-height: 0; overflow: hidden; }
|
.body { flex: 1; min-height: 0; overflow: hidden; }
|
||||||
.reading { overflow-y: auto; padding: 16px 20px 30vh; }
|
.reading { overflow-y: auto; padding: 18px 28px 30vh; }
|
||||||
.entrytitle { margin-top: 0 !important; }
|
.entrytitle { margin-top: 0 !important; font-size: 2.1em !important; }
|
||||||
.entrytitle::before { content: none !important; }
|
|
||||||
|
.key {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--light);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
.mobilebar {
|
.mobilebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px 12px calc(6px + env(safe-area-inset-bottom));
|
padding: 8px 12px calc(8px + env(safe-area-inset-bottom));
|
||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--rim);
|
||||||
background: var(--bg-alt);
|
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
.mobilebar button { flex: 1; padding: 8px; }
|
.mobilebar button { flex: 1; padding: 8px; }
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
header { padding: 14px 16px 10px; }
|
||||||
|
.row { flex-direction: column; align-items: flex-start; }
|
||||||
|
.sharebar { margin: 0 16px 8px; }
|
||||||
|
.title { margin: 8px 16px 4px; }
|
||||||
|
.reading { padding: 14px 16px 30vh; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
+19
-19
@@ -27,16 +27,16 @@
|
|||||||
let view: EditorView | undefined
|
let view: EditorView | undefined
|
||||||
|
|
||||||
const markdownHighlight = HighlightStyle.define([
|
const markdownHighlight = HighlightStyle.define([
|
||||||
{ tag: tags.heading, color: 'var(--accent)', fontWeight: '700' },
|
{ tag: tags.heading, color: 'var(--light)', fontWeight: '600' },
|
||||||
{ tag: tags.strong, color: 'var(--fg)', fontWeight: '700' },
|
{ tag: tags.strong, color: 'var(--light)', fontWeight: '700' },
|
||||||
{ tag: tags.emphasis, color: 'var(--fg)', fontStyle: 'italic' },
|
{ tag: tags.emphasis, color: 'var(--subtle)', fontStyle: 'italic' },
|
||||||
{ tag: tags.link, color: 'var(--accent)' },
|
{ tag: tags.link, color: 'var(--light)', textDecoration: 'underline', textDecorationColor: 'var(--overlay)' },
|
||||||
{ tag: tags.url, color: 'var(--accent-dim)' },
|
{ tag: tags.url, color: 'var(--light-dim)' },
|
||||||
{ tag: tags.monospace, color: 'var(--warn)' },
|
{ tag: tags.monospace, color: 'var(--light-soft)', backgroundColor: 'var(--surface)' },
|
||||||
{ tag: tags.quote, color: 'var(--fg-dim)', fontStyle: 'italic' },
|
{ tag: tags.quote, color: 'var(--subtle)', fontStyle: 'italic' },
|
||||||
{ tag: tags.list, color: 'var(--accent-dim)' },
|
{ tag: tags.list, color: 'var(--light-dim)' },
|
||||||
{ tag: tags.contentSeparator, color: 'var(--fg-faint)' },
|
{ tag: tags.contentSeparator, color: 'var(--muted)' },
|
||||||
{ tag: tags.processingInstruction, color: 'var(--fg-faint)' },
|
{ tag: tags.processingInstruction, color: 'var(--muted)' },
|
||||||
])
|
])
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -49,23 +49,23 @@
|
|||||||
const editorTheme = EditorView.theme(
|
const editorTheme = EditorView.theme(
|
||||||
{
|
{
|
||||||
'&': { color: 'var(--fg)', backgroundColor: 'transparent', height: '100%' },
|
'&': { color: 'var(--fg)', backgroundColor: 'transparent', height: '100%' },
|
||||||
'.cm-content': { padding: '12px 16px', maxWidth: '90ch' },
|
'.cm-content': { padding: '12px 28px', maxWidth: '90ch' },
|
||||||
'.cm-line': { padding: '0' },
|
'.cm-line': { padding: '0' },
|
||||||
'.cm-cursor, .cm-dropCursor': { borderLeft: '2px solid var(--accent)' },
|
'.cm-cursor, .cm-dropCursor': { borderLeft: '2px solid var(--light)' },
|
||||||
'.cm-panels': {
|
'.cm-panels': {
|
||||||
backgroundColor: 'var(--bg-alt)',
|
backgroundColor: 'var(--hull)',
|
||||||
color: 'var(--fg)',
|
color: 'var(--fg)',
|
||||||
borderColor: 'var(--line)',
|
borderColor: 'var(--rim)',
|
||||||
},
|
},
|
||||||
'.cm-panels-bottom': { borderTop: '1px solid var(--line)' },
|
'.cm-panels-bottom': { borderTop: '1px solid var(--rim)' },
|
||||||
/*
|
/*
|
||||||
* The `:` prompt is a bare text node in the panel; the input follows it.
|
* The `:` prompt is a bare text node in the panel; the input follows it.
|
||||||
* The input is an inline-block whose baseline sits below the text node's,
|
* The input is an inline-block whose baseline sits below the text node's,
|
||||||
* so the two are laid out as flex items instead of aligned by baseline.
|
* so the two are laid out as flex items instead of aligned by baseline.
|
||||||
*/
|
*/
|
||||||
'.cm-vim-panel': {
|
'.cm-vim-panel': {
|
||||||
padding: '2px 8px',
|
padding: '4px 12px',
|
||||||
color: 'var(--accent)',
|
color: 'var(--light)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
@@ -73,8 +73,8 @@
|
|||||||
fontFamily: 'inherit',
|
fontFamily: 'inherit',
|
||||||
fontSize: 'inherit',
|
fontSize: 'inherit',
|
||||||
lineHeight: 'inherit',
|
lineHeight: 'inherit',
|
||||||
color: 'var(--accent)',
|
color: 'var(--light)',
|
||||||
caretColor: 'var(--accent)',
|
caretColor: 'var(--light)',
|
||||||
flex: '1',
|
flex: '1',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+39
-30
@@ -60,8 +60,7 @@
|
|||||||
|
|
||||||
<div class="sheet">
|
<div class="sheet">
|
||||||
<header>
|
<header>
|
||||||
<span class="accent">:help</span>
|
<span class="label rule"><span class="marker"></span><span class="title">help</span> everything is a key or a `:` command</span>
|
||||||
<span class="faint">everything is a key or a `:` command</span>
|
|
||||||
<button onclick={() => (workspace.overlay = 'none')}>esc</button>
|
<button onclick={() => (workspace.overlay = 'none')}>esc</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -92,7 +91,7 @@
|
|||||||
<dl>
|
<dl>
|
||||||
{#each specs as spec (spec.name)}
|
{#each specs as spec (spec.name)}
|
||||||
<dt>
|
<dt>
|
||||||
{signature(spec)}{#if spec.args}<span class="args faint">{spec.args}</span>{/if}
|
{signature(spec)}{#if spec.args}<span class="args muted">{spec.args}</span>{/if}
|
||||||
</dt>
|
</dt>
|
||||||
<dd>{spec.help}</dd>
|
<dd>{spec.help}</dd>
|
||||||
{#if spec.bang}
|
{#if spec.bang}
|
||||||
@@ -105,59 +104,69 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="faint">
|
<footer>
|
||||||
inside the editor the full vim keymap is live — hjkl, w/b, dd, ciw, visual mode, macros, and
|
inside the editor the full vim keymap is live — hjkl, w/b, dd, ciw, visual mode, macros, and
|
||||||
<span class="accent">:w</span> to write. drop or paste an image, video or audio file into the
|
<span class="key">:w</span> to write. drop or paste an image, video or audio file into the
|
||||||
editor to attach it. <span class="accent">:12</span> jumps straight to entry 12. yanks and
|
editor to attach it. <span class="key">:12</span> jumps straight to entry 12. yanks and
|
||||||
deletes are copied to the system clipboard —
|
deletes are copied to the system clipboard —
|
||||||
<span class="accent">:set noclipboard</span> keeps them in vim's registers.
|
<span class="key">:set noclipboard</span> keeps them in vim's registers.
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* A free-standing panel, as the desktop draws one: hull, one rim, the frame's radius. */
|
||||||
.sheet {
|
.sheet {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 18px;
|
||||||
width: min(100%, 110ch);
|
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
background: var(--bg-alt);
|
background: color-mix(in oklab, var(--hull) 97%, transparent);
|
||||||
border: 1px solid var(--accent-dim);
|
border: 1px solid var(--surface-alt);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--frame-radius);
|
||||||
padding: 14px 16px;
|
padding: 20px 24px;
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header { display: flex; align-items: baseline; gap: 12px; }
|
header { display: flex; align-items: center; gap: 12px; }
|
||||||
header .faint { flex: 1; font-size: 12px; }
|
header .label { flex: 1; }
|
||||||
header button { align-self: center; }
|
header .marker { background: var(--light); }
|
||||||
|
header .title { color: var(--light); }
|
||||||
|
.sheet { width: min(100%, 112ch); overflow-y: auto; overflow-x: hidden; }
|
||||||
|
|
||||||
/* Newspaper columns, so the short groups fill the gaps under the long ones. */
|
/* Newspaper columns, so the short groups fill the gaps under the long ones. */
|
||||||
.cols { columns: 38ch; column-gap: 28px; }
|
.cols { columns: 38ch; column-gap: 32px; }
|
||||||
|
|
||||||
section { break-inside: avoid; min-width: 0; margin: 0 0 16px; }
|
section { break-inside: avoid; min-width: 0; margin: 0 0 20px; }
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 12px;
|
display: flex;
|
||||||
color: var(--fg-dim);
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.1em;
|
color: var(--muted);
|
||||||
margin: 0 0 6px;
|
|
||||||
border-bottom: 1px dashed var(--line);
|
|
||||||
padding-bottom: 4px;
|
|
||||||
}
|
}
|
||||||
|
h2::after { content: ''; height: 1px; flex: 1; background: var(--overlay); }
|
||||||
|
|
||||||
dl {
|
dl {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: max-content minmax(0, 1fr);
|
grid-template-columns: max-content minmax(0, 1fr);
|
||||||
gap: 3px 12px;
|
gap: 4px 14px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
dt { color: var(--accent); white-space: nowrap; }
|
dt { color: var(--light); white-space: nowrap; }
|
||||||
.args { margin-left: 0.6ch; }
|
.args { margin-left: 0.6ch; }
|
||||||
dd { margin: 0; color: var(--fg-dim); overflow-wrap: anywhere; }
|
dd { margin: 0; color: var(--subtle); overflow-wrap: anywhere; }
|
||||||
|
|
||||||
footer { font-size: 12px; border-top: 1px solid var(--line); padding-top: 10px; }
|
footer {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
border-top: 1px solid var(--overlay);
|
||||||
|
padding-top: 12px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.key { color: var(--light); }
|
||||||
</style>
|
</style>
|
||||||
+68
-25
@@ -35,26 +35,30 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<form onsubmit={submit}>
|
<form class="resolve" onsubmit={submit}>
|
||||||
<pre class="banner">{`╭──────────────────────────────────╮
|
<div class="brand">
|
||||||
│ ~/workspace │
|
<span class="mark" aria-hidden="true"></span>
|
||||||
╰──────────────────────────────────╯`}</pre>
|
<h1 class="display">workspace</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="faint">a private workspace. one user. one machine.</p>
|
<p class="label rule">a private workspace · one user</p>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<span class="accent">login:</span>
|
<span class="label">login</span>
|
||||||
<input bind:this={field} bind:value={username} autocomplete="username" spellcheck="false" />
|
<input bind:this={field} bind:value={username} autocomplete="username" spellcheck="false" />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<span class="accent">password:</span>
|
<span class="label">password</span>
|
||||||
<input type="password" bind:value={password} autocomplete="current-password" />
|
<input type="password" bind:value={password} autocomplete="current-password" />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button type="submit" disabled={busy}>{busy ? 'authenticating…' : 'enter'}</button>
|
<button class="enter" type="submit" disabled={busy}>
|
||||||
{#if error}<span class="error">{error}</span>{/if}
|
<span class="dot"></span>
|
||||||
|
{busy ? 'authenticating' : 'enter'}
|
||||||
|
</button>
|
||||||
|
{#if error}<span class="alarm">{error}</span>{/if}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
@@ -65,31 +69,70 @@
|
|||||||
place-items: center;
|
place-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
|
background:
|
||||||
|
radial-gradient(120% 70% at 30% 0%, #141713 0%, transparent 60%),
|
||||||
|
var(--base);
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
width: min(100%, 46ch);
|
width: min(100%, 44ch);
|
||||||
background: var(--bg-alt);
|
background: color-mix(in oklab, var(--hull) 97%, transparent);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--surface-alt);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--frame-radius);
|
||||||
padding: 20px;
|
padding: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner {
|
.brand { display: flex; align-items: center; gap: 14px; }
|
||||||
color: var(--accent);
|
h1 { margin: 0; font-size: 26px; text-shadow: 0 2px 24px var(--ink); }
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.3;
|
/* The ring-and-dot mark, larger than in the bar. */
|
||||||
margin: 0;
|
.mark {
|
||||||
overflow-x: auto;
|
position: relative;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex: none;
|
||||||
|
border: 2px solid var(--light);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.mark::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 3px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--light);
|
||||||
}
|
}
|
||||||
|
|
||||||
p { margin: 0 0 4px; font-size: 12px; }
|
p { margin: 0 0 4px; font-size: 10px; }
|
||||||
|
|
||||||
label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; }
|
label { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
label .label { font-size: 10px; }
|
||||||
|
input { height: 36px; }
|
||||||
|
|
||||||
.row { display: flex; align-items: center; gap: 12px; }
|
.row { display: flex; align-items: center; gap: 14px; margin-top: 4px; }
|
||||||
.row button { padding: 6px 16px; }
|
|
||||||
|
/* The desktop's action button: a hairline in light, a dot, a spaced label. */
|
||||||
|
.enter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 20px 0 16px;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
color: var(--light);
|
||||||
|
border-color: color-mix(in oklab, var(--light) 55%, transparent);
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.35;
|
||||||
|
transition: opacity var(--t-base) var(--ease-glide);
|
||||||
|
}
|
||||||
|
.enter:hover .dot, .enter:focus-visible .dot { opacity: 1; }
|
||||||
|
.alarm { font-size: 12px; }
|
||||||
</style>
|
</style>
|
||||||
@@ -21,8 +21,7 @@
|
|||||||
|
|
||||||
<div class="sheet">
|
<div class="sheet">
|
||||||
<header>
|
<header>
|
||||||
<span class="accent">:media</span>
|
<span class="label rule"><span class="marker"></span><span class="title">media</span> {workspace.media.length} files</span>
|
||||||
<span class="faint">{workspace.media.length} files</span>
|
|
||||||
<button onclick={() => (workspace.overlay = 'none')}>esc</button>
|
<button onclick={() => (workspace.overlay = 'none')}>esc</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -33,13 +32,13 @@
|
|||||||
{#if file.mime.startsWith('image/')}
|
{#if file.mime.startsWith('image/')}
|
||||||
<img src={file.url} alt={file.filename} loading="lazy" />
|
<img src={file.url} alt={file.filename} loading="lazy" />
|
||||||
{:else}
|
{:else}
|
||||||
<span class="faint">{file.mime.split('/')[0]}</span>
|
<span class="label">{file.mime.split('/')[0]}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<span class="name">{file.filename}</span>
|
<span class="name">{file.filename}</span>
|
||||||
<span class="faint">
|
<span class="muted">
|
||||||
{formatBytes(file.size)} · {formatStamp(file.created_at)} ·
|
{formatBytes(file.size)} · {formatStamp(file.created_at)} ·
|
||||||
{usage(file.node_ids)}
|
{usage(file.node_ids)}
|
||||||
</span>
|
</span>
|
||||||
@@ -55,50 +54,65 @@
|
|||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<button onclick={() => workspace.copy(location.origin + file.url)}>copy url</button>
|
<button onclick={() => workspace.copy(location.origin + file.url)}>copy url</button>
|
||||||
<button onclick={() => remove(file.id, file.filename, file.node_ids)}>rm</button>
|
<button class="danger" onclick={() => remove(file.id, file.filename, file.node_ids)}>rm</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{:else}
|
{:else}
|
||||||
<li class="faint">nothing uploaded yet — attach a file from the editor.</li>
|
<li class="muted">nothing uploaded yet — attach a file from the editor.</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* A free-standing panel, as the desktop draws one: hull, one rim, the frame's radius. */
|
||||||
.sheet {
|
.sheet {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 18px;
|
||||||
width: min(100%, 90ch);
|
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
background: var(--bg-alt);
|
background: color-mix(in oklab, var(--hull) 97%, transparent);
|
||||||
border: 1px solid var(--accent-dim);
|
border: 1px solid var(--surface-alt);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--frame-radius);
|
||||||
padding: 14px 16px;
|
padding: 20px 24px;
|
||||||
overflow: hidden;
|
animation: resolve var(--t-slow) var(--ease-snap) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
header { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
|
header { display: flex; align-items: center; gap: 12px; }
|
||||||
|
header .label { flex: 1; }
|
||||||
|
header .marker { background: var(--light); }
|
||||||
|
header .title { color: var(--light); }
|
||||||
|
.sheet { width: min(100%, 92ch); overflow: hidden; }
|
||||||
|
|
||||||
ul { list-style: none; margin: 0; padding: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
|
ul { list-style: none; margin: 0; padding: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
|
||||||
|
|
||||||
li { display: flex; align-items: center; gap: 12px; padding: 4px; border-bottom: 1px solid var(--line); }
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: var(--radius-m);
|
||||||
|
transition: background-color var(--t-base) var(--ease-glide);
|
||||||
|
}
|
||||||
|
li:hover { background: var(--surface); }
|
||||||
|
|
||||||
.thumb {
|
.thumb {
|
||||||
width: 48px;
|
width: 52px;
|
||||||
height: 36px;
|
height: 38px;
|
||||||
flex: none;
|
flex: none;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: var(--bg);
|
background: var(--surface);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--rim);
|
||||||
|
border-radius: var(--radius-s);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 10px;
|
|
||||||
}
|
}
|
||||||
|
.thumb .label { font-size: 9px; letter-spacing: 0.14em; }
|
||||||
.thumb img { width: 100%; height: 100%; object-fit: cover; }
|
.thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
|
||||||
.info { display: flex; flex-direction: column; min-width: 0; flex: 1; font-size: 12px; }
|
.info { display: flex; flex-direction: column; min-width: 0; flex: 1; font-size: 12px; }
|
||||||
.name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--fg); font-family: var(--font-sans); font-size: 14px; }
|
||||||
|
.muted { font-size: 11px; }
|
||||||
|
|
||||||
.actions { display: flex; gap: 4px; flex: none; }
|
.actions { display: flex; gap: 4px; flex: none; }
|
||||||
|
.danger:hover { background: var(--alarm); border-color: var(--alarm); color: var(--ink); }
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/*
|
||||||
|
* The right edge of the frame: the things a pointer needs a door for. Each
|
||||||
|
* door is labelled with the key that does the same thing, so the rail
|
||||||
|
* teaches the keymap instead of replacing it. `compact` lays the same doors
|
||||||
|
* out in a row, for the bottom band on a phone.
|
||||||
|
*/
|
||||||
|
import { hooks, runCommand } from './commands'
|
||||||
|
import { workspace } from './store.svelte'
|
||||||
|
|
||||||
|
const { compact = false }: { compact?: boolean } = $props()
|
||||||
|
|
||||||
|
type Door = { key: string; caption: string; title: string; run: () => unknown; on?: boolean }
|
||||||
|
|
||||||
|
const doors = $derived.by((): Door[] => {
|
||||||
|
if (workspace.view === 'board') {
|
||||||
|
return [
|
||||||
|
{ key: 'o', caption: 'card', title: 'new card on this list (o)', run: () => hooks.openCommandLine(':card ') },
|
||||||
|
{ key: '+', caption: 'list', title: 'new list (:list <name>)', run: () => hooks.openCommandLine(':list ') },
|
||||||
|
{ key: 'b', caption: 'tree', title: 'back to the tree (b, q)', run: () => runCommand('board'), on: true },
|
||||||
|
{ key: ':', caption: 'cmd', title: 'command line (:)', run: () => hooks.openCommandLine(':') },
|
||||||
|
{ key: '?', caption: 'help', title: 'help (?)', run: () => runCommand('help') },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const board: Door = workspace.space?.has_board
|
||||||
|
? { key: 'b', caption: 'board', title: 'board (b)', run: () => runCommand('board') }
|
||||||
|
: { key: '+b', caption: 'board', title: 'give this space a board (:set board)', run: () => runCommand('set board') }
|
||||||
|
return [
|
||||||
|
{ key: 'o', caption: 'new', title: 'new document (o)', run: () => runCommand('new') },
|
||||||
|
{ key: 'O', caption: 'dir', title: 'new folder (O)', run: () => hooks.openCommandLine(':mkdir ') },
|
||||||
|
...(workspace.space ? [board] : []),
|
||||||
|
{ key: '/', caption: 'find', title: 'search (/)', run: () => hooks.openCommandLine('/') },
|
||||||
|
{ key: ':', caption: 'cmd', title: 'command line (:)', run: () => hooks.openCommandLine(':') },
|
||||||
|
{ key: '?', caption: 'help', title: 'help (?)', run: () => runCommand('help') },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class="rail" class:compact aria-label="tools">
|
||||||
|
{#each doors as door (door.caption)}
|
||||||
|
<button class="door" class:on={door.on} title={door.title} onclick={door.run}>
|
||||||
|
<span class="key">{door.key}</span>
|
||||||
|
<span class="caption">{door.caption}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.rail {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--bar-h);
|
||||||
|
right: 0;
|
||||||
|
bottom: var(--status-h);
|
||||||
|
width: var(--rail-w);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.door {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
width: 40px;
|
||||||
|
padding: 7px 0 6px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A light bar grows down the left edge on hover, as on the desktop rail. */
|
||||||
|
.door::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 6px;
|
||||||
|
bottom: 6px;
|
||||||
|
width: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--light);
|
||||||
|
transform: scaleY(0);
|
||||||
|
transform-origin: top;
|
||||||
|
transition: transform var(--t-base) var(--ease-snap);
|
||||||
|
}
|
||||||
|
.door:hover { background: var(--surface-alt); color: var(--light); }
|
||||||
|
.door:hover::before { transform: scaleY(1); }
|
||||||
|
.door.on { color: var(--light); }
|
||||||
|
.door:focus-visible { background: var(--light); color: var(--ink); }
|
||||||
|
|
||||||
|
.key {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: transform var(--t-base) var(--ease-snap);
|
||||||
|
}
|
||||||
|
.door:hover .key { transform: scale(1.08); }
|
||||||
|
.caption {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.rail:not(.compact) { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact {
|
||||||
|
position: static;
|
||||||
|
width: auto;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 0;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.compact .door { width: 38px; padding: 4px 0; }
|
||||||
|
.compact .door::before { display: none; }
|
||||||
|
.compact .caption { display: none; }
|
||||||
|
</style>
|
||||||
+18
-19
@@ -25,20 +25,21 @@
|
|||||||
|
|
||||||
<main>
|
<main>
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="error">{error}</p>
|
<p class="alarm">{error}</p>
|
||||||
{:else if doc}
|
{:else if doc}
|
||||||
<article class="md">
|
<article class="md resolve">
|
||||||
<div class="meta faint">
|
<div class="meta label rule">
|
||||||
{formatStamp(doc.created_at)} {formatWeekday(doc.created_at)}
|
<span class="marker"></span>
|
||||||
|
<span>{formatStamp(doc.created_at)} {formatWeekday(doc.created_at)}</span>
|
||||||
<span class="tag">shared document</span>
|
<span class="tag">shared document</span>
|
||||||
</div>
|
</div>
|
||||||
{#if doc.name.trim()}<h1>{doc.name}</h1>{/if}
|
{#if doc.name.trim()}<h1>{doc.name}</h1>{/if}
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in renderMarkdown -->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in renderMarkdown -->
|
||||||
{@html renderMarkdown(doc.body, { token, key: shareKey })}
|
{@html renderMarkdown(doc.body, { token, key: shareKey })}
|
||||||
</article>
|
</article>
|
||||||
<footer class="faint">— written in ~/workspace</footer>
|
<footer class="label">— written in ~/workspace</footer>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="faint">loading…</p>
|
<p class="label">loading<span class="cursor">_</span></p>
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -46,26 +47,24 @@
|
|||||||
main {
|
main {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 40px 20px 20vh;
|
padding: 56px 20px 20vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
background:
|
||||||
|
radial-gradient(120% 70% at 30% 0%, #141713 0%, transparent 60%),
|
||||||
|
var(--base);
|
||||||
}
|
}
|
||||||
|
|
||||||
article { width: min(100%, 78ch); }
|
article { width: min(100%, 72ch); }
|
||||||
|
|
||||||
.meta { display: flex; gap: 10px; align-items: center; font-size: 12px; margin-bottom: 12px; }
|
.meta { margin-bottom: 28px; font-size: 10px; }
|
||||||
|
.meta .marker { background: var(--light); }
|
||||||
|
|
||||||
.tag {
|
.tag { color: var(--light); }
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 0 6px;
|
|
||||||
color: var(--accent-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 { margin-top: 0 !important; }
|
h1 { margin-top: 0 !important; font-size: 2.1em !important; }
|
||||||
h1::before { content: none !important; }
|
|
||||||
|
|
||||||
footer { width: min(100%, 78ch); margin-top: 40px; font-size: 12px; }
|
footer { width: min(100%, 72ch); margin-top: 56px; font-size: 10px; }
|
||||||
p { width: min(100%, 78ch); }
|
p { width: min(100%, 72ch); }
|
||||||
</style>
|
</style>
|
||||||
+91
-86
@@ -1,7 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { NodeSummary } from './api'
|
import type { NodeSummary } from './api'
|
||||||
import { hooks, runCommand } from './commands'
|
|
||||||
import SpaceBar from './SpaceBar.svelte'
|
|
||||||
import { workspace } from './store.svelte'
|
import { workspace } from './store.svelte'
|
||||||
import { formatDay, formatTime, formatWeekday } from './util'
|
import { formatDay, formatTime, formatWeekday } from './util'
|
||||||
|
|
||||||
@@ -29,33 +27,16 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="head">
|
<div class="head label rule">
|
||||||
<span class="accent name">~/{workspace.space?.slug ?? 'diary'}</span>
|
<span class="marker"></span>
|
||||||
<span class="faint count">
|
<span class="name">{workspace.space?.slug ?? 'diary'}</span>
|
||||||
{workspace.nodes.length} {workspace.nodes.length === 1 ? 'item' : 'items'}
|
<span class="count">{workspace.nodes.length} {workspace.nodes.length === 1 ? 'item' : 'items'}</span>
|
||||||
</span>
|
|
||||||
<span class="tools">
|
|
||||||
<!-- The board is the one view with no row in the list to click, so it
|
|
||||||
needs a door of its own; a space without one is offered the switch. -->
|
|
||||||
{#if workspace.space?.has_board}
|
|
||||||
<button class="board" title="board (b)" onclick={() => runCommand('board')}>board</button>
|
|
||||||
{:else if workspace.space}
|
|
||||||
<button class="board faint" title="give this space a board (:set board)" onclick={() => runCommand('set board')}>+board</button>
|
|
||||||
{/if}
|
|
||||||
<button title="new document (o)" onclick={() => runCommand('new')}>+</button>
|
|
||||||
<button title="new folder (O)" onclick={() => hooks.openCommandLine(':mkdir ')}>[+]</button>
|
|
||||||
<button title="search (/)" onclick={() => hooks.openCommandLine('/')}>/</button>
|
|
||||||
<button title="command line (:)" onclick={() => hooks.openCommandLine(':')}>:</button>
|
|
||||||
<button title="help (?)" onclick={() => runCommand('help')}>?</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SpaceBar />
|
|
||||||
|
|
||||||
{#if workspace.query}
|
{#if workspace.query}
|
||||||
<div class="filter">
|
<div class="filter">
|
||||||
<span class="faint">/</span>{workspace.query}
|
<span class="muted">/</span><span class="query">{workspace.query}</span>
|
||||||
<button title="clear search" onclick={() => workspace.guard(() => workspace.search(''))}>esc</button>
|
<button title="clear search (n)" onclick={() => workspace.guard(() => workspace.search(''))}>esc</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -65,9 +46,8 @@
|
|||||||
<!-- Day headings only make sense in a list that is a journal: documents,
|
<!-- Day headings only make sense in a list that is a journal: documents,
|
||||||
in date order, not a search and not a folder listing. -->
|
in date order, not a search and not a folder listing. -->
|
||||||
{#if !workspace.query && node.kind === 'document' && (!previous || previous.kind !== 'document' || formatDay(previous.created_at) !== formatDay(node.created_at))}
|
{#if !workspace.query && node.kind === 'document' && (!previous || previous.kind !== 'document' || formatDay(previous.created_at) !== formatDay(node.created_at))}
|
||||||
<li class="daybreak">
|
<li class="daybreak label rule">
|
||||||
<span>{formatDay(node.created_at)}</span>
|
<span><span class="day">{formatDay(node.created_at)}</span> {formatWeekday(node.created_at)}</span>
|
||||||
<span class="faint">{formatWeekday(node.created_at)}</span>
|
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
<li>
|
<li>
|
||||||
@@ -78,15 +58,15 @@
|
|||||||
data-index={index}
|
data-index={index}
|
||||||
onclick={() => pick(index)}
|
onclick={() => pick(index)}
|
||||||
>
|
>
|
||||||
<span class="caret">{index === workspace.cursor ? '>' : ' '}</span>
|
<span class="dot"></span>
|
||||||
{#if workspace.marked?.id === node.id}<span class="marked" title="marked — p drops it in a folder">m</span>{/if}
|
<span class="title">{label(node)}{#if node.kind !== 'document'}<span class="slash">/</span>{/if}</span>
|
||||||
<span class="time">{aside(node)}</span>
|
{#if workspace.marked?.id === node.id}<span class="badge" title="marked — p drops it in a folder">m</span>{/if}
|
||||||
<span class="title">{node.kind === 'document' ? '' : '/'}{label(node)}</span>
|
{#if node.shared}<span class="shared" title="shared"></span>{/if}
|
||||||
{#if node.shared}<span class="shared" title="shared">◉</span>{/if}
|
<span class="aside">{aside(node)}</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{:else}
|
{:else}
|
||||||
<li class="empty faint">
|
<li class="empty muted">
|
||||||
{workspace.query ? 'no matches' : 'nothing here yet — o writes, O makes a folder'}
|
{workspace.query ? 'no matches' : 'nothing here yet — o writes, O makes a folder'}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -98,87 +78,112 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--bg-alt);
|
|
||||||
border-right: 1px solid var(--line);
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
--pane: color-mix(in oklab, var(--surface) 55%, var(--base));
|
||||||
|
background: var(--pane);
|
||||||
|
border-right: 1px solid var(--rim);
|
||||||
}
|
}
|
||||||
|
|
||||||
.head,
|
.head {
|
||||||
.filter {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
padding: 16px 16px 10px 18px;
|
||||||
|
|
||||||
.filter {
|
|
||||||
color: var(--warn);
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.head .marker { background: var(--light); }
|
||||||
|
.name { color: var(--light); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.count { flex: none; }
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 0 10px 6px;
|
||||||
|
padding: 4px 6px 4px 10px;
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
background: var(--surface-alt);
|
||||||
|
font-size: 12px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.query { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--light); }
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 4px 0 24px;
|
padding: 2px 8px 24px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.daybreak {
|
.daybreak {
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 10px 10px 2px;
|
|
||||||
color: var(--fg-dim);
|
|
||||||
font-size: 12px;
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
background: var(--bg-alt);
|
z-index: 1;
|
||||||
|
padding: 14px 8px 6px 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
background: linear-gradient(var(--pane) 70%, transparent);
|
||||||
}
|
}
|
||||||
|
.day { color: var(--subtle); }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A row rests bare, lifts a step on hover, and the cursor inverts it — light
|
||||||
|
* fill, ink text — the same mark focus makes everywhere else.
|
||||||
|
*/
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin: 1px 0;
|
||||||
|
padding: 5px 10px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0;
|
border-radius: var(--radius-s);
|
||||||
padding: 2px 10px;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: var(--fg-dim);
|
font-size: 13px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: none;
|
||||||
|
color: var(--subtle);
|
||||||
}
|
}
|
||||||
|
.row:hover { background: var(--surface-alt); color: var(--light); border-color: transparent; }
|
||||||
|
.row:active { transform: none; }
|
||||||
|
.row.open { color: var(--light); }
|
||||||
|
.row.active, .row:focus-visible { background: var(--light); color: var(--ink); }
|
||||||
|
.row.active :is(.aside, .slash, .badge), .row:focus-visible :is(.aside, .slash, .badge) { color: var(--ink); }
|
||||||
|
|
||||||
.row:hover { background: var(--bg-lift); color: var(--fg); }
|
/* The open document carries a dot; the others keep its place. */
|
||||||
.row.open { color: var(--fg); }
|
.dot {
|
||||||
.row.active { background: var(--select); color: var(--fg); }
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
.caret { color: var(--accent); flex: none; width: 1ch; }
|
flex: none;
|
||||||
.time { color: var(--fg-faint); flex: none; font-size: 12px; }
|
border-radius: 999px;
|
||||||
.title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
background: transparent;
|
||||||
.shared { color: var(--accent); flex: none; }
|
transition: background-color var(--t-base) var(--ease-glide);
|
||||||
.marked { color: var(--warn); flex: none; font-weight: 700; }
|
|
||||||
.empty { padding: 12px 10px; }
|
|
||||||
|
|
||||||
.name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
||||||
/* The count is short and fixed-width-ish; the space name is the part that
|
|
||||||
can give way when the sidebar is narrow. */
|
|
||||||
.count { flex: none; white-space: nowrap; display: none; }
|
|
||||||
.tools { display: flex; gap: 1px; flex: none; }
|
|
||||||
.tools button { padding: 0 5px; line-height: 18px; }
|
|
||||||
|
|
||||||
@media (min-width: 480px) {
|
|
||||||
.count { display: inline; }
|
|
||||||
}
|
}
|
||||||
.tools .board { color: var(--accent); }
|
.row.open .dot { background: var(--light); }
|
||||||
.tools .board.faint { color: var(--fg-faint); }
|
.row.open.active .dot { background: var(--ink); }
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.slash { color: var(--muted); margin-left: 1px; }
|
||||||
|
.aside { flex: none; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||||
|
.badge { flex: none; font-size: 11px; font-weight: 700; color: var(--light-dim); }
|
||||||
|
.shared {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
flex: none;
|
||||||
|
border: 1.5px solid currentColor;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.empty { padding: 12px 10px; font-size: 12px; }
|
||||||
|
|
||||||
/* Roomier tap targets where there is no keyboard. */
|
/* Roomier tap targets where there is no keyboard. */
|
||||||
@media (pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
.row { padding: 7px 10px; }
|
.row { padding: 9px 10px; }
|
||||||
.tools button { padding: 4px 10px; }
|
|
||||||
.count { display: none; }
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
/*
|
|
||||||
* The two lines above the list: which space is open, and where inside it the
|
|
||||||
* list currently is. Both are clickable, because a tree is the one thing that
|
|
||||||
* is genuinely faster to navigate with a pointer than with `:cd`.
|
|
||||||
*/
|
|
||||||
import { hooks } from './commands'
|
|
||||||
import { workspace } from './store.svelte'
|
|
||||||
|
|
||||||
// The space is the first crumb, and it is already named by the bar above.
|
|
||||||
const crumbs = $derived(workspace.path.slice(1))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="spaces">
|
|
||||||
{#each workspace.spaces as space (space.id)}
|
|
||||||
<button
|
|
||||||
class="space"
|
|
||||||
class:on={space.id === workspace.spaceId}
|
|
||||||
onclick={() => workspace.guard(() => workspace.enterSpace(space.id))}
|
|
||||||
>
|
|
||||||
{space.slug}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
<button class="space add" title="new space (:space! <name>)" onclick={() => hooks.openCommandLine(':space! ')}>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if crumbs.length > 0}
|
|
||||||
<div class="crumbs">
|
|
||||||
<button onclick={() => workspace.spaceId !== null && workspace.guard(() => workspace.goTo(workspace.spaceId!))}>
|
|
||||||
/
|
|
||||||
</button>
|
|
||||||
{#each crumbs as crumb, index (crumb.id)}
|
|
||||||
<span class="faint">/</span>
|
|
||||||
<button
|
|
||||||
class:last={index === crumbs.length - 1}
|
|
||||||
onclick={() => workspace.guard(() => workspace.goTo(crumb.id))}
|
|
||||||
>
|
|
||||||
{crumb.slug}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.spaces,
|
|
||||||
.crumbs {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
flex: none;
|
|
||||||
overflow-x: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crumbs { font-size: 12px; }
|
|
||||||
|
|
||||||
button {
|
|
||||||
border: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--fg-dim);
|
|
||||||
padding: 0 6px;
|
|
||||||
line-height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover { color: var(--fg); background: var(--bg-lift); }
|
|
||||||
.space.on { color: var(--bg); background: var(--accent-dim); font-weight: 700; }
|
|
||||||
.add { color: var(--fg-faint); }
|
|
||||||
.crumbs .last { color: var(--accent); }
|
|
||||||
|
|
||||||
@media (pointer: coarse) {
|
|
||||||
button { padding: 4px 10px; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Rail from './Rail.svelte'
|
||||||
import { workspace } from './store.svelte'
|
import { workspace } from './store.svelte'
|
||||||
import { isTouchDevice } from './util'
|
import { isTouchDevice } from './util'
|
||||||
|
|
||||||
@@ -8,65 +9,78 @@
|
|||||||
|
|
||||||
// The path is the file name here: work/einarbeiten/01-was-ist-eebus.
|
// The path is the file name here: work/einarbeiten/01-was-ist-eebus.
|
||||||
const file = $derived(
|
const file = $derived(
|
||||||
workspace.open
|
workspace.open ? `${workspace.here}/${workspace.open.slug}` : `${workspace.here || '~'}/`,
|
||||||
? `${workspace.here}/${workspace.open.slug}${workspace.dirty ? ' [+]' : ''}`
|
|
||||||
: `${workspace.here || '~'}/`,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const lines = $derived(workspace.editing ? workspace.draft.body.split('\n').length : null)
|
const lines = $derived(workspace.editing ? workspace.draft.body.split('\n').length : null)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<span class="mode" class:insert={mode === 'INSERT'} class:visual={mode.startsWith('VISUAL')}>
|
<span
|
||||||
|
class="mode"
|
||||||
|
class:insert={mode === 'INSERT' || mode === 'EDIT'}
|
||||||
|
class:visual={mode.startsWith('VISUAL')}
|
||||||
|
>
|
||||||
{mode}
|
{mode}
|
||||||
</span>
|
</span>
|
||||||
<span class="file">{file}</span>
|
<span class="file">{file}{#if workspace.dirty}<span class="dirty"> [+]</span>{/if}</span>
|
||||||
|
|
||||||
<span class="msg" class:error={workspace.flash?.kind === 'error'}>
|
<span class="msg" class:alarm={workspace.flash?.kind === 'error'}>
|
||||||
{workspace.flash?.text ?? ''}
|
{workspace.flash?.text ?? ''}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{#if lines !== null}<span class="faint">{lines}L</span>{/if}
|
{#if lines !== null}<span class="readout">{lines}<span class="unit">L</span></span>{/if}
|
||||||
<span class="faint hint">
|
<span class="hint">
|
||||||
{isTouchDevice ? 'tap : for commands' : '? help · : command · / search'}
|
{isTouchDevice ? 'tap : for commands' : '? help · : command · / search'}
|
||||||
</span>
|
</span>
|
||||||
<span class="user accent">{workspace.user}</span>
|
|
||||||
|
<div class="tools"><Rail compact /></div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
footer {
|
footer {
|
||||||
|
position: absolute;
|
||||||
|
inset: auto 0 0 0;
|
||||||
|
height: var(--status-h);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 14px;
|
||||||
padding: 0 8px 0 0;
|
padding: 0 calc(var(--rail-w) + 4px) 0 calc(var(--frame) + 4px);
|
||||||
height: 24px;
|
font-size: 11px;
|
||||||
background: var(--bg-lift);
|
color: var(--subtle);
|
||||||
border-top: 1px solid var(--line);
|
|
||||||
font-size: 12px;
|
|
||||||
flex: none;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode {
|
.mode {
|
||||||
background: var(--accent-dim);
|
|
||||||
color: var(--bg);
|
|
||||||
font-weight: 700;
|
|
||||||
padding: 0 10px;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
letter-spacing: 0.08em;
|
height: 18px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex: none;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
background: var(--surface-alt);
|
||||||
|
color: var(--fg);
|
||||||
|
transition: background-color var(--t-base) var(--ease-glide), color var(--t-base) var(--ease-glide);
|
||||||
}
|
}
|
||||||
.mode.insert { background: var(--accent); }
|
.mode.insert { background: var(--light); color: var(--ink); }
|
||||||
.mode.visual { background: var(--warn); }
|
.mode.visual { background: var(--light-dim); color: var(--ink); }
|
||||||
|
|
||||||
.file { color: var(--fg-dim); flex: none; }
|
.file { flex: none; color: var(--subtle); }
|
||||||
.msg { flex: 1; overflow: hidden; text-overflow: ellipsis; color: var(--fg); }
|
.dirty { color: var(--light); }
|
||||||
.msg.error { color: var(--error); }
|
.msg { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--fg); }
|
||||||
.user { flex: none; }
|
.msg.alarm { color: var(--alarm); }
|
||||||
|
.readout { color: var(--fg); font-variant-numeric: tabular-nums; }
|
||||||
|
.unit { color: var(--muted); margin-left: 1px; }
|
||||||
|
.hint { color: var(--muted); letter-spacing: 0.06em; }
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
.tools { display: none; }
|
||||||
.hint { display: none; }
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
footer { padding-right: 6px; gap: 10px; }
|
||||||
|
.hint, .file, .readout { display: none; }
|
||||||
|
.tools { display: flex; flex: none; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+3
-22
@@ -1,5 +1,5 @@
|
|||||||
import { api, type BackupStatus } from './api'
|
import { api, type BackupStatus } from './api'
|
||||||
import { workspace, type Theme } from './store.svelte'
|
import { workspace } from './store.svelte'
|
||||||
import { formatDay, parseDay } from './util'
|
import { formatDay, parseDay } from './util'
|
||||||
|
|
||||||
/** How far the reading pane is asked to scroll: lines, a half page, or an end. */
|
/** How far the reading pane is asked to scroll: lines, a half page, or an end. */
|
||||||
@@ -58,16 +58,6 @@ const findFolder = (target: string) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const themes: Theme[] = ['mocha', 'green', 'amber', 'ice']
|
|
||||||
|
|
||||||
const setTheme = (name: string) => {
|
|
||||||
if (!themes.includes(name as Theme)) {
|
|
||||||
return workspace.say(`unknown theme: ${name} — try ${themes.join(', ')}`, 'error')
|
|
||||||
}
|
|
||||||
workspace.setTheme(name as Theme)
|
|
||||||
workspace.say(`theme ${name}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** `3 minutes ago`, or `never`. Backups are read at a glance, not to the second. */
|
/** `3 minutes ago`, or `never`. Backups are read at a glance, not to the second. */
|
||||||
const ago = (at: number | null) => {
|
const ago = (at: number | null) => {
|
||||||
if (!at) return 'never'
|
if (!at) return 'never'
|
||||||
@@ -649,18 +639,11 @@ export const commands: CommandSpec[] = [
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'theme',
|
|
||||||
args: '<mocha|green|amber|ice>',
|
|
||||||
group: 'view',
|
|
||||||
help: 'switch the colour scheme',
|
|
||||||
run: (arg) => setTheme(arg.trim() || workspace.theme),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'set',
|
name: 'set',
|
||||||
args: '<option>',
|
args: '<option>',
|
||||||
group: 'view',
|
group: 'view',
|
||||||
help: 'theme=mocha|green|amber|ice, vim, novim, clipboard, noclipboard, board, noboard',
|
help: 'vim, novim, clipboard, noclipboard, board, noboard',
|
||||||
run: (arg) => {
|
run: (arg) => {
|
||||||
const option = arg.trim()
|
const option = arg.trim()
|
||||||
if (option === 'vim') return workspace.setVim(true), workspace.say('vim keys on')
|
if (option === 'vim') return workspace.setVim(true), workspace.say('vim keys on')
|
||||||
@@ -673,8 +656,6 @@ export const commands: CommandSpec[] = [
|
|||||||
// options above this one goes to the server.
|
// options above this one goes to the server.
|
||||||
if (option === 'board') return workspace.guard(() => workspace.setBoard(true))
|
if (option === 'board') return workspace.guard(() => workspace.setBoard(true))
|
||||||
if (option === 'noboard') return workspace.guard(() => workspace.setBoard(false))
|
if (option === 'noboard') return workspace.guard(() => workspace.setBoard(false))
|
||||||
const theme = /^theme=(\w+)$/.exec(option)?.[1]
|
|
||||||
if (theme) return setTheme(theme)
|
|
||||||
workspace.say(`unknown option: ${option}`, 'error')
|
workspace.say(`unknown option: ${option}`, 'error')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -756,7 +737,7 @@ for (const spec of commands) {
|
|||||||
for (const alias of spec.aliases ?? []) lookup.set(alias, spec)
|
for (const alias of spec.aliases ?? []) lookup.set(alias, spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Run an ex-command line such as `w`, `date 2026-01-01` or `set theme=amber`. */
|
/** Run an ex-command line such as `w`, `date 2026-01-01` or `set novim`. */
|
||||||
export async function runCommand(line: string): Promise<void> {
|
export async function runCommand(line: string): Promise<void> {
|
||||||
const input = line.trim().replace(/^:/, '')
|
const input = line.trim().replace(/^:/, '')
|
||||||
if (!input) return
|
if (!input) return
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export type Pane = 'list' | 'document'
|
|||||||
/** The tree of documents, or the board of the space being browsed. */
|
/** The tree of documents, or the board of the space being browsed. */
|
||||||
export type View = 'tree' | 'board'
|
export type View = 'tree' | 'board'
|
||||||
export type Flash = { text: string; kind: 'info' | 'error' } | null
|
export type Flash = { text: string; kind: 'info' | 'error' } | null
|
||||||
export type Theme = 'mocha' | 'green' | 'amber' | 'ice'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The whole application state. It is deliberately one object: the keyboard
|
* The whole application state. It is deliberately one object: the keyboard
|
||||||
@@ -64,8 +63,6 @@ class Workspace {
|
|||||||
overlay = $state<'none' | 'help' | 'media'>('none')
|
overlay = $state<'none' | 'help' | 'media'>('none')
|
||||||
media = $state<MediaFile[]>([])
|
media = $state<MediaFile[]>([])
|
||||||
|
|
||||||
theme = $state<Theme>((localStorage.getItem('diary:theme') as Theme) ?? 'mocha')
|
|
||||||
|
|
||||||
vimEnabled = $state(localStorage.getItem('diary:vim') !== 'off')
|
vimEnabled = $state(localStorage.getItem('diary:vim') !== 'off')
|
||||||
|
|
||||||
/** Mirror vim yanks and deletes into the system clipboard, like `set clipboard=unnamed`. */
|
/** Mirror vim yanks and deletes into the system clipboard, like `set clipboard=unnamed`. */
|
||||||
@@ -747,11 +744,6 @@ class Workspace {
|
|||||||
this.media = await api.listMedia()
|
this.media = await api.listMedia()
|
||||||
}
|
}
|
||||||
|
|
||||||
setTheme(theme: Theme) {
|
|
||||||
this.theme = theme
|
|
||||||
localStorage.setItem('diary:theme', theme)
|
|
||||||
}
|
|
||||||
|
|
||||||
setVim(enabled: boolean) {
|
setVim(enabled: boolean) {
|
||||||
this.vimEnabled = enabled
|
this.vimEnabled = enabled
|
||||||
localStorage.setItem('diary:vim', enabled ? 'on' : 'off')
|
localStorage.setItem('diary:vim', enabled ? 'on' : 'off')
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import '@fontsource-variable/jetbrains-mono'
|
||||||
|
import '@fontsource-variable/noto-sans'
|
||||||
import { mount } from 'svelte'
|
import { mount } from 'svelte'
|
||||||
import App from './App.svelte'
|
import App from './App.svelte'
|
||||||
import './app.css'
|
import './app.css'
|
||||||
|
|||||||
Reference in new issue
Block a user