android: phone-shaped chrome and the real note card (M12 step 6)
Two things at once, because they answer one question: what should this look like,
and what should it look like ON A PHONE.
IDENTITY IS SHARED, INTERACTION IS NOT. The card now renders exactly what the web
and desktop render — note colour, checklists, label chips, reminders — using the
same palette values, so a note looks like your note on every surface. The chrome
does not: the desktop's title bar and sidebar are wrong for a thumb.
* NoteTint.kt carries the Tailwind colours from frontend/src/notes/colors.ts
VALUE FOR VALUE, generated from tailwindcss 3.4 rather than eyeballed. Dark
tints keep the web's alpha (dark:bg-*-950/40) instead of a precomputed blend,
because Compose composites translucency over the background exactly as CSS
does.
* Dynamic colour is GONE. It was the more Android-native choice and it made the
app look like a different product — on a stock emulator with no wallpaper it
renders as undifferentiated grey, which is what the operator saw. Three peer
surfaces share one identity; the brand #F5C518 is the same value the web
manifest and the launcher icon already use.
* The board is a two-column staggered grid, the Compose equivalent of the CSS
multi-column NoteGrid.vue uses.
PHONE ERGONOMICS, chosen with the operator:
* Search IS the top bar. After writing a note, finding one is the most common
thing you do, and burying it behind an icon costs a tap every time. Debounced
180ms and cancelled per keystroke — without that a fast typist queues one
full-text query per character and results land out of order.
* A + button is the only way in. One obvious target beat a capture bar and a
button competing for the same job.
* Navigation moved into a drawer behind the search bar's menu icon, which is
where archive/trash/labels/reminders now live. They had nowhere to go once
search took the top bar, and would otherwise have been unreachable.
* The compose sheet asks note-or-list up front. On a phone those are different
typing tasks and switching halfway is worse than choosing at the start. A
list takes one item per line — fast to type, versus a tap per row.
Three new bindings the UI needed: search_notes, reminder_notes, list_labels.
Search goes through the CORE so "what matches" cannot drift between surfaces;
filtering the loaded list in Kotlin would have been less code and a different
product. reminder_notes is its own call because the core models it that way —
"has a reminder" cuts across archived and active alike.
Empty states are per-destination. "Nothing here yet" is encouraging on an empty
board, wrong in Trash, and misleading after a search where the notes exist but
did not match.
Verified locally before pushing: bindings generated from a host .so and read back,
ktlint and detekt clean from the image's pinned CLIs, cargo fmt/clippy/test green
(107 tests). Two detekt findings were fixed by extraction rather than by relaxing
the rules — this is the first Compose code in the repo and the thresholds should
have to earn their exceptions.
Still unbuilt: tapping a card does nothing. The editor is next.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+29
-1
@@ -43,7 +43,7 @@ use thoughtsync_core::sync::blobs::BlobStore;
|
||||
use thoughtsync_core::sync::{client, compat, engine, push, state};
|
||||
|
||||
use models::{
|
||||
patch_from, Identity, Note, NoteDraft, NoteEdit, NoteQuery, ProbeResult, RevokeOutcome,
|
||||
patch_from, Identity, Label, Note, NoteDraft, NoteEdit, NoteQuery, ProbeResult, RevokeOutcome,
|
||||
SyncOutcome, SyncStatus,
|
||||
};
|
||||
|
||||
@@ -170,6 +170,34 @@ impl ThoughtSync {
|
||||
.map_err(CoreError::store)
|
||||
}
|
||||
|
||||
/// Full-text search across titles, bodies and checklist items.
|
||||
///
|
||||
/// The core owns the query — it searches the same columns the desktop and web
|
||||
/// search, so "what matches" cannot drift between surfaces. Filtering the
|
||||
/// board list in Kotlin would have been less code and a different product.
|
||||
pub fn search_notes(&self, query: String) -> Result<Vec<Note>, CoreError> {
|
||||
let conn = self.db.conn().map_err(CoreError::store)?;
|
||||
let notes = local::store::search(&conn, &query).map_err(CoreError::store)?;
|
||||
Ok(notes.into_iter().map(Note::from).collect())
|
||||
}
|
||||
|
||||
/// Notes carrying a reminder, soonest first.
|
||||
///
|
||||
/// A dedicated call rather than a board `view`, because that is how the core
|
||||
/// models it — `list_notes` only understands trashed/archived/default.
|
||||
pub fn reminder_notes(&self) -> Result<Vec<Note>, CoreError> {
|
||||
let conn = self.db.conn().map_err(CoreError::store)?;
|
||||
let notes = local::store::reminders(&conn).map_err(CoreError::store)?;
|
||||
Ok(notes.into_iter().map(Note::from).collect())
|
||||
}
|
||||
|
||||
/// Every label with its note count, for the navigation drawer.
|
||||
pub fn list_labels(&self) -> Result<Vec<Label>, CoreError> {
|
||||
let conn = self.db.conn().map_err(CoreError::store)?;
|
||||
let labels = local::store::list_labels(&conn).map_err(CoreError::store)?;
|
||||
Ok(labels.into_iter().map(Label::from).collect())
|
||||
}
|
||||
|
||||
pub fn trash_note(&self, id: String) -> Result<Note, CoreError> {
|
||||
let conn = self.db.conn().map_err(CoreError::store)?;
|
||||
local::store::trash(&conn, &id)
|
||||
|
||||
@@ -213,6 +213,35 @@ impl From<core_models::LinkPreview> for LinkPreview {
|
||||
}
|
||||
}
|
||||
|
||||
/// A label, as the sidebar lists them.
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct Label {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
/// Same colour vocabulary as notes, so one palette serves both.
|
||||
pub color: String,
|
||||
/// How many notes carry it. Only populated in listings — `None` elsewhere,
|
||||
/// matching the REST single-label responses.
|
||||
pub count: Option<i64>,
|
||||
}
|
||||
|
||||
impl From<core_models::Label> for Label {
|
||||
fn from(value: core_models::Label) -> Self {
|
||||
let core_models::Label {
|
||||
id,
|
||||
name,
|
||||
color,
|
||||
count,
|
||||
} = value;
|
||||
Label {
|
||||
id,
|
||||
name,
|
||||
color,
|
||||
count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────────── queries and edits ─────────────────────────────
|
||||
|
||||
/// What the board is asking for. Mirrors the core's `ListQuery`.
|
||||
|
||||
Reference in New Issue
Block a user