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,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,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,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
|