janela 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,172 @@
1
+ ---
2
+ Date: 2026-09-23
3
+ Status: Accepted
4
+ Related: ADR 001, ADR 010, ADR 015, ADR 021
5
+ Triggers:
6
+ - deciding whether an open issue belongs in the next release
7
+ - proposing a feature, or arguing that one is out of scope
8
+ - cutting a release and choosing the version number
9
+ - deciding whether a breaking change is still affordable
10
+ - wondering whether the project is drifting
11
+ Topics: vision, scope, forkability, releases, roadmap, planning
12
+ ---
13
+
14
+ # ADR 037: 1.0 Means the Surface Stops Moving, Not That Janela Is Finished
15
+
16
+ ## Context
17
+
18
+ Janela has a vision and a backlog and nothing in between. ADR 001 says
19
+ what the project is, the load-bearing 5% of a BI tool, and the README's
20
+ Design section lists what it will never be: natural-language query, a
21
+ warehouse, a row-level-security subsystem, a scheduling UI, an embedding
22
+ SDK, a mobile app, paginated reports. That is a stronger statement of
23
+ scope than most libraries manage. What neither says is what order things
24
+ happen in, or what has to be true before a host can build on this
25
+ without expecting the ground to move.
26
+
27
+ The gap shows up in three measurements taken today.
28
+
29
+ **The surface moves every release.** Counting entries marked breaking in
30
+ `CHANGELOG.md`: 0.5.0 carried one, 0.7.0 carried two, and the unreleased
31
+ section carries two more. Five breaking changes across four releases in
32
+ five days, every one of them correct and every one of them announced the
33
+ way ADR 015 requires. This is what alpha is for and there is nothing to
34
+ apologise for in it. It also means nobody outside this repository can
35
+ build anything on Janela yet, because the DSL, the helpers, the pane
36
+ URLs and the class names a theme targets have all changed underneath a
37
+ host at least once.
38
+
39
+ **The declared horizon ran out.** The `Before public release` milestone
40
+ stands at ten closed and one open. It worked, it is nearly spent, and
41
+ nothing replaced it. Thirteen of the fourteen open issues carry no
42
+ milestone at all.
43
+
44
+ **The issue list cannot rank itself.** Eight of those issues are
45
+ enhancements, four of them raised from a real install, and none has been
46
+ picked up while the last four releases went to hardening. That looked
47
+ like drift and is worth being precise about, because it was not. ADRs
48
+ 025, 032, 034 and 035 are one argument made four times: Janela refuses
49
+ rather than guesses about scope. Bounded predicates, a refused unscoped
50
+ read, a refused unnamed snapshot scope, and a doctor that reports what it
51
+ actually saw. That is the most coherent stretch of work in the project.
52
+ The problem is that the theme was never declared, so it was invisible
53
+ while it was happening, and now that it is finished there is nothing
54
+ declared to follow it. A flat list of fourteen issues is what a project
55
+ looks like between themes when it has no way of saying so.
56
+
57
+ Three options were considered.
58
+
59
+ **A roadmap with dates or quarters** was rejected. This project's
60
+ throughput is not predictable enough to commit to a calendar, and a
61
+ published roadmap that slips teaches people to stop reading it. The cost
62
+ is ongoing and lands every release; the benefit is mostly the appearance
63
+ of planning.
64
+
65
+ **Leaving it to the issue list** was rejected because that is the current
66
+ state, and the current state is what prompted the question. Labels
67
+ describe what an issue is. Nothing describes what it is for.
68
+
69
+ **Declaring 1.0 feature-complete** against what a commercial BI tool ships
70
+ was rejected for contradicting ADR 001 outright. Janela is not trying to
71
+ reach parity with anything. A 1.0 defined as "the features are all there"
72
+ has no end, because the list it is measured against is somebody else's.
73
+
74
+ ## Decision
75
+
76
+ **1.0 means the public surface stops moving, the 5% can be read, and the
77
+ doctor can be trusted. It does not mean Janela is finished, and it is not
78
+ a feature count.**
79
+
80
+ Three claims, each of which can be checked rather than argued about.
81
+
82
+ **The public surface stops moving.** After 1.0, everything
83
+ `docs/theming.md` lists as the contract, the measures and dimensions DSL,
84
+ `janela_pane` and `janela_frame`, the pane URL shape and the dashboard
85
+ filter parameters change only on a major version. Before 1.0 they may
86
+ change in any release, with the upgrade note ADR 015 requires. This is
87
+ the load-bearing claim and the other two exist to serve it: a surface is
88
+ only worth freezing once it is the right shape, which is why the feature
89
+ work below sits inside 1.0 rather than after it. Freezing an API that is
90
+ missing a limb only guarantees the limb arrives as a breaking change
91
+ later.
92
+
93
+ **The 5% can be read.** Janela draws three renderers, `table`, `bar` and
94
+ `line`. A part-to-whole split has to be drawn as two bars. A table pane
95
+ carries exactly two cells per row, so a label that needs context to be
96
+ understood cannot have any. A chart takes Chart.js's 2:1 default and a
97
+ host cannot correct it from outside. None of these is a feature beyond
98
+ the 5%; each is the 5% not finished, and the distinguishing test is that
99
+ four of them were raised by someone trying to use the gem rather than by
100
+ someone reading its source. The theme that follows "Janela refuses rather
101
+ than guesses" is legibility: a pane a person can actually read.
102
+
103
+ **The doctor can be trusted.** ADR 021 holds that a check people stop
104
+ trusting has stopped working. Every check in `CHECKS` bar two now has a
105
+ test that makes it fire and one that leaves it quiet.
106
+
107
+ The open issues sort as follows, and this sort is the substance of the
108
+ decision rather than an illustration of it.
109
+
110
+ In 1.0: #14 (a Sprockets host serves the engine's JavaScript or is told
111
+ it cannot), #53 (the last two untested checks), #27 (a ratio measure, so
112
+ the refusal in #20 offers somewhere to go), #30 (doughnut and pie, and
113
+ the categorical palette that makes them readable), #31 (a pane says how
114
+ prominent it is), #34 (a table carries an attribute column beside its
115
+ label), #24 (a chart's height is the host's to set), and #6 (how
116
+ contributions are accepted, who cuts a release, where to report a
117
+ vulnerability privately).
118
+
119
+ After 1.0: #18 (drill-down on time panes, which ADR 006 excluded
120
+ deliberately and which needs an ADR of its own before any code), #41 (a
121
+ command palette, which the issue itself argues belongs to the demo and
122
+ not the gem), and #45 (a cosmetic scroll drift on one demo page, where
123
+ the issue already allows that deciding not to fix it is a legitimate
124
+ answer).
125
+
126
+ Neither, and tracked as itself: #5 is an upstream pin on ActiveSupport
127
+ and is removed when ActiveSupport is fixed. #56 is a one-in-160 test
128
+ flake deliberately recorded rather than chased, and is evidence, not
129
+ work.
130
+
131
+ Needing a decision rather than a place: #29 asks Janela to help arrange
132
+ panes on a page. ADR 011 has panes not rendering in the host's layout and
133
+ ADR 001 prefers a fork to a configuration surface, so the default answer
134
+ is that arrangement belongs to the host and the issue closes into the
135
+ README's out-of-scope list. That is a scope decision and gets argued on
136
+ its own rather than settled here by omission.
137
+
138
+ **The roadmap is published rather than kept in the issue tracker.**
139
+ `docs/roadmap.md` ships in the gem and renders on the demo at
140
+ `/docs/roadmap`, because the demo reads the gem's own markdown rather
141
+ than restating it (ADR 010). One file is therefore the public statement,
142
+ the page a visitor reads and the copy in the installed gem at once. The
143
+ GitHub milestone remains the working tracker and the progress bar; it
144
+ says which issues, while the document says what the release is for. A
145
+ reader who has to open an issue tracker to find out where a library is
146
+ going has been told to do the maintainer's filing.
147
+
148
+ ## Consequences
149
+
150
+ - A breaking change is cheap now and expensive after 1.0. Anything that
151
+ wants to change the DSL, the helpers or the published class names
152
+ should be argued for before 1.0 rather than after, and the sort above
153
+ is deliberately biased that way.
154
+ - An issue outside 1.0 is not rejected. It is not blocking a release,
155
+ which is a different and weaker statement, and the roadmap says so in
156
+ those words so that nobody reads the list as a refusal.
157
+ - The eight issues in 1.0 are a commitment made in public. That is the
158
+ point of publishing it and also its only real cost: a roadmap on the
159
+ website can be held against the project in a way a milestone cannot.
160
+ - The current release is 0.8.0 and not 1.0, which this ADR answers
161
+ without further argument, because it carries two breaking changes.
162
+ - `docs/roadmap.md` has to be updated when a release lands, or it becomes
163
+ the stale artifact this ADR rejected dated roadmaps for being. It is
164
+ named in `/jan-release` for that reason.
165
+ - The README's Design section currently says snapshots are "not built
166
+ yet" and they shipped in 0.7.0. Publishing a roadmap beside a stale
167
+ status line makes the staleness worse, so that sentence is corrected in
168
+ the same change.
169
+ - What would change this decision: a real install blocked by something in
170
+ the after-1.0 list. The sort is a judgement about what a host needs to
171
+ read a dashboard, and a host saying otherwise is better evidence than
172
+ the judgement.
@@ -20,26 +20,28 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
20
20
 
21
21
  | Topic | ADRs |
22
22
  |-------|------|
23
- | **Vision, scope, forkability** | 001, 010, 012 |
24
- | **Open-source & host-decoupling** | 001, 022 |
23
+ | **Vision, scope, forkability** | 001, 010, 012, 037 |
24
+ | **Open-source & host-decoupling** | 001, 022, 036 |
25
25
  | **DSL & query layer** | 002, 006, 007, 020, 025 |
26
26
  | **Dependencies** | 002, 003, 004, 006, 017, 025 |
27
- | **Authorisation** | 002, 003, 004, 009, 017, 019, 022, 032, 033, 034 |
27
+ | **Authorisation** | 002, 003, 004, 009, 017, 019, 022, 032, 033, 034, 035 |
28
28
  | **Performance & storage** | 007, 017, 025 |
29
29
  | **Cross-filtering & Hotwire** | 003, 004, 005, 008, 024, 025 |
30
30
  | **Layouts & views** | 011, 012, 016, 018, 020, 027 |
31
- | **CSS & styling** | 016, 018, 023, 026, 027 |
31
+ | **CSS & styling** | 016, 018, 023, 026, 027, 036 |
32
32
  | **Frames, panes & persistence** | 012, 013, 014, 019, 029, 030, 033 |
33
- | **Naming rule** | 014, 023 |
33
+ | **Naming rule** | 014, 023, 036 |
34
34
  | **JavaScript delivery & charts** | 004, 006, 026 |
35
35
  | **Time dimensions** | 006, 025 |
36
36
  | **Routes, URLs & naming** | 005, 007, 008, 009, 011, 013, 022, 024, 025 |
37
37
  | **Snapshots & publishing** | 009, 020, 028, 033, 034 |
38
38
  | **AI agents & guidance** | 010, 015, 021 |
39
- | **Releases & upgrades** | 015, 021, 032, 034 |
39
+ | **The doctor & checks** | 021, 025, 032, 033, 035 |
40
+ | **Releases & upgrades** | 015, 021, 032, 034, 035, 036, 037 |
40
41
  | **Accessibility & keyboard** | 024 |
41
- | **Security** | 003, 025, 028, 031, 032, 034 |
42
+ | **Security** | 003, 025, 028, 031, 032, 034, 035 |
42
43
  | **Testing** | 003 |
44
+ | **Roadmap & planning** | 001, 037 |
43
45
 
44
46
  ## Chronological
45
47
 
@@ -79,7 +81,10 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
79
81
  | 032 | Janela Will Not Read a Model It Cannot Scope | 2026-09-20 | Accepted |
80
82
  | 033 | A Snapshot Is Told Who Owns It | 2026-09-21 | Accepted |
81
83
  | 034 | Janela Will Not Freeze a Scope the Host Has Not Named | 2026-09-21 | Accepted |
84
+ | 035 | A Check Does What Janela Does, or It Says What It Saw | 2026-09-22 | Accepted |
85
+ | 036 | Janela Publishes What a Theme May Target, and Vitral Is Only One | 2026-09-22 | Accepted |
86
+ | 037 | 1.0 Means the Surface Stops Moving, Not That Janela Is Finished | 2026-09-23 | Accepted |
82
87
 
83
88
  ## Next number
84
89
 
85
- Next ADR: 035
90
+ Next ADR: 038
data/docs/roadmap.md ADDED
@@ -0,0 +1,97 @@
1
+ ---
2
+ Topics: roadmap, releases, scope, planning
3
+ ---
4
+
5
+ # Where Janela Is Going
6
+
7
+ Janela is alpha. It works, it is tested against a real Rails application
8
+ in a real browser, and its public surface has changed in three of the
9
+ last four releases. This page says what has to be true before that stops,
10
+ what is in the next release, and what is deliberately not coming. The
11
+ reasoning behind it is ADR 037.
12
+
13
+ ## What 1.0 means
14
+
15
+ Not that Janela is finished. ADR 001 commits the project to the
16
+ load-bearing 5% of a BI tool and to staying small enough to fork, so a
17
+ 1.0 measured against what a commercial tool ships would never arrive.
18
+
19
+ 1.0 is three claims you can check.
20
+
21
+ **The public surface stops moving.** Everything listed as the contract in
22
+ [Theming Janela](theming), the measures and dimensions DSL, `janela_pane`
23
+ and `janela_frame`, the pane URL shape and the dashboard filter
24
+ parameters change only on a major version after 1.0. Until then they can
25
+ change in any release, and every change of that kind carries an entry in
26
+ `UPGRADING.md`.
27
+
28
+ **A pane can be read.** Janela draws tables, bars and lines. A
29
+ part-to-whole split currently has to be drawn as bars, a table row cannot
30
+ carry context beside its label, and a chart takes Chart.js's default
31
+ proportions whether or not they suit the page. Those are not extra
32
+ features. They are the 5% not finished, and most of them were found by
33
+ people installing the gem rather than reading it.
34
+
35
+ **The doctor can be trusted.** `rails janela:doctor` checks an
36
+ installation for the mistakes that produce a dashboard showing numbers
37
+ nobody should see. Two of its checks have no test that makes them fire.
38
+ A check nobody can prove is working is a check nobody should rely on.
39
+
40
+ ## In 1.0
41
+
42
+ | Issue | What |
43
+ | --- | --- |
44
+ | [#24](https://github.com/retail-tasker/janela/issues/24) | A chart's height and aspect ratio are the host's to set |
45
+ | [#27](https://github.com/retail-tasker/janela/issues/27) | A ratio measure, so refusing `average:` over a boolean offers somewhere to go |
46
+ | [#30](https://github.com/retail-tasker/janela/issues/30) | Doughnut and pie, and a categorical palette that makes them readable |
47
+ | [#31](https://github.com/retail-tasker/janela/issues/31) | A pane says how prominent it is |
48
+ | [#34](https://github.com/retail-tasker/janela/issues/34) | A table pane carries an attribute column beside its label |
49
+ | [#53](https://github.com/retail-tasker/janela/issues/53) | The last two doctor checks get tests |
50
+ | [#14](https://github.com/retail-tasker/janela/issues/14) | A Sprockets host serves the engine's JavaScript, or is told it cannot |
51
+ | [#6](https://github.com/retail-tasker/janela/issues/6) | How contributions are accepted, who cuts a release, where to report a vulnerability |
52
+
53
+ Progress is tracked on the
54
+ [1.0 milestone](https://github.com/retail-tasker/janela/milestone/2).
55
+
56
+ ## After 1.0
57
+
58
+ Wanted, not blocking a stable release. Being on this list is not a
59
+ refusal.
60
+
61
+ - **Drill-down on time panes** ([#18](https://github.com/retail-tasker/janela/issues/18)).
62
+ Clicking a month could filter every other pane to it, or narrow that
63
+ pane to weeks within it. Both are reasonable, they need different
64
+ things from the URL, and ADR 006 left the question open on purpose. It
65
+ needs a decision record before any code.
66
+ - **A command palette for the demo** ([#41](https://github.com/retail-tasker/janela/issues/41)).
67
+ The demo site, not the gem.
68
+ - **A scroll drift on the gallery page** ([#45](https://github.com/retail-tasker/janela/issues/45)).
69
+ Cosmetic, demo only, and possibly not worth fixing.
70
+
71
+ ## Not coming
72
+
73
+ Janela is meant to be small enough that forking it and adding your own
74
+ piece is a normal way to use it. These are the things you would be
75
+ adding yourself, and each is left out because something you already run
76
+ does it better.
77
+
78
+ Natural-language query. A separate data warehouse. A row-level-security
79
+ subsystem, because your application already has Pundit or CanCanCan and
80
+ Janela reads through it. A refresh-scheduling interface, because you
81
+ already have a scheduler and snapshots are an ActiveJob. An embedding
82
+ SDK. A mobile application. Print and paginated reports. A drag-and-drop
83
+ visual dashboard designer, though frames and panes are database records,
84
+ so an application can build its own editor on top of them.
85
+
86
+ Whether Janela should help arrange panes on a page is genuinely open
87
+ ([#29](https://github.com/retail-tasker/janela/issues/29)). The default
88
+ answer is that layout belongs to your application, and changing it would
89
+ need a decision record first.
90
+
91
+ ## How this page stays honest
92
+
93
+ It is updated when a release lands, not on a schedule, and it carries no
94
+ dates. A roadmap with dates on a project this size would be wrong within
95
+ a fortnight and would train you to ignore it. If something here has been
96
+ true for a long time and nothing has moved, that is worth reading as the
97
+ signal it is.
data/docs/theming.md ADDED
@@ -0,0 +1,177 @@
1
+ ---
2
+ Topics: styling, theming, css, host-integration
3
+ ---
4
+
5
+ # Theming Janela
6
+
7
+ Janela publishes the hooks; a theme supplies the taste. This page is the
8
+ contract: the class names and custom properties the engine renders, which
9
+ your own stylesheet or somebody else's theme may target, and which will
10
+ not change without an entry in `UPGRADING.md` (ADR 036).
11
+
12
+ Two stylesheets ship in the gem.
13
+
14
+ ```erb
15
+ <%= stylesheet_link_tag "janela" %> <%# the hooks, and enough style to be legible %>
16
+ <%= stylesheet_link_tag "vitral" %> <%# optional: one theme, stained glass %>
17
+ ```
18
+
19
+ `janela.css` is not a look. It is the grid, the pane's own markup and
20
+ enough base styling that a table does not run its label into its number.
21
+ Everything decorative is a theme's, and vitral is one theme rather than
22
+ the theme.
23
+
24
+ ## The contract
25
+
26
+ ### Custom properties
27
+
28
+ Three, and setting them moves everything that depends on them.
29
+
30
+ | Property | Default | What it does |
31
+ | --- | --- | --- |
32
+ | `--janela-space` | `0.25rem` | The base unit of the whole spacing scale. Every gap and padding is a multiple of it. |
33
+ | `--janela-line` | `rgba(128, 128, 128, 0.3)` | Rules between rows, borders on cards and fields. |
34
+ | `--janela-accent` | `rgb(54, 162, 235)` | A selected value, a hovered card. |
35
+
36
+ ```css
37
+ :root {
38
+ --janela-space: 0.3rem;
39
+ --janela-accent: #7c3aed;
40
+ }
41
+ ```
42
+
43
+ ### The grid
44
+
45
+ A frame's stored integers choose these. Nothing an analyst types reaches
46
+ CSS as a length: the number selects a rule that is already written
47
+ (ADR 016).
48
+
49
+ | Class | Range |
50
+ | --- | --- |
51
+ | `janela-frame` | the grid container itself |
52
+ | `janela-cols-N` | 1 to 12 |
53
+ | `janela-gap-N` | 0 to 8, multiplied by `--janela-space` |
54
+ | `janela-span-N` | 1 to 12, on a pane |
55
+
56
+ Below `40rem` the grid collapses to one column. That is in the
57
+ stylesheet rather than in the data, because a dashboard nobody can read
58
+ on a phone is not a choice worth offering.
59
+
60
+ ### The pane
61
+
62
+ What `janela_pane` and `janela_frame` put in your own pages. These are
63
+ the names a theme spends most of its time on.
64
+
65
+ | Class | On | Rendered when |
66
+ | --- | --- | --- |
67
+ | `janela-pane` | `<table>`, `<p>` or `<canvas>` | every pane, whatever the renderer |
68
+ | `janela-value` | `<p>` | a single value pane |
69
+ | `janela-value-label` | `<span>` | its caption |
70
+ | `janela-value-number` | `<strong>` | the number itself |
71
+ | `janela-chart` | `<canvas>` | a bar or line pane |
72
+ | `janela-empty` | `<p>` | a pane whose query returned nothing |
73
+ | `janela-error` | `<p>` | a pane that could not be read |
74
+
75
+ A table pane also renders a `<caption>`, its accessible name, and a
76
+ chart pane carries the same string as `aria-label`.
77
+
78
+ ### Hiding a heading you already wrote
79
+
80
+ Put `janela-own-headings` on any ancestor and a pane's caption and a
81
+ single value's label are hidden from sight while staying in the
82
+ accessibility tree.
83
+
84
+ ```erb
85
+ <div class="janela-own-headings">
86
+ <h3>Revenue by status</h3>
87
+ <%= janela_pane Order, :revenue, by: :status %>
88
+ </div>
89
+ ```
90
+
91
+ Use it when your own markup already says what the pane is, so the text
92
+ is not on screen twice. It hides rather than removes on purpose: a table
93
+ with no caption has no accessible name, so a screen reader lands on a
94
+ grid of numbers with nothing to say what they measure. `display: none`
95
+ would do that, which is why this rule ships here rather than being left
96
+ for each host to write.
97
+
98
+ A chart pane needs nothing: its title is only ever an `aria-label` and
99
+ was never drawn.
100
+
101
+ ## What is not the contract
102
+
103
+ `janela.css` also styles Janela's own pages, the frame index and the
104
+ editing forms: `janela-page`, `janela-card`, `janela-button`,
105
+ `janela-form`, `janela-field`, `janela-list`, `janela-crumb`,
106
+ `janela-flash` and the rest. They are scoped under `janela-page`, which
107
+ only the engine's own layout sets, so they cannot touch your pages.
108
+
109
+ **Those names may change in any release.** They are the engine's own
110
+ chrome rather than an interface. Restyle them if you want Janela's
111
+ pages to match your application, and expect to revisit it after an
112
+ upgrade; or point `Janela.theme` at a stylesheet of your own, below.
113
+
114
+ ## Writing a theme
115
+
116
+ A theme is any stylesheet, named once:
117
+
118
+ ```ruby
119
+ # config/initializers/janela.rb
120
+ Janela.theme = "midnight"
121
+ ```
122
+
123
+ The name is resolved against your own asset paths, so `midnight.css` in
124
+ your application works exactly as the gem's own `vitral` does. A name
125
+ that resolves to nothing raises rather than quietly rendering an
126
+ unthemed page.
127
+
128
+ That setting links the theme into **Janela's own pages**. Your pages load
129
+ whatever your layout says, so if you want the same look around a pane you
130
+ have embedded yourself, link the stylesheet there too. The asymmetry is
131
+ deliberate: Janela is an isolated engine and does not write your layout
132
+ (ADR 011).
133
+
134
+ A theme targets the contract above and nothing else. If it cannot be
135
+ written that way, the contract is missing something, which is worth an
136
+ issue rather than a workaround.
137
+
138
+ ## Vitral
139
+
140
+ The theme the gem ships. A *vitral* is a stained glass window: each pane
141
+ holds one of five colours, dark leading runs between them, and the light
142
+ comes from behind.
143
+
144
+ ```erb
145
+ <%= stylesheet_link_tag "vitral" %>
146
+ <body class="vitral">
147
+ ```
148
+
149
+ Nothing is repainted until that class is present, so linking the
150
+ stylesheet can never be the thing that broke a page.
151
+
152
+ **Its own public classes**, so the page around a dashboard can be made of
153
+ the same window: `vitral-pane`, `vitral-panes`, `vitral-button`,
154
+ `vitral-button-primary`, `vitral-lattice`.
155
+
156
+ **Retheme it from its custom properties** rather than by forking it. The
157
+ light is `--vitral-light-cobalt`, `-teal`, `-amber`, `-rose`, `-violet`;
158
+ the glass is `--vitral-pane-1` through `-5`; the leading is
159
+ `--vitral-came`; and `--vitral-ink`, `--vitral-muted`, `--vitral-ground`,
160
+ `--vitral-radius`, `--vitral-shadow` and `--vitral-blur` do what they
161
+ say.
162
+
163
+ ```css
164
+ :root {
165
+ --vitral-pane-1: rgba(120, 60, 200, 0.3);
166
+ --vitral-came: #1b1b1b;
167
+ }
168
+ ```
169
+
170
+ **It works without JavaScript.** Janela's own pages load none (ADR 011),
171
+ so the stained glass is CSS and the lattice that leans toward the pointer
172
+ is a separate optional controller. Reduced motion, reduced transparency
173
+ and increased contrast each fall back to a still, solid window, and so
174
+ does a browser without `backdrop-filter`.
175
+
176
+ `test/dummy` has a live page at `/vitral` showing all of it against real
177
+ panes.
@@ -6,8 +6,15 @@ module Janela
6
6
 
7
7
  attr_reader :model, :measures, :dimensions
8
8
 
9
+ # The class whose janela block this is, which a subclass shares with the
10
+ # parent it inherited from. A subclass gets a definition of its own, so
11
+ # anything reporting on a declaration rather than on a model groups by
12
+ # this or says the same thing once per class in an STI family (ADR 035).
13
+ attr_accessor :declared_by
14
+
9
15
  def initialize(model, &block)
10
16
  @model = model
17
+ @declared_by = model
11
18
  @measures = {}
12
19
  @dimensions = {}
13
20
  @block = block
@@ -19,7 +26,7 @@ module Janela
19
26
  # the queries they run are its own, because ActiveRecord adds the type
20
27
  # condition to a relation on the subclass (ADR 031).
21
28
  def for(model)
22
- self.class.new(model, &@block)
29
+ self.class.new(model, &@block).tap { |inherited| inherited.declared_by = declared_by }
23
30
  end
24
31
 
25
32
  def measure(name, **aggregate)