Capture & Journal

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.

The notes board

6 components

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.

NotesBoard
The board itself: capture bar, search, an archived toggle, and a masonry grid that splits into a Pinned section and everything else. At 644 lines it is the richest capture surface in Flux and the only one that owns six components of its own.
src/components/notes/notes-board.tsx
Default · pinned section present

Pinned

Ship the reporting rewrite
First cut by Friday. Ask about the export format before starting.
Saved
Questions for the retro
Why did the estimate double twice?
Saved

Others

Reading list
Two on systems, one on attention.
Saved
Groceries
Coffee, oats, lemons
Saved
Loading

Loading notes…

Empty · no query set

No notes yet. Take one above.

Empty · a query is set

No matching notes.

Archived view · populated
Old sprint notes

Superseded by the rewrite plan.

Conference ideas

Talk about the planner lock.

Archived view · empty

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.

QuickCapture
The bar at the top of the board. Collapsed it is a single button that costs nothing to look at; expanded it is a title, a body, and the smallest possible commitment to writing something down.
src/components/notes/notes-board.tsx
Collapsed
Expanded · empty, Add disabled
Expanded · body only, Add enabled

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".

NoteCard
One note, edited in place. There is no open, no modal and no save: the card on the board is the editor. The action row and the tag adder hide themselves until you hover, so a board at rest is text and nothing else.
src/components/notes/notes-board.tsx
At rest · actions at 60%, tag adder invisible
Questions for the retro
Why did the estimate double twice?
Saved
Hovered · pinned, tagged, everything at full opacity
Ship the reporting rewrite
First cut by Friday. Ask about the export format before starting.
work urgent
Saved
Checklist mode · the body textarea is replaced
Groceries
Coffee
Oats
Saved
Saving · the indicator flips for 750ms
Reading list
Two on systems, one on attention.
Saving…

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.

ColorPicker
Nine swatches in a five-column grid: the note's 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.
src/components/notes/notes-board.tsx
All nine swatches · teal selected
All nine card tints
default
bg-card
red
red-500/10
orange
orange-500/10
yellow
yellow-500/10
green
green-500/10
teal
teal-500/10
blue
blue-500/10
purple
purple-500/10
pink
pink-500/10

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.

ChecklistEditor
What a note's body becomes once it is a checklist. Every row is live: tick it, retype it, or remove it. The last row is always an empty adder, so there is no Add button to find.
src/components/notes/notes-board.tsx
Mixed · checked rows strike through
Coffee
Oats
Lemons
Empty · only the adder row

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.

ArchivedCard
The same note with the editing taken out. Read-only text, two buttons, no colour picker and no tags. Archiving is Flux's version of a soft delete, and this is what the note looks like on the other side of it.
src/components/notes/notes-board.tsx
Body note
Old sprint notes

Superseded by the rewrite plan.

Checklist note · bullets, not checkboxes
Groceries
  • • Coffee
  • • Oats
  • • Lemons

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.

Voice capture

2 components

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.

AudioJournal · recorder
The record head. Its state is a three-way union and the button row rebuilds itself for each: which buttons exist at all changes, not just how they look.
src/components/journal/audio-journal.tsx
idle · Record only, no badge
Audio journal
Record or upload. Entries wait until you commit them — processed at end of day, or immediately if you choose.
recording · Pause + Stop & save, default badge
● 1:24
paused · Resume + Stop & save, outline badge
⏸ 1:24
Signed out · controls disabled

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.

AudioJournal · entry row
One saved recording on its way to becoming a journal entry. Status is a five-way union, and each branch carries its own label, its own badge variant and its own set of actions, including two branches that offer nothing to click.
src/components/journal/audio-journal.tsx
pending · outline · Commit + Commit & process

Thoughts on the rewrite

Saved — not committed 1:24
committed · default · Process now

Recording 09/07/2026, 08:12:40

Queued for end-of-day 0:47
processing · default · no actions

Morning walk notes

Transcribing… 3:02
transcribed · outline · no actions, transcript shown

Retro prep

In journal 2:11

The estimate doubled twice and nobody said anything, which is the actual thing to talk about on Thursday.

failed · destructive · Retry, error text

Late call debrief

Failed — retry 0:19 Transcription provider returned 503

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.

Structured reflection

3 components

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.

Mental snapshot card
Three questions, three boxes: thoughts, feelings, ideas. Saving writes the row and immediately queues the same text for the context pipeline, so a check-in is both a diary and an input to what Flux learns about you.
src/app/(app)/mental/page.tsx
Default · three prompts
Mental State
A space to capture your current mental landscape.
Saving · spinner swaps in, button disabled
Recent snapshots · only when at least one exists
Recent snapshots
Your last few mental check-ins.

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.

NewEntryDialog Reproduced bug
The long-form route: a title, a date, fifteen rows of text and a tag line. This is the only capture surface in Flux with an explicit Save button, and it lives in the Journal page header rather than on the board.
src/app/(app)/journal/page.tsx
Empty · Save Entry disabled
New Journal Entry
Capture your thoughts, ideas, or reflections.
Filled · Save Entry enabled
Whitespace-only title · the dialog closes and nothing saves
Title and content are required.

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.

ReadEntryDialog
An entry, read back. It is deliberately not an editor: the only things you can do to a past reflection are read it and delete it.
src/app/(app)/journal/page.tsx
Titled · with tags
Daily Reflection
July 9, 2026

The estimate doubled twice and nobody said anything. Worth raising on Thursday rather than carrying it.

reflection work
Untitled · no tags row
Untitled entry
July 8, 2026

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.