view_primitives 0.1.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 (140) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +84 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +198 -0
  5. data/lib/generators/view_primitives/add/add_generator.rb +110 -0
  6. data/lib/generators/view_primitives/add/templates/accordion/accordion_component.html.erb +10 -0
  7. data/lib/generators/view_primitives/add/templates/accordion/accordion_component.rb.tt +22 -0
  8. data/lib/generators/view_primitives/add/templates/accordion/accordion_controller.js +15 -0
  9. data/lib/generators/view_primitives/add/templates/accordion/accordion_item_component.rb.tt +47 -0
  10. data/lib/generators/view_primitives/add/templates/alert/alert_component.rb.tt +62 -0
  11. data/lib/generators/view_primitives/add/templates/alert_dialog/alert_dialog_component.rb.tt +55 -0
  12. data/lib/generators/view_primitives/add/templates/aspect_ratio/aspect_ratio_component.rb.tt +18 -0
  13. data/lib/generators/view_primitives/add/templates/audio/audio_component.rb.tt +51 -0
  14. data/lib/generators/view_primitives/add/templates/avatar/avatar_component.rb.tt +37 -0
  15. data/lib/generators/view_primitives/add/templates/badge/badge_component.rb.tt +35 -0
  16. data/lib/generators/view_primitives/add/templates/banner/banner_component.rb.tt +29 -0
  17. data/lib/generators/view_primitives/add/templates/bottom_nav/bottom_nav_component.rb.tt +38 -0
  18. data/lib/generators/view_primitives/add/templates/breadcrumb/breadcrumb_component.rb.tt +37 -0
  19. data/lib/generators/view_primitives/add/templates/button/button_component.rb.tt +61 -0
  20. data/lib/generators/view_primitives/add/templates/button_group/button_group_component.rb.tt +23 -0
  21. data/lib/generators/view_primitives/add/templates/calendar/calendar_component.rb.tt +121 -0
  22. data/lib/generators/view_primitives/add/templates/calendar/calendar_controller.js +86 -0
  23. data/lib/generators/view_primitives/add/templates/card/card_component.rb.tt +16 -0
  24. data/lib/generators/view_primitives/add/templates/card/card_content_component.rb.tt +16 -0
  25. data/lib/generators/view_primitives/add/templates/card/card_description_component.rb.tt +17 -0
  26. data/lib/generators/view_primitives/add/templates/card/card_footer_component.rb.tt +16 -0
  27. data/lib/generators/view_primitives/add/templates/card/card_header_component.rb.tt +17 -0
  28. data/lib/generators/view_primitives/add/templates/card/card_title_component.rb.tt +17 -0
  29. data/lib/generators/view_primitives/add/templates/carousel/carousel_component.rb.tt +102 -0
  30. data/lib/generators/view_primitives/add/templates/carousel/carousel_controller.js +48 -0
  31. data/lib/generators/view_primitives/add/templates/chart/chart_component.rb.tt +63 -0
  32. data/lib/generators/view_primitives/add/templates/chart/chart_controller.js +29 -0
  33. data/lib/generators/view_primitives/add/templates/chat_bubble/chat_bubble_component.rb.tt +53 -0
  34. data/lib/generators/view_primitives/add/templates/checkbox/checkbox_component.rb.tt +50 -0
  35. data/lib/generators/view_primitives/add/templates/collapsible/collapsible_component.rb.tt +31 -0
  36. data/lib/generators/view_primitives/add/templates/combobox/combobox_component.rb.tt +87 -0
  37. data/lib/generators/view_primitives/add/templates/combobox/combobox_controller.js +38 -0
  38. data/lib/generators/view_primitives/add/templates/command/command_component.rb.tt +85 -0
  39. data/lib/generators/view_primitives/add/templates/command/command_controller.js +50 -0
  40. data/lib/generators/view_primitives/add/templates/context_menu/context_menu_component.rb.tt +47 -0
  41. data/lib/generators/view_primitives/add/templates/context_menu/context_menu_controller.js +20 -0
  42. data/lib/generators/view_primitives/add/templates/data_table/data_table_component.rb.tt +163 -0
  43. data/lib/generators/view_primitives/add/templates/data_table/data_table_controller.js +115 -0
  44. data/lib/generators/view_primitives/add/templates/date_picker/date_picker_component.rb.tt +92 -0
  45. data/lib/generators/view_primitives/add/templates/date_picker/date_picker_controller.js +48 -0
  46. data/lib/generators/view_primitives/add/templates/device_mockup/device_mockup_component.rb.tt +65 -0
  47. data/lib/generators/view_primitives/add/templates/dialog/dialog_component.rb.tt +71 -0
  48. data/lib/generators/view_primitives/add/templates/dialog/dialog_controller.js +15 -0
  49. data/lib/generators/view_primitives/add/templates/drawer/drawer_component.rb.tt +62 -0
  50. data/lib/generators/view_primitives/add/templates/drawer/drawer_controller.js +15 -0
  51. data/lib/generators/view_primitives/add/templates/dropdown_menu/dropdown_controller.js +17 -0
  52. data/lib/generators/view_primitives/add/templates/dropdown_menu/dropdown_menu_component.rb.tt +53 -0
  53. data/lib/generators/view_primitives/add/templates/embed/embed_component.rb.tt +271 -0
  54. data/lib/generators/view_primitives/add/templates/embed/embed_controller.js +43 -0
  55. data/lib/generators/view_primitives/add/templates/figure/figure_component.rb.tt +24 -0
  56. data/lib/generators/view_primitives/add/templates/file_input/file_input_component.rb.tt +31 -0
  57. data/lib/generators/view_primitives/add/templates/floating_label/floating_label_component.rb.tt +54 -0
  58. data/lib/generators/view_primitives/add/templates/footer/footer_component.rb.tt +51 -0
  59. data/lib/generators/view_primitives/add/templates/form_field/form_field_component.rb.tt +51 -0
  60. data/lib/generators/view_primitives/add/templates/gallery/gallery_component.rb.tt +83 -0
  61. data/lib/generators/view_primitives/add/templates/gallery/gallery_controller.js +28 -0
  62. data/lib/generators/view_primitives/add/templates/hover_card/hover_card_component.rb.tt +43 -0
  63. data/lib/generators/view_primitives/add/templates/iframe/iframe_component.rb.tt +59 -0
  64. data/lib/generators/view_primitives/add/templates/image/image_component.rb.tt +38 -0
  65. data/lib/generators/view_primitives/add/templates/indicator/indicator_component.rb.tt +46 -0
  66. data/lib/generators/view_primitives/add/templates/input/input_component.rb.tt +28 -0
  67. data/lib/generators/view_primitives/add/templates/input_otp/input_otp_component.rb.tt +65 -0
  68. data/lib/generators/view_primitives/add/templates/input_otp/input_otp_controller.js +39 -0
  69. data/lib/generators/view_primitives/add/templates/kbd/kbd_component.rb.tt +21 -0
  70. data/lib/generators/view_primitives/add/templates/label/label_component.rb.tt +23 -0
  71. data/lib/generators/view_primitives/add/templates/list_group/list_group_component.rb.tt +16 -0
  72. data/lib/generators/view_primitives/add/templates/list_group/list_group_item_component.rb.tt +31 -0
  73. data/lib/generators/view_primitives/add/templates/map_area/map_area_component.rb.tt +72 -0
  74. data/lib/generators/view_primitives/add/templates/mega_menu/mega_menu_component.rb.tt +130 -0
  75. data/lib/generators/view_primitives/add/templates/mega_menu/mega_menu_controller.js +23 -0
  76. data/lib/generators/view_primitives/add/templates/menubar/menubar_component.rb.tt +33 -0
  77. data/lib/generators/view_primitives/add/templates/menubar/menubar_controller.js +34 -0
  78. data/lib/generators/view_primitives/add/templates/menubar/menubar_menu_component.rb.tt +34 -0
  79. data/lib/generators/view_primitives/add/templates/navbar/navbar_component.rb.tt +90 -0
  80. data/lib/generators/view_primitives/add/templates/navbar/navbar_controller.js +11 -0
  81. data/lib/generators/view_primitives/add/templates/navigation_menu/navigation_menu_component.rb.tt +132 -0
  82. data/lib/generators/view_primitives/add/templates/navigation_menu/navigation_menu_controller.js +25 -0
  83. data/lib/generators/view_primitives/add/templates/number_input/number_input_component.rb.tt +34 -0
  84. data/lib/generators/view_primitives/add/templates/pagination/pagination_component.rb.tt +97 -0
  85. data/lib/generators/view_primitives/add/templates/picture/picture_component.rb.tt +63 -0
  86. data/lib/generators/view_primitives/add/templates/popover/popover_component.rb.tt +56 -0
  87. data/lib/generators/view_primitives/add/templates/popover/popover_controller.js +17 -0
  88. data/lib/generators/view_primitives/add/templates/progress/progress_component.rb.tt +28 -0
  89. data/lib/generators/view_primitives/add/templates/qr_code/qr_code_component.rb.tt +39 -0
  90. data/lib/generators/view_primitives/add/templates/radio_group/radio_group_component.rb.tt +51 -0
  91. data/lib/generators/view_primitives/add/templates/range/range_component.rb.tt +40 -0
  92. data/lib/generators/view_primitives/add/templates/rating/rating_component.rb.tt +42 -0
  93. data/lib/generators/view_primitives/add/templates/rating_input/rating_controller.js +47 -0
  94. data/lib/generators/view_primitives/add/templates/rating_input/rating_input_component.rb.tt +79 -0
  95. data/lib/generators/view_primitives/add/templates/resizable/resizable_component.rb.tt +91 -0
  96. data/lib/generators/view_primitives/add/templates/resizable/resizable_controller.js +38 -0
  97. data/lib/generators/view_primitives/add/templates/scroll_area/scroll_area_component.rb.tt +41 -0
  98. data/lib/generators/view_primitives/add/templates/search_input/search_input_component.rb.tt +50 -0
  99. data/lib/generators/view_primitives/add/templates/select/select_component.rb.tt +45 -0
  100. data/lib/generators/view_primitives/add/templates/separator/separator_component.rb.tt +25 -0
  101. data/lib/generators/view_primitives/add/templates/sheet/sheet_component.rb.tt +78 -0
  102. data/lib/generators/view_primitives/add/templates/sheet/sheet_controller.js +15 -0
  103. data/lib/generators/view_primitives/add/templates/sidebar/sidebar_component.rb.tt +169 -0
  104. data/lib/generators/view_primitives/add/templates/sidebar/sidebar_controller.js +11 -0
  105. data/lib/generators/view_primitives/add/templates/skeleton/skeleton_component.rb.tt +16 -0
  106. data/lib/generators/view_primitives/add/templates/speed_dial/speed_dial_component.rb.tt +111 -0
  107. data/lib/generators/view_primitives/add/templates/speed_dial/speed_dial_controller.js +22 -0
  108. data/lib/generators/view_primitives/add/templates/spinner/spinner_component.rb.tt +27 -0
  109. data/lib/generators/view_primitives/add/templates/stepper/stepper_component.rb.tt +99 -0
  110. data/lib/generators/view_primitives/add/templates/switch/switch_component.rb.tt +51 -0
  111. data/lib/generators/view_primitives/add/templates/tabs/tabs_component.html.erb +50 -0
  112. data/lib/generators/view_primitives/add/templates/tabs/tabs_component.rb.tt +16 -0
  113. data/lib/generators/view_primitives/add/templates/tabs/tabs_controller.js +26 -0
  114. data/lib/generators/view_primitives/add/templates/tabs/tabs_item_component.rb.tt +15 -0
  115. data/lib/generators/view_primitives/add/templates/textarea/textarea_component.rb.tt +24 -0
  116. data/lib/generators/view_primitives/add/templates/timeline/timeline_component.rb.tt +78 -0
  117. data/lib/generators/view_primitives/add/templates/timepicker/timepicker_component.rb.tt +140 -0
  118. data/lib/generators/view_primitives/add/templates/timepicker/timepicker_controller.js +92 -0
  119. data/lib/generators/view_primitives/add/templates/toaster/toaster_component.rb.tt +152 -0
  120. data/lib/generators/view_primitives/add/templates/toaster/toaster_controller.js +88 -0
  121. data/lib/generators/view_primitives/add/templates/toggle/toggle_component.rb.tt +41 -0
  122. data/lib/generators/view_primitives/add/templates/toggle/toggle_controller.js +12 -0
  123. data/lib/generators/view_primitives/add/templates/toggle_group/toggle_group_component.rb.tt +32 -0
  124. data/lib/generators/view_primitives/add/templates/toggle_group/toggle_group_controller.js +38 -0
  125. data/lib/generators/view_primitives/add/templates/tooltip/tooltip_component.rb.tt +42 -0
  126. data/lib/generators/view_primitives/add/templates/video/video_component.rb.tt +92 -0
  127. data/lib/generators/view_primitives/add/templates/wysiwyg/wysiwyg_component.rb.tt +88 -0
  128. data/lib/generators/view_primitives/add/templates/wysiwyg/wysiwyg_controller.js +40 -0
  129. data/lib/generators/view_primitives/components.rb +62 -0
  130. data/lib/generators/view_primitives/detector.rb +43 -0
  131. data/lib/generators/view_primitives/install/install_generator.rb +65 -0
  132. data/lib/generators/view_primitives/install/templates/application_component.rb.tt +5 -0
  133. data/lib/generators/view_primitives/install/templates/view_primitives.css +67 -0
  134. data/lib/generators/view_primitives/list/list_generator.rb +25 -0
  135. data/lib/view_primitives/class_helper.rb +11 -0
  136. data/lib/view_primitives/component_helper.rb +20 -0
  137. data/lib/view_primitives/railtie.rb +21 -0
  138. data/lib/view_primitives/version.rb +5 -0
  139. data/lib/view_primitives.rb +12 -0
  140. metadata +267 -0
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UI
4
+ class TooltipComponent < ApplicationComponent
5
+ BUBBLE_BASE = "absolute z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance " \
6
+ "bg-foreground text-background " \
7
+ "opacity-0 group-hover:opacity-100 pointer-events-none whitespace-nowrap " \
8
+ "transition-opacity duration-200"
9
+
10
+ POSITIONS = {
11
+ top: "bottom-full left-1/2 -translate-x-1/2 mb-2",
12
+ bottom: "top-full left-1/2 -translate-x-1/2 mt-2",
13
+ left: "right-full top-1/2 -translate-y-1/2 mr-2",
14
+ right: "left-full top-1/2 -translate-y-1/2 ml-2"
15
+ }.freeze
16
+
17
+ def initialize(text:, side: :top, **html_attrs)
18
+ @text = text
19
+ @side = side.to_sym
20
+ @extra_class = html_attrs.delete(:class)
21
+ @html_attrs = html_attrs
22
+ end
23
+
24
+ def call
25
+ content_tag(:span,
26
+ class: cn("relative inline-flex group", @extra_class),
27
+ **@html_attrs) do
28
+ concat content
29
+ concat tooltip_bubble
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def tooltip_bubble
36
+ content_tag(:span,
37
+ @text,
38
+ class: cn(BUBBLE_BASE, POSITIONS.fetch(@side, POSITIONS[:top])),
39
+ role: "tooltip")
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UI
4
+ class VideoComponent < ApplicationComponent
5
+ BASE = "max-w-full"
6
+
7
+ # Add <source> elements via v.with_source(src:, type:)
8
+ renders_many :sources, "UI::VideoComponent::SourceComponent"
9
+ # Add <track> elements via v.with_track(src:, kind:, label:, srclang:)
10
+ renders_many :tracks, "UI::VideoComponent::TrackComponent"
11
+
12
+ # poster: URL of the preview image shown before playback
13
+ # controls: show native browser controls (default: true)
14
+ # autoplay: start playing automatically — requires muted: true
15
+ # muted: mute the audio track
16
+ # loop: loop playback
17
+ # preload: :auto | :metadata (default) | :none
18
+ # playsinline: play inline on iOS instead of fullscreen
19
+ # width / height: explicit dimensions (prefer CSS or Aspect Ratio)
20
+ def initialize(poster: nil, controls: true, autoplay: false, muted: false,
21
+ loop: false, preload: :metadata, playsinline: true,
22
+ width: nil, height: nil, **html_attrs)
23
+ @poster = poster
24
+ @controls = controls
25
+ @autoplay = autoplay
26
+ @muted = muted
27
+ @loop = loop
28
+ @preload = preload
29
+ @playsinline = playsinline
30
+ @width = width
31
+ @height = height
32
+ @extra_class = html_attrs.delete(:class)
33
+ @html_attrs = html_attrs
34
+ end
35
+
36
+ def call
37
+ attrs = { class: cn(BASE, @extra_class), preload: @preload }
38
+ attrs[:poster] = @poster if @poster
39
+ attrs[:controls] = true if @controls
40
+ attrs[:autoplay] = true if @autoplay
41
+ attrs[:muted] = true if @muted || @autoplay
42
+ attrs[:loop] = true if @loop
43
+ attrs[:playsinline] = true if @playsinline
44
+ attrs[:width] = @width if @width
45
+ attrs[:height] = @height if @height
46
+
47
+ content_tag(:video, **attrs, **@html_attrs) do
48
+ sources.each { |s| concat s }
49
+ tracks.each { |t| concat t }
50
+ concat content if content?
51
+ end
52
+ end
53
+
54
+ # Represents a <source> element inside <video>.
55
+ # v.with_source(src: "video.mp4", type: "video/mp4")
56
+ class SourceComponent < ApplicationComponent
57
+ def initialize(src:, type:, **html_attrs)
58
+ @src = src
59
+ @type = type
60
+ @html_attrs = html_attrs
61
+ end
62
+
63
+ def call
64
+ tag.source(src: @src, type: @type, **@html_attrs)
65
+ end
66
+ end
67
+
68
+ # Represents a <track> element (captions, subtitles, chapters).
69
+ # v.with_track(src: "captions.vtt", kind: :subtitles, label: "English", srclang: "en")
70
+ # kind: :subtitles | :captions | :descriptions | :chapters | :metadata
71
+ class TrackComponent < ApplicationComponent
72
+ KINDS = %i[subtitles captions descriptions chapters metadata].freeze
73
+
74
+ def initialize(src:, kind: :subtitles, label: nil, srclang: nil, default: false, **html_attrs)
75
+ @src = src
76
+ @kind = KINDS.include?(kind.to_sym) ? kind.to_sym : :subtitles
77
+ @label = label
78
+ @srclang = srclang
79
+ @default = default
80
+ @html_attrs = html_attrs
81
+ end
82
+
83
+ def call
84
+ attrs = { src: @src, kind: @kind }
85
+ attrs[:label] = @label if @label
86
+ attrs[:srclang] = @srclang if @srclang
87
+ attrs[:default] = true if @default
88
+ tag.track(**attrs, **@html_attrs)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UI
4
+ class WysiwygComponent < ApplicationComponent
5
+ # Rich-text editor wrapper — picks between Trix and Quill via adapter:.
6
+ #
7
+ # Usage:
8
+ # ui :wysiwyg, name: "body" # Trix (default)
9
+ # ui :wysiwyg, name: "body", adapter: :quill
10
+ # ui :wysiwyg, name: "body", adapter: :quill,
11
+ # placeholder: "Write something...", height: 400
12
+ #
13
+ # Trix setup (ActionText):
14
+ # bundle add actiontext
15
+ # rails action_text:install
16
+ #
17
+ # Quill setup:
18
+ # # config/importmap.rb
19
+ # pin "quill", to: "https://esm.sh/quill@2"
20
+ # # In your CSS entry point:
21
+ # @import url("https://esm.sh/quill@2/dist/quill.snow.css");
22
+ #
23
+ # Options:
24
+ # name: form field name (required)
25
+ # adapter: :trix (default) | :quill
26
+ # value: initial HTML content
27
+ # placeholder: placeholder text
28
+ # toolbar: show editor toolbar (default: true)
29
+ # height: editor content area height in px (default: 200; Quill only)
30
+
31
+ ADAPTERS = %w[trix quill].freeze
32
+
33
+ WRAPPER_CLS = "rounded-md border border-input bg-background shadow-xs"
34
+
35
+ def initialize(name:, adapter: :trix, value: nil, placeholder: nil,
36
+ toolbar: true, height: 200, **html_attrs)
37
+ @name = name
38
+ @adapter = ADAPTERS.include?(adapter.to_s) ? adapter.to_s : "trix"
39
+ @value = value
40
+ @placeholder = placeholder
41
+ @toolbar = toolbar
42
+ @height = height
43
+ @extra_class = html_attrs.delete(:class)
44
+ @html_attrs = html_attrs
45
+ @input_id = "wysiwyg-#{SecureRandom.hex(4)}"
46
+ end
47
+
48
+ def call
49
+ content_tag(:div, class: cn(WRAPPER_CLS, @extra_class)) do
50
+ @adapter == "quill" ? quill_markup : trix_markup
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def trix_markup
57
+ safe_join([
58
+ tag.input(type: "hidden", id: @input_id, name: @name, value: @value),
59
+ tag.send(:"trix-editor",
60
+ input: @input_id,
61
+ placeholder: @placeholder,
62
+ class: "trix-content min-h-[200px] px-3 py-2 text-sm focus:outline-none")
63
+ ])
64
+ end
65
+
66
+ def quill_markup
67
+ content_tag(:div,
68
+ data: {
69
+ controller: "wysiwyg",
70
+ wysiwyg_adapter_value: "quill",
71
+ wysiwyg_placeholder_value: @placeholder.to_s,
72
+ wysiwyg_height_value: @height,
73
+ wysiwyg_toolbar_value: @toolbar
74
+ }) do
75
+ safe_join([
76
+ tag.div(
77
+ data: { wysiwyg_target: "editor" },
78
+ style: "height: #{@height}px"),
79
+ tag.input(
80
+ type: "hidden",
81
+ name: @name,
82
+ value: @value,
83
+ data: { wysiwyg_target: "input" })
84
+ ])
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,40 @@
1
+ // Quill adapter — requires Quill v2 in your importmap:
2
+ // pin "quill", to: "https://esm.sh/quill@2"
3
+ // Also add Quill's stylesheet to your CSS entry point:
4
+ // @import url("https://esm.sh/quill@2/dist/quill.snow.css");
5
+ import { Controller } from "@hotwired/stimulus"
6
+
7
+ export default class extends Controller {
8
+ static targets = ["editor", "input"]
9
+ static values = {
10
+ adapter: { type: String, default: "trix" },
11
+ placeholder: { type: String, default: "" },
12
+ height: { type: Number, default: 200 },
13
+ toolbar: { type: Boolean, default: true }
14
+ }
15
+
16
+ #quill = null
17
+
18
+ connect() {
19
+ if (this.adapterValue === "quill") this.#initQuill()
20
+ }
21
+
22
+ disconnect() {
23
+ this.#quill = null
24
+ }
25
+
26
+ async #initQuill() {
27
+ const { default: Quill } = await import("quill")
28
+ this.#quill = new Quill(this.editorTarget, {
29
+ theme: "snow",
30
+ placeholder: this.placeholderValue,
31
+ modules: { toolbar: this.toolbarValue }
32
+ })
33
+ if (this.inputTarget.value) {
34
+ this.#quill.root.innerHTML = this.inputTarget.value
35
+ }
36
+ this.#quill.on("text-change", () => {
37
+ this.inputTarget.value = this.#quill.root.innerHTML
38
+ })
39
+ }
40
+ }
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewPrimitives
4
+ module Generators
5
+ module Components
6
+ TEMPLATE_ROOT = File.expand_path("add/templates", __dir__)
7
+
8
+ # Stimulus controllers not colocated with the component template directory.
9
+ EXTRA_STIMULUS = {
10
+ "alert_dialog" => {source: "dialog/dialog_controller.js", name: "dialog"}
11
+ }.freeze
12
+
13
+ # Post-install instructions for components that require external dependencies.
14
+ SETUP_NOTES = {
15
+ "chart" => <<~TEXT,
16
+ Chart requires Chart.js. Add it to your importmap:
17
+
18
+ # config/importmap.rb
19
+ pin "chart.js", to: "https://esm.sh/chart.js@4"
20
+
21
+ Then use the component:
22
+
23
+ ui :chart, type: :bar,
24
+ labels: ["Jan", "Feb", "Mar"],
25
+ datasets: [{ label: "Revenue", data: [100, 200, 150] }]
26
+ TEXT
27
+ "wysiwyg" => <<~TEXT
28
+ WYSIWYG defaults to Trix (adapter: :trix). To use Trix, install ActionText:
29
+
30
+ bundle add actiontext
31
+ rails action_text:install
32
+
33
+ To use Quill (adapter: :quill), add it to your importmap:
34
+
35
+ # config/importmap.rb
36
+ pin "quill", to: "https://esm.sh/quill@2"
37
+
38
+ Also add Quill's stylesheet to your CSS entry point:
39
+
40
+ @import url("https://esm.sh/quill@2/dist/quill.snow.css");
41
+
42
+ Usage:
43
+
44
+ ui :wysiwyg, name: "body"
45
+ ui :wysiwyg, name: "body", adapter: :quill, placeholder: "Write something..."
46
+ TEXT
47
+ }.freeze
48
+
49
+ def self.supported
50
+ @supported ||= Dir.children(TEMPLATE_ROOT).sort.freeze
51
+ end
52
+
53
+ def self.primary_path(component)
54
+ "app/components/ui/#{component}_component.rb"
55
+ end
56
+
57
+ def self.installed?(component, root)
58
+ File.exist?(File.join(root, primary_path(component)))
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewPrimitives
4
+ module Generators
5
+ module Detector
6
+ TAILWIND_ENTRIES = [
7
+ {path: "app/assets/tailwind/application.css", stylesheets: "app/assets/stylesheets"},
8
+ {path: "app/assets/stylesheets/application.tailwind.css", stylesheets: "app/assets/stylesheets"},
9
+ {path: "app/assets/builds/tailwind.css", stylesheets: "app/assets/stylesheets"},
10
+ {path: "app/frontend/entrypoints/application.css", stylesheets: "app/frontend/stylesheets"},
11
+ {path: "app/javascript/entrypoints/application.css", stylesheets: "app/javascript/stylesheets"},
12
+ {path: "app/javascript/application.css", stylesheets: "app/javascript/stylesheets"}
13
+ ].freeze
14
+
15
+ JS_CONTROLLER_DIRS = %w[app/javascript/controllers app/frontend/controllers].freeze
16
+
17
+ private
18
+
19
+ def tailwind_entry
20
+ @tailwind_entry ||= TAILWIND_ENTRIES.find do |entry|
21
+ File.exist?(File.join(destination_root, entry[:path]))
22
+ end
23
+ end
24
+
25
+ def tailwind_entry_path = tailwind_entry&.fetch(:path)
26
+ def css_dest_dir = tailwind_entry&.fetch(:stylesheets) || "app/assets/stylesheets"
27
+ def css_dest_path = "#{css_dest_dir}/view_primitives.css"
28
+
29
+ def css_import_path
30
+ return unless tailwind_entry_path
31
+
32
+ Pathname.new("#{css_dest_dir}/view_primitives")
33
+ .relative_path_from(Pathname.new(File.dirname(tailwind_entry_path))).to_s
34
+ end
35
+
36
+ def js_controllers_dir
37
+ @js_controllers_dir ||= JS_CONTROLLER_DIRS.find do |dir|
38
+ File.exist?(File.join(destination_root, dir))
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../detector"
4
+
5
+ module ViewPrimitives
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ include Detector
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ def verify_ui_inflection
13
+ return if "ui/button".camelize == "UI::ButtonComponent"
14
+
15
+ say "\n Warning: ActiveSupport inflection for `UI` is not configured.", :yellow
16
+ say " ViewPrimitives expects `ui/button` to resolve to `UI::ButtonComponent`.", :yellow
17
+ say " The gem registers this automatically — restart the Rails server if you just installed.\n", :yellow
18
+ end
19
+
20
+ def create_application_component
21
+ target = "app/components/application_component.rb"
22
+
23
+ if File.exist?(File.join(destination_root, target))
24
+ say " ApplicationComponent already exists. Add `include ViewPrimitives::ClassHelper` manually.", :yellow
25
+ else
26
+ template "application_component.rb.tt", target
27
+ end
28
+ end
29
+
30
+ def create_css_variables
31
+ copy_file "view_primitives.css", css_dest_path
32
+ end
33
+
34
+ def inject_css_import
35
+ entry = tailwind_entry_path
36
+
37
+ unless entry
38
+ say "\n Could not detect a Tailwind CSS entry point.", :yellow
39
+ say " Add this line to your main CSS file:\n"
40
+ say " @import \"./view_primitives\";\n"
41
+ say " Common locations: app/assets/tailwind/application.css, " \
42
+ "app/assets/stylesheets/application.tailwind.css, app/javascript/application.css\n", :cyan
43
+ return
44
+ end
45
+
46
+ entry_content = File.read(File.join(destination_root, entry))
47
+
48
+ if entry_content.include?("view_primitives")
49
+ say " #{entry} already imports view_primitives — skipping.", :yellow
50
+ return
51
+ end
52
+
53
+ import_line = "@import \"#{css_import_path}\";\n"
54
+
55
+ if entry_content.include?('@import "tailwindcss"')
56
+ inject_into_file entry, import_line, after: "@import \"tailwindcss\"\n"
57
+ elsif entry_content.include?("@import 'tailwindcss'")
58
+ inject_into_file entry, import_line, after: "@import 'tailwindcss'\n"
59
+ else
60
+ append_to_file entry, "\n#{import_line}"
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationComponent < ViewComponent::Base
4
+ include ViewPrimitives::ClassHelper
5
+ end
@@ -0,0 +1,67 @@
1
+ @theme inline {
2
+ --color-background: var(--background);
3
+ --color-foreground: var(--foreground);
4
+ --color-card: var(--card);
5
+ --color-card-foreground: var(--card-foreground);
6
+ --color-popover: var(--popover);
7
+ --color-popover-foreground: var(--popover-foreground);
8
+ --color-primary: var(--primary);
9
+ --color-primary-foreground: var(--primary-foreground);
10
+ --color-secondary: var(--secondary);
11
+ --color-secondary-foreground: var(--secondary-foreground);
12
+ --color-muted: var(--muted);
13
+ --color-muted-foreground: var(--muted-foreground);
14
+ --color-accent: var(--accent);
15
+ --color-accent-foreground: var(--accent-foreground);
16
+ --color-destructive: var(--destructive);
17
+ --color-border: var(--border);
18
+ --color-input: var(--input);
19
+ --color-ring: var(--ring);
20
+ --radius-sm: calc(var(--radius) - 4px);
21
+ --radius-md: calc(var(--radius) - 2px);
22
+ --radius-lg: var(--radius);
23
+ --radius-xl: calc(var(--radius) + 4px);
24
+ }
25
+
26
+ :root {
27
+ --background: oklch(1 0 0);
28
+ --foreground: oklch(0.145 0 0);
29
+ --card: oklch(1 0 0);
30
+ --card-foreground: oklch(0.145 0 0);
31
+ --popover: oklch(1 0 0);
32
+ --popover-foreground: oklch(0.145 0 0);
33
+ --primary: oklch(0.205 0 0);
34
+ --primary-foreground: oklch(0.985 0 0);
35
+ --secondary: oklch(0.97 0 0);
36
+ --secondary-foreground: oklch(0.205 0 0);
37
+ --muted: oklch(0.97 0 0);
38
+ --muted-foreground: oklch(0.556 0 0);
39
+ --accent: oklch(0.97 0 0);
40
+ --accent-foreground: oklch(0.205 0 0);
41
+ --destructive: oklch(0.577 0.245 27.325);
42
+ --border: oklch(0.922 0 0);
43
+ --input: oklch(0.922 0 0);
44
+ --ring: oklch(0.708 0 0);
45
+ --radius: 0.625rem;
46
+ }
47
+
48
+ .dark {
49
+ --background: oklch(0.145 0 0);
50
+ --foreground: oklch(0.985 0 0);
51
+ --card: oklch(0.205 0 0);
52
+ --card-foreground: oklch(0.985 0 0);
53
+ --popover: oklch(0.205 0 0);
54
+ --popover-foreground: oklch(0.985 0 0);
55
+ --primary: oklch(0.922 0 0);
56
+ --primary-foreground: oklch(0.205 0 0);
57
+ --secondary: oklch(0.269 0 0);
58
+ --secondary-foreground: oklch(0.985 0 0);
59
+ --muted: oklch(0.269 0 0);
60
+ --muted-foreground: oklch(0.708 0 0);
61
+ --accent: oklch(0.269 0 0);
62
+ --accent-foreground: oklch(0.985 0 0);
63
+ --destructive: oklch(0.704 0.191 22.216);
64
+ --border: oklch(1 0 0 / 10%);
65
+ --input: oklch(1 0 0 / 15%);
66
+ --ring: oklch(0.556 0 0);
67
+ }
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../components"
4
+
5
+ module ViewPrimitives
6
+ module Generators
7
+ class ListGenerator < Rails::Generators::Base
8
+ desc "List available ViewPrimitives components and whether they are installed"
9
+
10
+ def list_components
11
+ say "\nViewPrimitives components:\n\n", :bold
12
+ say "COMPONENT STATUS"
13
+ say "-" * 32
14
+
15
+ Components.supported.each do |component|
16
+ status = Components.installed?(component, destination_root) ? "installed" : "—"
17
+ color = (status == "installed") ? :green : :cyan
18
+ say format("%-18s %s", component, status), color
19
+ end
20
+
21
+ say "\nInstall: rails g view_primitives:add <name>\n", :cyan
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewPrimitives
4
+ module ClassHelper
5
+ private
6
+
7
+ def cn(*classes)
8
+ classes.flatten.compact.reject(&:empty?).join(" ")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/inflections"
4
+
5
+ module ViewPrimitives
6
+ module ComponentHelper
7
+ # <%= ui :button, variant: :outline do %>Click<% end %>
8
+ def ui(name, *, **, &)
9
+ klass = "UI::#{name.to_s.camelize}Component".safe_constantize
10
+
11
+ unless klass
12
+ raise ViewPrimitives::ComponentNotFoundError,
13
+ "Component `UI::#{name.to_s.camelize}Component` not found. " \
14
+ "Run `rails g view_primitives:add #{name}` to generate it."
15
+ end
16
+
17
+ render(klass.new(*, **), &)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewPrimitives
4
+ class Railtie < Rails::Railtie
5
+ initializer "view_primitives.inflections" do
6
+ ActiveSupport::Inflector.inflections(:en) { |inflect| inflect.acronym "UI" }
7
+ end
8
+
9
+ generators do
10
+ %w[install add list].each do |gen|
11
+ require "generators/view_primitives/#{gen}/#{gen}_generator"
12
+ end
13
+ end
14
+
15
+ initializer "view_primitives.component_helper" do
16
+ %i[action_view action_mailer].each do |hook|
17
+ ActiveSupport.on_load(hook) { include ViewPrimitives::ComponentHelper }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewPrimitives
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "view_primitives/version"
4
+ require_relative "view_primitives/class_helper"
5
+ require_relative "view_primitives/component_helper"
6
+
7
+ module ViewPrimitives
8
+ class Error < StandardError; end
9
+ class ComponentNotFoundError < Error; end
10
+ end
11
+
12
+ require_relative "view_primitives/railtie" if defined?(Rails::Railtie)