Getting a thought out of your head before it evaporates, and turning what you caught into something the system can read. Everything here is optimised for the moment before the idea is gone: one tap to a note, one tap to a recording, no form to fill in first. The structure comes afterwards, and only when you ask for it.
A Keep-style masonry board living inside the Journal page: pin what matters, tint what needs finding again, and turn any note into a checklist without leaving it. Every edit saves itself on a 600ms debounce, so there is no Save button anywhere on this board and no way to lose a note by navigating away.
Loading notes…
No notes yet. Take one above.
No matching notes.
Superseded by the rewrite plan.
Talk about the planner lock.
No archived notes.
Two empty strings, and which one you get is decided by the search box. With a query set it reads "No matching notes."; with an empty query, "No notes yet. Take one above." Three structural details: the Pinned heading and its grid render only when something is pinned, the Others heading appears only when there are pinned notes and unpinned ones (so a board with no pins has an unlabelled grid, which is correct), and the archived count on the toggle is computed from the filtered set, so searching changes the number in the parentheses. The empty branch is keyed on others.length === 0 && pinned.length === 0, which means a search matching only pinned notes shows the pinned grid and no empty message.
Add needs a title or a body, not both (disabled={!title.trim() && !body.trim()}), which is why the body-only variant above is live. Enter in the title field jumps to the body rather than submitting. Close discards without asking: there is no confirmation, and the text is gone. Every note born here is stamped source: "journal".
The checklist toggle is a one-way door for your line breaks. Converting a note to a checklist seeds items by splitting the body on newlines and dropping blanks, but only when the checklist is empty; converting back leaves checklist populated, so the original body survives and a second conversion reuses the old items rather than re-reading the body. The pin icon swaps geometry (Pin to PinOff) rather than colour, the checklist button tints to --primary when active, and "Saved" is a 750ms timer, not a server acknowledgement: it says Saved because time passed, not because anything was confirmed written.
color field is a nine-way union and this is the whole of it. Picking one tints the card so a note can be found by colour on a crowded board.This is the one place Flux leaves its own design system. Every other colour in the product resolves through a token in globals.css; these nine name Tailwind's stock palette directly (bg-red-500/10 for the card, bg-red-400 for the swatch), so the board has a colour language that the theme cannot reach and a re-theme would not touch. Note the mismatch that follows from it: the teal swatch is Tailwind's #2dd4bf, not Flux's own --accent teal #009485, and the purple is #c084fc, not the product's --primary #673AB6. The grid is grid-cols-5 holding nine swatches, so the second row is deliberately short. default is the only swatch that is not a colour: it renders --background with a border, which on a white card reads as an empty circle.
Checked rows get line-through and drop to muted; nothing reorders, so a ticked item stays where it was. The adder commits on Enter or blur, which means clicking away from a half-typed item silently adds it. The per-row delete button is opacity-0 until you hover that specific row (a group/item scope, not the card's group), so it is invisible in the state above.
Superseded by the rewrite plan.
An archived checklist drops the checkboxes and renders a literal • bullet per item, keeping only line-through to show what was done. The title is conditional ({note.title && …}), so an untitled archived note starts straight at its body. Delete here is immediate: no confirmation dialog, unlike the dropdown route on a live NoteCard, which is also immediate. Archiving is reversible; the bin is not.
For the thought you cannot type: talk, and deal with it later. The design point is the gap between saving and committing. A recording sits inert until you say it counts, because a half-finished thought said out loud should not become a journal entry by default.
Sign in to record and save audio entries.
The badge glyphs are literal text, not icons. '● ' and '⏸ ' are typed into the JSX next to the mono-spaced timer, while every other control on the card uses Lucide. Reproduced quirk, signed-out state: "Upload audio" is rendered asChild over a <span>, so the disabled prop lands on an element that cannot be disabled. It never dims and never blocks a click, and only the hidden file input behind it is actually disabled. The Record button beside it dims correctly, so the two disagree about the same condition. Also worth knowing: Pause keeps the buffered chunks (nothing is discarded), and only Stop & save uploads.
Thoughts on the rewrite
Recording 09/07/2026, 08:12:40
Morning walk notes
Retro prep
The estimate doubled twice and nobody said anything, which is the actual thing to talk about on Thursday.
Late call debrief
The Process now button uses the spinner icon and does not spin. Loader2 is rendered without animate-spin, so it reads as a static swirl; the same icon on the Mental page does get the class. "Retry" is not a distinct path either: it calls the same commit-and-process function the pending row uses. Two smaller notes: a row with no title falls back to Recording {locale timestamp}, which is locale and timezone dependent, and "end of day" has no cron behind it. Committed entries from a previous day are only processed when someone loads the page, so an entry queued on Friday waits until you open the Journal again.
Capture with a shape to it. Where a note is whatever you type, these ask a question first: what are you thinking, how do you feel, what is the idea. That structure is not decoration. It is what the context pipeline reads, which is why a mental snapshot is queued for extraction the moment it saves.
09/07/2026, 08:12:40
Thoughts: The rewrite is bigger than the estimate said.
Feelings: Wired, not tired. Slightly behind.
Ideas: Cut the export format from the first pass.
08/07/2026, 19:40:02
Feelings: Flat after the late call.
Every one of the three fields is optional, but at least one must be filled. Saving with all three blank raises a "Nothing to save" toast rather than writing an empty row, and each field is stored as null rather than "" when blank, which is why the second recent row above shows only Feelings. The Save button is disabled by saving alone, never by emptiness, so the empty case is caught by a toast after the click rather than prevented before it. Note the placeholders end in ?...: a question mark followed by three literal periods, in all three fields, reproduced as shipped.
The toast fires, the dialog closes anyway, and the text is gone.
The Save button is wrapped in DialogClose, so it closes the dialog whether or not the save succeeded. The disabled rule tests !title || !content (raw truthiness) while the save handler tests .trim(), and the gap between those two is real: a title of a single space is truthy, so the button enables, the handler rejects it, a "Title and content are required." toast fires, and DialogClose closes the dialog regardless. The entry is not saved and the content is not recoverable. Reproduced as shipped, not fixed. Also note the dialog does not reset entryDate to today on reopen, only after a successful save, so a dialog opened across midnight keeps yesterday's date.
The estimate doubled twice and nobody said anything. Worth raising on Thursday rather than carrying it.
Flat after the late call. Nothing to add.
Delete is immediate and unconfirmed, and it sits in the primary footer position where Save usually lives. An empty title falls back to the literal string "Untitled entry" (the same fallback the board card uses), and the tags row is skipped entirely rather than rendering an empty flex container. The date is formatted with toLocaleDateString against the viewer's locale, so the string above is what a US-English reader sees and not what everyone sees.