Skip to content

Various UI changes - #88

Merged
randomdevpete merged 30 commits into
masterfrom
task-671-various-ui-changes
Aug 18, 2026
Merged

Various UI changes#88
randomdevpete merged 30 commits into
masterfrom
task-671-various-ui-changes

Conversation

@randomdevpete

Copy link
Copy Markdown
Owner

Interactive-session ticket (TODOS-671). Changes land one commit at a time as they're accepted live; description below grows with the session.

Changes so far

  • README: the useNavigate example's searchText now lives in a jotai atom instead of React's useState, matching the rest of the library's model.

The useNavigate example sat in a jotai-based routing library but reached for
React's own useState for the input's text. Use a plain jotai atom instead so
the example is consistent with the rest of the library's model.
…ICENSE.md

Copyright still read Downplay Ltd (the pre-transfer org) with a stale
2017-2018 range, in the root README/LICENSE and both packages'. Update to
Randomdev Ltd, 2017-2026, and rename each LICENSE to LICENSE.md so the
README's mention of it is a real link rather than plain text.
vertical-align: 0.05em over-corrected for input-mono's lower baseline vs
alegreya-sans, leaving inline code visibly high. -0.03em confirmed live
against the running page.
The tagline now starts under the small-caps 'arl' of the 'Jarl' heading
(measured live against the rendered wordmark, 4.72rem) instead of the page
margin. 'atomic' cycles per-letter between the brand's red and gold, a
2.4s round trip staggered 0.15s per letter so it reads as a pulse sweeping
the word rather than a flicker.

Both confirmed live in the shared browser: the indent within 1.4px of the
heading, and the animation's computed color sampled mid-cycle at both ends
of the range.
Switch, Route and Link are all real jarl-react exports (index.ts re-exports
each), and Switch's on prop takes a RouteAtom, not a string - the README's
old 'there's no <Switch/> component' line was simply out of date.

Separately, the tagline sat ~21px below the heading from the h1's default
bottom margin collapsing through the wrapper div. Made both margins
explicit so it reads as a subtitle directly under the wordmark instead of
a separate paragraph.
…-badges

Heading margin-bottom 0.35rem -> 0.15rem; tagline margin-bottom 0 (default) -> 1.5rem.
Confirmed live: 2.4px heading-to-strap, 24px strap-to-badges.
marked wasn't generating heading id attributes, so nothing was linkable yet.
Added a small GitHub-style slugifier to the heading renderer (dedup handled
per-document via a Map cleared before each parse) rather than pulling in an
extension package for it - matches the existing custom code renderer's
approach. Confirmed #switch/#route/#link resolve and scroll correctly on
both API pages, and that headings across the site still get unique ids.
Link's to prop is optional (LinkProps<T>.to?: T); passing an empty object
for params-less routes was noise. productRoute already demonstrates the
real params-passing case just below.
… jarl-atoms import

The opener still pitched against a manually-managed isLoading flag; reframe
it around the layout-churn cost of scattered component-level loading states
and suspense fallbacks instead, which is the actual pain this guide solves.
Kept the Suspense mention accurate - the example just below still uses one,
now framed as a single boundary rather than per-component ones.

Also collapsed the routes.ts example's two separate jarl-atoms imports
(staticRouteAtom/paramRouteAtom, then resolvedAtom) into one.
conventional-commits-parser's note-keyword matching is looser than the
Conventional Commits spec's 'footer only' rule: commit 25a5aa3's body
contained the plain-English sentence 'alongside the existing
breaking-change suppression' mid-paragraph, no colon, not a real footer,
and the parser swept it (plus the trailing Co-Authored-By line) into the
release notes as a fabricated BREAKING CHANGES entry.

What it described was also stale even if it had been real: the very next
commit (2db21b1) abandoned that reset-to-2.0.1 plan and continued from
2.5.0 instead, once it turned out npm's dependent-check and dist-tags
made the rollback impossible. Nothing about jarl's public API actually
broke in 2.6.0 - every other entry in the release is an internal
release-process change. Safe to hand-edit: @semantic-release/changelog
only ever prepends new releases, it never rewrites past entries.
…px letter-spacing

Alegreya Sans reads narrow/condensed at body sizes and no wider cut is
served by the Adobe kit; a size + letter-spacing bump on it barely helped.
Source Sans 3 was already loaded as a documented Google Fonts fallback, so
no new network cost. Compared live in the shared browser before committing.
Headings (Alverata/Cinzel) are untouched.
New copy: what a route atom actually is (a link up to rootAtom, matching
a URL piece, current-match + reverse-URL), how routing decisions decompose
to plain switch/if logic, and navigation via calling the atom setter directly.
Links rootAtom to its API reference, matching the Switch/Route/Link
convention already in place just below it. Confirmed live against the
running page, all five links checked.
The /to/ styling was informal shorthand for nested emphasis, not literal
slashes. **bold with _italic_ nested** avoids the same-delimiter ambiguity
markdown has with **bold with *italic* nested**. Confirmed live: renders
as <strong>how to build a URL <em>to</em> that route</strong>.
… setters

rows and activeSort (parsed sort key/direction) are now derived jotai atoms
read off the filter route atom, not values recomputed with useMemo inside
the component. Navigation drops jarl-react's useNavigate wrapper in favor
of jotai's own useSetAtom directly on the route atom - useNavigate is just
that call anyway (jarl-react/src/hooks.ts:26-29).

filterInput is a separate, un-navigated atom holding only the live input
text; typing never touches the URL, and Enter/Search commits it into the
filter route atom's setter. A small effect resyncs the input when the URL
changes some other way (back/forward, a shared link), so the field doesn't
go stale - that isn't live search, just correctness.

Verified interactively in the shared browser: typing leaves the URL/rows
untouched, Enter commits and filters to one row, sort toggling preserves
the filter, and back-navigation resyncs the input and restores all rows.
…eAtom sort

Grid and DataGridApp collapse into one component: both query params are
optional, so the route always matches, and <Route>'s function-as-child was
pure ceremony around atoms the component already had direct access to -
now it just reads them.

activeSort is transformRouteAtom over sort, reshaping the raw query value
into {key, direction} and back. filter chains off activeSort rather than
sort directly, so filter's own values carry the already-parsed sort
alongside the filter text - one atom holding everything the grid needs.
Both writes (sort toggle, filter commit) go through that single atom and
each naturally preserves the other field, since the value it doesn't
change is just read back out of the current match. This is the chain-of-
state-to-the-route's-tip shape jarl is meant to be used in.

Verified interactively: sort survives a filter commit and vice versa
(?sort=price&filter=tools -> toggle -> ?sort=-price&filter=tools, rows
correctly Tools-only and price-descending), zero console errors.
The block comment above createGridRoutes explained the whole down/up flow
in one place, disconnected from the code it described - readable once,
useless as a reference while reading the function. Split it: each atom
gets one or two lines right next to it (why sort is memoised off root,
what activeSort's getter/setter each do, why filter chains off activeSort
and not sort, why rows needs no useMemo, why filterInput stays local).
Only the memoisation note stays above the function, since it's about the
factory as a whole rather than any one atom in the chain.
…lue/setter hooks

useAtomValue + useSetAtom on the same atom were two separate hook calls
(and two subscriptions) for state that's read and written together
throughout the component. useAtom(routes.filter) -> [filter, setFilter]
is the idiomatic single call; every values? reference becomes filter.values?.

Verified live: sort survives a filter commit (?sort=price&filter=iron),
rows correctly Iron-only and price-ascending.
…d field lists

Dropped the sortKey/direction destructure - every read is filter.values?.key
/filter.values?.direction now, consistent with how filter.values?.filter was
already accessed. setFilter calls spread {...filter.values} and override
just what changed, instead of repeating all three fields each call; a small
defaults object (parseSort(undefined) plus filter: undefined) keeps the
spread's type whole for the case the route doesn't match, which is real to
the type even though it never actually happens here.

Also clarified the factory comment: the rootAtom-taking factory only exists
because this demo harness mounts at any root - a real app would just
declare these as static atoms.

Verified live: sort toggling still works (?sort=stock, arrow renders),
typecheck/lint/format all clean.
The Table styled-component was the one piece of DataGridApp.tsx that had
nothing to do with the demo's routing/state story - pure CSS taking up a
third of the file's vertical space. Split it into DataGridTable.tsx.

In its place, moved SortKey/SortDirection/sortColumns/parseSort/
stringifySort/filterWares/sortWares in from wares.ts - the logic that
actually explains what the atoms are doing. wares.ts is now just the
Ware type and the fixed inventory data, the one piece nobody needs to
read to understand the demo.

Confirmed no other consumer of the moved sort logic exists in the repo.
Verified live: sort toggling still works (?sort=category), zero console
errors, and the demo's 'View source' panel no longer shows any CSS - just
the story.
It doesn't depend on root at all - atom("") is the same regardless of
which URL prefix the demo mounts under - so it doesn't belong inside
createGridRoutes alongside the atoms that genuinely chain off root.
Simplifies the factory's return shape to just {filter, rows}.

Verified live: filter still commits on Enter (?sort=name&filter=wool),
zero console errors.
…ilename heading linked to GitHub

'View source' was a <details> disclosure defaulting closed, but the source
is the point of a demo - readers always want it. Replaced with a plain h2
showing the file's own name, linked to its blob on GitHub master.

New sourcePath prop on DemoPage (repo-relative, e.g.
'packages/docs/src/demos/DataGridApp.tsx') supplies both the displayed
filename and the link target; all four demo pages updated. githubRepoUrl
exported from Layout.tsx as the single source of truth for the repo URL
rather than duplicating it.

Verified live across all four demo pages: correct filename heading, correct
GitHub link, source always visible with no accordion, zero console errors.
…reads defaultSort directly

Two small follow-ups on the manual tuning pass: toggleSort's flip check
still read filter.values?.key/direction directly instead of the
currentFilter it uses everywhere else - now consistent, one source of
truth. defaultFilter spread parseSort(undefined) to get its key/direction,
which just returns defaultSort by construction - now says that directly
instead of relying on the two happening to agree.

Verified live: sort toggling still correct, zero console errors.
@randomdevpete
randomdevpete marked this pull request as ready for review August 18, 2026 03:17
@randomdevpete
randomdevpete merged commit f2ffefa into master Aug 18, 2026
5 checks passed
@randomdevpete
randomdevpete deleted the task-671-various-ui-changes branch August 18, 2026 03:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant