lostcoords-ui 0.4.1

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 (93) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +352 -0
  4. data/app/assets/tailwind/lostcoords/ui/avatar.css +11 -0
  5. data/app/assets/tailwind/lostcoords/ui/card.css +26 -0
  6. data/app/assets/tailwind/lostcoords/ui/copy-button.css +33 -0
  7. data/app/assets/tailwind/lostcoords/ui/dialog.css +60 -0
  8. data/app/assets/tailwind/lostcoords/ui/disclosure.css +43 -0
  9. data/app/assets/tailwind/lostcoords/ui/emoji-picker.css +462 -0
  10. data/app/assets/tailwind/lostcoords/ui/field.css +6 -0
  11. data/app/assets/tailwind/lostcoords/ui/glass.css +37 -0
  12. data/app/assets/tailwind/lostcoords/ui/layout.css +5 -0
  13. data/app/assets/tailwind/lostcoords/ui/popover.css +75 -0
  14. data/app/assets/tailwind/lostcoords/ui/reading.css +60 -0
  15. data/app/assets/tailwind/lostcoords/ui/sheet.css +170 -0
  16. data/app/assets/tailwind/lostcoords/ui/squircle.css +32 -0
  17. data/app/assets/tailwind/lostcoords/ui/theme.css +255 -0
  18. data/app/assets/tailwind/lostcoords/ui/typography.css +81 -0
  19. data/app/assets/tailwind/lostcoords/ui/ui.css +21 -0
  20. data/app/assets/tailwind/lostcoords/ui/utilities.css +45 -0
  21. data/app/assets/tailwind/lostcoords_ui/engine.css +1 -0
  22. data/app/helpers/lostcoords/ui/badge_helper.rb +162 -0
  23. data/app/helpers/lostcoords/ui/button_helper.rb +70 -0
  24. data/app/helpers/lostcoords/ui/card_helper.rb +60 -0
  25. data/app/helpers/lostcoords/ui/copy_button_helper.rb +40 -0
  26. data/app/helpers/lostcoords/ui/dialog_helper.rb +33 -0
  27. data/app/helpers/lostcoords/ui/emoji_picker_helper.rb +245 -0
  28. data/app/helpers/lostcoords/ui/feedback_helper.rb +30 -0
  29. data/app/helpers/lostcoords/ui/form_helper.rb +93 -0
  30. data/app/helpers/lostcoords/ui/glass_helper.rb +25 -0
  31. data/app/helpers/lostcoords/ui/icon_helper.rb +15 -0
  32. data/app/helpers/lostcoords/ui/layout_helper.rb +60 -0
  33. data/app/helpers/lostcoords/ui/menu_helper.rb +41 -0
  34. data/app/helpers/lostcoords/ui/primitives_helper.rb +152 -0
  35. data/app/helpers/lostcoords/ui/reading_helper.rb +15 -0
  36. data/app/helpers/lostcoords/ui/render_attributes_helper.rb +46 -0
  37. data/app/helpers/lostcoords/ui/sheet_helper.rb +54 -0
  38. data/app/helpers/lostcoords/ui/structures_helper.rb +159 -0
  39. data/app/helpers/lostcoords/ui/tailwind_helper.rb +14 -0
  40. data/app/helpers/lostcoords/ui/translation_helper.rb +13 -0
  41. data/app/helpers/lostcoords/ui/typography_helper.rb +28 -0
  42. data/app/javascript/lostcoords/ui/controllers/confirm_dialog_controller.js +39 -0
  43. data/app/javascript/lostcoords/ui/controllers/disclosure_controller.js +163 -0
  44. data/app/javascript/lostcoords/ui/controllers/emoji_picker_controller.js +366 -0
  45. data/app/javascript/lostcoords/ui/controllers/modal_controller.js +27 -0
  46. data/app/javascript/lostcoords/ui/controllers/row_menu_controller.js +83 -0
  47. data/app/javascript/lostcoords/ui/controllers/sheet_controller.js +422 -0
  48. data/app/javascript/lostcoords/ui/controllers/toast_controller.js +69 -0
  49. data/app/javascript/lostcoords/ui/controllers/ui_copy_controller.js +56 -0
  50. data/app/javascript/lostcoords/ui/controllers/ui_popover_controller.js +17 -0
  51. data/app/javascript/lostcoords/ui/controllers/ui_selection_controller.js +69 -0
  52. data/app/javascript/lostcoords/ui/controllers/ui_toggle_controller.js +13 -0
  53. data/app/javascript/lostcoords/ui/helpers/catalog_picker.js +455 -0
  54. data/app/javascript/lostcoords/ui/helpers/context_dialog.js +29 -0
  55. data/app/javascript/lostcoords/ui/helpers/dialog.js +31 -0
  56. data/app/javascript/lostcoords/ui/helpers/dismiss_stack.js +155 -0
  57. data/app/javascript/lostcoords/ui/helpers/floating_panel.js +108 -0
  58. data/app/javascript/lostcoords/ui/helpers/menu_keys.js +27 -0
  59. data/app/javascript/lostcoords/ui/helpers/sheet_motion.js +13 -0
  60. data/app/javascript/lostcoords/ui/helpers/top_layer.js +102 -0
  61. data/app/javascript/lostcoords/ui/helpers/viewport.js +13 -0
  62. data/app/views/lostcoords/ui/components/_emoji_picker.html.erb +176 -0
  63. data/app/views/lostcoords/ui/components/_emoji_picker_grid.html.erb +29 -0
  64. data/app/views/lostcoords/ui/components/_form_alert.html.erb +4 -0
  65. data/app/views/lostcoords/ui/components/_lc_dialog.html.erb +13 -0
  66. data/app/views/lostcoords/ui/components/_lc_disclosure.html.erb +5 -0
  67. data/app/views/lostcoords/ui/components/_skeleton.html.erb +4 -0
  68. data/app/views/lostcoords/ui/components/_spinner.html.erb +10 -0
  69. data/app/views/lostcoords/ui/components/_ui_floating_panel.html.erb +8 -0
  70. data/config/emoji_search_aliases.yml +146 -0
  71. data/config/emoji_search_names.yml +1917 -0
  72. data/config/emoji_search_names_ja.yml +6902 -0
  73. data/config/helper_renames.json +113 -0
  74. data/config/importmap.rb +3 -0
  75. data/config/locales/en.yml +33 -0
  76. data/config/locales/es.yml +33 -0
  77. data/config/locales/ja.yml +33 -0
  78. data/lib/lostcoords/ui/action_view.rb +10 -0
  79. data/lib/lostcoords/ui/builders.rb +42 -0
  80. data/lib/lostcoords/ui/components.rb +13 -0
  81. data/lib/lostcoords/ui/engine.rb +22 -0
  82. data/lib/lostcoords/ui/helpers.rb +11 -0
  83. data/lib/lostcoords/ui/icons.rb +24 -0
  84. data/lib/lostcoords/ui/rails.rb +3 -0
  85. data/lib/lostcoords/ui/resources.rb +17 -0
  86. data/lib/lostcoords/ui/tokens.rb +14 -0
  87. data/lib/lostcoords/ui/version.rb +5 -0
  88. data/lib/lostcoords/ui.rb +2 -0
  89. data/lib/tasks/lostcoords_ui.rake +15 -0
  90. data/script/migrate_helpers.rb +136 -0
  91. data/vendor/heroicons/LICENSE +21 -0
  92. data/vendor/heroicons/icons.json +1303 -0
  93. metadata +203 -0
@@ -0,0 +1,113 @@
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_surface_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
+ "eyebrow_class": "lc_eyebrow_class",
24
+ "field_class": "lc_field_class",
25
+ "field_label_class": "lc_field_label_class",
26
+ "form_actions_class": "lc_form_actions_class",
27
+ "form_alert_class": "lc_form_alert_class",
28
+ "form_alert_icon": "lc_form_alert_icon",
29
+ "form_card_class": "lc_form_card_class",
30
+ "glass_class": "lc_glass_class",
31
+ "heading_class": "lc_heading_class",
32
+ "heroicon": "lc_icon",
33
+ "icon_badge_class": "lc_icon_badge_class",
34
+ "icon_button_class": "lc_icon_button_class",
35
+ "icon_medallion_class": "lc_icon_medallion_class",
36
+ "inline_class": "lc_inline_class",
37
+ "input_class": "lc_input_class",
38
+ "menu_option_class": "lc_menu_option_class",
39
+ "menu_popover_class": "lc_menu_popover_class",
40
+ "pill_class": "lc_pill_class",
41
+ "pill_row_class": "lc_pill_row_class",
42
+ "popover_class": "lc_popover_class",
43
+ "prose_class": "lc_prose_class",
44
+ "section_header_class": "lc_section_header_class",
45
+ "segmented_class": "lc_segmented_class",
46
+ "segmented_tab_class": "lc_segmented_tab_class",
47
+ "select_class": "lc_select_class",
48
+ "sheet_chip_class": "lc_sheet_chip_class",
49
+ "sheet_data": "lc_sheet_data",
50
+ "sheet_datetime_input_class": "lc_sheet_datetime_input_class",
51
+ "sheet_row_class": "lc_sheet_row_class",
52
+ "sheet_row_label_class": "lc_sheet_row_label_class",
53
+ "sheet_text_input_class": "lc_sheet_text_input_class",
54
+ "skeleton_class": "lc_skeleton_class",
55
+ "spinner_class": "lc_spinner_class",
56
+ "stack_class": "lc_stack_class",
57
+ "status_pill_class": "lc_status_pill_class",
58
+ "text_class": "lc_text_class",
59
+ "textarea_class": "lc_textarea_class",
60
+ "tw": "lc_tw",
61
+ "ui_attributes": "lc_attributes",
62
+ "ui_avatar": "lc_avatar",
63
+ "ui_avatar_stack": "lc_avatar_stack",
64
+ "ui_badge": "lc_badge",
65
+ "ui_button": "lc_button",
66
+ "ui_button_attributes": "lc_button_attributes",
67
+ "ui_button_content": "lc_button_content",
68
+ "ui_button_link_to": "lc_button_link_to",
69
+ "ui_button_to": "lc_button_to",
70
+ "ui_card": "lc_card",
71
+ "ui_card_surface": "lc_card_surface",
72
+ "ui_container": "lc_container",
73
+ "ui_copy_button": "lc_copy_button",
74
+ "ui_copy_content": "lc_copy_content",
75
+ "ui_dialog": "lc_dialog",
76
+ "ui_disclosure": "lc_disclosure",
77
+ "ui_emoji_picker": "lc_emoji_picker",
78
+ "ui_empty_state": "lc_empty_state",
79
+ "ui_eyebrow": "lc_eyebrow",
80
+ "ui_field": "lc_field",
81
+ "ui_form_actions": "lc_form_actions",
82
+ "ui_form_alert": "lc_form_alert",
83
+ "ui_form_card": "lc_form_card",
84
+ "ui_heading": "lc_heading",
85
+ "ui_icon_badge": "lc_icon_badge",
86
+ "ui_icon_button": "lc_icon_button",
87
+ "ui_inline": "lc_inline",
88
+ "ui_input": "lc_input",
89
+ "ui_label!": "lc_label!",
90
+ "ui_medallion": "lc_medallion",
91
+ "ui_menu": "lc_menu",
92
+ "ui_menu_option": "lc_menu_option",
93
+ "ui_normalize_attributes": "lc_normalize_attributes",
94
+ "ui_option!": "lc_option!",
95
+ "ui_pill": "lc_pill",
96
+ "ui_pill_row": "lc_pill_row",
97
+ "ui_popover": "lc_popover",
98
+ "ui_prose": "lc_prose",
99
+ "ui_render_dialog": "lc_render_dialog",
100
+ "ui_render_panel": "lc_render_panel",
101
+ "ui_section": "lc_section",
102
+ "ui_segmented": "lc_segmented",
103
+ "ui_segmented_tab": "lc_segmented_tab",
104
+ "ui_select": "lc_select",
105
+ "ui_sheet": "lc_sheet",
106
+ "ui_skeleton": "lc_skeleton",
107
+ "ui_spinner": "lc_spinner",
108
+ "ui_stack": "lc_stack",
109
+ "ui_status_pill": "lc_status_pill",
110
+ "ui_text": "lc_text",
111
+ "ui_textarea": "lc_textarea",
112
+ "ui_toast": "lc_toast"
113
+ }
@@ -0,0 +1,3 @@
1
+ # Keys match the host's own helpers/ and controllers/ pins, so imports and stimulus-loading resolve the same way.
2
+ pin_all_from Lostcoords::Ui::Engine.root.join("app/javascript/lostcoords/ui/controllers"), under: "controllers", to: "lostcoords/ui/controllers", preload: false
3
+ pin_all_from Lostcoords::Ui::Engine.root.join("app/javascript/lostcoords/ui/helpers"), under: "helpers", to: "lostcoords/ui/helpers", preload: false
@@ -0,0 +1,33 @@
1
+ en:
2
+ lostcoords:
3
+ ui:
4
+ copy_button:
5
+ label: "Copy"
6
+ copied: "Copied"
7
+ retry: "Retry"
8
+ error: "Could not copy. Select the text and copy it manually."
9
+ emoji_picker:
10
+ recently_used: "Recently used"
11
+ search_label: "Search emoji"
12
+ search_placeholder: "Search emoji"
13
+ no_results: "No emoji found."
14
+ search_results: "Search results"
15
+ categories_nav: "Emoji categories"
16
+ skin_tone: "Skin tone"
17
+ skin_tones:
18
+ default: "Default"
19
+ light: "Light"
20
+ medium_light: "Medium-light"
21
+ medium: "Medium"
22
+ medium_dark: "Medium-dark"
23
+ dark: "Dark"
24
+ categories:
25
+ smileys_emotion: "Smileys & Emotion"
26
+ people_body: "People & Body"
27
+ animals_nature: "Animals & Nature"
28
+ food_drink: "Food & Drink"
29
+ travel_places: "Travel & Places"
30
+ activities: "Activities"
31
+ objects: "Objects"
32
+ symbols: "Symbols"
33
+ flags: "Flags"
@@ -0,0 +1,33 @@
1
+ es:
2
+ lostcoords:
3
+ ui:
4
+ copy_button:
5
+ label: "Copiar"
6
+ copied: "Copiado"
7
+ retry: "Reintentar"
8
+ error: "No se pudo copiar. Selecciona el texto y cópialo manualmente."
9
+ emoji_picker:
10
+ recently_used: "Usados recientemente"
11
+ search_label: "Buscar emoji"
12
+ search_placeholder: "Buscar emoji"
13
+ no_results: "No se encontraron emojis."
14
+ search_results: "Resultados"
15
+ categories_nav: "Categorías de emojis"
16
+ skin_tone: "Tono de piel"
17
+ skin_tones:
18
+ default: "Predeterminado"
19
+ light: "Claro"
20
+ medium_light: "Medio claro"
21
+ medium: "Medio"
22
+ medium_dark: "Medio oscuro"
23
+ dark: "Oscuro"
24
+ categories:
25
+ smileys_emotion: "Caras y emociones"
26
+ people_body: "Personas y cuerpo"
27
+ animals_nature: "Animales y naturaleza"
28
+ food_drink: "Comida y bebida"
29
+ travel_places: "Viajes y lugares"
30
+ activities: "Actividades"
31
+ objects: "Objetos"
32
+ symbols: "Símbolos"
33
+ flags: "Banderas"
@@ -0,0 +1,33 @@
1
+ ja:
2
+ lostcoords:
3
+ ui:
4
+ copy_button:
5
+ label: "コピー"
6
+ copied: "コピー済み"
7
+ retry: "再試行"
8
+ error: "コピーできませんでした。テキストを選択して手動でコピーしてください。"
9
+ emoji_picker:
10
+ recently_used: "最近使った絵文字"
11
+ search_label: "絵文字を検索"
12
+ search_placeholder: "絵文字を検索"
13
+ no_results: "一致する絵文字がありません。"
14
+ search_results: "検索結果"
15
+ categories_nav: "絵文字のカテゴリ"
16
+ skin_tone: "肌の色"
17
+ skin_tones:
18
+ default: "標準"
19
+ light: "薄い"
20
+ medium_light: "やや薄い"
21
+ medium: "中間"
22
+ medium_dark: "やや濃い"
23
+ dark: "濃い"
24
+ categories:
25
+ smileys_emotion: "表情と感情"
26
+ people_body: "人と体"
27
+ animals_nature: "動物と自然"
28
+ food_drink: "食べ物と飲み物"
29
+ travel_places: "旅行と場所"
30
+ activities: "アクティビティ"
31
+ objects: "もの"
32
+ symbols: "記号"
33
+ flags: "旗"
@@ -0,0 +1,10 @@
1
+ require "bigdecimal"
2
+ require "action_view"
3
+ require "active_support/core_ext/hash"
4
+ require "active_support/core_ext/object/blank"
5
+ require "tailwind_merge"
6
+ require "lostcoords/ui/resources"
7
+ require "lostcoords/ui/icons"
8
+ require "lostcoords/ui/helpers"
9
+
10
+ Lostcoords::Ui.load_translations!
@@ -0,0 +1,42 @@
1
+ require "prism"
2
+ require_relative "version"
3
+
4
+ module Lostcoords::Ui
5
+ module Builders
6
+ HELPERS = File.expand_path("../../../app/helpers/lostcoords/ui", __dir__)
7
+
8
+ Builder = Data.define(:helper, :name, :parameters, :path, :line) do
9
+ def signature = parameters ? "#{name}(#{parameters})" : name.to_s
10
+
11
+ def component?
12
+ !name.to_s.end_with?("_class") &&
13
+ !%i[lc_tw lc_sheet_data lc_form_alert_icon].include?(name) &&
14
+ helper != "EmojiPickerHelper"
15
+ end
16
+ end
17
+
18
+ def self.all
19
+ definitions.reject(&:component?)
20
+ end
21
+
22
+ def self.definitions
23
+ Dir["#{HELPERS}/*_helper.rb"].sort.flat_map { |path| public_instance_methods_in(path) }
24
+ end
25
+
26
+ def self.markdown
27
+ all.map { |builder| "- `#{builder.signature}` (#{builder.helper})" }.join("\n")
28
+ end
29
+
30
+ def self.public_instance_methods_in(path)
31
+ namespace = Prism.parse_file(path).value.statements.body.grep(Prism::ModuleNode).first
32
+ helper = namespace.body.body.first
33
+ visible = true
34
+ helper.body.body.filter_map do |node|
35
+ visible = false if node.is_a?(Prism::CallNode) && node.name == :private && node.arguments.nil?
36
+ next unless visible && node.is_a?(Prism::DefNode) && node.receiver.nil?
37
+
38
+ Builder.new(helper.constant_path.slice, node.name, node.parameters&.slice, path, node.location.start_line)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "builders"
2
+
3
+ module Lostcoords::Ui
4
+ module Components
5
+ def self.all
6
+ Builders.definitions.select(&:component?)
7
+ end
8
+
9
+ def self.markdown
10
+ all.map { |component| "- `#{component.signature}`" }.join("\n")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ module Lostcoords::Ui
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Lostcoords::Ui
4
+
5
+ initializer "lostcoords.ui.assets" do |app|
6
+ next unless app.config.respond_to?(:assets)
7
+
8
+ app.config.assets.paths << root.join("app/javascript")
9
+ end
10
+
11
+ initializer "lostcoords.ui.importmap", before: "importmap" do |app|
12
+ next unless app.config.respond_to?(:importmap)
13
+
14
+ app.config.importmap.paths << root.join("config/importmap.rb")
15
+ app.config.importmap.cache_sweepers << root.join("app/javascript")
16
+ end
17
+
18
+ initializer "lostcoords.ui.helpers" do
19
+ ActiveSupport.on_load(:action_view) { include Lostcoords::Ui::Helpers }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ module Lostcoords::Ui
2
+ module Helpers
3
+ paths = Dir[Lostcoords::Ui.root.join("app/helpers/lostcoords/ui/*_helper.rb")].sort_by do |path|
4
+ [ %w[tailwind_helper.rb button_helper.rb].index(File.basename(path)) || 2, path ]
5
+ end
6
+ paths.each do |path|
7
+ require path
8
+ include Lostcoords::Ui.const_get(File.basename(path, ".rb").camelize)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ require "json"
2
+ require "nokogiri"
3
+
4
+ module Lostcoords::Ui
5
+ module Icons
6
+ TEMPLATES = {}
7
+ LOCK = Mutex.new
8
+
9
+ def self.fetch(name, variant)
10
+ LOCK.synchronize do
11
+ @catalog ||= JSON.parse(File.read(Lostcoords::Ui.root.join("vendor/heroicons/icons.json")), freeze: true).fetch("icons")
12
+ source = @catalog.fetch(variant.to_s).fetch(name.to_s)
13
+ template = TEMPLATES[source] ||= begin
14
+ document = Nokogiri::XML(source) { |config| config.strict.nonet }
15
+ document.xpath("//text()[normalize-space(.) = '']").remove
16
+ document
17
+ end
18
+ template.dup.root
19
+ end
20
+ rescue KeyError
21
+ raise ArgumentError, "Unknown Heroicon #{name.inspect} with variant #{variant.inspect}"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ require "rails/engine"
2
+ require "lostcoords/ui/action_view"
3
+ require "lostcoords/ui/engine"
@@ -0,0 +1,17 @@
1
+ require "pathname"
2
+ require_relative "version"
3
+
4
+ module Lostcoords::Ui
5
+ def self.root
6
+ @root ||= Pathname.new(File.expand_path("../../..", __dir__)).freeze
7
+ end
8
+
9
+ def self.view_paths
10
+ [ root.join("app/views").to_s ]
11
+ end
12
+
13
+ def self.load_translations!
14
+ paths = (Dir[root.join("config/locales/*.yml")] + I18n.load_path).uniq
15
+ I18n.load_path = paths unless I18n.load_path == paths
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "version"
2
+
3
+ module Lostcoords::Ui
4
+ module Tokens
5
+ THEME_PATH = File.expand_path("../../../app/assets/tailwind/lostcoords/ui/theme.css", __dir__)
6
+ DECLARATION = /(?<![\w-])(--[\w-]+)\s*:/
7
+
8
+ def self.names
9
+ File.read(THEME_PATH).scan(DECLARATION).flatten.uniq.sort
10
+ end
11
+ end
12
+
13
+ TOKENS = Tokens.names.freeze
14
+ end
@@ -0,0 +1,5 @@
1
+ module Lostcoords
2
+ module Ui
3
+ VERSION = "0.4.1"
4
+ end
5
+ end
@@ -0,0 +1,2 @@
1
+ require "lostcoords/ui/action_view"
2
+ require "lostcoords/ui/rails" if defined?(::Rails::Engine)
@@ -0,0 +1,15 @@
1
+ namespace :lostcoords do
2
+ namespace :ui do
3
+ desc "Print the component rendering helpers and their signatures"
4
+ task :components do
5
+ require "lostcoords/ui/components"
6
+ puts Lostcoords::Ui::Components.markdown
7
+ end
8
+
9
+ desc "Print the style builders and their signatures"
10
+ task :builders do
11
+ require "lostcoords/ui/builders"
12
+ puts Lostcoords::Ui::Builders.markdown
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,136 @@
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
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Tailwind Labs, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.