Janela Publishes What a Theme May Target, and Vitral Is Only One
Context
ADR 023 split the styling in two: janela.css is structure, frozen and
boring, and vitral.css is taste, free to move. That was the right cut
and it has held for the theme. It no longer describes the other file.
What janela.css actually contains
Counted by where each name is used, in the engine’s own views and in the demo standing in for a host:
| Group | Names | Where they appear |
|---|---|---|
| The grid scale | janela-cols-1..12, janela-gap-0..8, janela-span-1..12 |
chosen by a frame’s own integers (ADR 016) |
| Pane primitives | janela-frame, janela-pane, janela-value, janela-value-label, janela-value-number, janela-chart, janela-empty, janela-error |
in a host’s own pages, wherever a pane is rendered |
| The engine’s own chrome | janela-card, janela-button, janela-crumb, janela-flash, janela-page, janela-form, janela-field, janela-heading, janela-subheading, janela-list, janela-list-row, janela-list-name, janela-actions, janela-exit, janela-hint, janela-muted, janela-danger, janela-errors |
only Janela’s own pages: 2 to 6 engine views each, and none in the demo but janela-form, twice |
The third group is a small UI kit for pages a host may never visit. ADR
016 said “the class names are public API”, meaning renaming one is
breaking and belongs in UPGRADING.md. Applied to the third group that
is a promise made to nobody, about markup only the engine renders, which
makes the engine’s own pages harder to change than the thing the gem is
for.
A theme other than vitral already works
Janela.theme is one line in the engine’s layout,
stylesheet_link_tag Janela.theme if Janela.theme.present?, so it
resolves any stylesheet name against the host’s own asset paths. Measured
against Janela’s own layout:
"vitral" -> janela.css + vitral.css
"application" -> janela.css + the host's own application.css
"no_such_theme" -> raises, "The asset 'no_such_theme.css' was not found in the load path"
nil -> janela.css alone
So somebody else’s skin needs nothing built. What it needs is to know
what it may target, and today nothing says. A theme author reads
vitral.css to find out, which makes every name vitral happens to use
into an accidental interface, and makes the three groups above
indistinguishable.
Four open issues are the same question
#26 a caption that cannot be hidden without losing it for screen
readers, #29 inter-pane layout being entirely the host’s problem, #30
a categorical palette, #31 a single value pane with no typography and
no way to say how prominent it is. Each one asks where a piece of look
belongs, and each has been answered one at a time so far, which is how a
split stops meaning anything: janela.css grew a UI kit that way,
without anyone deciding it should.
Options considered
Leave it, and decide each issue as it comes. What has been happening. It produced the drift above and gives a theme author nothing to read.
A third stylesheet, splitting the engine’s own chrome out of
janela.css. Tempting and rejected on ADR 023’s own consequence: a third
file is a third thing every Sprockets host has to declare for
precompilation, and #14 says the second one is already a rough edge. The
problem is a missing promise, not a missing file.
Rename the chrome to mark it private, janela-internal-card or
similar. Rejected: the rename is itself the breaking change it is trying
to make unnecessary, paid now for a benefit that is only cosmetic. Saying
which names are the contract costs nothing and is just as clear.
Move the pane primitives into the theme, so a host with no theme gets unstyled panes. Rejected: a pane has to be legible on install, which is ADR 016’s founding reason, and #26 is the sharp case, since hiding a caption accessibly is correctness rather than taste and a host that never opts into a theme still needs it.
Decision
Janela publishes the hooks; a theme supplies the taste. What a theme may target is written down, and what is not written down is Janela’s own chrome.
Three layers, named, in two files. No new stylesheet.
- The grid scale and the pane primitives are the contract. These are
what a host’s own pages contain and what a theme targets. Renaming one
is breaking and goes in
UPGRADING.mdwith the doctor taught to find the old name (ADR 015). - The engine’s own chrome is not the contract. The classes only Janela’s own views render may change in any release. A host restyling Janela’s own pages is welcome to target them and should expect to revisit it, which is the honest version of what was already true.
- ADR 016’s “the class names are public API” is narrowed, not superseded: it is true of everything a host’s markup contains, and was never meant as a promise about the engine’s breadcrumbs.
docs/theming.md carries the contract, because a theme author is not
going to read an ADR to find a class list. It names the two layers, every
class in them, every custom property, and what a theme is expected to
leave alone. Shipped in the gem beside the other docs.
A theme is any stylesheet the host names, and vitral is one of them.
Nothing more is built for this: Janela.theme = "midnight" already
resolves against the host’s asset paths, and a name that does not resolve
already raises rather than failing quietly. What changes is that this is
documented and supported rather than merely true, so a third party can
publish a theme against a contract instead of against whatever vitral
happens to do this month.
Where a new piece of look goes. The rule that answers the four open issues without arguing each one separately:
- Something a pane needs in order to be correct or legible on install is
a hook, and belongs in
janela.csson the contract. A caption that is announced but not seen is this (#26). - Something that is a choice about appearance is taste, and belongs in a theme. A categorical palette is this (#30).
- Something a host arranges is layout, and belongs in the grid scale as an integer that selects a rule, never as a value interpolated into a style attribute (ADR 016). Prominence for a single value pane is this (#31), and so is inter-pane layout (#29).
Where a feature has both halves, the hook ships in janela.css and the
taste in the theme. A palette is the clean example: the class that marks
which series a mark belongs to is a hook, and the colours it resolves to
are a theme’s.
Vitral stays in this repository, and stays one theme among any others. It is the reference implementation of the contract, which is a reason to keep it here rather than to privilege it: if a rule cannot be written against the published names, the contract is wrong.
Consequences
- A theme author has something to read, and a theme written against
docs/theming.mdkeeps working across releases in a way one written againstvitral.cssnever could. - The engine’s own pages get easier to change, because their markup stops
being an interface. That is a real loosening: a host that today targets
janela-cardwill find it moves one day, and the doc says so rather than leaving them to discover it. - Writing the contract down means reading every class in both files once and deciding which side it is on. That is the work, and it is the point.
- Not decided here: whether Vitral grows to answer #29, #30 and #31. This says where each half of each of those belongs; it does not commit to building them, and a component library large enough to lay out a host’s page is its own decision against ADR 001’s preference for staying small enough to fork.
- A theme is still linked only into Janela’s own layout. A host wanting the look on its own pages links the stylesheet itself and uses the theme’s public classes, exactly as ADR 023 decided. The contract does not change that asymmetry, it explains it.
- What would change this decision: the engine’s own chrome turning out to be something hosts genuinely restyle, in reports rather than in anticipation, at which point it has earned the promise this declines to make and should be moved onto the contract deliberately.