keystone_ui 0.7.0 → 0.9.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: 794dd244d2cfac2f3389a4e4405e8e127c759fb310aac4c9e00b55c6c4501be7
4
- data.tar.gz: 7ebebe2b82a82e423dd2ab440c5c804192355a9aab7f00ffffaf513f05c902de
3
+ metadata.gz: 01e2ba0387ee8dd44d772be3546d44dada399d754099e486b607a73056c85606
4
+ data.tar.gz: ea5acc7537974e4bd59f7415e267b0038148fecd0890dc0b91a86f1af9a3801d
5
5
  SHA512:
6
- metadata.gz: e214dca626bc889d34fb68f322d0fd311a118b017e2ede2092d433df032c41163e2469ac67fe305b78164692cbaf3a73b25691c9cbc34686446f70d971767443
7
- data.tar.gz: 811a7d3124b5d8b090239c38c0fd8b9dbfa74c225f92f1385d0f1ec0125a55d0b2234636a25b3d7d9010af37d23f91ba261e3d8154e2ef68ff5ff14e0041dfe3
6
+ metadata.gz: 1f7b432026de7ac8ededb44facb8488df5ded58f185b91b9981654c978d7a32ce567b43b5fe38359c036a1457092a633ca54e2b5a873654250aac36b99bd1a08
7
+ data.tar.gz: fb986e2fde289d1c0b969096a04a02be8664544fbe7046d93c358b8d054440da8eed66824e23a23644b02efb76e0d25c95647bf0b32d06582dd0a5b4a0fd5bb1
@@ -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 = "inline-flex items-center justify-center font-semibold rounded-lg border-0 cursor-pointer no-underline"
6
+ BASE_CLASSES = "ks-button"
7
7
 
8
8
  VARIANT_CLASSES = {
9
- secondary: "bg-gray-500 text-white hover:bg-gray-400",
10
- danger: "bg-red-600 text-white hover:bg-red-500"
9
+ secondary: "ks-button-secondary",
10
+ danger: "ks-button-danger"
11
11
  }.freeze
12
12
 
13
13
  SIZE_CLASSES = {
14
- sm: "text-sm px-3 py-1.5",
15
- md: "text-base px-4 py-2",
16
- lg: "text-lg px-5 py-3"
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
- "bg-accent-600 text-white hover:bg-accent-500"
30
+ "ks-button-primary"
31
31
  else
32
32
  VARIANT_CLASSES.fetch(@variant)
33
33
  end
@@ -0,0 +1,9 @@
1
+ <label class="<%= row_classes %>">
2
+ <input type="checkbox" name="<%= name %>" value="<%= value %>" <%= "checked" if checked? %> class="<%= input_classes %>">
3
+ <span>
4
+ <span class="<%= label_classes %>"><%= label %></span>
5
+ <% if hint? %>
6
+ <span class="<%= hint_classes %>"><%= hint %></span>
7
+ <% end %>
8
+ </span>
9
+ </label>
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class CheckboxRowComponent < ViewComponent::Base
6
+ ROW_CLASSES = "flex items-start gap-3 py-3 cursor-pointer"
7
+ INPUT_CLASSES = "mt-0.5 size-4 shrink-0 rounded border-surface-300 text-accent-600 focus:ring-accent-500"
8
+ LABEL_CLASSES = "block text-sm font-medium text-surface-900"
9
+ HINT_CLASSES = "block mt-0.5 text-sm text-surface-500"
10
+
11
+ attr_reader :name, :value, :label, :hint
12
+
13
+ def initialize(name:, value:, label:, hint: nil, checked: false)
14
+ @name = name
15
+ @value = value
16
+ @label = label
17
+ @hint = hint
18
+ @checked = checked
19
+ end
20
+
21
+ def checked?
22
+ @checked
23
+ end
24
+
25
+ def hint?
26
+ !@hint.nil?
27
+ end
28
+
29
+ def row_classes
30
+ ROW_CLASSES
31
+ end
32
+
33
+ def input_classes
34
+ INPUT_CLASSES
35
+ end
36
+
37
+ def label_classes
38
+ LABEL_CLASSES
39
+ end
40
+
41
+ def hint_classes
42
+ HINT_CLASSES
43
+ end
44
+ end
45
+ end
46
+ 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 %> <%= Keystone::Ui::SelectComponent::FOCUS_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 = "block text-sm font-medium text-gray-700 dark:text-gray-300"
8
- REQUIRED_CLASSES = "text-red-500 ml-0.5"
9
- HINT_CLASSES = "mt-1 text-sm text-gray-500 dark:text-gray-400"
10
- ERROR_CLASSES = "mt-1 text-sm text-red-600 dark:text-red-400"
11
- CHECKBOX_CLASSES = "rounded border-gray-300 text-accent-600 focus:ring-accent-500 dark:border-zinc-600 dark:bg-zinc-900"
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 = "block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-1 dark:bg-zinc-900 dark:border-zinc-700 dark:text-white dark:placeholder:text-gray-500"
6
+ BASE_CLASSES = "ks-input"
7
7
 
8
- DISABLED_CLASSES = "cursor-not-allowed bg-gray-50 text-gray-500 dark:bg-zinc-800 dark:text-gray-400"
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, FOCUS_CLASSES ]
30
+ tokens = [ BASE_CLASSES ]
33
31
  tokens << DISABLED_CLASSES if @disabled
34
32
  tokens.join(" ")
35
33
  end
@@ -12,7 +12,7 @@ module Keystone
12
12
  @shadow = shadow
13
13
  end
14
14
 
15
- BASE_CLASSES = "border border-gray-200 bg-white dark:bg-zinc-900 dark:border-zinc-700"
15
+ BASE_CLASSES = "ks-panel"
16
16
  SHADOW_CLASS = "shadow-sm"
17
17
 
18
18
  def classes
@@ -3,9 +3,9 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class SelectComponent < ViewComponent::Base
6
- BASE_CLASSES = "block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:outline-none focus:ring-1 dark:bg-zinc-900 dark:border-zinc-700 dark:text-white"
6
+ BASE_CLASSES = "ks-input"
7
7
 
8
- DISABLED_CLASSES = "cursor-not-allowed bg-gray-50 text-gray-500 dark:bg-zinc-800 dark:text-gray-400"
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, FOCUS_CLASSES ]
21
+ tokens = [ BASE_CLASSES ]
24
22
  tokens << DISABLED_CLASSES if @disabled
25
23
  tokens.join(" ")
26
24
  end
@@ -3,9 +3,9 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class TextareaComponent < ViewComponent::Base
6
- BASE_CLASSES = "block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-1 dark:bg-zinc-900 dark:border-zinc-700 dark:text-white dark:placeholder:text-gray-500"
6
+ BASE_CLASSES = "ks-input"
7
7
 
8
- DISABLED_CLASSES = "cursor-not-allowed bg-gray-50 text-gray-500 dark:bg-zinc-800 dark:text-gray-400"
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, FOCUS_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
@@ -145,6 +153,10 @@ module KeystoneUiHelper
145
153
  render Keystone::Ui::RadioCardComponent.new(**args)
146
154
  end
147
155
 
156
+ def ui_checkbox_row(**args)
157
+ render Keystone::Ui::CheckboxRowComponent.new(**args)
158
+ end
159
+
148
160
  def ui_progress(**args)
149
161
  render Keystone::Ui::ProgressComponent.new(**args)
150
162
  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
@@ -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
- source_css = tailwind_dir.join("keystone_source.css")
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
@@ -42,6 +42,7 @@ module Keystone
42
42
  Keystone::Ui::ShowPageComponent,
43
43
  Keystone::Ui::OptionCardComponent,
44
44
  Keystone::Ui::RadioCardComponent,
45
+ Keystone::Ui::CheckboxRowComponent,
45
46
  Keystone::Ui::ProgressComponent,
46
47
  Keystone::Ui::ColorPickerComponent,
47
48
  Keystone::Ui::MultiSelectComponent,
@@ -52,7 +53,8 @@ module Keystone
52
53
  Keystone::Ui::FunnelComponent,
53
54
  Keystone::Ui::PipelineComponent,
54
55
  Keystone::Ui::CodeComponent,
55
- Keystone::Ui::DisclosureComponent
56
+ Keystone::Ui::DisclosureComponent,
57
+ Keystone::Ui::ThemeToggleComponent
56
58
  ].freeze
57
59
 
58
60
  # 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KeystoneUi
4
- VERSION = "0.7.0"
4
+ VERSION = "0.9.0"
5
5
  end
data/lib/keystone_ui.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "view_component"
4
+ require "keystone_ui/styles"
4
5
  require "keystone_ui/configuration"
6
+ require "keystone_ui/theme_choice"
5
7
  require "keystone_ui/engine"
6
8
  require "keystone_ui/version"
7
9
 
@@ -117,6 +117,11 @@ one raises at render time.
117
117
  hue/saturation panel and writes the hex into a hidden input named `name`.
118
118
  - `ui_radio_card(name:, value:, label:, hint: nil, checked: false)` — a
119
119
  selectable card backed by a real radio input; selection styling is pure CSS.
120
+ - `ui_checkbox_row(name:, value:, label:, hint: nil, checked: false)` — a real
121
+ checkbox with its label and optional hint inside one `<label>`, so a tap
122
+ anywhere on the row toggles the box. A checked row submits `value` under
123
+ `name`; give several rows the same array name, e.g. `"shown[]"`, to submit the
124
+ checked values as a list. An unchecked row submits nothing. No JavaScript.
120
125
  - `ui_option_card(name:, value:, selected: false, input_data: {}, label_data: {})`
121
126
  — takes a block. A radio whose visible body is whatever the block renders.
122
127
  `input_data:`/`label_data:` become `data-*` attributes on the input and label.
@@ -41,6 +41,7 @@ develop:
41
41
  - ui_bottom_nav_item
42
42
  - ui_option_card
43
43
  - ui_radio_card
44
+ - ui_checkbox_row
44
45
  - ui_progress
45
46
  - ui_settings_link
46
47
  - ui_color_picker
@@ -68,6 +69,8 @@ sources:
68
69
  - app/components/keystone/ui/card_component.rb
69
70
  - app/components/keystone/ui/card_link_component.rb
70
71
  - app/components/keystone/ui/chart_card_component.rb
72
+ - app/components/keystone/ui/checkbox_row_component.rb
73
+ - app/components/keystone/ui/checkbox_row_component.html.erb
71
74
  - app/components/keystone/ui/code_component.rb
72
75
  - app/components/keystone/ui/color_picker_component.rb
73
76
  - app/components/keystone/ui/column_picker_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.7.0
4
+ version: 0.9.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-09-13 00:00:00.000000000 Z
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
@@ -54,10 +68,10 @@ files:
54
68
  - app/assets/javascripts/keystone_ui/stat_card_info_controller.js
55
69
  - app/assets/javascripts/keystone_ui/swipe_deck_controller.js
56
70
  - app/assets/javascripts/keystone_ui/tab_switcher_controller.js
71
+ - app/assets/javascripts/keystone_ui/theme_toggle_controller.js
57
72
  - app/assets/tailwind/keystone_ui_engine/color_picker.css
58
73
  - app/assets/tailwind/keystone_ui_engine/engine.css
59
74
  - app/assets/tailwind/keystone_ui_engine/nav.css
60
- - app/assets/tailwind/keystone_ui_engine/theme.css
61
75
  - app/components/keystone/ui/accordion_component.html.erb
62
76
  - app/components/keystone/ui/accordion_component.rb
63
77
  - app/components/keystone/ui/alert_component.html.erb
@@ -76,6 +90,8 @@ files:
76
90
  - app/components/keystone/ui/card_link_component.rb
77
91
  - app/components/keystone/ui/chart_card_component.html.erb
78
92
  - app/components/keystone/ui/chart_card_component.rb
93
+ - app/components/keystone/ui/checkbox_row_component.html.erb
94
+ - app/components/keystone/ui/checkbox_row_component.rb
79
95
  - app/components/keystone/ui/code_component.html.erb
80
96
  - app/components/keystone/ui/code_component.rb
81
97
  - app/components/keystone/ui/color_picker_component.html.erb
@@ -155,13 +171,18 @@ files:
155
171
  - app/components/keystone/ui/tab_switcher_component.rb
156
172
  - app/components/keystone/ui/textarea_component.html.erb
157
173
  - app/components/keystone/ui/textarea_component.rb
174
+ - app/components/keystone/ui/theme_toggle_component.html.erb
175
+ - app/components/keystone/ui/theme_toggle_component.rb
158
176
  - app/helpers/keystone_ui_helper.rb
159
177
  - config/importmap.rb
160
178
  - lib/generators/keystone/install_generator.rb
179
+ - lib/generators/keystone/layout_theme_attributes.rb
161
180
  - lib/keystone_ui.rb
162
181
  - lib/keystone_ui/configuration.rb
163
182
  - lib/keystone_ui/engine.rb
164
183
  - lib/keystone_ui/safelist.rb
184
+ - lib/keystone_ui/source_css.rb
185
+ - lib/keystone_ui/theme_choice.rb
165
186
  - lib/keystone_ui/version.rb
166
187
  - the_local/agents/keystone_ui-develop.md
167
188
  - 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
- }