keystone_ui 0.16.0 → 0.18.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/keystone_ui/file_upload_controller.js +2 -4
  3. data/app/components/keystone/ui/accordion_component.rb +9 -9
  4. data/app/components/keystone/ui/bottom_nav_component.rb +1 -1
  5. data/app/components/keystone/ui/bottom_nav_item_component.rb +2 -2
  6. data/app/components/keystone/ui/bucket_component.rb +9 -9
  7. data/app/components/keystone/ui/bucket_series_component.rb +1 -1
  8. data/app/components/keystone/ui/card_link_component.rb +4 -4
  9. data/app/components/keystone/ui/chart_card_component.rb +2 -2
  10. data/app/components/keystone/ui/checkbox_row_component.rb +4 -4
  11. data/app/components/keystone/ui/code_component.rb +3 -3
  12. data/app/components/keystone/ui/color_picker_component.html.erb +1 -1
  13. data/app/components/keystone/ui/color_picker_component.rb +4 -2
  14. data/app/components/keystone/ui/column_picker_component.rb +4 -4
  15. data/app/components/keystone/ui/copy_button_component.rb +1 -1
  16. data/app/components/keystone/ui/cta_banner_component.rb +7 -7
  17. data/app/components/keystone/ui/data_table_component.html.erb +4 -4
  18. data/app/components/keystone/ui/data_table_component.rb +14 -10
  19. data/app/components/keystone/ui/disclosure_component.rb +4 -4
  20. data/app/components/keystone/ui/feature_grid_component.rb +13 -13
  21. data/app/components/keystone/ui/file_upload_component.rb +10 -10
  22. data/app/components/keystone/ui/form_component.rb +1 -1
  23. data/app/components/keystone/ui/form_field_component.rb +2 -2
  24. data/app/components/keystone/ui/form_page_component.rb +2 -2
  25. data/app/components/keystone/ui/funnel_component.rb +12 -12
  26. data/app/components/keystone/ui/grid_component.rb +3 -3
  27. data/app/components/keystone/ui/hero_component.rb +11 -11
  28. data/app/components/keystone/ui/mobile_actions_component.rb +2 -2
  29. data/app/components/keystone/ui/mobile_header_component.rb +4 -4
  30. data/app/components/keystone/ui/modal_component.rb +5 -5
  31. data/app/components/keystone/ui/multi_select_component.rb +4 -4
  32. data/app/components/keystone/ui/nav_dropdown_component.rb +4 -4
  33. data/app/components/keystone/ui/nav_item_component.rb +1 -1
  34. data/app/components/keystone/ui/navbar_component.rb +1 -1
  35. data/app/components/keystone/ui/option_card_component.rb +2 -2
  36. data/app/components/keystone/ui/panel_component.rb +3 -3
  37. data/app/components/keystone/ui/pipeline_component.rb +14 -14
  38. data/app/components/keystone/ui/progress_component.rb +3 -3
  39. data/app/components/keystone/ui/radio_card_component.rb +4 -4
  40. data/app/components/keystone/ui/settings_link_component.html.erb +1 -1
  41. data/app/components/keystone/ui/settings_link_component.rb +3 -2
  42. data/app/components/keystone/ui/show_page_component.rb +2 -2
  43. data/app/components/keystone/ui/stat_card_component.html.erb +3 -3
  44. data/app/components/keystone/ui/stat_card_component.rb +17 -15
  45. data/app/components/keystone/ui/swipe_deck_component.html.erb +6 -6
  46. data/app/components/keystone/ui/swipe_deck_component.rb +7 -1
  47. data/app/components/keystone/ui/tab_switcher_component.rb +3 -3
  48. data/app/components/keystone/ui/theme_toggle_component.rb +2 -2
  49. data/app/helpers/keystone_ui_helper.rb +6 -1
  50. data/lib/keystone_ui/configuration.rb +16 -2
  51. data/lib/keystone_ui/look_choice.rb +19 -0
  52. data/lib/keystone_ui/source_css.rb +0 -1
  53. data/lib/keystone_ui/version.rb +1 -1
  54. data/lib/keystone_ui.rb +1 -0
  55. data/the_local/agents/keystone_ui-info.md +19 -0
  56. data/the_local/agents/keystone_ui-install.md +48 -4
  57. data/the_local/interface.yml +1 -0
  58. metadata +4 -4
  59. data/app/assets/tailwind/keystone_ui_engine/nav.css +0 -175
@@ -16,10 +16,10 @@ module Keystone
16
16
  SVG
17
17
 
18
18
  WRAPPER_CLASSES = "lg:hidden"
19
- BACK_LINK_CLASSES = "text-gray-500 dark:text-gray-400"
20
- TITLE_CLASSES = "absolute left-1/2 -translate-x-1/2 font-semibold text-gray-900 dark:text-white lg:hidden truncate max-w-[60%]"
21
- SUBTITLE_CLASSES = "block text-xs font-normal text-gray-500 dark:text-gray-400 truncate text-center"
22
- DROPDOWN_CLASSES = "hidden absolute right-0 z-50 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 dark:bg-zinc-800 dark:ring-white/10"
19
+ BACK_LINK_CLASSES = "ks-mobile-header-back"
20
+ TITLE_CLASSES = "ks-mobile-header-title absolute left-1/2 -translate-x-1/2 lg:hidden truncate max-w-[60%]"
21
+ SUBTITLE_CLASSES = "ks-mobile-header-subtitle block text-xs truncate text-center"
22
+ DROPDOWN_CLASSES = "ks-action-menu hidden absolute right-0 z-50 w-48 origin-top-right"
23
23
 
24
24
  # Renders the left-side and title of the mobile header.
25
25
  # Call from the left side of your navbar.
@@ -3,11 +3,11 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class ModalComponent < ViewComponent::Base
6
- BACKDROP_CLASSES = "hidden fixed inset-0 z-50 flex items-center justify-center bg-black/60"
7
- PANEL_CLASSES = "rounded-xl border border-gray-200 bg-white p-6 w-full mx-4 max-h-[80vh] flex flex-col dark:border-zinc-700 dark:bg-zinc-800"
8
- HEADER_CLASSES = "flex items-center justify-between mb-4"
9
- TITLE_CLASSES = "text-lg font-semibold text-gray-900 dark:text-gray-200"
10
- CLOSE_BUTTON_CLASSES = "text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
6
+ BACKDROP_CLASSES = "ks-modal-backdrop hidden fixed inset-0 z-50 flex items-center justify-center"
7
+ PANEL_CLASSES = "ks-modal-panel w-full max-h-[80vh] flex flex-col"
8
+ HEADER_CLASSES = "ks-modal-header flex items-center justify-between"
9
+ TITLE_CLASSES = "ks-modal-title text-lg"
10
+ CLOSE_BUTTON_CLASSES = "ks-modal-close"
11
11
  BODY_CLASSES = "overflow-auto flex-1"
12
12
 
13
13
  SIZE_CLASSES = {
@@ -4,10 +4,10 @@ module Keystone
4
4
  module Ui
5
5
  class MultiSelectComponent < ViewComponent::Base
6
6
  WRAPPER_CLASSES = "relative inline-block"
7
- TRIGGER_CLASSES = "inline-flex items-center gap-1 rounded-md border px-3 py-2 text-sm border-gray-300 bg-white text-gray-900 dark:border-zinc-700 dark:bg-zinc-900 dark:text-white"
8
- MENU_CLASSES = "absolute z-10 mt-1 w-56 rounded-md border border-gray-200 bg-white shadow-lg dark:border-zinc-700 dark:bg-zinc-900 hidden"
9
- OPTION_CLASSES = "flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-zinc-800 cursor-pointer"
10
- CHECKBOX_CLASSES = "rounded border-gray-300 text-accent-600 focus:ring-accent-500 dark:border-zinc-600"
7
+ TRIGGER_CLASSES = "ks-menu-trigger inline-flex items-center text-sm"
8
+ MENU_CLASSES = "ks-menu absolute z-10 w-56 hidden"
9
+ OPTION_CLASSES = "ks-menu-option flex items-center text-sm cursor-pointer"
10
+ CHECKBOX_CLASSES = "ks-menu-checkbox"
11
11
  CARET_ICON = <<~SVG.freeze
12
12
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
13
13
  <path fill-rule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
@@ -3,12 +3,12 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class NavDropdownComponent < ViewComponent::Base
6
- WRAPPER_CLASSES = "nav-dropdown"
7
- MENU_CLASSES = "nav-dropdown-menu hidden"
8
- TRIGGER_BASE = "nav-dropdown-trigger"
6
+ WRAPPER_CLASSES = "ks-nav-dropdown"
7
+ MENU_CLASSES = "ks-nav-dropdown-menu hidden"
8
+ TRIGGER_BASE = "ks-nav-dropdown-trigger"
9
9
  ACTIVE_CLASS = "active"
10
10
  CARET_ICON = <<~SVG.freeze
11
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="nav-dropdown-caret">
11
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="ks-nav-dropdown-caret">
12
12
  <path fill-rule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
13
13
  </svg>
14
14
  SVG
@@ -14,7 +14,7 @@ module Keystone
14
14
  ACTIVE_CLASS = "active"
15
15
 
16
16
  def link_classes
17
- @active ? ACTIVE_CLASS : ""
17
+ [ "ks-nav-item", (ACTIVE_CLASS if @active) ].compact.join(" ")
18
18
  end
19
19
  end
20
20
  end
@@ -7,7 +7,7 @@ module Keystone
7
7
  NAV_STICKY = "sticky top-0 z-40"
8
8
  MOBILE_LEFT_CLASSES = "lg:hidden flex items-center"
9
9
  LOGO_CLASSES = "logo"
10
- MOBILE_CENTER_CLASSES = "absolute left-1/2 -translate-x-1/2 font-semibold text-gray-900 dark:text-white lg:hidden truncate max-w-[60%]"
10
+ MOBILE_CENTER_CLASSES = "ks-navbar-title absolute left-1/2 -translate-x-1/2 lg:hidden truncate max-w-[60%]"
11
11
  DESKTOP_LINKS_CLASSES = "nav-container hidden lg:flex"
12
12
  MOBILE_RIGHT_CLASSES = "nav-user-controls ml-auto"
13
13
 
@@ -3,7 +3,7 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class OptionCardComponent < ViewComponent::Base
6
- BASE_CLASSES = "inline-flex items-center gap-2 px-3 py-2 rounded-lg border-2 transition cursor-pointer"
6
+ BASE_CLASSES = "ks-option-card inline-flex items-center transition cursor-pointer"
7
7
 
8
8
  attr_reader :name, :value, :input_data, :label_data
9
9
 
@@ -20,7 +20,7 @@ module Keystone
20
20
  end
21
21
 
22
22
  def classes
23
- border = selected? ? "border-accent-500" : "border-transparent"
23
+ border = selected? ? "ks-option-card-selected" : "border-transparent"
24
24
  "#{BASE_CLASSES} #{border}"
25
25
  end
26
26
  end
@@ -3,8 +3,8 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class PanelComponent < ViewComponent::Base
6
- PADDING_CLASSES = { sm: "p-4", md: "p-5", lg: "p-6" }.freeze
7
- RADIUS_CLASSES = { md: "rounded-lg", lg: "rounded-xl", xl: "rounded-2xl" }.freeze
6
+ PADDING_CLASSES = { sm: "ks-panel-padding-sm", md: "ks-panel-padding-md", lg: "ks-panel-padding-lg" }.freeze
7
+ RADIUS_CLASSES = { md: "ks-panel-radius-md", lg: "ks-panel-radius-lg", xl: "ks-panel-radius-xl" }.freeze
8
8
 
9
9
  def initialize(padding: :md, radius: :lg, shadow: true)
10
10
  @padding = padding
@@ -13,7 +13,7 @@ module Keystone
13
13
  end
14
14
 
15
15
  BASE_CLASSES = "ks-panel"
16
- SHADOW_CLASS = "shadow-sm"
16
+ SHADOW_CLASS = "ks-panel-shadow"
17
17
 
18
18
  def classes
19
19
  tokens = [ RADIUS_CLASSES.fetch(@radius), BASE_CLASSES, PADDING_CLASSES.fetch(@padding) ]
@@ -3,24 +3,24 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class PipelineComponent < ViewComponent::Base
6
- CONTAINER_CLASSES = "rounded-xl border border-surface-700 bg-surface-800 p-6"
7
- HEADER_CLASSES = "mb-4"
8
- TITLE_CLASSES = "text-lg font-semibold text-white"
9
- SUBTITLE_CLASSES = "mt-1 text-sm text-surface-400"
10
- TRACK_CLASSES = "flex flex-col gap-3 sm:flex-row sm:items-stretch"
11
- BOX_CLASSES = "flex flex-1 flex-col items-center gap-2 rounded-lg border border-surface-700 bg-surface-900 p-4 text-center"
12
- BOX_LABEL_CLASSES = "text-xs uppercase tracking-wide text-surface-500"
6
+ CONTAINER_CLASSES = "ks-pipeline"
7
+ HEADER_CLASSES = "ks-pipeline-header"
8
+ TITLE_CLASSES = "ks-pipeline-title text-lg"
9
+ SUBTITLE_CLASSES = "ks-pipeline-subtitle text-sm"
10
+ TRACK_CLASSES = "ks-pipeline-track flex flex-col sm:flex-row sm:items-stretch"
11
+ BOX_CLASSES = "ks-pipeline-box flex flex-1 flex-col items-center text-center"
12
+ BOX_LABEL_CLASSES = "ks-pipeline-box-label text-xs uppercase tracking-wide"
13
13
  CONNECTOR_CLASSES = "flex items-center justify-center"
14
- COUNT_BASE_CLASSES = "text-3xl font-bold"
14
+ COUNT_BASE_CLASSES = "ks-pipeline-count text-3xl"
15
15
  LINK_BASE_CLASSES = "link-toggle text-2xl leading-none"
16
- LINK_HEALTHY_CLASSES = "text-accent-500"
17
- LINK_BROKEN_CLASSES = "text-red-500"
16
+ LINK_HEALTHY_CLASSES = "ks-pipeline-link-healthy"
17
+ LINK_BROKEN_CLASSES = "ks-pipeline-link-broken"
18
18
 
19
19
  COUNT_CLASSES = {
20
- amber: "text-amber-400",
21
- emerald: "text-accent-400",
22
- danger: "text-red-400",
23
- muted: "text-surface-500"
20
+ amber: "ks-pipeline-count-amber",
21
+ emerald: "ks-pipeline-count-emerald",
22
+ danger: "ks-pipeline-count-danger",
23
+ muted: "ks-pipeline-count-muted"
24
24
  }.freeze
25
25
 
26
26
  attr_reader :title, :boxes, :links, :subtitle
@@ -3,9 +3,9 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class ProgressComponent < ViewComponent::Base
6
- TRACK_CLASSES = "w-full h-2 bg-surface-200 rounded-full overflow-hidden dark:bg-surface-700"
7
- BAR_CLASSES = "h-full bg-accent-500 rounded-full transition-all"
8
- LABEL_CLASSES = "mb-1 text-sm font-medium text-surface-700 dark:text-surface-300"
6
+ TRACK_CLASSES = "ks-progress-track w-full h-2 overflow-hidden"
7
+ BAR_CLASSES = "ks-progress-bar h-full transition-all"
8
+ LABEL_CLASSES = "ks-meter-label text-sm"
9
9
 
10
10
  attr_reader :value, :max, :label
11
11
 
@@ -3,10 +3,10 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class RadioCardComponent < ViewComponent::Base
6
- BASE_CLASSES = "inline-flex flex-col px-4 py-3 rounded-lg border-2 cursor-pointer transition"
7
- HIGHLIGHT_CLASSES = "border-gray-200 dark:border-zinc-700 peer-checked:border-accent-500 peer-checked:bg-accent-50 dark:peer-checked:bg-zinc-800"
8
- LABEL_CLASSES = "block font-medium text-gray-900 dark:text-gray-100"
9
- HINT_CLASSES = "block mt-1 text-sm text-surface-500 dark:text-gray-400"
6
+ BASE_CLASSES = "ks-radio-card inline-flex flex-col cursor-pointer transition"
7
+ HIGHLIGHT_CLASSES = "ks-radio-card-highlight"
8
+ LABEL_CLASSES = "ks-radio-card-label block"
9
+ HINT_CLASSES = "ks-radio-card-hint block text-sm"
10
10
 
11
11
  attr_reader :name, :value, :label, :hint
12
12
 
@@ -1,4 +1,4 @@
1
1
  <%= link_to @href, class: link_classes do %>
2
- <span class="font-medium"><%= @label %></span>
2
+ <span class="<%= LABEL_CLASSES %>"><%= @label %></span>
3
3
  <%= CHEVRON_ICON.html_safe %>
4
4
  <% end %>
@@ -3,12 +3,13 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class SettingsLinkComponent < ViewComponent::Base
6
+ LABEL_CLASSES = "ks-settings-link-label"
6
7
  attr_reader :label, :href
7
8
 
8
- LINK_CLASSES = "flex items-center justify-between px-4 py-3 text-gray-900 dark:text-gray-100 no-underline hover:bg-gray-50 dark:hover:bg-gray-800"
9
+ LINK_CLASSES = "ks-settings-link flex items-center justify-between no-underline"
9
10
 
10
11
  CHEVRON_ICON = <<~SVG.freeze
11
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5 text-gray-400">
12
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5 ks-settings-link-chevron">
12
13
  <path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
13
14
  </svg>
14
15
  SVG
@@ -4,8 +4,8 @@ module Keystone
4
4
  module Ui
5
5
  class ShowPageComponent < ViewComponent::Base
6
6
  DESKTOP_WRAPPER_CLASSES = "hidden md:block"
7
- TITLE_CLASSES = "text-2xl font-semibold text-gray-900 dark:text-white"
8
- SUBTITLE_CLASSES = "mt-1 text-sm text-gray-500 dark:text-gray-400"
7
+ TITLE_CLASSES = "ks-page-title text-2xl"
8
+ SUBTITLE_CLASSES = "ks-page-header-subtitle text-sm"
9
9
 
10
10
  def initialize(title:, back_url:, subtitle: nil)
11
11
  @title = title
@@ -1,13 +1,13 @@
1
1
  <div class="<%= classes %>"<%= info? ? ' data-controller="stat-card-info"'.html_safe : "" %>>
2
- <div class="flex items-start justify-between gap-2">
2
+ <div class="<%= HEADER_CLASSES %>">
3
3
  <p class="<%= label_classes %>"><%= label %></p>
4
4
  <% if info? %>
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
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 %>
9
+ <% if definition %><p><span class="<%= EMPHASIS_CLASSES %>">Captures:</span> <%= definition %></p><% end %>
10
+ <% if calculation %><p><span class="<%= EMPHASIS_CLASSES %>">Calc:</span> <%= calculation %></p><% end %>
11
11
  </div>
12
12
  <% end %>
13
13
  </div>
@@ -3,25 +3,27 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class StatCardComponent < ViewComponent::Base
6
- CARD_CLASSES = "relative rounded-xl border border-gray-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
7
- LABEL_CLASSES = "text-sm font-medium text-gray-500 dark:text-gray-400"
8
- VALUE_BASE_CLASSES = "mt-1 text-3xl font-bold"
9
- SUFFIX_CLASSES = "text-lg text-gray-500 dark:text-gray-400"
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
- CHANGE_ROW_CLASSES = "mt-1 h-5 text-sm font-medium"
6
+ EMPHASIS_CLASSES = "ks-stat-card-emphasis"
7
+ HEADER_CLASSES = "ks-stat-card-header flex items-start justify-between"
8
+ CARD_CLASSES = "ks-metric-card relative"
9
+ LABEL_CLASSES = "ks-stat-card-label text-sm"
10
+ VALUE_BASE_CLASSES = "ks-stat-card-value text-3xl"
11
+ SUFFIX_CLASSES = "ks-stat-card-suffix text-lg"
12
+ DISCLOSURE_CLASSES = "ks-stat-card-disclosure hidden peer-hover:block peer-focus-visible:block absolute inset-x-0 top-full z-10 text-sm"
13
+ CHANGE_ROW_CLASSES = "ks-stat-card-change h-5 text-sm"
12
14
  VALUE_LINK_CLASSES = "hover:underline focus:outline-none focus-visible:underline"
13
- INFO_BUTTON_CLASSES = "peer shrink-0 text-gray-400 transition hover:text-accent-600 dark:hover:text-accent-400"
15
+ INFO_BUTTON_CLASSES = "ks-stat-card-info peer shrink-0 transition"
14
16
 
15
17
  INFO_ICON = <<~SVG.freeze
16
18
  <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>
17
19
  SVG
18
20
 
19
21
  VARIANT_CLASSES = {
20
- neutral: "text-gray-900 dark:text-white",
21
- success: "text-green-600 dark:text-green-400",
22
- danger: "text-red-600 dark:text-red-400",
23
- warning: "text-yellow-600 dark:text-yellow-400",
24
- info: "text-accent-600 dark:text-accent-400"
22
+ neutral: "ks-tone-neutral",
23
+ success: "ks-tone-success",
24
+ danger: "ks-tone-danger",
25
+ warning: "ks-tone-warning",
26
+ info: "ks-tone-info"
25
27
  }.freeze
26
28
 
27
29
  attr_reader :label, :value, :suffix, :definition, :calculation, :change, :href
@@ -87,10 +89,10 @@ module Keystone
87
89
  end
88
90
 
89
91
  def change_classes
90
- return "text-gray-500 dark:text-gray-400" if !change? || @change.zero?
91
- return "text-red-600 dark:text-red-400" if @change.negative?
92
+ return "ks-tone-muted" if !change? || @change.zero?
93
+ return "ks-tone-danger" if @change.negative?
92
94
 
93
- "text-green-600 dark:text-green-400"
95
+ "ks-tone-success"
94
96
  end
95
97
 
96
98
  def disclosure_classes
@@ -2,10 +2,10 @@
2
2
  data-swipe-deck-empty-title-value="<%= @empty_title %>"
3
3
  data-swipe-deck-empty-subtitle-value="<%= @empty_subtitle %>">
4
4
  <% if empty? %>
5
- <div class="text-center py-16" data-testid="swipe-empty">
6
- <p class="text-2xl font-semibold text-gray-900 dark:text-white"><%= @empty_title %></p>
5
+ <div class="<%= EMPTY_CLASSES %>" data-testid="swipe-empty">
6
+ <p class="<%= EMPTY_TITLE_CLASSES %>"><%= @empty_title %></p>
7
7
  <% if @empty_subtitle %>
8
- <p class="mt-2 text-gray-500 dark:text-gray-400"><%= @empty_subtitle %></p>
8
+ <p class="<%= EMPTY_MESSAGE_CLASSES %>"><%= @empty_subtitle %></p>
9
9
  <% end %>
10
10
  </div>
11
11
  <% else %>
@@ -21,15 +21,15 @@
21
21
  <% end %>
22
22
  </div>
23
23
 
24
- <div class="flex justify-center gap-8 mt-8">
24
+ <div class="<%= ACTIONS_CLASSES %>">
25
25
  <button data-action="swipe-deck#skip" data-testid="skip-button"
26
- class="flex items-center justify-center size-14 rounded-full border-2 border-gray-300 dark:border-zinc-600 text-gray-400 hover:border-red-400 hover:text-red-400 transition-colors">
26
+ class="<%= REJECT_BUTTON_CLASSES %>">
27
27
  <svg class="size-6" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
28
28
  <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
29
29
  </svg>
30
30
  </button>
31
31
  <button data-action="swipe-deck#complete" data-testid="complete-button"
32
- class="flex items-center justify-center size-14 rounded-full border-2 border-gray-300 dark:border-zinc-600 text-gray-400 hover:border-green-400 hover:text-green-400 transition-colors">
32
+ class="<%= ACCEPT_BUTTON_CLASSES %>">
33
33
  <svg class="size-6" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
34
34
  <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
35
35
  </svg>
@@ -3,7 +3,13 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class SwipeDeckComponent < ViewComponent::Base
6
- CARD_CLASSES = "absolute inset-0 rounded-2xl border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 shadow-lg p-6 flex flex-col items-center justify-center transition-transform duration-300"
6
+ ACCEPT_BUTTON_CLASSES = "ks-swipe-button ks-swipe-button-accept flex items-center justify-center size-14 transition-colors"
7
+ REJECT_BUTTON_CLASSES = "ks-swipe-button ks-swipe-button-reject flex items-center justify-center size-14 transition-colors"
8
+ ACTIONS_CLASSES = "ks-swipe-actions flex justify-center"
9
+ EMPTY_MESSAGE_CLASSES = "ks-swipe-empty-message"
10
+ EMPTY_TITLE_CLASSES = "ks-swipe-empty-title text-2xl"
11
+ EMPTY_CLASSES = "ks-swipe-empty text-center"
12
+ CARD_CLASSES = "ks-swipe-card absolute inset-0 flex flex-col items-center justify-center transition-transform duration-300"
7
13
 
8
14
  STACK_SCALE_STEP = 0.05
9
15
  STACK_TRANSLATE_STEP = 8
@@ -3,8 +3,8 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class TabSwitcherComponent < ViewComponent::Base
6
- TAB_BAR_CLASSES = "mb-8 flex flex-wrap justify-center gap-2"
7
- TAB_BASE_CLASSES = "rounded-lg px-4 py-2 text-sm font-semibold transition text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
6
+ TAB_BAR_CLASSES = "ks-tab-bar flex flex-wrap justify-center"
7
+ TAB_BASE_CLASSES = "ks-tab text-sm transition"
8
8
  PANEL_CLASSES = "hidden"
9
9
 
10
10
  attr_reader :tabs
@@ -18,7 +18,7 @@ module Keystone
18
18
  end
19
19
 
20
20
  def tab_classes
21
- "#{TAB_BASE_CLASSES} data-[active]:bg-accent-500/10 data-[active]:text-accent-600 dark:data-[active]:bg-accent-500/10 dark:data-[active]:text-accent-400"
21
+ "#{TAB_BASE_CLASSES} ks-tab-active"
22
22
  end
23
23
 
24
24
  def panel_classes
@@ -4,8 +4,8 @@ module Keystone
4
4
  module Ui
5
5
  class ThemeToggleComponent < ViewComponent::Base
6
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"
7
+ GROUP_CLASSES = "ks-theme-toggle inline-flex"
8
+ OPTION_CLASSES = "ks-theme-toggle-option ks-button ks-button-sm ks-button-secondary"
9
9
 
10
10
  def initialize(current: nil)
11
11
  @current = current
@@ -46,7 +46,7 @@ module KeystoneUiHelper
46
46
  end
47
47
 
48
48
  def keystone_theme_attributes
49
- keystone_theme_choice.html_attributes_markup.html_safe
49
+ keystone_theme_choice.html_attributes.merge(keystone_look_choice.html_attributes).map { |name, value| %(#{name}="#{value}") }.join(" ").html_safe
50
50
  end
51
51
 
52
52
  def ui_copy_button(**args)
@@ -218,4 +218,9 @@ module KeystoneUiHelper
218
218
  def keystone_theme_choice
219
219
  KeystoneUi::ThemeChoice.new(cookies[KeystoneUi::ThemeChoice::COOKIE], supplied: KeystoneUi.configuration.supplied_theme_mode(self))
220
220
  end
221
+
222
+ def keystone_look_choice
223
+ config = KeystoneUi.configuration
224
+ KeystoneUi::LookChoice.new(looks: config.looks.keys, default: config.default_look, supplied: config.supplied_look(self))
225
+ end
221
226
  end
@@ -2,14 +2,28 @@
2
2
 
3
3
  module KeystoneUi
4
4
  class Configuration
5
- attr_accessor :accent, :surface, :theme_mode_supplier
6
- attr_reader :tailwind_imports, :tailwind_sources
5
+ attr_accessor :accent, :surface, :theme_mode_supplier, :look_supplier
6
+ attr_reader :tailwind_imports, :tailwind_sources, :looks, :default_look
7
7
 
8
8
  def initialize
9
9
  @accent = :blue
10
10
  @surface = :zinc
11
11
  @tailwind_imports = []
12
12
  @tailwind_sources = []
13
+ @looks = {}
14
+ end
15
+
16
+ def default_look=(name)
17
+ @default_look = name&.to_s
18
+ end
19
+
20
+ def register_look(name, path)
21
+ @looks[name.to_s] = path.to_s
22
+ @tailwind_imports << path.to_s
23
+ end
24
+
25
+ def supplied_look(view)
26
+ look_supplier&.call(view)&.to_s
13
27
  end
14
28
 
15
29
  def supplied_theme_mode(view)
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ class LookChoice
5
+ def initialize(looks:, default:, supplied: nil)
6
+ @looks = looks
7
+ @default = default
8
+ @supplied = supplied
9
+ end
10
+
11
+ def name
12
+ [ @supplied, @default ].find { |candidate| @looks.include?(candidate) }
13
+ end
14
+
15
+ def html_attributes
16
+ name ? { "data-look" => name } : {}
17
+ end
18
+ end
19
+ end
@@ -21,7 +21,6 @@ module KeystoneUi
21
21
  [
22
22
  %(@import "#{KeystoneUi::Styles.tailwind_file}";),
23
23
  %(@source "#{@root}/app/components/**/*.{erb,rb}";),
24
- %(@import "#{@root}/app/assets/tailwind/keystone_ui_engine/nav.css";),
25
24
  %(@import "#{@root}/app/assets/tailwind/keystone_ui_engine/color_picker.css";),
26
25
  %(@import "#{@root}/app/assets/tailwind/keystone_ui_engine/grid_safelist.css";),
27
26
  *@imports.map { |import| %(@import "#{import}";) },
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KeystoneUi
4
- VERSION = "0.16.0"
4
+ VERSION = "0.18.0"
5
5
  end
data/lib/keystone_ui.rb CHANGED
@@ -4,6 +4,7 @@ require "view_component"
4
4
  require "keystone_ui/styles"
5
5
  require "keystone_ui/configuration"
6
6
  require "keystone_ui/theme_choice"
7
+ require "keystone_ui/look_choice"
7
8
  require "keystone_ui/engine"
8
9
  require "keystone_ui/version"
9
10
 
@@ -74,6 +74,25 @@ holds the catalog.
74
74
  Tailwind's stock `gray-*` and `zinc-*` neutrals, or fixed status colors such
75
75
  as green and amber, directly. Changing the defaults belongs to the install
76
76
  local.
77
+ - **Looks.** A look is one CSS file that sets the `--ks-` variables from
78
+ keystone_ui-styles 0.6.0 or later: corner radius, font, label weight, border
79
+ width, padding and colours. Each colour variable has a `-dark` partner read
80
+ on a dark page. Buttons, panels, cards, alerts, badges, form fields, the
81
+ modal, the mobile action menu, the column picker, multi select, copy button,
82
+ theme toggle, checkbox row, radio card, option card, file upload and colour
83
+ picker read these variables. So do the data display components: stat card,
84
+ chart card, card link, CTA banner, feature grid, hero, data table, code,
85
+ accordion, disclosure, tab switcher, progress, funnel, bucket, pipeline and
86
+ swipe deck. So do the navigation components: navbar, nav item, nav dropdown,
87
+ bottom nav, mobile header and settings link. A host imports its look after `keystone_source.css`, and a gem ships one through
88
+ `tailwind_imports`, both set up through the install local.
89
+ - **Registered looks.** Looks can also be registered by name, and each page
90
+ gets one: the name another gem supplies for the request, then the configured
91
+ default, each used only when registered. The layout's `<html>` tag carries
92
+ `data-look="<name>"`, and a registered look file scopes its variables to
93
+ `:root[data-look="<name>"]`. A host with no registered looks gets no
94
+ `data-look`. Registering and choosing looks is set up through the install
95
+ local.
77
96
  - **Light, dark, system and custom themes.** Every component has dark-mode
78
97
  styling. The theme is chosen in this order: the user's choice stored in a
79
98
  cookie, then a mode another gem supplies, then light. System leaves the page
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: keystone_ui-install
3
- description: Use to hook Keystone UI into a project — adding the gem, running the install generator to wire Tailwind, the Stimulus controllers and the layout's theme attributes, and configuring the palette, the theme mode supplier and extra Tailwind imports and sources.
3
+ description: Use to hook Keystone UI into a project — adding the gem, running the install generator to wire Tailwind, the Stimulus controllers and the layout's theme attributes, and configuring the palette, the theme mode supplier, registered looks and extra Tailwind imports and sources.
4
4
  tools: Bash, Read, Edit
5
5
  scope: UI — pages, forms, tables, navigation, dashboards
6
6
  ---
@@ -22,18 +22,23 @@ built on ViewComponent; hook it in before building any screen with those helpers
22
22
  `surface` (palette names, `:blue` and `:zinc` unless set),
23
23
  `theme_mode_supplier` (a callable that supplies a light, dark, system or
24
24
  custom mode),
25
+ `register_look` (a look's CSS file by name), `default_look` and
26
+ `look_supplier` (which registered look a page gets),
25
27
  and the `tailwind_imports` and `tailwind_sources` lists (extra CSS files and
26
28
  scan paths added to the Tailwind build).
27
29
  - `keystone_theme_attributes` — a layout helper placed inside the `<html>` tag.
28
30
  It writes `data-theme="light"`, `data-theme="dark"` or `data-theme="custom"`
29
31
  for the page's mode, and writes nothing for system so the page follows the
30
32
  operating system. The mode is the `keystone_theme` cookie's choice, then the
31
- configured `theme_mode_supplier`, then light.
33
+ configured `theme_mode_supplier`, then light. It also writes
34
+ `data-look="<name>"` for the page's look: the `look_supplier`'s name, then
35
+ `default_look`, each used only when registered. With no look registered it
36
+ writes no `data-look`.
32
37
 
33
38
  ## How to use it
34
39
 
35
40
  1. Confirm the prerequisites: Ruby >= 3.2 and **tailwindcss-rails v4+** in the
36
- host app. The gem brings ViewComponent and keystone_ui-styles with it.
41
+ host app. The gem brings ViewComponent and keystone_ui-styles 0.6.0 or later with it.
37
42
  Tailwind does not have to be initialized first, because the generator creates
38
43
  the stylesheet if it is missing.
39
44
 
@@ -128,6 +133,32 @@ built on ViewComponent; hook it in before building any screen with those helpers
128
133
  or per-user colors generated at runtime by a companion theming gem. Ask which
129
134
  the app wants before wiring either.
130
135
 
136
+ To change how components look beyond the palette, write a look file: one CSS
137
+ file holding a `:root` rule, outside any `@layer` block, that sets the
138
+ `--ks-` variables from keystone_ui-styles for corner radius, font, label
139
+ weight, border width, padding and colours. Set each colour's `-dark`
140
+ partner too, or dark pages keep the default colour. Import it after
141
+ Keystone:
142
+
143
+ ```css
144
+ @import "tailwindcss";
145
+ @import "./keystone_source.css";
146
+ @import "./look.css";
147
+ ```
148
+
149
+ Buttons, panels, cards, alerts, badges, form fields, the modal, the mobile
150
+ action menu, the column picker, multi select, copy button, theme toggle,
151
+ checkbox row, radio card, option card, file upload and colour picker read
152
+ these variables. So do the data display components: stat card, chart card,
153
+ card link, CTA banner, feature grid, hero, data table, code, accordion,
154
+ disclosure, tab switcher, progress, funnel, bucket, pipeline and swipe deck.
155
+ So do the navigation components: navbar, nav item, nav dropdown, bottom nav,
156
+ mobile header and settings link. The keystone_ui-styles README lists every
157
+ variable and its default.
158
+
159
+ To offer several looks and choose one per page, register them by name in
160
+ step 7 rather than importing them here.
161
+
131
162
  7. Write `config/initializers/keystone_ui.rb` only if one of the settings below
132
163
  is wanted. Ask the developer about each rather than adding any by default.
133
164
  The engine reads the configuration after initializers have run, so this file
@@ -140,6 +171,9 @@ built on ViewComponent; hook it in before building any screen with those helpers
140
171
  config.accent = :emerald
141
172
  config.surface = :slate
142
173
  config.theme_mode_supplier = ->(view) { view.current_user&.theme }
174
+ config.register_look :compact, "/absolute/path/to/compact.css"
175
+ config.default_look = :compact
176
+ config.look_supplier = ->(view) { view.current_user&.look }
143
177
  config.tailwind_imports << "/absolute/path/to/extra.css"
144
178
  config.tailwind_sources << "/absolute/path/to/components/**/*.{erb,rb}"
145
179
  end
@@ -157,11 +191,21 @@ built on ViewComponent; hook it in before building any screen with those helpers
157
191
  `data-theme="custom"`. The theme toggle does not offer custom, so only a
158
192
  supplier sets it. Ask the developer which gem or code supplies custom and
159
193
  its colors before returning it.
194
+ - `register_look :name, path` — registers a look file by name and appends it
195
+ to `tailwind_imports`, so `keystone_source.css` imports it. A look offered
196
+ this way scopes its variables to `:root[data-look="<name>"]` instead of
197
+ `:root`, so several can be imported at once.
198
+ - `default_look = :name` — the look a page gets when nothing else chooses one.
199
+ - `look_supplier` — a callable that receives the view and returns a look
200
+ name for the request. A name that is not registered, or `nil`, leaves the
201
+ page on the default look.
160
202
  - `tailwind_imports` and `tailwind_sources` — lists to append to, never
161
203
  assign. Each import becomes an `@import` line and each source becomes an
162
204
  `@source` line in `keystone_source.css` on the next boot. They are for
163
205
  another gem or engine whose CSS or templates must be in the same Tailwind
164
- build, and that gem normally appends its own entries.
206
+ build, and that gem normally appends its own entries. A gem that ships a
207
+ look appends its look file to `tailwind_imports` from its own initializer,
208
+ and `keystone_source.css` imports it after keystone_ui-styles.
165
209
 
166
210
  ## Conventions
167
211