Refactor todo format to remove dash prefix

This commit is contained in:
Corey Johnson 2025-07-10 14:53:48 -07:00
parent 3abd4447b2
commit 785d90e140
12 changed files with 58 additions and 51 deletions

View File

@ -103,6 +103,7 @@
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/commands": "^6.8.1",
"@codemirror/language": "^6.11.1",
"@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.37.2",
"@lezer/generator": "^1.7.3",
@ -121,10 +122,9 @@
},
},
"packages/werewolf-ui": {
"name": "werewolfUI",
"name": "@workshop/werewolf-ui",
"version": "0.1.0",
"dependencies": {
"bun-plugin-tailwind": "^0.0.15",
"hono": "^4.8.3",
"lucide-static": "^0.525.0",
"tailwindcss": "^4.0.6",
@ -146,6 +146,8 @@
"@codemirror/language": ["@codemirror/language@6.11.1", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.1.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ=="],
"@codemirror/search": ["@codemirror/search@6.5.11", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "crelt": "^1.0.5" } }, "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA=="],
"@codemirror/state": ["@codemirror/state@6.5.2", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA=="],
"@codemirror/view": ["@codemirror/view@6.37.2", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw=="],
@ -218,6 +220,8 @@
"@workshop/todo": ["@workshop/todo@workspace:packages/todo"],
"@workshop/werewolf-ui": ["@workshop/werewolf-ui@workspace:packages/werewolf-ui"],
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"ajv": ["ajv@6.10.0", "", { "dependencies": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg=="],
@ -730,8 +734,6 @@
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
"werewolfUI": ["werewolfUI@workspace:packages/werewolf-ui"],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
@ -780,6 +782,14 @@
"@workshop/nano-remix/hono": ["hono@4.8.4", "", {}, "sha512-KOIBp1+iUs0HrKztM4EHiB2UtzZDTBihDtOF5K6+WaJjCPeaW4Q92R8j63jOhvJI5+tZSMuKD9REVEXXY9illg=="],
"@workshop/todo/@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
"@workshop/todo/hono": ["hono@4.8.4", "", {}, "sha512-KOIBp1+iUs0HrKztM4EHiB2UtzZDTBihDtOF5K6+WaJjCPeaW4Q92R8j63jOhvJI5+tZSMuKD9REVEXXY9illg=="],
"@workshop/werewolf-ui/@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
"@workshop/werewolf-ui/hono": ["hono@4.8.4", "", {}, "sha512-KOIBp1+iUs0HrKztM4EHiB2UtzZDTBihDtOF5K6+WaJjCPeaW4Q92R8j63jOhvJI5+tZSMuKD9REVEXXY9illg=="],
"ajv/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
"amqplib/readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="],
@ -836,10 +846,6 @@
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"werewolfUI/@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
"werewolfUI/hono": ["hono@4.8.4", "", {}, "sha512-KOIBp1+iUs0HrKztM4EHiB2UtzZDTBihDtOF5K6+WaJjCPeaW4Q92R8j63jOhvJI5+tZSMuKD9REVEXXY9illg=="],
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
@ -890,6 +896,10 @@
"@workshop/nano-remix/@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"@workshop/todo/@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"@workshop/werewolf-ui/@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"amqplib/readable-stream/inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"amqplib/readable-stream/string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
@ -904,8 +914,6 @@
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"werewolfUI/@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"@modelcontextprotocol/sdk/express/body-parser/bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],

View File

@ -27,7 +27,6 @@ try {
await Promise.all([run(["bun", "run", "--filter=@workshop/http", "start"]), run(["bun", "bot:discord"])])
console.log("✅ All processes completed successfully")
} catch (error) {
console.log(`🌭`, error.message)
console.error("❌ One or more processes failed:", error)
process.exit(1)
}

View File

@ -9,12 +9,9 @@ type BuildRouteOptions = {
export const buildRoute = async ({ distDir, routeName, filepath, force = false }: BuildRouteOptions) => {
if (!force && !(await shouldRebuild(routeName, filepath, distDir))) {
console.log(`🌭 Skipping build for ${routeName} - up to date`)
return
}
console.log(`🌭 Building route ${routeName} from ${filepath}`)
const scriptPath = join(import.meta.dirname, "../scripts/build.ts")
const proc = Bun.spawn({

View File

@ -12,6 +12,7 @@
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/commands": "^6.8.1",
"@codemirror/language": "^6.11.1",
"@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.37.2",
"@lezer/generator": "^1.7.3",

View File

@ -44,7 +44,7 @@ const handleNewlineChange = (opts: HandleNewlineChangeOpts) => {
userEvent: "delete",
})
} else {
const prefix = `- [ ] `
const prefix = `[ ] `
opts.update.view.dispatch({
changes: { from: insertPos, to: insertPos, insert: prefix },
selection: { anchor: insertPos + prefix.length },

View File

@ -4,11 +4,14 @@
transform-origin: center center;
}
.todo-completed {
text-decoration: line-through !important;
.todo-completed * {
text-decoration: line-through;
color: black !important;
opacity: 0.5;
}
.todo-completed .todo-indent {
text-decoration: none #10B981 !important;
}
@keyframes todoCompleting {

View File

@ -3,26 +3,26 @@ import { Todo } from "@/todo"
import { test, expect } from "bun:test"
test("parsing valid todos", () => {
expectTodo("- [ ] some words and that is it", { done: false, text: "some words and that is it" })
expectTodo("- [x] has #some #tags and @2/5/25 5m", {
expectTodo("[ ] some words and that is it", { done: false, text: "some words and that is it" })
expectTodo("[x] has #some #tags and @2/5/25 5m", {
done: true,
text: "has #some #tags and @2/5/25 5m",
tags: ["some", "tags"],
timeEstimate: 300,
dueDate: DateTime.fromISO("2025-02-05"),
})
expectTodo("- [ ] works with iso dates @2055/01/2", {
expectTodo("[ ] works with iso dates @2055/01/2", {
text: "works with iso dates @2055/01/2",
dueDate: DateTime.fromISO("2055-01-02"),
})
expectTodo(" - [ ] handles nested todos", { text: "handles nested todos", indent: " " })
expectTodo(" [ ] handles nested todos", { text: "handles nested todos", indent: " " })
})
test("todo nodes", () => {
const todo = Todo.parse(" - [ ] some #words and @2/2/25")
const todo = Todo.parse(" [ ] some #words and @2/2/25")
expect(todo!.nodes).toEqual([
{ type: "indent", content: " " },
{ type: "checkbox", content: "- [ ] ", checked: false },
{ type: "checkbox", content: "[ ] ", checked: false },
{ type: "text", content: "some" },
{ type: "whitespace", content: " " },
{ type: "tag", content: "#words" },
@ -34,18 +34,18 @@ test("todo nodes", () => {
})
test("parsing invalid todos", () => {
expectInvalidTodo("- [ ]")
expectInvalidTodo(" -[x")
expectInvalidTodo("-[omg] some text")
expectInvalidTodo("[ ")
expectInvalidTodo(" -[]")
expectInvalidTodo("-[] some text")
})
// Helpers
test("todo to string", () => {
const todo = Todo.parse("- [ ] some #words and @2/2/25")
expect(todo!.toString()).toBe("- [ ] some #words and @2/2/25")
const todo = Todo.parse("[ ] some #words and @2/2/25")
expect(todo!.toString()).toBe("[ ] some #words and @2/2/25")
const nestedTodo = Todo.parse(" - [x] more #words and @2/2/25")
expect(nestedTodo!.toString()).toBe(" - [x] more #words and @2/2/25")
const nestedTodo = Todo.parse(" [x] more #words and @2/2/25")
expect(nestedTodo!.toString()).toBe(" [x] more #words and @2/2/25")
})
const expectTodo = (input: string, expectation: Partial<Todo>) => {

View File

@ -8,7 +8,7 @@ export type TodoJSON = {
children: TodoJSON[]
}
export const checkboxRegex = /^\s*-\s*\[\s*(\w?)\s*\]\s*/
export const checkboxRegex = /^\s*\[\s*(\w?)\s*\]\s*/
export class Todo {
done: boolean
@ -28,7 +28,7 @@ export class Todo {
}
toString() {
let string = `${this.indent}- [${this.done ? "x" : " "}] ${this.text}`
let string = `${this.indent}[${this.done ? "x" : " "}] ${this.text}`
this.children.forEach((child) => {
string += "\n" + child.toString()
})

View File

@ -10,6 +10,7 @@ import { todoClickHandler } from "@/todoClickHandler"
import { dateAutocompletion } from "@/dateAutocompletion"
import { todoTimer } from "@/todoTimer"
import { DateTime } from "luxon"
import { searchKeymap } from "@codemirror/search"
import "./todo.css"
@ -44,6 +45,7 @@ const createEditorExtensions = (
keymap.of(historyKeymap),
keymap.of(defaultKeymap),
keymap.of(foldKeymap),
keymap.of(searchKeymap),
]
}
@ -120,7 +122,7 @@ export const TodoEditor = ({ defaultValue, onChange }: TodoEditorProps) => {
}
const KeyboardShortcuts = ({ onClose }: { onClose: () => void }) => {
const shortcuts = buildKeyBindings(undefined as any).filter((k) => !k.hidden)
const shortcuts = buildKeyBindings(undefined as any).filter((k) => k.label)
return (
<div class="fixed inset-0 bg-[#00000088] z-50" onClick={onClose}>

View File

@ -5,8 +5,9 @@ import { type RefObject } from "hono/jsx"
import { parseTodoList, todoListToString } from "@/todoList"
import { todoTimer } from "./todoTimer"
import { triggerTodoCompletionEffect } from "./todoCompletion"
import { selectNextOccurrence } from "@codemirror/search"
export type Command = KeyBinding & { label: string; key: string; hidden?: boolean }
export type Command = KeyBinding & { label?: string; key: string }
export const buildKeyBindings = (
filterElRef: RefObject<HTMLInputElement>,
@ -79,9 +80,7 @@ export const buildKeyBindings = (
},
},
{
label: "Stop the todo timer",
key: "Escape",
hidden: true,
run: (view: EditorView) => {
const timerPlugin = view.plugin(todoTimer)
if (timerPlugin?.currentWidget) {
@ -100,13 +99,11 @@ export const buildKeyBindings = (
},
},
{
label: "Indent the todo item",
key: "Tab",
preventDefault: true,
run: indentMore,
},
{
label: "Unindent the todo item",
key: "Shift-Tab",
preventDefault: true,
run: indentLess,

View File

@ -30,13 +30,13 @@ test("todoListToString", () => {
const result = todoListToString(todoList)
const expected = `# Today
- [ ] a task with a #tag
- [x] a #tag and date @2/2/25 in a task
[ ] a task with a #tag
[x] a #tag and date @2/2/25 in a task
# Tomorrow
- [ ] another task
- [ ] a subtask
- [x] completed
- [ ] a task with a @1/2/25 date
[ ] another task
[ ] a subtask
[x] completed
[ ] a task with a @1/2/25 date
`
expect(result).toEqual(expected)
@ -44,15 +44,15 @@ test("todoListToString", () => {
test("parseTodoList", () => {
const todoListString = `# Today
- [ ] a task with a #tag
- [x] a #tag and date @2/2/25 in a task
[ ] a task with a #tag
[x] a #tag and date @2/2/25 in a task
# Tomorrow
- [ ] another task
- [ ] a subtask
- [x] completed
[ ] another task
[ ] a subtask
[x] completed
- [ ] a task with a @1/2/25 date`
[ ] a task with a @1/2/25 date`
const result = parseTodoList(todoListString)

View File

@ -66,7 +66,7 @@ export const todoListToString = (todoList: TodoList): string => {
}
const todoJsonToString = (todoOrString: Todo): string => {
let result = `${todoOrString.indent}- [${todoOrString.done ? "x" : " "}] ${todoOrString.text}\n`
let result = `${todoOrString.indent}[${todoOrString.done ? "x" : " "}] ${todoOrString.text}\n`
todoOrString.children.forEach((child) => {
result += todoJsonToString(child)