keystone_ui 0.6.0 → 0.8.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/MIT-LICENSE +21 -0
- data/app/assets/javascripts/keystone_ui/index.js +2 -0
- data/app/assets/javascripts/keystone_ui/theme_toggle_controller.js +25 -0
- data/app/components/keystone/ui/button_component.rb +7 -7
- data/app/components/keystone/ui/form_field_component.html.erb +1 -1
- data/app/components/keystone/ui/form_field_component.rb +5 -5
- data/app/components/keystone/ui/input_component.rb +3 -5
- data/app/components/keystone/ui/panel_component.rb +1 -1
- data/app/components/keystone/ui/select_component.rb +3 -5
- data/app/components/keystone/ui/stat_card_component.html.erb +5 -7
- data/app/components/keystone/ui/stat_card_component.rb +15 -5
- data/app/components/keystone/ui/textarea_component.rb +3 -5
- data/app/components/keystone/ui/theme_toggle_component.html.erb +10 -0
- data/app/components/keystone/ui/theme_toggle_component.rb +23 -0
- data/app/helpers/keystone_ui_helper.rb +8 -0
- data/config/importmap.rb +1 -0
- data/lib/generators/keystone/install_generator.rb +25 -0
- data/lib/generators/keystone/layout_theme_attributes.rb +17 -0
- data/lib/keystone_ui/engine.rb +3 -15
- data/lib/keystone_ui/safelist.rb +2 -1
- data/lib/keystone_ui/source_css.rb +31 -0
- data/lib/keystone_ui/theme_choice.rb +20 -0
- data/lib/keystone_ui/version.rb +1 -1
- data/lib/keystone_ui.rb +2 -0
- data/the_local/agents/keystone_ui-develop.md +8 -4
- data/the_local/interface.yml +1 -0
- metadata +23 -3
- data/app/assets/tailwind/keystone_ui_engine/theme.css +0 -32
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9007e4cf813a277f78f01bdf10df84d28856a5eaf1e7460c51e77db5d8b13143
|
|
4
|
+
data.tar.gz: a31771c5217f422c78000dc895be2314e9ecac0de206155f3d3a123b3da362af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5649e25777890bf3bd01be1f42260c92038b66cef09c71f1b6e4655658f6d602102d0adcef0bd4b063edb837490818df7734c84b3f0e0198e2a859a83b2ea10
|
|
7
|
+
data.tar.gz: 52bce90d7e68c9ead4e459f71376c16257b235b1418ef92c156bd8d7fb1bd2b33a17cf7111bcb0412d19d79e6ad0626ce8d2aa72db8fe00988fbaae41705151d
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tyler Schneider
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -12,6 +12,7 @@ import AccordionController from "keystone_ui/accordion_controller"
|
|
|
12
12
|
import StatCardInfoController from "keystone_ui/stat_card_info_controller"
|
|
13
13
|
import AutoSubmitController from "keystone_ui/auto_submit_controller"
|
|
14
14
|
import LineChartController from "keystone_ui/line_chart_controller"
|
|
15
|
+
import ThemeToggleController from "keystone_ui/theme_toggle_controller"
|
|
15
16
|
|
|
16
17
|
export function registerControllers(application) {
|
|
17
18
|
application.register("color-picker", ColorPickerController)
|
|
@@ -28,6 +29,7 @@ export function registerControllers(application) {
|
|
|
28
29
|
application.register("stat-card-info", StatCardInfoController)
|
|
29
30
|
application.register("auto-submit", AutoSubmitController)
|
|
30
31
|
application.register("line-chart", LineChartController)
|
|
32
|
+
application.register("theme-toggle", ThemeToggleController)
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export { ColorPickerController, MultiSelectController, SwipeDeckController, ColumnPickerController, FileUploadController, DropdownController, DismissController, ModalController, ClipboardController, TabSwitcherController, AccordionController, StatCardInfoController, AutoSubmitController, LineChartController }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
const COOKIE = "keystone_theme"
|
|
4
|
+
const ONE_YEAR = 60 * 60 * 24 * 365
|
|
5
|
+
|
|
6
|
+
export default class extends Controller {
|
|
7
|
+
choose({ params: { mode } }) {
|
|
8
|
+
const page = this.element.ownerDocument
|
|
9
|
+
this.#showPressed(mode)
|
|
10
|
+
|
|
11
|
+
if (mode === "system") {
|
|
12
|
+
delete page.documentElement.dataset.theme
|
|
13
|
+
page.cookie = `${COOKIE}=; path=/; max-age=0; samesite=lax`
|
|
14
|
+
} else {
|
|
15
|
+
page.documentElement.dataset.theme = mode
|
|
16
|
+
page.cookie = `${COOKIE}=${mode}; path=/; max-age=${ONE_YEAR}; samesite=lax`
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#showPressed(mode) {
|
|
21
|
+
this.element.querySelectorAll("[data-theme-toggle-mode-param]").forEach((option) => {
|
|
22
|
+
option.setAttribute("aria-pressed", String(option.dataset.themeToggleModeParam === mode))
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
module Keystone
|
|
4
4
|
module Ui
|
|
5
5
|
class ButtonComponent < ViewComponent::Base
|
|
6
|
-
BASE_CLASSES = "
|
|
6
|
+
BASE_CLASSES = "ks-button"
|
|
7
7
|
|
|
8
8
|
VARIANT_CLASSES = {
|
|
9
|
-
secondary: "
|
|
10
|
-
danger: "
|
|
9
|
+
secondary: "ks-button-secondary",
|
|
10
|
+
danger: "ks-button-danger"
|
|
11
11
|
}.freeze
|
|
12
12
|
|
|
13
13
|
SIZE_CLASSES = {
|
|
14
|
-
sm: "
|
|
15
|
-
md: "
|
|
16
|
-
lg: "
|
|
14
|
+
sm: "ks-button-sm",
|
|
15
|
+
md: "ks-button-md",
|
|
16
|
+
lg: "ks-button-lg"
|
|
17
17
|
}.freeze
|
|
18
18
|
|
|
19
19
|
def initialize(label:, href: nil, variant: :primary, size: :md, type: :submit, data: nil)
|
|
@@ -27,7 +27,7 @@ module Keystone
|
|
|
27
27
|
|
|
28
28
|
def classes
|
|
29
29
|
variant_css = if @variant == :primary
|
|
30
|
-
"
|
|
30
|
+
"ks-button-primary"
|
|
31
31
|
else
|
|
32
32
|
VARIANT_CLASSES.fetch(@variant)
|
|
33
33
|
end
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<% elsif textarea? %>
|
|
15
15
|
<textarea name="<%= @attribute %>" rows="3" class="<%= Keystone::Ui::TextareaComponent::BASE_CLASSES %>"<% if @placeholder %> placeholder="<%= @placeholder %>"<% end %>><%= @value %></textarea>
|
|
16
16
|
<% elsif select? %>
|
|
17
|
-
<select name="<%= @attribute %>" class="<%= Keystone::Ui::SelectComponent::BASE_CLASSES %>
|
|
17
|
+
<select name="<%= @attribute %>" class="<%= Keystone::Ui::SelectComponent::BASE_CLASSES %>">
|
|
18
18
|
<% unless required? %><option value=""></option><% end %>
|
|
19
19
|
<% select_options.each do |label, value| %>
|
|
20
20
|
<option value="<%= value %>" <%= "selected" if @value.to_s == value.to_s %>><%= label %></option>
|
|
@@ -4,11 +4,11 @@ module Keystone
|
|
|
4
4
|
module Ui
|
|
5
5
|
class FormFieldComponent < ViewComponent::Base
|
|
6
6
|
WRAPPER_CLASSES = "space-y-1"
|
|
7
|
-
LABEL_CLASSES = "
|
|
8
|
-
REQUIRED_CLASSES = "
|
|
9
|
-
HINT_CLASSES = "
|
|
10
|
-
ERROR_CLASSES = "
|
|
11
|
-
CHECKBOX_CLASSES = "
|
|
7
|
+
LABEL_CLASSES = "ks-label"
|
|
8
|
+
REQUIRED_CLASSES = "ks-required"
|
|
9
|
+
HINT_CLASSES = "ks-hint"
|
|
10
|
+
ERROR_CLASSES = "ks-error"
|
|
11
|
+
CHECKBOX_CLASSES = "ks-checkbox"
|
|
12
12
|
CHECKBOX_WRAPPER_CLASSES = "flex items-center gap-2"
|
|
13
13
|
|
|
14
14
|
def initialize(attribute:, label: nil, type: :text, required: false, hint: nil, placeholder: nil, min: nil, max: nil, step: nil, value: nil, options: [], errors: [])
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module Keystone
|
|
4
4
|
module Ui
|
|
5
5
|
class InputComponent < ViewComponent::Base
|
|
6
|
-
BASE_CLASSES = "
|
|
6
|
+
BASE_CLASSES = "ks-input"
|
|
7
7
|
|
|
8
|
-
DISABLED_CLASSES = "
|
|
8
|
+
DISABLED_CLASSES = "ks-input-disabled"
|
|
9
9
|
|
|
10
10
|
TYPE_MAP = {
|
|
11
11
|
text: "text",
|
|
@@ -26,10 +26,8 @@ module Keystone
|
|
|
26
26
|
@step = step
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
FOCUS_CLASSES = "focus:border-accent-500 focus:ring-accent-500 dark:focus:border-accent-400 dark:focus:ring-accent-400"
|
|
30
|
-
|
|
31
29
|
def classes
|
|
32
|
-
tokens = [ BASE_CLASSES
|
|
30
|
+
tokens = [ BASE_CLASSES ]
|
|
33
31
|
tokens << DISABLED_CLASSES if @disabled
|
|
34
32
|
tokens.join(" ")
|
|
35
33
|
end
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module Keystone
|
|
4
4
|
module Ui
|
|
5
5
|
class SelectComponent < ViewComponent::Base
|
|
6
|
-
BASE_CLASSES = "
|
|
6
|
+
BASE_CLASSES = "ks-input"
|
|
7
7
|
|
|
8
|
-
DISABLED_CLASSES = "
|
|
8
|
+
DISABLED_CLASSES = "ks-input-disabled"
|
|
9
9
|
|
|
10
10
|
attr_reader :options, :selected, :include_blank
|
|
11
11
|
|
|
@@ -17,10 +17,8 @@ module Keystone
|
|
|
17
17
|
@disabled = disabled
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
FOCUS_CLASSES = "focus:border-accent-500 focus:ring-accent-500 dark:focus:border-accent-400 dark:focus:ring-accent-400"
|
|
21
|
-
|
|
22
20
|
def classes
|
|
23
|
-
tokens = [ BASE_CLASSES
|
|
21
|
+
tokens = [ BASE_CLASSES ]
|
|
24
22
|
tokens << DISABLED_CLASSES if @disabled
|
|
25
23
|
tokens.join(" ")
|
|
26
24
|
end
|
|
@@ -5,18 +5,16 @@
|
|
|
5
5
|
<button type="button" class="<%= info_button_classes %>" data-action="click->stat-card-info#toggle" aria-label="Show metric details">
|
|
6
6
|
<%= info_icon.html_safe %>
|
|
7
7
|
</button>
|
|
8
|
+
<div class="<%= disclosure_classes %>" data-stat-card-info-target="panel">
|
|
9
|
+
<% if definition %><p><span class="font-medium text-gray-700 dark:text-gray-300">Captures:</span> <%= definition %></p><% end %>
|
|
10
|
+
<% if calculation %><p><span class="font-medium text-gray-700 dark:text-gray-300">Calc:</span> <%= calculation %></p><% end %>
|
|
11
|
+
</div>
|
|
8
12
|
<% end %>
|
|
9
13
|
</div>
|
|
10
14
|
<p class="<%= value_classes %>">
|
|
11
|
-
<%= value %><% if suffix? %><span class="<%= suffix_classes %>"> <%= suffix %></span><% end %>
|
|
15
|
+
<% if link? %><a href="<%= href %>" class="<%= value_link_classes %>"><%= value %></a><% else %><%= value %><% end %><% if suffix? %><span class="<%= suffix_classes %>"> <%= suffix %></span><% end %>
|
|
12
16
|
</p>
|
|
13
17
|
<% if change? %>
|
|
14
18
|
<p class="mt-1 text-sm font-medium <%= change_classes %>"><%= change_label %></p>
|
|
15
19
|
<% end %>
|
|
16
|
-
<% if info? %>
|
|
17
|
-
<div class="<%= disclosure_classes %>" data-stat-card-info-target="panel">
|
|
18
|
-
<% if definition %><p><span class="font-medium text-gray-700 dark:text-gray-300">Captures:</span> <%= definition %></p><% end %>
|
|
19
|
-
<% if calculation %><p><span class="font-medium text-gray-700 dark:text-gray-300">Calc:</span> <%= calculation %></p><% end %>
|
|
20
|
-
</div>
|
|
21
|
-
<% end %>
|
|
22
20
|
</div>
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
module Keystone
|
|
4
4
|
module Ui
|
|
5
5
|
class StatCardComponent < ViewComponent::Base
|
|
6
|
-
CARD_CLASSES = "rounded-xl border border-gray-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
|
6
|
+
CARD_CLASSES = "relative rounded-xl border border-gray-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
|
7
7
|
LABEL_CLASSES = "text-sm font-medium text-gray-500 dark:text-gray-400"
|
|
8
8
|
VALUE_BASE_CLASSES = "mt-1 text-3xl font-bold"
|
|
9
9
|
SUFFIX_CLASSES = "text-lg text-gray-500 dark:text-gray-400"
|
|
10
|
-
DISCLOSURE_CLASSES = "hidden mt-
|
|
11
|
-
|
|
10
|
+
DISCLOSURE_CLASSES = "hidden peer-hover:block peer-focus-visible:block absolute inset-x-0 top-full z-10 mt-2 space-y-1 rounded-lg border border-gray-200 bg-white p-3 text-sm text-gray-600 shadow-lg dark:border-zinc-700 dark:bg-zinc-800 dark:text-gray-400"
|
|
11
|
+
VALUE_LINK_CLASSES = "hover:underline focus:outline-none focus-visible:underline"
|
|
12
|
+
INFO_BUTTON_CLASSES = "peer shrink-0 text-gray-400 transition hover:text-accent-600 dark:hover:text-accent-400"
|
|
12
13
|
|
|
13
14
|
INFO_ICON = <<~SVG.freeze
|
|
14
15
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
|
@@ -22,9 +23,9 @@ module Keystone
|
|
|
22
23
|
info: "text-accent-600 dark:text-accent-400"
|
|
23
24
|
}.freeze
|
|
24
25
|
|
|
25
|
-
attr_reader :label, :value, :suffix, :definition, :calculation, :change
|
|
26
|
+
attr_reader :label, :value, :suffix, :definition, :calculation, :change, :href
|
|
26
27
|
|
|
27
|
-
def initialize(label:, value:, variant: :neutral, suffix: nil, definition: nil, calculation: nil, change: nil)
|
|
28
|
+
def initialize(label:, value:, variant: :neutral, suffix: nil, definition: nil, calculation: nil, change: nil, href: nil)
|
|
28
29
|
@label = label
|
|
29
30
|
@value = value
|
|
30
31
|
@variant = variant
|
|
@@ -32,6 +33,7 @@ module Keystone
|
|
|
32
33
|
@definition = definition
|
|
33
34
|
@calculation = calculation
|
|
34
35
|
@change = change
|
|
36
|
+
@href = href
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
def classes
|
|
@@ -62,6 +64,14 @@ module Keystone
|
|
|
62
64
|
!@change.nil?
|
|
63
65
|
end
|
|
64
66
|
|
|
67
|
+
def link?
|
|
68
|
+
!@href.nil?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def value_link_classes
|
|
72
|
+
VALUE_LINK_CLASSES
|
|
73
|
+
end
|
|
74
|
+
|
|
65
75
|
def change_label
|
|
66
76
|
formatted = "#{format("%.1f", @change.abs)}%"
|
|
67
77
|
return formatted if @change.zero?
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module Keystone
|
|
4
4
|
module Ui
|
|
5
5
|
class TextareaComponent < ViewComponent::Base
|
|
6
|
-
BASE_CLASSES = "
|
|
6
|
+
BASE_CLASSES = "ks-input"
|
|
7
7
|
|
|
8
|
-
DISABLED_CLASSES = "
|
|
8
|
+
DISABLED_CLASSES = "ks-input-disabled"
|
|
9
9
|
|
|
10
10
|
def initialize(name:, value: nil, rows: 3, placeholder: nil, disabled: false)
|
|
11
11
|
@name = name
|
|
@@ -15,10 +15,8 @@ module Keystone
|
|
|
15
15
|
@disabled = disabled
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
FOCUS_CLASSES = "focus:border-accent-500 focus:ring-accent-500 dark:focus:border-accent-400 dark:focus:ring-accent-400"
|
|
19
|
-
|
|
20
18
|
def classes
|
|
21
|
-
tokens = [ BASE_CLASSES
|
|
19
|
+
tokens = [ BASE_CLASSES ]
|
|
22
20
|
tokens << DISABLED_CLASSES if @disabled
|
|
23
21
|
tokens.join(" ")
|
|
24
22
|
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div class="<%= GROUP_CLASSES %>" role="group" aria-label="Theme" data-controller="theme-toggle">
|
|
2
|
+
<% options.each do |label, mode| %>
|
|
3
|
+
<button
|
|
4
|
+
type="button"
|
|
5
|
+
class="<%= OPTION_CLASSES %>"
|
|
6
|
+
aria-pressed="<%= pressed?(mode) %>"
|
|
7
|
+
data-action="theme-toggle#choose"
|
|
8
|
+
data-theme-toggle-mode-param="<%= mode %>"><%= label %></button>
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class ThemeToggleComponent < ViewComponent::Base
|
|
6
|
+
OPTIONS = [ [ "Light", "light" ], [ "Dark", "dark" ], [ "System", "system" ] ].freeze
|
|
7
|
+
GROUP_CLASSES = "inline-flex gap-1"
|
|
8
|
+
OPTION_CLASSES = "ks-button ks-button-sm ks-button-secondary aria-pressed:bg-accent-600 aria-pressed:hover:bg-accent-500"
|
|
9
|
+
|
|
10
|
+
def initialize(current: nil)
|
|
11
|
+
@current = current
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def options
|
|
15
|
+
OPTIONS
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def pressed?(mode)
|
|
19
|
+
mode == (KeystoneUi::ThemeChoice::MODES.include?(@current) ? @current : "system")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -41,6 +41,14 @@ module KeystoneUiHelper
|
|
|
41
41
|
render Keystone::Ui::BadgeComponent.new(**args)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
def ui_theme_toggle
|
|
45
|
+
render Keystone::Ui::ThemeToggleComponent.new(current: cookies[KeystoneUi::ThemeChoice::COOKIE])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def keystone_theme_attributes
|
|
49
|
+
KeystoneUi::ThemeChoice.new(cookies[KeystoneUi::ThemeChoice::COOKIE]).html_attributes_markup.html_safe
|
|
50
|
+
end
|
|
51
|
+
|
|
44
52
|
def ui_copy_button(**args)
|
|
45
53
|
render Keystone::Ui::CopyButtonComponent.new(**args)
|
|
46
54
|
end
|
data/config/importmap.rb
CHANGED
|
@@ -13,6 +13,7 @@ pin "keystone_ui/accordion_controller", to: "keystone_ui/accordion_controller.js
|
|
|
13
13
|
pin "keystone_ui/stat_card_info_controller", to: "keystone_ui/stat_card_info_controller.js"
|
|
14
14
|
pin "keystone_ui/auto_submit_controller", to: "keystone_ui/auto_submit_controller.js"
|
|
15
15
|
pin "keystone_ui/line_chart_controller", to: "keystone_ui/line_chart_controller.js"
|
|
16
|
+
pin "keystone_ui/theme_toggle_controller", to: "keystone_ui/theme_toggle_controller.js"
|
|
16
17
|
|
|
17
18
|
# Chart.js for the line-chart controller, so host apps don't pin it themselves.
|
|
18
19
|
# Vendored as a single self-contained bundle (deps inlined, no external imports).
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "layout_theme_attributes"
|
|
4
|
+
|
|
3
5
|
module Keystone
|
|
4
6
|
class InstallGenerator < Rails::Generators::Base
|
|
5
7
|
desc "Set up Keystone UI in your Rails application"
|
|
@@ -11,6 +13,8 @@ module Keystone
|
|
|
11
13
|
JS_IMPORT = 'import { registerControllers } from "keystone_ui/index"'
|
|
12
14
|
JS_REGISTER = "registerControllers(application)"
|
|
13
15
|
|
|
16
|
+
LAYOUT_PATH = "app/views/layouts/application.html.erb"
|
|
17
|
+
|
|
14
18
|
def setup_tailwind
|
|
15
19
|
say ""
|
|
16
20
|
say "Keystone UI — setup", :green
|
|
@@ -86,5 +90,26 @@ module Keystone
|
|
|
86
90
|
append_to_file js_path, "\n#{JS_IMPORT}\n#{JS_REGISTER}\n"
|
|
87
91
|
say " ✔ Registered Keystone UI controllers", :green
|
|
88
92
|
end
|
|
93
|
+
|
|
94
|
+
def setup_theme
|
|
95
|
+
say ""
|
|
96
|
+
say "Wiring the Keystone UI theme toggle...", :green
|
|
97
|
+
|
|
98
|
+
layout_path = Rails.root.join(LAYOUT_PATH)
|
|
99
|
+
unless layout_path.exist?
|
|
100
|
+
say " ⚠ #{LAYOUT_PATH} not found — add `<%= keystone_theme_attributes %>` to your <html> tag manually.", :yellow
|
|
101
|
+
return
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
layout = layout_path.read
|
|
105
|
+
updated = Keystone::LayoutThemeAttributes.new(layout).apply
|
|
106
|
+
|
|
107
|
+
if updated == layout
|
|
108
|
+
say " ✔ Layout already marks the theme", :green
|
|
109
|
+
else
|
|
110
|
+
File.write(layout_path, updated)
|
|
111
|
+
say " ✔ Added the theme attributes to the layout's html tag", :green
|
|
112
|
+
end
|
|
113
|
+
end
|
|
89
114
|
end
|
|
90
115
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
class LayoutThemeAttributes
|
|
5
|
+
HELPER_CALL = "<%= keystone_theme_attributes %>"
|
|
6
|
+
|
|
7
|
+
def initialize(layout)
|
|
8
|
+
@layout = layout
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def apply
|
|
12
|
+
return @layout if @layout.include?(HELPER_CALL)
|
|
13
|
+
|
|
14
|
+
@layout.sub(/<html\b/, "<html #{HELPER_CALL}")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/keystone_ui/engine.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "keystone_ui/source_css"
|
|
4
|
+
|
|
3
5
|
module KeystoneUi
|
|
4
6
|
class Engine < ::Rails::Engine
|
|
5
7
|
config.autoload_paths << root.join("app/components")
|
|
@@ -27,21 +29,7 @@ module KeystoneUi
|
|
|
27
29
|
keystone_import = '@import "./keystone_source.css";'
|
|
28
30
|
next unless css_path.read.include?(keystone_import)
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
lines = [ %(@source "#{root}/app/components/**/*.{erb,rb}";) ]
|
|
32
|
-
|
|
33
|
-
# Import theme CSS (accent + surface custom property defaults)
|
|
34
|
-
theme_css = root.join("app/assets/tailwind/keystone_ui_engine/theme.css")
|
|
35
|
-
lines << %(@import "#{theme_css}";) if theme_css.exist?
|
|
36
|
-
|
|
37
|
-
# Import component CSS files shipped with the gem
|
|
38
|
-
nav_css = root.join("app/assets/tailwind/keystone_ui_engine/nav.css")
|
|
39
|
-
lines << %(@import "#{nav_css}";) if nav_css.exist?
|
|
40
|
-
|
|
41
|
-
color_picker_css = root.join("app/assets/tailwind/keystone_ui_engine/color_picker.css")
|
|
42
|
-
lines << %(@import "#{color_picker_css}";) if color_picker_css.exist?
|
|
43
|
-
|
|
44
|
-
source_css.write(lines.join("\n") + "\n")
|
|
32
|
+
tailwind_dir.join("keystone_source.css").write(KeystoneUi::SourceCss.new(root).to_s)
|
|
45
33
|
end
|
|
46
34
|
end
|
|
47
35
|
end
|
data/lib/keystone_ui/safelist.rb
CHANGED
|
@@ -52,7 +52,8 @@ module Keystone
|
|
|
52
52
|
Keystone::Ui::FunnelComponent,
|
|
53
53
|
Keystone::Ui::PipelineComponent,
|
|
54
54
|
Keystone::Ui::CodeComponent,
|
|
55
|
-
Keystone::Ui::DisclosureComponent
|
|
55
|
+
Keystone::Ui::DisclosureComponent,
|
|
56
|
+
Keystone::Ui::ThemeToggleComponent
|
|
56
57
|
].freeze
|
|
57
58
|
|
|
58
59
|
# Constants that hold non-CSS values (e.g. HTML input type maps)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "keystone_ui/styles"
|
|
5
|
+
|
|
6
|
+
module KeystoneUi
|
|
7
|
+
class SourceCss
|
|
8
|
+
def initialize(root)
|
|
9
|
+
@root = Pathname.new(root)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_s
|
|
13
|
+
lines.join("\n") + "\n"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def styles_entry
|
|
19
|
+
KeystoneUi::Styles::Engine.root.join("app/assets/tailwind/keystone_ui_styles/engine.css")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def lines
|
|
23
|
+
[
|
|
24
|
+
%(@import "#{styles_entry}";),
|
|
25
|
+
%(@source "#{@root}/app/components/**/*.{erb,rb}";),
|
|
26
|
+
%(@import "#{@root}/app/assets/tailwind/keystone_ui_engine/nav.css";),
|
|
27
|
+
%(@import "#{@root}/app/assets/tailwind/keystone_ui_engine/color_picker.css";)
|
|
28
|
+
]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KeystoneUi
|
|
4
|
+
class ThemeChoice
|
|
5
|
+
COOKIE = "keystone_theme"
|
|
6
|
+
MODES = %w[light dark].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(mode)
|
|
9
|
+
@mode = mode
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def html_attributes
|
|
13
|
+
MODES.include?(@mode) ? { "data-theme" => @mode } : {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def html_attributes_markup
|
|
17
|
+
html_attributes.map { |name, value| %(#{name}="#{value}") }.join(" ")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/keystone_ui/version.rb
CHANGED
data/lib/keystone_ui.rb
CHANGED
|
@@ -157,11 +157,14 @@ one raises at render time.
|
|
|
157
157
|
close control.
|
|
158
158
|
- `ui_progress(value:, max:, label: nil)` — a labeled progress bar. The percent
|
|
159
159
|
is `value / max`, rounded and clamped at 100.
|
|
160
|
-
- `ui_stat_card(label:, value:, variant: :neutral, suffix: nil, definition: nil, calculation: nil, change: nil)`
|
|
160
|
+
- `ui_stat_card(label:, value:, variant: :neutral, suffix: nil, definition: nil, calculation: nil, change: nil, href: nil)`
|
|
161
161
|
— a single metric tile. `variant:` `:neutral` `:success` `:danger` `:warning`
|
|
162
162
|
`:info` colors the value. `change:` is a signed number rendered as `▲ 4.2%` in
|
|
163
|
-
green when positive, `▼` in red when negative, plain when zero.
|
|
164
|
-
|
|
163
|
+
green when positive, `▼` in red when negative, plain when zero. `href:` makes
|
|
164
|
+
the value a link to its drill-down screen; only the value is linked, never the
|
|
165
|
+
whole card. Passing `definition:` and/or `calculation:` adds an info button
|
|
166
|
+
whose details show in a panel floating below the card while the button is
|
|
167
|
+
hovered or focused, and tapping the button toggles it.
|
|
165
168
|
- `ui_copy_button(text:, label: "Copy", success_message: "Copied!", error_message: "Failed!")`
|
|
166
169
|
— copies `text:` to the clipboard.
|
|
167
170
|
- `ui_code(language: nil, caption: nil)` — takes a block holding the code. The
|
|
@@ -242,7 +245,8 @@ one raises at render time.
|
|
|
242
245
|
5. Fill the body with the leaf helpers from the Interface above. Reach for the
|
|
243
246
|
most specific one that fits — `ui_form_field` over `ui_input`,
|
|
244
247
|
`ui_data_table` over a hand-built `<table>`, `ui_stat_card` over a panel with
|
|
245
|
-
text in it.
|
|
248
|
+
text in it. To make a stat card clickable, pass `href:` — never wrap it in
|
|
249
|
+
`ui_card_link`, because a tap on the info button would then follow the link.
|
|
246
250
|
|
|
247
251
|
6. For a table, decide how columns are declared. Use `{ key: "Label" }` hashes
|
|
248
252
|
when every column is plain. Switch the whole set to `Keystone::Ui::Column`
|
data/the_local/interface.yml
CHANGED
|
@@ -105,6 +105,7 @@ sources:
|
|
|
105
105
|
- app/components/keystone/ui/settings_link_component.rb
|
|
106
106
|
- app/components/keystone/ui/show_page_component.rb
|
|
107
107
|
- app/components/keystone/ui/stat_card_component.rb
|
|
108
|
+
- app/components/keystone/ui/stat_card_component.html.erb
|
|
108
109
|
- app/components/keystone/ui/swipe_deck_component.rb
|
|
109
110
|
- app/components/keystone/ui/tab_switcher_component.rb
|
|
110
111
|
- app/components/keystone/ui/textarea_component.rb
|
metadata
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: keystone_ui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tyler Schneider
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: keystone_ui-styles
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.1.0
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 0.1.0
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: view_component
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -37,6 +51,7 @@ executables: []
|
|
|
37
51
|
extensions: []
|
|
38
52
|
extra_rdoc_files: []
|
|
39
53
|
files:
|
|
54
|
+
- MIT-LICENSE
|
|
40
55
|
- app/assets/javascripts/keystone_ui/accordion_controller.js
|
|
41
56
|
- app/assets/javascripts/keystone_ui/auto_submit_controller.js
|
|
42
57
|
- app/assets/javascripts/keystone_ui/chartjs.js
|
|
@@ -53,10 +68,10 @@ files:
|
|
|
53
68
|
- app/assets/javascripts/keystone_ui/stat_card_info_controller.js
|
|
54
69
|
- app/assets/javascripts/keystone_ui/swipe_deck_controller.js
|
|
55
70
|
- app/assets/javascripts/keystone_ui/tab_switcher_controller.js
|
|
71
|
+
- app/assets/javascripts/keystone_ui/theme_toggle_controller.js
|
|
56
72
|
- app/assets/tailwind/keystone_ui_engine/color_picker.css
|
|
57
73
|
- app/assets/tailwind/keystone_ui_engine/engine.css
|
|
58
74
|
- app/assets/tailwind/keystone_ui_engine/nav.css
|
|
59
|
-
- app/assets/tailwind/keystone_ui_engine/theme.css
|
|
60
75
|
- app/components/keystone/ui/accordion_component.html.erb
|
|
61
76
|
- app/components/keystone/ui/accordion_component.rb
|
|
62
77
|
- app/components/keystone/ui/alert_component.html.erb
|
|
@@ -154,13 +169,18 @@ files:
|
|
|
154
169
|
- app/components/keystone/ui/tab_switcher_component.rb
|
|
155
170
|
- app/components/keystone/ui/textarea_component.html.erb
|
|
156
171
|
- app/components/keystone/ui/textarea_component.rb
|
|
172
|
+
- app/components/keystone/ui/theme_toggle_component.html.erb
|
|
173
|
+
- app/components/keystone/ui/theme_toggle_component.rb
|
|
157
174
|
- app/helpers/keystone_ui_helper.rb
|
|
158
175
|
- config/importmap.rb
|
|
159
176
|
- lib/generators/keystone/install_generator.rb
|
|
177
|
+
- lib/generators/keystone/layout_theme_attributes.rb
|
|
160
178
|
- lib/keystone_ui.rb
|
|
161
179
|
- lib/keystone_ui/configuration.rb
|
|
162
180
|
- lib/keystone_ui/engine.rb
|
|
163
181
|
- lib/keystone_ui/safelist.rb
|
|
182
|
+
- lib/keystone_ui/source_css.rb
|
|
183
|
+
- lib/keystone_ui/theme_choice.rb
|
|
164
184
|
- lib/keystone_ui/version.rb
|
|
165
185
|
- the_local/agents/keystone_ui-develop.md
|
|
166
186
|
- the_local/agents/keystone_ui-info.md
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/* Keystone UI theme — defines accent and surface color tokens as CSS custom properties.
|
|
2
|
-
Apps override these variables to customize the color palette. */
|
|
3
|
-
|
|
4
|
-
@theme {
|
|
5
|
-
/* Accent — defaults to blue */
|
|
6
|
-
--color-accent-50: #eff6ff;
|
|
7
|
-
--color-accent-100: #dbeafe;
|
|
8
|
-
--color-accent-200: #bfdbfe;
|
|
9
|
-
--color-accent-300: #93c5fd;
|
|
10
|
-
--color-accent-400: #60a5fa;
|
|
11
|
-
--color-accent-500: #3b82f6;
|
|
12
|
-
--color-accent-600: #2563eb;
|
|
13
|
-
--color-accent-700: #1d4ed8;
|
|
14
|
-
--color-accent-800: #1e40af;
|
|
15
|
-
--color-accent-900: #1e3a8a;
|
|
16
|
-
|
|
17
|
-
/* Surface — defaults to zinc */
|
|
18
|
-
--color-surface-50: #fafafa;
|
|
19
|
-
--color-surface-100: #f4f4f5;
|
|
20
|
-
--color-surface-200: #e4e4e7;
|
|
21
|
-
--color-surface-300: #d4d4d8;
|
|
22
|
-
--color-surface-400: #a1a1aa;
|
|
23
|
-
--color-surface-500: #71717a;
|
|
24
|
-
--color-surface-600: #52525b;
|
|
25
|
-
--color-surface-700: #3f3f46;
|
|
26
|
-
--color-surface-800: #27272a;
|
|
27
|
-
--color-surface-900: #18181b;
|
|
28
|
-
--color-surface-950: #09090b;
|
|
29
|
-
|
|
30
|
-
/* Accent — darkest shade */
|
|
31
|
-
--color-accent-950: #172554;
|
|
32
|
-
}
|