janela 0.7.0 → 0.9.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +36 -5
  4. data/UPGRADING.md +111 -0
  5. data/app/assets/javascripts/janela/frame_controller.js +15 -0
  6. data/app/assets/stylesheets/janela.css +29 -0
  7. data/app/controllers/janela/application_controller.rb +7 -0
  8. data/app/controllers/janela/panes_controller.rb +22 -7
  9. data/app/controllers/janela/queries_controller.rb +2 -1
  10. data/app/helpers/janela/frames_helper.rb +26 -7
  11. data/app/models/janela/frame.rb +20 -0
  12. data/app/models/janela/pane.rb +67 -4
  13. data/app/models/janela/query.rb +7 -2
  14. data/app/views/janela/frames/_content.html.erb +26 -0
  15. data/app/views/janela/frames/_frame.html.erb +9 -1
  16. data/app/views/janela/frames/_pane.html.erb +2 -2
  17. data/app/views/janela/panes/_content_form.html.erb +33 -0
  18. data/app/views/janela/panes/_row.html.erb +1 -1
  19. data/app/views/janela/panes/edit.html.erb +5 -1
  20. data/app/views/janela/panes/new.html.erb +6 -1
  21. data/app/views/janela/queries/_query.html.erb +15 -11
  22. data/config/locales/en.yml +5 -0
  23. data/db/migrate/20260924000001_add_content_to_janela_panes.rb +16 -0
  24. data/db/migrate/20260924000002_add_key_to_janela_frames.rb +10 -0
  25. data/docs/composing.md +269 -0
  26. data/docs/decisions/032-janela-will-not-read-a-model-it-cannot-scope.md +1 -0
  27. data/docs/decisions/035-a-check-does-what-janela-does-or-says-what-it-saw.md +262 -0
  28. data/docs/decisions/036-janela-publishes-what-a-theme-may-target.md +171 -0
  29. data/docs/decisions/037-what-1-0-means.md +172 -0
  30. data/docs/decisions/038-a-ratio-is-a-measure-of-its-own.md +194 -0
  31. data/docs/decisions/039-a-pane-can-hold-words-and-only-code-writes-markup.md +168 -0
  32. data/docs/decisions/040-a-host-can-fix-a-frames-filter.md +145 -0
  33. data/docs/decisions/041-a-host-finds-its-frame-by-owner-and-key.md +112 -0
  34. data/docs/decisions/042-a-charts-title-is-a-figcaption.md +174 -0
  35. data/docs/decisions/INDEX.md +26 -15
  36. data/docs/multi-tenancy.md +22 -0
  37. data/docs/naming.md +7 -0
  38. data/docs/roadmap.md +218 -0
  39. data/docs/theming.md +179 -0
  40. data/lib/janela/definition.rb +16 -1
  41. data/lib/janela/doctor.rb +121 -32
  42. data/lib/janela/model.rb +6 -1
  43. data/lib/janela/version.rb +1 -1
  44. data/lib/janela.rb +47 -3
  45. metadata +30 -15
@@ -0,0 +1,174 @@
1
+ ---
2
+ Date: 2026-09-28
3
+ Status: Accepted
4
+ Related: ADR 016, ADR 018, ADR 024, ADR 026, ADR 036
5
+ Triggers:
6
+ - adding a title, caption or visible label to any renderer
7
+ - changing what a chart pane's markup contains, or what carries its classes
8
+ - extending or reading `.janela-own-headings`
9
+ - deciding what a theme may target in `janela.css` (ADR 036)
10
+ - a chart type that cannot sit inside a figure the way bar and line do
11
+ Topics: rendering, charts, accessibility, styling, public-api, theming
12
+ ---
13
+
14
+ # ADR 042: A Chart's Title Is a Figcaption, and the Canvas Points to It
15
+
16
+ ## Context
17
+
18
+ #61 found that a chart pane (`renderer: bar` or `renderer: line`) never
19
+ shows its title to a sighted reader. `janela/queries/_query.html.erb`
20
+ renders it as a bare `<canvas role="img" aria-label="<%= query.title
21
+ %>">`, and `chart_controller.js` passes the same string only into the
22
+ dataset's `label`, with the legend that would draw it turned off. A
23
+ table pane's `<caption>` and a single value pane's `<span
24
+ class="janela-value-label">` both render the same `query.title` as
25
+ visible text. A chart pane is the one renderer that does not.
26
+
27
+ This is not new. #26's own investigation, closed and shipped in 0.8.0,
28
+ already found and recorded the same fact while building
29
+ `.janela-own-headings`:
30
+
31
+ > The table's and the single value's are visible ... The chart's is
32
+ > not ... A chart pane is already announced without being seen.
33
+
34
+ #26 used that as the *model* the other two renderers should be brought
35
+ into line with for hiding purposes, not as a gap to close, and the
36
+ comment above `.janela-own-headings` in `janela.css` still says so:
37
+ "a chart pane needs nothing: its title was only ever an aria-label."
38
+ That sentence is accurate about the code and wrong about the design.
39
+ Nobody decided a chart should be the one renderer with no visible
40
+ title; it fell out of `legend: { display: false }` turning off the
41
+ only thing that would have drawn it.
42
+
43
+ ### Why the issue's own fix does not hold up
44
+
45
+ #61 proposes `plugins.title: { display: true, text: this.titleValue }`,
46
+ Chart.js's own title option. It works, and it was rejected, for three
47
+ reasons that all come from the same place: the text would live inside
48
+ the canvas bitmap rather than in the document.
49
+
50
+ - **It cannot be hidden by `.janela-own-headings`.** That rule hides
51
+ the table's `<caption>` and the value's `<span>` visually while
52
+ leaving them in the accessibility tree, by targeting real elements
53
+ with CSS (#26). A host with its own heading above a chart pane would
54
+ have no way to suppress the canvas-drawn one short of reconfiguring
55
+ Chart.js, so the one mechanism #26 shipped for this exact problem
56
+ would not cover the one renderer it was modelled on.
57
+ - **It contradicts ADR 026 directly**, not just in spirit. That
58
+ decision's third numbered point is "every renderer degrades to
59
+ something readable with no JavaScript, because a pane is rendered on
60
+ the server before anything runs." A title Chart.js paints is not
61
+ there until the chart runtime has loaded and `connect()` has run;
62
+ server-rendered text is there in the same response that draws the
63
+ rest of the pane.
64
+ - **It is not text.** Not selectable, not found by the browser's find
65
+ bar, not sized by the vitral theme's typography, not read by a
66
+ screen reader that has switched off image descriptions but still
67
+ reads a page's headings.
68
+
69
+ None of that is a knock on Chart.js. It is the same argument ADR 026
70
+ already settled for the chart itself: a renderer draws in HTML unless
71
+ it genuinely cannot, and a caption is not a case a canvas is needed for.
72
+
73
+ ### Where the title should live
74
+
75
+ A table pane already pairs a `<table>` with a `<caption>`. HTML has the
76
+ matching pair for an image with a caption: `<figure>` and
77
+ `<figcaption>`. Wrapping the existing canvas in one costs nothing Chart.js
78
+ cares about, since `new Chart(this.element, ...)` still receives the
79
+ canvas itself, unchanged.
80
+
81
+ One thing about that pairing had to be checked rather than assumed:
82
+ `<figcaption>` gives its text to the `<figure>` as a description, not
83
+ to an arbitrary element nested inside it. A screen reader is not
84
+ guaranteed to treat the figcaption as the canvas's own accessible name
85
+ just because it sits beside it. The canvas needs an explicit
86
+ `aria-labelledby` pointing at the figcaption's id; the implicit
87
+ figure/figcaption pairing is not enough on its own and this record
88
+ does not lean on it.
89
+
90
+ ### What was considered
91
+
92
+ **Chart.js's `plugins.title`**, #61's own suggestion. Rejected above.
93
+
94
+ **A bare sibling, `<div class="janela-chart-title">` before an
95
+ unwrapped canvas.** Considered and rejected only because HTML already
96
+ has the element built for exactly this pairing, and using it costs
97
+ nothing: a chart pane on its own page already reads as a captioned
98
+ figure to a browser's own outline, not just to Janela's CSS.
99
+
100
+ **Leaving `aria-label` on the canvas alongside the new visible text.**
101
+ Rejected: two independent strings for the same title can drift, and
102
+ `aria-labelledby` pointing at the one that is now visible cannot.
103
+
104
+ ## Decision
105
+
106
+ **A chart pane's title is a `<figcaption>` inside a `<figure>` that
107
+ wraps the canvas, and the canvas's accessible name is
108
+ `aria-labelledby` pointing at it.**
109
+
110
+ ```erb
111
+ <figure class="janela-pane">
112
+ <figcaption class="janela-chart-title" id="<%= title_id %>"><%= query.title %></figcaption>
113
+ <canvas class="janela-chart"
114
+ data-controller="janela--chart"
115
+ ...
116
+ role="img" aria-labelledby="<%= title_id %>"></canvas>
117
+ </figure>
118
+ ```
119
+
120
+ **`janela-pane` moves from the canvas to the figure; `janela-chart`
121
+ stays on the canvas.** The figure is now the grid item and the thing a
122
+ theme styles as a pane, the same job `<table class="janela-pane">` and
123
+ `<p class="janela-pane janela-value">` already do for the other two
124
+ renderers. The canvas keeps `janela-chart` because that is what
125
+ `canvas.janela-chart { width: 100% !important; max-height: 20rem; }`
126
+ and the Stimulus controller both already target, and because #61's own
127
+ system test selects `canvas.janela-chart` directly; nothing that
128
+ selects the canvas by that class alone stops matching. What stops
129
+ matching is anything that selected `.janela-pane.janela-chart` as one
130
+ element, which is the breaking part of this change.
131
+
132
+ **`janela-chart-title` joins the pane primitives on the contract**
133
+ (ADR 036), the same group `janela-value-label` and a table's `caption`
134
+ selector are already in. Its base legibility rule (size, weight,
135
+ spacing) goes in `janela.css` next to the other two, not in vitral,
136
+ for the same reason theirs does: a pane has to be legible with no
137
+ theme installed.
138
+
139
+ **`.janela-own-headings` gets a third rule.** It already hides the
140
+ table's caption and the value's label visually while keeping them in
141
+ the accessibility tree; `figcaption.janela-chart-title` joins that
142
+ selector, and the comment above it that says a chart pane needs
143
+ nothing is corrected to say why it no longer does.
144
+
145
+ **Chart.js's dataset `label` and the disabled legend are unchanged.**
146
+ The visible title no longer depends on either, which is the point:
147
+ it is there whether or not the chart runtime ever finishes loading.
148
+
149
+ ## Consequences
150
+
151
+ - A chart pane finally shows its title the way the other two renderers
152
+ do, closing #61 and the half of #26 that was left standing on
153
+ purpose.
154
+ - Breaking change: `janela-pane` is no longer on the `<canvas>`, and
155
+ `aria-label` is replaced by `aria-labelledby`. Anything a host wrote
156
+ against either goes in `UPGRADING.md` (ADR 015), and the doctor's
157
+ list of things to check for an old release is worth a line.
158
+ - `docs/theming.md` gains `janela-chart-title` in the pane primitives
159
+ table (ADR 036), and `janela.css` gains its rule and its line in
160
+ `.janela-own-headings`.
161
+ - The system test in `test/system/chart_test.rb` asserts
162
+ `canvas.janela-chart[aria-label='...']` today; it moves to asserting
163
+ the figcaption's text and the `aria-labelledby` wiring, which is
164
+ build work, not a decision.
165
+ - Chart.js keeps drawing bar and line exactly as it does today; nothing
166
+ about `chart_controller.js`'s own options needs to change for this,
167
+ only the markup around it.
168
+ - What would change this decision: a chart type that cannot sit inside
169
+ a figure the way bar and line do, or a case where wrapping the canvas
170
+ measurably breaks Chart.js's own sizing against its parent. Neither
171
+ is expected, since the canvas already sits one level inside a turbo
172
+ frame today and gains only one more ordinary block-level ancestor,
173
+ but it is exactly the kind of assumption to re-check against the demo
174
+ before the build lands rather than after.
@@ -20,26 +20,29 @@ 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 |
25
- | **DSL & query layer** | 002, 006, 007, 020, 025 |
23
+ | **Vision, scope, forkability** | 001, 010, 012, 037 |
24
+ | **Open-source & host-decoupling** | 001, 022, 036, 041 |
25
+ | **DSL & query layer** | 002, 006, 007, 020, 025, 038 |
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, 039, 040 |
28
28
  | **Performance & storage** | 007, 017, 025 |
29
- | **Cross-filtering & Hotwire** | 003, 004, 005, 008, 024, 025 |
30
- | **Layouts & views** | 011, 012, 016, 018, 020, 027 |
31
- | **CSS & styling** | 016, 018, 023, 026, 027 |
32
- | **Frames, panes & persistence** | 012, 013, 014, 019, 029, 030, 033 |
33
- | **Naming rule** | 014, 023 |
34
- | **JavaScript delivery & charts** | 004, 006, 026 |
29
+ | **Ordering & formatting** | 007, 020, 038 |
30
+ | **Cross-filtering & Hotwire** | 003, 004, 005, 008, 024, 025, 040 |
31
+ | **Layouts & views** | 011, 012, 016, 018, 020, 027, 039 |
32
+ | **CSS & styling** | 016, 018, 023, 026, 027, 036, 042 |
33
+ | **Frames, panes & persistence** | 012, 013, 014, 019, 029, 030, 033, 039, 040, 041 |
34
+ | **Naming rule** | 014, 023, 036 |
35
+ | **JavaScript delivery & charts** | 004, 006, 026, 042 |
35
36
  | **Time dimensions** | 006, 025 |
36
- | **Routes, URLs & naming** | 005, 007, 008, 009, 011, 013, 022, 024, 025 |
37
+ | **Routes, URLs & naming** | 005, 007, 008, 009, 011, 013, 022, 024, 025, 040, 041 |
37
38
  | **Snapshots & publishing** | 009, 020, 028, 033, 034 |
38
39
  | **AI agents & guidance** | 010, 015, 021 |
39
- | **Releases & upgrades** | 015, 021, 032, 034 |
40
- | **Accessibility & keyboard** | 024 |
41
- | **Security** | 003, 025, 028, 031, 032, 034 |
40
+ | **The doctor & checks** | 021, 025, 032, 033, 035 |
41
+ | **Releases & upgrades** | 015, 021, 032, 034, 035, 036, 037, 042 |
42
+ | **Accessibility & keyboard** | 024, 042 |
43
+ | **Security** | 003, 025, 028, 031, 032, 034, 035 |
42
44
  | **Testing** | 003 |
45
+ | **Roadmap & planning** | 001, 037 |
43
46
 
44
47
  ## Chronological
45
48
 
@@ -79,7 +82,15 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
79
82
  | 032 | Janela Will Not Read a Model It Cannot Scope | 2026-09-20 | Accepted |
80
83
  | 033 | A Snapshot Is Told Who Owns It | 2026-09-21 | Accepted |
81
84
  | 034 | Janela Will Not Freeze a Scope the Host Has Not Named | 2026-09-21 | Accepted |
85
+ | 035 | A Check Does What Janela Does, or It Says What It Saw | 2026-09-22 | Accepted |
86
+ | 036 | Janela Publishes What a Theme May Target, and Vitral Is Only One | 2026-09-22 | Accepted |
87
+ | 037 | 1.0 Means the Surface Stops Moving, Not That Janela Is Finished | 2026-09-23 | Accepted |
88
+ | 038 | A Ratio Is a Measure of Its Own, Stored as a Fraction and Read as a Percentage | 2026-09-24 | Proposed |
89
+ | 039 | A Pane Can Hold Words, and Only Code Writes Markup | 2026-09-24 | Accepted |
90
+ | 040 | A Host Can Fix a Frame's Filter, and No Click Removes It | 2026-09-24 | Accepted |
91
+ | 041 | A Host Finds Its Frame by Owner and Key | 2026-09-24 | Accepted |
92
+ | 042 | A Chart's Title Is a Figcaption, and the Canvas Points to It | 2026-09-28 | Accepted |
82
93
 
83
94
  ## Next number
84
95
 
85
- Next ADR: 035
96
+ Next ADR: 043
@@ -149,6 +149,28 @@ save failed silently. `bin/rails janela:doctor` reports this for you:
149
149
  it asks your policy for a scope over frames and looks for an owner in
150
150
  what comes back.
151
151
 
152
+ ### A frame of your own for a page
153
+
154
+ Your code finds a frame it keeps for one of its pages by owner and key
155
+ (ADR 041):
156
+
157
+ ```ruby
158
+ Janela::Frame.for(Current.account, :overview)
159
+ Janela::Frame.for(queue, :analytics) { |frame| frame.name = "#{queue.name} analytics" }
160
+ ```
161
+
162
+ When the owner is the tenant, nothing changes in your policy. When it is
163
+ one of your own records, such as the queue above, the frame belongs to
164
+ the tenant through that record, and your scope has to say so or the
165
+ frame is hidden from the people it was made for:
166
+
167
+ ```ruby
168
+ def resolve
169
+ scope.where(owner: Current.account)
170
+ .or(scope.where(owner_type: "Queue", owner_id: Current.account.queues.select(:id)))
171
+ end
172
+ ```
173
+
152
174
  ## Snapshots
153
175
 
154
176
  A snapshot carries the same polymorphic owner a frame does, so your
data/docs/naming.md CHANGED
@@ -44,6 +44,13 @@ from the thing itself rather than from a list of synonyms for "chart".
44
44
  | **Query** | The runtime object that calculates one pane | Not a window word, on purpose. It is an implementation detail rather than something a person arranges, so it gets the plain name for what it does. |
45
45
  | **Grid** | How a frame is divided: `columns`, `gap`, and each pane's `span` | A window is divided into panes, and the grid is the division. Small integers that choose a class the stylesheet already defines, so nothing an analyst types reaches CSS (ADR 016). |
46
46
  | **Vitral** | The optional stained glass theme | A stained glass window, in the same language. See ADR 023. |
47
+ | **Vista** | The roadmap | The view through the window. What you can see from here is also what is ahead: *outlook* and *prospect* carry the same double meaning in English, and this one carries it in Portuguese. |
48
+ | **Horizonte** | The roadmap's far half: wanted, not blocking a release | The horizon is the edge of what you can see. Work still in sight, as against work that is not in the picture at all. |
49
+
50
+ The last two are a step away from anatomy. A window's parts name the
51
+ things a host arranges; what you see through it names the things the
52
+ project has not built yet. Both halves come from the same object, which
53
+ is the point of picking an object rather than a list of synonyms.
47
54
 
48
55
  The anatomy was argued over before it was settled. *Sash* was
49
56
  considered for the dashboard and rejected: a sash is one layer inside a
data/docs/roadmap.md ADDED
@@ -0,0 +1,218 @@
1
+ ---
2
+ Topics: roadmap, releases, scope, planning
3
+ ---
4
+
5
+ # Vista
6
+
7
+ <p style="text-align: center;">Vista shows where Janela is heading.</p>
8
+
9
+ <svg viewBox="0 0 680 360" width="100%" role="img" aria-labelledby="vista-title vista-desc" class="vista-art" style="display: block; margin: 1.75rem 0; border-radius: 10px;">
10
+ <title id="vista-title">Vista</title>
11
+ <desc id="vista-desc">Sea and sky with a horizon across them. Eight lights burn on the near water, one for each issue in 1.0, and three sit far off at the horizon for the work still in sight past it. A low sun rises and sets on the horizon as the pointer moves up and down, and never climbs higher: the sky above is empty, because what is not coming is not in view.</desc>
12
+
13
+ <style>
14
+ .vista-art .v-far { transform: translate3d(calc(var(--vitral-shift-x, 0) * 13px), calc(var(--vitral-shift-y, 0) * 7px), 0); transition: transform .45s cubic-bezier(.2,.7,.3,1); }
15
+ .vista-art .v-mid { transform: translate3d(calc(var(--vitral-shift-x, 0) * 6px), calc(var(--vitral-shift-y, 0) * 3px), 0); transition: transform .45s cubic-bezier(.2,.7,.3,1); }
16
+ .vista-art .v-near { transform: translate3d(calc(var(--vitral-shift-x, 0) * -8px), calc(var(--vitral-shift-y, 0) * -4px), 0); transition: transform .45s cubic-bezier(.2,.7,.3,1); }
17
+ .vista-art .v-glow-a { opacity: calc(.6 - var(--vitral-shift-x, 0) * 1.2); transform: translate3d(calc(var(--vitral-shift-x, 0) * -40px), calc(var(--vitral-shift-y, 0) * 16px), 0); transition: transform .9s cubic-bezier(.2,.7,.3,1), opacity .9s ease; }
18
+ .vista-art .v-glow-b { opacity: calc(.6 + var(--vitral-shift-x, 0) * 1.2); transform: translate3d(calc(var(--vitral-shift-x, 0) * 44px), calc(var(--vitral-shift-y, 0) * -14px), 0); transition: transform .9s cubic-bezier(.2,.7,.3,1), opacity .9s ease; }
19
+ .vista-art .v-sun { transform: translate3d(calc(var(--vitral-shift-x, 0) * 10px), calc(var(--vitral-shift-y, 0) * 84px), 0); transition: transform 1.1s cubic-bezier(.2,.7,.3,1); }
20
+ .vista-art .v-glint { opacity: calc(.45 - var(--vitral-shift-y, 0) * .9); transform: translate3d(calc(var(--vitral-shift-x, 0) * 10px), 0, 0); transition: opacity 1.1s ease, transform 1.1s cubic-bezier(.2,.7,.3,1); }
21
+ @media (prefers-reduced-motion: reduce) {
22
+ .vista-art .v-sun, .vista-art .v-glint { transform: none; opacity: .45; }
23
+ .vista-art .v-far, .vista-art .v-mid, .vista-art .v-near, .vista-art .v-glow-a, .vista-art .v-glow-b { transform: none; opacity: .6; }
24
+ }
25
+ </style>
26
+
27
+ <defs>
28
+ <clipPath id="vFrame"><rect x="0" y="0" width="680" height="360" rx="10"/></clipPath>
29
+ <clipPath id="vAbove"><rect x="0" y="0" width="680" height="196"/></clipPath>
30
+ <radialGradient id="vSun">
31
+ <stop offset="0" stop-color="#FFF4D6" stop-opacity="0.95"/>
32
+ <stop offset="0.7" stop-color="#FFD58A" stop-opacity="0.9"/>
33
+ <stop offset="1" stop-color="#F7A96B" stop-opacity="0.75"/>
34
+ </radialGradient>
35
+ <radialGradient id="vSunHalo">
36
+ <stop offset="0" stop-color="#FFD98F" stop-opacity="0.5"/>
37
+ <stop offset="1" stop-color="#F7A96B" stop-opacity="0"/>
38
+ </radialGradient>
39
+ <linearGradient id="vSky" x1="0" y1="0" x2="0.22" y2="1">
40
+ <stop offset="0" stop-color="#5B49C9" stop-opacity="0.54"/>
41
+ <stop offset="0.38" stop-color="#8A6EE4" stop-opacity="0.34"/>
42
+ <stop offset="0.7" stop-color="#E06082" stop-opacity="0.29"/>
43
+ <stop offset="1" stop-color="#F0B046" stop-opacity="0.46"/>
44
+ </linearGradient>
45
+ <linearGradient id="vSea" x1="0" y1="0" x2="0.08" y2="1">
46
+ <stop offset="0" stop-color="#F0B046" stop-opacity="0.34"/>
47
+ <stop offset="0.22" stop-color="#1CA89E" stop-opacity="0.38"/>
48
+ <stop offset="1" stop-color="#132F5C" stop-opacity="0.78"/>
49
+ </linearGradient>
50
+ <radialGradient id="vLamp">
51
+ <stop offset="0" stop-color="#FFE9B8" stop-opacity="0.78"/>
52
+ <stop offset="1" stop-color="#FFD98F" stop-opacity="0"/>
53
+ </radialGradient>
54
+ <radialGradient id="vP1"><stop offset="0" stop-color="#367AEB" stop-opacity="0.6"/><stop offset="1" stop-color="#367AEB" stop-opacity="0"/></radialGradient>
55
+ <radialGradient id="vP2"><stop offset="0" stop-color="#1CA89E" stop-opacity="0.30"/><stop offset="1" stop-color="#1CA89E" stop-opacity="0"/></radialGradient>
56
+ <radialGradient id="vP3"><stop offset="0" stop-color="#E06082" stop-opacity="0.5"/><stop offset="1" stop-color="#E06082" stop-opacity="0"/></radialGradient>
57
+ </defs>
58
+
59
+ <g clip-path="url(#vFrame)">
60
+ <g class="v-far">
61
+ <rect x="-30" y="-30" width="740" height="256" fill="url(#vSky)"/>
62
+ <ellipse class="v-glow-a" cx="150" cy="104" rx="230" ry="132" fill="url(#vP1)"/>
63
+ <ellipse class="v-glow-b" cx="520" cy="140" rx="250" ry="100" fill="url(#vP3)"/>
64
+ </g>
65
+
66
+ <g clip-path="url(#vAbove)">
67
+ <g class="v-sun">
68
+ <circle cx="432" cy="192" r="96" fill="url(#vSunHalo)"/>
69
+ <circle cx="432" cy="192" r="24" fill="url(#vSun)"/>
70
+ </g>
71
+ </g>
72
+ <g class="v-mid">
73
+ <rect x="-30" y="196" width="740" height="204" fill="url(#vSea)"/>
74
+ <ellipse cx="580" cy="288" rx="220" ry="124" fill="url(#vP2)"/>
75
+ <path d="M6 200.0 h18 M46 200.0 h18 M80 200.0 h11 M145 200.0 h9 M173 200.0 h8 M204 200.0 h22 M235 200.0 h7 M266 200.0 h18 M305 200.0 h19 M339 200.0 h23 M372 200.0 h30 M424 200.0 h24 M490 200.0 h16 M519 200.0 h30 M555 200.0 h12 M581 200.0 h30 M618 200.0 h22 M650 200.0 h9" stroke="#FFF3DA" stroke-opacity="0.28" stroke-width="1" stroke-linecap="round"/>
76
+ <path d="M74 204.2 h12 M100 204.2 h12 M195 204.2 h28 M245 204.2 h13 M384 204.2 h10 M404 204.2 h23 M441 204.2 h31 M527 204.2 h30 M568 204.2 h12 M604 204.2 h12 M639 204.2 h23 M669 204.2 h8" stroke="#FFF3DA" stroke-opacity="0.28" stroke-width="1" stroke-linecap="round"/>
77
+ <path d="M127 209.4 h23 M169 209.4 h16 M278 209.4 h32 M329 209.4 h27 M398 209.4 h27 M431 209.4 h25 M477 209.4 h17 M501 209.4 h23 M548 209.4 h28 M598 209.4 h33 M650 209.4 h32" stroke="#FFF3DA" stroke-opacity="0.27" stroke-width="1" stroke-linecap="round"/>
78
+ <path d="M6 215.8 h32 M56 215.8 h27 M148 215.8 h38 M208 215.8 h20 M247 215.8 h22 M370 215.8 h23 M487 215.8 h35 M538 215.8 h35 M593 215.8 h15 M631 215.8 h36" stroke="#FFF3DA" stroke-opacity="0.26" stroke-width="1" stroke-linecap="round"/>
79
+ <path d="M6 223.7 h28 M44 223.7 h26 M96 223.7 h32 M141 223.7 h15 M169 223.7 h17 M207 223.7 h26 M258 223.7 h41 M308 223.7 h11 M326 223.7 h32 M492 223.7 h30 M543 223.7 h31" stroke="#FFF3DA" stroke-opacity="0.25" stroke-width="1" stroke-linecap="round"/>
80
+ <path d="M56 233.5 h23 M99 233.5 h33 M149 233.5 h39 M197 233.5 h44 M252 233.5 h27 M308 233.5 h24 M361 233.5 h39 M477 233.5 h40 M542 233.5 h18 M591 233.5 h46 M664 233.5 h22" stroke="#FFF3DA" stroke-opacity="0.24" stroke-width="1" stroke-linecap="round"/>
81
+ <path d="M52 245.5 h34 M107 245.5 h13 M246 245.5 h33 M309 245.5 h40 M494 245.5 h38 M562 245.5 h35 M615 245.5 h46" stroke="#FFF3DA" stroke-opacity="0.22" stroke-width="1" stroke-linecap="round"/>
82
+ <path d="M6 260.4 h52 M147 260.4 h37 M193 260.4 h58 M271 260.4 h50 M414 260.4 h33 M482 260.4 h26 M521 260.4 h33 M589 260.4 h35 M639 260.4 h42" stroke="#FFF3DA" stroke-opacity="0.20" stroke-width="1" stroke-linecap="round"/>
83
+ <path d="M79 278.8 h28 M126 278.8 h34 M310 278.8 h39 M432 278.8 h49 M507 278.8 h60 M605 278.8 h28 M669 278.8 h63" stroke="#FFF3DA" stroke-opacity="0.17" stroke-width="1" stroke-linecap="round"/>
84
+ <path d="M296 301.6 h69 M404 301.6 h26 M521 301.6 h73 M610 301.6 h49" stroke="#FFF3DA" stroke-opacity="0.14" stroke-width="1" stroke-linecap="round"/>
85
+ <path d="M156 329.6 h58 M314 329.6 h91 M422 329.6 h26 M479 329.6 h75 M587 329.6 h58" stroke="#FFF3DA" stroke-opacity="0.09" stroke-width="1" stroke-linecap="round"/>
86
+ <circle cx="186" cy="202" r="8" fill="url(#vLamp)" opacity="0.2"/>
87
+ <circle cx="186" cy="202" r="1.7" fill="#FFF3DC" opacity="0.55"/>
88
+ <circle cx="322" cy="201" r="8" fill="url(#vLamp)" opacity="0.2"/>
89
+ <circle cx="322" cy="201" r="1.5" fill="#FFF3DC" opacity="0.55"/>
90
+ <circle cx="540" cy="202.5" r="8" fill="url(#vLamp)" opacity="0.2"/>
91
+ <circle cx="540" cy="202.5" r="1.6" fill="#FFF3DC" opacity="0.55"/>
92
+ </g>
93
+
94
+ <g class="v-glint" stroke="#FFE2A6" stroke-linecap="round">
95
+ <path d="M404 201 h56" stroke-width="2" stroke-opacity="0.7"/>
96
+ <path d="M412 207 h40" stroke-width="1.6" stroke-opacity="0.55"/>
97
+ <path d="M418 214 h28" stroke-width="1.4" stroke-opacity="0.42"/>
98
+ <path d="M423 223 h18" stroke-width="1.2" stroke-opacity="0.3"/>
99
+ <path d="M427 234 h10" stroke-width="1" stroke-opacity="0.2"/>
100
+ </g>
101
+ <g class="v-near">
102
+ <circle cx="74" cy="330" r="22" fill="url(#vLamp)" opacity="0.4"/>
103
+ <circle cx="74" cy="330" r="4.3" fill="#FFF6E2" opacity="0.88"/>
104
+ <circle cx="154" cy="302" r="20" fill="url(#vLamp)" opacity="0.4"/>
105
+ <circle cx="154" cy="302" r="4.0" fill="#FFF6E2" opacity="0.88"/>
106
+ <circle cx="238" cy="340" r="21" fill="url(#vLamp)" opacity="0.4"/>
107
+ <circle cx="238" cy="340" r="4.2" fill="#FFF6E2" opacity="0.88"/>
108
+ <circle cx="312" cy="283" r="18" fill="url(#vLamp)" opacity="0.4"/>
109
+ <circle cx="312" cy="283" r="3.6" fill="#FFF6E2" opacity="0.88"/>
110
+ <circle cx="392" cy="314" r="19" fill="url(#vLamp)" opacity="0.4"/>
111
+ <circle cx="392" cy="314" r="3.8" fill="#FFF6E2" opacity="0.88"/>
112
+ <circle cx="470" cy="268" r="16" fill="url(#vLamp)" opacity="0.4"/>
113
+ <circle cx="470" cy="268" r="3.3" fill="#FFF6E2" opacity="0.88"/>
114
+ <circle cx="552" cy="294" r="18" fill="url(#vLamp)" opacity="0.4"/>
115
+ <circle cx="552" cy="294" r="3.5" fill="#FFF6E2" opacity="0.88"/>
116
+ <circle cx="630" cy="256" r="16" fill="url(#vLamp)" opacity="0.4"/>
117
+ <circle cx="630" cy="256" r="3.1" fill="#FFF6E2" opacity="0.88"/>
118
+ </g>
119
+
120
+ <path d="M0 196 H680" stroke="#FFF1D2" stroke-opacity="0.72" stroke-width="1.5" fill="none"/>
121
+ <text x="664" y="218" text-anchor="end" font-size="10" letter-spacing="0.13em" fill="#FFF4DE" opacity="0.6">HORIZONTE</text>
122
+ </g>
123
+ </svg>
124
+
125
+ Janela is alpha. It works, it is tested against a real Rails application
126
+ in a real browser, and its public surface has changed in three of the
127
+ last four releases. This page says what has to be true before that stops,
128
+ what is in the next release, and what is deliberately not coming. The
129
+ reasoning behind it is ADR 037.
130
+
131
+ ## What 1.0 means
132
+
133
+ Not that Janela is finished. ADR 001 commits the project to the
134
+ load-bearing 5% of a BI tool and to staying small enough to fork, so a
135
+ 1.0 measured against what a commercial tool ships would never arrive.
136
+
137
+ 1.0 is three claims you can check.
138
+
139
+ **The public surface stops moving.** Everything listed as the contract in
140
+ [Theming Janela](theming), the measures and dimensions DSL, `janela_pane`
141
+ and `janela_frame`, the pane URL shape and the dashboard filter
142
+ parameters change only on a major version after 1.0. Until then they can
143
+ change in any release, and every change of that kind carries an entry in
144
+ `UPGRADING.md`.
145
+
146
+ **A pane can be read.** Janela draws tables, bars and lines. A
147
+ part-to-whole split currently has to be drawn as bars, a table row cannot
148
+ carry context beside its label, and a chart takes Chart.js's default
149
+ proportions whether or not they suit the page. Those are not extra
150
+ features. They are the 5% not finished, and most of them were found by
151
+ people installing the gem rather than reading it.
152
+
153
+ **The doctor can be trusted.** `rails janela:doctor` checks an
154
+ installation for the mistakes that produce a dashboard showing numbers
155
+ nobody should see. Two of its checks have no test that makes them fire.
156
+ A check nobody can prove is working is a check nobody should rely on.
157
+
158
+ ## In 1.0
159
+
160
+ The eight lights burning in the near ground.
161
+
162
+ | Issue | What |
163
+ | --- | --- |
164
+ | [#24](https://github.com/retail-tasker/janela/issues/24) | A chart's height and aspect ratio are the host's to set |
165
+ | [#27](https://github.com/retail-tasker/janela/issues/27) | A ratio measure, so refusing `average:` over a boolean offers somewhere to go |
166
+ | [#30](https://github.com/retail-tasker/janela/issues/30) | Doughnut and pie, and a categorical palette that makes them readable |
167
+ | [#31](https://github.com/retail-tasker/janela/issues/31) | A pane says how prominent it is |
168
+ | [#34](https://github.com/retail-tasker/janela/issues/34) | A table pane carries an attribute column beside its label |
169
+ | [#53](https://github.com/retail-tasker/janela/issues/53) | The last two doctor checks get tests |
170
+ | [#14](https://github.com/retail-tasker/janela/issues/14) | A Sprockets host serves the engine's JavaScript, or is told it cannot |
171
+ | [#6](https://github.com/retail-tasker/janela/issues/6) | How contributions are accepted, who cuts a release, where to report a vulnerability |
172
+
173
+ Progress is tracked on the
174
+ [1.0 milestone](https://github.com/retail-tasker/janela/milestone/2).
175
+
176
+ ## Horizonte
177
+
178
+ The three lights far off at the horizon. Wanted, not blocking a stable
179
+ release, and being on this list is not a refusal.
180
+
181
+ - **Drill-down on time panes** ([#18](https://github.com/retail-tasker/janela/issues/18)).
182
+ Clicking a month could filter every other pane to it, or narrow that
183
+ pane to weeks within it. Both are reasonable, they need different
184
+ things from the URL, and ADR 006 left the question open on purpose. It
185
+ needs a decision record before any code.
186
+ - **A command palette for the demo** ([#41](https://github.com/retail-tasker/janela/issues/41)).
187
+ The demo site, not the gem.
188
+ - **A scroll drift on the gallery page** ([#45](https://github.com/retail-tasker/janela/issues/45)).
189
+ Cosmetic, demo only, and possibly not worth fixing.
190
+
191
+ ## Not coming
192
+
193
+ The sky above the horizon is empty, and that is the honest part of the
194
+ picture. Janela is meant to be small enough that forking it and adding
195
+ your own piece is a normal way to use it. These are the things you would
196
+ be adding yourself, and each is left out because something you already
197
+ run does it better.
198
+
199
+ Natural-language query. A separate data warehouse. A row-level-security
200
+ subsystem, because your application already has Pundit or CanCanCan and
201
+ Janela reads through it. A refresh-scheduling interface, because you
202
+ already have a scheduler and snapshots are an ActiveJob. An embedding
203
+ SDK. A mobile application. Print and paginated reports. A drag-and-drop
204
+ visual dashboard designer, though frames and panes are database records,
205
+ so an application can build its own editor on top of them.
206
+
207
+ Whether Janela should help arrange panes on a page is genuinely open
208
+ ([#29](https://github.com/retail-tasker/janela/issues/29)). The default
209
+ answer is that layout belongs to your application, and changing it would
210
+ need a decision record first.
211
+
212
+ ## How this page stays honest
213
+
214
+ It is updated when a release lands, not on a schedule, and it carries no
215
+ dates. A roadmap with dates on a project this size would be wrong within
216
+ a fortnight and would train you to ignore it. If something here has been
217
+ true for a long time and nothing has moved, that is worth reading as the
218
+ signal it is.