keystone_ui 0.41.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: fff79f2588c403bbbb1d39b9fec8075955b0f758019fdea442d1dbc081750f99
4
- data.tar.gz: f92a6ccfa5f383ccc81952c008c8d3dcd59d61906b36d62de4bb9186a3bfcd58
3
+ metadata.gz: 28436277e0b304a48d5093a2220bb1df0e04f94315f085781e93cb34207ea654
4
+ data.tar.gz: 82bb8f8de9b4132f47da04e7ab3423ba3c73e216b2ae79659f8709b52888bfde
5
5
  SHA512:
6
- metadata.gz: f61c6841e06dcb1fed471c93fbfb41f7655b32e44348ea45df84d961c9b735b051f6e40232738e4df478ea34dac3e6a0c48c8172c2367ddb7f90c292fafdb6cf
7
- data.tar.gz: d319f7e7a1782535045369da6220ba4dfe0c9fad7a7b15ec8132dddd23b75e69943881df988275cef18b3c41c898afca053d4d2592b3e7e8b091b5acab993e02
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
@@ -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
@@ -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.41.0"
4
+ VERSION = "0.42.0"
5
5
  end
@@ -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.
@@ -397,6 +400,10 @@ two exceptions: `ui_page`'s `padding:` treats any value other than `:none` as
397
400
  - `ui_disclosure(open: false)` takes a block yielding the component. Fill
398
401
  its `summary` slot with the header, and the rest of the block is the body.
399
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.
400
407
 
401
408
  10. Explain figures.
402
409
 
@@ -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.41.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.16.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.16.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