feat(transactions): the transaction page as a receipt with context
The amount is the headline, notes come first at full width, then the last twelve visits to the same place as a dot timeline next to the paying account's days around it with the running balance. Fields edit in place: j and k move, Enter opens and saves, Esc cancels, s c, s b, s t and s s jump to classification. A matching subscription shows as a suggestion, L links it.
This commit is contained in:
1 parent
58b356ea8b
commit
349a6a0b32
14 files changed
+2137
-683
No files matched your search
@@ -0,0 +1,67 @@
|
||||
export type Inline =
|
||||
| { kind: "text" | "bold" | "code"; text: string }
|
||||
| { kind: "link"; text: string; href: string };
|
||||
|
||||
export type Block =
|
||||
| { kind: "head"; line: Inline[] }
|
||||
| { kind: "list"; ordered: boolean; items: Inline[][] }
|
||||
| { kind: "para"; lines: Inline[][] };
|
||||
|
||||
const TOKEN =
|
||||
/\*\*([^*]+)\*\*|`([^`]+)`|\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>()]*[^\s<>().,;:!?'"])/g;
|
||||
const BULLET = /^\s*(?:[-*•]|(\d+)[.)])\s+(.*)$/;
|
||||
const HEAD = /^#{1,6}\s+(.*)$/;
|
||||
const LINES = /\r?\n/;
|
||||
|
||||
export function inline(text: string): Inline[] {
|
||||
const out: Inline[] = [];
|
||||
let last = 0;
|
||||
for (const match of text.matchAll(TOKEN)) {
|
||||
const [whole, bold, code, label, href, url] = match;
|
||||
if (match.index > last) {
|
||||
out.push({ kind: "text", text: text.slice(last, match.index) });
|
||||
}
|
||||
if (bold) {
|
||||
out.push({ kind: "bold", text: bold });
|
||||
} else if (code) {
|
||||
out.push({ kind: "code", text: code });
|
||||
} else {
|
||||
out.push({ href: href ?? url, kind: "link", text: label ?? url });
|
||||
}
|
||||
last = match.index + whole.length;
|
||||
}
|
||||
if (last < text.length) {
|
||||
out.push({ kind: "text", text: text.slice(last) });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function blocks(notes: string): Block[] {
|
||||
const result: Block[] = [];
|
||||
let open: Block | null = null;
|
||||
for (const raw of notes.split(LINES)) {
|
||||
const line = raw.trimEnd();
|
||||
const head = line.match(HEAD);
|
||||
const bullet = line.match(BULLET);
|
||||
if (!line.trim()) {
|
||||
open = null;
|
||||
} else if (head) {
|
||||
result.push({ kind: "head", line: inline(head[1]) });
|
||||
open = null;
|
||||
} else if (bullet) {
|
||||
const ordered = Boolean(bullet[1]);
|
||||
if (open?.kind !== "list" || open.ordered !== ordered) {
|
||||
open = { items: [], kind: "list", ordered };
|
||||
result.push(open);
|
||||
}
|
||||
open.items.push(inline(bullet[2]));
|
||||
} else {
|
||||
if (open?.kind !== "para") {
|
||||
open = { kind: "para", lines: [] };
|
||||
result.push(open);
|
||||
}
|
||||
open.lines.push(inline(line));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in new issue
Block a user