lostcoords-ui 0.5.0 → 0.7.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +111 -49
  3. data/app/assets/tailwind/lostcoords/ui/card.css +5 -2
  4. data/app/assets/tailwind/lostcoords/ui/{disclosure.css → collapsible.css} +1 -1
  5. data/app/assets/tailwind/lostcoords/ui/components/code-block.css +5 -0
  6. data/app/assets/tailwind/lostcoords/ui/components/collapsible.css +5 -0
  7. data/app/assets/tailwind/lostcoords/ui/components/dialog.css +9 -0
  8. data/app/assets/tailwind/lostcoords/ui/components/emoji-picker.css +5 -0
  9. data/app/assets/tailwind/lostcoords/ui/components/menu.css +5 -0
  10. data/app/assets/tailwind/lostcoords/ui/components/prose.css +3 -0
  11. data/app/assets/tailwind/lostcoords/ui/components/table.css +3 -0
  12. data/app/assets/tailwind/lostcoords/ui/components/toast.css +5 -0
  13. data/app/assets/tailwind/lostcoords/ui/components/tooltip.css +4 -0
  14. data/app/assets/tailwind/lostcoords/ui/components.css +12 -25
  15. data/app/assets/tailwind/lostcoords/ui/core.css +21 -0
  16. data/app/assets/tailwind/lostcoords/ui/emoji-picker.css +45 -47
  17. data/app/assets/tailwind/lostcoords/ui/popover.css +0 -52
  18. data/app/assets/tailwind/lostcoords/ui/{reading.css → prose.css} +6 -0
  19. data/app/assets/tailwind/lostcoords/ui/sheet.css +3 -64
  20. data/app/assets/tailwind/lostcoords/ui/theme.css +16 -1
  21. data/app/assets/tailwind/lostcoords/ui/toast.css +23 -0
  22. data/app/assets/tailwind/lostcoords/ui/tooltip.css +12 -11
  23. data/app/assets/tailwind/lostcoords/ui/typography.css +1 -7
  24. data/app/helpers/lostcoords/ui/badge_helper.rb +2 -2
  25. data/app/helpers/lostcoords/ui/button_helper.rb +12 -6
  26. data/app/helpers/lostcoords/ui/choice_helper.rb +11 -7
  27. data/app/helpers/lostcoords/ui/collapsible_helper.rb +9 -0
  28. data/app/helpers/lostcoords/ui/dialog_helper.rb +10 -5
  29. data/app/helpers/lostcoords/ui/emoji_picker_helper.rb +6 -0
  30. data/app/helpers/lostcoords/ui/feedback_helper.rb +2 -2
  31. data/app/helpers/lostcoords/ui/form_helper.rb +4 -3
  32. data/app/helpers/lostcoords/ui/glass_helper.rb +1 -1
  33. data/app/helpers/lostcoords/ui/menu_helper.rb +37 -2
  34. data/app/helpers/lostcoords/ui/navigation_helper.rb +17 -4
  35. data/app/helpers/lostcoords/ui/render_attributes_helper.rb +17 -1
  36. data/app/helpers/lostcoords/ui/sheet_helper.rb +4 -0
  37. data/app/helpers/lostcoords/ui/structures_helper.rb +7 -95
  38. data/app/helpers/lostcoords/ui/toast_helper.rb +22 -0
  39. data/app/helpers/lostcoords/ui/{overlay_helper.rb → tooltip_helper.rb} +3 -3
  40. data/app/helpers/lostcoords/ui/typography_helper.rb +30 -0
  41. data/app/javascript/lostcoords/ui/controllers/lc_collapsible_controller.js +1 -1
  42. data/app/javascript/lostcoords/ui/controllers/lc_emoji_picker_controller.js +34 -6
  43. data/app/javascript/lostcoords/ui/controllers/lc_kbd_controller.js +12 -0
  44. data/app/javascript/lostcoords/ui/controllers/lc_menu_controller.js +23 -1
  45. data/app/javascript/lostcoords/ui/controllers/lc_selection_controller.js +8 -16
  46. data/app/javascript/lostcoords/ui/controllers/lc_sheet_controller.js +15 -3
  47. data/app/javascript/lostcoords/ui/controllers/lc_shortcut_controller.js +45 -0
  48. data/app/javascript/lostcoords/ui/controllers/lc_tabs_controller.js +18 -15
  49. data/app/javascript/lostcoords/ui/controllers/lc_toast_region_controller.js +62 -0
  50. data/app/javascript/lostcoords/ui/controllers/lc_tooltip_controller.js +46 -6
  51. data/app/javascript/lostcoords/ui/helpers/catalog_picker.js +72 -20
  52. data/app/javascript/lostcoords/ui/helpers/dismiss_stack.js +23 -0
  53. data/app/javascript/lostcoords/ui/helpers/floating_panel.js +6 -7
  54. data/app/javascript/lostcoords/ui/helpers/menu_keys.js +19 -18
  55. data/app/javascript/lostcoords/ui/helpers/roving_focus.js +175 -0
  56. data/app/javascript/lostcoords/ui/helpers/shortcuts.js +162 -0
  57. data/app/javascript/lostcoords/ui/helpers/top_layer.js +48 -10
  58. data/app/javascript/lostcoords/ui/index.js +6 -0
  59. data/app/views/lostcoords/ui/components/_emoji_picker.html.erb +27 -27
  60. data/app/views/lostcoords/ui/components/_emoji_picker_grid.html.erb +3 -3
  61. data/app/views/lostcoords/ui/components/_lc_code_block.html.erb +1 -1
  62. data/lib/lostcoords/ui/action_view.rb +1 -0
  63. data/lib/lostcoords/ui/builders.rb +2 -2
  64. data/lib/lostcoords/ui/form_builder.rb +55 -10
  65. data/lib/lostcoords/ui/stylesheets.rb +25 -0
  66. data/lib/lostcoords/ui/version.rb +1 -1
  67. data/lib/tasks/lostcoords_ui.rake +11 -0
  68. metadata +23 -6
  69. data/config/helper_renames.json +0 -111
  70. data/script/migrate_helpers.rb +0 -136
@@ -10,8 +10,8 @@ module Lostcoords::Ui
10
10
 
11
11
  def component?
12
12
  !name.to_s.end_with?("_class") &&
13
- !%i[lc_tw lc_sheet_data lc_alert_icon].include?(name) &&
14
- helper != "EmojiPickerHelper"
13
+ !%i[lc_tw lc_sheet_data lc_shortcut_data lc_alert_icon].include?(name) &&
14
+ (helper != "EmojiPickerHelper" || name == :lc_emoji_picker)
15
15
  end
16
16
  end
17
17
 
@@ -1,4 +1,13 @@
1
1
  module Lostcoords::Ui
2
+ # Rails wraps a field with errors in <div class="field_with_errors">. Controls this builder renders show
3
+ # errors themselves, so the wrapper is skipped while it renders; other forms keep Rails' behaviour.
4
+ module SkipsFieldErrorWrapper
5
+ def error_wrapping(html_tag)
6
+ @template_object.instance_variable_get(:@lc_skip_field_error_wrapper) ? html_tag : super
7
+ end
8
+ end
9
+ ActionView::Helpers::Tags::Base.prepend(SkipsFieldErrorWrapper)
10
+
2
11
  # Rails form builder that renders the library's controls and connects model errors to them.
3
12
  #
4
13
  # <%= lc_form_with model: @project do |form| %>
@@ -16,6 +25,21 @@ module Lostcoords::Ui
16
25
  date_field time_field datetime_field datetime_local_field month_field week_field
17
26
  ].freeze
18
27
 
28
+ # Every helper that renders a control, wrapped so Rails' error wrapper stays out of its markup.
29
+ WRAPPED = [ *field_helpers, :select, :collection_select, :grouped_collection_select, :time_zone_select,
30
+ :collection_check_boxes, :collection_radio_buttons, :switch, :choice ].uniq - %i[label fields_for fields hidden_field]
31
+
32
+ module WithoutFieldErrorWrapper
33
+ WRAPPED.each do |helper|
34
+ module_eval <<~RUBY, __FILE__, __LINE__ + 1
35
+ def #{helper}(...)
36
+ without_field_error_wrapper { super(...) }
37
+ end
38
+ RUBY
39
+ end
40
+ end
41
+ prepend WithoutFieldErrorWrapper
42
+
19
43
  INPUTS.each do |selector|
20
44
  define_method(selector) do |method, options = {}|
21
45
  size = control_size!(options)
@@ -33,6 +57,11 @@ module Lostcoords::Ui
33
57
  super(method, choices, options, control_options(method, html_options, "select", @template.lc_select_class(size: size)), &block)
34
58
  end
35
59
 
60
+ SELECT_OPTIONS = %i[include_blank prompt selected disabled].freeze
61
+
62
+ alias_method :rails_check_box, :check_box
63
+ private :rails_check_box
64
+
36
65
  def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
37
66
  super(method, control_options(method, options, "checkbox-input", @template.lc_checkbox_class), checked_value, unchecked_value)
38
67
  end
@@ -44,13 +73,14 @@ module Lostcoords::Ui
44
73
 
45
74
  # A checkbox drawn as a switch, with its label beside it.
46
75
  def switch(method, label: nil, hint: nil, checked_value: "1", unchecked_value: "0", **options)
47
- input = check_box(method, { role: "switch", class: @template.lc_switch_class(extra: options.delete(:class)), data: { slot: "switch-input" } }.merge(options), checked_value, unchecked_value)
76
+ input = rails_check_box(method, control_options(method, { role: "switch" }.merge(options), "switch-input", @template.lc_switch_class, described: true), checked_value, unchecked_value)
48
77
  choice_label(method, label, hint, "switch", input)
49
78
  end
50
79
 
51
80
  # A checkbox with its label beside it.
52
81
  def choice(method, label: nil, hint: nil, checked_value: "1", unchecked_value: "0", **options)
53
- choice_label(method, label, hint, "checkbox", check_box(method, options, checked_value, unchecked_value))
82
+ input = rails_check_box(method, control_options(method, options, "checkbox-input", @template.lc_checkbox_class, described: true), checked_value, unchecked_value)
83
+ choice_label(method, label, hint, "checkbox", input)
54
84
  end
55
85
 
56
86
  def label(method, text = nil, options = {}, &block)
@@ -68,36 +98,51 @@ module Lostcoords::Ui
68
98
  end
69
99
  variant = options.delete(:variant) || :primary
70
100
  size = options.delete(:size) || :md
71
- super(value, options.merge(class: @template.lc_button_class(variant: variant, size: size, extra: options[:class])))
101
+ data = { slot: "button", variant: variant, size: size }.merge(options.delete(:data) || {})
102
+ super(value, options.merge(class: @template.lc_button_class(variant: variant, size: size, extra: options[:class]), data: data))
72
103
  end
73
104
 
74
105
  # Label, control, hint and the attribute's errors in one field. `as:` names the control helper;
75
- # a block renders a custom control and receives its attributes to splat with `**control`.
76
- def field(method, label: nil, hint: nil, as: :text_field, **options, &block)
106
+ # `as: :select` takes `choices:` and Rails' select options such as `include_blank:`. A block renders
107
+ # a custom control and receives its attributes to splat with `**control`.
108
+ def field(method, label: nil, hint: nil, as: :text_field, choices: nil, **options, &block)
77
109
  @template.lc_field(label: label || human_name(method), id: field_id(method), hint: hint, error: errors_for(method)) do |control|
78
- if block
79
- @template.capture(control, &block)
110
+ next @template.capture(control, &block) if block
111
+
112
+ attributes = @template.send(:lc_attributes, options.except(*SELECT_OPTIONS), control.to_hash.except(:id))
113
+ if as.to_sym == :select
114
+ select(method, choices, options.slice(*SELECT_OPTIONS), attributes)
80
115
  else
81
- public_send(as, method, **options, **control.to_hash.except(:id))
116
+ public_send(as, method, attributes.merge(options.slice(*SELECT_OPTIONS)))
82
117
  end
83
118
  end
84
119
  end
85
120
 
86
121
  private
122
+ def without_field_error_wrapper
123
+ previous = @template.instance_variable_get(:@lc_skip_field_error_wrapper)
124
+ @template.instance_variable_set(:@lc_skip_field_error_wrapper, true)
125
+ yield
126
+ ensure
127
+ @template.instance_variable_set(:@lc_skip_field_error_wrapper, previous)
128
+ end
129
+
87
130
  def control_size!(options)
88
131
  options[:size].is_a?(Symbol) || options[:size].is_a?(String) && options[:size].match?(/\A[a-z]+\z/) ? options.delete(:size).to_sym : :md
89
132
  end
90
133
 
91
- def control_options(method, options, slot, classes)
134
+ # described: points aria-describedby at the error a choice label renders, as lc_field does for its control.
135
+ def control_options(method, options, slot, classes, described: false)
92
136
  options = options.dup
93
137
  aria = (options.delete(:aria) || {}).dup
94
138
  aria[:invalid] = true if errors_for(method).any? && !aria.key?(:invalid)
139
+ aria[:describedby] = [ aria[:describedby], "#{field_id(method)}-error" ].compact.join(" ") if described && errors_for(method).any?
95
140
  data = { slot: slot }.merge(options.delete(:data) || {})
96
141
  options.merge(class: @template.lc_tw(classes, options[:class]), aria: aria, data: data)
97
142
  end
98
143
 
99
144
  def choice_label(method, text, hint, slot, input)
100
- @template.send(:lc_choice_label, slot, text || human_name(method), hint, input, for: field_id(method))
145
+ @template.send(:lc_choice_label, slot, text || human_name(method), hint, input, errors: errors_for(method), error_id: "#{field_id(method)}-error", for: field_id(method))
101
146
  end
102
147
 
103
148
  def errors_for(method)
@@ -0,0 +1,25 @@
1
+ require "fileutils"
2
+ require "pathname"
3
+
4
+ module Lostcoords::Ui
5
+ # The Tailwind entries a host can import: the document defaults, core.css and one file per opt-in component.
6
+ module Stylesheets
7
+ ROOT = Pathname(File.expand_path("../../../app/assets/tailwind/lostcoords/ui", __dir__))
8
+
9
+ def self.entries
10
+ [ ROOT.join("base.css"), ROOT.join("core.css"), *ROOT.glob("components/*.css").sort ]
11
+ end
12
+
13
+ # tailwindcss-rails gives an engine one generated import, of engine.css. These proxies give every
14
+ # entry a stable path in the host, such as builds/tailwind/lostcoords_ui/components/dialog.css.
15
+ def self.write(builds)
16
+ directory = Pathname(builds).join("lostcoords_ui")
17
+ FileUtils.rm_rf(directory)
18
+ entries.each do |entry|
19
+ path = directory.join(entry.relative_path_from(ROOT))
20
+ FileUtils.mkdir_p(path.dirname)
21
+ path.write("/* Generated by lostcoords-ui before every Tailwind build; do not edit. */\n@import \"#{entry}\";\n")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,5 @@
1
1
  module Lostcoords
2
2
  module Ui
3
- VERSION = "0.5.0"
3
+ VERSION = "0.7.0"
4
4
  end
5
5
  end
@@ -13,3 +13,14 @@ namespace :lostcoords do
13
13
  end
14
14
  end
15
15
  end
16
+
17
+ # Runs with tailwindcss-rails' own engines step, before every tailwindcss:build and tailwindcss:watch.
18
+ # Apps without tailwindcss-rails import the gem's stylesheets by path instead.
19
+ if defined?(Tailwindcss::Engines)
20
+ namespace :tailwindcss do
21
+ task :engines do
22
+ require "lostcoords/ui/stylesheets"
23
+ Lostcoords::Ui::Stylesheets.write(Rails.root.join("app/assets/builds/tailwind"))
24
+ end
25
+ end
26
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lostcoords-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lost Coordinates
@@ -90,20 +90,31 @@ files:
90
90
  - app/assets/tailwind/lostcoords/ui/card.css
91
91
  - app/assets/tailwind/lostcoords/ui/choice.css
92
92
  - app/assets/tailwind/lostcoords/ui/code-block.css
93
+ - app/assets/tailwind/lostcoords/ui/collapsible.css
93
94
  - app/assets/tailwind/lostcoords/ui/components.css
95
+ - app/assets/tailwind/lostcoords/ui/components/code-block.css
96
+ - app/assets/tailwind/lostcoords/ui/components/collapsible.css
97
+ - app/assets/tailwind/lostcoords/ui/components/dialog.css
98
+ - app/assets/tailwind/lostcoords/ui/components/emoji-picker.css
99
+ - app/assets/tailwind/lostcoords/ui/components/menu.css
100
+ - app/assets/tailwind/lostcoords/ui/components/prose.css
101
+ - app/assets/tailwind/lostcoords/ui/components/table.css
102
+ - app/assets/tailwind/lostcoords/ui/components/toast.css
103
+ - app/assets/tailwind/lostcoords/ui/components/tooltip.css
94
104
  - app/assets/tailwind/lostcoords/ui/copy-button.css
105
+ - app/assets/tailwind/lostcoords/ui/core.css
95
106
  - app/assets/tailwind/lostcoords/ui/dialog.css
96
- - app/assets/tailwind/lostcoords/ui/disclosure.css
97
107
  - app/assets/tailwind/lostcoords/ui/emoji-picker.css
98
108
  - app/assets/tailwind/lostcoords/ui/field.css
99
109
  - app/assets/tailwind/lostcoords/ui/glass.css
100
110
  - app/assets/tailwind/lostcoords/ui/layout.css
101
111
  - app/assets/tailwind/lostcoords/ui/popover.css
102
- - app/assets/tailwind/lostcoords/ui/reading.css
112
+ - app/assets/tailwind/lostcoords/ui/prose.css
103
113
  - app/assets/tailwind/lostcoords/ui/sheet.css
104
114
  - app/assets/tailwind/lostcoords/ui/squircle.css
105
115
  - app/assets/tailwind/lostcoords/ui/table.css
106
116
  - app/assets/tailwind/lostcoords/ui/theme.css
117
+ - app/assets/tailwind/lostcoords/ui/toast.css
107
118
  - app/assets/tailwind/lostcoords/ui/tooltip.css
108
119
  - app/assets/tailwind/lostcoords/ui/typography.css
109
120
  - app/assets/tailwind/lostcoords/ui/ui.css
@@ -114,6 +125,7 @@ files:
114
125
  - app/helpers/lostcoords/ui/card_helper.rb
115
126
  - app/helpers/lostcoords/ui/choice_helper.rb
116
127
  - app/helpers/lostcoords/ui/code_block_helper.rb
128
+ - app/helpers/lostcoords/ui/collapsible_helper.rb
117
129
  - app/helpers/lostcoords/ui/copy_button_helper.rb
118
130
  - app/helpers/lostcoords/ui/description_list_helper.rb
119
131
  - app/helpers/lostcoords/ui/dialog_helper.rb
@@ -125,7 +137,6 @@ files:
125
137
  - app/helpers/lostcoords/ui/layout_helper.rb
126
138
  - app/helpers/lostcoords/ui/menu_helper.rb
127
139
  - app/helpers/lostcoords/ui/navigation_helper.rb
128
- - app/helpers/lostcoords/ui/overlay_helper.rb
129
140
  - app/helpers/lostcoords/ui/primitives_helper.rb
130
141
  - app/helpers/lostcoords/ui/reading_helper.rb
131
142
  - app/helpers/lostcoords/ui/render_attributes_helper.rb
@@ -133,6 +144,8 @@ files:
133
144
  - app/helpers/lostcoords/ui/structures_helper.rb
134
145
  - app/helpers/lostcoords/ui/table_helper.rb
135
146
  - app/helpers/lostcoords/ui/tailwind_helper.rb
147
+ - app/helpers/lostcoords/ui/toast_helper.rb
148
+ - app/helpers/lostcoords/ui/tooltip_helper.rb
136
149
  - app/helpers/lostcoords/ui/translation_helper.rb
137
150
  - app/helpers/lostcoords/ui/typography_helper.rb
138
151
  - app/javascript/lostcoords/ui/controllers/lc_code_block_controller.js
@@ -141,12 +154,15 @@ files:
141
154
  - app/javascript/lostcoords/ui/controllers/lc_copy_controller.js
142
155
  - app/javascript/lostcoords/ui/controllers/lc_dialog_controller.js
143
156
  - app/javascript/lostcoords/ui/controllers/lc_emoji_picker_controller.js
157
+ - app/javascript/lostcoords/ui/controllers/lc_kbd_controller.js
144
158
  - app/javascript/lostcoords/ui/controllers/lc_menu_controller.js
145
159
  - app/javascript/lostcoords/ui/controllers/lc_popover_controller.js
146
160
  - app/javascript/lostcoords/ui/controllers/lc_selection_controller.js
147
161
  - app/javascript/lostcoords/ui/controllers/lc_sheet_controller.js
162
+ - app/javascript/lostcoords/ui/controllers/lc_shortcut_controller.js
148
163
  - app/javascript/lostcoords/ui/controllers/lc_tabs_controller.js
149
164
  - app/javascript/lostcoords/ui/controllers/lc_toast_controller.js
165
+ - app/javascript/lostcoords/ui/controllers/lc_toast_region_controller.js
150
166
  - app/javascript/lostcoords/ui/controllers/lc_toggle_controller.js
151
167
  - app/javascript/lostcoords/ui/controllers/lc_tooltip_controller.js
152
168
  - app/javascript/lostcoords/ui/helpers/catalog_picker.js
@@ -155,7 +171,9 @@ files:
155
171
  - app/javascript/lostcoords/ui/helpers/dismiss_stack.js
156
172
  - app/javascript/lostcoords/ui/helpers/floating_panel.js
157
173
  - app/javascript/lostcoords/ui/helpers/menu_keys.js
174
+ - app/javascript/lostcoords/ui/helpers/roving_focus.js
158
175
  - app/javascript/lostcoords/ui/helpers/sheet_motion.js
176
+ - app/javascript/lostcoords/ui/helpers/shortcuts.js
159
177
  - app/javascript/lostcoords/ui/helpers/top_layer.js
160
178
  - app/javascript/lostcoords/ui/helpers/viewport.js
161
179
  - app/javascript/lostcoords/ui/index.js
@@ -168,7 +186,6 @@ files:
168
186
  - config/emoji_search_aliases.yml
169
187
  - config/emoji_search_names.yml
170
188
  - config/emoji_search_names_ja.yml
171
- - config/helper_renames.json
172
189
  - config/importmap.rb
173
190
  - config/locales/en.yml
174
191
  - config/locales/es.yml
@@ -184,10 +201,10 @@ files:
184
201
  - lib/lostcoords/ui/parts.rb
185
202
  - lib/lostcoords/ui/rails.rb
186
203
  - lib/lostcoords/ui/resources.rb
204
+ - lib/lostcoords/ui/stylesheets.rb
187
205
  - lib/lostcoords/ui/tokens.rb
188
206
  - lib/lostcoords/ui/version.rb
189
207
  - lib/tasks/lostcoords_ui.rake
190
- - script/migrate_helpers.rb
191
208
  - vendor/heroicons/LICENSE
192
209
  - vendor/heroicons/icons.json
193
210
  homepage: https://github.com/lostcoords/ui
@@ -1,111 +0,0 @@
1
- {
2
- "avatar_class": "lc_avatar_class",
3
- "avatar_stack_class": "lc_avatar_stack_class",
4
- "avatar_stack_row_class": "lc_avatar_stack_row_class",
5
- "badge_class": "lc_badge_class",
6
- "button_class": "lc_button_class",
7
- "button_variants": "lc_button_variants",
8
- "card_class": "lc_card_class",
9
- "card_surface_class": "lc_card_class",
10
- "container_class": "lc_container_class",
11
- "dialog_class": "lc_dialog_class",
12
- "dialog_kinds": "lc_dialog_kinds",
13
- "dialog_surface_class": "lc_dialog_surface_class",
14
- "dropdown_trigger_class": "lc_dropdown_trigger_class",
15
- "emoji_picker_count": "lc_emoji_picker_count",
16
- "emoji_picker_groups": "lc_emoji_picker_groups",
17
- "emoji_picker_search_labels": "lc_emoji_picker_search_labels",
18
- "emoji_picker_search_name": "lc_emoji_picker_search_name",
19
- "emoji_picker_search_text": "lc_emoji_picker_search_text",
20
- "emoji_picker_toneable?": "lc_emoji_picker_toneable?",
21
- "emoji_picker_version": "lc_emoji_picker_version",
22
- "empty_state_class": "lc_empty_state_class",
23
- "field_class": "lc_field_class",
24
- "field_label_class": "lc_field_label_class",
25
- "form_actions_class": "lc_inline_class",
26
- "form_alert_class": "lc_alert_class",
27
- "form_alert_icon": "lc_alert_icon",
28
- "form_card_class": "lc_card_class",
29
- "glass_class": "lc_glass_class",
30
- "heading_class": "lc_heading_class",
31
- "heroicon": "lc_icon",
32
- "icon_badge_class": "lc_icon_badge_class",
33
- "icon_button_class": "lc_icon_button_class",
34
- "icon_medallion_class": "lc_icon_badge_class",
35
- "inline_class": "lc_inline_class",
36
- "input_class": "lc_input_class",
37
- "menu_option_class": "lc_menu_option_class",
38
- "menu_popover_class": "lc_menu_popover_class",
39
- "pill_class": "lc_toggle_class",
40
- "pill_row_class": "lc_inline_class",
41
- "popover_class": "lc_popover_class",
42
- "prose_class": "lc_prose_class",
43
- "section_header_class": "lc_section_header_class",
44
- "segmented_class": "lc_toggle_group_class",
45
- "segmented_tab_class": "lc_toggle_group_item_class",
46
- "select_class": "lc_select_class",
47
- "sheet_chip_class": "lc_sheet_chip_class",
48
- "sheet_data": "lc_sheet_data",
49
- "sheet_datetime_input_class": "lc_sheet_datetime_input_class",
50
- "sheet_row_class": "lc_sheet_row_class",
51
- "sheet_row_label_class": "lc_sheet_row_label_class",
52
- "sheet_text_input_class": "lc_sheet_text_input_class",
53
- "skeleton_class": "lc_skeleton_class",
54
- "spinner_class": "lc_spinner_class",
55
- "stack_class": "lc_stack_class",
56
- "status_pill_class": "lc_badge_class",
57
- "text_class": "lc_text_class",
58
- "textarea_class": "lc_textarea_class",
59
- "tw": "lc_tw",
60
- "ui_attributes": "lc_attributes",
61
- "ui_avatar": "lc_avatar",
62
- "ui_avatar_stack": "lc_avatar_stack",
63
- "ui_badge": "lc_badge",
64
- "ui_button": "lc_button",
65
- "ui_button_attributes": "lc_button_attributes",
66
- "ui_button_content": "lc_button_content",
67
- "ui_button_link_to": "lc_button_link_to",
68
- "ui_button_to": "lc_button_to",
69
- "ui_card": "lc_card",
70
- "ui_card_surface": "lc_card",
71
- "ui_container": "lc_container",
72
- "ui_copy_button": "lc_copy_button",
73
- "ui_copy_content": "lc_copy_content",
74
- "ui_dialog": "lc_dialog",
75
- "ui_disclosure": "lc_collapsible",
76
- "ui_emoji_picker": "lc_emoji_picker",
77
- "ui_empty_state": "lc_empty_state",
78
- "ui_field": "lc_field",
79
- "ui_form_actions": "lc_inline",
80
- "ui_form_alert": "lc_alert",
81
- "ui_form_card": "lc_card",
82
- "ui_heading": "lc_heading",
83
- "ui_icon_badge": "lc_icon_badge",
84
- "ui_icon_button": "lc_icon_button",
85
- "ui_inline": "lc_inline",
86
- "ui_input": "lc_input",
87
- "ui_label!": "lc_label!",
88
- "ui_medallion": "lc_icon_badge",
89
- "ui_menu": "lc_menu",
90
- "ui_menu_option": "lc_menu_option",
91
- "ui_normalize_attributes": "lc_normalize_attributes",
92
- "ui_option!": "lc_option!",
93
- "ui_pill": "lc_toggle",
94
- "ui_pill_row": "lc_inline",
95
- "ui_popover": "lc_popover",
96
- "ui_prose": "lc_prose",
97
- "ui_render_dialog": "lc_render_dialog",
98
- "ui_render_panel": "lc_render_panel",
99
- "ui_section": "lc_section",
100
- "ui_segmented": "lc_toggle_group",
101
- "ui_segmented_tab": "lc_toggle_group_item",
102
- "ui_select": "lc_select",
103
- "ui_sheet": "lc_dialog",
104
- "ui_skeleton": "lc_skeleton",
105
- "ui_spinner": "lc_spinner",
106
- "ui_stack": "lc_stack",
107
- "ui_status_pill": "lc_badge",
108
- "ui_text": "lc_text",
109
- "ui_textarea": "lc_textarea",
110
- "ui_toast": "lc_toast"
111
- }
@@ -1,136 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "json"
4
- require "find"
5
- require "optparse"
6
- require "ripper"
7
-
8
- module HelperMigration
9
- LIBRARY = File.expand_path("..", __dir__)
10
- NAMES = JSON.parse(File.read(File.join(LIBRARY, "config/helper_renames.json"))).freeze
11
- PARTIALS = %w[dialog disclosure].to_h do |name|
12
- [ "lostcoords/ui/components/ui_#{name}", "lostcoords/ui/components/lc_#{name}" ]
13
- end.freeze
14
-
15
- def self.method_locations(node, locations = {})
16
- return locations unless node.is_a?(Array)
17
-
18
- name = case node.first
19
- when :def, :vcall, :fcall, :command, :symbol then node[1]
20
- when :defs, :call, :command_call then node[3]
21
- end
22
- locations[name[2]] = true if name.is_a?(Array) && name.first == :@ident
23
- node.each { |child| method_locations(child, locations) }
24
- locations
25
- end
26
-
27
- def self.rewrite(source, program: source, prefix_lines: 0)
28
- syntax = Ripper.sexp(program)
29
- raise ArgumentError, "Cannot parse Ruby source for helper migration" unless syntax
30
-
31
- methods = method_locations(syntax)
32
- offsets = [ 0 ]
33
- source.each_line { |line| offsets << offsets.last + line.bytesize }
34
- contexts = []
35
- edits = Ripper.lex(program).filter_map do |position, type, value, _|
36
- replacement = case type
37
- when :on_ident then NAMES[value] if methods[position]
38
- when :on_tstring_content then contexts.last == :symbol ? NAMES[value] : PARTIALS[value]
39
- end
40
- case type
41
- when :on_symbeg then contexts << :symbol unless value == ":"
42
- when :on_qsymbols_beg, :on_symbols_beg then contexts << :symbol
43
- when :on_tstring_beg, :on_heredoc_beg, :on_qwords_beg, :on_words_beg, :on_regexp_beg then contexts << :string
44
- when :on_tstring_end, :on_heredoc_end, :on_regexp_end then contexts.pop
45
- end
46
- line = position[0] - prefix_lines
47
- [ offsets.fetch(line - 1) + position[1], value.bytesize, replacement ] if replacement && line.positive?
48
- end
49
- updated = source.b
50
- edits.reverse_each { |offset, length, value| updated[offset, length] = value.b }
51
- updated.force_encoding(source.encoding)
52
- end
53
-
54
- def self.ruby(source)
55
- rewrite(source)
56
- end
57
-
58
- def self.erb(source)
59
- program = source.b.gsub(/[^\r\n]/n, " ")
60
- source.to_enum(:scan, /(<%(?!%)(?:==?|-)?)(.*?)(-?%>)/m).each do
61
- match = Regexp.last_match
62
- opening, body, closing = match.captures
63
- next if body.start_with?("#")
64
-
65
- start = match.pre_match.bytesize + opening.bytesize
66
- program[start, body.bytesize] = body.b
67
- program[start + body.bytesize, closing.bytesize] = ";" + " " * (closing.bytesize - 1)
68
- end
69
- signature = source[/<%#\s*locals:\s*\((.*?)\)\s*%>/m, 1]
70
- prefix = "def __template(#{signature})\n"
71
- suffix = "\nend"
72
- rewrite(source, program: prefix + program.force_encoding(source.encoding) + suffix, prefix_lines: prefix.count("\n"))
73
- end
74
-
75
- def self.markdown(source)
76
- source = source.gsub(/(^```(ruby|rb|erb)[^\n]*\n)(.*?)(^```[ \t]*$)/m) do
77
- opening, language, body, closing = Regexp.last_match.captures
78
- "#{opening}#{language == 'erb' ? erb(body) : ruby(body)}#{closing}"
79
- end
80
- source.gsub(/`([^`\n]+)`/) { |match| NAMES.key?(Regexp.last_match[1]) ? "`#{NAMES.fetch(Regexp.last_match[1])}`" : match }
81
- end
82
-
83
- def self.changes(root)
84
- files = Find.find(root).filter_map do |path|
85
- next if File.symlink?(path)
86
- if File.directory?(path)
87
- name = File.basename(path)
88
- Find.prune if path != root && (name.start_with?(".") || %w[node_modules vendor tmp log storage pkg coverage builds dist].include?(name))
89
- next
90
- end
91
- path.delete_prefix("#{root}/")
92
- end
93
- files.sort.filter_map do |relative|
94
- path = File.join(root, relative)
95
- next if File.symlink?(path) || relative.start_with?("vendor/")
96
- next if root == LIBRARY && (relative == "README.md" || relative.start_with?("docs/", "plans/"))
97
-
98
- extension = File.extname(path)
99
- next unless %w[.rb .erb .md].include?(extension) || extension.empty?
100
-
101
- source = File.binread(path).force_encoding(Encoding::UTF_8)
102
- updated = case extension
103
- when ".rb" then ruby(source)
104
- when ".erb" then erb(source)
105
- when ".md" then markdown(source)
106
- else ruby(source) if source.lines.first&.match?(/\A#!.*ruby/)
107
- end
108
- next unless updated
109
-
110
- name = File.basename(path).sub(/\A_ui_(dialog|disclosure)\.html\.erb\z/, '_lc_\1.html.erb')
111
- destination = File.join(File.dirname(path), name)
112
- abort "Refusing to overwrite #{destination}" if destination != path && (File.exist?(destination) || File.symlink?(destination))
113
- [ path, destination, updated ] if updated != source || destination != path
114
- end
115
- end
116
- end
117
-
118
- if $PROGRAM_NAME == __FILE__
119
- write = false
120
- OptionParser.new do |options|
121
- options.banner = "Usage: ruby migrate_helpers.rb [checkout] [--write]"
122
- options.on("--write", "Apply the migration after checking every destination") { write = true }
123
- end.parse!
124
- abort "Expected one checkout path" if ARGV.size > 1
125
- root = File.realpath(ARGV.first || Dir.pwd)
126
- changes = HelperMigration.changes(root)
127
- changes.each do |path, destination, source|
128
- puts path.delete_prefix("#{root}/")
129
- next unless write
130
-
131
- File.binwrite(path, source)
132
- File.rename(path, destination) unless path == destination
133
- end
134
- puts "#{changes.size} files #{write ? 'updated' : 'would change'}"
135
- exit 1 if changes.any? && !write
136
- end