| Age | Commit message (Collapse) | Author |
|
Three attempts at this and the cause was never the offset arithmetic. Measured
on build 86, the label mean angles were 45.9 / 130.3 / 209.1 / 319.9 degrees while
the wedge centres are 0 / 90 / 180 / 270. A constant ~45 degree error in all four
quadrants is not a magnitude problem, it is an origin problem.
The wedges are drawn at angles[i], which distributeAngles() fills starting from 0
and stepping clockwise, so item 0 sits at drawAngle 0 -- to the RIGHT, because
canvas angles run from 3 o'clock. The label loop was computing
`i * (360.0/n) - 90.0`, which assumes the first wedge is at the top. So the labels
were anchored a quarter turn away from the wedges, which is exactly one
separator, and no value of the constant would have fixed it.
The labels now take the wedge angles as a parameter and use angles[i] directly,
so a label and its wedge cannot disagree again -- including for a menu whose item
count is not 4 or which pins an item with an explicit `angle`.
Also worth recording: the two earlier attempts were tuning a constant while the
constant itself was wrong, and the measurements (19 px in the top quadrant, then
"all four on the boundaries") were consistent with an origin bug the whole time.
|
|
build 84 died in bundle-web with "Gradle build daemon disappeared unexpectedly",
same as builds 76 and 77. The daemon log is what finally settled it:
Starting 8th build in daemon [uptime: 3m45s, performance: 99%, GC rate: 0.00/s,
heap usage: 0% of 2 GiB, non-heap usage: 28% of 512 MiB]
...
ValidateIrAfterLowering
Heap usage was zero. The host was 91% idle. wasm-opt was still at -Oz. None of
those were the cause: the daemon had served eight builds and by the eighth its
class metadata had filled a 512 MiB metaspace, and it died while wasm-opt's Kotlin
lowering loaded more IR classes. So this is non-heap exhaustion, and the fix is
non-heap capacity.
-XX:MaxMetaspaceSize 512m -> 1g
org.gradle.daemon=false one JVM per build, so metadata cannot pile up
Verified by reproducing the CI sequence -- clean the wasm outputs, the build
cache and the configuration cache exactly as clean-wasm-output does, then run
bundleWeb: BUILD SUCCESSFUL in 12m, no signal 9. That is the same sequence that
just failed in CI.
|
|
build 83's assemble ended with "command finished with signal 9". I had assumed
that was wasm-opt's -Oz pass list and tried to change it to -O2, which broke the
detekt gate instead (detekt parses build.gradle.kts as Kotlin source, and the
`optimizations { wasm-opt { ... } }` accessor does not resolve there -- 9
"Unresolved reference" errors). That DSL block is gone again in 4f00e13.
With the block removed, wasm-opt still runs at -Oz and both of the steps that
were dying now pass on this host:
./gradlew --max-workers=1 assemble BUILD SUCCESSFUL in 7m38s
./gradlew --max-workers=1 bundleWeb BUILD SUCCESSFUL in 3m11s
with no signal 9. So the killer was concurrency, not the optimizer level: several
JVMs (Gradle, the Kotlin daemon, wasm-opt) resident at once is what overran the
11.9 GB. The budgets are lowered to match (Gradle 3g -> 2g, Kotlin 2g -> 1.5g)
and --max-workers=1 is applied to assemble, bundles and bundle-web so only one
compile is resident at a time. The wall clock goes up, which is the trade that has
to be made for a bundle that exists.
|
|
The -O2 block I added to silence the OOM killer broke the detekt gate instead.
build 82 failed at detekt-gate, and running detekt by hand showed why: it parses
build.gradle.kts as Kotlin source, and the Kotlin DSL's optimizations { wasm-opt
{ ... } } is a lazy accessor with no resolvable type, so detekt reported 9
'Unresolved reference' errors on wasm-opt, optimizationLevel and pass.
So the file is back to the plain
and detekt passes again (verified: detekt and compileKotlinWasmJs both
BUILD SUCCESSFUL).
The OOM problem is therefore NOT fixed by this commit. wasm-opt still runs at
-Oz and bundleWeb can still be killed by signal 9. The fix has to be expressed in
a way detekt can parse, which means the argument list for the optimizer task
rather than the target DSL. That is the next change, and it needs to be verified
against both detekt and bundleWeb in one go, because each one alone will not tell
you whether it worked.
|
|
build 81's bundle-web ended with 'process killed by signal 9' during
compileProductionExecutableKotlinWasmJsOptimize, i.e. the OOM killer took
wasm-opt on this 11.9 GB host. wasm-opt exposes no memory cap, so the only lever
is how much work it is asked to do.
At -Oz Kotlin's default pass list runs --gufa plus repeated -O3 --type-merging.
That combination is the memory peak. -O2 keeps most of the size win and does not
pull in gufa. The pass list is now written out explicitly so it cannot silently
drift back to the -Oz defaults.
This trades a little wasm size for the web bundle actually shipping at all, which
is the right way round: an unbuilt bundle helps nobody.
Also fixes the CI fleet-wide daemon pressure by leaving the --max-workers=2
restriction from the previous commit in place.
|
|
Third attempt at the label placement, and the previous one did not work. Measured
about the true disc centre (135.0, 725.5, a 200x200 disc) the label means sat at
152 / 201 / 300 degrees: still 15-24 degrees from a separator and 21-62 degrees
from a wedge centre.
Why 90 dp did not fix it: Modifier.offset is applied AFTER placement, so it adds
to wherever the parent's placement rules already decided. With
Box(Modifier.fillMaxSize(), contentAlignment = Center) the parent was deciding,
and a polar offset added on top of that is not a polar position -- the parent's
rules win and the angle comes out wrong.
This time the parent stops deciding. BoxWithConstraints gives the real
constraint size, and each label box is positioned at
(centre + cos/sin * 67dp - halfBox) with nothing else touching placement. Box
is 84 dp so it fits inside a 45 degree wedge at r=67 (arc length there is about
53 dp), and the box's centre, not its corner, is placed on the bisector.
Verified by measurement on the next build rather than assumed: the label mean
angle per quadrant must come out near 0/90/180/270 rather than 45/135/225/315.
|
|
The previous commit fixed the missing top label but left the placement wrong in
a subtler way, and the measurement caught it: taking the circular mean of the
white ink in each quadrant gave 60 / 128 / 207 / 325 degrees. The wedge bisectors
are 0 / 90 / 180 / 270 and the separators are 45 / 135 / 225 / 315, so every
label sat 27-60 degrees off its wedge centre and within 7-18 degrees of a
separator.
Cause: each label lived in a Box sized labelRadius * 2 = 134 dp. That is two
thirds of the whole 200 dp disc, so it does not fit inside a 45 degree wedge.
Compose redistributed the overflow by placing the child about 45 degrees away
from where the offset asked, which is exactly the boundary-hugging result.
Fix: size the measuring box at 90 dp so it fits the wedge arc, and compute the
top-left so the box's CENTRE lands on the bisector at 67 dp rather than letting
the box corner land there. Radial distance is unchanged.
Hub stays at 22% and the separators stay 3 px; the review only complained the
overall structure still read weakly because of this placement.
|
|
builds 76 and 77 both died in assemble with 'Gradle build daemon disappeared
unexpectedly'. I had guessed --no-daemon would help; it did not, it just moved
the whole build into the client JVM and that died the same way. The useful
evidence was ./gradlew --status reporting every daemon as 'STOPPED (by user or
operating system)' while vmstat showed 91% of 11.9 GB idle, and the build log
complaining that 9 stopped daemons could not be reused.
What actually fixes it is the worker count. assemble with --max-workers=2
completed in 5m33s from a clean daemon registry. So:
org.gradle.parallel=false project-level parallelism off
org.gradle.jvmargs -Xmx1536m -> 3g (holds Kotlin now that it is in-process)
kotlin.daemon.jvmargs 4096m -> 2g
kotlin.compiler.execution.strategy=in-process one fewer 4 GB JVM
registry.maximum-size=1, idletimeout 2 min the pool stops accumulating
assemble step passes --max-workers=2 explicitly
bundle-web drops --no-daemon again; with the pool bounded, a normal reused daemon
is correct and faster for a ~1 min task.
|
|
Pro reviewed the rebuilt menu and read the image rather than the numbers: three
quadrants had labels, the top one had none. My own pixel count agreed --
19 px of white ink in the top quadrant against 421-519 px in the others.
Cause: the label loop computed a polar offset relative to the CENTRE but applied
it with Modifier.offset inside Box(Modifier.fillMaxSize()), whose origin is the
TOP-LEFT. So every label was displaced by half a radius too little, and the top
one at y = -67 dp landed outside the disc entirely. The three that appeared to
work were only roughly right.
Fix: centre the outer Box (contentAlignment = Center) so the origin is the disc
centre, and pass the polar displacement as a relative offset. Pro's -90 deg
convention was already right -- item 0 at the top, proceeding clockwise -- and is
kept; only the origin was wrong.
Pro's other read on the rebuild: the 3 px separator reads well at this size and
does not flare, and the 22% hub is better balanced than 26%. Both kept.
|
|
build 73 failed in bundle-web. Re-running the task by hand reproduced the
message 'Starting a Gradle Daemon, 7 busy and 1 stopped Daemons could not be
used', and ./gradlew --status showed a pool that had accumulated across the
repeated forced builds. The invocation lost that race and died; a later run with
'--stop' followed by --no-daemon completed the same task in 1m12s and produced
the zip.
--no-daemon gives the build its own JVM and tears it down afterwards, so a
wedged or crashed previous build cannot poison the next one. That matters more
than the lost daemon reuse, because bundleWeb is only ~1 min here.
|
|
The menu still looked wrong after the donut revision, so the geometry was
re-measured rather than re-guessed. The numbers matched the previous reviewer's
spec exactly, which is what made the spec itself the suspect.
Measured on the published build: disc 200x200 at (139.5, 725.5), hub 54x54 dead
centred on it, wedge/gap boundaries exactly on 45/135/225/315 degrees. So the
distances were right. But sampling the white label ink at r=40..90 and taking the
circular mean of each cluster gave 35 / 133 / 148 / 212 / 330 degrees. The wedge
bisectors are 0/90/180/270. The labels were sitting on the SEPARATOR axes, not
the wedge centres.
Cause: the labels were placed with a 3x3 Row/Column grid. That grid happens to put
cell centres on the diagonals, so a polar control was being laid out with
Cartesian logic and the 61 dp radius -- correct as the midpoint of the 26..96 band
-- landed the glyphs on the boundaries.
Labels are now placed in polar coordinates: angle from the wedge bisector (top,
then clockwise), radius 67 dp, centred on their own measured glyph box rather than
a grid slot. Horizontal text is kept; rotating the left/right labels would read as
an instrument dial rather than school navigation, so the radial relationship comes
from position only.
Also, per the same review, the separators stop being angular gaps. A 2 degree gap
is 0.94 px wide at the hub and the wedge/gap contrast was 1.34:1, so it read as
four notches in one disc. They are now a constant 3 px stroke in #24272D, which
does not flare with radius, and pieWedge moves #5C6068 -> #626873 to reach about
3:1 against it. The hub drops from 26% to 22% of the radius so the centre is less
dominant. Labels go to 15sp semibold, which also helps the longest (3 chars) sit
level with the 2-char ones.
|
|
build 69 died in 'bundles' with no wasm-web zip in distributions, while the same
task run by hand took 8m45s and produced the zip. wasm-opt -O3/-Oz plus webpack
is by far the slowest thing in this build, and six bundle tasks in one
invocation pushed the step past the worker's timeout.
Splitting bundleWeb into its own step with timeout=3600 keeps the fast bundles
where they are and stops the web bundle from riding on their clock. The deploy
step already asserts every expected zip exists before uploading, so a silent
missing bundle is now reported by name rather than as a bare FTP exit 3.
|
|
|
|
Reviewed with GPT 5.5 Think Deeper over the M365 relay. It confirmed the donut
was the right call and rejected one part of it.
Keep the donut. The hub is 6.76% of the disc area (radius 26dp of 100dp)
against ~23.3% per wedge, so it is usable without crowding the items, and
removing the cross from behind the "W" fixes a real defect. It also supplies the
missing semantic rule: the centre is a meta-action, not a fifth command, and
must not be styled like a wedge -- recorded at the glyph, since the tree is
currently one level deep and "W" is correct only at the root.
Dividers: replace the radial strokes with a 2dp negative-space gap in the disc
colour. Contrasting strokes read as construction lines and emphasise the pie's
geometry rather than four selectable objects. The gap is paint only -- hit
testing keeps the exact 90-degree boundaries, so a pointer in a gap still
belongs deterministically to one adjacent sector.
The near-coincident ring was the actual mistake: a selection ring at r-1.5/3px
sat 3px outside the frame ring at r-1.2/2.4px and read as two parallel lines.
Selection is now the corresponding arc of the single existing frame, drawn at
5px in the accent colour.
Label band unchanged at 39dp: the hub turns out to validate it rather than
invalidate it -- 61dp is exactly the midpoint of the now-visible 26..96dp
annulus, so moving labels either way would make the hub look wrong.
Pro also confirmed the arithmetic behind the 200dp diameter is sound (Blender
and Kando both use 100px radius; Gnome-Pie's formula gives 46px only as the
geometric minimum).
|
|
Three separate defects, all found by measuring captures rather than reading
the code.
1. Note row title stayed black. NoteButton's title Text omitted color=, so it
inherited LocalContentColor and got Compose Material's default black --
identical to the KUKURI/card bugs, but my earlier sweep MISSED it. That
sweep's parenthesis matching over-ran past a single-line call into the
next statement, absorbed that statement's color=, and reported zero. The
detector now tracks depth per character across the file; it reports 0
genuinely uncoloured Text() calls across all 13 files, so this class is
closed by construction rather than by spot fixes.
2. Light mode left white-on-ground text. Five sites had a hard-coded
Color.White with nothing filled behind them, so they vanished on the light
ground: the Home "今日" heading, the CountChip number, the pie hub ring,
the pie child dots, and the pie "W". Each now takes the theme's paired
colour (onSurface / pieInk / pieLine).
The other Color.White uses are left alone deliberately -- they sit on a
filled accent (wine primary, personal-kind chip, FAB, active nav item,
filled filter/action buttons), where white is correct in both themes.
Verified each by reading its background rather than by blanket replace.
3. Pie menu reshape, asked for as "もっといい形に". The wedges ran to the
centre and the divider lines crossed the hub, so the "W" had a line
through it and the four items read as one pie. It is now a donut: a hub
disc drawn over the wedges, dividers starting at the hub radius instead of
the centre, and a ring on the touched wedge's outer edge so the current
selection is shown by position as well as colour.
|
|
Follow-up to the KUKURI title fix -- same defect, same cause, six more sites.
The card content on Home / Note / Timeline rendered black on the dark ground:
HomeSection label (TODO section heading)
Today's section label (今日 section heading)
NoteRow body (note text, the unread/bold variant)
timeline row body (the line text)
timeline row `from` (the author column)
second timeline body (the line with an attachment/badge)
All omitted color, so they inherited LocalContentColor; no Surface in those
columns sets it, so they took Compose Material's default black. Adjacent
elements in the same rows -- the note caption, the badge, the right-aligned
due labels -- already passed an explicit color, which is why only part of each
row looked wrong.
Every Text in modules/orgflow-ui now names a color: a sweep of the whole module
for Text(...) calls with no color= argument returns zero, so this class of bug
is closed rather than patched at six sites.
onSurface rather than White, so light mode stays correct.
|
|
Dark mode left the titles black. Measured from the deployed capture: the
"KUKURI" title rendered as #1b1c1f, luminance 11%, i.e. DARKER than the
#313338 ground it sat on.
Cause: all three call sites omitted color, so they inherited
LocalContentColor -- and no Surface in those columns sets it, so they got
Compose Material's default, black.
- NavRail "KUKURI" heading
- NarrowScaffold "KUKURI" heading (phone layout)
- FramePlaceholder frame title
onSurface is the paired colour for the background/surface faces actually in
use, so it is correct in both themes rather than hard-coding white -- which
would have re-broken light mode.
Home / Timeline / Note measured #fcfcfc / #fafafa and were already white;
their apparent dimness is contrast against the invisible title, not a
separate defect. The phone tab bar already passed an explicit colour.
|
|
fetch-github-ios derived the version with
sed "s/.*-\([0-9][^/]*\)\.zip/\1/"
but that step lives inside a Python triple-quoted string, where \1 is an
OCTAL escape. Python consumed it as chr(1) and handed the shell a replacement
character, so every build staged the file as
kukuri-ios-\x01.zip
which then failed the FTP upload. Build 60 surfaced it as
'FTP FAILED: kukuri-ios-\x01.zip' -- the artifact assertion added in the
previous commit is what made this visible at all.
Use parameter expansion instead (no backreference to escape), and reject a
non-numeric version rather than carrying it into a filename. Verified:
'kukuri-wasm-web-0.1.0.zip' -> 'kukuri-ios-0.1.0.zip', and 'abc' is rejected.
|
|
The previous commit wrote the shell line-continuations as a literal double
backslash, so the embedded script failed 'bash -n' with 'Syntax error: word
unexpected' and the deploy step would have aborted on every run.
Now verified with 'bash -n' on the extracted script, and the clean-wasm-output
step re-checked the same way.
|
|
Build 58 exited 3 in deploy-to-lighttpd with no usable message. The FTP loop
had no error handling, and the underlying fault was upstream and silent: a Zip
task whose from(...) directory does not exist archives nothing and does NOT
fail, so a bundles step that produced no kukuri-wasm-web zip left the loop
uploading a short list and dying partway on curl's exit code 3.
Check for all five expected artifacts before uploading anything, name the
missing one, and report which file failed on FTP instead of surfacing a bare
exit code.
Diagnosed by reproducing the CI sequence exactly (clean outputs, then all
bundle tasks in one invocation) and confirming bundleWeb alone succeeds in
6m07s -- the earlier missing-zip results came from the harness killing the
build at its timeout, not from a defect in the task.
|
|
The build cache alone was not enough. A reused configuration-cache entry made
the detekt gate fail with
ClassNotFoundException: io.gitlab.arturbosch.detekt.cli.Main
Reusing configuration cache.
...while the same task passed locally with --no-configuration-cache, so the
cached task graph held a classpath that no longer resolved. That cache is also
what pins the Wasm compile UP-TO-DATE, which is how a stale bundle shipped
past a green build.
Note: master.cfg lives in git AND on the master at
/var/lib/buildbot/masters/default/master.cfg. A change here does nothing until
it is copied across and 'buildbot reconfig' runs -- the first attempt at this
fix appeared to work because the step list still lacked clean-wasm-output.
|
|
The served web bundle was stale while every build reported SUCCESS.
Measured from the deployed pixels, not inferred: the nav rail rendered at
~232px and the pie disc at exactly 132x132px -- both are the PRE-change
values, while the source has had 280dp and diameterDp=200 since bd86609.
Cause: the Kotlin/Wasm compile stayed UP-TO-DATE against build/ outputs
left in the tree by an earlier local build, so `bundles` repackaged the old
wasm. Clearing the Gradle build cache alone does not help, because the
up-to-date check also keys on the compile output already being present.
Add a clean-wasm-output step ahead of `bundles` that removes the Wasm
compile/dist/webpack outputs and the build cache. Only build directories are
touched, never source.
Note the correction to an earlier claim in this repo's history: the
content-addressed wasm filename was taken as proof that the served artifact
matched the source. It does not -- the address only changes when the compile
actually runs, so an unchanged address meant "nothing recompiled", not
"already up to date". Measuring the rendered pixels is what caught it.
|
|
The bundles step asked for `bundleWasm`, which is not a task. Gradle's
camelCase abbreviation matching resolves it to `bundleWasmNode`, so the
build reports SUCCESS while building the node bundle again and leaving the
web bundle to `bundleWeb` alone. Confirmed:
$ ./gradlew bundleWasm --dry-run
:modules:orgflow-domain:bundleWasmNode SKIPPED
BUILD SUCCESSFUL
Harmless today, but it silently retargets if another bundleWasm* task ever
appears, and it misrepresents what the pipeline does.
Separately, while chasing this: the deployed web wasm looked stale because
its bytes never changed across builds. The cause was the Gradle build cache
serving pre-change entries, not a broken pipeline. With the cache removed,
the same source edit produces a new content-addressed wasm name and
orgflow-ui.js picks it up. The wasm filename is content-addressed, which
makes it a cheap oracle: 38dp produced 5e232b18..., and restoring 39dp
reproduced 8bc1b48e... exactly -- so the served artifact does contain the
current PieMenu change. The 2026-09-27 mtime inside the zip is Gradle
preserving timestamps for unchanged entries.
|
|
master.cfg checks out with method='clean' and buildbot builds inside this
same tree, so every build silently reverts uncommitted changes here. That
cost a finished PieMenu edit, which had to be reconstructed by hand.
Add a script that snapshots staged+unstaged diffs to
/ROM/worker-cache/uncommitted-<stamp>.patch. Run it before gradlew, before
pushing, and before any build not initiated here.
|
|
The 120dp disc was 60% of what shipping radial menus actually use, which
left 4-character labels touching the outer ring and the divider lines.
Match the measured values instead of the geometric minimum:
Blender pie_menu_radius = 100px -> 200px diameter
Kando --child-distance: 100px -> 200px diameter
Fly-Pie child-offset = 106px -> 212px diameter
Gnome-Pie radius = 100..150 -> 200..300px diameter
Gnome-Pie's sliceRenderer gives the geometric lower bound for 4 wedges as
(slice_radius + slice_gap) / tan(pi/N) = 46px, so 100px is not a geometric
requirement -- it is where the products settled for legibility and room to
move. Left column goes 232dp -> 280dp to hold a 247px painted disc plus
14dp padding on each side.
Label band moves 21dp -> 39dp so the text sits mid-band between the 26dp
centre hub and the outer ring, touching neither the hub, the dividers, nor
the frame.
Restores the working-tree edit that the buildbot's `method='clean'` checkout
discarded when it ran inside this directory; a copy is kept at
/ROM/tmp_probe/pie-recovered.patch.
|
|
The worker container could not start and, once started, could not reach the
master. Two independent host-level causes, both measured rather than assumed:
1. pf was disabled, so podman refused to start the container at all
("The pf kernel module must be loaded to support ipMasq networks").
/etc/pf.conf already carries the matching NAT rule for podman's
10.88.0.0/16, and the ruleset has no block rules, so enabling it changes
no filtering.
2. The CNI bridge path returns EACCES on every outbound connect, including to
the container's own gateway -- verified from a fresh container with no
volumes, against 10.88.0.1, 192.168.3.110, 8.8.8.8 and 1.1.1.1. The
route table inside the container is correct, so this is ocijail refusing
the connect. --network=host was measured to work for all four and costs
nothing here: the worker only makes outbound connections.
Also record, because both cost real time:
- GRADLE_USER_HOME (6.3G) and .konan (1.7G) live in the container's writable
layer with no named volume, so `podman rm` destroys them.
- `podman commit` does not preserve them: buildah stages into /var/tmp on the
root zfs pool, which has 1.5G free against a 6G layer (ENOSPC). Extracting
the two directories with --tmpdir pointed at /ROM works.
- Secrets exist in two locations and they DIFFER. /root/buildbot/secrets is
the set the container mounts; /ROM/wiiu/secrets has a different
worker_pass and an empty git_pass.
- `master.cfg`'s TOKEN lines look corrupted when a tool redacts
"Authorization:" in rendered output. They are intact.
extract-caches.sh + recreate-worker-hostnet.sh make the recovery rerunnable;
RECOVERY.txt documents the evidence and the traps. Named .txt because
.gitignore excludes *.md as local-only.
Verified after the change: master reports connected_to [{masterid: 1}],
twistd logs "Connected to buildmaster; worker is ready", and the toolchain
inside the container is intact (JDK 21.0.12, node 20.20.2, gradle 6.3G,
konan prebuilt, Android build-tools 35/36).
|
|
Reported as "wrong and hard to see", and the cause was a colour source that
had nothing to do with the pie.
The disc was filled with MaterialTheme.colors.secondary. In dark that is
#3A3C42 against a page ground of #313338 - a difference small enough that the
disc read as part of the background. The wedges were then drawn at white
alpha 0.16 on top of that, so they vanished as well. The result was an outline
with labels and no visible body, which is what the screenshot showed.
The pie now carries its own four colours in the theme, alongside the calendar
colours, because "is this visible" is a property of that surface and should
not be inherited from a general-purpose role like secondary:
dark ground #313338 -> face #4A4D55 -> wedge #5C6068, line #EDEDEF, ink white
light ground #F2F0F0 -> face #FFFFFF -> wedge #C9C6C6, line #3A3436, ink #1A1617
Wedges are now opaque rather than a white overlay, so the three steps (ground,
face, wedge) are guaranteed to differ by construction rather than by luck.
The light values needed a second pass. The first attempt used #E2E0E0 for the
wedge, which on a #F2F0F0 ground was judged too close to separate - the same
failure as the dark case, caught by checking the light theme rather than
assuming the dark fix carried over. #C9C6C6 gives the wedge a clear edge
against both the white face and the grey ground.
Verified in both themes at 3.2x: the disc body is visible, the outer ring is
present, the cross reads as four sections, the labels sit inside their own
wedges in the mid band, and in each case it is unambiguous which label belongs
to which wedge.
|
|
Three reported problems, all real, all reproduced in a capture first.
1. There was no outer ring. The edge of the disc was defined only by a faint
difference from the page background, so the extent of the pie was not
clear. There is now a white ring on the perimeter.
2. The labels sat outside the circle. That was a deliberate choice to avoid
the occlusion that Callahan et al. 1988 recorded users complaining about -
but it makes it impossible to tell which wedge a name belongs to. The names
are now inside, in the outer band of each wedge, so they overlap neither the
centre W nor the divider. The occlusion concern is handled by placement
rather than by moving the text out.
3. Four wedges read as eight. The previous version left a two-degree gap
between wedges, and those gaps appeared as four extra lines. The gaps are
gone; the boundaries are drawn as lines instead, which for four items is
the four diagonal lines that read as a cross.
Sizes, measured rather than guessed where possible:
- the disc is 120dp. At 132dp it overflowed the sidebar by 12px: the column
is 232px and its padding takes 14dp each side, leaving about 204px, so
120dp is the largest that fits.
- the dividers are 2.6 wide at alpha 0.85. The earlier 1.6 / 0.55 was
reported as too faint, and a boundary that cannot be read invites the
mis-targeting that 1993 recorded as fine-tuning and reselection.
- labels are 13sp bold white, inset 21dp on all sides. With a 60dp radius,
a centre cut-out of about 16dp and a few dp of ring, the usable band runs
from about 26dp to 52dp from the centre; 21dp inset centres the text at
about 39dp, mid-band.
That last figure is arithmetic, not measurement - no study measures where
text should sit inside a sector (recorded in 2.40). It follows from the
constraint "clear of the centre W and clear of the divider, unambiguously
inside its own wedge".
Result checked in a capture at 3.2x: ring complete, four labels inside their
wedges in the mid band, dividers clear, and the wedge each name belongs to is
obvious. No remaining defect reported.
|
|
The pie was a List<String> of four quadrant names with a separate
Map<String, List<String>> for children. That cannot express a tree, so the
submenu mechanism only worked one level deep and the angles were hardcoded in
the drawing code. It is now PieItem: id, label, optional angle, children.
Modelled on Kando (kando-menu/kando, MIT), whose schema I cloned and read
(src/common/settings-schemata/menu-settings-v2.ts). Kando's items are a
recursive union of button and submenu types over a common base of name, icon
and iconTheme, and the part worth taking is how angle works there - its own
comment says an unset angle "will be computed when the menu is opened", while
a set angle is a "fixed angle" and the siblings redistribute around it. FSMP
adopts exactly that: leave angle out and the items divide evenly, set it and
the item keeps that direction. That is what makes spatial memory possible
later without changing the model.
One thing deliberately not taken: Kando's three-way type split. Kando is a
general launcher, so it needs to distinguish "has children" from "also fires
on its own". The FSMP pie narrows a set rather than launching anything, so
children being empty or not is the whole distinction. Adding a type field
would mean checking which type each item is at every use site for no gain.
The item count is now backed by measurement rather than assertion.
Kurtenbach, Sellen & Buxton 1993 (Human-Computer Interaction 8(1):1-23, full
text obtained) tested 4, 5, 7, 8, 11 and 12 slices - not 4/6/8/10/12 as the
commonly repeated version has it, and not 10 at all. Even sizes (4, 8, 12)
were easier to learn; odd sizes (5, 7, 11) were harder; 12 beat 11 on both
speed and accuracy because a clock face is a usable metaphor. Their
conclusion in Kurtenbach & Buxton 1994 (DOI 10.1145/191666.191759) is
narrower than "12 is the limit": "if menus contain an even numbers of items
and less th[a]n [ten] of them ... marking menus will have a distinct
advantage". Four quadrants satisfies both, and 1993 also found that up to
five slices the hidden/exposed distinction barely matters, which is where
four sits.
Also recorded, because they are widely repeated and wrong:
- pie menus are not faster "because of Fitts's law". Callahan et al. 1988
fitted a Fitts expression after the fact as an explanation; they did not
test the law. Their measurement was about 15% faster at 8 items, with
the error-rate difference only marginally significant (P=0.087).
- 7+-2 must not be used for menu item count. Cowan 2001 says Miller's seven
"was meant more as a rough estimate and a rhetorical device than as a
real capacity limit". The working-memory figure (3-5) is about retention,
not about how many options can be shown.
- there is no optimal choice count at all. The choice-overload
meta-analyses found effects near zero.
- icons must carry labels: ISO 9186 sets a 66% comprehension threshold for
symbols, and that is a pass mark, not an average. Wiedenbeck 1999 is the
empirical comparison. NN/g is not a citable academic source.
What has NO evidence, now recorded as undecided rather than guessed: how much
text fits in a sector, the right boundary margin, optimum hierarchy depth
(1994 explicitly calls it an open question), a label length limit, thresholds
for group count, and the effect size of progressive disclosure. The one-degree
gap drawn between wedges is a judgement - the smallest gap that still shows
the boundary - not a measured value.
|
|
pie layout
+"-1"/"+1" is information about distance from today, not a step button. The row
now reads 今日から -21日 (or +N日) - how far the centre date is from today -
and pressing it returns to today. Showing it as a step control gave no sense of
distance, which was the point of the row.
The same row now exists on the month and year levels, in the unit that fits:
今月から +Nヶ月 and 今年から +N年. It is omitted when the value is zero, since
the centre already shows the current month or year and repeating it says
nothing.
Re-entering the month view. key(f) only re-keys when the frame changes, so
pressing Calendar while already in Calendar did nothing - the state survived.
A reentry counter is now part of the key, so pressing the same frame again
resets it to the default view. That fixes the case the previous fix missed.
The pie menu labels are finally placed correctly, after two wrong approaches
that are worth recording because the failure was invisible in the code:
1. Computing offsets from the centre. Modifier.offset moves a node from
where layout put it, so with a centre-aligned parent every label was
displaced by half the parent's size. Only one of four appeared.
2. A nested Box(fillMaxSize()) with per-slot Alignment. The inner box takes
the window's size rather than the parent's, so labels were drawn far from
the disc and nothing appeared near it.
Both failed for the same reason: the offset origin depends on the parent's
dimensions, which cannot be corrected with constants. The fix is to stop
computing positions and let layout do it - a Row of [label column, disc, label
column], each column a Column of two labels. The disc and its labels cannot
drift apart now because they are siblings in the same layout.
Month and year labels carried the same defect and are fixed by the same
change, since they all go through PieDial.
Also fixed a compile break of my own making, worth noting because it presented
as something unrelated: the file stopped compiling with "Unresolved reference"
on functions that plainly exist. The cause was a doubled /** - two doc-comment
openers in a row - which left one unterminated, so everything after it parsed
as comment. This is the second time a comment has caused a cascade of
unrelated errors in this file; Kotlin's nested comments mean an accidental /*
anywhere, including inside a comment, swallows the rest of the file.
|
|
labels
Seven items, one of which was a real bug with a clear cause.
Calendar would not return to the month view. Calling CalendarFrame() from a
when(f) means Compose keeps its remember blocks alive when you navigate away,
so leaving in the year or day view and coming back left you in that view.
Wrapping the frames in key(f) gives each frame its own state, so Calendar
always opens at the month view per 2.27.
The day view header is now the requested shape: the centre is the date being
viewed, written the same way the body used to write it (9月26日(土)), and the
row below carries the offset from today as pressable chips. Pressing them
moves. The offset is measured from today, not from the day being viewed - with
today+3 on screen the row reads [今日] [-1日] [+1日] and those go to today,
+2 and +4. Showing the current day again in that row would duplicate the
centre. The first version of this did get the arithmetic wrong, sending +N
back to the same day; caught and fixed while wiring it.
The same bar shape is used for all three levels: chevron, centred value,
chevron, and a pill on the month view. The month pill says 今月, not 今日 -
the day view is the one that says today.
Month event-bar labels were squashed: 9sp text in a 16dp bar left no vertical
padding at all. The bar is 20dp, the text 10sp with a 12sp line height, and
4dp of horizontal inset. Days with more than one event keep plain 5dp colour
bars - there is not enough height per bar for legible text, and colour alone
beats unreadable text.
Year view day taps now work. The previous version deliberately made days
non-tappable to keep the year level about picking a month; the request was to
jump to a day directly, so tapping a day in the year view opens that day.
Spacing between sections, grounded in a measurement rather than a convention.
Brumby & Zhuang 2015 (CHI, DOI 10.1145/2702123.2702177 - open access, read in
full) measured 30px between items in a column and 250px between column
groups, about 1:8, and concluded that what actually governs search time is
whether the visual groups match the semantic ones - grouping unrelated items
visually slowed search down (M=3.34s semantically arranged vs M=4.23s random,
p<.001). This app had 6dp between rows and 16dp between sections, only 1:2.7.
Sections are now 48dp for 1:8. The paper measured a ratio, not absolute
values, so only the ratio is borrowed and the absolute size follows the row
height here - which is more to stand on than 8dp-grid conventions: a survey
found that Gestalt proximity gives only a monotonic relation and that the
8dp grid, 44pt and 48dp+8dp figures are industry conventions with no
measurement study cited behind them.
Pie menu quadrant labels are now always visible, placed outside the circle at
1.15x the radius. They were previously only shown while held, to avoid the
occlusion that Callahan et al. 1988 (DOI 10.1145/57167.57182) recorded users
complaining about - but an always-present control with no labels gives no
reason to reach for it. Keeping them outside means the disc, its bands and
the centre W are still never covered.
Checked and NOT reproduced: black text in dark mode on Home, Timeline and
Note. The most recent captures of all three were read carefully and every
string is white or light grey. 2.31's spacing change is the only edit those
frames received in this pass. If specific text still reads as black, it needs
a screenshot that names it.
|
|
The narrow layout was already implemented to 2.22's skeleton, but measuring it
at a real phone width found two problems, and getting to that measurement took
three failed approaches worth recording.
Why a Galaxy S25 user agent changed nothing: Firefox will not make a window
narrower than 500 CSS px. Requesting 380/400/412/450/500 returned 500 every
time, while 600 and 700 were honoured exactly. An S25 is 384-412 px, so the
real width is unreachable in a top-level window - the limit is the window, not
the UA. Shrinking the Compose canvas element to 384 also failed: the canvas
attribute became 384 but Compose kept laying out against 500, so the capture
showed 500-wide content cut off at 384 and the tab bar lost Live and settings
off the right edge. What works is an iframe of the exact size - it has its own
viewport, so the app inside sees innerWidth 384 and Compose lays out against
real dimensions. phone-host.html is now part of the web resources so it ships
with the bundle, and shot-phone.py drives it. Recorded as 2.29 so this is not
re-derived later.
With a true 384px viewport, the tab bar did not fit. Laying labels out with
SpaceEvenly got only Home, Timeline, Calendar and Note on screen; Live and
settings were pushed off and never drawn. Six text labels do not fit 384px.
So the tabs are now an icon over a short label, each weighted equally
(2.28):
- icons drawn on a Canvas, no image assets: house, list, calendar, page,
camera, gear
- short labels, chosen rather than truncated - cutting a word mid-way makes
it unreadable, so separate short names are used: Home / Line / Cal / Note /
Live / Settings
- labels stay English. One tab reading 設定 among five English ones mixes
languages inside a single row; in-page headings stay Japanese, but the tab
row is a list of proper names.
- icons are never shown alone, for the same reason 2.22 rejected an
icon-only nav: NN/g's Icon Usability (2014) found icons on their own do
not convey meaning reliably.
Wide layout gets the same six glyphs on the left-column nav, as asked. The
same frame now looks the same at both widths, so nothing has to be relearned
when the window changes; wide has room for the full words, so it keeps
"Timeline" and "Calendar".
Calendar bar, per the same request: the centre pill on the month view is now
今月 rather than 今日 (the day view keeps 今日), and the pill is drawn with a
fill so it reads as pressable rather than as a heading. Both bars were
re-measured at 384px and fit.
One capture trap worth knowing: the browser profile is reused so the wasm
caches and startup is fast, which means a stale build can be served and the
screenshot silently shows the previous version - it did, showing "Time" and
"設定" after those strings had already changed. The URL now carries a
cache-buster that the harness forwards to the inner iframe.
|
|
event bars
Seven requests. The first one is the important one: the bottom band was
dead space on every frame, not just the calendar.
Measured before the change: the bottom row is 131px tall and the pie disc
occupies only x=1134-1265, so 874px of that row was empty on every frame.
After moving the pie to the bottom of the left column the month grid runs
from y=90 to y=1022 - five rows on a 187px pitch with 2px left over, where
before it stopped at y=740 with 284px unused. The grid now uses the whole
height. Recorded as 2.25.
Why the left column: the pie is an always-on control whose grammar is "touch
and drag, there is no open action" (2.19), so its position does not change how
it works. The left column already had a weight-based gap below the five
frames, so the pie consumes no new space, and the bottom-left is still the
bottom of the screen - the one-handed reach argument in 2.22 (Hoober 2013,
Fitts) survives. The narrow layout is unchanged: there the tab bar owns the
bottom edge, and the FAB/pie left-right pair was fixed in 2.22.
The calendar's three levels now share one bar (2.26). Left chevron, centred
value, right chevron, and on the month view a Today pill at the trailing edge.
The centre is pressable and goes up a level. The year view gets the same shape
and its chevrons step a year, with the state range widened to 120 years so
years connect rather than stopping two either side.
The day view keeps the centre pinned to today, as asked, and moves between
days with the chevrons and the horizontal strip; the date being viewed is
shown below the bar rather than in it, because the centre is now a fixed
label. LazyRow's deferred realisation is what keeps the continuous day strip
from getting heavy - the same mechanism the month view already uses through
its start/end range.
Month event bars can now carry text. This one is deliberately conditional:
nothing measures whether text inside a calendar cell is readable at a glance,
which is the same gap recorded for the weather icon, so the label appears only
when there is one event in the day (a single bar can be tall enough) and is
ellipsised when long. Multi-event days keep the plain colour bars. The design
note says this is a retreat path, not evidence that text is better.
2.27 records that Calendar opens at the month view and returns to it, since a
month is the granularity most "when is it" questions start from.
Also confirmed for the narrow layout: the ASCII the request refers to already
exists in 2.22 - search at the top, the five-item tab bar with settings at the
bottom edge, and the FAB/pie pair directly above it. Nothing there changed.
|
|
Three requests in one, all recorded in 2.24 and 2.24.1.
1. Stop copying the xpm. dwgo's XPM files were converted to PNG and their
dominant colours measured (six-colour reduction, top entries): clear sky
#98D6D3, rain sky #206AB8, snow sky #38A4F4, fog #D4DEE7, dust #D8C9A0,
tcu #B3C2BB, brk #E2F1F0. Those are now REFERENCE values, not the palette.
dwgo's clear sky is a greenish teal, which sits badly against this app's
wine-and-charcoal theme, so the hues are pulled toward blue and only the
value relationship is kept - light at the top, darker at the bottom. The
chosen colours live in one object, WeatherInk, and the drawing is this
project's own. dwgo is GPL-3.0; nothing of its art is bundled.
2. More conditions. Seven states now, chosen by whether they change a
decision about going outside:
clear, cloudy, rain, snow, thunder, fog, dust
brk and parts fold into cloudy (one overcast is enough); hail folds into
rain (it is still "something is falling"); thunder, fog and dust are
promoted to their own states. Thunder gets a bolt rather than dwgo's
towering-cumulus cloud alone, because the state exists here to warn.
Ordering when several apply: precipitation, then thunder, then visibility,
then sky. The first half follows dwgo's comment ("rain, then fog and then
sky conditions"); the position of thunder is FSMP's own call, since dwgo
files tcu under sky.
3. Compound conditions ("rain then clear"). Home shows ONE current state and
no compound. Three reasons, in 2.24.1:
- The JMA's own terminology page defines "のち" and then advises against
it: "possible to avoid, aim to give the specific time band; for
example 'clear then cloudy' should be 'clear, cloudy from early
afternoon'". It also defines 一時 as under a quarter of the period and
時々 as under half, i.e. these words are encodings of uncertainty that
a reader has to already know.
- METAR has no "then". Rain that has stopped is RE (recent weather, a
past fact - FAA AC 00-45H via BOM), and NSW may not be used in the
initial period. Transitions live in TAF time groups (FM/TEMPO/BECMG).
The data model itself splits by time rather than carrying a sequence.
- Home answers "what is it like right now" - the same question Dockspace
names as the widget's whole job. A compound answer does not answer it.
So: Home one state; the day view lists states by time band; the weekly
view picks the more consequential state for a mixed day; and the detail
behind a tap shows the breakdown along the time axis, which is where
"rain then clear" actually belongs.
Two drawing bugs worth noting since both were invisible from the code:
- a local `fun cloud(...)` could not resolve drawCircle, because local
functions do not inherit the enclosing DrawScope receiver
- nineteen unrelated "Unresolved reference" errors all came from one
comment containing "pixmaps/*.xpm" - the "/*" inside it opened a nested
comment that never closed, so the rest of the file parsed as comment
|
|
The icon was a flat yellow disc with eight spokes, which conveyed "sun" and
nothing else, and did not distinguish the states the way a weather glyph needs
to. dwgo's pixmaps are the reference now: they are 64x64 XPM images, converted
to PNG and inspected directly, and the drawing method was copied from what
they actually look like rather than from a description.
What was taken from the images:
- a vertical sky gradient as the glyph's background (light blue at the top,
deeper blue at the bottom), which is what gives the set its atmosphere
- clouds as overlapping filled circles with a white top and a darker grey
underside, so the mass reads as volume rather than as an outline
- rain as thin slanted lines, not drops
- snow as one large six-armed crystal with branches, centred
- the sun as a pale yellow disc with a soft halo, offset to the upper right
- the palette itself: sky #B3E5FC to #4FC3F7, cloud #FFFFFF over #78909C,
sun #FFF9C4, rain #DAE9F7
Licensing: dwgo is GPL-3.0. No image file is copied or bundled - only the
drawing method is reproduced, in this project's own Canvas code. FSMP
therefore stays under its own terms, and section 2.24 records dwgo as a
design reference rather than a dependency.
The glyph is drawn at 28dp rather than 20dp so the cloud shading and the
snowflake branches survive; at 20dp they collapsed into blobs and the whole
point of the style was lost. The tap target requirement is still met because
the whole chip row is the button, not just the icon.
Two bugs found while wiring this up, both worth recording because they
presented as something else entirely:
- A local `fun cloud(...)` inside the draw scope could not resolve
drawCircle: local functions do not inherit the enclosing DrawScope
receiver. It is now a top-level DrawScope extension.
- The file failed to compile with nineteen "Unresolved reference" errors
across unrelated functions. The actual cause was a comment containing
"pixmaps/*.xpm": the "/*" inside the string opened a nested comment
that never closed, so everything after it parsed as comment text. One
line was responsible for all nineteen errors.
|
|
dwgo is real. The earlier note that it could not be identified was wrong:
dwgo is "Don't Want to Go Outside", a dockapp-type weather application at
https://github.com/gasparfm/dwgo. Its source settles the question of how
many weather states an icon should carry, so section 2.24 now cites it.
What dwgo does. It maps METAR codes straight onto the background image, with
eleven themes (default, clear, cloudy, broken, tcu, rainy, snowy, hail, foggy,
dust, otparts - src/dwgo.cpp:322) chosen from three groups: precipitation
(rain/snow/hail), visibility (fog/dust/other, covering FC/FU/HZ/SA/SS/VA) and
sky (clear/cloudy/brkovc/tcu). It picks exactly one, in a fixed order, and the
source comment says why: "The most important for the display theme is rain,
then fog and then sky conditions". One background can be shown, so the most
significant phenomenon wins.
Why FSMP still uses four states. dwgo's granularity comes from the
observation vocabulary, not from what a person can read at a glance - the
codes exist, so it distinguishes them. Nothing in it measures whether fog is
tellable from cloud in a glance, and the number-versus-icon question has no
study at all (recorded in 2.24), so there is no evidence base for a finer
scale. Apple's Weather widgets and the shipping Dock-type widgets both keep
to current conditions, so FSMP stays with clear/cloudy/rain/snow.
What FSMP does take from dwgo: a defined precedence when observations
overlap. Precipitation beats visibility beats sky, in that order, because
rain matters more to a decision about going outside than the state of the
sky. That ordering is borrowed from dwgo's design judgement and is not
FSMP's own measurement; the code says so, and reduced visibility folds into
cloudy rather than becoming a fifth state.
This is a design correction in one direction and a refusal in the other: the
earlier text implied the granularity was simply unknown, when in fact a
working dockapp shows a finer scale is easy to implement - the reason not to
is that nothing shows it is readable.
|
|
Home's top-right now carries current temperature, a condition icon and
high/low. The design and its evidence are recorded in section 2.24; this
commit is the implementation.
The three items are exactly the three the evidence converged on: Apple's HIG
states the Weather widgets prioritise "current high and low temperatures and
weather conditions", and Dockspace - a shipping Dock-type widget - describes
the same three and says it is "intentionally minimal. It does not try to show
a 7-day forecast." So no seven-day outlook on Home; that belongs behind a tap.
The icon is drawn on a Canvas rather than shipped as an asset, and the
condition is rounded to four states (clear, cloudy, rain, snow). Rounding is
deliberate: there is no evidence that a glance can distinguish finer
categories such as fog, sleet or thunderstorm, so claiming that resolution
would be unsupported.
Colour changes only with the condition, never as a severity signal. Simmons &
Sutter 2009 (DOI 10.1175/2009wcas1005.1) found a one-standard-deviation rise
in the false-alarm ratio associated with 12-29% more expected deaths, so
spending warning colour on ordinary rain is not free. Official advisories and
warnings are the only things that will recolour this later.
High/low is rendered smaller than the current temperature because the HIG
notes people care most about current conditions - the hierarchy follows the
stated priority rather than treating all three as equal.
Not yet wired: the values are samples. Section 2.24 fixes the DISPLAY; the
data source is still open, and so are the icon's final form, the default unit,
and the exact colours for advisory levels.
|
|
menu labels
Calendar height (this took several measured attempts to settle).
The month grid has to both fill the height it is given AND not clip the last
week, and the library's two height modes each give one but not the other:
- Wrap lets the cell decide its height (aspectRatio or height). With
aspectRatio(1f) the row height follows the width, so at 1280px wide a
6-week month wants 1093px and the last week is cut off.
- Fill spreads the month's days over the parent's height, but it
distributes over THAT MONTH'S WEEK COUNT. A 5-week month therefore stops
after 5 rows: measured 130px pitch, rows ending at y=740 with 284px of
ground still unused below. Removing the cell's fillMaxHeight collapses
cells to 26px instead.
So neither mode does what this screen needs. The month's week count is
computed, the available height is divided by it, and the row height is passed
into the cell. Measured after the change: 5 rows on a 157px pitch running to
y=874, i.e. the full allocation. The 150px below that is the pie's row, not
dead space (verified by sampling: the pie disc occupies y=887-1011).
Cells are now horizontal rather than square, matching Google Calendar's month
view, and the date sits top-left where a wide cell reads better.
Close is gone. Its action was "scroll back to this month", which the Today
button already did, so it was a second control for one job.
Month view no longer carries a Today button (asked for). The month is reached
with the header and chevrons, and the year view also returns to it.
Day view keeps only the two controls that were asked for: previous/next day
and Today. Stepping reuses the month view's chevrons so left and right keep
one meaning.
API note: kotlinx-datetime 0.7.1 exposes plus(Int, DateTimeUnit.Day); there is
no plusDays on the common LocalDate (checked with javap against the klib's JVM
artefact).
The radial menu now names what is held.
Kurtenbach & Buxton 1993 (CHI '93, DOI 10.1145/169059.169426) note that
"menu labels strongly suggested the correct angle", which is what kept
confusions away, and they call for a follow-up with less suggestive labels -
so the label is doing real work in direction recall. Callahan et al. 1988
(CHI '88, DOI 10.1145/57167.57182) also record a subject complaining that the
pie "occluded the target prompt", so the held item's name is drawn OUTSIDE the
disc rather than inside it, and only for the one item being held - naming all
of them at once would refill the disc with text and recreate the occlusion
complaint.
The 132dp diameter is justified against the tap-size floors (Material 48dp,
Apple 44pt, WCAG 2.2 SC 2.5.8 24 CSS px), and Kurtenbach & Buxton 1991
(UIST '91) put the useful ceiling at 12 slices for even divisions, so four
quadrants plus children sits well inside the limit.
Its own note: the earlier report that "the colour does not change between dark
and light" was a measurement fault on my side, not a bug. The theme toggle
does work - the sidebar ground measures #313338 dark and #F2F0F0 light when
toggled. The pixel checker I wrote was sampling every 5px and skipping the
thin strokes; verify-shot.py now samples densely enough.
|
|
the text
Six requests from the review, plus the reason the earlier verification was
wrong.
1. Close removed. It had nothing to close in a month view - its action was
"scroll back to this month", which the Today button already does, so it was
a second control for the same job.
2. The month grid now uses the height it is given.
Wrap + weight(1f) was still clipping: Wrap sizes rows from DayCell's
aspectRatio(1f), so it never used the height that weight handed it, and the
6th row came out 52px tall with 150px of ground left unused below.
Measured, not eyeballed:
before - rows at y=90..874 with the last 52px: CLIPPED, 190px unused
after - six rows on a 131px pitch, y=90..874: complete, 150px unused
ContentHeightMode.Fill is the mode whose documented contract matches
weight(1f) ("the days in each month spread to fill the parent's height"),
so the grid now divides the available height into six equal rows.
3. Day view can step to the previous/next day. It could only be left via the
back arrow, so seeing the next day meant going back to the month grid and
picking it again. The step buttons reuse the month view's chevrons so left
and right keep the same meaning.
API note: kotlinx-datetime 0.7.1 has plus(Int, DateTimeUnit.Day) and
minus(...); plusDays/minusDays do not exist on the common LocalDate
(verified with javap on the klib's JVM artefact).
4. Text colours follow the theme. The date digits were hard-coded Color.White,
so in the light theme they were white on a white cell and effectively
invisible - this is what "the colour does not change between dark and light"
was describing. Every heading-band and cell colour now comes from
CalendarColors; only text sitting on a coloured event block stays white,
which is correct in both themes.
5. The font has one override point. WebTheme takes a fontFamily argument
(default: the bundled Zen Kaku Gothic New) and feeds MaterialTheme's
typography, so no screen sets a family of its own. Passing
FontFamily.SansSerif switches the whole app to the system font:
WebTheme(fontFamily = FontFamily.SansSerif) { ... }
Verification method changed. Vision reading of these captures has been
unreliable - it reported "the whole month is shown to the 30th" for a capture
whose last row was 52px tall, and described a one-cell highlight as a
two-cell block. Checks that can be answered from pixels now use
/root/shots/verify-shot.py instead: it reports the sidebar background (dark
vs light), the nav highlight band, the cell-row runs with their pitch, and
warns explicitly when the last row is shorter than the pitch (clipped) or
reaches the bottom edge.
|
|
The web shell never loaded its font. webfont.js stores the bundled TTF as
base64 in window.__orgflowFontB64, but the fetch/XHR shim in index.html only
looked for globalThis.__orgflowFontBlobs, which nothing ever created. Every
font request therefore fell through to the real fetch, the nested
composeResources path does not exist on the deploy server (its FTP forbids
MKD, which is why the font is shipped flat in the first place), and the app
rendered with no font at all.
Visible effect: every date number and every Japanese label came out as tofu
boxes. Confirmed by capturing the running app - document.fonts reported
"loaded n=0", and the calendar grid showed squares where the digits belong.
Fix: decode __orgflowFontB64 into the Blob map the shim expects. The decode
runs once at parse time and again lazily on the first matching request, because
webfont.js is loaded after this script and may not have run yet when the first
font read happens.
Two other things were established while chasing this, and are worth recording
because they look like application bugs but are not:
- The month grid appeared to stop at the 19th. It was not a layout bug: the
browser chrome left only 650px of viewport, and a 7-column month grid needs
roughly 190px per row, so six rows cannot fit. The screenshot tool now runs
the browser in kiosk mode and sizes it to the full screen (1280x1024), and
the grid shows the whole month plus the out-dates.
- The calendar's "today" cell looked like a two-cell red block in one capture.
It is a single cell; the earlier reading was off because the viewport was
small and the cells were being squeezed.
|
|
The review screenshots showed the pie widget covering the Timeline's message
input (right end) and the calendar's cells for Oct 2-3. It also explained a
symptom reported earlier as "dates cannot be pressed".
Cause: MainArea laid the content out as
Column(Modifier.weight(1f).fillMaxSize())
Combining weight with fillMaxSize makes the child try to fill the parent's
whole height instead of the height that weight handed it, so content spilled
into the region occupied by the pie and the tab bar and got covered.
Fix: give the content column weight(1f) plus fillMaxWidth() only, in both the
wide and narrow scaffolds, and keep the pie in its own row so it takes space
rather than floating over content. The month view's earlier 72dp reservation
is still there as a second line of defence, but the real fix is here - it
applies to every frame, not just the calendar.
Why not float the pie: Pibernik et al. 2019 (Future Internet 11(7):148) tested
FABs against toolbars and rejected the hypothesis that they improve usability,
and no study establishes a safe overlap margin. With no evidence supporting an
overlay, spending layout space is the conservative choice, and it makes
"cannot press the dates" impossible by construction.
Also visible in the same review and already correct: light theme now renders
black-on-white with the full grid to Oct 3, the Today button is present, the
sidebar date duplication is gone, and Home shows the Live-style layout with a
NEW badge and unread dots.
|
|
Everything here comes from the light-theme review. The root cause of most of
it was one mistake: the calendar carried its own dark palette, so switching
to light left it dark while the sidebar went light, and text that used
MaterialTheme.colors.onSurface turned black on those dark surfaces.
Colours now travel with the theme.
WebTheme publishes a CalendarColors via CompositionLocal holding ground,
cell, header, today, the matching "on" colours, and the event palette.
Both themes define their own; screens read from it and never hard-code.
Example 3's mechanism (cell offset from the ground so the 1dp gap reads as
a gridline) is kept; only the direction of the offset differs - dark goes
lighter than the ground, light goes white on the light ground.
This fixes: the calendar staying dark in light mode, the day view's hour
labels being dark-on-dark, the year view's date numbers being invisible,
and the Close button's white text on a light header.
Contrast is held to WCAG 2.2, checked against the W3C text rather than from
memory: SC 1.4.3 requires 4.5:1 for body text (3:1 for large), SC 1.4.11 3:1
for UI parts. There is no relaxation for secondary text, so the dimmed text
throughout was raised (alpha 0.5-0.6 to about 0.78). The old values were very
likely below 4.5:1, which is what "the font is black and I cannot see it"
was describing. APCA was not used: it does not appear in the WCAG 3 draft,
which still says the contrast algorithm is to be determined, so WCAG 2.2 is
the operative standard.
The pie is no longer overlapped.
The worry was that a floating pie over the grid makes dates untappable.
There is no evidence for overlapping it: Pibernik et al. 2019 (Future
Internet 11(7):148) tested FABs against toolbars and rejected the hypothesis
that they improve usability, and no study measures a safe overlap margin.
So instead of laying the pie over content, the month view now reserves
72dp at the bottom for it and puts the Close button inside that reserved
band. Nothing is covered.
Answers to the open questions, with the evidence that settled them:
- Week view on Home: no. Chipley & Barlow 2007 tested dashboard folklore and
could not support a fixed item count; the "7 plus or minus 2" rule is
specifically criticised as misapplied in IEEE Trans. Prof. Communication
2002. Adding a whole week also inserts a granularity between the year
overview and the day detail without a clear question for it to answer, so
Home stays on today (the week belongs to Calendar).
- Weather: only as ambient information, if at all. No research exists on
weather in a school app. Matthews 2006 (DIS) gives the conditions for
glanceable peripheral displays - it must not demand attention, changes must
be graspable at a glance, and detail must be reachable. Iqbal & Bailey 2008
(CHI) show notifications cause interruption cost. So: a small current
condition and temperature, colour change only when it matters (rain,
extreme heat), tap for the week, and no notifications. Whether it belongs
at all is still a product decision, not an evidence-backed one.
- Year view density: Shneiderman 1996 (IEEE Symp. Visual Languages) sets
overview-first, zoom and filter, details on demand; Hornbaek et al. 2011
(Personal and Ubiquitous Computing) supports overview+detail on mobile.
The year view therefore shows one bar per day with events rather than a
full stack - density at the overview level, detail when you zoom in.
Also in this pass: a Today button in the month header returns to the current
month (asked for), and the year view now marks days that have events using the
same bars as the month view.
The duplicated date is gone: the sidebar showed "月日 6限目" while Home showed
"今日 9/22(火) 3限". Home keeps it, the sidebar drops it.
|
|
Six changes requested in one pass, all compile-verified for wasm, jvm and
android and pushed to the local preview server for review.
1. Fix: the month grid was cut off when the width was tight.
HorizontalCalendar defaults to ContentHeightMode.Fill, whose documented
contract is "the days in each month spread to fill the parent's height" and
which therefore requires the calendar itself to be given a height. We gave
it only fillMaxWidth, so the height allocation was indeterminate and the
last weeks vanished (observed: rows ending at the 19th or 26th). Setting
ContentHeightMode.Wrap hands the height back to DayCell's aspectRatio(1f),
which derives it from the seven columns' width, so the grid is complete at
any width.
2. Calendar now matches the library's Example 3 (fetched the real sample
source rather than working from the screenshot):
- cell: aspectRatio(1f), 1dp border only when selected, padding(1dp),
background; the "gridlines" are that 1dp gap letting the page
background show through, not drawn lines
- date: top-right (align TopEnd, top 3dp / end 4dp), 12sp
- event bars: bottom-centre column, fillMaxWidth, 5dp tall, 6dp apart,
bottom padding 8dp
- month title: 40dp bar, 22sp, Medium, centred, arrows on both sides
- weekday row: 12sp, white, FontWeight.Light
The previous attempt copied the screenshot's look approximately (bars at
55% width and 3dp, date centred); the sample is the authority.
3. Event bars and colours now follow the background instead of hard-coding
Example 3's palette. Example 3 sits on #0E0E0E, this app sits on #313338,
so reusing its values made the calendar look like a black hole. The
mechanism (cell one step lighter than the ground, so the 1dp gap reads as a
gridline) is kept and only the values are mapped onto the theme's ramp:
ground #313338, cell #3A3C42, header #2B2D31, today wine #5A1524. Values
live in WebTheme.kt so no screen carries its own palette again.
4. Day view rebuilt on the Google Calendar model. It was a table of
time-and-item rows; it is now a fixed hour height with a long vertical
surface, events placed at their start time with height proportional to
duration, hour labels down a narrow left column, and vertical scrolling to
reach other times of day. Event colours reuse the month view's pie-quadrant
colours so the two views agree.
5. Calendar drills down three levels: year -> month -> day. Tapping the month
title goes up to the year; tapping a month in the year view comes back down
to that month; tapping a day opens the day view. Reason: reaching a distant
month otherwise means pressing the arrow many times, and "when is it"
questions often start from a year-level overview. Uses the library's
VerticalYearCalendar (2.7.0 has it under com.kizitonwose.calendar.compose,
gated by ExperimentalCalendarApi - the package is NOT ...compose.yearcalendar,
which is where the state's remember function lives).
6. Home and Live now share one shape: section heading + full-width card rows
with a right-aligned note. Home adds what Live does not have - NEW and unread
- shown as a count badge on the heading and a dot plus bolder text on the
row, reusing the same red as Live's on-air indicator so "live" and "new"
read as the same kind of attention. Live's on-air row is emphasised with a
dot in the heading, a filled watching button, and bold title.
EventKind and the calendar colour constants became internal so Home/Live can
share them instead of each screen defining its own copy.
Remaining known gaps: sample event data is still hard-coded; the Close button
resets the visible month because the month view has nothing to close; the
year view does not yet show event marks; and the calendar colour ramp is
deliberately provisional - it is a mapping of Example 3's contrast onto this
theme, not a measured choice.
|
|
The month grid now follows the Calendar Library reference screenshot that was
given as the target for this screen.
What changes, day cell:
- Events are drawn as short coloured bars along the bottom edge of the cell
(about 55% of the cell width, 3dp tall). Stacked from the top when a day
has more than one event, so a date and its bars never overlap.
- Previously a day with events showed a single bullet "•" in the theme
accent, and the old code comment argued against bars because they would
collide with the date number. Putting them on their own line inside the
cell removes that collision.
- Colours come from the new EventKind enum, mapped onto the pie's four
quadrants (section 2.5.1): year=teal, activity=blue, personal=red,
committee=brown, school=gray. The reference image's five colours are used
as-is rather than inventing a palette.
What changes, month chrome:
- Header is centred and reads "September 2026" (English month name + year)
with chevrons on both sides, replacing the left-aligned Japanese
"9月2026".
- Weekday row uses uppercase three-letter names (MON TUE WED THU FRI SAT
SUN) instead of single Japanese characters.
- A thin dark divider separates the weekday row from the grid, matching the
reference's grid lines.
- The caption "予定ドットのある日をタップ" is replaced by a centred pill
"Close" button, per the reference. In the month view there is nothing to
close, so it currently resets the visible month; the intended behaviour
still needs a decision.
- Cell corner radius 8dp -> 4dp to match the reference's squarer cells.
Colours follow the reference image exactly (sampled values): teal #008080,
blue #0000FF, red #FF0000, brown #A52A2A, gray #808080. Whether these specific
hues should be the app's event colours, or should derive from the theme, is not
settled by the reference alone and remains open.
Sample event data (3/12/17/22) is still hard-coded in eventsOf(); wiring it to
real sources is separate work. Compiles for wasm, jvm and android.
|
|
Timeline, Note as note buttons
Calendar month (screen 3 ASCII): the day cell no longer draws an event bar. The
ASCII shows only "15 16 17 [18] 19 20 21 <-today" plus the note "point = event",
and the bar I had added was what collided with the date. The cell is now date +
selection/today fill + one small dot, and today and selected are passed in
rather than guessed per cell.
Calendar day (screen 4 ASCII): back to the specified frame - a time column
(850/940/1030/1120/1215/1310) with a rule per slot and that slot's items to the
right, timetable and auto-source mixed on the same axis. I had replaced this with
a Google-style free-floating hour grid, which is not what the design draws. The
header takes a list of dates, so single-day and multi-day both work, and the
weekday is shown next to the date.
Note (screen 5, and 2.6): Note is the list of notes, filtered by the content
kind - "全部/課題/文書/成果物" - with the notes themselves as full-row buttons
(探究ノート, 数学プリント, 班レポート, 発表スライド) and a bordered entry for
the unsubmitted view. The explanatory line ("残るもの") is gone; it told the
reader nothing. Per 2.6 a note is a set of cards, so tasks/documents/artefacts
are not sibling entries beside notes - they are what the notes contain, which is
what the filter selects between.
Timeline (screen 2): rows are separated by rules again (they were missing), and
the NEW badge is gone - the ASCII has no badge there (only Home's timeline digest
carries [NEW], which is left as is).
Live (screen 6): section headings plus full-row buttons, with the teacher-only
start action as a filled button. Dropped the descriptive subtitle.
Home: unchanged in substance - the five ASCII blocks stay, since Home is the
navigational hub and is meant to be the fullest frame.
|
|
redrawn to the 2.22 ASCII, calendar to Example 3 + hour grid
Home (2.22, screen 1 ASCII): the frame now carries the five blocks the design
draws - date/period header, the three count chips (unsubmitted / answer needed /
confirm), TODO, today (timetable + same-day notice + deadline on one axis) and a
timeline digest with the NEW marker. Home is the navigational hub, so it is the
fullest frame. My previous pass had flattened it to two lists.
Timeline (2.22, screen 2 ASCII): reduced to what the design actually draws - one
line per post, "o <sender> <body> <attachment/badge> <time>", five samples, plus
the note that feeds are per-scope and newest-first. The previous pass had turned
each post into a card with a bordered panel, which the ASCII does not show and
which made it heavier than Home. No destination chip: the pie shows scope.
Pie (2.5.1 + the whole-frame ASCII): redrawn to the specified shape -
+-------+-------+
| 一年 | 活動 |
+-------W-------+
| 個人 |委員会 |
+-------+-------+
one circle, cross-divided into four quadrants, W in the centre, with the personal
scope pinned to a quadrant because it lives outside the organisation tree. Touching
highlights a quadrant and dims the rest; releasing commits; returning to the
centre commits whole. Only while dragging past 62 percent of the radius does the
branch extend and the concrete (level 3) nodes appear - the design says the
concrete is visible while holding and is not for browsing, so it must not be
permanently on screen. Branch and nodes are drawn outside the disc, on a canvas
sized for it.
Calendar: the month grid now passes OutDateStyle.EndOfGrid as Example 3 does.
Selecting a day opens a Google-Calendar-style day view - an hour axis from 8:00
to 19:00 filling the screen, with each event drawn as a block whose vertical
position and height come from its start time and duration, timetable and
auto-source entries together. It takes a *list* of dates, so the same composable
serves a single day or a week of columns rather than being day-only.
|
|
three levels
Palette: dark is no longer near-black. Ground 0xFF313338, surface 0xFF2B2D31,
raised 0xFF3A3C42, text white - the Discord register, per user direction ("not
that dark, about Discord"). Wine 0xFF8C2438 stays the accent on both grounds;
light keeps the wine primary on pale grey. No purple anywhere.
Icons: settings, appearance and search are drawn shapes now instead of text
glyphs - a gear (teeth + ring + hub), a moon for dark / sun with rays for light,
and a magnifier. All Canvas, no icon dependency, so nothing new has to resolve on
Wasm. The bottom-left cluster is +, appearance, gear as circular icon buttons.
Pie: it now renders the three levels 2.5.1 actually specifies (whole > top-level
group > concrete), not just the four quadrants. Structure: W in the centre is
level 1 (whole); the cross-divided quadrants are level 2, with the personal scope
pinned to one quadrant because it sits outside the organisation tree; dragging
outward past 62 percent of the radius extends a branch (the "spoke" of 2.5.1)
beyond the circle and level 3 concrete nodes appear along it - 1/2/3-kumi for the
year scope, photo/art/science clubs for activities, and so on. The concrete nodes
are only visible while holding, and release commits whichever node is nearest;
returning to the centre commits whole, which is the cancel path in 2.19. Child
lists are data (a children map) rather than hardcoded UI, matching the design's
"the school defines both the groups and the concrete scopes".
Also fixed: the appearance icon read MaterialTheme inside a DrawScope (not a
composable context) - the surface colour is now captured before the Canvas.
|
|
The UI still died at startup, now with a different symbol:
IrLinkageError: Function 'now' can not be called: No function found for symbol
'com.kizitonwose.calendar.core/now|now@kotlinx.datetime.LocalDate.Companion(
kotlin.time.Clock;kotlinx.datetime.TimeZone){}[0]'
So replacing the calendar library's YearMonth.now() with LocalDate.now() was not a
fix - it only moved the failure one level down. Every now/todayIn entry point in
this stack is declared as taking a kotlin.time.Clock, and that symbol does not
resolve in this combination (Kotlin 2.1.21 + kotlinx-datetime 0.7.1 + Compose MP
1.8.2, Wasm target). kotlin.time.Clock only becomes usable in later Kotlin
versions, and kotlinx-datetime 0.7 is built against that newer shape.
Get "today" from the platform instead: a new expect/actual todayLocalDate() with
one actual per target - JS Date on wasmJs, Clock.System.todayIn() on jvm and
android (where the Clock symbol does resolve). The calendar month is then built
as YearMonth(year, monthNumber) from that date, which also avoids the
kizitonwose Month vs kotlinx Month type clash that broke the Android compile.
Wasm js(...) has its own constraint worth recording: the call must be the single
expression forming a top-level function body, and the result must be a
primitive/string/function - so the Date reads are folded into one arrow IIFE
returning YYYYMMDD as an Int, which Kotlin then splits.
Verified: compileKotlinWasmJs now passes.
|
|
Android target compiles too
The Wasm-only fix for the calendar's YearMonth was not enough. Two further
problems showed up once the Android target compiled the same code:
1. YearMonth(today.year, today.month) failed type inference on Android:
kotlinx-datetime's Month and com.kizitonwose.calendar.core's Month are
different types.
2. The obvious repair, today.month.number, does not exist either - Month has no
`number` property in kotlinx-datetime 0.7.1.
Take the month as an Int straight off LocalDate instead: today.monthNumber is an
Int property on LocalDate (verified present in the 0.7.1 sources before use),
which sidesteps the Month type mismatch entirely and works on both targets.
Verified with both compilations in one run -
compileDebugKotlinAndroid and compileKotlinWasmJs both pass.
|
|
button-shaped search and controls
Colour (per user direction: "black background with white, and wine instead of
black"): the theme now has a dark default - ink (near-black) ground, warm off-white
text - with wine as the accent, and a light variant where the same wine becomes
the primary against a pale grey ground with white surfaces. Purple is gone
entirely. The [D/L] control in the bottom-left cluster toggles between them, and
Main.kt holds the appearance state. The wine itself was desaturated and deepened
(0xFF6E1B2E -> 0xFF8C2438 for the dark ground) so it reads as wine rather than a
flat red on either ground.
Pie: the previous attempt got this wrong. Per 2.5.1's drawing (2026-09-22, fixed)
the pie is ONE circle divided by a cross into four quadrants, with the centre
labelled W (whole); the quadrants are the top-level scopes plus the personal
scope, which is pinned as its own quadrant because it sits outside the
organisation tree. The implementation had instead lined up three separate circles
beside the centre, which is not the specified shape at all. 2.19 also fixes the
interaction: there is no "open" gesture (touch and drag), the centre is always
whole regardless of where you press, the touched quadrant highlights while the
others dim, release commits, dragging outward expands to the concrete children
with a spoke reaching outside the circle, and returning to the centre cancels to
whole. PieMenu.kt now draws that single cross-divided circle as an icon (Canvas
arcs, dimmed/selected states, W in the centre) and tracks drag position to
quadrant, committing on release. The outward expansion to concrete children and
the 150-200ms expand / 100ms contract animation from 2.19 are the next step and
are marked as such in the file.
Chrome: search and the bottom-left controls are buttons now instead of bracketed
text - a rounded search button with a glyph, and circular +, D/L, gear buttons.
The narrow form's FAB is the same circular button.
Also fixed here: CalendarFrame no longer calls YearMonth.now(). The Wasm build
compiled it but the runtime died on load with
IrLinkageError: Function 'now' can not be called: No function found for symbol
'com.kizitonwose.calendar.core/YearMonth.Companion.now'
The symbol requires kotlin.time.Clock, which this Kotlin version does not resolve
for that library's binary, so the calendar library''s now() is unusable here;
building the YearMonth from LocalDate.now() gives the same value without it.
|