all: trim the history essays out of the longest comments
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 2m0s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 3m11s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m51s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m39s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 9m4s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 2m0s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 3m11s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m51s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m39s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 9m4s
From the audit (#5179). Ten comment blocks narrated how the code got here: milestone numbers, earlier values, the operator's verdict on an old design. Each now says what the code does and why, and the history stays in git, Scribe and docs/sync.md. The protocol-version comment in sync.py points at docs/sync.md's policy section, which already lists every bump. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -143,28 +143,18 @@ fun List<EditorBlock>.plusTask(): Pair<List<EditorBlock>, Long> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-read ONE prose block for `- [ ] ` lines somebody typed by hand.
|
||||
* Re-read ONE prose block for `- [ ] ` lines somebody typed by hand, so a marker
|
||||
* typed in the editor becomes an item without closing and reopening the note.
|
||||
*
|
||||
* [splitBlocks] runs once, when the editor opens. After that the blocks are the state
|
||||
* and nothing reads the body again — every edit travels the other way, through
|
||||
* [joinBlocks]. So a marker typed by hand stayed literal text on screen until the note
|
||||
* was closed and reopened, even though it was already a real item in storage and the
|
||||
* card was already drawing a checkbox for it. The editor was the only place that
|
||||
* disagreed.
|
||||
* **On blur, and only the block being left:** re-splitting on a keystroke would move
|
||||
* the caret, and converting the moment `- [ ]` is complete would catch an item with
|
||||
* no text yet.
|
||||
*
|
||||
* **On blur, and only the block being left.** There is no good moment to convert while
|
||||
* someone is typing: re-splitting on a keystroke moves the caret out of the word being
|
||||
* written, and converting the instant `- [ ]` is complete does it before the item has
|
||||
* any text. Blur is the one moment the person has demonstrably finished with the block,
|
||||
* so a re-split costs no caret and cannot catch a half-typed line.
|
||||
* Returns THIS LIST when there was nothing to promote; the caller relies on that to
|
||||
* leave the state alone, so a blur that changed nothing doesn't re-key every field.
|
||||
*
|
||||
* Returns THIS LIST, not an equal copy, when there was nothing to promote — the caller
|
||||
* leans on that to leave the state alone, and a blur that changed nothing must not
|
||||
* re-key every field below it.
|
||||
*
|
||||
* Non-canonical markers (`- [X]`, an odd bullet) come back canonical, exactly as they
|
||||
* would have on reopen. That is the only case where this changes the body rather than
|
||||
* only the way it is drawn.
|
||||
* Non-canonical markers (`- [X]`, an odd bullet) come back canonical, as they would on
|
||||
* reopen.
|
||||
*/
|
||||
internal fun List<EditorBlock>.promotingTasks(index: Int): List<EditorBlock> {
|
||||
val block = getOrNull(index)
|
||||
|
||||
@@ -101,22 +101,13 @@ fun NoteCard(
|
||||
.border(1.dp, if (dark) CARD_EDGE_DARK else CARD_EDGE_LIGHT, RoundedCornerShape(CARD_RADIUS))
|
||||
.padding(12.dp),
|
||||
) {
|
||||
// TAGS FIRST. They used to sit under everything else, which on a tall note put
|
||||
// the one thing that says what a note IS below the fold of a glance. A board is
|
||||
// scanned, not read, and the answer to "which of these is about the thing I am
|
||||
// looking for" should be the first thing the eye lands on rather than the last.
|
||||
// TAGS FIRST, on a row of their own above the body: a board is scanned, and
|
||||
// what a note is about should be the first thing the eye lands on. Not beside
|
||||
// the body, whose first line is the note's name.
|
||||
//
|
||||
// Above the body rather than beside it, because the body's first line is the
|
||||
// note's NAME (M13 steps 3 and 4) and a chip floated next to it would compete
|
||||
// with the thing that identifies the note. A row of its own costs one line and
|
||||
// only on notes that have tags at all.
|
||||
//
|
||||
// ONLY the labels whose text is not still in the note. `via_tag` means exactly
|
||||
// "backed by body text" since M311, so a chip for one printed the same tag
|
||||
// twice — once where it was typed, once up here — and the card was carrying
|
||||
// furniture for information it was already showing. A tag left in prose is
|
||||
// tinted in place instead; see [tintTags]. What reaches this row is what the
|
||||
// body cannot say: a tag lifted off its own line, and a label added by hand.
|
||||
// Only labels whose text is not still in the note (`via_tag` is false). A tag
|
||||
// left in prose is tinted in place instead (see [tintTags]), so this row holds
|
||||
// what the body cannot say: a tag lifted off its own line, or one added by hand.
|
||||
val chips = note.labels.filterNot { it.viaTag }
|
||||
if (chips.isNotEmpty()) {
|
||||
LabelChips(labels = chips)
|
||||
@@ -192,34 +183,16 @@ fun NoteCard(
|
||||
}
|
||||
|
||||
/**
|
||||
* What you can do to a note without opening it.
|
||||
* What you can do to a note without opening it, on a long press.
|
||||
*
|
||||
* The board used to have none of this, and the operator's read of that was not "the
|
||||
* actions are in the editor" — it was *"there are no long hold context menus in the
|
||||
* app I have no way to delete notes."* Trash was three interactions deep (open, ⋮,
|
||||
* Move to trash), and on a phone that is far enough from the gesture people reach
|
||||
* for that it may as well not exist.
|
||||
* The same items as the editor's overflow, in the same words from the same string
|
||||
* resources, dispatched through [EditorAction] so `BoardViewModel.onEditorAction`
|
||||
* stays the one place they are handled.
|
||||
*
|
||||
* **The same items as the editor's overflow, in the same words, from the same string
|
||||
* resources.** A note has one vocabulary of things that can be done to it, and two
|
||||
* surfaces that named them differently would be describing two different apps. It
|
||||
* dispatches [EditorAction] for the same reason — `BoardViewModel.onEditorAction` is
|
||||
* already the exhaustive dispatcher for every one of them, so the board reuses the
|
||||
* seam rather than growing a parallel one that could drift.
|
||||
* Gated on the NOTE (`note.trashed`), not on the view, because Reminders and search
|
||||
* mix piles: a note already in Trash must not offer "Move to trash".
|
||||
*
|
||||
* **Gated on the NOTE, not on the destination.** `note.trashed` is what the editor
|
||||
* gates its own read-only mode on, and it is the only reading that survives the views
|
||||
* that mix piles: Reminders cuts across archived and active alike, and a search hits
|
||||
* whatever matches. A menu that offered "Move to trash" on a note already in the
|
||||
* trash would be offering to do something twice.
|
||||
*
|
||||
* **Colour is absent**, though #2946 suggested it. It was left out because `note.color`
|
||||
* was already scheduled for removal; M315 removed it. There is no colour to set on a
|
||||
* note any more — a card is one neutral surface and the only coloured thing on a board
|
||||
* is a tag — so the row this menu never grew is a row that could not exist.
|
||||
*
|
||||
* Labels are absent too, for a duller reason: the picker they open is editor state,
|
||||
* and hoisting it to the board is a bigger change than the friction actually reported.
|
||||
* No Labels item: the picker it opens is editor state.
|
||||
*/
|
||||
@Composable
|
||||
private fun NoteMenu(
|
||||
@@ -555,31 +528,13 @@ fun noteCardSurface(dark: Boolean): Color = if (dark) CARD_SURFACE_DARK else CAR
|
||||
private val CARD_SURFACE_LIGHT = Color(0xFFFFFFFF)
|
||||
private val CARD_SURFACE_DARK = Color(0xFF171717)
|
||||
|
||||
// THE CARD'S EDGE — one grey, every card, both themes. Since M315 it is the only thing
|
||||
// that differs from the board by more than a hair, which makes it structure rather than
|
||||
// decoration: it is what a card IS.
|
||||
// THE CARD'S EDGE — one neutral grey, every card, both themes. The fill barely differs
|
||||
// from the board, so this line is what makes a card a card. Neutral on purpose: a
|
||||
// coloured edge would carry information and turn a board into a grid of outlines.
|
||||
//
|
||||
// It was already neutral before the fill was. The version that came from the palette
|
||||
// was a `{hue}-900` border and failed twice over: the line measured 1.56-2.09 against
|
||||
// its own fill while the fill managed only 1.03-1.05 against the board, so it was the
|
||||
// loudest thing on the card — and it carried the same information the fill did, so a
|
||||
// field of cards read as a grid of outlines however different the colours inside were.
|
||||
// A neutral line carries no information at all, which is exactly what lets it be
|
||||
// structure instead of content. The fill is that same argument one size up.
|
||||
//
|
||||
// MEASURED AGAINST ONE FILL NOW, and deliberately left where it was. #B8B8B8 on white
|
||||
// is 1.98 and #404040 on #171717 is 1.73 — both inside the ranges these values already
|
||||
// shipped at across twenty fills (light 1.57-1.98, dark 1.58-1.73), but at the top of
|
||||
// them rather than the ~1.6-1.7 the pair was originally matched on. Softening the light
|
||||
// edge to re-match would weaken the only boundary a white card on a #FAFAFA board has,
|
||||
// and the complaint that started M315 was about fill, never about edge weight. If an
|
||||
// operator pass disagrees it is one constant, in two files.
|
||||
//
|
||||
// NOT a translucent black/white edge, which is the tidier way to write this and was
|
||||
// measured and rejected: a border composites over what is under it, so `White` at 20%
|
||||
// came out #56396D on a purple card and #A3C9C1 on a teal one. With one fill that
|
||||
// argument no longer bites — but an opaque grey is what NoteCard.vue must also write,
|
||||
// and two surfaces stating the same hex is how they stay the same card.
|
||||
// Opaque rather than a translucent black/white, which would composite differently over
|
||||
// whatever is under it. NoteCard.vue writes the same two hex values, which is how the
|
||||
// surfaces stay the same card. Contrast: #B8B8B8 on white 1.98, #404040 on #171717 1.73.
|
||||
private val CARD_EDGE_LIGHT = Color(0xFFB8B8B8)
|
||||
private val CARD_EDGE_DARK = Color(0xFF404040)
|
||||
private val CHIP_RADIUS = 6.dp
|
||||
|
||||
@@ -44,22 +44,9 @@ data class NoteTint(
|
||||
val lightChipForeground: Color,
|
||||
val darkChipForeground: Color,
|
||||
/**
|
||||
* THE INK A TAG IS DRAWN IN — inline in the prose AND as a chip's text — see
|
||||
* [tagInk].
|
||||
*
|
||||
* These were two columns until M315, and the split was real while it lasted: a chip
|
||||
* brought its own `-100` fill and could afford `-700`, while inline text sat on
|
||||
* whatever the card was, which included a gray-tagged card at `neutral-200` where
|
||||
* `-700` measured 3.98 (green), 4.11 (orange) and 4.34 (teal), all under the 4.5
|
||||
* body text needs. One step deeper cleared every fill at once.
|
||||
*
|
||||
* The twenty card fills that split was solving for are gone, so both jobs take this
|
||||
* one value. The direction is deliberate: since M311 a tag whose text is in the body
|
||||
* is drawn where it was typed and NOT repeated as a chip, so the inline token is the
|
||||
* common case and collapsing onto ITS column leaves what is seen most exactly as it
|
||||
* was. The chip is strictly better for the move — on its own fill it goes from
|
||||
* 4.52-8.23 to 6.37-12.01 in light. Dark needed no decision: the two columns already
|
||||
* held the same value for all ten hues.
|
||||
* THE INK A TAG IS DRAWN IN — inline in the prose and as a chip's text — see
|
||||
* [tagInk]. One value serves both: `-800` light / `-300` dark clears body-text
|
||||
* contrast on the card and on a chip's own fill.
|
||||
*/
|
||||
val lightTagInk: Color,
|
||||
val darkTagInk: Color,
|
||||
|
||||
@@ -187,14 +187,9 @@ pub fn evaluate(info: &ServerInfo) -> Compatibility {
|
||||
|
||||
/// Who this client says it is, set once by the host application at startup.
|
||||
///
|
||||
/// THE CORE CANNOT KNOW THIS, and the value it used to invent was wrong twice. It
|
||||
/// was `inkwell-desktop/{CARGO_PKG_VERSION}`, and this crate is compiled into
|
||||
/// the desktop app AND the Android app — so every phone in the field announced
|
||||
/// itself as a desktop. The version was worse: `CARGO_PKG_VERSION` here is the
|
||||
/// version of the CORE crate, a number no build stamps and no user has ever seen,
|
||||
/// while the thing a reader of that header wants is the app's own build (note 3127
|
||||
/// §5 — with no version tags, the artifact's self-report is the only answer to
|
||||
/// "which build is this?").
|
||||
/// The core can't know this: it is compiled into both the desktop and the Android
|
||||
/// app, and its own crate version is a number no build stamps. The app's build is
|
||||
/// what a reader of the header wants (note 3127 §5).
|
||||
///
|
||||
/// So the host names itself. `OnceLock` because identity is fixed for the life of
|
||||
/// the process and a second caller should be ignored rather than race the first.
|
||||
|
||||
@@ -110,28 +110,18 @@ export function plusTask(blocks: EditorBlock[]): { blocks: EditorBlock[]; focus:
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-read ONE prose block for `- [ ] ` lines somebody typed by hand.
|
||||
* Re-read ONE prose block for `- [ ] ` lines somebody typed by hand, so a marker
|
||||
* typed in the editor becomes an item without closing and reopening the note.
|
||||
*
|
||||
* `splitBlocks` runs once, when the editor opens. After that the blocks are the state
|
||||
* and nothing reads the body again — every edit travels the other way, through
|
||||
* `joinBlocks`. So a marker typed by hand stayed literal text on screen until the note
|
||||
* was closed and reopened, even though it was already a real item in storage and the
|
||||
* card was already drawing a checkbox for it. The editor was the only place that
|
||||
* disagreed.
|
||||
* On blur, and only the block being left: re-splitting on a keystroke would move the
|
||||
* caret, and converting the moment `- [ ]` is complete would catch an item with no
|
||||
* text yet.
|
||||
*
|
||||
* ON BLUR, and only the block being left. There is no good moment to convert while
|
||||
* someone is typing: re-splitting on a keystroke moves the caret out of the word being
|
||||
* written, and converting the instant `- [ ]` is complete does it before the item has
|
||||
* any text. Blur is the one moment the person has demonstrably finished with the block,
|
||||
* so a re-split costs no caret and cannot catch a half-typed line.
|
||||
* Returns THE SAME ARRAY when there was nothing to promote; the caller relies on that
|
||||
* to leave the ref alone, so a blur that changed nothing doesn't re-key every field.
|
||||
*
|
||||
* Returns THE SAME ARRAY, not an equal copy, when there was nothing to promote — the
|
||||
* caller leans on that to leave the ref alone, and a blur that changed nothing must not
|
||||
* re-key every field below it.
|
||||
*
|
||||
* Non-canonical markers (`- [X]`, an odd bullet) come back canonical, exactly as they
|
||||
* would have on reopen. That is the only case where this changes the body rather than
|
||||
* only the way it is drawn.
|
||||
* Non-canonical markers (`- [X]`, an odd bullet) come back canonical, as they would on
|
||||
* reopen.
|
||||
*/
|
||||
export function promoteTasks(blocks: EditorBlock[], index: number): EditorBlock[] {
|
||||
const block = blocks[index];
|
||||
|
||||
@@ -36,29 +36,12 @@ export const NOTE_SWATCH_CLASSES: Record<NoteColor, string> = {
|
||||
};
|
||||
|
||||
// A chip's SHELL: its fill and its hairline edge, keyed by the colour vocabulary. The
|
||||
// INK is not here — see TAG_TEXT_CLASSES, which one table now serves both a chip's text
|
||||
// and a `#tag` left in the prose. Compose the two with `labelChipClasses`.
|
||||
// ink is TAG_TEXT_CLASSES; `labelChipClasses` composes the two.
|
||||
//
|
||||
// THE RING IS NOT DECORATION. A chip's fill measures 1.02-1.26 against the card in
|
||||
// light and 1.02-1.73 in dark — that is to say, very nearly nothing. The pill's shape
|
||||
// is the edge; the fill only tints it. (Dark red is the extreme at 1.02, which is
|
||||
// invisible: without the ring that chip would be loose text.) An edge holds the shape
|
||||
// against any background, where shifting the fill only moves which card it collides
|
||||
// with.
|
||||
//
|
||||
// AT 65%, NOT 60%, AND SOLVED FOR RATHER THAN GUESSED. 0.60 was chosen against a worst
|
||||
// case that no longer exists — a chip sitting on a card of its own colour, back when a
|
||||
// note took its first tag's fill. Against the one card surface (M315) the ring is the
|
||||
// ink at alpha over a known fill, so the alpha that clears the 3:1 of WCAG 1.4.11 for
|
||||
// all ten hues can simply be solved: 0.60 gives 2.75-3.82 in light and misses for six
|
||||
// of them, 0.65 gives 3.03-4.36 and misses for none. Dark is 4.52-5.76. 0.80 was the
|
||||
// number the old comment named as the fallback and it is more than is needed — it
|
||||
// draws a hard outline where a hairline does the job.
|
||||
//
|
||||
// `default`'s ring was `black/10 dark:white/15` here and the ink at alpha on the phone —
|
||||
// 1.36 against its own fill in light against Compose's 3.21, so the two surfaces were
|
||||
// drawing visibly different pills for the same chip. It is the ink at 65% on both now,
|
||||
// like every other key: one rule for ten hues, not nine and an exception.
|
||||
// The ring is what gives the pill its shape: a chip's fill measures only 1.02-1.73
|
||||
// against the card. It is the ink at 65%, the lowest alpha that clears WCAG 1.4.11's
|
||||
// 3:1 for all ten hues on the one card surface (light 3.03-4.36, dark 4.52-5.76).
|
||||
// `default` follows the same rule as every other key.
|
||||
//
|
||||
// Mirrored in NoteTint.kt as `chipBackground` and `chipBorder` / CHIP_EDGE_ALPHA.
|
||||
export const LABEL_CHIP_SHELL: Record<NoteColor, string> = {
|
||||
@@ -74,26 +57,13 @@ export const LABEL_CHIP_SHELL: Record<NoteColor, string> = {
|
||||
gray: "bg-neutral-200 dark:bg-neutral-700 ring-1 ring-inset ring-neutral-800/65 dark:ring-neutral-200/65",
|
||||
};
|
||||
|
||||
// THE INK A TAG IS DRAWN IN — one table, for a `#tag` left in the prose AND for a
|
||||
// chip's text. It was two, and the split was real while it lasted: a chip carried its
|
||||
// own `-100` fill and could afford `-700`, while inline text sat on whatever the card
|
||||
// was, which included a gray-tagged card at `neutral-200` where `-700` measured 3.98
|
||||
// (green), 4.11 (orange) and 4.34 (teal) — all under the 4.5 body text needs. One step
|
||||
// deeper cleared every fill at once, so inline got `-800` and the chip kept `-700`.
|
||||
//
|
||||
// M315 removed the twenty card fills the split was solving for, and this is the payoff:
|
||||
// against ONE card surface both jobs can take the same value. `-800`/`-300` is the one
|
||||
// they take, and the direction is deliberate — the inline token is the common case
|
||||
// (since M311 a tag whose text is in the body is drawn where it was typed and NOT
|
||||
// repeated as a chip), so collapsing onto the inline column leaves what is seen most
|
||||
// exactly as it was, and moves only the chip. The chip is strictly better for it:
|
||||
// THE INK A TAG IS DRAWN IN — one table, for a `#tag` left in the prose and for a
|
||||
// chip's text. `-800` in light and `-300` in dark clear body-text contrast in both
|
||||
// places:
|
||||
//
|
||||
// inline, on the card as a chip, on its own fill
|
||||
// light `-800` 7.09 - 15.13 6.37 - 12.01 (was 4.52 - 8.23)
|
||||
// dark `-300` 9.45 - 14.23 8.23 - 11.88 (unchanged)
|
||||
//
|
||||
// Dark needed no decision at all: the two tables were already the same value for all
|
||||
// ten hues, which is on its own most of the argument that one table was always enough.
|
||||
// light `-800` 7.09 - 15.13 6.37 - 12.01
|
||||
// dark `-300` 9.45 - 14.23 8.23 - 11.88
|
||||
//
|
||||
// Mirrored in NoteTint.kt as `lightTagInk` / `darkTagInk`.
|
||||
export const TAG_TEXT_CLASSES: Record<NoteColor, string> = {
|
||||
@@ -249,33 +219,9 @@ export function resolveLabelColor(label: { name: string; color?: string | null }
|
||||
// chips of one colour are the same tag. The chip's text is what says which tag it is.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// THE CARD SURFACE — one neutral, no hue, no note involved (M315).
|
||||
//
|
||||
// This used to be a function of the note: a palette class for a tagged one, a fill
|
||||
// generated from the id for the rest. Both are gone. The operator's verdict after
|
||||
// four passes at it — "my coloring attempt has failed and nothing looks right… we've
|
||||
// tried a lot to make the color work and somehow it never seems to land" — and the
|
||||
// diagnosis underneath it is that a card's fill was being asked to carry meaning it
|
||||
// could not carry. Nine keys is too few to identify anything on a board of any size,
|
||||
// and a generated fill identifies nothing by construction, so a coloured board taught
|
||||
// the eye to read hue as significant and then handed it noise.
|
||||
//
|
||||
// COLOUR NOW LIVES ONLY ON THE TAG — the chip and the inline `#tag`, both of which sit
|
||||
// on this one known background from here on. That is a smaller job done properly
|
||||
// instead of a larger one done four times.
|
||||
//
|
||||
// The codebase had already made this argument about the card's EDGE, one level down:
|
||||
// the hue-coded border came out as "a neutral line carries no information at all,
|
||||
// which is exactly what lets it be structure instead of content". The fill is the same
|
||||
// argument at the next size up.
|
||||
//
|
||||
// THE VALUES. `bg-white dark:bg-neutral-900` — which is exactly what `default` always
|
||||
// was, and exactly what the note EDITOR panel has always been (NoteEditor.vue), so
|
||||
// this is a collapse onto a surface both other surfaces already used rather than a
|
||||
// new colour anybody has to like.
|
||||
//
|
||||
// Measured against the operator's constraint, "not the same color as their background
|
||||
// but close to it":
|
||||
// THE CARD SURFACE — one neutral, the same for every note. Colour lives only on tags
|
||||
// (the chip and the inline `#tag`); a card's fill can't carry meaning, because nine
|
||||
// keys identify nothing on a board of any size. It is the editor panel's surface too.
|
||||
//
|
||||
// card vs board light #ffffff on #fafafa 1.04
|
||||
// dark #171717 on #0a0a0a 1.10
|
||||
@@ -283,11 +229,7 @@ export function resolveLabelColor(label: { name: string; color?: string | null }
|
||||
// dark #404040 on #171717 1.73
|
||||
// body vs card light #171717 on #ffffff 17.93 (needs 4.5)
|
||||
// dark #fafafa on #171717 17.17
|
||||
// muted vs card light #404040 on #ffffff 10.37
|
||||
// dark #e5e5e5 on #171717 14.23
|
||||
//
|
||||
// The fill is deliberately the WEAKEST of those numbers. A card is not separated from
|
||||
// the board by its fill and never was — the edge and the shadow do that, which is why
|
||||
// 1.04 is enough and why it has to stay near 1: a fill that separated on its own would
|
||||
// be a panel, and a board of panels is the wall this whole line of work started from.
|
||||
// The fill is deliberately the weakest number: the edge and the shadow separate a card
|
||||
// from the board, and a fill strong enough to do it alone would make every card a panel.
|
||||
export const NOTE_CARD_SURFACE = "bg-white dark:bg-neutral-900";
|
||||
|
||||
+7
-33
@@ -51,45 +51,19 @@ DEFAULT_LIMIT = 500
|
||||
MAX_LIMIT = 1000
|
||||
MAX_PUSH = 1000 # per-batch change cap
|
||||
|
||||
# --- protocol versioning (M10.6) --------------------------------------------
|
||||
# --- protocol versioning ------------------------------------------------------
|
||||
#
|
||||
# The client<->server compatibility contract. These integers version the WIRE
|
||||
# PROTOCOL, deliberately separate from the app's release version, so a client and
|
||||
# server on different releases can still work out whether they can talk. Without
|
||||
# that separation every protocol change would force app<->server lockstep.
|
||||
# PROTOCOL, separately from the app's release version, so a client and server on
|
||||
# different releases can still work out whether they can talk.
|
||||
#
|
||||
# SYNC_PROTOCOL_VERSION what this server speaks.
|
||||
# MIN_CLIENT_PROTOCOL_VERSION the oldest client protocol it still accepts.
|
||||
#
|
||||
# Bump SYNC_PROTOCOL_VERSION for ANY wire change. Raise
|
||||
# MIN_CLIENT_PROTOCOL_VERSION only for a genuinely BREAKING one: it is the switch
|
||||
# that hard-blocks older clients, so additive changes must leave it alone.
|
||||
# v2 (M13): `kind` and `title` both left the wire. Dropping a field a v1 client sends
|
||||
# and expects back is breaking, so the FLOOR moves too — a v1 client would keep pushing
|
||||
# both and would read back notes carrying neither.
|
||||
#
|
||||
# One bump for the pair: they landed in the same protocol generation, and nothing ever
|
||||
# ran against a half-applied v2.
|
||||
# v4 (M315): `color` left the note. The FLOOR DELIBERATELY DOES NOT MOVE, and v2 is
|
||||
# the precedent that makes saying so worthwhile — it dropped `kind` and `title` and did
|
||||
# raise the floor, on the rule that dropping a field a client sends and expects back is
|
||||
# breaking. `color` fails the second half of that: a v3 client reading a v4 note gets
|
||||
# `"default"` from its own serde default and draws the colour it derives locally, which
|
||||
# is the board it drew yesterday; a v3 client PUSHING `color` has the key ignored, since
|
||||
# `_assign_note_fields` reads its payload key by key and never validates the shape.
|
||||
# Neither direction errors and neither loses anything visible. `title` was the note's
|
||||
# NAME; this is a field that no longer renders anywhere.
|
||||
# v5 (#5168): attachments became a sync entity — `PUT /attachments/<id>` uploads a file
|
||||
# attached offline, and push takes `attachment` and `preview` deletes. Additive, so the
|
||||
# floor stays: a v4 client never sends either, and gets the same notes it always did.
|
||||
# v6 (#5175): shared notes. A client asking `changes?shares=1` gets the notes shared
|
||||
# with it, each saying how it is held (`permission`, `shared`, `shared_by`), and a
|
||||
# `revoked` list of notes that have left it; push takes body edits to notes shared at
|
||||
# `edit`. Additive and opt-in, so the floor stays: a v5 client never asks, and gets
|
||||
# its own notes exactly as before.
|
||||
# v7 (#5176): a recipient's own pin, archive and place. On a shared note the feed
|
||||
# carries the caller's own, and push takes them, stamped `state_at`, for a note shared
|
||||
# at any level. Additive, so the floor stays: a v6 client pushes only text, as before.
|
||||
# Bump SYNC_PROTOCOL_VERSION for ANY wire change. Raise the floor only for a
|
||||
# BREAKING one: it hard-blocks older clients, so additive changes leave it alone.
|
||||
# What each version changed, and why the floor moved or didn't, is in docs/sync.md
|
||||
# ("The policy").
|
||||
SYNC_PROTOCOL_VERSION = 7
|
||||
MIN_CLIENT_PROTOCOL_VERSION = 3
|
||||
|
||||
|
||||
Reference in New Issue
Block a user