ruby_ui 1.1.0 → 1.3.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 +4 -4
- data/lib/generators/ruby_ui/component_generator.rb +5 -1
- data/lib/generators/ruby_ui/dependencies.yml +32 -0
- data/lib/generators/ruby_ui/install/install_generator.rb +1 -1
- data/lib/generators/ruby_ui/install/templates/tailwind.css.erb +1 -1
- data/lib/generators/ruby_ui/javascript_utils.rb +27 -6
- data/lib/ruby_ui/avatar/avatar.rb +3 -0
- data/lib/ruby_ui/avatar/avatar_controller.js +33 -0
- data/lib/ruby_ui/avatar/avatar_fallback.rb +3 -0
- data/lib/ruby_ui/avatar/avatar_image.rb +4 -0
- data/lib/ruby_ui/base.rb +6 -0
- data/lib/ruby_ui/calendar/calendar.rb +3 -1
- data/lib/ruby_ui/calendar/calendar_controller.js +66 -7
- data/lib/ruby_ui/calendar/calendar_days.rb +20 -0
- data/lib/ruby_ui/calendar/calendar_docs.rb +9 -0
- data/lib/ruby_ui/combobox/combobox_badge.rb +17 -0
- data/lib/ruby_ui/combobox/combobox_badge_trigger.rb +47 -0
- data/lib/ruby_ui/combobox/combobox_clear_button.rb +40 -0
- data/lib/ruby_ui/combobox/combobox_controller.js +4 -2
- data/lib/ruby_ui/combobox/combobox_docs.rb +199 -64
- data/lib/ruby_ui/combobox/combobox_input_trigger.rb +64 -0
- data/lib/ruby_ui/combobox/combobox_item_indicator.rb +30 -0
- data/lib/ruby_ui/command/command_controller.js +10 -19
- data/lib/ruby_ui/command/command_dialog.rb +4 -1
- data/lib/ruby_ui/command/command_dialog_content.rb +2 -2
- data/lib/ruby_ui/command/command_dialog_controller.js +34 -0
- data/lib/ruby_ui/command/command_dialog_trigger.rb +2 -2
- data/lib/ruby_ui/data_table/data_table.rb +29 -0
- data/lib/ruby_ui/data_table/data_table_bulk_actions.rb +18 -0
- data/lib/ruby_ui/data_table/data_table_column_toggle.rb +62 -0
- data/lib/ruby_ui/data_table/data_table_column_visibility_controller.js +14 -0
- data/lib/ruby_ui/data_table/data_table_controller.js +57 -0
- data/lib/ruby_ui/data_table/data_table_docs.rb +180 -0
- data/lib/ruby_ui/data_table/data_table_expand_toggle.rb +53 -0
- data/lib/ruby_ui/data_table/data_table_form.rb +39 -0
- data/lib/ruby_ui/data_table/data_table_kaminari_adapter.rb +17 -0
- data/lib/ruby_ui/data_table/data_table_manual_adapter.rb +17 -0
- data/lib/ruby_ui/data_table/data_table_pagination.rb +100 -0
- data/lib/ruby_ui/data_table/data_table_pagination_bar.rb +15 -0
- data/lib/ruby_ui/data_table/data_table_pagy_adapter.rb +17 -0
- data/lib/ruby_ui/data_table/data_table_per_page_select.rb +35 -0
- data/lib/ruby_ui/data_table/data_table_row_checkbox.rb +30 -0
- data/lib/ruby_ui/data_table/data_table_search.rb +57 -0
- data/lib/ruby_ui/data_table/data_table_search_controller.js +62 -0
- data/lib/ruby_ui/data_table/data_table_select_all_checkbox.rb +21 -0
- data/lib/ruby_ui/data_table/data_table_selection_summary.rb +25 -0
- data/lib/ruby_ui/data_table/data_table_sort_head.rb +112 -0
- data/lib/ruby_ui/data_table/data_table_toolbar.rb +15 -0
- data/lib/ruby_ui/date_picker/date_picker.rb +85 -0
- data/lib/ruby_ui/date_picker/date_picker_docs.rb +23 -0
- data/lib/ruby_ui/native_select/native_select.rb +39 -0
- data/lib/ruby_ui/native_select/native_select_docs.rb +83 -0
- data/lib/ruby_ui/native_select/native_select_group.rb +15 -0
- data/lib/ruby_ui/native_select/native_select_icon.rb +39 -0
- data/lib/ruby_ui/native_select/native_select_option.rb +15 -0
- data/lib/ruby_ui/select/select_value.rb +2 -1
- data/lib/ruby_ui/sheet/sheet.rb +9 -1
- data/lib/ruby_ui/sheet/sheet_controller.js +6 -0
- data/lib/ruby_ui/theme_toggle/theme_toggle.rb +14 -2
- data/lib/ruby_ui/theme_toggle/theme_toggle_controller.js +27 -19
- data/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb +12 -42
- data/lib/ruby_ui/toast/toast.rb +18 -0
- data/lib/ruby_ui/toast/toast_action.rb +27 -0
- data/lib/ruby_ui/toast/toast_cancel.rb +27 -0
- data/lib/ruby_ui/toast/toast_close.rb +40 -0
- data/lib/ruby_ui/toast/toast_controller.js +151 -0
- data/lib/ruby_ui/toast/toast_description.rb +18 -0
- data/lib/ruby_ui/toast/toast_docs.rb +12 -0
- data/lib/ruby_ui/toast/toast_icon.rb +65 -0
- data/lib/ruby_ui/toast/toast_item.rb +72 -0
- data/lib/ruby_ui/toast/toast_region.rb +124 -0
- data/lib/ruby_ui/toast/toast_title.rb +18 -0
- data/lib/ruby_ui/toast/toaster_controller.js +306 -0
- data/lib/ruby_ui/toggle/toggle.rb +101 -0
- data/lib/ruby_ui/toggle/toggle_controller.js +33 -0
- data/lib/ruby_ui/toggle_group/toggle_group.rb +119 -0
- data/lib/ruby_ui/toggle_group/toggle_group_controller.js +126 -0
- data/lib/ruby_ui/toggle_group/toggle_group_item.rb +67 -0
- data/lib/ruby_ui/tooltip/tooltip_content.rb +12 -5
- data/lib/ruby_ui/tooltip/tooltip_controller.js +58 -22
- data/lib/ruby_ui/tooltip/tooltip_docs.rb +13 -0
- data/lib/ruby_ui/tooltip/tooltip_trigger.rb +10 -3
- data/lib/ruby_ui.rb +3 -1
- metadata +66 -10
- data/lib/ruby_ui/theme_toggle/set_dark_mode.rb +0 -16
- data/lib/ruby_ui/theme_toggle/set_light_mode.rb +0 -16
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
// Module-level map survives controller disconnect/connect across Turbo Frame swaps.
|
|
4
|
+
// Keyed by the search form's action URL.
|
|
5
|
+
const PENDING_FOCUS = new Map();
|
|
6
|
+
|
|
7
|
+
export default class extends Controller {
|
|
8
|
+
static values = { delay: { type: Number, default: 300 } };
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this.timer = null;
|
|
12
|
+
this.beforeFrameRender = this.captureBeforeRender.bind(this);
|
|
13
|
+
document.addEventListener("turbo:before-frame-render", this.beforeFrameRender);
|
|
14
|
+
// New instance after a Turbo Frame swap — check for captured state.
|
|
15
|
+
this.restoreIfPending();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
disconnect() {
|
|
19
|
+
clearTimeout(this.timer);
|
|
20
|
+
document.removeEventListener("turbo:before-frame-render", this.beforeFrameRender);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
submit(event) {
|
|
24
|
+
if (event && event.type !== "input") return;
|
|
25
|
+
clearTimeout(this.timer);
|
|
26
|
+
if (this.delayValue <= 0) return;
|
|
27
|
+
this.timer = setTimeout(() => this.element.requestSubmit(), this.delayValue);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
captureBeforeRender() {
|
|
31
|
+
const input = this.input();
|
|
32
|
+
if (!input || document.activeElement !== input) return;
|
|
33
|
+
PENDING_FOCUS.set(this.key(), {
|
|
34
|
+
selectionStart: input.selectionStart,
|
|
35
|
+
selectionEnd: input.selectionEnd
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
restoreIfPending() {
|
|
40
|
+
const state = PENDING_FOCUS.get(this.key());
|
|
41
|
+
if (!state) return;
|
|
42
|
+
PENDING_FOCUS.delete(this.key());
|
|
43
|
+
const input = this.input();
|
|
44
|
+
if (!input) return;
|
|
45
|
+
input.focus();
|
|
46
|
+
const len = input.value.length;
|
|
47
|
+
try {
|
|
48
|
+
input.setSelectionRange(
|
|
49
|
+
Math.min(state.selectionStart ?? len, len),
|
|
50
|
+
Math.min(state.selectionEnd ?? len, len)
|
|
51
|
+
);
|
|
52
|
+
} catch (e) {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
input() {
|
|
56
|
+
return this.element.querySelector('input[type="search"]');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
key() {
|
|
60
|
+
return this.element.action || "_";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class DataTableSelectAllCheckbox < Base
|
|
5
|
+
def view_template
|
|
6
|
+
render RubyUI::Checkbox.new(**attrs)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def default_attrs
|
|
12
|
+
{
|
|
13
|
+
aria_label: "Select all",
|
|
14
|
+
data: {
|
|
15
|
+
"ruby-ui--data-table-target": "selectAll",
|
|
16
|
+
action: "change->ruby-ui--data-table#toggleAll"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class DataTableSelectionSummary < Base
|
|
5
|
+
def initialize(total_on_page: 0, **attrs)
|
|
6
|
+
@total_on_page = total_on_page
|
|
7
|
+
super(**attrs)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def view_template
|
|
11
|
+
div(**attrs) do
|
|
12
|
+
plain "0 of #{@total_on_page} row(s) selected."
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def default_attrs
|
|
19
|
+
{
|
|
20
|
+
class: "text-sm text-muted-foreground",
|
|
21
|
+
data: {"ruby-ui--data-table-target": "selectionSummary"}
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
|
|
5
|
+
module RubyUI
|
|
6
|
+
class DataTableSortHead < Base
|
|
7
|
+
def initialize(column_key:, label:, sort: nil, direction: nil, sort_param: "sort", direction_param: "direction", page_param: "page", path: "", query: {}, **attrs)
|
|
8
|
+
@column_key = column_key
|
|
9
|
+
@label = label
|
|
10
|
+
@sort = sort
|
|
11
|
+
@direction = direction
|
|
12
|
+
@sort_param = sort_param
|
|
13
|
+
@direction_param = direction_param
|
|
14
|
+
@page_param = page_param
|
|
15
|
+
@path = path
|
|
16
|
+
@query = query.to_h.transform_keys(&:to_s)
|
|
17
|
+
super(**attrs)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def view_template
|
|
21
|
+
render RubyUI::TableHead.new(class: "text-foreground whitespace-nowrap", **attrs) do
|
|
22
|
+
a(href: sort_href, class: "inline-flex items-center gap-1 text-inherit no-underline hover:text-foreground transition-colors") do
|
|
23
|
+
plain @label
|
|
24
|
+
sort_icon
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def current_direction
|
|
32
|
+
(@sort.to_s == @column_key.to_s) ? @direction : nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def next_params
|
|
36
|
+
next_dir = {nil => "asc", "asc" => "desc", "desc" => nil}[current_direction]
|
|
37
|
+
base = @query.except(@sort_param, @direction_param, @page_param)
|
|
38
|
+
next_dir ? base.merge(@sort_param => @column_key.to_s, @direction_param => next_dir) : base
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def sort_href
|
|
42
|
+
qs = build_query(next_params)
|
|
43
|
+
qs.empty? ? @path : "#{@path}?#{qs}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def build_query(hash)
|
|
47
|
+
hash.flat_map { |k, v|
|
|
48
|
+
Array(v).map { |val| "#{CGI.escape(k.to_s)}=#{CGI.escape(val.to_s)}" }
|
|
49
|
+
}.join("&")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def sort_icon
|
|
53
|
+
icon_name = case current_direction
|
|
54
|
+
when "asc" then :chevron_up
|
|
55
|
+
when "desc" then :chevron_down
|
|
56
|
+
else :chevrons_up_down
|
|
57
|
+
end
|
|
58
|
+
icon_class = current_direction ? "inline-block w-3 h-3" : "inline-block w-3 h-3 opacity-30"
|
|
59
|
+
render_sort_svg(icon_name, icon_class)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_sort_svg(icon_name, icon_class)
|
|
63
|
+
case icon_name
|
|
64
|
+
when :chevron_up
|
|
65
|
+
# chevron-up: polyline pointing up
|
|
66
|
+
svg(
|
|
67
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
68
|
+
width: "12",
|
|
69
|
+
height: "12",
|
|
70
|
+
viewBox: "0 0 24 24",
|
|
71
|
+
fill: "none",
|
|
72
|
+
stroke: "currentColor",
|
|
73
|
+
stroke_width: "2",
|
|
74
|
+
stroke_linecap: "round",
|
|
75
|
+
stroke_linejoin: "round",
|
|
76
|
+
class: icon_class
|
|
77
|
+
) { |s| s.polyline(points: "18 15 12 9 6 15") }
|
|
78
|
+
when :chevron_down
|
|
79
|
+
# chevron-down: polyline pointing down
|
|
80
|
+
svg(
|
|
81
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
82
|
+
width: "12",
|
|
83
|
+
height: "12",
|
|
84
|
+
viewBox: "0 0 24 24",
|
|
85
|
+
fill: "none",
|
|
86
|
+
stroke: "currentColor",
|
|
87
|
+
stroke_width: "2",
|
|
88
|
+
stroke_linecap: "round",
|
|
89
|
+
stroke_linejoin: "round",
|
|
90
|
+
class: icon_class
|
|
91
|
+
) { |s| s.polyline(points: "6 9 12 15 18 9") }
|
|
92
|
+
else
|
|
93
|
+
# chevrons-up-down
|
|
94
|
+
svg(
|
|
95
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
96
|
+
width: "12",
|
|
97
|
+
height: "12",
|
|
98
|
+
viewBox: "0 0 24 24",
|
|
99
|
+
fill: "none",
|
|
100
|
+
stroke: "currentColor",
|
|
101
|
+
stroke_width: "2",
|
|
102
|
+
stroke_linecap: "round",
|
|
103
|
+
stroke_linejoin: "round",
|
|
104
|
+
class: icon_class
|
|
105
|
+
) do |s|
|
|
106
|
+
s.polyline(points: "8 15 12 19 16 15")
|
|
107
|
+
s.polyline(points: "8 9 12 5 16 9")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module RubyUI
|
|
6
|
+
class DatePicker < Base
|
|
7
|
+
def initialize(
|
|
8
|
+
id: nil,
|
|
9
|
+
name: nil,
|
|
10
|
+
label: "Select a date",
|
|
11
|
+
value: nil,
|
|
12
|
+
placeholder: "Select a date",
|
|
13
|
+
selected_date: value,
|
|
14
|
+
date_format: "yyyy-MM-dd",
|
|
15
|
+
popover_options: {},
|
|
16
|
+
input_attrs: {},
|
|
17
|
+
calendar_attrs: {},
|
|
18
|
+
trigger_attrs: {},
|
|
19
|
+
content_attrs: {},
|
|
20
|
+
**attrs
|
|
21
|
+
)
|
|
22
|
+
@id = id || "date-picker-#{SecureRandom.hex(4)}"
|
|
23
|
+
@name = name
|
|
24
|
+
@label = label
|
|
25
|
+
@value = value || selected_date&.to_s
|
|
26
|
+
@placeholder = placeholder
|
|
27
|
+
@selected_date = selected_date
|
|
28
|
+
@date_format = date_format
|
|
29
|
+
@popover_options = {trigger: "click"}.merge(popover_options)
|
|
30
|
+
@input_attrs = input_attrs
|
|
31
|
+
@calendar_attrs = calendar_attrs
|
|
32
|
+
@trigger_attrs = trigger_attrs
|
|
33
|
+
@content_attrs = content_attrs
|
|
34
|
+
super(**attrs)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def view_template
|
|
38
|
+
div(**attrs) do
|
|
39
|
+
RubyUI.Popover(options: @popover_options) do
|
|
40
|
+
RubyUI.PopoverTrigger(**trigger_attrs) do
|
|
41
|
+
div(class: "grid w-full max-w-sm items-center gap-1.5") do
|
|
42
|
+
label(for: @id) { @label } if @label
|
|
43
|
+
RubyUI.Input(**input_attrs)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
RubyUI.PopoverContent(**content_attrs) do
|
|
47
|
+
RubyUI.Calendar(input_id: "##{@id}", selected_date: @selected_date, date_format: @date_format, **calendar_attrs)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def default_attrs
|
|
56
|
+
{
|
|
57
|
+
class: "space-y-4 w-[260px]"
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def trigger_attrs
|
|
62
|
+
mix({class: "w-full"}, @trigger_attrs)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def input_attrs
|
|
66
|
+
mix({
|
|
67
|
+
type: "string",
|
|
68
|
+
placeholder: @placeholder,
|
|
69
|
+
id: @id,
|
|
70
|
+
name: @name,
|
|
71
|
+
value: @value,
|
|
72
|
+
data_controller: "ruby-ui--calendar-input",
|
|
73
|
+
class: "rounded-md border shadow"
|
|
74
|
+
}.compact, @input_attrs)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def calendar_attrs
|
|
78
|
+
mix({}, @calendar_attrs)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def content_attrs
|
|
82
|
+
mix({}, @content_attrs)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Views::Docs::DatePicker < Views::Base
|
|
4
|
+
def view_template
|
|
5
|
+
component = "DatePicker"
|
|
6
|
+
|
|
7
|
+
div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
|
|
8
|
+
render Docs::Header.new(title: "Date Picker", description: "A date picker component with input.")
|
|
9
|
+
|
|
10
|
+
Heading(level: 2) { "Usage" }
|
|
11
|
+
|
|
12
|
+
render Docs::VisualCodeExample.new(title: "Single Date", context: self) do
|
|
13
|
+
<<~RUBY
|
|
14
|
+
DatePicker(id: "date")
|
|
15
|
+
RUBY
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
render Components::ComponentSetup::Tabs.new(component_name: component)
|
|
19
|
+
|
|
20
|
+
render Docs::ComponentsTable.new(component_files(component))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class NativeSelect < Base
|
|
5
|
+
def initialize(size: :default, **attrs)
|
|
6
|
+
@size = size
|
|
7
|
+
super(**attrs)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def view_template(&block)
|
|
11
|
+
div(
|
|
12
|
+
class: "group/native-select relative w-fit has-[select:disabled]:opacity-50"
|
|
13
|
+
) do
|
|
14
|
+
select(**attrs, &block)
|
|
15
|
+
render RubyUI::NativeSelectIcon.new
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def default_attrs
|
|
22
|
+
{
|
|
23
|
+
data: {
|
|
24
|
+
ruby_ui__form_field_target: "input",
|
|
25
|
+
action: "change->ruby-ui--form-field#onChange invalid->ruby-ui--form-field#onInvalid"
|
|
26
|
+
},
|
|
27
|
+
class: [
|
|
28
|
+
"border-border bg-transparent text-sm w-full min-w-0 appearance-none rounded-md border py-1 pr-8 pl-2.5 shadow-xs transition-[color,box-shadow] outline-none select-none ring-0 ring-ring/0",
|
|
29
|
+
"placeholder:text-muted-foreground",
|
|
30
|
+
"selection:bg-primary selection:text-primary-foreground",
|
|
31
|
+
"focus-visible:outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-2",
|
|
32
|
+
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
33
|
+
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive aria-invalid:ring-2",
|
|
34
|
+
(@size == :sm) ? "h-7 rounded-md py-0.5" : "h-9"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Views::Docs::NativeSelect < Views::Base
|
|
4
|
+
def view_template
|
|
5
|
+
component = "NativeSelect"
|
|
6
|
+
|
|
7
|
+
div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
|
|
8
|
+
render Docs::Header.new(title: "Native Select", description: "A styled native HTML select element with consistent design system integration.")
|
|
9
|
+
|
|
10
|
+
Heading(level: 2) { "Usage" }
|
|
11
|
+
|
|
12
|
+
render Docs::VisualCodeExample.new(title: "Default", context: self) do
|
|
13
|
+
<<~RUBY
|
|
14
|
+
div(class: "grid w-full max-w-sm items-center gap-1.5") do
|
|
15
|
+
NativeSelect do
|
|
16
|
+
NativeSelectOption(value: "") { "Select a fruit" }
|
|
17
|
+
NativeSelectOption(value: "apple") { "Apple" }
|
|
18
|
+
NativeSelectOption(value: "banana") { "Banana" }
|
|
19
|
+
NativeSelectOption(value: "blueberry") { "Blueberry" }
|
|
20
|
+
NativeSelectOption(value: "pineapple") { "Pineapple" }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
RUBY
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
render Docs::VisualCodeExample.new(title: "Groups", description: "Use NativeSelectGroup to organize options into categories.", context: self) do
|
|
27
|
+
<<~RUBY
|
|
28
|
+
div(class: "grid w-full max-w-sm items-center gap-1.5") do
|
|
29
|
+
NativeSelect do
|
|
30
|
+
NativeSelectOption(value: "") { "Select a department" }
|
|
31
|
+
NativeSelectGroup(label: "Engineering") do
|
|
32
|
+
NativeSelectOption(value: "frontend") { "Frontend" }
|
|
33
|
+
NativeSelectOption(value: "backend") { "Backend" }
|
|
34
|
+
NativeSelectOption(value: "devops") { "DevOps" }
|
|
35
|
+
end
|
|
36
|
+
NativeSelectGroup(label: "Sales") do
|
|
37
|
+
NativeSelectOption(value: "account_executive") { "Account Executive" }
|
|
38
|
+
NativeSelectOption(value: "sales_development") { "Sales Development" }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
RUBY
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
render Docs::VisualCodeExample.new(title: "Disabled", description: "Add the disabled attribute to the NativeSelect component to disable the select.", context: self) do
|
|
46
|
+
<<~RUBY
|
|
47
|
+
div(class: "grid w-full max-w-sm items-center gap-1.5") do
|
|
48
|
+
NativeSelect(disabled: true) do
|
|
49
|
+
NativeSelectOption(value: "") { "Select a fruit" }
|
|
50
|
+
NativeSelectOption(value: "apple") { "Apple" }
|
|
51
|
+
NativeSelectOption(value: "banana") { "Banana" }
|
|
52
|
+
NativeSelectOption(value: "blueberry") { "Blueberry" }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
RUBY
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
render Docs::VisualCodeExample.new(title: "Invalid", description: "Use aria-invalid to show validation errors.", context: self) do
|
|
59
|
+
<<~RUBY
|
|
60
|
+
div(class: "grid w-full max-w-sm items-center gap-1.5") do
|
|
61
|
+
NativeSelect(aria: {invalid: "true"}) do
|
|
62
|
+
NativeSelectOption(value: "") { "Select a fruit" }
|
|
63
|
+
NativeSelectOption(value: "apple") { "Apple" }
|
|
64
|
+
NativeSelectOption(value: "banana") { "Banana" }
|
|
65
|
+
NativeSelectOption(value: "blueberry") { "Blueberry" }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
RUBY
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
Heading(level: 2) { "Native Select vs Select" }
|
|
72
|
+
|
|
73
|
+
div(class: "space-y-2 text-sm text-muted-foreground") do
|
|
74
|
+
p { "NativeSelect: Choose for native browser behavior, superior performance, or mobile-optimized dropdowns." }
|
|
75
|
+
p { "Select: Choose for custom styling, animations, or complex interactions." }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
render Components::ComponentSetup::Tabs.new(component_name: component)
|
|
79
|
+
|
|
80
|
+
render Docs::ComponentsTable.new(component_files(component))
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class NativeSelectIcon < Base
|
|
5
|
+
def view_template(&block)
|
|
6
|
+
span(**attrs) do
|
|
7
|
+
if block
|
|
8
|
+
block.call
|
|
9
|
+
else
|
|
10
|
+
icon
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def icon
|
|
18
|
+
svg(
|
|
19
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
20
|
+
viewbox: "0 0 24 24",
|
|
21
|
+
fill: "none",
|
|
22
|
+
stroke: "currentColor",
|
|
23
|
+
stroke_width: "2",
|
|
24
|
+
stroke_linecap: "round",
|
|
25
|
+
stroke_linejoin: "round",
|
|
26
|
+
class: "size-4",
|
|
27
|
+
aria_hidden: "true"
|
|
28
|
+
) do |s|
|
|
29
|
+
s.path(d: "m6 9 6 6 6-6")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def default_attrs
|
|
34
|
+
{
|
|
35
|
+
class: "text-muted-foreground pointer-events-none absolute top-1/2 right-2.5 -translate-y-1/2 select-none"
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/ruby_ui/sheet/sheet.rb
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyUI
|
|
4
4
|
class Sheet < Base
|
|
5
|
+
def initialize(open: false, **attrs)
|
|
6
|
+
@open = open
|
|
7
|
+
super(**attrs)
|
|
8
|
+
end
|
|
9
|
+
|
|
5
10
|
def view_template(&)
|
|
6
11
|
div(**attrs, &)
|
|
7
12
|
end
|
|
@@ -10,7 +15,10 @@ module RubyUI
|
|
|
10
15
|
|
|
11
16
|
def default_attrs
|
|
12
17
|
{
|
|
13
|
-
data: {
|
|
18
|
+
data: {
|
|
19
|
+
controller: "ruby-ui--sheet",
|
|
20
|
+
ruby_ui__sheet_open_value: @open.to_s
|
|
21
|
+
}
|
|
14
22
|
}
|
|
15
23
|
end
|
|
16
24
|
end
|
|
@@ -3,6 +3,12 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
3
3
|
export default class extends Controller {
|
|
4
4
|
static targets = ["content"]
|
|
5
5
|
|
|
6
|
+
static values = { open: false }
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
if (this.openValue) this.open()
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
open() {
|
|
7
13
|
document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML)
|
|
8
14
|
}
|
|
@@ -2,8 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyUI
|
|
4
4
|
class ThemeToggle < Base
|
|
5
|
-
def view_template(&)
|
|
6
|
-
|
|
5
|
+
def view_template(&block)
|
|
6
|
+
RubyUI.Toggle(
|
|
7
|
+
variant: :default,
|
|
8
|
+
size: :default,
|
|
9
|
+
aria: {label: "Toggle theme"},
|
|
10
|
+
wrapper: {
|
|
11
|
+
data: {
|
|
12
|
+
controller: "ruby-ui--theme-toggle",
|
|
13
|
+
action: "ruby-ui--toggle:change->ruby-ui--theme-toggle#apply"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
**attrs,
|
|
17
|
+
&block
|
|
18
|
+
)
|
|
7
19
|
end
|
|
8
20
|
end
|
|
9
21
|
end
|
|
@@ -1,30 +1,38 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
|
+
// Connects to data-controller="ruby-ui--theme-toggle"
|
|
4
|
+
// Sits on the same wrapper as ruby-ui--toggle. Listens for the toggle's
|
|
5
|
+
// ruby-ui--toggle:change event. pressed = dark mode.
|
|
3
6
|
export default class extends Controller {
|
|
4
|
-
|
|
5
|
-
this.
|
|
7
|
+
connect() {
|
|
8
|
+
this.applyTheme(this.currentTheme())
|
|
6
9
|
}
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} else {
|
|
14
|
-
document.documentElement.classList.remove('dark')
|
|
15
|
-
document.documentElement.classList.add('light')
|
|
16
|
-
}
|
|
11
|
+
apply(event) {
|
|
12
|
+
const pressed = event.detail?.pressed
|
|
13
|
+
const theme = pressed ? "dark" : "light"
|
|
14
|
+
localStorage.theme = theme
|
|
15
|
+
this.applyTheme(theme)
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
localStorage.theme
|
|
22
|
-
|
|
18
|
+
currentTheme() {
|
|
19
|
+
if (localStorage.theme === "dark") return "dark"
|
|
20
|
+
if (localStorage.theme === "light") return "light"
|
|
21
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
applyTheme(theme) {
|
|
25
|
+
const html = document.documentElement
|
|
26
|
+
if (theme === "dark") {
|
|
27
|
+
html.classList.add("dark")
|
|
28
|
+
html.classList.remove("light")
|
|
29
|
+
} else {
|
|
30
|
+
html.classList.add("light")
|
|
31
|
+
html.classList.remove("dark")
|
|
32
|
+
}
|
|
33
|
+
// Flip the sibling Toggle controller's pressed value; it will propagate
|
|
34
|
+
// aria-pressed / data-state to the button target.
|
|
35
|
+
const dark = theme === "dark"
|
|
36
|
+
this.element.setAttribute("data-ruby-ui--toggle-pressed-value", dark ? "true" : "false")
|
|
29
37
|
}
|
|
30
38
|
}
|