feat: component library, validation and Shell layout #2

Merged
cartopolis-agent merged 3 commits from feat/components into main 2026-10-09 17:47:24 +00:00
Collaborator

Adds three things to the fork, each with its own commit:

  • Component library — @garage44/mithril/components and components.css: form fields and elements that all take their state as a model signal, with a light/dark theme built on light-dark() tokens and Bun's CSS bundling.
  • Validation — @garage44/mithril/validation: validation($signal, [rules]), chainable, gathered into a $v group with $valid / $validate(); fields take it as a validation prop.
  • Shell layout — Shell, NavGroup, NavItem: nav, main and context panels on one grid that responds to its own width through container queries.

A sandbox in examples/components shows all of it (bun run dev). Notes in docs/notes/components.md and docs/notes/validation.md.

bun run lint and bun test (997 pass) are green. Rendered in headless Chrome only.

Adds three things to the fork, each with its own commit: - **Component library** — `@garage44/mithril/components` and `components.css`: form fields and elements that all take their state as a `model` signal, with a light/dark theme built on `light-dark()` tokens and Bun's CSS bundling. - **Validation** — `@garage44/mithril/validation`: `validation($signal, [rules])`, chainable, gathered into a `$v` group with `$valid` / `$validate()`; fields take it as a `validation` prop. - **Shell layout** — `Shell`, `NavGroup`, `NavItem`: nav, main and context panels on one grid that responds to its own width through container queries. A sandbox in `examples/components` shows all of it (`bun run dev`). Notes in `docs/notes/components.md` and `docs/notes/validation.md`. `bun run lint` and `bun test` (997 pass) are green. Rendered in headless Chrome only.
`@garage44/mithril/components` and `@garage44/mithril/components.css`: form
fields (text, textarea, number, checkbox, switch, select, radio, segmented,
slider, tags) and elements (button, badge, spinner, progress, alert, card,
table, tabs, collapse, dialog, toasts, theme switch), with a sandbox in
examples/components that shows each one beside its model's live value.

Every stateful component takes one `model` prop, a signal (`state.$key` or
`signal()`): the view reads it and input writes it. That is the same shape for
a field's value, the active tab, a dialog's open state and a table's sort, so
an app drives any of them by writing a signal. A value/oninput pair was the
alternative and lost because every call site repeats the handler and the two
halves can disagree.

The components are `m()` rather than TSX, because a .tsx file in a package is
compiled with the consumer's JSX settings, and they are MithrilComponent
classes because TypeScript rejects a plain object as a TSX element type.

The theme is one `light-dark()` pair per token; `data-theme` pins either half
on the document or any subtree. The tokens are redeclared on `[data-theme]`
because Bun lowers `light-dark()` to toggle variables that resolve where they
are declared, so a pinned dark panel in a light page otherwise came out light.
Contrast pairs were measured, and two light status tones darkened to clear
4.5:1. docs/notes/components.md has the rest.
`@garage44/mithril/validation`: `validation($signal, [required, email])`
holds one model's rules and chains more (`.minLength(3).maxLength(20)`);
`validations({...})` gathers a form's into `$v`, whose `$valid`,
`$validate()`, `$reset()` and `$errors` act on all of them. Rules: required,
email, url, minLength, maxLength, min, max, integer, pattern, oneOf, matches,
minItems, maxItems and check, with replaceable default messages.

Every field takes an entry as its `validation` prop, which replaces the
`validate` callback added with the components: whether a field was touched
now lives on the validation as a signal, so `$v.$validate()` at submit can
show every field's error at once, and a submit button bound to `$v.$valid`
redraws as the form fills in. A `required` rule marks the field required.

Nothing is cached: an error is recomputed on each read, which is what makes a
view reading `$valid` subscribe to every model, including the other model a
`matches` rule reads. Chained methods are typed by value, so `.email()` on a
number model is a compile error. Lengths count graphemes rather than code
points, so an emoji is one character. The sandbox gains a sign-up form;
docs/notes/validation.md has the rest.
feat(components): add a Shell layout with nav, main and context panels
All checks were successful
Test / test (pull_request) Successful in 21s
a58838514b
`Shell` is the application frame of a chat client or an admin tool:
navigation on the left (`NavGroup`, `NavItem`), the main column with a header,
its own scrolling content and a footer, and a context panel on the right for
what main has selected, with a footer for a form's Save and Cancel.

It is one CSS grid that answers to its own width through container queries,
not to the window's: three columns from 1100 px, the context sliding over main
from 720, and below that main alone with nav and context as drawers. A media
query would only ever see the window, so a shell embedded in a narrower box,
as the sandbox's is, would lay itself out for the wrong width.

Open state and panel widths are models like every other component's state:
`navOpen`, `contextOpen`, and `navWidth`/`contextWidth`, which make the edges
draggable by pointer or keyboard. Escape, the scrim and the context's close
button write false. The sandbox's frame switches between phone, tablet and
card width, and `?shell` shows the demo as a whole page.

The sandbox's tsconfig named `m.fragment` as the JSX fragment factory, copied
from the state example; Bun then compiled `<>` to a component call that throws
at mount. It is `m.Fragment`, as the root tsconfig has it.
cartopolis-agent deleted branch feat/components 2026-10-09 17:47:24 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jeroen/mithril!2
No description provided.