Skip to main content

max / makeover-layout

0.15.0: a form can ask for a day, and for a moment FieldKind gains Date and DateTime, on the argument Email was admitted on: a webview emits a different type=, which is a native picker, the platform's own validation, and on a touch device the date keyboard. Described as Text with a hint reading "YYYY-MM-DD", all three are lost and the hint is doing the platform's job in prose. Two members and not one or five, from a count rather than from symmetry: 13 sites of date and 13 of datetime-local across the MNW server and goingson, and zero of time, month or week. A member added for a case nobody has is a member designed against nothing, which is File's reasoning about accept applied to a whole member. Apart from each other because the question differs rather than the precision. "Which day does this expire" and "at what moment does this publish" are asked by different screens and answered by different controls, and a host that collapsed them would ask half the tree for a precision it does not want. DATE_FORMAT and DATETIME_FORMAT are constants rather than a sentence in a doc comment. The reason to name a wire format at all is that a host picking its own would disagree with a server silently and per-host; a constant is what a host that cannot emit the native control has to meet. The datetime is local and carries no zone, which is a property of the value rather than an omission: the moment is not fixed until something outside the description supplies one. FieldKind::temporal names the pair once, for offers_options' reason. Also corrects Radio's doc comment, which claimed radio was the one HTML input type this enum was missing. That was already false when 0.8.1 wrote it (file arrived at 0.11.0) and is more false now. The enum grows by a site being measured, not by a list being completed.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-11 22:52 UTC
Signed with PGP, not checked
Commit: 2deb098572547c7f5611fb4c853962933db64e2d
Parent: 47bb019
2 files changed, +166 insertions, -4 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.14.0"
3 + version = "0.15.0"
4 4 edition = "2024"
5 5 description = "The renderer-agnostic half of the make-family design system: what a thing IS, named as intents and relationships and never as values. Colour defers to makeover, spacing to makeover-geometry; what is left is composition."
6 6 license = "MIT"
M src/lib.rs +165 -3
@@ -178,6 +178,22 @@
178 178 //! `Cell` was measured to hold, and the count is in that crate's history
179 179 //! rather than assumed here.
180 180 //!
181 + //! 0.15.0 adds [`FieldKind::Date`] and [`FieldKind::DateTime`], on the argument
182 + //! [`FieldKind::Email`] was admitted on: a webview emits a different `type=`,
183 + //! which is a native picker, the platform's validation and a different keyboard
184 + //! on a touch device. Described as text with a "YYYY-MM-DD" hint, all three are
185 + //! lost.
186 + //!
187 + //! Two members and not one or five, from a count rather than from symmetry: 13
188 + //! sites of `date` and 13 of `datetime-local` across the MNW server and
189 + //! goingson, and zero of `time`, `month` or `week`. The wire format each takes
190 + //! is named here as [`DATE_FORMAT`] and [`DATETIME_FORMAT`], because a host
191 + //! left to pick its own would disagree with a server silently, and
192 + //! [`FieldKind::temporal`] is the pair asked about once rather than at each
193 + //! renderer. `FieldKind`'s own comment claiming `radio` was the last HTML input
194 + //! type missing was already false when 0.8.1 wrote it; these are what it was
195 + //! missing.
196 + //!
181 197 //! What each of the 0.12.0 findings deliberately leaves out is the address — what pressing a
182 198 //! header calls, and where an empty state's "Add your first project" button
183 199 //! goes. That is the boundary this crate is defined by, and four findings moved
@@ -1284,6 +1300,63 @@
1284 1300 ///
1285 1301 /// Added 0.5.0, from goingson's contact-phone form.
1286 1302 Tel,
1303 + /// A calendar day, with no time of day in it.
1304 + ///
1305 + /// [`Email`](Self::Email)'s argument, and it carries further: a webview
1306 + /// emits `type="date"`, which is a native picker, the platform's own
1307 + /// validation, and on a touch device the date keyboard. Described as
1308 + /// [`Text`](Self::Text) with a hint reading "YYYY-MM-DD", all three are
1309 + /// lost and the hint is doing the platform's job in prose.
1310 + ///
1311 + /// The membership test passes on every host without stretching: a webview
1312 + /// and a Tauri app emit the input, egui has a date picker, a terminal
1313 + /// prompts for a day and can validate it, a CLI takes an argument.
1314 + ///
1315 + /// # The value is ISO 8601, `YYYY-MM-DD`
1316 + ///
1317 + /// Named here rather than left to each host, because a host that picks
1318 + /// differently sends a server something it parses differently, and the
1319 + /// failure is silent and per-host. It is `<input type="date">`'s own wire
1320 + /// format, so the webview renderer owes nothing to honour it and the other
1321 + /// hosts have one spelling to meet. [`DATE_FORMAT`] is the constant, and a
1322 + /// test asserts this doc and that constant agree.
1323 + ///
1324 + /// Added 0.15.0, from the MNW server's git access-token expiry
1325 + /// (`user_ssh_keys_tab.html`) and six further sites across the server and
1326 + /// goingson.
1327 + Date,
1328 + /// A calendar day and a time of day together.
1329 + ///
1330 + /// Apart from [`Date`](Self::Date) because the question is different rather
1331 + /// than more precise: "which day does this expire" and "at what moment does
1332 + /// this publish" are asked by different screens and answered by different
1333 + /// controls. A webview emits `type="datetime-local"` for one and
1334 + /// `type="date"` for the other, and a host that collapsed them would ask
1335 + /// half the tree for a precision it does not want.
1336 + ///
1337 + /// Both arrived together on measurement rather than on symmetry: 13 sites
1338 + /// of each across the MNW server and goingson, and **zero** of `time`,
1339 + /// `month` or `week`, which is why those are not here. A member added for a
1340 + /// case nobody has is a member designed against nothing, which is
1341 + /// [`File`](Self::File)'s reasoning about `accept` applied to a whole
1342 + /// member.
1343 + ///
1344 + /// # The value is `YYYY-MM-DDTHH:MM`, local, with no zone
1345 + ///
1346 + /// `<input type="datetime-local">`'s own format, and the "local" is the
1347 + /// load-bearing half: the value carries no offset and no `Z`, so the moment
1348 + /// it names is only fixed once something supplies a zone. That is the app's
1349 + /// business and not the description's. Seconds are absent, which is the
1350 + /// browser's own default and is left as the rule rather than restated as a
1351 + /// constraint. [`DATETIME_FORMAT`] is the constant.
1352 + ///
1353 + /// [`Field::min`] and [`Field::max`] already take "the host's own spelling
1354 + /// of a bound", so a floor of *not in the past* needs nothing new here: it
1355 + /// is a string in this same format.
1356 + ///
1357 + /// Added 0.15.0, from goingson's snooze picker and day planner and the MNW
1358 + /// server's publish-at fields.
1359 + DateTime,
1287 1360 /// Several lines of text.
1288 1361 Textarea,
1289 1362 /// One of a fixed set, offered behind a control that shows one at a time.
@@ -1300,9 +1373,12 @@
1300 1373 /// afterwards — and had already promoted that out of a checkbox by hand,
1301 1374 /// with a comment giving this reason, before the description could say it.
1302 1375 ///
1303 - /// It is also the one HTML input type this enum was missing. Everything
1304 - /// else here is an `<input type=...>`, a `<select>` or a `<textarea>`, and
1305 - /// the hole was `radio`.
1376 + /// It was described here at 0.8.1 as "the one HTML input type this enum was
1377 + /// missing", which was not true then and is not true now: `file` arrived at
1378 + /// 0.11.0 and `date` and `datetime-local` at 0.15.0. Everything here is
1379 + /// still an `<input type=...>`, a `<select>` or a `<textarea>`, and the way
1380 + /// this enum grows is by a site being measured rather than by a list being
1381 + /// completed, so "the last one" is not a claim it should make again.
1306 1382 ///
1307 1383 /// Added 0.8.1, from audiofiles' Add Library form.
1308 1384 Radio,
@@ -1332,7 +1408,36 @@
1332 1408 Hidden,
1333 1409 }
1334 1410
1411 + /// The wire format a [`FieldKind::Date`] value takes: ISO 8601, `YYYY-MM-DD`.
1412 + ///
1413 + /// A constant rather than a sentence in a doc comment, because the reason to
1414 + /// name the format at all is that a host picking its own would fail silently
1415 + /// against a server parsing another. A host that cannot emit the native control
1416 + /// still has one spelling to meet, and can say which one it meant.
1417 + pub const DATE_FORMAT: &str = "%Y-%m-%d";
1418 +
1419 + /// The wire format a [`FieldKind::DateTime`] value takes: `YYYY-MM-DDTHH:MM`,
1420 + /// local, carrying no zone and no seconds.
1421 + ///
1422 + /// [`DATE_FORMAT`]'s sibling and there for its reason. The absent zone is a
1423 + /// property of the value rather than an omission: the moment is not fixed until
1424 + /// something outside the description supplies one.
1425 + pub const DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M";
1426 +
1335 1427 impl FieldKind {
1428 + /// Whether the value the kind takes is a moment rather than a string.
1429 + ///
1430 + /// Named once here for the reason [`offers_options`](Self::offers_options)
1431 + /// is: two kinds answer yes, and a host that has to parse or format a value
1432 + /// needs to ask without spelling the pair out at each renderer. A third
1433 + /// temporal kind should land here and nowhere else.
1434 + ///
1435 + /// The format each one takes is [`DATE_FORMAT`] and [`DATETIME_FORMAT`].
1436 + #[must_use]
1437 + pub const fn temporal(self) -> bool {
1438 + matches!(self, Self::Date | Self::DateTime)
1439 + }
1440 +
1336 1441 /// Whether the field is drawn at all.
1337 1442 #[must_use]
1338 1443 pub const fn visible(self) -> bool {
@@ -2152,6 +2257,63 @@
2152 2257 assert_eq!(RowPart::Tokens.intent(), "content");
2153 2258 }
2154 2259
2260 + #[test]
2261 + fn the_two_temporal_kinds_are_the_two_that_name_a_moment() {
2262 + // The pair is named once so a host with parsing to do asks here rather
2263 + // than spelling it out, which is `offers_options`' reason.
2264 + assert!(FieldKind::Date.temporal());
2265 + assert!(FieldKind::DateTime.temporal());
2266 +
2267 + for kind in [
2268 + FieldKind::Text,
2269 + FieldKind::Secret,
2270 + FieldKind::Number,
2271 + FieldKind::Email,
2272 + FieldKind::Url,
2273 + FieldKind::Tel,
2274 + FieldKind::Textarea,
2275 + FieldKind::Select,
2276 + FieldKind::Radio,
2277 + FieldKind::Checkbox,
2278 + FieldKind::File,
2279 + FieldKind::Hidden,
2280 + ] {
2281 + assert!(!kind.temporal(), "{kind:?}");
2282 + }
2283 + }
2284 +
2285 + #[test]
2286 + fn a_date_carries_no_time_and_a_datetime_carries_no_zone() {
2287 + // The formats are the whole reason the members are worth naming apart
2288 + // from text, so the doc comments and the constants have to agree. A
2289 + // host reading one and meeting the other is the silent failure.
2290 + assert_eq!(DATE_FORMAT, "%Y-%m-%d");
2291 + assert!(!DATE_FORMAT.contains("%H"), "a day carries no hour");
2292 +
2293 + assert_eq!(DATETIME_FORMAT, "%Y-%m-%dT%H:%M");
2294 + assert!(
2295 + DATETIME_FORMAT.starts_with(DATE_FORMAT),
2296 + "a moment starts with the day it is on"
2297 + );
2298 + // Local, and that is a property of the value rather than an omission.
2299 + assert!(!DATETIME_FORMAT.contains("%Z"), "no zone name");
2300 + assert!(!DATETIME_FORMAT.ends_with('Z'), "not UTC-stamped");
2301 + assert!(!DATETIME_FORMAT.contains("%S"), "no seconds by default");
2302 + }
2303 +
2304 + #[test]
2305 + fn a_temporal_kind_takes_a_label_above_it_and_offers_no_options() {
2306 + // Neither is a checkbox and neither is a fixed set, so both fall where
2307 + // text does. Asserted because a new kind lands in three predicates and
2308 + // only one of them is the interesting one.
2309 + for kind in [FieldKind::Date, FieldKind::DateTime] {
2310 + assert!(kind.visible(), "{kind:?}");
2311 + assert!(!kind.confidential(), "{kind:?}");
2312 + assert!(!kind.labels_itself(), "{kind:?}");
2313 + assert!(!kind.offers_options(), "{kind:?}");
2314 + }
2315 + }
2316 +
2155 2317 #[test]
2156 2318 fn a_cell_part_names_an_intent_and_only_the_value_is_text() {
2157 2319 // The table half of what RowPart::intent does for rows. A cell holding