keystone_ui 0.44.0 → 0.46.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/app/components/keystone/ui/bar_chart_component.html.erb +13 -0
- data/app/components/keystone/ui/bar_chart_component.rb +82 -0
- data/app/components/keystone/ui/column.rb +3 -2
- data/app/components/keystone/ui/data_table_component.html.erb +18 -0
- data/app/components/keystone/ui/data_table_component.rb +35 -3
- data/app/helpers/keystone_ui_helper.rb +4 -0
- data/lib/keystone_ui/safelist.rb +1 -0
- data/lib/keystone_ui/version.rb +1 -1
- data/the_local/agents/keystone_ui-develop.md +12 -2
- data/the_local/interface.yml +1 -0
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1209af69898ff5cfa1879680b8d9de9a9ca569474d6471a85f906f1bdcac7425
|
|
4
|
+
data.tar.gz: 4f7d5750c98755ccc5ac4a7c17095f018219e731417101d60e2ea5b2f2e164d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 251cec86be528ce9676410c4eb8f317df3954060faa79c4e841c918681618b39f7778d2fd22e51116ec12fd4237b5dd1f617d549bd06e0c4149c412869b5da25
|
|
7
|
+
data.tar.gz: d1f9983ec813ce4ef08ee138470df4c7d60b583a07742fa0855a1d4b5ca7b69a5d1c3c0b98a0ea11defbd026c9cbe7aa99cc0a65e53613808e33446722666cbf
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg class="ks-bar-chart" viewBox="0 0 <%= Keystone::Ui::BarChartComponent::WIDTH %> <%= Keystone::Ui::BarChartComponent::HEIGHT %>" role="img">
|
|
2
|
+
<% ticks.each do |tick| %>
|
|
3
|
+
<line class="ks-bar-chart-grid" x1="<%= Keystone::Ui::BarChartComponent::LEFT %>" x2="<%= Keystone::Ui::BarChartComponent::WIDTH - Keystone::Ui::BarChartComponent::RIGHT %>" y1="<%= tick[:y].round(2) %>" y2="<%= tick[:y].round(2) %>"></line>
|
|
4
|
+
<text class="ks-bar-chart-text" data-axis-tick x="<%= axis_x %>" y="<%= (tick[:y] + 4).round(2) %>" text-anchor="end"><%= tick[:label] %></text>
|
|
5
|
+
<% end %>
|
|
6
|
+
<% drawn_bars.each do |bar| %>
|
|
7
|
+
<rect class="<%= bar[:classes] %>" x="<%= bar[:x].round(2) %>" y="<%= bar[:y].round(2) %>" width="<%= bar[:width].round(2) %>" height="<%= bar[:height].round(2) %>"></rect>
|
|
8
|
+
<% if show_values? %>
|
|
9
|
+
<text class="ks-bar-chart-value" x="<%= bar[:center].round(2) %>" y="<%= (bar[:y] - 4).round(2) %>" text-anchor="middle"><%= bar[:value] %></text>
|
|
10
|
+
<% end %>
|
|
11
|
+
<text class="ks-bar-chart-text" data-bar-label x="<%= bar[:center].round(2) %>" y="<%= label_baseline %>" text-anchor="middle"><%= bar[:label] %></text>
|
|
12
|
+
<% end %>
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class BarChartComponent < ViewComponent::Base
|
|
6
|
+
WIDTH = 600
|
|
7
|
+
HEIGHT = 240
|
|
8
|
+
LEFT = 40
|
|
9
|
+
RIGHT = 8
|
|
10
|
+
TOP = 18
|
|
11
|
+
BOTTOM = 24
|
|
12
|
+
BAR_SHARE = 0.7
|
|
13
|
+
TICK_COUNT = 4
|
|
14
|
+
ROUND_STEPS = [ 1, 2, 2.5, 5, 10 ].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :bars
|
|
17
|
+
|
|
18
|
+
def initialize(bars:, show_values: false)
|
|
19
|
+
@bars = bars
|
|
20
|
+
@show_values = show_values
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def show_values?
|
|
24
|
+
@show_values
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def drawn_bars
|
|
28
|
+
bars.each_with_index.map do |bar, index|
|
|
29
|
+
height = bar[:value].to_f / axis_max * plot_height
|
|
30
|
+
{ x: LEFT + (index * band) + (band * (1 - BAR_SHARE) / 2), y: TOP + plot_height - height, width: band * BAR_SHARE, height: height,
|
|
31
|
+
center: LEFT + (index * band) + (band / 2), label: bar[:label], value: bar[:value],
|
|
32
|
+
classes: [ "ks-bar-chart-bar", ("ks-bar-chart-bar-shaded" if bar[:shaded]) ].compact.join(" ") }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ticks
|
|
37
|
+
(0..TICK_COUNT).map do |position|
|
|
38
|
+
value = (step * position).round(6)
|
|
39
|
+
{ y: TOP + plot_height - (value / axis_max * plot_height), label: (value % 1).zero? ? value.to_i.to_s : value.to_s }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def axis_x
|
|
44
|
+
LEFT - 6
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def label_baseline
|
|
48
|
+
HEIGHT - 6
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def axis_max
|
|
54
|
+
step * (largest / step).ceil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def step
|
|
58
|
+
@step ||= begin
|
|
59
|
+
raw = largest / TICK_COUNT
|
|
60
|
+
magnitude = 10.0**Math.log10(raw).floor
|
|
61
|
+
ROUND_STEPS.map { |round| round * magnitude }.find { |candidate| candidate >= raw }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def largest
|
|
66
|
+
bars.map { |bar| bar[:value].to_f }.select(&:positive?).max || 1.0
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def band
|
|
70
|
+
plot_width / [ bars.size, 1 ].max
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def plot_width
|
|
74
|
+
WIDTH - LEFT - RIGHT
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def plot_height
|
|
78
|
+
HEIGHT - TOP - BOTTOM
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
module Keystone
|
|
4
4
|
module Ui
|
|
5
5
|
class Column
|
|
6
|
-
attr_reader :key, :header_text
|
|
6
|
+
attr_reader :key, :header_text, :align
|
|
7
7
|
|
|
8
|
-
def initialize(key, header_text, mobile_hidden: false, sortable: false, hideable: false, locked: false)
|
|
8
|
+
def initialize(key, header_text, mobile_hidden: false, sortable: false, hideable: false, locked: false, align: nil)
|
|
9
9
|
@key = key
|
|
10
|
+
@align = align
|
|
10
11
|
@header_text = header_text
|
|
11
12
|
@mobile_hidden = mobile_hidden
|
|
12
13
|
@sortable = sortable
|
|
@@ -36,6 +36,14 @@
|
|
|
36
36
|
<% end %>
|
|
37
37
|
<% else %>
|
|
38
38
|
<% @items.each_with_index do |item, idx| %>
|
|
39
|
+
<% if (group = group_starting_at(idx)) %>
|
|
40
|
+
<tr class="<%= Keystone::Ui::DataTableComponent::HEAD_CLASSES %>" data-table-group>
|
|
41
|
+
<% group_cells(group).each do |cell| %>
|
|
42
|
+
<th scope="colgroup" class="<%= cell[:classes] %>"><%= cell[:value] %></th>
|
|
43
|
+
<% end %>
|
|
44
|
+
<% if actions? %><th></th><% end %>
|
|
45
|
+
</tr>
|
|
46
|
+
<% end %>
|
|
39
47
|
<tr>
|
|
40
48
|
<% row_cells[idx].each do |cell| %>
|
|
41
49
|
<td class="<%= cell[:classes] %>">
|
|
@@ -60,5 +68,15 @@
|
|
|
60
68
|
<% end %>
|
|
61
69
|
<% end %>
|
|
62
70
|
</tbody>
|
|
71
|
+
<% if total_cells.any? %>
|
|
72
|
+
<tfoot class="<%= Keystone::Ui::DataTableComponent::HEAD_CLASSES %>">
|
|
73
|
+
<tr>
|
|
74
|
+
<% total_cells.each do |cell| %>
|
|
75
|
+
<th scope="row" class="<%= cell[:classes] %>"><%= cell[:value] %></th>
|
|
76
|
+
<% end %>
|
|
77
|
+
<% if actions? %><th></th><% end %>
|
|
78
|
+
</tr>
|
|
79
|
+
</tfoot>
|
|
80
|
+
<% end %>
|
|
63
81
|
</table>
|
|
64
82
|
</div>
|
|
@@ -16,6 +16,7 @@ module Keystone
|
|
|
16
16
|
ROW_CLASSES_MIDDLE = "ks-table-cell-middle text-sm whitespace-nowrap"
|
|
17
17
|
ROW_CLASSES_LAST = "ks-table-cell-last text-right text-sm whitespace-nowrap"
|
|
18
18
|
|
|
19
|
+
ALIGN_CLASSES = { start: "text-left", end: "text-right" }.freeze
|
|
19
20
|
MOBILE_HIDDEN_CLASSES = "hidden sm:table-cell"
|
|
20
21
|
HEADER_LOCKED_CLASSES = "ks-table-header-locked sticky left-0"
|
|
21
22
|
CELL_LOCKED_CLASSES = "ks-table-cell-locked sticky left-0"
|
|
@@ -29,8 +30,11 @@ module Keystone
|
|
|
29
30
|
SORT_DESC_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a.75.75 0 0 1 .75.75v10.638l3.96-4.158a.75.75 0 1 1 1.08 1.04l-5.25 5.5a.75.75 0 0 1-1.08 0l-5.25-5.5a.75.75 0 0 1 1.08-1.04l3.96 4.158V3.75A.75.75 0 0 1 10 3Z" clip-rule="evenodd" /></svg>'
|
|
30
31
|
SORT_NEUTRAL_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a.75.75 0 0 1 .55.24l3.25 3.5a.75.75 0 1 1-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 0 1-1.1-1.02l3.25-3.5A.75.75 0 0 1 10 3Zm-3.76 9.2a.75.75 0 0 1 1.06.04l2.7 2.908 2.7-2.908a.75.75 0 1 1 1.1 1.02l-3.25 3.5a.75.75 0 0 1-1.1 0l-3.25-3.5a.75.75 0 0 1 .04-1.06Z" clip-rule="evenodd" /></svg>'
|
|
31
32
|
|
|
32
|
-
def initialize(items:, columns:, empty_message: nil, sort: nil, sort_direction: nil, sort_url: nil, hidden_columns: [], key: nil)
|
|
33
|
+
def initialize(items:, columns:, empty_message: nil, sort: nil, sort_direction: nil, sort_url: nil, hidden_columns: [], key: nil, group_by: nil, group_totals: {}, total: nil)
|
|
33
34
|
@items = items.to_a
|
|
35
|
+
@group_by = group_by
|
|
36
|
+
@group_totals = group_totals.to_h.transform_keys(&:to_s)
|
|
37
|
+
@total = total&.to_h&.transform_keys(&:to_s)
|
|
34
38
|
@all_columns = columns.map { |col| normalize_column(col) }
|
|
35
39
|
@default_hidden = hidden_columns
|
|
36
40
|
@layout = SavedLayout.new(columns: @all_columns, value: nil, default_hidden: hidden_columns)
|
|
@@ -83,7 +87,7 @@ module Keystone
|
|
|
83
87
|
|
|
84
88
|
cell = {
|
|
85
89
|
label: column.header_text,
|
|
86
|
-
classes: tokens.join(" "),
|
|
90
|
+
classes: aligned(tokens.join(" "), column),
|
|
87
91
|
scope: "col"
|
|
88
92
|
}
|
|
89
93
|
|
|
@@ -127,7 +131,7 @@ module Keystone
|
|
|
127
131
|
|
|
128
132
|
cell = {
|
|
129
133
|
value: resolve_value(item, column.key),
|
|
130
|
-
classes: tokens.join(" ")
|
|
134
|
+
classes: aligned(tokens.join(" "), column)
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
link_block = @link_blocks[column.key]
|
|
@@ -146,6 +150,28 @@ module Keystone
|
|
|
146
150
|
visual_column_count
|
|
147
151
|
end
|
|
148
152
|
|
|
153
|
+
def group_starting_at(index)
|
|
154
|
+
return unless @group_by
|
|
155
|
+
|
|
156
|
+
name = resolve_value(@items[index], @group_by)
|
|
157
|
+
name if index.zero? || name != resolve_value(@items[index - 1], @group_by)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def total_cells
|
|
161
|
+
return [] unless @total
|
|
162
|
+
|
|
163
|
+
@columns.map.with_index do |column, index|
|
|
164
|
+
{ value: @total[column.key.to_s], classes: aligned(header_classes_for(index), column) }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def group_cells(name)
|
|
169
|
+
subtotals = @group_totals.fetch(name.to_s, {}).to_h.transform_keys(&:to_s)
|
|
170
|
+
@columns.map.with_index do |column, index|
|
|
171
|
+
{ value: index.zero? ? name : subtotals[column.key.to_s], classes: aligned(header_classes_for(index), column) }
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
149
175
|
private
|
|
150
176
|
|
|
151
177
|
def apply_saved_layout
|
|
@@ -170,6 +196,12 @@ module Keystone
|
|
|
170
196
|
end
|
|
171
197
|
end
|
|
172
198
|
|
|
199
|
+
def aligned(classes, column)
|
|
200
|
+
return classes unless column.align
|
|
201
|
+
|
|
202
|
+
[ *classes.split.grep_v(/\Atext-(left|right)\z/), ALIGN_CLASSES.fetch(column.align) ].join(" ")
|
|
203
|
+
end
|
|
204
|
+
|
|
173
205
|
def resolve_value(item, key)
|
|
174
206
|
if item.respond_to?(key)
|
|
175
207
|
item.public_send(key)
|
|
@@ -21,6 +21,10 @@ module KeystoneUiHelper
|
|
|
21
21
|
render Keystone::Ui::LineChartComponent.new(**args)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
def ui_bar_chart(**args)
|
|
25
|
+
render Keystone::Ui::BarChartComponent.new(**args)
|
|
26
|
+
end
|
|
27
|
+
|
|
24
28
|
def ui_cta_banner(**args, &block)
|
|
25
29
|
render Keystone::Ui::CtaBannerComponent.new(**args), &block
|
|
26
30
|
end
|
data/lib/keystone_ui/safelist.rb
CHANGED
data/lib/keystone_ui/version.rb
CHANGED
|
@@ -82,6 +82,7 @@ not available in the app yet, that is `keystone_ui-install`'s job.
|
|
|
82
82
|
- `ui_swipe_deck` — a stack of cards accepted or rejected by button or swipe.
|
|
83
83
|
- `ui_chart_card` — a titled card holding a chart.
|
|
84
84
|
- `ui_line_chart` — a line chart with one line per series, over labels or dates.
|
|
85
|
+
- `ui_bar_chart` — a bar chart drawn on the server, which needs no JavaScript and prints.
|
|
85
86
|
- `ui_funnel` — a conversion funnel drawn as bars or one joined shape.
|
|
86
87
|
- `ui_bucket` — an upright container filled toward a goal.
|
|
87
88
|
- `ui_bucket_series` — a wrapping row of buckets.
|
|
@@ -333,13 +334,18 @@ two exceptions: `ui_page`'s `padding:` treats any value other than `:none` as
|
|
|
333
334
|
|
|
334
335
|
8. Build tables.
|
|
335
336
|
|
|
336
|
-
- `ui_data_table(items:, columns:, empty_message: nil, sort: nil, sort_direction: nil, sort_url: nil, hidden_columns: [], key: nil)`
|
|
337
|
+
- `ui_data_table(items:, columns:, empty_message: nil, sort: nil, sort_direction: nil, sort_url: nil, hidden_columns: [], key: nil, group_by: nil, group_totals: {}, total: nil)`
|
|
337
338
|
takes a block yielding the table. `items:` are records or hashes, and a
|
|
338
339
|
cell's value is the column key called on the item, or `item[key]`.
|
|
340
|
+
`group_by:` lists rows under heading rows named by that key's value,
|
|
341
|
+
`group_totals:` (`{ "Group name" => { column_key => value } }`) puts
|
|
342
|
+
subtotals in each heading row, and `total:` (`{ column_key => value }`)
|
|
343
|
+
ends the table with a total row. Pass every subtotal and total already
|
|
344
|
+
calculated. `align: :start` or `:end` on a `Column` aligns it left or right.
|
|
339
345
|
- `columns:` takes `{ key: "Label" }` hashes when every column is plain.
|
|
340
346
|
Switch the whole set to `Keystone::Ui::Column` objects once one column
|
|
341
347
|
needs an option.
|
|
342
|
-
- `Keystone::Ui::Column.new(key, header_text, mobile_hidden: false, sortable: false, hideable: false, locked: false)`
|
|
348
|
+
- `Keystone::Ui::Column.new(key, header_text, mobile_hidden: false, sortable: false, hideable: false, locked: false, align: nil)`
|
|
343
349
|
— `mobile_hidden:` hides the column below `sm:`, `sortable:` gives it a
|
|
344
350
|
sort header, and `hideable:` lets it be hidden and moved.
|
|
345
351
|
`locked: true` on the first column keeps it in view while the rest scrolls
|
|
@@ -450,6 +456,10 @@ two exceptions: `ui_page`'s `padding:` treats any value other than `:none` as
|
|
|
450
456
|
|
|
451
457
|
- `ui_chart_card(title:, height: :md)` takes a block holding a chart, with
|
|
452
458
|
`height:` `:sm` `:md` `:lg`.
|
|
459
|
+
- `ui_bar_chart(bars:, show_values: false)` — `bars:` is
|
|
460
|
+
`[{ label:, value:, shaded: }, ...]`, where `shaded: true` draws a bar
|
|
461
|
+
faded. It is drawn on the server as an SVG, so use it, not
|
|
462
|
+
`ui_line_chart`, for a chart that must print or show without JavaScript.
|
|
453
463
|
- `ui_line_chart(series:, labels: nil, dates: nil, height: :md)` —
|
|
454
464
|
`series:` is `[{ name:, data:, color:, dashed: }, ...]`, where `color:`
|
|
455
465
|
is a CSS colour and `dashed: true` is optional. Pass exactly one of
|
data/the_local/interface.yml
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: keystone_ui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.46.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tyler Schneider
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.22.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.22.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: view_component
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -82,6 +82,8 @@ files:
|
|
|
82
82
|
- app/components/keystone/ui/alert_component.rb
|
|
83
83
|
- app/components/keystone/ui/badge_component.html.erb
|
|
84
84
|
- app/components/keystone/ui/badge_component.rb
|
|
85
|
+
- app/components/keystone/ui/bar_chart_component.html.erb
|
|
86
|
+
- app/components/keystone/ui/bar_chart_component.rb
|
|
85
87
|
- app/components/keystone/ui/bottom_nav_component.html.erb
|
|
86
88
|
- app/components/keystone/ui/bottom_nav_component.rb
|
|
87
89
|
- app/components/keystone/ui/bottom_nav_item_component.html.erb
|