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.
- checksums.yaml +7 -0
- data/app/assets/javascripts/keystone_ui/accordion_controller.js +21 -0
- data/app/assets/javascripts/keystone_ui/auto_submit_controller.js +7 -0
- data/app/assets/javascripts/keystone_ui/chartjs.js +24 -0
- data/app/assets/javascripts/keystone_ui/clipboard_controller.js +25 -0
- data/app/assets/javascripts/keystone_ui/color_picker_controller.js +158 -0
- data/app/assets/javascripts/keystone_ui/column_picker_controller.js +47 -0
- data/app/assets/javascripts/keystone_ui/dismiss_controller.js +7 -0
- data/app/assets/javascripts/keystone_ui/dropdown_controller.js +16 -0
- data/app/assets/javascripts/keystone_ui/file_upload_controller.js +61 -0
- data/app/assets/javascripts/keystone_ui/index.js +33 -0
- data/app/assets/javascripts/keystone_ui/line_chart_controller.js +38 -0
- data/app/assets/javascripts/keystone_ui/modal_controller.js +19 -0
- data/app/assets/javascripts/keystone_ui/multi_select_controller.js +35 -0
- data/app/assets/javascripts/keystone_ui/stat_card_info_controller.js +9 -0
- data/app/assets/javascripts/keystone_ui/swipe_deck_controller.js +123 -0
- data/app/assets/javascripts/keystone_ui/tab_switcher_controller.js +19 -0
- data/app/assets/tailwind/keystone_ui_engine/color_picker.css +31 -0
- data/app/assets/tailwind/keystone_ui_engine/engine.css +7 -0
- data/app/assets/tailwind/keystone_ui_engine/nav.css +175 -0
- data/app/assets/tailwind/keystone_ui_engine/theme.css +32 -0
- data/app/components/keystone/ui/accordion_component.html.erb +19 -0
- data/app/components/keystone/ui/accordion_component.rb +51 -0
- data/app/components/keystone/ui/alert_component.html.erb +15 -0
- data/app/components/keystone/ui/alert_component.rb +58 -0
- data/app/components/keystone/ui/badge_component.html.erb +1 -0
- data/app/components/keystone/ui/badge_component.rb +28 -0
- data/app/components/keystone/ui/bottom_nav_component.html.erb +3 -0
- data/app/components/keystone/ui/bottom_nav_component.rb +13 -0
- data/app/components/keystone/ui/bottom_nav_item_component.html.erb +4 -0
- data/app/components/keystone/ui/bottom_nav_item_component.rb +24 -0
- data/app/components/keystone/ui/button_component.html.erb +1 -0
- data/app/components/keystone/ui/button_component.rb +57 -0
- data/app/components/keystone/ui/card_component.html.erb +9 -0
- data/app/components/keystone/ui/card_component.rb +33 -0
- data/app/components/keystone/ui/card_link_component.html.erb +3 -0
- data/app/components/keystone/ui/card_link_component.rb +25 -0
- data/app/components/keystone/ui/chart_card_component.html.erb +6 -0
- data/app/components/keystone/ui/chart_card_component.rb +35 -0
- data/app/components/keystone/ui/code_component.html.erb +6 -0
- data/app/components/keystone/ui/code_component.rb +40 -0
- data/app/components/keystone/ui/color_picker_component.html.erb +37 -0
- data/app/components/keystone/ui/color_picker_component.rb +31 -0
- data/app/components/keystone/ui/column.rb +21 -0
- data/app/components/keystone/ui/column_picker_component.html.erb +20 -0
- data/app/components/keystone/ui/column_picker_component.rb +35 -0
- data/app/components/keystone/ui/copy_button_component.html.erb +11 -0
- data/app/components/keystone/ui/copy_button_component.rb +39 -0
- data/app/components/keystone/ui/cta_banner_component.html.erb +9 -0
- data/app/components/keystone/ui/cta_banner_component.rb +39 -0
- data/app/components/keystone/ui/data_table_component.html.erb +54 -0
- data/app/components/keystone/ui/data_table_component.rb +173 -0
- data/app/components/keystone/ui/disclosure_component.html.erb +9 -0
- data/app/components/keystone/ui/disclosure_component.rb +46 -0
- data/app/components/keystone/ui/feature_grid_component.html.erb +17 -0
- data/app/components/keystone/ui/feature_grid_component.rb +55 -0
- data/app/components/keystone/ui/file_upload_component.html.erb +18 -0
- data/app/components/keystone/ui/file_upload_component.rb +85 -0
- data/app/components/keystone/ui/form_component.html.erb +3 -0
- data/app/components/keystone/ui/form_component.rb +27 -0
- data/app/components/keystone/ui/form_field_component.html.erb +36 -0
- data/app/components/keystone/ui/form_field_component.rb +83 -0
- data/app/components/keystone/ui/form_page_component.html.erb +10 -0
- data/app/components/keystone/ui/form_page_component.rb +21 -0
- data/app/components/keystone/ui/funnel_component.html.erb +14 -0
- data/app/components/keystone/ui/funnel_component.rb +85 -0
- data/app/components/keystone/ui/grid_component.html.erb +3 -0
- data/app/components/keystone/ui/grid_component.rb +38 -0
- data/app/components/keystone/ui/hero_component.html.erb +27 -0
- data/app/components/keystone/ui/hero_component.rb +74 -0
- data/app/components/keystone/ui/input_component.html.erb +1 -0
- data/app/components/keystone/ui/input_component.rb +57 -0
- data/app/components/keystone/ui/line_chart_component.html.erb +3 -0
- data/app/components/keystone/ui/line_chart_component.rb +48 -0
- data/app/components/keystone/ui/mobile_actions_component.html.erb +11 -0
- data/app/components/keystone/ui/mobile_actions_component.rb +21 -0
- data/app/components/keystone/ui/mobile_header_component.html.erb +11 -0
- data/app/components/keystone/ui/mobile_header_component.rb +38 -0
- data/app/components/keystone/ui/modal_component.html.erb +13 -0
- data/app/components/keystone/ui/modal_component.rb +68 -0
- data/app/components/keystone/ui/multi_select_component.html.erb +21 -0
- data/app/components/keystone/ui/multi_select_component.rb +40 -0
- data/app/components/keystone/ui/nav_dropdown_component.html.erb +11 -0
- data/app/components/keystone/ui/nav_dropdown_component.rb +33 -0
- data/app/components/keystone/ui/nav_item_component.html.erb +1 -0
- data/app/components/keystone/ui/nav_item_component.rb +21 -0
- data/app/components/keystone/ui/navbar_component.html.erb +34 -0
- data/app/components/keystone/ui/navbar_component.rb +32 -0
- data/app/components/keystone/ui/option_card_component.html.erb +9 -0
- data/app/components/keystone/ui/option_card_component.rb +28 -0
- data/app/components/keystone/ui/page_component.html.erb +3 -0
- data/app/components/keystone/ui/page_component.rb +40 -0
- data/app/components/keystone/ui/page_header_component.html.erb +17 -0
- data/app/components/keystone/ui/page_header_component.rb +42 -0
- data/app/components/keystone/ui/panel_component.html.erb +3 -0
- data/app/components/keystone/ui/panel_component.rb +25 -0
- data/app/components/keystone/ui/pipeline_component.html.erb +34 -0
- data/app/components/keystone/ui/pipeline_component.rb +48 -0
- data/app/components/keystone/ui/progress_component.html.erb +8 -0
- data/app/components/keystone/ui/progress_component.rb +35 -0
- data/app/components/keystone/ui/radio_card_component.html.erb +9 -0
- data/app/components/keystone/ui/radio_card_component.rb +42 -0
- data/app/components/keystone/ui/section_component.html.erb +18 -0
- data/app/components/keystone/ui/section_component.rb +32 -0
- data/app/components/keystone/ui/select_component.html.erb +8 -0
- data/app/components/keystone/ui/select_component.rb +38 -0
- data/app/components/keystone/ui/settings_link_component.html.erb +4 -0
- data/app/components/keystone/ui/settings_link_component.rb +26 -0
- data/app/components/keystone/ui/show_page_component.html.erb +4 -0
- data/app/components/keystone/ui/show_page_component.rb +21 -0
- data/app/components/keystone/ui/stat_card_component.html.erb +22 -0
- data/app/components/keystone/ui/stat_card_component.rb +92 -0
- data/app/components/keystone/ui/swipe_deck_component.html.erb +39 -0
- data/app/components/keystone/ui/swipe_deck_component.rb +52 -0
- data/app/components/keystone/ui/tab_switcher_component.html.erb +14 -0
- data/app/components/keystone/ui/tab_switcher_component.rb +33 -0
- data/app/components/keystone/ui/textarea_component.html.erb +1 -0
- data/app/components/keystone/ui/textarea_component.rb +38 -0
- data/app/helpers/keystone_ui_helper.rb +195 -0
- data/config/importmap.rb +19 -0
- data/lib/generators/keystone/install_generator.rb +90 -0
- data/lib/keystone_ui/configuration.rb +24 -0
- data/lib/keystone_ui/engine.rb +47 -0
- data/lib/keystone_ui/safelist.rb +106 -0
- data/lib/keystone_ui/version.rb +5 -0
- data/lib/keystone_ui.rb +9 -0
- data/the_local/agents/keystone_ui-develop.md +297 -0
- data/the_local/agents/keystone_ui-info.md +81 -0
- data/the_local/agents/keystone_ui-install.md +137 -0
- data/the_local/interface.yml +110 -0
- metadata +203 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class MobileHeaderComponent < ViewComponent::Base
|
|
6
|
+
BACK_ICON = <<~SVG.freeze
|
|
7
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
|
8
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
|
|
9
|
+
</svg>
|
|
10
|
+
SVG
|
|
11
|
+
|
|
12
|
+
ELLIPSIS_ICON = <<~SVG.freeze
|
|
13
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
|
14
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z" />
|
|
15
|
+
</svg>
|
|
16
|
+
SVG
|
|
17
|
+
|
|
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"
|
|
23
|
+
|
|
24
|
+
# Renders the left-side and title of the mobile header.
|
|
25
|
+
# Call from the left side of your navbar.
|
|
26
|
+
#
|
|
27
|
+
# Usage in navbar:
|
|
28
|
+
# <% if content_for?(:form_page) %>
|
|
29
|
+
# <%= ui_mobile_header_left %>
|
|
30
|
+
# <% end %>
|
|
31
|
+
def initialize(title:, back_url:, subtitle: nil)
|
|
32
|
+
@title = title
|
|
33
|
+
@back_url = back_url
|
|
34
|
+
@subtitle = subtitle
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div data-controller="modal" data-modal-target="backdrop" class="<%= backdrop_classes %>" data-action="click->modal#closeOnBackdrop">
|
|
2
|
+
<div class="<%= panel_classes %>">
|
|
3
|
+
<div class="<%= header_classes %>">
|
|
4
|
+
<h3 class="<%= title_classes %>"><%= title %></h3>
|
|
5
|
+
<button type="button" data-action="click->modal#close" class="<%= close_button_classes %>" aria-label="Close">
|
|
6
|
+
<%= close_icon.html_safe %>
|
|
7
|
+
</button>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="<%= body_classes %>">
|
|
10
|
+
<%= content %>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
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"
|
|
11
|
+
BODY_CLASSES = "overflow-auto flex-1"
|
|
12
|
+
|
|
13
|
+
SIZE_CLASSES = {
|
|
14
|
+
sm: "max-w-sm",
|
|
15
|
+
md: "max-w-lg",
|
|
16
|
+
lg: "max-w-2xl",
|
|
17
|
+
xl: "max-w-4xl"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
CLOSE_ICON = <<~SVG.freeze
|
|
21
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
|
22
|
+
SVG
|
|
23
|
+
|
|
24
|
+
attr_reader :title
|
|
25
|
+
|
|
26
|
+
def initialize(title:, size: :md)
|
|
27
|
+
@title = title
|
|
28
|
+
@size = size
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def backdrop_classes
|
|
32
|
+
BACKDROP_CLASSES
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def panel_classes
|
|
36
|
+
[ PANEL_CLASSES, size_class ].join(" ")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def header_classes
|
|
40
|
+
HEADER_CLASSES
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def title_classes
|
|
44
|
+
TITLE_CLASSES
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def close_button_classes
|
|
48
|
+
CLOSE_BUTTON_CLASSES
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def body_classes
|
|
52
|
+
BODY_CLASSES
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def size_class
|
|
56
|
+
SIZE_CLASSES.fetch(@size)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def close_icon
|
|
60
|
+
CLOSE_ICON
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def wrapper_data
|
|
64
|
+
{ controller: "modal" }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<div class="<%= WRAPPER_CLASSES %>" data-controller="multi-select" data-multi-select-label-value="<%= @label %>">
|
|
2
|
+
<button type="button"
|
|
3
|
+
data-action="click->multi-select#toggle"
|
|
4
|
+
class="<%= TRIGGER_CLASSES %>">
|
|
5
|
+
<span data-multi-select-target="label"><%= display_text %></span>
|
|
6
|
+
<%= CARET_ICON.html_safe %>
|
|
7
|
+
</button>
|
|
8
|
+
<div data-multi-select-target="menu" class="<%= MENU_CLASSES %>">
|
|
9
|
+
<% @options.each do |opt_label, opt_value| %>
|
|
10
|
+
<label class="<%= OPTION_CLASSES %>">
|
|
11
|
+
<input type="checkbox"
|
|
12
|
+
name="<%= @name %>"
|
|
13
|
+
value="<%= opt_value %>"
|
|
14
|
+
<%= "checked" if selected?(opt_value) %>
|
|
15
|
+
data-action="change->multi-select#updateLabel"
|
|
16
|
+
class="<%= CHECKBOX_CLASSES %>">
|
|
17
|
+
<%= opt_label %>
|
|
18
|
+
</label>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class MultiSelectComponent < ViewComponent::Base
|
|
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"
|
|
11
|
+
CARET_ICON = <<~SVG.freeze
|
|
12
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
|
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" />
|
|
14
|
+
</svg>
|
|
15
|
+
SVG
|
|
16
|
+
|
|
17
|
+
attr_reader :name, :label, :options, :selected
|
|
18
|
+
|
|
19
|
+
def initialize(name:, label:, options:, selected: [])
|
|
20
|
+
@name = name
|
|
21
|
+
@label = label
|
|
22
|
+
@options = options
|
|
23
|
+
@selected = Array(selected).map(&:to_s)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def selected?(value)
|
|
27
|
+
@selected.include?(value.to_s)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def display_text
|
|
31
|
+
checked = @selected.reject(&:empty?)
|
|
32
|
+
if checked.empty?
|
|
33
|
+
"All #{@label}"
|
|
34
|
+
else
|
|
35
|
+
"#{checked.length} selected"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<div class="<%= WRAPPER_CLASSES %>" data-controller="dropdown">
|
|
2
|
+
<button type="button"
|
|
3
|
+
data-action="click->dropdown#toggle click@window->dropdown#hide"
|
|
4
|
+
class="<%= trigger_classes %>">
|
|
5
|
+
<%= @title %>
|
|
6
|
+
<%= CARET_ICON.html_safe %>
|
|
7
|
+
</button>
|
|
8
|
+
<nav data-dropdown-target="menu" class="<%= MENU_CLASSES %>">
|
|
9
|
+
<%= content %>
|
|
10
|
+
</nav>
|
|
11
|
+
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class NavDropdownComponent < ViewComponent::Base
|
|
6
|
+
WRAPPER_CLASSES = "nav-dropdown"
|
|
7
|
+
MENU_CLASSES = "nav-dropdown-menu hidden"
|
|
8
|
+
TRIGGER_BASE = "nav-dropdown-trigger"
|
|
9
|
+
ACTIVE_CLASS = "active"
|
|
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">
|
|
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
|
+
</svg>
|
|
14
|
+
SVG
|
|
15
|
+
|
|
16
|
+
attr_reader :title, :area
|
|
17
|
+
|
|
18
|
+
def initialize(title:, area:, active: false)
|
|
19
|
+
@title = title
|
|
20
|
+
@area = area
|
|
21
|
+
@active = active
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def trigger_classes
|
|
25
|
+
[ TRIGGER_BASE, (ACTIVE_CLASS if @active) ].compact.join(" ")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def wrapper_data
|
|
29
|
+
{ controller: "dropdown" }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= link_to @label, @href, class: link_classes %>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class NavItemComponent < ViewComponent::Base
|
|
6
|
+
attr_reader :label, :href
|
|
7
|
+
|
|
8
|
+
def initialize(label:, href:, active: false)
|
|
9
|
+
@label = label
|
|
10
|
+
@href = href
|
|
11
|
+
@active = active
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
ACTIVE_CLASS = "active"
|
|
15
|
+
|
|
16
|
+
def link_classes
|
|
17
|
+
@active ? ACTIVE_CLASS : ""
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<nav class="<%= nav_classes %>">
|
|
2
|
+
<% if mobile_left? %>
|
|
3
|
+
<div class="<%= MOBILE_LEFT_CLASSES %>">
|
|
4
|
+
<%= mobile_left %>
|
|
5
|
+
</div>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<% if logo? %>
|
|
9
|
+
<div class="<%= LOGO_CLASSES %>">
|
|
10
|
+
<%= logo %>
|
|
11
|
+
</div>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
14
|
+
<% if mobile_center? %>
|
|
15
|
+
<span class="<%= MOBILE_CENTER_CLASSES %>">
|
|
16
|
+
<%= mobile_center %>
|
|
17
|
+
</span>
|
|
18
|
+
<% end %>
|
|
19
|
+
|
|
20
|
+
<% if desktop_links? %>
|
|
21
|
+
<div class="<%= DESKTOP_LINKS_CLASSES %>">
|
|
22
|
+
<nav><%= desktop_links %></nav>
|
|
23
|
+
<% if desktop_right? %>
|
|
24
|
+
<nav><%= desktop_right %></nav>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
28
|
+
|
|
29
|
+
<% if mobile_right? %>
|
|
30
|
+
<div class="<%= MOBILE_RIGHT_CLASSES %>">
|
|
31
|
+
<%= mobile_right %>
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|
|
34
|
+
</nav>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class NavbarComponent < ViewComponent::Base
|
|
6
|
+
NAV_BASE = "top-nav"
|
|
7
|
+
NAV_STICKY = "sticky top-0 z-40"
|
|
8
|
+
MOBILE_LEFT_CLASSES = "lg:hidden flex items-center"
|
|
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%]"
|
|
11
|
+
DESKTOP_LINKS_CLASSES = "nav-container hidden lg:flex"
|
|
12
|
+
MOBILE_RIGHT_CLASSES = "nav-user-controls ml-auto"
|
|
13
|
+
|
|
14
|
+
renders_one :logo
|
|
15
|
+
renders_one :desktop_links
|
|
16
|
+
renders_one :desktop_right
|
|
17
|
+
renders_one :mobile_left
|
|
18
|
+
renders_one :mobile_center
|
|
19
|
+
renders_one :mobile_right
|
|
20
|
+
|
|
21
|
+
def initialize(sticky: true)
|
|
22
|
+
@sticky = sticky
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def nav_classes
|
|
26
|
+
classes = [ NAV_BASE ]
|
|
27
|
+
classes << NAV_STICKY if @sticky
|
|
28
|
+
classes.join(" ")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<label class="relative cursor-pointer" <%= tag.attributes(label_data.transform_keys { |k| "data-#{k}" }) unless label_data.empty? %>>
|
|
2
|
+
<input type="radio" name="<%= name %>" value="<%= value %>"
|
|
3
|
+
<%= "checked" if selected? %>
|
|
4
|
+
class="sr-only"
|
|
5
|
+
<%= tag.attributes(input_data.transform_keys { |k| "data-#{k}" }) unless input_data.empty? %>>
|
|
6
|
+
<span class="<%= classes %>">
|
|
7
|
+
<%= content %>
|
|
8
|
+
</span>
|
|
9
|
+
</label>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
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"
|
|
7
|
+
|
|
8
|
+
attr_reader :name, :value, :input_data, :label_data
|
|
9
|
+
|
|
10
|
+
def initialize(name:, value:, selected: false, input_data: {}, label_data: {})
|
|
11
|
+
@name = name
|
|
12
|
+
@value = value
|
|
13
|
+
@selected = selected
|
|
14
|
+
@input_data = input_data
|
|
15
|
+
@label_data = label_data
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def selected?
|
|
19
|
+
@selected
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def classes
|
|
23
|
+
border = selected? ? "border-accent-500" : "border-transparent"
|
|
24
|
+
"#{BASE_CLASSES} #{border}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class PageComponent < ViewComponent::Base
|
|
6
|
+
MAX_WIDTH_CLASSES = {
|
|
7
|
+
sm: "max-w-2xl",
|
|
8
|
+
md: "max-w-4xl",
|
|
9
|
+
lg: "max-w-6xl",
|
|
10
|
+
xl: "max-w-7xl",
|
|
11
|
+
full: ""
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
PADDING_CLASSES = "px-4 py-4 sm:px-6 lg:px-8"
|
|
15
|
+
|
|
16
|
+
TOP_OFFSET_CLASSES = {
|
|
17
|
+
sm: "pt-12",
|
|
18
|
+
md: "pt-16",
|
|
19
|
+
lg: "pt-20",
|
|
20
|
+
xl: "pt-24"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def initialize(max_width: :full, padding: :standard, top_offset: nil)
|
|
24
|
+
@max_width = max_width
|
|
25
|
+
@padding = padding
|
|
26
|
+
@top_offset = top_offset
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def classes
|
|
30
|
+
tokens = []
|
|
31
|
+
tokens << PADDING_CLASSES unless @padding == :none
|
|
32
|
+
tokens << TOP_OFFSET_CLASSES.fetch(@top_offset) if @top_offset
|
|
33
|
+
width_class = MAX_WIDTH_CLASSES.fetch(@max_width)
|
|
34
|
+
tokens << width_class unless width_class.empty?
|
|
35
|
+
tokens << "mx-auto" unless @max_width == :full
|
|
36
|
+
tokens.join(" ")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<% if action_url %>
|
|
2
|
+
<% content_for :mobile_action_url, action_url %>
|
|
3
|
+
<% content_for :mobile_action_label, action_label %>
|
|
4
|
+
<% end %>
|
|
5
|
+
<div class="<%= WRAPPER_CLASSES %>">
|
|
6
|
+
<div>
|
|
7
|
+
<h1 class="<%= TITLE_CLASSES %>"><%= @title %></h1>
|
|
8
|
+
<% if subtitle? %>
|
|
9
|
+
<p class="<%= SUBTITLE_CLASSES %>"><%= subtitle_text %></p>
|
|
10
|
+
<% end %>
|
|
11
|
+
</div>
|
|
12
|
+
<% if action? %>
|
|
13
|
+
<div class="<%= ACTIONS_CLASSES %>">
|
|
14
|
+
<%= @action_block.call %>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class PageHeaderComponent < ViewComponent::Base
|
|
6
|
+
WRAPPER_CLASSES = "hidden sm:block mb-6 sm:flex sm:items-center sm:justify-between"
|
|
7
|
+
TITLE_CLASSES = "text-2xl font-bold text-gray-900 dark:text-white"
|
|
8
|
+
SUBTITLE_CLASSES = "mt-1 text-sm text-gray-500 dark:text-gray-400"
|
|
9
|
+
ACTIONS_CLASSES = "page-header-actions hidden sm:block mt-4 sm:mt-0 sm:ml-4 flex-shrink-0"
|
|
10
|
+
|
|
11
|
+
attr_reader :title, :action_url, :action_label
|
|
12
|
+
|
|
13
|
+
def initialize(title:, subtitle: nil, action_url: nil, action_label: "Add new")
|
|
14
|
+
@title = title
|
|
15
|
+
@subtitle = subtitle
|
|
16
|
+
@action_url = action_url
|
|
17
|
+
@action_label = action_label
|
|
18
|
+
@action_block = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def before_render
|
|
22
|
+
content
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def action(&block)
|
|
26
|
+
@action_block = block
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def action?
|
|
30
|
+
!!@action_block
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def subtitle?
|
|
34
|
+
!@subtitle.nil?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def subtitle_text
|
|
38
|
+
@subtitle
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
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
|
|
8
|
+
|
|
9
|
+
def initialize(padding: :md, radius: :lg, shadow: true)
|
|
10
|
+
@padding = padding
|
|
11
|
+
@radius = radius
|
|
12
|
+
@shadow = shadow
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
BASE_CLASSES = "border border-gray-200 bg-white dark:bg-zinc-900 dark:border-zinc-700"
|
|
16
|
+
SHADOW_CLASS = "shadow-sm"
|
|
17
|
+
|
|
18
|
+
def classes
|
|
19
|
+
tokens = [ RADIUS_CLASSES.fetch(@radius), BASE_CLASSES, PADDING_CLASSES.fetch(@padding) ]
|
|
20
|
+
tokens << SHADOW_CLASS if @shadow
|
|
21
|
+
tokens.join(" ")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<div class="<%= CONTAINER_CLASSES %>">
|
|
2
|
+
<div class="<%= HEADER_CLASSES %>">
|
|
3
|
+
<h3 class="<%= TITLE_CLASSES %>"><%= title %></h3>
|
|
4
|
+
<% if subtitle %><p class="<%= SUBTITLE_CLASSES %>"><%= subtitle %></p><% end %>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="<%= TRACK_CLASSES %>">
|
|
8
|
+
<% boxes.each_with_index do |box, i| %>
|
|
9
|
+
<div class="<%= BOX_CLASSES %>">
|
|
10
|
+
<span class="<%= BOX_LABEL_CLASSES %>"><%= box[:label] %></span>
|
|
11
|
+
<% unless box[:count].nil? %>
|
|
12
|
+
<span class="<%= count_class(box[:accent]) %>"><%= box[:count] %></span>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% if box[:action] %>
|
|
15
|
+
<%= form_with url: box[:action][:url], method: :post do %>
|
|
16
|
+
<% box[:action].fetch(:params, {}).each do |k, v| %><%= hidden_field_tag k, v %><% end %>
|
|
17
|
+
<%= helpers.ui_button(label: box[:action][:label], type: :submit, variant: box[:action].fetch(:variant, :primary)) %>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<% if (link = link_after(i)) %>
|
|
23
|
+
<div class="<%= CONNECTOR_CLASSES %>">
|
|
24
|
+
<%= form_with url: link[:url], method: :post do %>
|
|
25
|
+
<% link.fetch(:params, {}).each do |k, v| %><%= hidden_field_tag k, v %><% end %>
|
|
26
|
+
<button type="submit" class="<%= link_classes(link) %>" title="<%= link[:broken] ? "Broken handoff — click to fix" : "Healthy handoff — click to break" %>">
|
|
27
|
+
<% if link[:broken] %>✗<% else %>✓<% end %>
|
|
28
|
+
</button>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
31
|
+
<% end %>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
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"
|
|
13
|
+
CONNECTOR_CLASSES = "flex items-center justify-center"
|
|
14
|
+
COUNT_BASE_CLASSES = "text-3xl font-bold"
|
|
15
|
+
LINK_BASE_CLASSES = "link-toggle text-2xl leading-none"
|
|
16
|
+
LINK_HEALTHY_CLASSES = "text-accent-500"
|
|
17
|
+
LINK_BROKEN_CLASSES = "text-red-500"
|
|
18
|
+
|
|
19
|
+
COUNT_CLASSES = {
|
|
20
|
+
amber: "text-amber-400",
|
|
21
|
+
emerald: "text-accent-400",
|
|
22
|
+
danger: "text-red-400",
|
|
23
|
+
muted: "text-surface-500"
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
attr_reader :title, :boxes, :links, :subtitle
|
|
27
|
+
|
|
28
|
+
def initialize(title:, boxes:, links:, subtitle: nil)
|
|
29
|
+
@title = title
|
|
30
|
+
@boxes = boxes
|
|
31
|
+
@links = links
|
|
32
|
+
@subtitle = subtitle
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def count_class(accent)
|
|
36
|
+
"#{COUNT_BASE_CLASSES} #{COUNT_CLASSES.fetch(accent, COUNT_CLASSES[:muted])}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def link_after(index)
|
|
40
|
+
links[index]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def link_classes(link)
|
|
44
|
+
"#{LINK_BASE_CLASSES} #{link[:broken] ? LINK_BROKEN_CLASSES : LINK_HEALTHY_CLASSES}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class ProgressComponent < ViewComponent::Base
|
|
6
|
+
TRACK_CLASSES = "w-full h-2 bg-surface-200 rounded-full overflow-hidden"
|
|
7
|
+
BAR_CLASSES = "h-full bg-accent-500 rounded-full transition-all"
|
|
8
|
+
LABEL_CLASSES = "mb-1 text-sm font-medium text-surface-700"
|
|
9
|
+
|
|
10
|
+
attr_reader :value, :max, :label
|
|
11
|
+
|
|
12
|
+
def initialize(value:, max:, label: nil)
|
|
13
|
+
@value = value
|
|
14
|
+
@max = max
|
|
15
|
+
@label = label
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def percent
|
|
19
|
+
[ (value.to_f / max * 100).round, 100 ].min
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def track_classes
|
|
23
|
+
TRACK_CLASSES
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def bar_classes
|
|
27
|
+
BAR_CLASSES
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def label_classes
|
|
31
|
+
LABEL_CLASSES
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<label class="block cursor-pointer">
|
|
2
|
+
<input type="radio" name="<%= name %>" value="<%= value %>" <%= "checked" if checked? %> class="peer sr-only">
|
|
3
|
+
<span class="<%= classes %>">
|
|
4
|
+
<span class="<%= label_classes %>"><%= label %></span>
|
|
5
|
+
<% if hint? %>
|
|
6
|
+
<span class="<%= hint_classes %>"><%= hint %></span>
|
|
7
|
+
<% end %>
|
|
8
|
+
</span>
|
|
9
|
+
</label>
|