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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/ruby_ui/component_generator.rb +5 -1
  3. data/lib/generators/ruby_ui/dependencies.yml +32 -0
  4. data/lib/generators/ruby_ui/install/install_generator.rb +1 -1
  5. data/lib/generators/ruby_ui/install/templates/tailwind.css.erb +1 -1
  6. data/lib/generators/ruby_ui/javascript_utils.rb +27 -6
  7. data/lib/ruby_ui/avatar/avatar.rb +3 -0
  8. data/lib/ruby_ui/avatar/avatar_controller.js +33 -0
  9. data/lib/ruby_ui/avatar/avatar_fallback.rb +3 -0
  10. data/lib/ruby_ui/avatar/avatar_image.rb +4 -0
  11. data/lib/ruby_ui/base.rb +6 -0
  12. data/lib/ruby_ui/calendar/calendar.rb +3 -1
  13. data/lib/ruby_ui/calendar/calendar_controller.js +66 -7
  14. data/lib/ruby_ui/calendar/calendar_days.rb +20 -0
  15. data/lib/ruby_ui/calendar/calendar_docs.rb +9 -0
  16. data/lib/ruby_ui/combobox/combobox_badge.rb +17 -0
  17. data/lib/ruby_ui/combobox/combobox_badge_trigger.rb +47 -0
  18. data/lib/ruby_ui/combobox/combobox_clear_button.rb +40 -0
  19. data/lib/ruby_ui/combobox/combobox_controller.js +4 -2
  20. data/lib/ruby_ui/combobox/combobox_docs.rb +199 -64
  21. data/lib/ruby_ui/combobox/combobox_input_trigger.rb +64 -0
  22. data/lib/ruby_ui/combobox/combobox_item_indicator.rb +30 -0
  23. data/lib/ruby_ui/command/command_controller.js +10 -19
  24. data/lib/ruby_ui/command/command_dialog.rb +4 -1
  25. data/lib/ruby_ui/command/command_dialog_content.rb +2 -2
  26. data/lib/ruby_ui/command/command_dialog_controller.js +34 -0
  27. data/lib/ruby_ui/command/command_dialog_trigger.rb +2 -2
  28. data/lib/ruby_ui/data_table/data_table.rb +29 -0
  29. data/lib/ruby_ui/data_table/data_table_bulk_actions.rb +18 -0
  30. data/lib/ruby_ui/data_table/data_table_column_toggle.rb +62 -0
  31. data/lib/ruby_ui/data_table/data_table_column_visibility_controller.js +14 -0
  32. data/lib/ruby_ui/data_table/data_table_controller.js +57 -0
  33. data/lib/ruby_ui/data_table/data_table_docs.rb +180 -0
  34. data/lib/ruby_ui/data_table/data_table_expand_toggle.rb +53 -0
  35. data/lib/ruby_ui/data_table/data_table_form.rb +39 -0
  36. data/lib/ruby_ui/data_table/data_table_kaminari_adapter.rb +17 -0
  37. data/lib/ruby_ui/data_table/data_table_manual_adapter.rb +17 -0
  38. data/lib/ruby_ui/data_table/data_table_pagination.rb +100 -0
  39. data/lib/ruby_ui/data_table/data_table_pagination_bar.rb +15 -0
  40. data/lib/ruby_ui/data_table/data_table_pagy_adapter.rb +17 -0
  41. data/lib/ruby_ui/data_table/data_table_per_page_select.rb +35 -0
  42. data/lib/ruby_ui/data_table/data_table_row_checkbox.rb +30 -0
  43. data/lib/ruby_ui/data_table/data_table_search.rb +57 -0
  44. data/lib/ruby_ui/data_table/data_table_search_controller.js +62 -0
  45. data/lib/ruby_ui/data_table/data_table_select_all_checkbox.rb +21 -0
  46. data/lib/ruby_ui/data_table/data_table_selection_summary.rb +25 -0
  47. data/lib/ruby_ui/data_table/data_table_sort_head.rb +112 -0
  48. data/lib/ruby_ui/data_table/data_table_toolbar.rb +15 -0
  49. data/lib/ruby_ui/date_picker/date_picker.rb +85 -0
  50. data/lib/ruby_ui/date_picker/date_picker_docs.rb +23 -0
  51. data/lib/ruby_ui/native_select/native_select.rb +39 -0
  52. data/lib/ruby_ui/native_select/native_select_docs.rb +83 -0
  53. data/lib/ruby_ui/native_select/native_select_group.rb +15 -0
  54. data/lib/ruby_ui/native_select/native_select_icon.rb +39 -0
  55. data/lib/ruby_ui/native_select/native_select_option.rb +15 -0
  56. data/lib/ruby_ui/select/select_value.rb +2 -1
  57. data/lib/ruby_ui/sheet/sheet.rb +9 -1
  58. data/lib/ruby_ui/sheet/sheet_controller.js +6 -0
  59. data/lib/ruby_ui/theme_toggle/theme_toggle.rb +14 -2
  60. data/lib/ruby_ui/theme_toggle/theme_toggle_controller.js +27 -19
  61. data/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb +12 -42
  62. data/lib/ruby_ui/toast/toast.rb +18 -0
  63. data/lib/ruby_ui/toast/toast_action.rb +27 -0
  64. data/lib/ruby_ui/toast/toast_cancel.rb +27 -0
  65. data/lib/ruby_ui/toast/toast_close.rb +40 -0
  66. data/lib/ruby_ui/toast/toast_controller.js +151 -0
  67. data/lib/ruby_ui/toast/toast_description.rb +18 -0
  68. data/lib/ruby_ui/toast/toast_docs.rb +12 -0
  69. data/lib/ruby_ui/toast/toast_icon.rb +65 -0
  70. data/lib/ruby_ui/toast/toast_item.rb +72 -0
  71. data/lib/ruby_ui/toast/toast_region.rb +124 -0
  72. data/lib/ruby_ui/toast/toast_title.rb +18 -0
  73. data/lib/ruby_ui/toast/toaster_controller.js +306 -0
  74. data/lib/ruby_ui/toggle/toggle.rb +101 -0
  75. data/lib/ruby_ui/toggle/toggle_controller.js +33 -0
  76. data/lib/ruby_ui/toggle_group/toggle_group.rb +119 -0
  77. data/lib/ruby_ui/toggle_group/toggle_group_controller.js +126 -0
  78. data/lib/ruby_ui/toggle_group/toggle_group_item.rb +67 -0
  79. data/lib/ruby_ui/tooltip/tooltip_content.rb +12 -5
  80. data/lib/ruby_ui/tooltip/tooltip_controller.js +58 -22
  81. data/lib/ruby_ui/tooltip/tooltip_docs.rb +13 -0
  82. data/lib/ruby_ui/tooltip/tooltip_trigger.rb +10 -3
  83. data/lib/ruby_ui.rb +3 -1
  84. metadata +66 -10
  85. data/lib/ruby_ui/theme_toggle/set_dark_mode.rb +0 -16
  86. 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,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class DataTableToolbar < Base
5
+ def view_template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {class: "flex items-center justify-between gap-2"}
13
+ end
14
+ end
15
+ 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,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class NativeSelectGroup < Base
5
+ def view_template(&)
6
+ optgroup(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {}
13
+ end
14
+ end
15
+ 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
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class NativeSelectOption < Base
5
+ def view_template(&)
6
+ option(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {}
13
+ end
14
+ end
15
+ end
@@ -9,7 +9,8 @@ module RubyUI
9
9
 
10
10
  def view_template(&block)
11
11
  span(**attrs) do
12
- block ? block.call : @placeholder
12
+ value = block ? block.call : @placeholder
13
+ value || @placeholder
13
14
  end
14
15
  end
15
16
 
@@ -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: {controller: "ruby-ui--sheet"}
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
- div(**attrs, &)
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
- initialize() {
5
- this.setTheme()
7
+ connect() {
8
+ this.applyTheme(this.currentTheme())
6
9
  }
7
10
 
8
- setTheme() {
9
- // On page load or when changing themes, best to add inline in `head` to avoid FOUC
10
- if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
11
- document.documentElement.classList.add('dark')
12
- document.documentElement.classList.remove('light')
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
- setLightTheme() {
20
- // Whenever the user explicitly chooses light mode
21
- localStorage.theme = 'light'
22
- this.setTheme()
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
- setDarkTheme() {
26
- // Whenever the user explicitly chooses dark mode
27
- localStorage.theme = 'dark'
28
- this.setTheme()
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
  }