ruby_ui 1.1.0 → 1.2.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 +10 -0
- data/lib/generators/ruby_ui/install/install_generator.rb +1 -1
- data/lib/generators/ruby_ui/javascript_utils.rb +4 -0
- data/lib/ruby_ui/combobox/combobox.rb +7 -1
- 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_checkbox.rb +1 -7
- data/lib/ruby_ui/combobox/combobox_clear_button.rb +40 -0
- data/lib/ruby_ui/combobox/combobox_controller.js +243 -53
- 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.rb +5 -7
- data/lib/ruby_ui/combobox/combobox_item_indicator.rb +30 -0
- data/lib/ruby_ui/combobox/combobox_list_group.rb +1 -1
- data/lib/ruby_ui/combobox/combobox_popover.rb +0 -5
- data/lib/ruby_ui/combobox/combobox_radio.rb +1 -8
- data/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb +1 -7
- data/lib/ruby_ui/combobox/combobox_trigger.rb +19 -19
- 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/masked_input/masked_input.rb +11 -1
- data/lib/ruby_ui/masked_input/masked_input_controller.js +13 -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.rb +1 -1
- metadata +43 -3
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class DataTableSearch < Base
|
|
5
|
+
def initialize(path:, name: "search", value: nil, frame_id: nil, placeholder: "Search...", debounce: 300, preserved_params: {}, **attrs)
|
|
6
|
+
@path = path
|
|
7
|
+
@name = name
|
|
8
|
+
@value = value
|
|
9
|
+
@frame_id = frame_id
|
|
10
|
+
@placeholder = placeholder
|
|
11
|
+
@debounce = debounce
|
|
12
|
+
@preserved_params = preserved_params
|
|
13
|
+
super(**attrs)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def view_template
|
|
17
|
+
form_attrs = {method: "get", action: @path}
|
|
18
|
+
form_attrs[:data] = form_data
|
|
19
|
+
|
|
20
|
+
form(**attrs.merge(form_attrs)) do
|
|
21
|
+
render RubyUI::Input.new(
|
|
22
|
+
type: :search,
|
|
23
|
+
name: @name,
|
|
24
|
+
value: @value,
|
|
25
|
+
placeholder: @placeholder,
|
|
26
|
+
autocomplete: "off"
|
|
27
|
+
)
|
|
28
|
+
@preserved_params.each do |k, v|
|
|
29
|
+
next if v.nil? || (v.respond_to?(:empty?) && v.empty?)
|
|
30
|
+
next if k.to_s == @name
|
|
31
|
+
input(type: "hidden", name: k.to_s, value: v.to_s)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def debounce_enabled?
|
|
39
|
+
@debounce && @debounce.to_i > 0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def form_data
|
|
43
|
+
base = {}
|
|
44
|
+
base[:turbo_frame] = @frame_id if @frame_id
|
|
45
|
+
if debounce_enabled?
|
|
46
|
+
base[:controller] = "ruby-ui--data-table-search"
|
|
47
|
+
base[:"ruby-ui--data-table-search-delay-value"] = @debounce.to_i
|
|
48
|
+
base[:action] = "input->ruby-ui--data-table-search#submit"
|
|
49
|
+
end
|
|
50
|
+
base
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def default_attrs
|
|
54
|
+
{class: "max-w-sm flex-1"}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -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
|
|
@@ -2,8 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyUI
|
|
4
4
|
class MaskedInput < Base
|
|
5
|
+
def initialize(save_unmasked: false, **attrs)
|
|
6
|
+
@save_unmasked = save_unmasked
|
|
7
|
+
super(**attrs)
|
|
8
|
+
end
|
|
9
|
+
|
|
5
10
|
def view_template
|
|
6
|
-
|
|
11
|
+
if @save_unmasked
|
|
12
|
+
Input(type: "text", **attrs.merge(name: "#{attrs[:name]}-masked"))
|
|
13
|
+
input(type: "hidden", name: attrs[:name], value: attrs[:value])
|
|
14
|
+
else
|
|
15
|
+
Input(type: "text", **attrs)
|
|
16
|
+
end
|
|
7
17
|
end
|
|
8
18
|
|
|
9
19
|
private
|
|
@@ -5,5 +5,18 @@ import { MaskInput } from "maska";
|
|
|
5
5
|
export default class extends Controller {
|
|
6
6
|
connect() {
|
|
7
7
|
new MaskInput(this.element)
|
|
8
|
+
this.#boundSync = this.#sync.bind(this);
|
|
9
|
+
this.element.addEventListener("maska", this.#boundSync);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
disconnect() {
|
|
13
|
+
this.element.removeEventListener("maska", this.#boundSync);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#boundSync = null;
|
|
17
|
+
|
|
18
|
+
#sync(event) {
|
|
19
|
+
const hidden = this.element.nextElementSibling;
|
|
20
|
+
if (hidden?.type === "hidden") hidden.value = event.detail.unmasked;
|
|
8
21
|
}
|
|
9
22
|
}
|
|
@@ -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.rb
CHANGED
metadata
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_ui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- George Kettle
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-04-29 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: phlex
|
|
14
15
|
requirement: !ruby/object:Gem::Requirement
|
|
15
16
|
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.1'
|
|
16
20
|
- - ">="
|
|
17
21
|
- !ruby/object:Gem::Version
|
|
18
22
|
version: 2.1.2
|
|
@@ -20,6 +24,9 @@ dependencies:
|
|
|
20
24
|
prerelease: false
|
|
21
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
26
|
requirements:
|
|
27
|
+
- - "~>"
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '2.1'
|
|
23
30
|
- - ">="
|
|
24
31
|
- !ruby/object:Gem::Version
|
|
25
32
|
version: 2.1.2
|
|
@@ -200,11 +207,16 @@ files:
|
|
|
200
207
|
- lib/ruby_ui/collapsible/collapsible_docs.rb
|
|
201
208
|
- lib/ruby_ui/collapsible/collapsible_trigger.rb
|
|
202
209
|
- lib/ruby_ui/combobox/combobox.rb
|
|
210
|
+
- lib/ruby_ui/combobox/combobox_badge.rb
|
|
211
|
+
- lib/ruby_ui/combobox/combobox_badge_trigger.rb
|
|
203
212
|
- lib/ruby_ui/combobox/combobox_checkbox.rb
|
|
213
|
+
- lib/ruby_ui/combobox/combobox_clear_button.rb
|
|
204
214
|
- lib/ruby_ui/combobox/combobox_controller.js
|
|
205
215
|
- lib/ruby_ui/combobox/combobox_docs.rb
|
|
206
216
|
- lib/ruby_ui/combobox/combobox_empty_state.rb
|
|
217
|
+
- lib/ruby_ui/combobox/combobox_input_trigger.rb
|
|
207
218
|
- lib/ruby_ui/combobox/combobox_item.rb
|
|
219
|
+
- lib/ruby_ui/combobox/combobox_item_indicator.rb
|
|
208
220
|
- lib/ruby_ui/combobox/combobox_list.rb
|
|
209
221
|
- lib/ruby_ui/combobox/combobox_list_group.rb
|
|
210
222
|
- lib/ruby_ui/combobox/combobox_popover.rb
|
|
@@ -231,6 +243,27 @@ files:
|
|
|
231
243
|
- lib/ruby_ui/context_menu/context_menu_label.rb
|
|
232
244
|
- lib/ruby_ui/context_menu/context_menu_separator.rb
|
|
233
245
|
- lib/ruby_ui/context_menu/context_menu_trigger.rb
|
|
246
|
+
- lib/ruby_ui/data_table/data_table.rb
|
|
247
|
+
- lib/ruby_ui/data_table/data_table_bulk_actions.rb
|
|
248
|
+
- lib/ruby_ui/data_table/data_table_column_toggle.rb
|
|
249
|
+
- lib/ruby_ui/data_table/data_table_column_visibility_controller.js
|
|
250
|
+
- lib/ruby_ui/data_table/data_table_controller.js
|
|
251
|
+
- lib/ruby_ui/data_table/data_table_docs.rb
|
|
252
|
+
- lib/ruby_ui/data_table/data_table_expand_toggle.rb
|
|
253
|
+
- lib/ruby_ui/data_table/data_table_form.rb
|
|
254
|
+
- lib/ruby_ui/data_table/data_table_kaminari_adapter.rb
|
|
255
|
+
- lib/ruby_ui/data_table/data_table_manual_adapter.rb
|
|
256
|
+
- lib/ruby_ui/data_table/data_table_pagination.rb
|
|
257
|
+
- lib/ruby_ui/data_table/data_table_pagination_bar.rb
|
|
258
|
+
- lib/ruby_ui/data_table/data_table_pagy_adapter.rb
|
|
259
|
+
- lib/ruby_ui/data_table/data_table_per_page_select.rb
|
|
260
|
+
- lib/ruby_ui/data_table/data_table_row_checkbox.rb
|
|
261
|
+
- lib/ruby_ui/data_table/data_table_search.rb
|
|
262
|
+
- lib/ruby_ui/data_table/data_table_search_controller.js
|
|
263
|
+
- lib/ruby_ui/data_table/data_table_select_all_checkbox.rb
|
|
264
|
+
- lib/ruby_ui/data_table/data_table_selection_summary.rb
|
|
265
|
+
- lib/ruby_ui/data_table/data_table_sort_head.rb
|
|
266
|
+
- lib/ruby_ui/data_table/data_table_toolbar.rb
|
|
234
267
|
- lib/ruby_ui/dialog/dialog.rb
|
|
235
268
|
- lib/ruby_ui/dialog/dialog_content.rb
|
|
236
269
|
- lib/ruby_ui/dialog/dialog_controller.js
|
|
@@ -274,6 +307,11 @@ files:
|
|
|
274
307
|
- lib/ruby_ui/masked_input/masked_input.rb
|
|
275
308
|
- lib/ruby_ui/masked_input/masked_input_controller.js
|
|
276
309
|
- lib/ruby_ui/masked_input/masked_input_docs.rb
|
|
310
|
+
- lib/ruby_ui/native_select/native_select.rb
|
|
311
|
+
- lib/ruby_ui/native_select/native_select_docs.rb
|
|
312
|
+
- lib/ruby_ui/native_select/native_select_group.rb
|
|
313
|
+
- lib/ruby_ui/native_select/native_select_icon.rb
|
|
314
|
+
- lib/ruby_ui/native_select/native_select_option.rb
|
|
277
315
|
- lib/ruby_ui/pagination/pagination.rb
|
|
278
316
|
- lib/ruby_ui/pagination/pagination_content.rb
|
|
279
317
|
- lib/ruby_ui/pagination/pagination_docs.rb
|
|
@@ -383,6 +421,7 @@ homepage: https://rubygems.org/gems/ruby_ui
|
|
|
383
421
|
licenses:
|
|
384
422
|
- MIT
|
|
385
423
|
metadata: {}
|
|
424
|
+
post_install_message:
|
|
386
425
|
rdoc_options: []
|
|
387
426
|
require_paths:
|
|
388
427
|
- lib
|
|
@@ -397,7 +436,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
397
436
|
- !ruby/object:Gem::Version
|
|
398
437
|
version: '0'
|
|
399
438
|
requirements: []
|
|
400
|
-
rubygems_version: 3.
|
|
439
|
+
rubygems_version: 3.5.22
|
|
440
|
+
signing_key:
|
|
401
441
|
specification_version: 4
|
|
402
442
|
summary: RubyUI is a UI Component Library for Ruby developers.
|
|
403
443
|
test_files: []
|