plutonium 0.66.0 → 0.68.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/.claude/skills/plutonium-dashboard/SKILL.md +17 -2
- data/.claude/skills/plutonium-resource/SKILL.md +17 -10
- data/.claude/skills/plutonium-ui/SKILL.md +13 -3
- data/CHANGELOG.md +35 -0
- data/SECURITY.md +1 -1
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +56 -3
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +32 -32
- data/app/assets/plutonium.min.js.map +4 -4
- data/config/brakeman.ignore +1 -1
- data/config/locales/en/dashboard.yml +1 -0
- data/config/locales/en/js.yml +4 -0
- data/config/locales/en/ui.yml +19 -0
- data/docs/reference/dashboard/dsl.md +54 -3
- data/docs/reference/resource/definition.md +14 -7
- data/docs/reference/resource/query.md +11 -1
- data/docs/reference/ui/displays.md +19 -2
- data/docs/reference/ui/forms.md +77 -4
- data/gemfiles/rails_7.gemfile +1 -0
- data/gemfiles/rails_7.gemfile.lock +13 -3
- data/gemfiles/rails_8.0.gemfile +1 -0
- data/gemfiles/rails_8.0.gemfile.lock +10 -1
- data/gemfiles/rails_8.1.gemfile +1 -0
- data/gemfiles/rails_8.1.gemfile.lock +10 -1
- data/lib/plutonium/auth/rodauth.rb +7 -0
- data/lib/plutonium/core/controller.rb +0 -2
- data/lib/plutonium/dashboard/base.rb +80 -1
- data/lib/plutonium/dashboard/card.rb +12 -13
- data/lib/plutonium/dashboard/controller.rb +28 -10
- data/lib/plutonium/dashboard/dsl.rb +59 -2
- data/lib/plutonium/dashboard/evaluation.rb +30 -0
- data/lib/plutonium/dashboard/filter.rb +114 -0
- data/lib/plutonium/dashboard/link.rb +35 -0
- data/lib/plutonium/dashboard/route_resolution.rb +6 -2
- data/lib/plutonium/helpers/content_helper.rb +0 -3
- data/lib/plutonium/query/filter.rb +2 -1
- data/lib/plutonium/query/filters/tags.rb +36 -0
- data/lib/plutonium/resource/query_object.rb +6 -0
- data/lib/plutonium/routing/dashboard_registration.rb +5 -0
- data/lib/plutonium/ui/avatar.rb +2 -2
- data/lib/plutonium/ui/component/resolves_tags.rb +13 -0
- data/lib/plutonium/ui/dashboard/board.rb +4 -1
- data/lib/plutonium/ui/dashboard/frame.rb +1 -1
- data/lib/plutonium/ui/dashboard/metric.rb +18 -5
- data/lib/plutonium/ui/dashboard/toolbar.rb +76 -0
- data/lib/plutonium/ui/display/base.rb +50 -2
- data/lib/plutonium/ui/display/components/badge.rb +1 -1
- data/lib/plutonium/ui/display/components/binary.rb +20 -0
- data/lib/plutonium/ui/display/components/boolean.rb +1 -1
- data/lib/plutonium/ui/display/components/code.rb +23 -0
- data/lib/plutonium/ui/display/components/color.rb +14 -6
- data/lib/plutonium/ui/display/components/duration.rb +31 -0
- data/lib/plutonium/ui/display/components/file_size.rb +22 -0
- data/lib/plutonium/ui/display/components/key_value.rb +37 -0
- data/lib/plutonium/ui/display/components/list.rb +33 -0
- data/lib/plutonium/ui/display/components/number.rb +32 -0
- data/lib/plutonium/ui/display/components/progress.rb +36 -0
- data/lib/plutonium/ui/display/components/rating.rb +36 -0
- data/lib/plutonium/ui/display/components/relative_time.rb +25 -0
- data/lib/plutonium/ui/display/components/rich_text.rb +23 -0
- data/lib/plutonium/ui/display/components/tags.rb +18 -0
- data/lib/plutonium/ui/display/options/inferred_types.rb +13 -0
- data/lib/plutonium/ui/display/theme.rb +24 -2
- data/lib/plutonium/ui/form/base.rb +50 -1
- data/lib/plutonium/ui/form/components/binary.rb +40 -0
- data/lib/plutonium/ui/form/components/key_value_store.rb +22 -71
- data/lib/plutonium/ui/form/components/list.rb +46 -0
- data/lib/plutonium/ui/form/components/rating.rb +51 -0
- data/lib/plutonium/ui/form/components/rich_text.rb +20 -0
- data/lib/plutonium/ui/form/components/secure_association.rb +31 -8
- data/lib/plutonium/ui/form/components/secure_association_checkboxes.rb +30 -0
- data/lib/plutonium/ui/form/components/secure_association_choices.rb +123 -0
- data/lib/plutonium/ui/form/components/secure_association_radio_buttons.rb +45 -0
- data/lib/plutonium/ui/form/components/secure_association_toggles.rb +18 -0
- data/lib/plutonium/ui/form/components/tags.rb +45 -0
- data/lib/plutonium/ui/form/components/tags_filter.rb +40 -0
- data/lib/plutonium/ui/form/components/uppy.rb +1 -1
- data/lib/plutonium/ui/form/options/inferred_types.rb +16 -15
- data/lib/plutonium/ui/form/theme.rb +34 -4
- data/lib/plutonium/ui/grid/card.rb +25 -5
- data/lib/plutonium/ui/layout/base.rb +10 -1
- data/lib/plutonium/ui/options/rich_text_field.rb +18 -0
- data/lib/plutonium/ui/options/secret_field.rb +32 -0
- data/lib/plutonium/ui/options/tags_field.rb +23 -0
- data/lib/plutonium/ui/page/dashboard.rb +1 -0
- data/lib/plutonium/ui/table/components/scopes_bar.rb +1 -1
- data/lib/plutonium/ui/table/components/scopes_pills.rb +1 -1
- data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
- data/lib/plutonium/ui/table/components/view_switcher.rb +4 -2
- data/lib/plutonium/ui/table/display_theme.rb +30 -2
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/plutonium.gemspec +1 -1
- data/src/css/components.css +43 -0
- data/src/css/flatpickr.css +91 -0
- data/src/js/controllers/easymde_controller.js +1 -0
- data/src/js/controllers/register_controllers.js +2 -0
- data/src/js/controllers/slim_select_controller.js +32 -3
- data/src/js/controllers/timeago_controller.js +40 -0
- metadata +35 -4
|
@@ -9,7 +9,7 @@ module Plutonium
|
|
|
9
9
|
#
|
|
10
10
|
# {value: 1_240} # just the number
|
|
11
11
|
# {value: 1_240, previous: 1_100} # change computed: +12.7%
|
|
12
|
-
# {value: 1_240, change: 12.7} #
|
|
12
|
+
# {value: 1_240, change: 12.7} # +12.7% (+12.7 pts with change_unit: :points)
|
|
13
13
|
# {value: 1_240, change: "+140", trend: :up} # change shown verbatim
|
|
14
14
|
# {value: 98.2, change_label: "vs. last week"} # caption after the change
|
|
15
15
|
#
|
|
@@ -36,13 +36,22 @@ module Plutonium
|
|
|
36
36
|
data[:change_label] ||= card.options[:change_label]
|
|
37
37
|
|
|
38
38
|
if !data.key?(:change) && data[:previous].is_a?(Numeric) && data[:value].is_a?(Numeric)
|
|
39
|
-
data[:change] = percent_change(data[:value], data[:previous])
|
|
39
|
+
data[:change] = points? ? data[:value] - data[:previous] : percent_change(data[:value], data[:previous])
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Round to the precision the change is shown at, so float noise
|
|
43
|
+
# (0.3 against 0.1 + 0.2) reads as no change rather than "-0" heading
|
|
44
|
+
# down. `+ 0.0` turns a rounded -0.0 into 0.0.
|
|
45
|
+
data[:change] = data[:change].round(1) + 0.0 if data[:change].is_a?(Numeric)
|
|
46
|
+
|
|
47
|
+
# A `previous` of zero leaves a percent `change` nil but still sets
|
|
48
|
+
# the trend key, so a captioned row shows a neutral dash.
|
|
42
49
|
data[:trend] ||= infer_trend(data[:change]) if data.key?(:change)
|
|
43
50
|
data
|
|
44
51
|
end
|
|
45
52
|
|
|
53
|
+
def points? = card.options[:change_unit] == :points
|
|
54
|
+
|
|
46
55
|
def percent_change(value, previous)
|
|
47
56
|
return nil if previous.zero?
|
|
48
57
|
|
|
@@ -65,9 +74,12 @@ module Plutonium
|
|
|
65
74
|
end
|
|
66
75
|
|
|
67
76
|
def render_change(data)
|
|
77
|
+
# A caption with no change or trend (a count of ratings, say) is
|
|
78
|
+
# not a trend: no icon, neutral tone.
|
|
79
|
+
trending = data.key?(:trend)
|
|
68
80
|
trend = data[:trend] || :flat
|
|
69
|
-
div(class: tokens("pu-metric-change", change_tone_class(trend))) do
|
|
70
|
-
render_trend_icon(trend)
|
|
81
|
+
div(class: tokens("pu-metric-change", change_tone_class(trending ? trend : :flat))) do
|
|
82
|
+
render_trend_icon(trend) if trending
|
|
71
83
|
span(class: "font-medium") { format_change(data[:change]) } if data[:change]
|
|
72
84
|
span(class: "pu-metric-change-label") { data[:change_label] } if data[:change_label].present?
|
|
73
85
|
end
|
|
@@ -93,7 +105,8 @@ module Plutonium
|
|
|
93
105
|
case change
|
|
94
106
|
when Numeric
|
|
95
107
|
sign = change.positive? ? "+" : ""
|
|
96
|
-
|
|
108
|
+
rounded = number_to_rounded(change, precision: 1, strip_insignificant_zeros: true)
|
|
109
|
+
points? ? t("plutonium.dashboard.metric.points", change: "#{sign}#{rounded}") : "#{sign}#{rounded}%"
|
|
97
110
|
else change.to_s
|
|
98
111
|
end
|
|
99
112
|
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Dashboard
|
|
6
|
+
# The row above the card grid: each filter as a segmented control of
|
|
7
|
+
# links, so the choice is a plain URL that survives a reload and can be
|
|
8
|
+
# shared, and the dashboard's links on the right.
|
|
9
|
+
class Toolbar < Plutonium::UI::Component::Base
|
|
10
|
+
def initialize(dashboard:)
|
|
11
|
+
@dashboard = dashboard
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render?
|
|
15
|
+
dashboard.class.filters.any? || links.any?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def view_template
|
|
19
|
+
div(class: "pu-dashboard-toolbar mb-4 flex flex-wrap items-center justify-between gap-3") do
|
|
20
|
+
div(class: "flex flex-wrap items-center gap-3") do
|
|
21
|
+
dashboard.class.filters.each { |filter| render_filter(filter) }
|
|
22
|
+
end
|
|
23
|
+
div(class: "flex flex-wrap items-center gap-2") do
|
|
24
|
+
links.each { |link, href| render_link(link, href) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :dashboard
|
|
32
|
+
|
|
33
|
+
# Visible links paired with their href. A link whose `href:` resolves
|
|
34
|
+
# to nil (a proc that returns nothing for this user) is left out
|
|
35
|
+
# rather than rendered as a dead button.
|
|
36
|
+
def links
|
|
37
|
+
@links ||= dashboard.visible_links.filter_map do |link|
|
|
38
|
+
href = link.href_for(dashboard)
|
|
39
|
+
[link, href] if href.present?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def render_filter(filter)
|
|
44
|
+
current = dashboard.filter_value(filter.key)
|
|
45
|
+
nav(class: "pu-dashboard-filter inline-flex rounded-[var(--pu-radius-md)] border border-[var(--pu-border)] p-0.5",
|
|
46
|
+
aria: {label: filter.label}, data: {dashboard_filter: filter.key}) do
|
|
47
|
+
dashboard.filter_choices(filter).each do |value, label|
|
|
48
|
+
selected = value == current
|
|
49
|
+
a(
|
|
50
|
+
href: filter_url(filter, value),
|
|
51
|
+
class: tokens(
|
|
52
|
+
"rounded-[var(--pu-radius-sm)] px-3 py-1 text-sm transition-colors",
|
|
53
|
+
selected ? "bg-[var(--pu-surface-alt)] font-medium text-[var(--pu-text)]" : "text-[var(--pu-text-muted)] hover:text-[var(--pu-text)]"
|
|
54
|
+
),
|
|
55
|
+
aria: {current: selected ? "true" : nil}
|
|
56
|
+
) { Plutonium::Translation.resolve(label) }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def render_link(link, href)
|
|
62
|
+
a(href:, class: "pu-btn pu-btn-sm pu-btn-outline", data: {dashboard_link: link.key}) do
|
|
63
|
+
render link.icon.new(class: "size-4") if link.icon
|
|
64
|
+
span { link.label }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The page with this filter set and the rest of the query string kept.
|
|
69
|
+
def filter_url(filter, value)
|
|
70
|
+
query = dashboard.request.query_parameters.merge(filter.key.to_s => value)
|
|
71
|
+
"#{dashboard.request.path}?#{query.to_query}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -17,7 +17,10 @@ module Plutonium
|
|
|
17
17
|
def markdown_tag(**, &)
|
|
18
18
|
create_component(Plutonium::UI::Display::Components::Markdown, :markdown, **, &)
|
|
19
19
|
end
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
def rich_text_tag(**, &)
|
|
22
|
+
create_component(Plutonium::UI::Display::Components::RichText, :rich_text, **, &)
|
|
23
|
+
end
|
|
21
24
|
|
|
22
25
|
def attachment_tag(**, &)
|
|
23
26
|
create_component(Plutonium::UI::Display::Components::Attachment, :attachment, **, &)
|
|
@@ -51,12 +54,57 @@ module Plutonium
|
|
|
51
54
|
create_component(Plutonium::UI::Display::Components::Currency, :currency, **, &)
|
|
52
55
|
end
|
|
53
56
|
|
|
57
|
+
def number_tag(**, &)
|
|
58
|
+
create_component(Plutonium::UI::Display::Components::Number, :number, **, &)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def key_value_tag(**, &)
|
|
62
|
+
create_component(Plutonium::UI::Display::Components::KeyValue, :key_value, **, &)
|
|
63
|
+
end
|
|
64
|
+
alias_method :hstore_tag, :key_value_tag
|
|
65
|
+
|
|
66
|
+
def list_tag(**, &)
|
|
67
|
+
create_component(Plutonium::UI::Display::Components::List, :list, **, &)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def tags_tag(**, &)
|
|
71
|
+
create_component(Plutonium::UI::Display::Components::Tags, :list, **, &)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def relative_time_tag(**, &)
|
|
75
|
+
create_component(Plutonium::UI::Display::Components::RelativeTime, :relative_time, **, &)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def code_tag(**, &)
|
|
79
|
+
create_component(Plutonium::UI::Display::Components::Code, :code, **, &)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def duration_tag(**, &)
|
|
83
|
+
create_component(Plutonium::UI::Display::Components::Duration, :duration, **, &)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def file_size_tag(**, &)
|
|
87
|
+
create_component(Plutonium::UI::Display::Components::FileSize, :file_size, **, &)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def progress_tag(**, &)
|
|
91
|
+
create_component(Plutonium::UI::Display::Components::Progress, :progress, **, &)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def rating_tag(**, &)
|
|
95
|
+
create_component(Plutonium::UI::Display::Components::Rating, :rating, **, &)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def binary_tag(**, &)
|
|
99
|
+
create_component(Plutonium::UI::Display::Components::Binary, :binary, **, &)
|
|
100
|
+
end
|
|
101
|
+
|
|
54
102
|
# Type aliases for common column types
|
|
55
103
|
alias_method :float_tag, :number_tag
|
|
56
104
|
alias_method :decimal_tag, :number_tag
|
|
57
105
|
alias_method :jsonb_tag, :json_tag
|
|
58
|
-
alias_method :key_value_tag, :hstore_tag
|
|
59
106
|
alias_method :phlexi_tag, :phlexi_render_tag
|
|
107
|
+
alias_method :secret_tag, :password_tag
|
|
60
108
|
end
|
|
61
109
|
|
|
62
110
|
private
|
|
@@ -61,7 +61,7 @@ module Plutonium
|
|
|
61
61
|
|
|
62
62
|
def render_value(value)
|
|
63
63
|
variant = self.class.variant_for(value, colors: @colors)
|
|
64
|
-
span(**attributes, class: tokens("pu-badge", "pu-badge-#{variant}")) do
|
|
64
|
+
span(**mix(attributes, {class: tokens("pu-badge", "pu-badge-#{variant}")})) do
|
|
65
65
|
plain value_label(value)
|
|
66
66
|
end
|
|
67
67
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A `binary` column's size, never its bytes.
|
|
8
|
+
class Binary < Phlexi::Display::Components::Base
|
|
9
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
10
|
+
|
|
11
|
+
def render_value(value)
|
|
12
|
+
span(**attributes) do
|
|
13
|
+
Plutonium::Translation.t("plutonium.ui.display.binary", size: ActiveSupport::NumberHelper.number_to_human_size(value.bytesize))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -19,7 +19,7 @@ module Plutonium
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
21
|
def pill(label:, variant:, icon:)
|
|
22
|
-
span(**attributes, class: tokens("pu-badge", variant), "aria-label": label) do
|
|
22
|
+
span(**mix(attributes, {class: tokens("pu-badge", variant)}), "aria-label": label) do
|
|
23
23
|
render icon.new(class: "w-3.5 h-3.5")
|
|
24
24
|
plain label
|
|
25
25
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A value in monospace with a copy button: IDs, keys, references.
|
|
8
|
+
class Code < Phlexi::Display::Components::Base
|
|
9
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
10
|
+
|
|
11
|
+
def render_value(value)
|
|
12
|
+
div(**attributes, data: {controller: "clipboard"}) do
|
|
13
|
+
code(class: themed(:code_value), data: {clipboard_target: "source"}) { value }
|
|
14
|
+
button(type: :button, class: themed(:code_copy), data: {action: "clipboard#copy"}) do
|
|
15
|
+
Plutonium::Translation.t("plutonium.ui.display.code.copy")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -7,15 +7,23 @@ module Plutonium
|
|
|
7
7
|
class Color < Phlexi::Display::Components::Base
|
|
8
8
|
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
9
9
|
|
|
10
|
+
# Hex, a named color, or a plain rgb()/hsl() call. The value is
|
|
11
|
+
# interpolated into `style:`, so anything else (a `;` or `url(`)
|
|
12
|
+
# would let a stored string inject arbitrary CSS.
|
|
13
|
+
CSS_COLOR = /\A(?:#\h{3,8}|[a-z]+|(?:rgb|hsl)a?\([\d\s.,%\/]+\))\z/i
|
|
14
|
+
|
|
10
15
|
def render_value(value)
|
|
11
|
-
div(**attributes
|
|
12
|
-
div(
|
|
13
|
-
|
|
14
|
-
style: "background-color: #{value};"
|
|
15
|
-
)
|
|
16
|
-
span(class: "text-sm text-[var(--pu-text-muted)]") { value }
|
|
16
|
+
div(**attributes) do
|
|
17
|
+
div(class: themed(:color_indicator), style: swatch_style(value))
|
|
18
|
+
span(class: themed(:color_label)) { value }
|
|
17
19
|
end
|
|
18
20
|
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def swatch_style(value)
|
|
25
|
+
"background-color: #{value};" if CSS_COLOR.match?(value)
|
|
26
|
+
end
|
|
19
27
|
end
|
|
20
28
|
end
|
|
21
29
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A number of seconds (or an ActiveSupport::Duration) as "2h 15m".
|
|
8
|
+
class Duration < Phlexi::Display::Components::Base
|
|
9
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
10
|
+
|
|
11
|
+
UNITS = {days: 86_400, hours: 3600, minutes: 60, seconds: 1}.freeze
|
|
12
|
+
|
|
13
|
+
def render_value(value)
|
|
14
|
+
span(**attributes) { value }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def normalize_value(value)
|
|
20
|
+
remaining = value.to_i
|
|
21
|
+
parts = UNITS.filter_map do |unit, size|
|
|
22
|
+
count, remaining = remaining.divmod(size)
|
|
23
|
+
Plutonium::Translation.t("plutonium.ui.display.duration.#{unit}", count:) if count.positive?
|
|
24
|
+
end
|
|
25
|
+
parts.empty? ? Plutonium::Translation.t("plutonium.ui.display.duration.seconds", count: 0) : parts.join(" ")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A byte count as "12 KB".
|
|
8
|
+
class FileSize < Phlexi::Display::Components::Base
|
|
9
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
10
|
+
|
|
11
|
+
def render_value(value)
|
|
12
|
+
span(**attributes) { value }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def normalize_value(value) = ActiveSupport::NumberHelper.number_to_human_size(value)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A Hash as a two-column list of keys and values: hstore columns, or
|
|
8
|
+
# any hash-valued field declared `as: :key_value`. Nested hashes and
|
|
9
|
+
# arrays stay on one line as compact JSON.
|
|
10
|
+
class KeyValue < Phlexi::Display::Components::Base
|
|
11
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
12
|
+
|
|
13
|
+
def render_value(value)
|
|
14
|
+
dl(**attributes) do
|
|
15
|
+
value.each do |key, item|
|
|
16
|
+
dt(class: themed(:key_value_key)) { key.to_s }
|
|
17
|
+
dd(class: themed(:key_value_value)) { format_item(item) }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def normalize_value(value) = value.to_h
|
|
25
|
+
|
|
26
|
+
def format_item(item)
|
|
27
|
+
case item
|
|
28
|
+
when nil then "—"
|
|
29
|
+
when Hash, Array then code(class: themed(:key_value_nested)) { item.to_json }
|
|
30
|
+
else item.to_s
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# An array value as a row of chips.
|
|
8
|
+
class List < Phlexi::Display::Components::Base
|
|
9
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
10
|
+
|
|
11
|
+
# DisplaysValue renders a multi-valued field once per item; a list
|
|
12
|
+
# is one component for the whole array.
|
|
13
|
+
def view_template
|
|
14
|
+
items = normalize_value(field.value)
|
|
15
|
+
return render(field.placeholder_tag(**@placeholder_attributes)) if items.empty?
|
|
16
|
+
|
|
17
|
+
wrapped { render_value(items) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render_value(value)
|
|
21
|
+
ul(**attributes) do
|
|
22
|
+
value.each { |item| li(class: themed(:list_item)) { item.to_s } }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def normalize_value(value) = Array(value).compact_blank
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# phlexi's Number coerces through Float(), which drops a decimal
|
|
8
|
+
# column's scale (10.50 → "10.5") and digits past ~15 significant
|
|
9
|
+
# figures. Decimals stay BigDecimal and render at the column's scale.
|
|
10
|
+
# Explicit `options:` still go straight to number_to_delimited.
|
|
11
|
+
class Number < Phlexi::Display::Components::Number
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def format_number(value)
|
|
15
|
+
return super unless value.is_a?(BigDecimal) && @options.empty?
|
|
16
|
+
|
|
17
|
+
digits = scale ? ActiveSupport::NumberHelper.number_to_rounded(value, precision: scale) : value.to_s("F")
|
|
18
|
+
ActiveSupport::NumberHelper.number_to_delimited(digits)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def normalize_value(value)
|
|
22
|
+
value.is_a?(BigDecimal) ? value : super
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def scale
|
|
26
|
+
field.object.class.try(:type_for_attribute, field.key)&.scale
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A value out of `max:` (default 100) as a bar and a percentage.
|
|
8
|
+
#
|
|
9
|
+
# display :completion, as: :progress # 0..100
|
|
10
|
+
# display :ratio, as: :progress, max: 1 # 0..1
|
|
11
|
+
class Progress < Phlexi::Display::Components::Base
|
|
12
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
13
|
+
|
|
14
|
+
def render_value(value)
|
|
15
|
+
percent = (value.to_f / @max * 100).clamp(0, 100).round
|
|
16
|
+
div(**attributes, role: :progressbar, aria_valuenow: value, aria_valuemin: 0, aria_valuemax: @max) do
|
|
17
|
+
div(class: themed(:progress_track)) do
|
|
18
|
+
div(class: themed(:progress_bar), style: "width: #{percent}%")
|
|
19
|
+
end
|
|
20
|
+
span(class: themed(:progress_label)) { "#{percent}%" }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def build_attributes
|
|
27
|
+
super
|
|
28
|
+
@max = attributes.delete(:max) || 100
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def normalize_value(value) = value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A score as stars out of `max:` (default 5).
|
|
8
|
+
class Rating < Phlexi::Display::Components::Base
|
|
9
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
10
|
+
|
|
11
|
+
def render_value(value)
|
|
12
|
+
filled = value.to_f.round.clamp(0, @max)
|
|
13
|
+
label = Plutonium::Translation.t("plutonium.ui.display.rating.label", value: filled, max: @max)
|
|
14
|
+
div(**attributes, role: :img, aria_label: label) do
|
|
15
|
+
@max.times do |index|
|
|
16
|
+
state = (index < filled) ? "filled" : "empty"
|
|
17
|
+
span(data: {rating_star: state}) do
|
|
18
|
+
render Phlex::TablerIcons::Star.new(class: themed(:"rating_#{state}"))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def build_attributes
|
|
27
|
+
super
|
|
28
|
+
@max = attributes.delete(:max) || 5
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def normalize_value(value) = value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A timestamp as "3 minutes ago", kept live by the timeago controller.
|
|
8
|
+
# The absolute time is the fallback text and the tooltip.
|
|
9
|
+
class RelativeTime < Phlexi::Display::Components::Base
|
|
10
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
11
|
+
|
|
12
|
+
def render_value(value)
|
|
13
|
+
absolute = I18n.l(value, format: :long)
|
|
14
|
+
time(**attributes, datetime: value.iso8601, title: absolute,
|
|
15
|
+
data: {controller: "timeago", timeago_datetime_value: value.iso8601}) { absolute }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def normalize_value(value) = value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A `has_rich_text` field. Action Text renders and sanitizes the HTML
|
|
8
|
+
# (including attachments) through its content layout.
|
|
9
|
+
class RichText < Phlexi::Display::Components::Base
|
|
10
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
11
|
+
|
|
12
|
+
def render_value(value)
|
|
13
|
+
div(**attributes) { raw(safe(value.to_s)) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def normalize_value(value) = value
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A no_fly_list `has_tags` context as chips. Reads the tag records
|
|
8
|
+
# through the context's association, so a table can preload it
|
|
9
|
+
# (`includes(:labels)`) to avoid a query per row.
|
|
10
|
+
class Tags < List
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def normalize_value(value) = value.map(&:name)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -6,16 +6,29 @@ module Plutonium
|
|
|
6
6
|
module Options
|
|
7
7
|
module InferredTypes
|
|
8
8
|
include Plutonium::UI::Options::HasCentsField
|
|
9
|
+
include Plutonium::UI::Options::SecretField
|
|
10
|
+
include Plutonium::UI::Options::TagsField
|
|
11
|
+
include Plutonium::UI::Options::RichTextField
|
|
9
12
|
|
|
10
13
|
private
|
|
11
14
|
|
|
12
15
|
def infer_field_component
|
|
16
|
+
return :tags if tags_field?
|
|
17
|
+
return :rich_text if rich_text_field?
|
|
18
|
+
|
|
19
|
+
# Mask the same secret-bearing names the form does, so the show
|
|
20
|
+
# page and table never print what the form refuses to echo.
|
|
21
|
+
return :password if secret_field_name?
|
|
22
|
+
|
|
13
23
|
# has_cents decimal accessors infer as :float/:decimal; render money.
|
|
14
24
|
return :currency if has_cents_field?
|
|
15
25
|
|
|
16
26
|
case inferred_field_type
|
|
17
27
|
when :attachment
|
|
18
28
|
:attachment
|
|
29
|
+
when :binary
|
|
30
|
+
# phlexi-display falls back to :string, printing the raw bytes.
|
|
31
|
+
:binary
|
|
19
32
|
when :boolean
|
|
20
33
|
# phlexi-display falls back to :string, rendering "true"/"false".
|
|
21
34
|
:boolean
|