janela 0.8.0 → 0.10.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +41 -2
- data/UPGRADING.md +75 -0
- data/app/assets/javascripts/janela/chart_controller.js +29 -3
- data/app/assets/javascripts/janela/frame_controller.js +15 -0
- data/app/assets/stylesheets/janela.css +16 -8
- data/app/controllers/janela/application_controller.rb +7 -0
- data/app/controllers/janela/panes_controller.rb +22 -7
- data/app/controllers/janela/queries_controller.rb +2 -1
- data/app/helpers/janela/frames_helper.rb +26 -7
- data/app/models/janela/frame.rb +49 -0
- data/app/models/janela/pane.rb +67 -4
- data/app/models/janela/query.rb +10 -2
- data/app/views/janela/frames/_content.html.erb +26 -0
- data/app/views/janela/frames/_frame.html.erb +9 -1
- data/app/views/janela/frames/_pane.html.erb +2 -2
- data/app/views/janela/panes/_content_form.html.erb +33 -0
- data/app/views/janela/panes/_row.html.erb +1 -1
- data/app/views/janela/panes/edit.html.erb +5 -1
- data/app/views/janela/panes/new.html.erb +6 -1
- data/app/views/janela/queries/_query.html.erb +15 -11
- data/config/locales/en.yml +5 -0
- data/db/migrate/20260924000001_add_content_to_janela_panes.rb +16 -0
- data/db/migrate/20260924000002_add_key_to_janela_frames.rb +10 -0
- data/db/migrate/20260928000001_add_default_filter_to_janela_frames.rb +10 -0
- data/docs/composing.md +288 -0
- data/docs/decisions/038-a-ratio-is-a-measure-of-its-own.md +194 -0
- data/docs/decisions/039-a-pane-can-hold-words-and-only-code-writes-markup.md +168 -0
- data/docs/decisions/040-a-host-can-fix-a-frames-filter.md +145 -0
- data/docs/decisions/041-a-host-finds-its-frame-by-owner-and-key.md +112 -0
- data/docs/decisions/042-a-charts-title-is-a-figcaption.md +174 -0
- data/docs/decisions/043-a-frames-default-filter-names-the-model-it-narrows.md +163 -0
- data/docs/decisions/INDEX.md +19 -12
- data/docs/multi-tenancy.md +22 -0
- data/docs/naming.md +7 -0
- data/docs/roadmap.md +129 -8
- data/docs/theming.md +18 -16
- data/lib/janela/definition.rb +8 -0
- data/lib/janela/version.rb +1 -1
- metadata +20 -18
|
@@ -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.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-09-28
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Related: ADR 012, ADR 014, ADR 020, ADR 025, ADR 040
|
|
5
|
+
Triggers:
|
|
6
|
+
- a frame that should always exclude some rows, regardless of who renders it
|
|
7
|
+
- adding a column to Janela::Frame or Janela::Pane
|
|
8
|
+
- a host copying the same where: condition into every view that renders one frame
|
|
9
|
+
- validating a stored Ransack condition at save time rather than at render
|
|
10
|
+
Topics: frames, filters, cross-filtering, persistence, ransack
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# ADR 043: A Frame's Default Filter Names the Model It Narrows
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
#63, found while dogfooding: ADR 040 gives a host a way to narrow a
|
|
18
|
+
frame to the current record, `where: { project_id_eq: @project.id }`,
|
|
19
|
+
code, per request, correctly not data because the value is inherently
|
|
20
|
+
per-record. It does not give a frame a way to say something that is
|
|
21
|
+
true on every render, forever: "this queue never counts an archived
|
|
22
|
+
row." That is not per-record narrowing, it is a permanent property of
|
|
23
|
+
what the dashboard means, and today it can only be expressed by typing
|
|
24
|
+
the same Ransack condition into every view that renders the frame.
|
|
25
|
+
|
|
26
|
+
ADR 012's own argument is that composition moved out of ERB because
|
|
27
|
+
the analyst, not the developer, owns a dashboard, and a decision typed
|
|
28
|
+
into a view is "a file held by the wrong owner." A permanent filter is
|
|
29
|
+
exactly that file, with nowhere on the data side to move to: `Frame`
|
|
30
|
+
and `Pane` have no column that could hold one.
|
|
31
|
+
|
|
32
|
+
### What breaks the obvious shape
|
|
33
|
+
|
|
34
|
+
The obvious answer, a `default_where` column on `Frame` applied to
|
|
35
|
+
every pane in it, runs into a fact ADR 020 already used to reject the
|
|
36
|
+
frame as the layer for a different per-pane setting: a frame can hold
|
|
37
|
+
panes from more than one model. Measured against the demo, reusing the
|
|
38
|
+
existing bound check rather than assuming it:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
Order.janela.narrow(Order.all, "expedited_eq" => "false")
|
|
42
|
+
# => Janela::BadRequest: Order does not allow filtering on expedited_eq.
|
|
43
|
+
# Declare a janela dimension, or add it to ransackable_attributes.
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`expedited` is a real column on `Order`, not a declared dimension. A
|
|
47
|
+
condition applied uniformly to every pane in a frame raises for any
|
|
48
|
+
pane whose model, or whose declared dimension set, does not carry the
|
|
49
|
+
attribute the condition names. ADR 020's own case was a format string
|
|
50
|
+
being "right for the currency and wrong for the count sitting next to
|
|
51
|
+
it"; a stored Ransack condition is at least as specific to one model.
|
|
52
|
+
|
|
53
|
+
### What was considered for where the mismatch goes
|
|
54
|
+
|
|
55
|
+
**Raise for every pane whose model does not match.** Rejected: the
|
|
56
|
+
frame breaks the moment an analyst adds a pane over a second model, or
|
|
57
|
+
the moment a declared dimension is removed from the first, and the
|
|
58
|
+
failure is frame-wide rather than local to the pane that changed.
|
|
59
|
+
|
|
60
|
+
**Skip the condition quietly for a pane it does not apply to.** Rejected
|
|
61
|
+
outright. ADR 024, ADR 032 and ADR 034 all refuse the same shape of
|
|
62
|
+
quiet: a filter that means one thing for one pane and nothing for
|
|
63
|
+
another, on the same frame, with nothing on the page saying so.
|
|
64
|
+
|
|
65
|
+
**Restrict a frame with a default to panes over one model.** Considered
|
|
66
|
+
and rejected as an unnecessary restriction on `Pane`, which already
|
|
67
|
+
allows any model with a `janela` block. The mismatch is the default's
|
|
68
|
+
problem to solve, not a new constraint on what a frame may hold.
|
|
69
|
+
|
|
70
|
+
## Decision
|
|
71
|
+
|
|
72
|
+
**A frame's default filter names the one model it narrows, and only a
|
|
73
|
+
pane over that model takes it.** `Janela::Frame` gains two columns,
|
|
74
|
+
`default_model` and `default_where`, both nullable and present or
|
|
75
|
+
absent together:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
frame.update!(default_model: "orders", default_where: { "status_not_eq" => "archived" })
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**`default_model` is validated the same way `Pane#model` already is**:
|
|
82
|
+
blank, or a route key naming a model with a `janela` block, checked
|
|
83
|
+
through `Janela.definition!` and rescued the same way
|
|
84
|
+
`declared_by_a_janela_block` does. It does not have to match any pane
|
|
85
|
+
the frame currently holds. A frame can carry a default before its first
|
|
86
|
+
matching pane exists, and a pane over a different model is simply never
|
|
87
|
+
narrowed by it, no error, because the condition was never claimed to be
|
|
88
|
+
about it.
|
|
89
|
+
|
|
90
|
+
**`default_where` is validated against that one model's `Definition` at
|
|
91
|
+
save time**, not only discovered wrong at render. `Definition#narrow`
|
|
92
|
+
already runs `Ransack#ransack` and the three checks ADR 025 built
|
|
93
|
+
(dropped filters, disallowed predicates, oversized value lists); a new
|
|
94
|
+
validation calls it against `default_model`'s definition and turns a
|
|
95
|
+
raised `Janela::BadRequest` into a validation error on `default_where`,
|
|
96
|
+
the same sentence a request would have raised, read at the point an
|
|
97
|
+
analyst can still fix it. This is new ground: nothing today validates a
|
|
98
|
+
persisted, arbitrary Ransack condition at save time, only `fixed` and
|
|
99
|
+
the reader's `q[...]`, which are validated fresh on every render because
|
|
100
|
+
neither is ever stored.
|
|
101
|
+
|
|
102
|
+
**It composes as a third layer, ahead of the two ADR 040 already
|
|
103
|
+
built.** `Pane#query` asks its frame for the default that applies to its
|
|
104
|
+
own model and passes it to `Query`, which narrows with it before `fixed`
|
|
105
|
+
narrows again, before the reader's own filters:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
# Janela::Frame
|
|
109
|
+
def default_for(model)
|
|
110
|
+
default_model == model.model_name.route_key ? default_where.to_h : {}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Janela::Query#result
|
|
114
|
+
on = definition.narrow(on || model.all, default) if default.present? # frame, permanent
|
|
115
|
+
on = definition.narrow(on, fixed) if fixed.present? # host, per-record (ADR 040)
|
|
116
|
+
definition.query(measure, by: dimension, where: applicable_filters, on: on, ...) # reader
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Applied even on a pane's own dimension, the same as `fixed`: the point
|
|
120
|
+
of "this queue never counts an archived row" is that no pane on it ever
|
|
121
|
+
shows archived as one of its own bars either. Nothing the reader does
|
|
122
|
+
in `q[...]` can remove it, for the same reason nothing the reader does
|
|
123
|
+
can remove a host's fixed filter.
|
|
124
|
+
|
|
125
|
+
**Only a stored frame can have one.** A hand composed `janela_frame do
|
|
126
|
+
... end` block has no `Frame` row for a default to live on; a host
|
|
127
|
+
composing a page in ERB already writes its own permanent conditions in
|
|
128
|
+
its own code, in one place, which is what ADR 012 left unchanged for
|
|
129
|
+
that path. This is additive to the data-driven half only.
|
|
130
|
+
|
|
131
|
+
**Vocabulary, so the three layers stay distinct.** *Default* is the
|
|
132
|
+
frame's own, permanent, data. *Fixed* stays ADR 040's word for the
|
|
133
|
+
host's own, per-record, code. *Filters*, or `q[...]`, stays the
|
|
134
|
+
reader's. Three words, three owners, and none of them is allowed to
|
|
135
|
+
mean another.
|
|
136
|
+
|
|
137
|
+
## Consequences
|
|
138
|
+
|
|
139
|
+
- An analyst declares "this queue never counts an archived row" once,
|
|
140
|
+
as a row, and it holds across every page that ever renders the frame,
|
|
141
|
+
including one that does not exist yet.
|
|
142
|
+
- **A stale default is a frame-wide failure, not a pane-wide one.**
|
|
143
|
+
If `default_model`'s `janela` block later drops the dimension
|
|
144
|
+
`default_where` names, every pane over that model in the frame raises
|
|
145
|
+
at render, the same "missing pane" treatment `declared_by_a_janela_block`
|
|
146
|
+
already gives a stale measure or dimension, just wider: one condition
|
|
147
|
+
now speaks for every pane that shares its model rather than for one
|
|
148
|
+
row. Worth a line in the engine's own edit form once built.
|
|
149
|
+
- `janela_frames` gains `default_model` and `default_where`, both
|
|
150
|
+
nullable. Existing frames are unaffected either way; needs a migration
|
|
151
|
+
and an `UPGRADING.md` entry (ADR 015).
|
|
152
|
+
- **Not decided here: a frame with panes over two models, each wanting
|
|
153
|
+
its own permanent default.** One `(model, where)` pair per frame is
|
|
154
|
+
what is built. A second model needing one of its own is the trigger to
|
|
155
|
+
revisit this as a has-many rather than a second pair of columns, the
|
|
156
|
+
same way ADR 040 left a signed filter for the day a host's need for
|
|
157
|
+
one is real rather than guessed at.
|
|
158
|
+
- The engine's own frame form needs a way to set both columns together,
|
|
159
|
+
and to clear both together; not built here.
|
|
160
|
+
- What would change this decision: a host needing a permanent filter
|
|
161
|
+
that is not one model's own condition, such as one spanning an
|
|
162
|
+
association two different pane models both reach through. Nothing
|
|
163
|
+
proposes that today.
|
data/docs/decisions/INDEX.md
CHANGED
|
@@ -21,24 +21,25 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
|
|
|
21
21
|
| Topic | ADRs |
|
|
22
22
|
|-------|------|
|
|
23
23
|
| **Vision, scope, forkability** | 001, 010, 012, 037 |
|
|
24
|
-
| **Open-source & host-decoupling** | 001, 022, 036 |
|
|
25
|
-
| **DSL & query layer** | 002, 006, 007, 020, 025 |
|
|
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, 035 |
|
|
27
|
+
| **Authorisation** | 002, 003, 004, 009, 017, 019, 022, 032, 033, 034, 035, 039, 040 |
|
|
28
28
|
| **Performance & storage** | 007, 017, 025 |
|
|
29
|
-
| **
|
|
30
|
-
| **
|
|
31
|
-
| **
|
|
32
|
-
| **
|
|
29
|
+
| **Ordering & formatting** | 007, 020, 038 |
|
|
30
|
+
| **Cross-filtering & Hotwire** | 003, 004, 005, 008, 024, 025, 040, 043 |
|
|
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, 043 |
|
|
33
34
|
| **Naming rule** | 014, 023, 036 |
|
|
34
|
-
| **JavaScript delivery & charts** | 004, 006, 026 |
|
|
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
40
|
| **The doctor & checks** | 021, 025, 032, 033, 035 |
|
|
40
|
-
| **Releases & upgrades** | 015, 021, 032, 034, 035, 036, 037 |
|
|
41
|
-
| **Accessibility & keyboard** | 024 |
|
|
41
|
+
| **Releases & upgrades** | 015, 021, 032, 034, 035, 036, 037, 042 |
|
|
42
|
+
| **Accessibility & keyboard** | 024, 042 |
|
|
42
43
|
| **Security** | 003, 025, 028, 031, 032, 034, 035 |
|
|
43
44
|
| **Testing** | 003 |
|
|
44
45
|
| **Roadmap & planning** | 001, 037 |
|
|
@@ -84,7 +85,13 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
|
|
|
84
85
|
| 035 | A Check Does What Janela Does, or It Says What It Saw | 2026-09-22 | Accepted |
|
|
85
86
|
| 036 | Janela Publishes What a Theme May Target, and Vitral Is Only One | 2026-09-22 | Accepted |
|
|
86
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 |
|
|
93
|
+
| 043 | A Frame's Default Filter Names the Model It Narrows | 2026-09-28 | Accepted |
|
|
87
94
|
|
|
88
95
|
## Next number
|
|
89
96
|
|
|
90
|
-
Next ADR:
|
|
97
|
+
Next ADR: 044
|
data/docs/multi-tenancy.md
CHANGED
|
@@ -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
CHANGED
|
@@ -2,7 +2,125 @@
|
|
|
2
2
|
Topics: roadmap, releases, scope, planning
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
#
|
|
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>
|
|
6
124
|
|
|
7
125
|
Janela is alpha. It works, it is tested against a real Rails application
|
|
8
126
|
in a real browser, and its public surface has changed in three of the
|
|
@@ -39,6 +157,8 @@ A check nobody can prove is working is a check nobody should rely on.
|
|
|
39
157
|
|
|
40
158
|
## In 1.0
|
|
41
159
|
|
|
160
|
+
The eight lights burning in the near ground.
|
|
161
|
+
|
|
42
162
|
| Issue | What |
|
|
43
163
|
| --- | --- |
|
|
44
164
|
| [#24](https://github.com/retail-tasker/janela/issues/24) | A chart's height and aspect ratio are the host's to set |
|
|
@@ -53,10 +173,10 @@ A check nobody can prove is working is a check nobody should rely on.
|
|
|
53
173
|
Progress is tracked on the
|
|
54
174
|
[1.0 milestone](https://github.com/retail-tasker/janela/milestone/2).
|
|
55
175
|
|
|
56
|
-
##
|
|
176
|
+
## Horizonte
|
|
57
177
|
|
|
58
|
-
|
|
59
|
-
refusal.
|
|
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.
|
|
60
180
|
|
|
61
181
|
- **Drill-down on time panes** ([#18](https://github.com/retail-tasker/janela/issues/18)).
|
|
62
182
|
Clicking a month could filter every other pane to it, or narrow that
|
|
@@ -70,10 +190,11 @@ refusal.
|
|
|
70
190
|
|
|
71
191
|
## Not coming
|
|
72
192
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
77
198
|
|
|
78
199
|
Natural-language query. A separate data warehouse. A row-level-security
|
|
79
200
|
subsystem, because your application already has Pundit or CanCanCan and
|