janela 0.3.0 → 0.4.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,104 @@
1
+ ---
2
+ Date: 2026-09-16
3
+ Status: Accepted
4
+ Related: ADR 004, ADR 005, ADR 011, ADR 019
5
+ Triggers:
6
+ - a route helper raising inside Janela's controllers
7
+ - host code inherited from ApplicationController running in the engine
8
+ - authentication redirects
9
+ - anything that would make the engine less isolated
10
+ Topics: host-integration, routing, engine-isolation, authentication
11
+ ---
12
+
13
+ # ADR 022: A Host's Route Helpers Work Inside the Engine
14
+
15
+ ## Context
16
+
17
+ `isolate_namespace Janela` points every route helper evaluated inside
18
+ Janela's controllers at the engine's own route set. That is the right
19
+ default for the engine's own code. It is the wrong answer for the
20
+ host's, and the host's runs there constantly, because
21
+ `Janela::ApplicationController` inherits the host's
22
+ `ApplicationController` on purpose (ADR 004).
23
+
24
+ Three reports, one cause, and each host found it separately:
25
+
26
+ 1. A host's application layout calling `root_path` in its nav, which
27
+ made every pane 500. Solved another way in 0.2.1 by not rendering
28
+ panes in the host's layout (ADR 011).
29
+ 2. A host authenticating per controller, whose `before_action`
30
+ redirected to `new_session_path`.
31
+ 3. A host authenticating globally, whose `Authentication` concern
32
+ redirected to `new_session_path` from inside the engine and raised
33
+ `ActionController::UrlGenerationError`. An unauthenticated visitor
34
+ got a 500 instead of a sign-in page.
35
+
36
+ Case 3 is the one that shows the shape of the problem. The README had
37
+ framed the fix as something to do "if your app authenticates per
38
+ controller", which is not the trigger at all. The trigger is that a
39
+ URL is generated by host code while it is inside the engine. A
40
+ `rescue_from` that redirects, an `after_action`, a flash partial and an
41
+ audit callback all qualify. Each host discovers it on its own, in
42
+ production, as a 500.
43
+
44
+ `main_app.new_session_path` is the documented fix and it works. It
45
+ also asks every host to know that Janela is an isolated engine, and to
46
+ remember it in code that has nothing to do with Janela. That is the
47
+ opposite of what installing this gem is supposed to feel like.
48
+
49
+ The option of including the host's `url_helpers` wholesale was
50
+ considered and rejected. Both sets of helpers would then be present,
51
+ and a name in both would resolve by module order, so `frames_path`
52
+ inside Janela's own view could silently become the host's `/frames`.
53
+ Trading a loud `UrlGenerationError` for a quietly wrong link is a bad
54
+ trade.
55
+
56
+ ## Decision
57
+
58
+ **Janela forwards to the host exactly those route helpers the engine
59
+ does not define, and no others.**
60
+
61
+ The set is computed when routes are loaded, as the host's helper names
62
+ minus the engine's, and each one is defined to call `main_app`. It is
63
+ rebuilt whenever routes reload, so a name that goes away goes away
64
+ here too.
65
+
66
+ ```ruby
67
+ # In a host's ApplicationController, running inside Janela:
68
+ redirect_to new_session_path # works, forwarded to the host
69
+ frames_path # Janela's, never the host's /frames
70
+ main_app.frames_path # the host's, said unambiguously
71
+ ```
72
+
73
+ Two properties matter more than the convenience:
74
+
75
+ **The engine's own routes can never be shadowed.** A name is forwarded
76
+ only when Janela does not define it, so no host route can change what
77
+ one of Janela's own views means. Precedence is decided by
78
+ construction, not by the order modules happened to be included.
79
+
80
+ **`main_app` still means what it means.** Nothing is taken away. A
81
+ host that prefers to be explicit, or that needs a name Janela also
82
+ uses, says `main_app.` and is unambiguous.
83
+
84
+ **This covers named helpers only.** `url_for(record)` and
85
+ `redirect_to @record` resolve polymorphically at call time, so there
86
+ is no name to forward and they still need `main_app.`. That boundary
87
+ is documented rather than papered over.
88
+
89
+ ## Consequences
90
+
91
+ - A host's authentication redirect works inside Janela with no
92
+ configuration, no reopened class and no knowledge that an engine is
93
+ involved. That was the whole complaint in three reports.
94
+ - Janela is slightly less isolated than a textbook engine, in one
95
+ direction only: host names in, never Janela names out. ADR 005's
96
+ mount independence is untouched, since the engine still generates
97
+ every one of its own URLs through its own routes.
98
+ - A host route helper shares a name with one of Janela's and the host
99
+ loses the bare name inside the engine. Janela's route names are few
100
+ and dull, which makes this rare, and `main_app.` remains.
101
+ - The README's explanation was wrong about the cause and is corrected
102
+ here and there.
103
+ - Polymorphic routing is a known gap. If it bites someone the answer
104
+ is another ADR, not a quiet widening of this one.
@@ -0,0 +1,86 @@
1
+ ---
2
+ Date: 2026-09-16
3
+ Status: Accepted
4
+ Related: ADR 011, ADR 016, ADR 021
5
+ Triggers:
6
+ - changing how Janela looks
7
+ - adding a stylesheet, a class or a custom property
8
+ - adding a Stimulus controller to the gem
9
+ - adding a setting
10
+ Topics: styling, host-integration, configuration, naming
11
+ ---
12
+
13
+ # ADR 023: Vitral Is a Theme, Not the Stylesheet
14
+
15
+ ## Context
16
+
17
+ ADR 016 gave Janela one stylesheet, deliberately plain: the grid classes
18
+ an analyst's numbers choose from, and just enough style that a pane is
19
+ legible on install. That was right, and it left an obvious gap. A host
20
+ that wants a dashboard to look like something has to write all of it,
21
+ and the first thing anyone does with a new library is judge how it
22
+ looks.
23
+
24
+ The temptation is to make `janela.css` prettier. That would be a
25
+ mistake. Structure and taste have different lifetimes: the grid classes
26
+ are load bearing and must not change, while taste is the first thing a
27
+ host will want to replace and the first thing we will want to revise.
28
+ Putting both in one file means every visual revision risks the layout,
29
+ and every host that dislikes the look has to fight rules it also needs.
30
+
31
+ ## Decision
32
+
33
+ **A second stylesheet, `vitral.css`, optional and separate.**
34
+
35
+ A *vitral* is a stained glass window. Janela is a window, its parts are
36
+ frames and panes, and this is what the glass looks like. The name
37
+ follows the same rule as the rest: an uncommon but conceivable word, so
38
+ it never collides with the vocabulary a host already uses for its own
39
+ things.
40
+
41
+ ```erb
42
+ <%= stylesheet_link_tag "janela" %>
43
+ <%= stylesheet_link_tag "vitral" %>
44
+ ```
45
+
46
+ Four rules hold it in place.
47
+
48
+ **Nothing is repainted until asked.** The light and the leading hang off
49
+ a `vitral` class. A host that links the stylesheet and adds no class
50
+ gets nothing, which means linking it can never be the thing that broke
51
+ a page.
52
+
53
+ **It is a small library, not a private skin.** `vitral-pane`,
54
+ `vitral-panes` and `vitral-button` are public, so the page around a
55
+ dashboard can be made of the same window. Everything else is a custom
56
+ property, so a host rethemes from its own stylesheet rather than by
57
+ forking this one.
58
+
59
+ **The theme must be complete without JavaScript.** Janela's own pages
60
+ load none at all (ADR 011), so the stained glass is CSS, and the part
61
+ that answers the pointer is a separate optional controller. The
62
+ stylesheet carries a static lattice for everyone who never loads it.
63
+
64
+ **Janela's own pages wear it by name.** `Janela.theme = "vitral"` is the
65
+ third setting this gem has, and it earns that the same way the second
66
+ did: which theme, if any, is a judgement made once about the whole
67
+ application, and that is what configuration is for (ADR 021). A host
68
+ that leaves it unset gets the structural stylesheet, exactly as before.
69
+
70
+ ## Consequences
71
+
72
+ - Janela looks like something out of the box, and a host that hates it
73
+ removes one line rather than overriding a hundred rules.
74
+ - `janela.css` can stay frozen and boring while the theme moves. A
75
+ revision to the look is not a revision to the grid.
76
+ - The lattice is an inline SVG data URI rather than an image, because a
77
+ stylesheet shipped in a gem cannot rely on a host's asset pipeline to
78
+ resolve a `url()`.
79
+ - Accessibility is the theme's problem, not the host's: reduced motion,
80
+ reduced transparency and increased contrast each fall back to a
81
+ still, solid window, and so does a browser without `backdrop-filter`.
82
+ - A visual change to vitral is visible to every host that opted in, so
83
+ it belongs in the changelog like any other change a host can see
84
+ (ADR 015). It is not a breaking change, because nothing depends on it.
85
+ - A third stylesheet is a third thing for a Sprockets host to declare
86
+ for precompilation. The engine declares both itself.
@@ -0,0 +1,127 @@
1
+ ---
2
+ Date: 2026-09-16
3
+ Status: Accepted
4
+ Related: ADR 003, ADR 005, ADR 008, ADR 009, ADR 018
5
+ Triggers:
6
+ - changing what a click on a value does
7
+ - adding a filter predicate or changing the URL grammar
8
+ - adding a keyboard shortcut
9
+ - anything a mouse can do that a keyboard cannot
10
+ Topics: cross-filtering, urls, accessibility, ransack
11
+ ---
12
+
13
+ # ADR 024: Selecting More Than One Value
14
+
15
+ ## Context
16
+
17
+ A click on a value writes one Ransack condition, `q[status_eq]=paid`,
18
+ and clicking the same value again removes it. One value per dimension.
19
+ Every real dashboard eventually needs two: paid and pending, APAC and
20
+ EU. A commercial tool does this with a modifier click, and everyone
21
+ already knows the gesture.
22
+
23
+ The gesture is the easy part. `toggle(event)` already receives the
24
+ event, so `ctrlKey || metaKey` is there for a table button, and
25
+ Chart.js hands its own click handler the native event. What needs
26
+ deciding is what the URL says, what happens to the null group, and
27
+ what someone without a mouse does instead, because a modifier click is
28
+ invisible, absent on touch and unreachable from a keyboard. Deciding
29
+ the gesture without deciding that last part would build a feature only
30
+ some people can use (#35, #36).
31
+
32
+ Three things were measured against the dummy before writing this,
33
+ rather than assumed:
34
+
35
+ - `status_in: [paid, pending]` passes Janela's allowlist guard and
36
+ returns the sum of both. A dimension's allowlist is per attribute, so
37
+ the predicate needs nothing added.
38
+ - `status_eq: paid` still works, so an existing link keeps working
39
+ whatever a new click writes.
40
+ - `channel_null: 1` together with `channel_in: [web]` **returns zero**.
41
+ Ransack ANDs its conditions, so that combination asks for rows whose
42
+ channel is both null and web.
43
+
44
+ That last one is the whole difficulty. It does not raise. It renders
45
+ an empty dashboard, which reads as a bug in Janela rather than as an
46
+ impossible question.
47
+
48
+ ## Decision
49
+
50
+ **A click selects, a modifier click adds, and the selection is a set.**
51
+
52
+ Following the convention every list in every operating system already
53
+ uses, so that nothing has to be learned:
54
+
55
+ | Gesture | Result |
56
+ | --- | --- |
57
+ | Click an unselected value | That value alone is selected |
58
+ | Click the only selected value | The dimension is cleared |
59
+ | Click a value while others are selected | That value replaces them |
60
+ | Ctrl or Cmd click | That value is added or removed, the rest stay |
61
+
62
+ **Janela writes `_in`, and keeps reading `_eq`.** A click always
63
+ produces `q[status_in][]=paid`, one value or five, so there is one
64
+ shape in the controller, one in the view and one in a stored snapshot.
65
+ A hand written or previously shared `_eq` link keeps working, because
66
+ Ransack accepts it and because a URL somebody already sent should not
67
+ stop working to suit us (ADR 005).
68
+
69
+ **The null group is exclusive within its dimension.** Selecting
70
+ `(none)` clears the other values of that dimension, and selecting a
71
+ value clears `(none)`. The combination is unanswerable, and the honest
72
+ options are to refuse it or to render nothing and let it look broken.
73
+
74
+ Ransack can express the OR through its `g[]` grouping, and that is
75
+ rejected. It would make the URL unreadable, and ADR 005's grammar is
76
+ built on a URL a person can read and edit. It would also have to be
77
+ understood by every stored snapshot and every hand written link
78
+ forever, to serve a question almost nobody asks.
79
+
80
+ **The keyboard gets the same two gestures, not a different feature.**
81
+ A value is already a real `<button>` (ADR 018), so Enter is a click
82
+ and **Ctrl or Cmd with Enter is a modifier click**, using the same
83
+ flags on the same event. Nothing new is invented, and nothing has to
84
+ be learned twice.
85
+
86
+ **Janela binds exactly two keys, and only inside the frame.**
87
+
88
+ - `Escape` clears the frame's filters.
89
+ - `Enter` and `Space` act on the focused value, as they already do.
90
+
91
+ No single letter keys. A single letter belongs to the host
92
+ application, to its own shortcuts, and to any text field on the page.
93
+ Janela is a guest in someone else's application and will not take a
94
+ key that could mean something there. A host that wants `c` for clear
95
+ binds its own control to `janela--frame#clear`, which is how the clear
96
+ button already works.
97
+
98
+ **A chart stays a mouse surface, and the table is the accessible one.**
99
+ A bar is painted pixels with nothing focusable behind it. Rather than
100
+ build a parallel focus model inside a canvas, Janela says plainly that
101
+ the table renders the same data and is operable by keyboard, which is
102
+ what ADR 018 made a table the universal renderer for. A chart
103
+ highlights every selected value rather than one.
104
+
105
+ ## Consequences
106
+
107
+ - A dashboard can answer "paid and pending", which is the ordinary
108
+ question this could not previously express.
109
+ - `Query#selected_value` becomes `selected_values` and returns an
110
+ array. A host that overrode a pane view touches it, so this is a
111
+ breaking change, and it goes in UPGRADING.md with the version that
112
+ carries it (ADR 015).
113
+ - A shared link's shape changes from `q[status_eq]=paid` to
114
+ `q[status_in][]=paid`. Older links keep working, so nothing that was
115
+ sent stops working.
116
+ - The page URL and every pane `src` now carry repeated parameters. The
117
+ controller sorts filters so an unchanged `src` is never reloaded, and
118
+ it must sort the values inside a dimension too or a set will
119
+ serialise two ways and refetch every pane for nothing.
120
+ - Touch has no modifier key, so a touch user gets replace-only
121
+ selection. That is a real gap and this ADR does not close it. The
122
+ answer, when someone needs it, is a control the host can render that
123
+ makes the next clicks additive, with the modifier as its accelerator
124
+ rather than the only path. Not built, because the simple thing has
125
+ not yet failed.
126
+ - Snapshots store filters as JSON, so an array needs nothing new, and
127
+ a snapshot taken under `_eq` still reads.
@@ -65,7 +65,10 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
65
65
  | 019 | A Created Frame Asks the Host Who Owns It | 2026-09-16 | Accepted |
66
66
  | 020 | Formatting Belongs to the Measure | 2026-09-16 | Accepted |
67
67
  | 021 | A Check Has a Name, and a Host Can Silence It | 2026-09-16 | Accepted |
68
+ | 022 | A Host's Route Helpers Work Inside the Engine | 2026-09-16 | Accepted |
69
+ | 023 | Vitral Is a Theme, Not the Stylesheet | 2026-09-16 | Accepted |
70
+ | 024 | Selecting More Than One Value | 2026-09-16 | Accepted |
68
71
 
69
72
  ## Next number
70
73
 
71
- Next ADR: 022
74
+ Next ADR: 025
data/docs/naming.md ADDED
@@ -0,0 +1,172 @@
1
+ ---
2
+ Topics: naming, vocabulary, design, renaming
3
+ ---
4
+
5
+ # Naming Things Is Hard
6
+
7
+ There are two hard problems in computer science, and this page is about
8
+ the one that is not cache invalidation. Every name in Janela was chosen
9
+ on purpose, several were changed after they shipped, and one rule sits
10
+ under all of them.
11
+
12
+ ## The rule
13
+
14
+ > With words like Janela, Frame and Pane I am purposefully selecting
15
+ > uncommon but understandable, conceivable words so that people aren't
16
+ > pigeonholed and can select their own nomenclature.
17
+
18
+ A library that calls its main idea a Dashboard has taken that word from
19
+ every application that installs it. Your app probably already has a
20
+ `Dashboard`, or a `Report`, or an `Insight`, and whatever you call yours
21
+ is the word your users know. So Janela's own vocabulary is deliberately
22
+ a step to the side: close enough to understand on first read, unusual
23
+ enough that it never collides with the words you already use, and never
24
+ the word your users have to see.
25
+
26
+ That gives two tests for any name in this codebase:
27
+
28
+ - **Conceivable.** Someone reading it for the first time can guess what
29
+ it is without looking it up.
30
+ - **Uncommon.** It is unlikely to already be a model, a table, a route
31
+ or a word on your screens.
32
+
33
+ ## The window
34
+
35
+ *Janela* is Portuguese for window. The word is the same in Portugal and
36
+ in Brazil. Once the library is a window, the rest of its anatomy follows
37
+ from the thing itself rather than from a list of synonyms for "chart".
38
+
39
+ | Name | What it is | Why this word |
40
+ | --- | --- | --- |
41
+ | **Janela** | The library | A window onto your data. Uncommon in English, obvious once explained. |
42
+ | **Frame** | A dashboard: a name and a grid of panes | A window frame holds the panes. It is also what Turbo calls the element that makes cross-filtering work (ADR 003), which is a happy accident rather than the reason. |
43
+ | **Pane** | One visual inside a frame, stored as a row | A pane of glass sits in a frame. You look through each one at part of the picture. |
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
+ | **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
+ | **Vitral** | The optional stained glass theme | A stained glass window, in the same language. See ADR 023. |
47
+
48
+ The anatomy was argued over before it was settled. *Sash* was
49
+ considered for the dashboard and rejected: a sash is one layer inside a
50
+ window, the moving part that holds glass, and the thing that holds
51
+ panes is the frame.
52
+
53
+ ## Words that stay ordinary
54
+
55
+ Not everything gets an unusual name, and the exceptions follow the same
56
+ reasoning.
57
+
58
+ **Measure and dimension** are the words the business intelligence field
59
+ already agreed on. An analyst who has used any tool of that kind knows
60
+ exactly what `measure :revenue` and `dimension :region` mean. They are
61
+ also method names inside a model's `janela` block rather than classes
62
+ sitting in your namespace, so there is nothing for them to collide with.
63
+ A domain language should speak its reader's language (ADR 002).
64
+
65
+ **Snapshot** and **owner** are plain because what they describe is
66
+ plain: the numbers frozen at an instant, and whatever a frame belongs
67
+ to. Janela assigns an owner and never reads it, so it has no reason to
68
+ give it a clever name (ADR 009, ADR 019).
69
+
70
+ **The doctor** is the conventional name for a command that reads your
71
+ setup and tells you what is wrong. Homebrew, Flutter, npm and Bundler
72
+ all ship one, so the word already means the right thing (ADR 021).
73
+
74
+ ## Your words, not ours
75
+
76
+ None of Janela's vocabulary has to reach your users. Two places decide
77
+ what they see.
78
+
79
+ **The noun comes from your locale file.** Every heading the engine
80
+ renders uses `Janela::Frame.model_name.human`, so your users read
81
+ whatever you call them:
82
+
83
+ ```yaml
84
+ en:
85
+ activerecord:
86
+ models:
87
+ janela/frame:
88
+ one: "Report"
89
+ other: "Reports"
90
+ ```
91
+
92
+ **The address is wherever you mount it.** Frames live at the mount root
93
+ and a pane's URL sits beneath the same path, so the words in the address
94
+ bar are yours too:
95
+
96
+ ```ruby
97
+ mount Janela::Engine => "/insights"
98
+ ```
99
+
100
+ ```
101
+ /insights every frame
102
+ /insights/3 one frame
103
+ /insights/orders/revenue a pane
104
+ ```
105
+
106
+ ADR 013 first gave frames a configurable path segment of their own. ADR
107
+ 014 took it back out, because a segment named `dashboards` or `reports`
108
+ is exactly the kind of word a host model already owns, and it shadowed
109
+ that model's panes. Keeping Janela's own words out of your URLs turned
110
+ out to be the fix as well as the principle.
111
+
112
+ ## One word, one meaning
113
+
114
+ A single name used for two things is worse than an awkward name, so
115
+ some sentences in this codebase are spelled more carefully than they
116
+ would be in conversation.
117
+
118
+ **Frame** on its own always means the dashboard. The HTML element is
119
+ always written **turbo frame**, in prose, in comments and in commit
120
+ messages, so the two can never be confused.
121
+
122
+ **Grid** always means a frame's layout, its columns and gap. The lines
123
+ drawn between panes by the theme are leading, never grid.
124
+
125
+ **Pane** always means the stored row. That is why the runtime object
126
+ had to give the name up: it was `Janela::Pane` until ADR 014 renamed it
127
+ `Janela::Query` so the record could take the word it deserved.
128
+
129
+ ## When a name turns out wrong
130
+
131
+ Names were changed after release, and each change was treated as
132
+ breaking rather than tidied away:
133
+
134
+ - `janela_dashboard` became `janela_frame`, and the Stimulus controller
135
+ `janela--dashboard` became `janela--frame`.
136
+ - `Janela::Pane` became `Janela::Query`, freeing `Pane` for the record.
137
+ - `Janela::DashboardHelper` became `Janela::FramesHelper`.
138
+
139
+ Every one of those is listed in `UPGRADING.md` with the exact
140
+ replacement, and `bin/rails janela:doctor` finds any old name left in
141
+ your code and names what replaced it (ADR 015). Renaming is allowed. A
142
+ rename a user has to discover for themselves is not.
143
+
144
+ ## The look follows the name
145
+
146
+ The demo and the vitral theme are not decoration picked separately.
147
+ Once the library is a window, the design had one obvious direction.
148
+
149
+ - **The panes are glass.** Each one holds its own colour, and the colour
150
+ cycles by position so a row is never monochrome.
151
+ - **The lines between them are leading,** dark and slightly uneven, the
152
+ way lead holds real stained glass. The theme calls its colour
153
+ `--vitral-came`, after the lead strip itself, but that is a styling
154
+ detail to override rather than a word you need to know.
155
+ - **The light comes from behind.** Shafts fall from a sun at the top
156
+ right, and hovering the mark fans light through the window, splitting
157
+ into its colours as it comes out the front.
158
+ - **The lattice leans toward you.** Its nodes reach for the cursor,
159
+ because a dashboard is meant to respond to the person looking at it.
160
+
161
+ ## Naming something new
162
+
163
+ If you are adding to Janela or forking it, the same checks apply:
164
+
165
+ 1. Would a stranger guess what it is from the name alone?
166
+ 2. Is it a word a Rails application is likely to have already?
167
+ 3. Does it already mean something else in this codebase, or in Rails,
168
+ or in Turbo?
169
+ 4. Is it something a person arranges, which earns a window word, or an
170
+ implementation detail, which gets a plain one?
171
+ 5. If you are renaming, have you added it to `UPGRADING.md` and taught
172
+ the doctor to find the old name?
data/lib/janela/engine.rb CHANGED
@@ -24,7 +24,16 @@ module Janela
24
24
  # Janela's own layout links janela.css, and a host on Sprockets serves it
25
25
  # in production only if something declared it.
26
26
  initializer "janela.assets" do |app|
27
- app.config.assets.precompile << "janela.css" if app.config.respond_to?(:assets) && app.config.assets.respond_to?(:precompile)
27
+ if app.config.respond_to?(:assets) && app.config.assets.respond_to?(:precompile)
28
+ app.config.assets.precompile << "janela.css"
29
+ app.config.assets.precompile << "vitral.css"
30
+ end
31
+ end
32
+
33
+ # A host's route names are only known once its routes are drawn, which is
34
+ # lazy and happens again on every reload in development.
35
+ initializer "janela.host_routes" do |app|
36
+ app.config.after_routes_loaded { Janela::HostRoutes.define! }
28
37
  end
29
38
 
30
39
  initializer "janela.importmap", before: "importmap" do |app|
@@ -0,0 +1,31 @@
1
+ module Janela
2
+ # Route helpers the host application has and the engine does not, forwarded
3
+ # to the host.
4
+ #
5
+ # isolate_namespace points every route helper inside Janela's controllers at
6
+ # the engine's own routes, and a host's ApplicationController runs there,
7
+ # because Janela's controllers inherit it. So an authentication redirect, a
8
+ # rescue_from or an after_action that names one of the host's own routes
9
+ # raises where it would work anywhere else in the application (ADR 022).
10
+ #
11
+ # Only names the engine does not define are forwarded, so the engine's own
12
+ # routes can never be shadowed by a host's. main_app stays the unambiguous
13
+ # way to say either.
14
+ module HostRoutes
15
+ def self.define!(host: Rails.application.routes, engine: Janela::Engine.routes)
16
+ # Routes reload in development, so a name that has gone is removed
17
+ # rather than left behind pointing at nothing.
18
+ instance_methods(false).each { |method| remove_method(method) }
19
+
20
+ forwarded(host: host, engine: engine).each do |name|
21
+ define_method(name) do |*args, **options, &block|
22
+ main_app.public_send(name, *args, **options, &block)
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.forwarded(host: Rails.application.routes, engine: Janela::Engine.routes)
28
+ host.named_routes.helper_names - engine.named_routes.helper_names
29
+ end
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Janela
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/janela.rb CHANGED
@@ -10,6 +10,7 @@ require "janela/definition"
10
10
  require "janela/measure"
11
11
  require "janela/dimension"
12
12
  require "janela/doctor"
13
+ require "janela/host_routes"
13
14
 
14
15
  module Janela
15
16
  class Error < StandardError; end
@@ -24,6 +25,12 @@ module Janela
24
25
  # and authorisation apply to dashboards with no configuration.
25
26
  mattr_accessor :parent_controller, default: "ApplicationController"
26
27
 
28
+ # The stylesheet Janela's own pages load on top of janela.css. Nil means the
29
+ # structural one only, which is what a host that has its own look wants. The
30
+ # gem ships "vitral". A host's own pages are untouched either way: they load
31
+ # whatever that host's layout says (ADR 023).
32
+ mattr_accessor :theme, default: nil
33
+
27
34
  # Checks janela:doctor should not report, by the name it prints beside each
28
35
  # finding. A check that is a false alarm for one application stays a false
29
36
  # alarm, and that is a judgement made once at boot, which is what a setting
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: janela
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jay Killeen
@@ -99,7 +99,9 @@ files:
99
99
  - app/assets/javascripts/janela/chart_controller.js
100
100
  - app/assets/javascripts/janela/frame_controller.js
101
101
  - app/assets/javascripts/janela/vendor/chart.js
102
+ - app/assets/javascripts/janela/vitral_controller.js
102
103
  - app/assets/stylesheets/janela.css
104
+ - app/assets/stylesheets/vitral.css
103
105
  - app/controllers/janela/application_controller.rb
104
106
  - app/controllers/janela/frames_controller.rb
105
107
  - app/controllers/janela/panes_controller.rb
@@ -155,13 +157,18 @@ files:
155
157
  - docs/decisions/019-a-created-frame-asks-the-host-who-owns-it.md
156
158
  - docs/decisions/020-formatting-belongs-to-the-measure.md
157
159
  - docs/decisions/021-a-check-has-a-name-a-host-can-silence.md
160
+ - docs/decisions/022-host-route-helpers-work-inside-the-engine.md
161
+ - docs/decisions/023-vitral-is-a-theme-not-the-stylesheet.md
162
+ - docs/decisions/024-selecting-more-than-one-value.md
158
163
  - docs/decisions/INDEX.md
159
164
  - docs/multi-tenancy.md
165
+ - docs/naming.md
160
166
  - lib/janela.rb
161
167
  - lib/janela/definition.rb
162
168
  - lib/janela/dimension.rb
163
169
  - lib/janela/doctor.rb
164
170
  - lib/janela/engine.rb
171
+ - lib/janela/host_routes.rb
165
172
  - lib/janela/measure.rb
166
173
  - lib/janela/model.rb
167
174
  - lib/janela/version.rb
@@ -176,18 +183,14 @@ metadata:
176
183
  bug_tracker_uri: https://github.com/retail-tasker/janela/issues
177
184
  rubygems_mfa_required: 'true'
178
185
  post_install_message: |
179
- Janela 0.3.0 needs two things from you. Run the migrations, because the
180
- mount root now serves an index of frames:
181
-
182
- bin/rails janela:install:migrations && bin/rails db:migrate
183
-
184
- Then rename the Stimulus controller you register, the helper that wraps
185
- your panes, and a few constants.
186
+ Janela 0.4.0 selects more than one value in a dimension. Most applications
187
+ need do nothing. You need to act only if you override a pane view, where
188
+ selected_value is now selected_values, or if something of yours reads
189
+ Janela's URLs, where a click now writes q[field_in][] rather than
190
+ q[field_eq]. Links already shared keep working.
186
191
 
187
192
  Steps: UPGRADING.md in this gem, or
188
193
  https://github.com/retail-tasker/janela/blob/main/UPGRADING.md
189
-
190
- Then run: bin/rails janela:doctor
191
194
  rdoc_options: []
192
195
  require_paths:
193
196
  - lib