keystone_ui 0.40.0 → 0.42.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50be7dcd619f5461a74ec4c8b7d2d6e09deb955af9b17817b1ef690f842f5a07
4
- data.tar.gz: 1148f6cf6bba00291658cec6c0672eeae3500093ebbffec67bbbc412fabe72eb
3
+ metadata.gz: 28436277e0b304a48d5093a2220bb1df0e04f94315f085781e93cb34207ea654
4
+ data.tar.gz: 82bb8f8de9b4132f47da04e7ab3423ba3c73e216b2ae79659f8709b52888bfde
5
5
  SHA512:
6
- metadata.gz: 884b4a4d0b72cf66bc76d5e94af4322a65f1b82aa81a5fba5b6b3f3fbad865524d6dd7fb75dce0a33d1fec8e88e41691a4a57b045e5d920751b3c6fe1453e545
7
- data.tar.gz: ca9f20d6ef81510e165a8c6a938e6a2cc0cb37dfc1c426b97f17e1695686521727d5e0336809084761d8c0e59f90aef425fe912878d79661e44fa89e4d12137b
6
+ metadata.gz: 3e63c3495c2b1709109cb25584e9a9e5c5b7fcaef4cfb1cc1e948641b5ae06a22acc3703f52d038d69a7371b57d39a554d75a8ac671e90662f525734979ab8fd
7
+ data.tar.gz: 8941b3263e4944bbfc4e6d6dbc8eb82f74908ee51b922f9c6f61b6cb9c4af869171c22b8e5cbdc4bef6e7c238e740d2f531b143cd5abaddba2a5d73b36d34b1a
@@ -11,18 +11,92 @@ const MILLISECONDS_IN_A_DAY = 86_400_000
11
11
  export const dayLabel = (day) =>
12
12
  new Date(day * MILLISECONDS_IN_A_DAY).toLocaleDateString("en-US", { timeZone: "UTC", month: "short", day: "numeric", year: "numeric" })
13
13
 
14
- const DATED_OPTIONS = {
15
- scales: { x: { type: "linear", bounds: "data", ticks: { precision: 0, callback: dayLabel } } },
16
- plugins: { tooltip: { callbacks: { title: ([point]) => dayLabel(point.parsed.x) } } }
14
+ const DATED_AXIS = { type: "linear", bounds: "data" }
15
+ const DATED_TICKS = { precision: 0, callback: dayLabel }
16
+ const DATED_TOOLTIP = { callbacks: { title: ([point]) => dayLabel(point.parsed.x) } }
17
+
18
+ const tooltip = (theme) => ({
19
+ backgroundColor: theme.surface,
20
+ borderColor: theme.border,
21
+ borderWidth: 1,
22
+ titleColor: theme.text,
23
+ bodyColor: theme.text
24
+ })
25
+
26
+ const DRAW_IN_MILLISECONDS = 600
27
+
28
+ const pointStep = (context) => DRAW_IN_MILLISECONDS / context.chart.data.datasets[context.datasetIndex].data.length
29
+
30
+ const drawsIn = (context) => context.type === "data" && context.mode === "default"
31
+
32
+ const drawIn = {
33
+ x: {
34
+ type: "number",
35
+ easing: "linear",
36
+ from: NaN,
37
+ duration: (context) => (drawsIn(context) ? pointStep(context) : 0),
38
+ delay: (context) => (drawsIn(context) ? context.index * pointStep(context) : 0)
39
+ },
40
+ y: { duration: 0 }
17
41
  }
18
42
 
19
- export const chartOptions = (dated) => ({
43
+ const axisText = (theme) => ({ color: theme.mutedText, font: { size: 11, family: theme.font } })
44
+
45
+ export const chartOptions = (dated, theme = {}, animated = false) => ({
20
46
  responsive: true,
21
47
  maintainAspectRatio: false,
22
48
  interaction: { mode: "index", intersect: false },
23
- ...(dated ? DATED_OPTIONS : {})
49
+ ...(animated ? { animations: drawIn } : { animation: false }),
50
+ elements: { line: { borderWidth: 1.5 }, point: { radius: 0, hoverRadius: 4 } },
51
+ scales: {
52
+ x: { ...(dated ? DATED_AXIS : {}), grid: { display: false }, ticks: { ...(dated ? DATED_TICKS : {}), ...axisText(theme) } },
53
+ y: { border: { display: false }, grid: { color: theme.border }, ticks: axisText(theme) }
54
+ },
55
+ plugins: { tooltip: { ...(dated ? DATED_TOOLTIP : {}), ...tooltip(theme) } }
24
56
  })
25
57
 
58
+ const LIGHT_THEMES = ["light", "custom"]
59
+
60
+ export const isDarkMode = (theme, prefersDark) => theme === "dark" || (prefersDark && !LIGHT_THEMES.includes(theme))
61
+
62
+ const CHART_COLOR_COUNT = 5
63
+
64
+ const INHERITED_FONTS = ["", "inherit"]
65
+
66
+ export const themeColors = (read, dark, inheritedFont) => {
67
+ const variable = (name) => read(dark ? `${name}-dark` : name)
68
+ const bodyFont = read("--ks-font-body")
69
+ return {
70
+ series: Array.from({ length: CHART_COLOR_COUNT }, (_, index) => variable(`--ks-color-chart-${index + 1}`)),
71
+ border: variable("--ks-color-border"),
72
+ surface: variable("--ks-color-surface"),
73
+ text: variable("--ks-color-text"),
74
+ mutedText: variable("--ks-color-text-muted"),
75
+ font: INHERITED_FONTS.includes(bodyFont) ? inheritedFont : bodyFont
76
+ }
77
+ }
78
+
79
+ export const styleDatasets = (datasets, theme, resolve) => {
80
+ let uncolored = 0
81
+ return datasets.map((dataset) => {
82
+ const color = dataset.borderColor ? resolve(dataset.borderColor) : theme.series[uncolored++ % theme.series.length]
83
+ return { ...dataset, borderColor: color, pointHoverBackgroundColor: color }
84
+ })
85
+ }
86
+
87
+ export const watchTheme = ({ document, matchMedia, MutationObserver }, onChange) => {
88
+ const observer = new MutationObserver(onChange)
89
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "data-look"] })
90
+ observer.observe(document.head, { childList: true, subtree: true, characterData: true })
91
+ const darkPreference = matchMedia("(prefers-color-scheme: dark)")
92
+ darkPreference.addEventListener("change", onChange)
93
+
94
+ return () => {
95
+ observer.disconnect()
96
+ darkPreference.removeEventListener("change", onChange)
97
+ }
98
+ }
99
+
26
100
  export default class extends Controller {
27
101
  static targets = ["canvas"]
28
102
  static values = { data: Object, dated: Boolean }
@@ -31,27 +105,38 @@ export default class extends Controller {
31
105
  const Chart = await loadChart()
32
106
  if (!this.element.isConnected) return
33
107
 
108
+ const { theme, resolve } = this.readTheme()
109
+ this.animated = !matchMedia("(prefers-reduced-motion: reduce)").matches
34
110
  this.chart = new Chart(this.canvasTarget, {
35
111
  type: "line",
36
- data: this.resolveColors(this.dataValue),
37
- options: chartOptions(this.datedValue)
112
+ data: { ...this.dataValue, datasets: styleDatasets(this.dataValue.datasets, theme, resolve) },
113
+ options: chartOptions(this.datedValue, theme, this.animated)
38
114
  })
115
+ this.stopWatchingTheme = watchTheme({ document, matchMedia: (query) => matchMedia(query), MutationObserver }, () => this.redraw())
39
116
  }
40
117
 
41
118
  disconnect() {
119
+ this.stopWatchingTheme?.()
42
120
  this.chart?.destroy()
43
121
  }
44
122
 
45
- resolveColors(data) {
123
+ redraw() {
124
+ const { theme, resolve } = this.readTheme()
125
+ const styled = styleDatasets(this.dataValue.datasets, theme, resolve)
126
+ this.chart.data.datasets.forEach((dataset, index) => Object.assign(dataset, styled[index]))
127
+ this.chart.options = chartOptions(this.datedValue, theme, this.animated)
128
+ this.chart.update("none")
129
+ }
130
+
131
+ readTheme() {
46
132
  const styles = getComputedStyle(this.element)
133
+ const read = (name) => styles.getPropertyValue(name).trim()
47
134
  const resolve = (color) => {
48
135
  const match = typeof color === "string" && color.match(/^var\((--[^)]+)\)$/)
49
- return match ? styles.getPropertyValue(match[1]).trim() || color : color
136
+ return match ? read(match[1]) || color : color
50
137
  }
51
138
 
52
- data.datasets.forEach((dataset) => {
53
- if (dataset.borderColor) dataset.borderColor = resolve(dataset.borderColor)
54
- })
55
- return data
139
+ const dark = isDarkMode(document.documentElement.dataset.theme, matchMedia("(prefers-color-scheme: dark)").matches)
140
+ return { theme: themeColors(read, dark, styles.fontFamily), resolve }
56
141
  }
57
142
  }
@@ -0,0 +1 @@
1
+ <p class="ks-caption"><%= text %></p>
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class CaptionComponent < ViewComponent::Base
6
+ attr_reader :text
7
+
8
+ def initialize(text:)
9
+ @text = text
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ <% if note %>
2
+ <div class="ks-heading-row">
3
+ <h2 class="<%= classes %>"><%= text %></h2>
4
+ <span class="ks-heading-note"><%= note %></span>
5
+ </div>
6
+ <% else %>
7
+ <h2 class="<%= classes %>"><%= text %></h2>
8
+ <% end %>
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class HeadingComponent < ViewComponent::Base
6
+ SIZE_CLASSES = { sm: "ks-heading-sm", md: "ks-heading-md", lg: "ks-heading-lg" }.freeze
7
+
8
+ attr_reader :text, :note
9
+
10
+ def initialize(text:, size: :md, note: nil)
11
+ @text = text
12
+ @note = note
13
+ @size = size
14
+ end
15
+
16
+ def classes
17
+ [ "ks-heading", SIZE_CLASSES.fetch(@size) ].join(" ")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -7,12 +7,19 @@
7
7
  <div class="<%= SIDEBAR_LOGO_CLASSES %>"><%= logo %></div>
8
8
  <% end %>
9
9
  <% groups.each do |group| %>
10
- <details class="<%= SIDEBAR_GROUP_CLASSES %>"<%= " open" if group.active %>>
11
- <summary class="<%= sidebar_group_label_classes(group) %>"><%= group.label %></summary>
12
- <% group.tabs.each do |tab| %>
13
- <a class="<%= sidebar_tab_classes(tab) %>" href="<%= tab.href %>"><%= tab.label %></a>
14
- <% end %>
15
- </details>
10
+ <% if group.standalone %>
11
+ <% tab = group.tabs.first %>
12
+ <a class="<%= sidebar_tab_classes(tab) %>" href="<%= tab.href %>"><%= tab.label %></a>
13
+ <% else %>
14
+ <details class="<%= SIDEBAR_GROUP_CLASSES %>"<%= " open" if group.active %>>
15
+ <summary class="<%= sidebar_group_label_classes(group) %>"><%= group.label %></summary>
16
+ <div class="<%= SIDEBAR_GROUP_TABS_CLASSES %>">
17
+ <% group.tabs.each do |tab| %>
18
+ <a class="<%= sidebar_tab_classes(tab) %>" href="<%= tab.href %>"><%= tab.label %></a>
19
+ <% end %>
20
+ </div>
21
+ </details>
22
+ <% end %>
16
23
  <% end %>
17
24
  <% if menus? %>
18
25
  <div class="<%= SIDEBAR_MENUS_CLASSES %>"><%= menus %></div>
@@ -33,9 +40,14 @@
33
40
  <% end %>
34
41
  <% navbar.with_desktop_links do %>
35
42
  <% groups.each do |group| %>
36
- <%= render Keystone::Ui::NavDropdownComponent.new(title: group.label, area: group.label, active: group.active) do %>
37
- <% group.tabs.each do |tab| %>
38
- <%= render Keystone::Ui::NavItemComponent.new(label: tab.label, href: tab.href, active: tab.active) %>
43
+ <% if group.standalone %>
44
+ <% tab = group.tabs.first %>
45
+ <%= render Keystone::Ui::NavItemComponent.new(label: tab.label, href: tab.href, active: tab.active) %>
46
+ <% else %>
47
+ <%= render Keystone::Ui::NavDropdownComponent.new(title: group.label, area: group.label, active: group.active) do %>
48
+ <% group.tabs.each do |tab| %>
49
+ <%= render Keystone::Ui::NavItemComponent.new(label: tab.label, href: tab.href, active: tab.active) %>
50
+ <% end %>
39
51
  <% end %>
40
52
  <% end %>
41
53
  <% end %>
@@ -11,6 +11,7 @@ module Keystone
11
11
  SIDEBAR_LOGO_CLASSES = "ks-sidebar-logo"
12
12
  SIDEBAR_GROUP_CLASSES = "flex flex-col"
13
13
  SIDEBAR_GROUP_LABEL_CLASSES = "ks-sidebar-group-label"
14
+ SIDEBAR_GROUP_TABS_CLASSES = "ks-sidebar-group-tabs"
14
15
  SIDEBAR_TAB_CLASSES = "ks-sidebar-tab"
15
16
  ACTIVE_CLASS = "active"
16
17
  SIDEBAR_MENUS_CLASSES = [
@@ -0,0 +1 @@
1
+ <p class="ks-paragraph"><%= text %></p>
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class ParagraphComponent < ViewComponent::Base
6
+ attr_reader :text
7
+
8
+ def initialize(text:)
9
+ @text = text
10
+ end
11
+ end
12
+ end
13
+ end
@@ -237,6 +237,18 @@ module KeystoneUiHelper
237
237
  render Keystone::Ui::FigureComponent.new(**args)
238
238
  end
239
239
 
240
+ def ui_heading(**args)
241
+ render Keystone::Ui::HeadingComponent.new(**args)
242
+ end
243
+
244
+ def ui_caption(**args)
245
+ render Keystone::Ui::CaptionComponent.new(**args)
246
+ end
247
+
248
+ def ui_paragraph(**args)
249
+ render Keystone::Ui::ParagraphComponent.new(**args)
250
+ end
251
+
240
252
  def ui_info(**args, &block)
241
253
  render(Keystone::Ui::InfoComponent.new(**args), &block)
242
254
  end
@@ -31,6 +31,12 @@ module KeystoneUi
31
31
  @navigation_groups << group
32
32
  end
33
33
 
34
+ def navigation_tab(key, label:, href:, permitted:)
35
+ group = NavigationGroup.new(label, standalone: true)
36
+ group.tab(key, label: label, href: href, permitted: permitted)
37
+ @navigation_groups << group
38
+ end
39
+
34
40
  def supplied_look(view)
35
41
  look_supplier&.call(view)&.to_s
36
42
  end
@@ -10,11 +10,16 @@ module KeystoneUi
10
10
 
11
11
  attr_reader :label, :tabs
12
12
 
13
- def initialize(label)
13
+ def initialize(label, standalone: false)
14
14
  @label = label
15
+ @standalone = standalone
15
16
  @tabs = []
16
17
  end
17
18
 
19
+ def standalone?
20
+ @standalone
21
+ end
22
+
18
23
  def tabs_permitted_for(view)
19
24
  tabs.select { |tab| tab.permitted.call(view) }
20
25
  end
@@ -14,7 +14,9 @@ module Keystone
14
14
  Keystone::Ui::LineChartComponent,
15
15
  Keystone::Ui::CtaBannerComponent,
16
16
  Keystone::Ui::FeatureGridComponent,
17
+ Keystone::Ui::CaptionComponent,
17
18
  Keystone::Ui::FigureComponent,
19
+ Keystone::Ui::HeadingComponent,
18
20
  Keystone::Ui::HeroComponent,
19
21
  Keystone::Ui::ModalComponent,
20
22
  Keystone::Ui::SelectComponent,
@@ -32,6 +34,7 @@ module Keystone
32
34
  Keystone::Ui::TextareaComponent,
33
35
  Keystone::Ui::FormFieldComponent,
34
36
  Keystone::Ui::PageHeaderComponent,
37
+ Keystone::Ui::ParagraphComponent,
35
38
  Keystone::Ui::AlertComponent,
36
39
  Keystone::Ui::NavDropdownComponent,
37
40
  Keystone::Ui::NavItemComponent,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KeystoneUi
4
- VERSION = "0.40.0"
4
+ VERSION = "0.42.0"
5
5
  end
@@ -4,7 +4,7 @@ require "keystone_ui/navigation_order"
4
4
 
5
5
  module KeystoneUi
6
6
  class VisibleNavigation
7
- Group = Struct.new(:label, :tabs, :active, keyword_init: true)
7
+ Group = Struct.new(:label, :tabs, :active, :standalone, keyword_init: true)
8
8
  Tab = Struct.new(:label, :href, :active, keyword_init: true)
9
9
 
10
10
  def initialize(declared_groups, view:, saved_order:, current_tab:)
@@ -28,7 +28,7 @@ module KeystoneUi
28
28
 
29
29
  def visible_group(group, tabs)
30
30
  visible_tabs = tabs.map { |tab| visible_tab(tab) }
31
- Group.new(label: group.label, tabs: visible_tabs, active: visible_tabs.any?(&:active))
31
+ Group.new(label: group.label, tabs: visible_tabs, active: visible_tabs.any?(&:active), standalone: group.standalone?)
32
32
  end
33
33
 
34
34
  def visible_tab(tab)
@@ -63,6 +63,9 @@ not available in the app yet, that is `keystone_ui-install`'s job.
63
63
  - `ui_button` — a link or a button styled as a button.
64
64
  - `ui_badge` — a coloured pill holding a short label.
65
65
  - `ui_figure` — one amount as inline text, coloured as a gain or a loss.
66
+ - `ui_heading` — a heading at one of three sizes, with an optional short note on its right.
67
+ - `ui_caption` — small uppercase muted text placed above a title or a figure.
68
+ - `ui_paragraph` — a paragraph of running text.
66
69
  - `ui_alert` — a banner message, optionally dismissible.
67
70
  - `ui_progress` — a labelled progress bar.
68
71
  - `ui_stat_card` — a single metric tile with optional change, link and info button.
@@ -197,7 +200,10 @@ two exceptions: `ui_page`'s `padding:` treats any value other than `:none` as
197
200
  it goes around `yield`. It draws the tabs the app declares with
198
201
  `config.navigation_group` as a top bar of menus from `lg:` up, then the
199
202
  content, and below `lg:` it draws only the content. It leaves out tabs
200
- whose permission check fails and groups with no tab left.
203
+ whose permission check fails and groups with no tab left. A tab declared
204
+ outside any group with `config.navigation_tab` is drawn as a plain link
205
+ in the top bar and as a plain tab with no group label in the sidebar,
206
+ which does not open or close.
201
207
  - Inside its block, `navigation.with_logo do ... end` puts the app's logo at
202
208
  the left of the top bar, and `navigation.with_menus do ... end` puts menus
203
209
  such as the account menu at its right, so the layout draws no second bar.
@@ -394,6 +400,10 @@ two exceptions: `ui_page`'s `padding:` treats any value other than `:none` as
394
400
  - `ui_disclosure(open: false)` takes a block yielding the component. Fill
395
401
  its `summary` slot with the header, and the rest of the block is the body.
396
402
  - `ui_accordion(items: [])` — `items:` is `[{ question:, answer: }, ...]`.
403
+ - `ui_heading(text:, size: :md, note: nil)` — `size:` is `:sm` `:md` `:lg`,
404
+ and `note:` puts a short muted note at the right end of the heading's row.
405
+ - `ui_caption(text:)` shows small uppercase muted text above a title or a figure.
406
+ - `ui_paragraph(text:)` shows running text.
397
407
 
398
408
  10. Explain figures.
399
409
 
@@ -31,6 +31,8 @@ built on ViewComponent; hook it in before building any screen with those helpers
31
31
  saves to, and the navigation's placement under the key `:navigation`),
32
32
  `navigation_group` (a labelled group of desktop tabs that the navigation
33
33
  helper draws, each tab declared with `tab key, label:, href:, permitted:`),
34
+ `navigation_tab` (a single desktop tab outside any group, declared with
35
+ `navigation_tab key, label:, href:, permitted:`),
34
36
  `current_tab_supplier` (a callable that supplies the key of the declared tab
35
37
  the current page belongs to, so the navigation marks it and its group
36
38
  active),
@@ -324,6 +326,11 @@ built on ViewComponent; hook it in before building any screen with those helpers
324
326
  `href:`, when two tabs share a key, or when a group has no tabs. Drawing
325
327
  the declared groups in the layout is done with the navigation helper,
326
328
  which belongs to `keystone_ui-develop`.
329
+ - `navigation_tab key, label:, href:, permitted:` — declares one tab
330
+ outside any group, such as a Console or Clients tab, in its place among
331
+ the groups. The navigation helper draws it as a plain link in the top bar
332
+ and as a plain tab with no group label in the sidebar. A saved `"order"`
333
+ names it under `"group"` by its label.
327
334
  - `current_tab_supplier` — a callable that receives the view and returns the
328
335
  key of the declared tab the current page belongs to, or `nil`.
329
336
  The navigation shows that tab and its group as active in the top bar and
@@ -64,6 +64,9 @@ develop:
64
64
  - ui_calculation
65
65
  - ui_info
66
66
  - ui_figure
67
+ - ui_heading
68
+ - ui_caption
69
+ - ui_paragraph
67
70
  - ui_breakdown
68
71
  - ui_theme_toggle
69
72
  - Keystone::Ui::Column
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.40.0
4
+ version: 0.42.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.15.0
18
+ version: 0.19.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.15.0
25
+ version: 0.19.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: view_component
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -98,6 +98,8 @@ files:
98
98
  - app/components/keystone/ui/button_component.rb
99
99
  - app/components/keystone/ui/calculation_component.html.erb
100
100
  - app/components/keystone/ui/calculation_component.rb
101
+ - app/components/keystone/ui/caption_component.html.erb
102
+ - app/components/keystone/ui/caption_component.rb
101
103
  - app/components/keystone/ui/card_component.html.erb
102
104
  - app/components/keystone/ui/card_component.rb
103
105
  - app/components/keystone/ui/card_link_component.html.erb
@@ -139,6 +141,8 @@ files:
139
141
  - app/components/keystone/ui/funnel_component.rb
140
142
  - app/components/keystone/ui/grid_component.html.erb
141
143
  - app/components/keystone/ui/grid_component.rb
144
+ - app/components/keystone/ui/heading_component.html.erb
145
+ - app/components/keystone/ui/heading_component.rb
142
146
  - app/components/keystone/ui/hero_component.html.erb
143
147
  - app/components/keystone/ui/hero_component.rb
144
148
  - app/components/keystone/ui/info_component.html.erb
@@ -171,6 +175,8 @@ files:
171
175
  - app/components/keystone/ui/page_header_component.rb
172
176
  - app/components/keystone/ui/panel_component.html.erb
173
177
  - app/components/keystone/ui/panel_component.rb
178
+ - app/components/keystone/ui/paragraph_component.html.erb
179
+ - app/components/keystone/ui/paragraph_component.rb
174
180
  - app/components/keystone/ui/pipeline_component.html.erb
175
181
  - app/components/keystone/ui/pipeline_component.rb
176
182
  - app/components/keystone/ui/progress_component.html.erb