keystone_ui 0.6.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 (131) hide show
  1. checksums.yaml +7 -0
  2. data/app/assets/javascripts/keystone_ui/accordion_controller.js +21 -0
  3. data/app/assets/javascripts/keystone_ui/auto_submit_controller.js +7 -0
  4. data/app/assets/javascripts/keystone_ui/chartjs.js +24 -0
  5. data/app/assets/javascripts/keystone_ui/clipboard_controller.js +25 -0
  6. data/app/assets/javascripts/keystone_ui/color_picker_controller.js +158 -0
  7. data/app/assets/javascripts/keystone_ui/column_picker_controller.js +47 -0
  8. data/app/assets/javascripts/keystone_ui/dismiss_controller.js +7 -0
  9. data/app/assets/javascripts/keystone_ui/dropdown_controller.js +16 -0
  10. data/app/assets/javascripts/keystone_ui/file_upload_controller.js +61 -0
  11. data/app/assets/javascripts/keystone_ui/index.js +33 -0
  12. data/app/assets/javascripts/keystone_ui/line_chart_controller.js +38 -0
  13. data/app/assets/javascripts/keystone_ui/modal_controller.js +19 -0
  14. data/app/assets/javascripts/keystone_ui/multi_select_controller.js +35 -0
  15. data/app/assets/javascripts/keystone_ui/stat_card_info_controller.js +9 -0
  16. data/app/assets/javascripts/keystone_ui/swipe_deck_controller.js +123 -0
  17. data/app/assets/javascripts/keystone_ui/tab_switcher_controller.js +19 -0
  18. data/app/assets/tailwind/keystone_ui_engine/color_picker.css +31 -0
  19. data/app/assets/tailwind/keystone_ui_engine/engine.css +7 -0
  20. data/app/assets/tailwind/keystone_ui_engine/nav.css +175 -0
  21. data/app/assets/tailwind/keystone_ui_engine/theme.css +32 -0
  22. data/app/components/keystone/ui/accordion_component.html.erb +19 -0
  23. data/app/components/keystone/ui/accordion_component.rb +51 -0
  24. data/app/components/keystone/ui/alert_component.html.erb +15 -0
  25. data/app/components/keystone/ui/alert_component.rb +58 -0
  26. data/app/components/keystone/ui/badge_component.html.erb +1 -0
  27. data/app/components/keystone/ui/badge_component.rb +28 -0
  28. data/app/components/keystone/ui/bottom_nav_component.html.erb +3 -0
  29. data/app/components/keystone/ui/bottom_nav_component.rb +13 -0
  30. data/app/components/keystone/ui/bottom_nav_item_component.html.erb +4 -0
  31. data/app/components/keystone/ui/bottom_nav_item_component.rb +24 -0
  32. data/app/components/keystone/ui/button_component.html.erb +1 -0
  33. data/app/components/keystone/ui/button_component.rb +57 -0
  34. data/app/components/keystone/ui/card_component.html.erb +9 -0
  35. data/app/components/keystone/ui/card_component.rb +33 -0
  36. data/app/components/keystone/ui/card_link_component.html.erb +3 -0
  37. data/app/components/keystone/ui/card_link_component.rb +25 -0
  38. data/app/components/keystone/ui/chart_card_component.html.erb +6 -0
  39. data/app/components/keystone/ui/chart_card_component.rb +35 -0
  40. data/app/components/keystone/ui/code_component.html.erb +6 -0
  41. data/app/components/keystone/ui/code_component.rb +40 -0
  42. data/app/components/keystone/ui/color_picker_component.html.erb +37 -0
  43. data/app/components/keystone/ui/color_picker_component.rb +31 -0
  44. data/app/components/keystone/ui/column.rb +21 -0
  45. data/app/components/keystone/ui/column_picker_component.html.erb +20 -0
  46. data/app/components/keystone/ui/column_picker_component.rb +35 -0
  47. data/app/components/keystone/ui/copy_button_component.html.erb +11 -0
  48. data/app/components/keystone/ui/copy_button_component.rb +39 -0
  49. data/app/components/keystone/ui/cta_banner_component.html.erb +9 -0
  50. data/app/components/keystone/ui/cta_banner_component.rb +39 -0
  51. data/app/components/keystone/ui/data_table_component.html.erb +54 -0
  52. data/app/components/keystone/ui/data_table_component.rb +173 -0
  53. data/app/components/keystone/ui/disclosure_component.html.erb +9 -0
  54. data/app/components/keystone/ui/disclosure_component.rb +46 -0
  55. data/app/components/keystone/ui/feature_grid_component.html.erb +17 -0
  56. data/app/components/keystone/ui/feature_grid_component.rb +55 -0
  57. data/app/components/keystone/ui/file_upload_component.html.erb +18 -0
  58. data/app/components/keystone/ui/file_upload_component.rb +85 -0
  59. data/app/components/keystone/ui/form_component.html.erb +3 -0
  60. data/app/components/keystone/ui/form_component.rb +27 -0
  61. data/app/components/keystone/ui/form_field_component.html.erb +36 -0
  62. data/app/components/keystone/ui/form_field_component.rb +83 -0
  63. data/app/components/keystone/ui/form_page_component.html.erb +10 -0
  64. data/app/components/keystone/ui/form_page_component.rb +21 -0
  65. data/app/components/keystone/ui/funnel_component.html.erb +14 -0
  66. data/app/components/keystone/ui/funnel_component.rb +85 -0
  67. data/app/components/keystone/ui/grid_component.html.erb +3 -0
  68. data/app/components/keystone/ui/grid_component.rb +38 -0
  69. data/app/components/keystone/ui/hero_component.html.erb +27 -0
  70. data/app/components/keystone/ui/hero_component.rb +74 -0
  71. data/app/components/keystone/ui/input_component.html.erb +1 -0
  72. data/app/components/keystone/ui/input_component.rb +57 -0
  73. data/app/components/keystone/ui/line_chart_component.html.erb +3 -0
  74. data/app/components/keystone/ui/line_chart_component.rb +48 -0
  75. data/app/components/keystone/ui/mobile_actions_component.html.erb +11 -0
  76. data/app/components/keystone/ui/mobile_actions_component.rb +21 -0
  77. data/app/components/keystone/ui/mobile_header_component.html.erb +11 -0
  78. data/app/components/keystone/ui/mobile_header_component.rb +38 -0
  79. data/app/components/keystone/ui/modal_component.html.erb +13 -0
  80. data/app/components/keystone/ui/modal_component.rb +68 -0
  81. data/app/components/keystone/ui/multi_select_component.html.erb +21 -0
  82. data/app/components/keystone/ui/multi_select_component.rb +40 -0
  83. data/app/components/keystone/ui/nav_dropdown_component.html.erb +11 -0
  84. data/app/components/keystone/ui/nav_dropdown_component.rb +33 -0
  85. data/app/components/keystone/ui/nav_item_component.html.erb +1 -0
  86. data/app/components/keystone/ui/nav_item_component.rb +21 -0
  87. data/app/components/keystone/ui/navbar_component.html.erb +34 -0
  88. data/app/components/keystone/ui/navbar_component.rb +32 -0
  89. data/app/components/keystone/ui/option_card_component.html.erb +9 -0
  90. data/app/components/keystone/ui/option_card_component.rb +28 -0
  91. data/app/components/keystone/ui/page_component.html.erb +3 -0
  92. data/app/components/keystone/ui/page_component.rb +40 -0
  93. data/app/components/keystone/ui/page_header_component.html.erb +17 -0
  94. data/app/components/keystone/ui/page_header_component.rb +42 -0
  95. data/app/components/keystone/ui/panel_component.html.erb +3 -0
  96. data/app/components/keystone/ui/panel_component.rb +25 -0
  97. data/app/components/keystone/ui/pipeline_component.html.erb +34 -0
  98. data/app/components/keystone/ui/pipeline_component.rb +48 -0
  99. data/app/components/keystone/ui/progress_component.html.erb +8 -0
  100. data/app/components/keystone/ui/progress_component.rb +35 -0
  101. data/app/components/keystone/ui/radio_card_component.html.erb +9 -0
  102. data/app/components/keystone/ui/radio_card_component.rb +42 -0
  103. data/app/components/keystone/ui/section_component.html.erb +18 -0
  104. data/app/components/keystone/ui/section_component.rb +32 -0
  105. data/app/components/keystone/ui/select_component.html.erb +8 -0
  106. data/app/components/keystone/ui/select_component.rb +38 -0
  107. data/app/components/keystone/ui/settings_link_component.html.erb +4 -0
  108. data/app/components/keystone/ui/settings_link_component.rb +26 -0
  109. data/app/components/keystone/ui/show_page_component.html.erb +4 -0
  110. data/app/components/keystone/ui/show_page_component.rb +21 -0
  111. data/app/components/keystone/ui/stat_card_component.html.erb +22 -0
  112. data/app/components/keystone/ui/stat_card_component.rb +92 -0
  113. data/app/components/keystone/ui/swipe_deck_component.html.erb +39 -0
  114. data/app/components/keystone/ui/swipe_deck_component.rb +52 -0
  115. data/app/components/keystone/ui/tab_switcher_component.html.erb +14 -0
  116. data/app/components/keystone/ui/tab_switcher_component.rb +33 -0
  117. data/app/components/keystone/ui/textarea_component.html.erb +1 -0
  118. data/app/components/keystone/ui/textarea_component.rb +38 -0
  119. data/app/helpers/keystone_ui_helper.rb +195 -0
  120. data/config/importmap.rb +19 -0
  121. data/lib/generators/keystone/install_generator.rb +90 -0
  122. data/lib/keystone_ui/configuration.rb +24 -0
  123. data/lib/keystone_ui/engine.rb +47 -0
  124. data/lib/keystone_ui/safelist.rb +106 -0
  125. data/lib/keystone_ui/version.rb +5 -0
  126. data/lib/keystone_ui.rb +9 -0
  127. data/the_local/agents/keystone_ui-develop.md +297 -0
  128. data/the_local/agents/keystone_ui-info.md +81 -0
  129. data/the_local/agents/keystone_ui-install.md +137 -0
  130. data/the_local/interface.yml +110 -0
  131. metadata +203 -0
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class RadioCardComponent < ViewComponent::Base
6
+ BASE_CLASSES = "block p-4 rounded-lg border-2 cursor-pointer transition"
7
+ HIGHLIGHT_CLASSES = "border-surface-200 peer-checked:border-accent-500 peer-checked:bg-accent-50"
8
+ LABEL_CLASSES = "block font-medium text-surface-900"
9
+ HINT_CLASSES = "block mt-1 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 classes
30
+ "#{BASE_CLASSES} #{HIGHLIGHT_CLASSES}"
31
+ end
32
+
33
+ def label_classes
34
+ LABEL_CLASSES
35
+ end
36
+
37
+ def hint_classes
38
+ HINT_CLASSES
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,18 @@
1
+ <div class="<%= spacing_class %>">
2
+ <% if header? %>
3
+ <div class="<%= HEADER_CLASSES %>">
4
+ <div>
5
+ <h2 class="<%= TITLE_CLASSES %>"><%= @title %></h2>
6
+ <% if @subtitle %>
7
+ <p class="<%= SUBTITLE_CLASSES %>"><%= @subtitle %></p>
8
+ <% end %>
9
+ </div>
10
+ <% if @action %>
11
+ <a href="<%= @action[:href] %>" class="<%= action_classes %>">
12
+ <%= @action[:label] %>
13
+ </a>
14
+ <% end %>
15
+ </div>
16
+ <% end %>
17
+ <%= content %>
18
+ </div>
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class SectionComponent < ViewComponent::Base
6
+ SPACING_CLASSES = { sm: "mt-4", md: "mt-6", lg: "mt-8" }.freeze
7
+ HEADER_CLASSES = "flex items-center justify-between mb-4"
8
+ TITLE_CLASSES = "text-lg font-semibold text-gray-900 dark:text-white"
9
+ SUBTITLE_CLASSES = "mt-1 text-sm text-gray-500 dark:text-gray-400"
10
+ ACTION_BASE_CLASSES = "text-sm"
11
+
12
+ def initialize(title: nil, subtitle: nil, action: nil, spacing: :md)
13
+ @title = title
14
+ @subtitle = subtitle
15
+ @action = action
16
+ @spacing = spacing
17
+ end
18
+
19
+ def spacing_class
20
+ SPACING_CLASSES.fetch(@spacing)
21
+ end
22
+
23
+ def action_classes
24
+ "#{ACTION_BASE_CLASSES} text-accent-600 hover:text-accent-900 dark:text-accent-400 dark:hover:text-accent-300"
25
+ end
26
+
27
+ def header?
28
+ !@title.nil?
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,8 @@
1
+ <%= tag.select(**tag_options) do %>
2
+ <% if include_blank %>
3
+ <option value=""><%= include_blank %></option>
4
+ <% end %>
5
+ <% options.each do |label, value| %>
6
+ <option value="<%= value %>" <%= "selected" if selected.to_s == value.to_s %>><%= label %></option>
7
+ <% end %>
8
+ <% end %>
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
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"
7
+
8
+ DISABLED_CLASSES = "cursor-not-allowed bg-gray-50 text-gray-500 dark:bg-zinc-800 dark:text-gray-400"
9
+
10
+ attr_reader :options, :selected, :include_blank
11
+
12
+ def initialize(name:, options: [], selected: nil, include_blank: nil, disabled: false)
13
+ @name = name
14
+ @options = options
15
+ @selected = selected
16
+ @include_blank = include_blank
17
+ @disabled = disabled
18
+ end
19
+
20
+ FOCUS_CLASSES = "focus:border-accent-500 focus:ring-accent-500 dark:focus:border-accent-400 dark:focus:ring-accent-400"
21
+
22
+ def classes
23
+ tokens = [ BASE_CLASSES, FOCUS_CLASSES ]
24
+ tokens << DISABLED_CLASSES if @disabled
25
+ tokens.join(" ")
26
+ end
27
+
28
+ def tag_options
29
+ opts = {
30
+ name: @name,
31
+ class: classes
32
+ }
33
+ opts[:disabled] = true if @disabled
34
+ opts
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,4 @@
1
+ <%= link_to @href, class: link_classes do %>
2
+ <span class="font-medium"><%= @label %></span>
3
+ <%= CHEVRON_ICON.html_safe %>
4
+ <% end %>
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class SettingsLinkComponent < ViewComponent::Base
6
+ attr_reader :label, :href
7
+
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
+
10
+ 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
+ <path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
13
+ </svg>
14
+ SVG
15
+
16
+ def initialize(label:, href:)
17
+ @label = label
18
+ @href = href
19
+ end
20
+
21
+ def link_classes
22
+ LINK_CLASSES
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,4 @@
1
+ <% content_for :show_page, true %>
2
+ <% content_for :show_page_title, @title %>
3
+ <% content_for :show_page_back_url, @back_url %>
4
+ <% content_for :show_page_subtitle, @subtitle if @subtitle.present? %>
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class ShowPageComponent < ViewComponent::Base
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"
9
+
10
+ def initialize(title:, back_url:, subtitle: nil)
11
+ @title = title
12
+ @back_url = back_url
13
+ @subtitle = subtitle
14
+ end
15
+
16
+ def subtitle?
17
+ !@subtitle.nil?
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ <div class="<%= classes %>"<%= info? ? ' data-controller="stat-card-info"'.html_safe : "" %>>
2
+ <div class="flex items-start justify-between gap-2">
3
+ <p class="<%= label_classes %>"><%= label %></p>
4
+ <% if info? %>
5
+ <button type="button" class="<%= info_button_classes %>" data-action="click->stat-card-info#toggle" aria-label="Show metric details">
6
+ <%= info_icon.html_safe %>
7
+ </button>
8
+ <% end %>
9
+ </div>
10
+ <p class="<%= value_classes %>">
11
+ <%= value %><% if suffix? %><span class="<%= suffix_classes %>"> <%= suffix %></span><% end %>
12
+ </p>
13
+ <% if change? %>
14
+ <p class="mt-1 text-sm font-medium <%= change_classes %>"><%= change_label %></p>
15
+ <% 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
+ </div>
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
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"
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 mt-4 space-y-1 border-t border-gray-200 pt-3 text-sm text-gray-600 dark:border-zinc-700 dark:text-gray-400"
11
+ INFO_BUTTON_CLASSES = "shrink-0 text-gray-400 transition hover:text-accent-600 dark:hover:text-accent-400"
12
+
13
+ INFO_ICON = <<~SVG.freeze
14
+ <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>
15
+ SVG
16
+
17
+ VARIANT_CLASSES = {
18
+ neutral: "text-gray-900 dark:text-white",
19
+ success: "text-green-600 dark:text-green-400",
20
+ danger: "text-red-600 dark:text-red-400",
21
+ warning: "text-yellow-600 dark:text-yellow-400",
22
+ info: "text-accent-600 dark:text-accent-400"
23
+ }.freeze
24
+
25
+ attr_reader :label, :value, :suffix, :definition, :calculation, :change
26
+
27
+ def initialize(label:, value:, variant: :neutral, suffix: nil, definition: nil, calculation: nil, change: nil)
28
+ @label = label
29
+ @value = value
30
+ @variant = variant
31
+ @suffix = suffix
32
+ @definition = definition
33
+ @calculation = calculation
34
+ @change = change
35
+ end
36
+
37
+ def classes
38
+ CARD_CLASSES
39
+ end
40
+
41
+ def label_classes
42
+ LABEL_CLASSES
43
+ end
44
+
45
+ def value_classes
46
+ "#{VALUE_BASE_CLASSES} #{VARIANT_CLASSES.fetch(@variant)}"
47
+ end
48
+
49
+ def suffix_classes
50
+ SUFFIX_CLASSES
51
+ end
52
+
53
+ def suffix?
54
+ !@suffix.nil?
55
+ end
56
+
57
+ def info?
58
+ !@definition.nil? || !@calculation.nil?
59
+ end
60
+
61
+ def change?
62
+ !@change.nil?
63
+ end
64
+
65
+ def change_label
66
+ formatted = "#{format("%.1f", @change.abs)}%"
67
+ return formatted if @change.zero?
68
+
69
+ "#{@change.negative? ? "▼" : "▲"} #{formatted}"
70
+ end
71
+
72
+ def change_classes
73
+ return "text-gray-500 dark:text-gray-400" if @change.zero?
74
+ return "text-red-600 dark:text-red-400" if @change.negative?
75
+
76
+ "text-green-600 dark:text-green-400"
77
+ end
78
+
79
+ def disclosure_classes
80
+ DISCLOSURE_CLASSES
81
+ end
82
+
83
+ def info_button_classes
84
+ INFO_BUTTON_CLASSES
85
+ end
86
+
87
+ def info_icon
88
+ INFO_ICON
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,39 @@
1
+ <div data-controller="swipe-deck"
2
+ data-swipe-deck-empty-title-value="<%= @empty_title %>"
3
+ data-swipe-deck-empty-subtitle-value="<%= @empty_subtitle %>">
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>
7
+ <% if @empty_subtitle %>
8
+ <p class="mt-2 text-gray-500 dark:text-gray-400"><%= @empty_subtitle %></p>
9
+ <% end %>
10
+ </div>
11
+ <% else %>
12
+ <div class="relative h-80" data-swipe-deck-target="stack">
13
+ <% card_data.each do |card| %>
14
+ <div class="<%= CARD_CLASSES %>"
15
+ data-swipe-deck-target="card"
16
+ data-item-id="<%= item_id(card[:item], fallback_index: card[:index]) %>"
17
+ data-testid="swipe-card"
18
+ style="z-index: <%= card[:z_index] %>; transform: <%= card[:transform] %>;">
19
+ <%= capture(card[:item], &@item_block) %>
20
+ </div>
21
+ <% end %>
22
+ </div>
23
+
24
+ <div class="flex justify-center gap-8 mt-8">
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">
27
+ <svg class="size-6" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
28
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
29
+ </svg>
30
+ </button>
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">
33
+ <svg class="size-6" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
34
+ <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
35
+ </svg>
36
+ </button>
37
+ </div>
38
+ <% end %>
39
+ </div>
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
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"
7
+
8
+ STACK_SCALE_STEP = 0.05
9
+ STACK_TRANSLATE_STEP = 8
10
+
11
+ def initialize(items:, empty_title: "All done!", empty_subtitle: nil)
12
+ @items = items
13
+ @empty_title = empty_title
14
+ @empty_subtitle = empty_subtitle
15
+ @item_block = nil
16
+ end
17
+
18
+ def item(&block)
19
+ @item_block = block
20
+ end
21
+
22
+ def before_render
23
+ content
24
+ end
25
+
26
+ def card_data
27
+ @items.each_with_index.map do |item, index|
28
+ scale = 1 - (index * STACK_SCALE_STEP)
29
+ translate_y = index * STACK_TRANSLATE_STEP
30
+ {
31
+ item: item,
32
+ index: index,
33
+ z_index: @items.length - index,
34
+ transform: "scale(#{scale}) translateY(#{translate_y}px)"
35
+ }
36
+ end
37
+ end
38
+
39
+ def empty?
40
+ @items.empty?
41
+ end
42
+
43
+ def item_id(item, fallback_index: nil)
44
+ if item.respond_to?(:id)
45
+ item.id
46
+ else
47
+ fallback_index
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,14 @@
1
+ <div data-controller="tab-switcher">
2
+ <div class="<%= classes %>">
3
+ <% tabs.each_with_index do |tab, index| %>
4
+ <button
5
+ type="button"
6
+ class="<%= tab_classes %>"
7
+ data-tab-switcher-target="tab"
8
+ data-action="click->tab-switcher#select"
9
+ data-index="<%= index %>"
10
+ <%= "data-active" if index == 0 %>><%= tab %></button>
11
+ <% end %>
12
+ </div>
13
+ <%= content %>
14
+ </div>
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
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"
8
+ PANEL_CLASSES = "hidden"
9
+
10
+ attr_reader :tabs
11
+
12
+ def initialize(tabs:)
13
+ @tabs = tabs
14
+ end
15
+
16
+ def classes
17
+ TAB_BAR_CLASSES
18
+ end
19
+
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"
22
+ end
23
+
24
+ def panel_classes
25
+ PANEL_CLASSES
26
+ end
27
+
28
+ def wrapper_data
29
+ { controller: "tab-switcher" }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1 @@
1
+ <%= tag.textarea(@value, **tag_options) %>
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
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"
7
+
8
+ DISABLED_CLASSES = "cursor-not-allowed bg-gray-50 text-gray-500 dark:bg-zinc-800 dark:text-gray-400"
9
+
10
+ def initialize(name:, value: nil, rows: 3, placeholder: nil, disabled: false)
11
+ @name = name
12
+ @value = value
13
+ @rows = rows
14
+ @placeholder = placeholder
15
+ @disabled = disabled
16
+ end
17
+
18
+ FOCUS_CLASSES = "focus:border-accent-500 focus:ring-accent-500 dark:focus:border-accent-400 dark:focus:ring-accent-400"
19
+
20
+ def classes
21
+ tokens = [ BASE_CLASSES, FOCUS_CLASSES ]
22
+ tokens << DISABLED_CLASSES if @disabled
23
+ tokens.join(" ")
24
+ end
25
+
26
+ def tag_options
27
+ options = {
28
+ name: @name,
29
+ rows: @rows,
30
+ class: classes
31
+ }
32
+ options[:placeholder] = @placeholder unless @placeholder.nil?
33
+ options[:disabled] = true if @disabled
34
+ options
35
+ end
36
+ end
37
+ end
38
+ end