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,37 @@
|
|
|
1
|
+
<div class="relative inline-block" data-controller="<%= controller_name %>" data-color-picker-value-value="<%= value %>">
|
|
2
|
+
<% if label %>
|
|
3
|
+
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"><%= label %></label>
|
|
4
|
+
<% end %>
|
|
5
|
+
|
|
6
|
+
<div class="flex items-center gap-2">
|
|
7
|
+
<button type="button"
|
|
8
|
+
class="<%= swatch_classes %>"
|
|
9
|
+
style="background-color: <%= value %>;"
|
|
10
|
+
data-color-picker-target="swatch"
|
|
11
|
+
data-action="color-picker#toggle">
|
|
12
|
+
</button>
|
|
13
|
+
<span class="text-sm text-gray-500 dark:text-gray-400" data-color-picker-target="hexLabel"><%= value %></span>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<input type="hidden" name="<%= name %>" value="<%= value %>" data-color-picker-target="input">
|
|
17
|
+
|
|
18
|
+
<div class="<%= panel_classes %>" data-color-picker-target="panel">
|
|
19
|
+
<%# Saturation/Value gradient area %>
|
|
20
|
+
<div class="w-48 h-36 rounded cursor-crosshair relative mb-2"
|
|
21
|
+
data-color-picker-target="svArea"
|
|
22
|
+
data-action="pointerdown->color-picker#svDown">
|
|
23
|
+
<div class="absolute inset-0 rounded" style="background: linear-gradient(to right, #fff, hsl(0, 100%, 50%));" data-color-picker-target="svGradient"></div>
|
|
24
|
+
<div class="absolute inset-0 rounded" style="background: linear-gradient(to bottom, transparent, #000);"></div>
|
|
25
|
+
<div class="absolute w-3 h-3 rounded-full border-2 border-white shadow -translate-x-1/2 -translate-y-1/2 pointer-events-none"
|
|
26
|
+
data-color-picker-target="svCursor"
|
|
27
|
+
style="left: 100%; top: 0;">
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<%# Hue slider %>
|
|
32
|
+
<input type="range" min="0" max="360" value="0"
|
|
33
|
+
class="w-48 keystone-hue-slider"
|
|
34
|
+
data-color-picker-target="hueSlider"
|
|
35
|
+
data-action="input->color-picker#hueChanged">
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class ColorPickerComponent < ViewComponent::Base
|
|
6
|
+
SWATCH_CLASSES = "w-10 h-10 rounded-lg border border-gray-300 dark:border-zinc-600 cursor-pointer"
|
|
7
|
+
|
|
8
|
+
PANEL_CLASSES = "absolute z-50 mt-2 p-3 rounded-lg shadow-lg bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 hidden"
|
|
9
|
+
|
|
10
|
+
attr_reader :name, :value, :label
|
|
11
|
+
|
|
12
|
+
def initialize(name:, value: "#000000", label: nil)
|
|
13
|
+
@name = name
|
|
14
|
+
@value = value
|
|
15
|
+
@label = label
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def controller_name
|
|
19
|
+
"color-picker"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def swatch_classes
|
|
23
|
+
SWATCH_CLASSES
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def panel_classes
|
|
27
|
+
PANEL_CLASSES
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class Column
|
|
6
|
+
attr_reader :key, :header_text
|
|
7
|
+
|
|
8
|
+
def initialize(key, header_text, mobile_hidden: false, sortable: false, hideable: false)
|
|
9
|
+
@key = key
|
|
10
|
+
@header_text = header_text
|
|
11
|
+
@mobile_hidden = mobile_hidden
|
|
12
|
+
@sortable = sortable
|
|
13
|
+
@hideable = hideable
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def mobile_hidden? = @mobile_hidden
|
|
17
|
+
def sortable? = @sortable
|
|
18
|
+
def hideable? = @hideable
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<div class="<%= WRAPPER_CLASSES %>" data-controller="column-picker" <% if @save_url %>data-column-picker-save-url-value="<%= @save_url %>"<% end %>>
|
|
2
|
+
<button type="button"
|
|
3
|
+
data-action="click->column-picker#toggle"
|
|
4
|
+
class="<%= TRIGGER_CLASSES %>">
|
|
5
|
+
<%= COLUMNS_ICON.html_safe %>
|
|
6
|
+
Columns
|
|
7
|
+
</button>
|
|
8
|
+
<div data-column-picker-target="menu" class="<%= MENU_CLASSES %>">
|
|
9
|
+
<% hideable_columns.each do |col| %>
|
|
10
|
+
<label class="<%= OPTION_CLASSES %>">
|
|
11
|
+
<input type="checkbox"
|
|
12
|
+
value="<%= col.key %>"
|
|
13
|
+
<%= "checked" unless hidden?(col.key) %>
|
|
14
|
+
data-action="change->column-picker#save"
|
|
15
|
+
class="<%= CHECKBOX_CLASSES %>">
|
|
16
|
+
<%= col.header_text %>
|
|
17
|
+
</label>
|
|
18
|
+
<% end %>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class ColumnPickerComponent < 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 right-0 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
|
+
|
|
12
|
+
COLUMNS_ICON = <<~SVG.freeze
|
|
13
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
|
14
|
+
<path fill-rule="evenodd" d="M.99 5.24A2.25 2.25 0 0 1 3.25 3h13.5A2.25 2.25 0 0 1 19 5.25l.01 9.5A2.25 2.25 0 0 1 16.76 17H3.26A2.25 2.25 0 0 1 1 14.75l-.01-9.5Zm8.26 9.52v-3.5l-2.25.01v3.5l2.25-.01Zm1.5 0 2.25.01v-3.5l-2.25-.01v3.5Zm-1.5-5v-3.5l-2.25.01v3.49l2.25.01Zm1.5-.01 2.25.01v-3.5l-2.25-.01v3.5Z" clip-rule="evenodd" />
|
|
15
|
+
</svg>
|
|
16
|
+
SVG
|
|
17
|
+
|
|
18
|
+
attr_reader :save_url
|
|
19
|
+
|
|
20
|
+
def initialize(columns:, hidden_columns: [], save_url: nil)
|
|
21
|
+
@columns = columns
|
|
22
|
+
@hidden_keys = Array(hidden_columns).map(&:to_sym).to_set
|
|
23
|
+
@save_url = save_url
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def hideable_columns
|
|
27
|
+
@columns.select(&:hideable?)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def hidden?(key)
|
|
31
|
+
@hidden_keys.include?(key.to_sym)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<button
|
|
2
|
+
type="button"
|
|
3
|
+
class="<%= classes %>"
|
|
4
|
+
data-controller="clipboard"
|
|
5
|
+
data-action="click->clipboard#copy"
|
|
6
|
+
data-clipboard-text="<%= text %>"
|
|
7
|
+
data-clipboard-success-message-value="<%= success_message %>"
|
|
8
|
+
data-clipboard-error-message-value="<%= error_message %>">
|
|
9
|
+
<%= copy_icon.html_safe %>
|
|
10
|
+
<%= label %>
|
|
11
|
+
</button>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class CopyButtonComponent < ViewComponent::Base
|
|
6
|
+
BUTTON_CLASSES = "inline-flex items-center gap-1.5 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 transition hover:bg-gray-50 dark:border-zinc-600 dark:bg-zinc-800 dark:text-gray-300 dark:hover:bg-zinc-700"
|
|
7
|
+
|
|
8
|
+
COPY_ICON = <<~SVG.freeze
|
|
9
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
|
|
10
|
+
SVG
|
|
11
|
+
|
|
12
|
+
attr_reader :text, :label, :success_message, :error_message
|
|
13
|
+
|
|
14
|
+
def initialize(text:, label: "Copy", success_message: "Copied!", error_message: "Failed!")
|
|
15
|
+
@text = text
|
|
16
|
+
@label = label
|
|
17
|
+
@success_message = success_message
|
|
18
|
+
@error_message = error_message
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def classes
|
|
22
|
+
BUTTON_CLASSES
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def copy_icon
|
|
26
|
+
COPY_ICON
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def wrapper_data
|
|
30
|
+
{
|
|
31
|
+
controller: "clipboard",
|
|
32
|
+
"clipboard-text": @text,
|
|
33
|
+
"clipboard-success-message-value": @success_message,
|
|
34
|
+
"clipboard-error-message-value": @error_message
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class CtaBannerComponent < ViewComponent::Base
|
|
6
|
+
CARD_LAYOUT_CLASSES = "rounded-2xl border px-6 py-12 text-center lg:px-16 lg:py-16"
|
|
7
|
+
TITLE_BASE_CLASSES = "mb-4 text-3xl font-bold tracking-tight sm:text-4xl"
|
|
8
|
+
SUBTITLE_BASE_CLASSES = "mx-auto mb-8 max-w-2xl text-lg"
|
|
9
|
+
ACTIONS_CLASSES = "flex flex-wrap justify-center gap-4"
|
|
10
|
+
|
|
11
|
+
attr_reader :title, :subtitle
|
|
12
|
+
|
|
13
|
+
def initialize(title:, subtitle: nil)
|
|
14
|
+
@title = title
|
|
15
|
+
@subtitle = subtitle
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def classes
|
|
19
|
+
"#{CARD_LAYOUT_CLASSES} border-surface-200 dark:border-surface-700 bg-surface-50 dark:bg-surface-800"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def title_classes
|
|
23
|
+
"#{TITLE_BASE_CLASSES} text-surface-900 dark:text-white"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def subtitle_classes
|
|
27
|
+
"#{SUBTITLE_BASE_CLASSES} text-surface-500 dark:text-surface-400"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def subtitle?
|
|
31
|
+
!@subtitle.nil?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def actions_classes
|
|
35
|
+
ACTIONS_CLASSES
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<div class="overflow-hidden rounded-lg border border-gray-200 shadow-sm dark:border-zinc-700 dark:shadow-none">
|
|
2
|
+
<table class="relative min-w-full divide-y divide-gray-300 dark:divide-white/15">
|
|
3
|
+
<thead class="bg-gray-50 dark:bg-gray-800/75">
|
|
4
|
+
<tr>
|
|
5
|
+
<% header_cells.each do |cell| %>
|
|
6
|
+
<th scope="<%= cell[:scope] %>" class="<%= cell[:classes] %>">
|
|
7
|
+
<% if cell[:sort_href] %>
|
|
8
|
+
<a href="<%= cell[:sort_href] %>" class="<%= Keystone::Ui::DataTableComponent::SORT_LINK_CLASSES %>" data-turbo-action="replace">
|
|
9
|
+
<%= cell[:label] %>
|
|
10
|
+
<% if cell[:sort_active] %>
|
|
11
|
+
<span class="<%= Keystone::Ui::DataTableComponent::SORT_ICON_CLASSES %> <%= Keystone::Ui::DataTableComponent::SORT_ICON_ACTIVE %>"><%= (cell[:sort_direction] == :asc ? Keystone::Ui::DataTableComponent::SORT_ASC_ICON : Keystone::Ui::DataTableComponent::SORT_DESC_ICON).html_safe %></span>
|
|
12
|
+
<% else %>
|
|
13
|
+
<span class="<%= Keystone::Ui::DataTableComponent::SORT_ICON_CLASSES %> <%= Keystone::Ui::DataTableComponent::SORT_ICON_INACTIVE %>"><%= Keystone::Ui::DataTableComponent::SORT_NEUTRAL_ICON.html_safe %></span>
|
|
14
|
+
<% end %>
|
|
15
|
+
</a>
|
|
16
|
+
<% else %>
|
|
17
|
+
<%= cell[:label] %>
|
|
18
|
+
<% end %>
|
|
19
|
+
</th>
|
|
20
|
+
<% end %>
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/10 dark:bg-gray-800/50">
|
|
24
|
+
<% if empty? %>
|
|
25
|
+
<% if @empty_message %>
|
|
26
|
+
<tr>
|
|
27
|
+
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400" colspan="<%= column_count %>">
|
|
28
|
+
<%= @empty_message %>
|
|
29
|
+
</td>
|
|
30
|
+
</tr>
|
|
31
|
+
<% end %>
|
|
32
|
+
<% else %>
|
|
33
|
+
<% @items.each_with_index do |item, idx| %>
|
|
34
|
+
<tr>
|
|
35
|
+
<% row_cells[idx].each do |cell| %>
|
|
36
|
+
<td class="<%= cell[:classes] %>">
|
|
37
|
+
<% if cell[:href] %>
|
|
38
|
+
<a href="<%= cell[:href] %>"><%= cell[:value] %></a>
|
|
39
|
+
<% else %>
|
|
40
|
+
<%= cell[:value] %>
|
|
41
|
+
<% end %>
|
|
42
|
+
</td>
|
|
43
|
+
<% end %>
|
|
44
|
+
<% if actions? %>
|
|
45
|
+
<td class="<%= Keystone::Ui::DataTableComponent::ROW_CLASSES_LAST %>">
|
|
46
|
+
<%= capture(item, &@actions_block) %>
|
|
47
|
+
</td>
|
|
48
|
+
<% end %>
|
|
49
|
+
</tr>
|
|
50
|
+
<% end %>
|
|
51
|
+
<% end %>
|
|
52
|
+
</tbody>
|
|
53
|
+
</table>
|
|
54
|
+
</div>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class DataTableComponent < ViewComponent::Base
|
|
6
|
+
HEADER_CLASSES_FIRST = "py-3.5 pr-3 pl-6 text-left text-sm font-semibold text-gray-900 dark:text-gray-200"
|
|
7
|
+
HEADER_CLASSES_MIDDLE = "px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-200"
|
|
8
|
+
HEADER_CLASSES_LAST = "py-3.5 pr-6 pl-3"
|
|
9
|
+
|
|
10
|
+
ROW_CLASSES_FIRST = "py-4 pr-3 pl-6 text-sm font-medium whitespace-nowrap text-gray-900 dark:text-white"
|
|
11
|
+
ROW_CLASSES_MIDDLE = "px-3 py-4 text-sm whitespace-nowrap text-gray-500 dark:text-gray-400"
|
|
12
|
+
ROW_CLASSES_LAST = "py-4 pr-6 pl-3 text-right text-sm font-medium whitespace-nowrap"
|
|
13
|
+
|
|
14
|
+
MOBILE_HIDDEN_CLASSES = "hidden sm:table-cell"
|
|
15
|
+
|
|
16
|
+
SORT_LINK_CLASSES = "group inline-flex items-center gap-1"
|
|
17
|
+
SORT_ICON_CLASSES = "h-4 w-4 flex-shrink-0"
|
|
18
|
+
SORT_ICON_ACTIVE = "text-gray-700 dark:text-gray-300"
|
|
19
|
+
SORT_ICON_INACTIVE = "text-gray-400 dark:text-gray-500 invisible group-hover:visible"
|
|
20
|
+
|
|
21
|
+
SORT_ASC_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z" clip-rule="evenodd" /></svg>'
|
|
22
|
+
SORT_DESC_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a.75.75 0 0 1 .75.75v10.638l3.96-4.158a.75.75 0 1 1 1.08 1.04l-5.25 5.5a.75.75 0 0 1-1.08 0l-5.25-5.5a.75.75 0 0 1 1.08-1.04l3.96 4.158V3.75A.75.75 0 0 1 10 3Z" clip-rule="evenodd" /></svg>'
|
|
23
|
+
SORT_NEUTRAL_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a.75.75 0 0 1 .55.24l3.25 3.5a.75.75 0 1 1-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 0 1-1.1-1.02l3.25-3.5A.75.75 0 0 1 10 3Zm-3.76 9.2a.75.75 0 0 1 1.06.04l2.7 2.908 2.7-2.908a.75.75 0 1 1 1.1 1.02l-3.25 3.5a.75.75 0 0 1-1.1 0l-3.25-3.5a.75.75 0 0 1 .04-1.06Z" clip-rule="evenodd" /></svg>'
|
|
24
|
+
|
|
25
|
+
def initialize(items:, columns:, empty_message: nil, sort: nil, sort_direction: nil, sort_url: nil, hidden_columns: [])
|
|
26
|
+
@items = items
|
|
27
|
+
all_columns = columns.map { |col| normalize_column(col) }
|
|
28
|
+
hidden_keys = Array(hidden_columns).map(&:to_sym).to_set
|
|
29
|
+
@columns = all_columns.reject { |col| col.hideable? && hidden_keys.include?(col.key) }
|
|
30
|
+
@empty_message = empty_message
|
|
31
|
+
@sort = sort&.to_sym
|
|
32
|
+
@sort_direction = sort_direction&.to_sym
|
|
33
|
+
@sort_url = sort_url
|
|
34
|
+
@actions_block = nil
|
|
35
|
+
@link_blocks = {}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def before_render
|
|
39
|
+
content
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def link(column_key, &block)
|
|
43
|
+
@link_blocks[column_key] = block
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def actions(&block)
|
|
47
|
+
@actions_block = block
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def actions?
|
|
51
|
+
!!@actions_block
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def column_keys
|
|
55
|
+
@column_keys ||= @columns.map(&:key)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def column_labels
|
|
59
|
+
@column_labels ||= @columns.map(&:header_text)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def header_cells
|
|
63
|
+
cells = @columns.map.with_index do |column, index|
|
|
64
|
+
tokens = [ header_classes_for(index) ]
|
|
65
|
+
tokens << MOBILE_HIDDEN_CLASSES if column.mobile_hidden?
|
|
66
|
+
|
|
67
|
+
cell = {
|
|
68
|
+
label: column.header_text,
|
|
69
|
+
classes: tokens.join(" "),
|
|
70
|
+
scope: "col"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if @sort_url
|
|
74
|
+
active = column.sortable? && @sort == column.key
|
|
75
|
+
cell[:sortable] = column.sortable?
|
|
76
|
+
|
|
77
|
+
if column.sortable?
|
|
78
|
+
if active
|
|
79
|
+
cell[:sort_active] = true
|
|
80
|
+
cell[:sort_direction] = @sort_direction
|
|
81
|
+
next_dir = @sort_direction == :asc ? :desc : :asc
|
|
82
|
+
else
|
|
83
|
+
cell[:sort_active] = false
|
|
84
|
+
next_dir = :asc
|
|
85
|
+
end
|
|
86
|
+
cell[:sort_href] = @sort_url.call(column.key, next_dir)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
cell
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if actions?
|
|
94
|
+
cells << {
|
|
95
|
+
label: "Actions",
|
|
96
|
+
classes: HEADER_CLASSES_LAST,
|
|
97
|
+
scope: "col"
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
cells
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def row_cells
|
|
105
|
+
@row_cells ||= @items.map do |item|
|
|
106
|
+
@columns.map.with_index do |column, index|
|
|
107
|
+
tokens = [ row_classes_for(index) ]
|
|
108
|
+
tokens << MOBILE_HIDDEN_CLASSES if column.mobile_hidden?
|
|
109
|
+
|
|
110
|
+
cell = {
|
|
111
|
+
value: resolve_value(item, column.key),
|
|
112
|
+
classes: tokens.join(" ")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
link_block = @link_blocks[column.key]
|
|
116
|
+
cell[:href] = link_block.call(item) if link_block
|
|
117
|
+
|
|
118
|
+
cell
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def empty?
|
|
124
|
+
@items.empty?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def column_count
|
|
128
|
+
visual_column_count
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def visual_column_count
|
|
134
|
+
@columns.length + (actions? ? 1 : 0)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def normalize_column(col)
|
|
138
|
+
case col
|
|
139
|
+
when Column then col
|
|
140
|
+
when Hash
|
|
141
|
+
key, label = col.first
|
|
142
|
+
Column.new(key, label)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def resolve_value(item, key)
|
|
147
|
+
if item.respond_to?(key)
|
|
148
|
+
item.public_send(key)
|
|
149
|
+
else
|
|
150
|
+
item[key]
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def header_classes_for(index)
|
|
155
|
+
last_data_index = @columns.length - 1
|
|
156
|
+
|
|
157
|
+
return HEADER_CLASSES_FIRST if index.zero?
|
|
158
|
+
return HEADER_CLASSES_LAST if index == last_data_index && !actions?
|
|
159
|
+
|
|
160
|
+
HEADER_CLASSES_MIDDLE
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def row_classes_for(index)
|
|
164
|
+
last_data_index = @columns.length - 1
|
|
165
|
+
|
|
166
|
+
return ROW_CLASSES_FIRST if index.zero?
|
|
167
|
+
return ROW_CLASSES_LAST if index == last_data_index && !actions?
|
|
168
|
+
|
|
169
|
+
ROW_CLASSES_MIDDLE
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<details class="<%= wrapper_classes %>"<% if open? %> open<% end %>>
|
|
2
|
+
<summary class="<%= summary_classes %>">
|
|
3
|
+
<span><%= summary %></span>
|
|
4
|
+
<span class="<%= icon_classes %>"><%= caret_icon.html_safe %></span>
|
|
5
|
+
</summary>
|
|
6
|
+
<div class="<%= body_classes %>">
|
|
7
|
+
<%= content %>
|
|
8
|
+
</div>
|
|
9
|
+
</details>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class DisclosureComponent < ViewComponent::Base
|
|
6
|
+
WRAPPER_CLASSES = "group rounded-xl border border-surface-200 dark:border-surface-700"
|
|
7
|
+
SUMMARY_CLASSES = "flex cursor-pointer list-none items-center justify-between gap-4 px-6 py-4 font-semibold text-surface-900 [&::-webkit-details-marker]:hidden dark:text-white"
|
|
8
|
+
ICON_CLASSES = "h-4 w-4 shrink-0 text-surface-400 transition-transform group-open:rotate-180"
|
|
9
|
+
BODY_CLASSES = "px-6 pb-4 text-sm text-surface-600 dark:text-surface-400"
|
|
10
|
+
|
|
11
|
+
CARET_ICON = <<~SVG.freeze
|
|
12
|
+
<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="M19 9l-7 7-7-7" /></svg>
|
|
13
|
+
SVG
|
|
14
|
+
|
|
15
|
+
renders_one :summary
|
|
16
|
+
|
|
17
|
+
def initialize(open: false)
|
|
18
|
+
@open = open
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def open?
|
|
22
|
+
@open
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def wrapper_classes
|
|
26
|
+
WRAPPER_CLASSES
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def summary_classes
|
|
30
|
+
SUMMARY_CLASSES
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def icon_classes
|
|
34
|
+
ICON_CLASSES
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def body_classes
|
|
38
|
+
BODY_CLASSES
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def caret_icon
|
|
42
|
+
CARET_ICON
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<h2 class="<%= title_classes %>"><%= title %></h2>
|
|
3
|
+
<% if subtitle? %>
|
|
4
|
+
<p class="<%= subtitle_classes %>"><%= subtitle %></p>
|
|
5
|
+
<% end %>
|
|
6
|
+
<div class="<%= classes %>">
|
|
7
|
+
<% features.each do |feature| %>
|
|
8
|
+
<div class="<%= card_classes %>">
|
|
9
|
+
<div class="<%= icon_classes %>">
|
|
10
|
+
<%= feature[:icon].html_safe %>
|
|
11
|
+
</div>
|
|
12
|
+
<h3 class="<%= card_title_classes %>"><%= feature[:title] %></h3>
|
|
13
|
+
<p class="<%= card_description_classes %>"><%= feature[:description] %></p>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Keystone
|
|
4
|
+
module Ui
|
|
5
|
+
class FeatureGridComponent < ViewComponent::Base
|
|
6
|
+
GRID_CLASSES = "grid gap-6 sm:grid-cols-2 lg:grid-cols-3"
|
|
7
|
+
TITLE_BASE_CLASSES = "mb-4 text-center text-3xl font-bold tracking-tight sm:text-4xl"
|
|
8
|
+
SUBTITLE_BASE_CLASSES = "mx-auto mb-16 max-w-2xl text-center text-lg"
|
|
9
|
+
CARD_LAYOUT_CLASSES = "rounded-xl border p-6 transition"
|
|
10
|
+
ICON_BASE_CLASSES = "mb-4 flex size-10 items-center justify-center rounded-lg text-lg"
|
|
11
|
+
CARD_TITLE_BASE_CLASSES = "mb-2 text-lg font-semibold"
|
|
12
|
+
CARD_DESCRIPTION_BASE_CLASSES = "text-sm"
|
|
13
|
+
|
|
14
|
+
attr_reader :title, :subtitle, :features
|
|
15
|
+
|
|
16
|
+
def initialize(title:, features:, subtitle: nil)
|
|
17
|
+
@title = title
|
|
18
|
+
@subtitle = subtitle
|
|
19
|
+
@features = features
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def classes
|
|
23
|
+
GRID_CLASSES
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def title_classes
|
|
27
|
+
"#{TITLE_BASE_CLASSES} text-surface-900 dark:text-white"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def subtitle_classes
|
|
31
|
+
"#{SUBTITLE_BASE_CLASSES} text-surface-500 dark:text-surface-400"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def subtitle?
|
|
35
|
+
!@subtitle.nil?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def card_classes
|
|
39
|
+
"#{CARD_LAYOUT_CLASSES} border-surface-200 dark:border-surface-700 bg-white dark:bg-surface-800 hover:border-accent-500/50 dark:hover:border-accent-500/50"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def icon_classes
|
|
43
|
+
"#{ICON_BASE_CLASSES} bg-accent-500/10 text-accent-600 dark:text-accent-400"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def card_title_classes
|
|
47
|
+
"#{CARD_TITLE_BASE_CLASSES} text-surface-900 dark:text-white"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def card_description_classes
|
|
51
|
+
"#{CARD_DESCRIPTION_BASE_CLASSES} text-surface-500 dark:text-surface-400"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<div class="<%= WRAPPER_CLASSES %>" data-controller="file-upload">
|
|
2
|
+
<span class="<%= LABEL_CLASSES %>"><%= label_text %></span>
|
|
3
|
+
|
|
4
|
+
<label class="<%= DROP_ZONE_CLASSES %>" data-file-upload-target="dropZone">
|
|
5
|
+
<div class="<%= DROP_ZONE_INNER_CLASSES %>">
|
|
6
|
+
<%= UPLOAD_ICON.html_safe %>
|
|
7
|
+
<p class="<%= PROMPT_CLASSES %>">
|
|
8
|
+
<%= prompt_text %> <span class="<%= BROWSE_CLASSES %>">browse</span>
|
|
9
|
+
</p>
|
|
10
|
+
<p class="<%= FILE_NAME_CLASSES %>" data-file-upload-target="fileName"></p>
|
|
11
|
+
</div>
|
|
12
|
+
<%= tag.input(**tag_options) %>
|
|
13
|
+
</label>
|
|
14
|
+
|
|
15
|
+
<% if hint? %>
|
|
16
|
+
<p class="<%= HINT_CLASSES %>"><%= hint_text %></p>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|