Smoke-sweep fixes: exact saves, local dates, safer remove, readable markers
- Garden and plant dialogs keep centimeters as the source of truth (LengthField in lib/units.ts): a no-change Save no longer rewrites 900 cm as 899.922 or a 45 cm spacing as 44.958, bumping versions and writing bogus history entries on the way. - The UI stamps every date with the browser's local day (lib/dates.ts). Journal notes already did; plop placement, fill and removal now do too, so a 9 pm placement isn't "planted tomorrow". The fill endpoint gained an optional plantedAt; API and agent callers still default to UTC today. - Removing an object that holds plants asks first and says how many go with it. An empty one still goes straight away (one Undo restores it). - The expanded plant card's action row wraps instead of clipping "Delete". - Monogram lettering switches to a dark ink on pale marker colors (garlic, cabbage, marigold) instead of near-white on near-white. - Copy-as-plan proposes the next free year and warns when the typed name already exists, so two gardens can't both read as "the 2027 plan". - Plan cards show the base name with a "2027 plan" tag, so the year — the point of the name — survives truncation. - A rejected model spec now says which model and why: a wrapped ErrInvalidInput's reason reaches the client as the 400's message, and the Settings field shows it inline instead of toasting "invalid input". Also defuses a clock bomb in TestRemainingReturnsWhenAPlantingIsRemoved, which only passed while the real date was before 2026-08-01. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -227,3 +227,45 @@ export function formatLength(cm: number, unit: UnitPref): string {
|
||||
export function formatSize(widthCm: number, heightCm: number, unit: UnitPref): string {
|
||||
return `${formatLength(widthCm, unit)} × ${formatLength(heightCm, unit)}`
|
||||
}
|
||||
|
||||
// --- Typed-length fields ----------------------------------------------------
|
||||
// A dialog field that takes a length holds TWO things: the text the person sees
|
||||
// and the centimeters it means. The centimeters change only when the person
|
||||
// types; re-showing the field in another unit, or saving without touching it,
|
||||
// reuses them as they are. Parsing the displayed text back on save is how a
|
||||
// no-change Save once turned 900 cm into 899.922 — "29′ 6.3″" is the nearest
|
||||
// tenth of an inch, not the number that was loaded.
|
||||
|
||||
/** A length as typed and as stored; `cm` is null while the text doesn't parse. */
|
||||
export interface LengthField {
|
||||
text: string
|
||||
cm: number | null
|
||||
}
|
||||
|
||||
/** A dimension field (meters, or feet and inches) showing `cm`. */
|
||||
export function dimensionField(cm: number, unit: UnitPref): LengthField {
|
||||
return { text: formatDimensionInput(cm, unit), cm }
|
||||
}
|
||||
|
||||
/** The person typed `text` into a dimension field. */
|
||||
export function editDimensionField(text: string, unit: UnitPref): LengthField {
|
||||
return { text, cm: parseDimension(text, unit) }
|
||||
}
|
||||
|
||||
/** Re-show a dimension field in another unit; the centimeters don't move. A
|
||||
* field that doesn't parse keeps its text, so the typo stays visible. */
|
||||
export function convertDimensionField(field: LengthField, unit: UnitPref): LengthField {
|
||||
return field.cm === null ? field : dimensionField(field.cm, unit)
|
||||
}
|
||||
|
||||
/** A spacing field (cm, or inches) showing `cm`. */
|
||||
export function spacingField(cm: number, unit: UnitPref): LengthField {
|
||||
return { text: String(spacingFromCm(cm, unit)), cm }
|
||||
}
|
||||
|
||||
/** The person typed `text` into a spacing field. */
|
||||
export function editSpacingField(text: string, unit: UnitPref): LengthField {
|
||||
const trimmed = text.trim()
|
||||
const n = Number(trimmed)
|
||||
return { text, cm: trimmed !== '' && Number.isFinite(n) ? cmFromSpacing(n, unit) : null }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user