senren-ui 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 (182) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +33 -0
  3. data/CONTRIBUTING.md +63 -0
  4. data/LICENSE +21 -0
  5. data/README.md +135 -0
  6. data/Rakefile +22 -0
  7. data/docs/visual_style.md +51 -0
  8. data/lib/generators/senren/component/component_generator.rb +62 -0
  9. data/lib/generators/senren/component/templates/component.html.erb.tt +3 -0
  10. data/lib/generators/senren/component/templates/component.rb.tt +13 -0
  11. data/lib/generators/senren/component/templates/component_test.rb.tt +16 -0
  12. data/lib/generators/senren/component/templates/controller.js.tt +23 -0
  13. data/lib/generators/senren/component/templates/system_test.rb.tt +7 -0
  14. data/lib/generators/senren/install/install_generator.rb +67 -0
  15. data/lib/generators/senren/install/templates/base_component.rb.tt +45 -0
  16. data/lib/generators/senren/install/templates/conventions.md.tt +66 -0
  17. data/lib/generators/senren/install/templates/installed_components.yml.tt +4 -0
  18. data/lib/generators/senren/install/templates/senren.css.tt +164 -0
  19. data/lib/senren/rails/component_copier.rb +111 -0
  20. data/lib/senren/rails/doctor.rb +86 -0
  21. data/lib/senren/rails/engine.rb +16 -0
  22. data/lib/senren/rails/host_paths.rb +36 -0
  23. data/lib/senren/rails/installer.rb +83 -0
  24. data/lib/senren/rails/llms_writer.rb +149 -0
  25. data/lib/senren/rails/registry.rb +161 -0
  26. data/lib/senren/rails/skill_writer.rb +166 -0
  27. data/lib/senren/rails/version.rb +7 -0
  28. data/lib/senren/rails.rb +39 -0
  29. data/lib/tasks/senren.rake +74 -0
  30. data/registry/components.yml +1053 -0
  31. data/registry/groups.yml +25 -0
  32. data/registry/recipes.yml +79 -0
  33. data/templates/components/accordion/accordion_component.html.erb +16 -0
  34. data/templates/components/accordion/accordion_component.rb +31 -0
  35. data/templates/components/activity_feed/activity_feed_component.html.erb +22 -0
  36. data/templates/components/activity_feed/activity_feed_component.rb +19 -0
  37. data/templates/components/alert/alert_component.html.erb +9 -0
  38. data/templates/components/alert/alert_component.rb +18 -0
  39. data/templates/components/alert_dialog/alert_dialog_component.html.erb +34 -0
  40. data/templates/components/alert_dialog/alert_dialog_component.rb +21 -0
  41. data/templates/components/api_key_field/api_key_field_component.html.erb +13 -0
  42. data/templates/components/api_key_field/api_key_field_component.rb +20 -0
  43. data/templates/components/app_shell/app_shell_component.html.erb +28 -0
  44. data/templates/components/app_shell/app_shell_component.rb +24 -0
  45. data/templates/components/aspect_ratio/aspect_ratio_component.html.erb +3 -0
  46. data/templates/components/aspect_ratio/aspect_ratio_component.rb +14 -0
  47. data/templates/components/avatar/avatar_component.html.erb +27 -0
  48. data/templates/components/avatar/avatar_component.rb +30 -0
  49. data/templates/components/badge/badge_component.html.erb +1 -0
  50. data/templates/components/badge/badge_component.rb +16 -0
  51. data/templates/components/billing_plan_card/billing_plan_card_component.html.erb +28 -0
  52. data/templates/components/billing_plan_card/billing_plan_card_component.rb +27 -0
  53. data/templates/components/breadcrumb/breadcrumb_component.html.erb +23 -0
  54. data/templates/components/breadcrumb/breadcrumb_component.rb +30 -0
  55. data/templates/components/bulk_action_bar/bulk_action_bar_component.html.erb +12 -0
  56. data/templates/components/bulk_action_bar/bulk_action_bar_component.rb +24 -0
  57. data/templates/components/button/button_component.html.erb +6 -0
  58. data/templates/components/button/button_component.rb +29 -0
  59. data/templates/components/calendar/calendar_component.html.erb +21 -0
  60. data/templates/components/calendar/calendar_component.rb +30 -0
  61. data/templates/components/card/card_component.html.erb +13 -0
  62. data/templates/components/card/card_component.rb +17 -0
  63. data/templates/components/carousel/carousel_component.html.erb +68 -0
  64. data/templates/components/carousel/carousel_component.rb +34 -0
  65. data/templates/components/checkbox/checkbox_component.html.erb +8 -0
  66. data/templates/components/checkbox/checkbox_component.rb +19 -0
  67. data/templates/components/checkbox_group/checkbox_group_component.html.erb +10 -0
  68. data/templates/components/checkbox_group/checkbox_group_component.rb +30 -0
  69. data/templates/components/clipboard/clipboard_component.html.erb +7 -0
  70. data/templates/components/clipboard/clipboard_component.rb +17 -0
  71. data/templates/components/codeblock/codeblock_component.html.erb +11 -0
  72. data/templates/components/codeblock/codeblock_component.rb +31 -0
  73. data/templates/components/collapsible/collapsible_component.html.erb +9 -0
  74. data/templates/components/collapsible/collapsible_component.rb +19 -0
  75. data/templates/components/combobox/combobox_component.html.erb +19 -0
  76. data/templates/components/combobox/combobox_component.rb +38 -0
  77. data/templates/components/command/command_component.html.erb +22 -0
  78. data/templates/components/command/command_component.rb +38 -0
  79. data/templates/components/context_menu/context_menu_component.html.erb +11 -0
  80. data/templates/components/context_menu/context_menu_component.rb +11 -0
  81. data/templates/components/data_table/data_table_component.html.erb +50 -0
  82. data/templates/components/data_table/data_table_component.rb +42 -0
  83. data/templates/components/date_picker/date_picker_component.html.erb +5 -0
  84. data/templates/components/date_picker/date_picker_component.rb +21 -0
  85. data/templates/components/dialog/dialog_component.html.erb +38 -0
  86. data/templates/components/dialog/dialog_component.rb +22 -0
  87. data/templates/components/dropdown_menu/dropdown_menu_component.html.erb +12 -0
  88. data/templates/components/dropdown_menu/dropdown_menu_component.rb +36 -0
  89. data/templates/components/empty_state/empty_state_component.html.erb +18 -0
  90. data/templates/components/empty_state/empty_state_component.rb +22 -0
  91. data/templates/components/filter_bar/filter_bar_component.html.erb +5 -0
  92. data/templates/components/filter_bar/filter_bar_component.rb +15 -0
  93. data/templates/components/form/form_component.html.erb +3 -0
  94. data/templates/components/form/form_component.rb +18 -0
  95. data/templates/components/hover_card/hover_card_component.html.erb +10 -0
  96. data/templates/components/hover_card/hover_card_component.rb +11 -0
  97. data/templates/components/input/input_component.html.erb +1 -0
  98. data/templates/components/input/input_component.rb +28 -0
  99. data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +35 -0
  100. data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +26 -0
  101. data/templates/components/label/label_component.html.erb +4 -0
  102. data/templates/components/label/label_component.rb +19 -0
  103. data/templates/components/link/link_component.html.erb +1 -0
  104. data/templates/components/link/link_component.rb +25 -0
  105. data/templates/components/masked_input/masked_input_component.html.erb +1 -0
  106. data/templates/components/masked_input/masked_input_component.rb +18 -0
  107. data/templates/components/native_select/native_select_component.html.erb +14 -0
  108. data/templates/components/native_select/native_select_component.rb +52 -0
  109. data/templates/components/page_header/page_header_component.html.erb +20 -0
  110. data/templates/components/page_header/page_header_component.rb +19 -0
  111. data/templates/components/pagination/pagination_component.html.erb +11 -0
  112. data/templates/components/pagination/pagination_component.rb +24 -0
  113. data/templates/components/popover/popover_component.html.erb +9 -0
  114. data/templates/components/popover/popover_component.rb +11 -0
  115. data/templates/components/progress/progress_component.html.erb +11 -0
  116. data/templates/components/progress/progress_component.rb +26 -0
  117. data/templates/components/radio_button/radio_button_component.html.erb +8 -0
  118. data/templates/components/radio_button/radio_button_component.rb +19 -0
  119. data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.html.erb +32 -0
  120. data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.rb +30 -0
  121. data/templates/components/search_input/search_input_component.html.erb +14 -0
  122. data/templates/components/search_input/search_input_component.rb +18 -0
  123. data/templates/components/select/select_component.html.erb +1 -0
  124. data/templates/components/select/select_component.rb +19 -0
  125. data/templates/components/separator/separator_component.html.erb +1 -0
  126. data/templates/components/separator/separator_component.rb +12 -0
  127. data/templates/components/settings_section/settings_section_component.html.erb +20 -0
  128. data/templates/components/settings_section/settings_section_component.rb +18 -0
  129. data/templates/components/sheet/sheet_component.html.erb +37 -0
  130. data/templates/components/sheet/sheet_component.rb +27 -0
  131. data/templates/components/shortcut_key/shortcut_key_component.html.erb +6 -0
  132. data/templates/components/shortcut_key/shortcut_key_component.rb +15 -0
  133. data/templates/components/sidebar/sidebar_component.html.erb +14 -0
  134. data/templates/components/sidebar/sidebar_component.rb +37 -0
  135. data/templates/components/skeleton/skeleton_component.html.erb +1 -0
  136. data/templates/components/skeleton/skeleton_component.rb +13 -0
  137. data/templates/components/stat_card/stat_card_component.html.erb +20 -0
  138. data/templates/components/stat_card/stat_card_component.rb +31 -0
  139. data/templates/components/switch/switch_component.html.erb +11 -0
  140. data/templates/components/switch/switch_component.rb +19 -0
  141. data/templates/components/table/table_component.html.erb +26 -0
  142. data/templates/components/table/table_component.rb +35 -0
  143. data/templates/components/tabs/tabs_component.html.erb +18 -0
  144. data/templates/components/tabs/tabs_component.rb +35 -0
  145. data/templates/components/team_member_list/team_member_list_component.html.erb +22 -0
  146. data/templates/components/team_member_list/team_member_list_component.rb +26 -0
  147. data/templates/components/textarea/textarea_component.html.erb +1 -0
  148. data/templates/components/textarea/textarea_component.rb +23 -0
  149. data/templates/components/theme_toggle/theme_toggle_component.html.erb +4 -0
  150. data/templates/components/theme_toggle/theme_toggle_component.rb +15 -0
  151. data/templates/components/tooltip/tooltip_component.html.erb +9 -0
  152. data/templates/components/tooltip/tooltip_component.rb +16 -0
  153. data/templates/components/top_nav/top_nav_component.html.erb +21 -0
  154. data/templates/components/top_nav/top_nav_component.rb +44 -0
  155. data/templates/components/typography/typography_component.html.erb +1 -0
  156. data/templates/components/typography/typography_component.rb +24 -0
  157. data/templates/controllers/accordion_controller.js +27 -0
  158. data/templates/controllers/alert_dialog_controller.js +38 -0
  159. data/templates/controllers/api_key_field_controller.js +36 -0
  160. data/templates/controllers/calendar_controller.js +16 -0
  161. data/templates/controllers/carousel_controller.js +50 -0
  162. data/templates/controllers/clipboard_controller.js +17 -0
  163. data/templates/controllers/collapsible_controller.js +13 -0
  164. data/templates/controllers/combobox_controller.js +64 -0
  165. data/templates/controllers/command_controller.js +80 -0
  166. data/templates/controllers/context_menu_controller.js +36 -0
  167. data/templates/controllers/data_table_controller.js +34 -0
  168. data/templates/controllers/date_picker_controller.js +17 -0
  169. data/templates/controllers/dialog_controller.js +50 -0
  170. data/templates/controllers/dropdown_menu_controller.js +92 -0
  171. data/templates/controllers/hover_card_controller.js +17 -0
  172. data/templates/controllers/invite_member_dialog_controller.js +28 -0
  173. data/templates/controllers/masked_input_controller.js +30 -0
  174. data/templates/controllers/popover_controller.js +42 -0
  175. data/templates/controllers/rich_text_editor_lite_controller.js +443 -0
  176. data/templates/controllers/select_controller.js +10 -0
  177. data/templates/controllers/sheet_controller.js +34 -0
  178. data/templates/controllers/sidebar_controller.js +10 -0
  179. data/templates/controllers/tabs_controller.js +41 -0
  180. data/templates/controllers/theme_toggle_controller.js +24 -0
  181. data/templates/controllers/tooltip_controller.js +10 -0
  182. metadata +257 -0
@@ -0,0 +1,42 @@
1
+ module Senren
2
+ class DataTableComponent < BaseComponent
3
+ renders_one :toolbar
4
+ renders_one :footer
5
+
6
+ VARIANTS = { default: '' }.freeze
7
+ SIZES = { md: '' }.freeze
8
+
9
+ def initialize(columns: [], rows: [], caption: nil, empty_text: 'No records found.', sortable: true,
10
+ variant: :default, class_name: nil, **html)
11
+ super(variant: variant, size: :md, class_name: class_name, **html)
12
+ @columns = Array(columns)
13
+ @rows = Array(rows)
14
+ @caption = caption
15
+ @empty_text = empty_text
16
+ @sortable = sortable
17
+ end
18
+
19
+ attr_reader :columns, :rows, :caption, :empty_text
20
+
21
+ def sortable? = !!@sortable
22
+
23
+ def cell_value(row, column)
24
+ key = column_key(column)
25
+ row.is_a?(Hash) ? (row[key] || row[key.to_s]) : row[key.to_i]
26
+ end
27
+
28
+ def column_label(column)
29
+ column.is_a?(Hash) ? (column[:label] || column['label']) : column.to_s.tr('_', ' ').capitalize
30
+ end
31
+
32
+ def column_sort_key(column)
33
+ column_key(column).to_s
34
+ end
35
+
36
+ private
37
+
38
+ def column_key(column)
39
+ column.is_a?(Hash) ? (column[:key] || column['key']) : column
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ <div <%= tag.attributes(**root_attrs("flex w-full items-center gap-2", data: { controller: "senren--date-picker" })) %>>
2
+ <input type="date" id="<%= id %>" name="<%= name %>" value="<%= value %>" placeholder="<%= placeholder %>" data-senren--date-picker-target="input" class="hover:cursor-pointerh-10 w-full rounded-(--senren-radius) border bg-[hsl(var(--senren-background))] px-3 text-sm text-[hsl(var(--senren-foreground))] outline-none transition-colors focus:ring-2 focus:ring-[hsl(var(--senren-ring))] <%= self.class::VARIANTS[variant] %>">
3
+ <button type="button" data-action="click->senren--date-picker#today" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) border border-[hsl(var(--senren-border))] px-3 text-sm font-medium hover:bg-[hsl(var(--senren-accent))]">Today</button>
4
+ <button type="button" data-action="click->senren--date-picker#clear" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) px-3 text-sm text-[hsl(var(--senren-muted-foreground))] hover:bg-[hsl(var(--senren-accent))]">Clear</button>
5
+ </div>
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class DatePickerComponent < BaseComponent
5
+ VARIANTS = {
6
+ default: 'border-[hsl(var(--senren-input))]',
7
+ error: 'border-[hsl(var(--senren-destructive))]'
8
+ }.freeze
9
+ SIZES = { md: '' }.freeze
10
+
11
+ def initialize(name:, value: nil, id: nil, placeholder: 'Select date', variant: :default, class_name: nil, **html)
12
+ super(variant: variant, size: :md, class_name: class_name, **html)
13
+ @name = name
14
+ @value = value
15
+ @id = id || name
16
+ @placeholder = placeholder
17
+ end
18
+
19
+ attr_reader :name, :value, :id, :placeholder
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ <div data-controller="senren--dialog" data-senren-component="dialog" data-senren--dialog-open-value="<%= open %>" id="<%= dom_id %>">
2
+ <% if trigger? %>
3
+ <span data-action="click->senren--dialog#open" data-senren--dialog-target="trigger" aria-controls="<%= dom_id %>-panel">
4
+ <%= trigger %>
5
+ </span>
6
+ <% else %>
7
+ <button type="button" data-action="click->senren--dialog#open"
8
+ class="hidden" data-senren--dialog-target="trigger" aria-controls="<%= dom_id %>-panel">Open</button>
9
+ <% end %>
10
+
11
+ <div data-senren--dialog-target="overlay" hidden
12
+ class="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm"></div>
13
+
14
+ <div data-senren--dialog-target="panel" hidden
15
+ role="dialog" aria-modal="true" aria-labelledby="<%= dom_id %>-title"
16
+ id="<%= dom_id %>-panel"
17
+ tabindex="-1"
18
+ class="fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-6 shadow-lg focus:outline-none">
19
+ <% if title? %>
20
+ <h2 id="<%= dom_id %>-title" class="text-lg font-semibold leading-none tracking-tight"><%= title %></h2>
21
+ <% end %>
22
+ <% if description? %>
23
+ <p class="mt-1.5 text-sm text-[hsl(var(--senren-muted-foreground))]"><%= description %></p>
24
+ <% end %>
25
+ <% if body? %>
26
+ <div class="mt-4"><%= body %></div>
27
+ <% end %>
28
+ <% if footer? %>
29
+ <div class="mt-6 flex justify-end gap-2"><%= footer %></div>
30
+ <% end %>
31
+
32
+ <button type="button" data-action="click->senren--dialog#close"
33
+ aria-label="Close"
34
+ class="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
35
+ <span aria-hidden="true">×</span>
36
+ </button>
37
+ </div>
38
+ </div>
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class DialogComponent < BaseComponent
5
+ renders_one :trigger
6
+ renders_one :title
7
+ renders_one :description
8
+ renders_one :body
9
+ renders_one :footer
10
+
11
+ VARIANTS = { default: '' }.freeze
12
+ SIZES = { md: '' }.freeze
13
+
14
+ def initialize(open: false, id: nil, class_name: nil, **html)
15
+ super(variant: :default, size: :md, class_name: class_name, **html)
16
+ @open = open
17
+ @dom_id = id || "senren-dialog-#{SecureRandom.hex(3)}"
18
+ end
19
+
20
+ attr_reader :open, :dom_id
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ <div data-controller="senren--dropdown-menu" class="relative inline-block" data-state="closed" data-senren-component="dropdown_menu">
2
+ <div class="inline-flex cursor-pointer" data-state="closed" aria-haspopup="menu" aria-expanded="false" data-senren--dropdown-menu-target="trigger" data-action="click->senren--dropdown-menu#toggle keydown->senren--dropdown-menu#onTriggerKey">
3
+ <%= trigger %>
4
+ </div>
5
+
6
+ <div data-senren--dropdown-menu-target="menu" role="menu" hidden
7
+ class="absolute right-0 z-50 mt-2 w-56 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-1 shadow-md">
8
+ <% items.each do |it| %>
9
+ <%= it %>
10
+ <% end %>
11
+ </div>
12
+ </div>
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class DropdownMenuComponent < BaseComponent
5
+ renders_one :trigger
6
+ renders_many :items, 'ItemTag'
7
+
8
+ VARIANTS = { default: '' }.freeze
9
+ SIZES = { md: '' }.freeze
10
+
11
+ def initialize(class_name: nil, **html)
12
+ super(variant: :default, size: :md, class_name: class_name, **html)
13
+ end
14
+
15
+ class ItemTag < ViewComponent::Base
16
+ def initialize(href: nil, method: nil, destructive: false, **opts)
17
+ @href = href
18
+ @method = method
19
+ @destructive = destructive
20
+ @opts = opts
21
+ end
22
+
23
+ def call
24
+ klass = 'block w-full text-left px-3 py-2 text-sm rounded-sm hover:bg-[hsl(var(--senren-accent))] focus:bg-[hsl(var(--senren-accent))] outline-none cursor-pointer'
25
+ klass += ' text-[hsl(var(--senren-destructive))]' if @destructive
26
+ if @href
27
+ link_to(content, @href, role: 'menuitem', method: @method, class: klass,
28
+ data: { action: 'click->senren--dropdown-menu#close keydown->senren--dropdown-menu#onItemKey' })
29
+ else
30
+ tag.button(content, type: 'button', role: 'menuitem', class: klass,
31
+ data: { action: 'click->senren--dropdown-menu#close keydown->senren--dropdown-menu#onItemKey' }, **@opts)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,18 @@
1
+ <%= tag.div(**root_attrs("rounded-(--senren-radius) border border-dashed p-8 text-center text-[hsl(var(--senren-muted-foreground))]")) do %>
2
+ <% if content? && title.blank? && description.blank? %>
3
+ <%= content %>
4
+ <% else %>
5
+ <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-[hsl(var(--senren-accent))] text-[hsl(var(--senren-accent-foreground))]">
6
+ <% if icon? %><%= icon %><% else %><span class="text-lg font-semibold">+</span><% end %>
7
+ </div>
8
+ <% if title.present? %>
9
+ <h2 class="mt-4 font-display text-lg font-semibold text-[hsl(var(--senren-foreground))]"><%= title %></h2>
10
+ <% end %>
11
+ <% if description.present? %>
12
+ <p class="mx-auto mt-2 max-w-sm text-sm leading-6"><%= description %></p>
13
+ <% end %>
14
+ <% if actions? %>
15
+ <div class="mt-5 flex justify-center gap-2"><%= actions %></div>
16
+ <% end %>
17
+ <% end %>
18
+ <% end %>
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class EmptyStateComponent < BaseComponent
5
+ renders_one :icon
6
+ renders_one :actions
7
+
8
+ VARIANTS = {
9
+ default: 'border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))]',
10
+ illustrated: 'border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-muted)/0.35)]'
11
+ }.freeze
12
+ SIZES = { md: '' }.freeze
13
+
14
+ def initialize(title: nil, description: nil, variant: :default, class_name: nil, **html)
15
+ super(variant: variant, size: :md, class_name: class_name, **html)
16
+ @title = title
17
+ @description = description
18
+ end
19
+
20
+ attr_reader :title, :description
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ <%= tag.div(**root_attrs("rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] p-3 shadow-sm", role: "search", "aria-label": label)) do %>
2
+ <div class="flex flex-wrap items-center gap-2">
3
+ <%= content %>
4
+ </div>
5
+ <% end %>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class FilterBarComponent < BaseComponent
5
+ VARIANTS = { default: '' }.freeze
6
+ SIZES = { md: '' }.freeze
7
+
8
+ def initialize(label: 'Filters', class_name: nil, **html)
9
+ super(variant: :default, size: :md, class_name: class_name, **html)
10
+ @label = label
11
+ end
12
+
13
+ attr_reader :label
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ <%= form_with(model: model, url: url, method: method, multipart: multipart, **root_attrs("space-y-4")) do |f| %>
2
+ <%= content || capture(f, &Proc.new) %>
3
+ <% end %>
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class FormComponent < BaseComponent
5
+ VARIANTS = { default: '' }.freeze
6
+ SIZES = { md: '' }.freeze
7
+
8
+ def initialize(model: nil, url: nil, method: :post, multipart: false, class_name: nil, **html)
9
+ super(variant: :default, size: :md, class_name: class_name, **html)
10
+ @model = model
11
+ @url = url
12
+ @method = method
13
+ @multipart = multipart
14
+ end
15
+
16
+ attr_reader :model, :url, :method, :multipart
17
+ end
18
+ end
@@ -0,0 +1,10 @@
1
+ <span data-controller="senren--hover-card" class="relative inline-block" data-senren-component="hover_card">
2
+ <span tabindex="0"
3
+ data-action="mouseenter->senren--hover-card#show mouseleave->senren--hover-card#hide focus->senren--hover-card#show blur->senren--hover-card#hide">
4
+ <%= trigger %>
5
+ </span>
6
+ <span hidden data-senren--hover-card-target="panel"
7
+ class="absolute z-40 mt-2 w-64 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-4 shadow-md">
8
+ <%= content_panel || content %>
9
+ </span>
10
+ </span>
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class HoverCardComponent < BaseComponent
5
+ renders_one :trigger
6
+ renders_one :content_panel
7
+
8
+ VARIANTS = { default: '' }.freeze
9
+ SIZES = { md: '' }.freeze
10
+ end
11
+ end
@@ -0,0 +1 @@
1
+ <%= tag.input(**root_attrs("flex w-full rounded-(--senren-radius) border bg-[hsl(var(--senren-background))] text-[hsl(var(--senren-foreground))] file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-[hsl(var(--senren-muted-foreground))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50", id: id, name: name, type: type, value: value, placeholder: placeholder, "aria-invalid": variant == :error)) %>
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class InputComponent < BaseComponent
5
+ VARIANTS = {
6
+ default: 'border-[hsl(var(--senren-input))] focus-visible:ring-[hsl(var(--senren-ring))]',
7
+ error: 'border-[hsl(var(--senren-destructive))] focus-visible:ring-[hsl(var(--senren-destructive))]'
8
+ }.freeze
9
+
10
+ SIZES = {
11
+ sm: 'h-8 text-sm px-2.5',
12
+ md: 'h-10 text-sm px-3',
13
+ lg: 'h-12 text-base px-4'
14
+ }.freeze
15
+
16
+ def initialize(name:, type: 'text', value: nil, placeholder: nil, id: nil, variant: :default, size: :md,
17
+ class_name: nil, **html)
18
+ super(variant: variant, size: size, class_name: class_name, **html)
19
+ @name = name
20
+ @type = type
21
+ @value = value
22
+ @placeholder = placeholder
23
+ @id = id || name.to_s.parameterize
24
+ end
25
+
26
+ attr_reader :name, :type, :value, :placeholder, :id
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ <%= tag.div(**root_attrs("inline-block", data: { controller: "senren--invite-member-dialog" })) do %>
2
+ <% if trigger? %>
3
+ <span data-action="click->senren--invite-member-dialog#open" data-senren--invite-member-dialog-target="trigger"><%= trigger %></span>
4
+ <% else %>
5
+ <button type="button" class="inline-flex h-10 cursor-pointer items-center justify-center rounded-(--senren-radius) bg-[hsl(var(--senren-primary))] px-4 text-sm font-medium text-[hsl(var(--senren-primary-foreground))] hover:opacity-90" data-action="click->senren--invite-member-dialog#open" data-senren--invite-member-dialog-target="trigger">
6
+ <%= button_label %>
7
+ </button>
8
+ <% end %>
9
+
10
+ <div hidden data-senren--invite-member-dialog-target="overlay" class="fixed inset-0 z-40 bg-[hsl(var(--senren-foreground)/0.42)] backdrop-blur-sm"></div>
11
+ <div hidden id="<%= dom_id %>" data-senren--invite-member-dialog-target="panel" role="dialog" aria-modal="true" aria-labelledby="<%= dom_id %>-title" tabindex="-1" class="fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] p-6 text-[hsl(var(--senren-popover-foreground))] shadow-lg">
12
+ <h2 id="<%= dom_id %>-title" class="font-display text-lg font-semibold"><%= title %></h2>
13
+ <% if description.present? %>
14
+ <p class="mt-1.5 text-sm leading-6 text-[hsl(var(--senren-muted-foreground))]"><%= description %></p>
15
+ <% end %>
16
+
17
+ <div class="mt-5 space-y-4">
18
+ <% if content? %>
19
+ <%= content %>
20
+ <% else %>
21
+ <label class="block text-sm font-medium text-[hsl(var(--senren-foreground))]" for="<%= dom_id %>-email">Email</label>
22
+ <input id="<%= dom_id %>-email" name="<%= email_name %>" type="email" placeholder="teammate@company.com" class="h-10 w-full rounded-(--senren-radius) border border-[hsl(var(--senren-input))] bg-[hsl(var(--senren-background))] px-3 text-sm outline-none focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
23
+ <label class="block text-sm font-medium text-[hsl(var(--senren-foreground))]" for="<%= dom_id %>-role">Role</label>
24
+ <select id="<%= dom_id %>-role" name="<%= role_name %>" class="h-10 w-full rounded-(--senren-radius) border border-[hsl(var(--senren-input))] bg-[hsl(var(--senren-background))] px-3 text-sm outline-none focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
25
+ <% roles.each do |role| %><option><%= role %></option><% end %>
26
+ </select>
27
+ <% end %>
28
+ </div>
29
+
30
+ <div class="mt-6 flex justify-end gap-2">
31
+ <button type="button" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) px-4 text-sm font-medium hover:bg-[hsl(var(--senren-accent))]" data-action="click->senren--invite-member-dialog#close">Cancel</button>
32
+ <% if footer? %><%= footer %><% else %><button type="submit" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) bg-[hsl(var(--senren-primary))] px-4 text-sm font-medium text-[hsl(var(--senren-primary-foreground))] hover:opacity-90">Send invite</button><% end %>
33
+ </div>
34
+ </div>
35
+ <% end %>
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class InviteMemberDialogComponent < BaseComponent
5
+ renders_one :trigger
6
+ renders_one :footer
7
+
8
+ VARIANTS = { default: '' }.freeze
9
+ SIZES = { md: '' }.freeze
10
+
11
+ def initialize(title: 'Invite teammate', description: 'Send an invitation to join this workspace.',
12
+ email_name: 'email', role_name: 'role', roles: %w[Member
13
+ Admin], button_label: 'Invite member', id: nil, class_name: nil, **html)
14
+ super(variant: :default, size: :md, class_name: class_name, **html)
15
+ @title = title
16
+ @description = description
17
+ @email_name = email_name
18
+ @role_name = role_name
19
+ @roles = Array(roles)
20
+ @button_label = button_label
21
+ @dom_id = id || "senren-invite-member-#{SecureRandom.hex(3)}"
22
+ end
23
+
24
+ attr_reader :title, :description, :email_name, :role_name, :roles, :button_label, :dom_id
25
+ end
26
+ end
@@ -0,0 +1,4 @@
1
+ <%= tag.label(**root_attrs("text-sm font-medium leading-none text-[hsl(var(--senren-foreground))] peer-disabled:cursor-not-allowed peer-disabled:opacity-70", for: for_field)) do %>
2
+ <%= content %>
3
+ <% if variant == :required %><span class="text-[hsl(var(--senren-destructive))]" aria-hidden="true"> *</span><% end %>
4
+ <% end %>
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class LabelComponent < BaseComponent
5
+ VARIANTS = {
6
+ default: '',
7
+ required: ''
8
+ }.freeze
9
+
10
+ SIZES = { md: '' }.freeze
11
+
12
+ def initialize(for_field:, variant: :default, class_name: nil, **html)
13
+ super(variant: variant, size: :md, class_name: class_name, **html)
14
+ @for_field = for_field
15
+ end
16
+
17
+ attr_reader :for_field
18
+ end
19
+ end
@@ -0,0 +1 @@
1
+ <%= tag.a content, **root_attrs("inline-flex items-center gap-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))] rounded-sm", href: href, **external_attrs) %>
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class LinkComponent < BaseComponent
5
+ VARIANTS = {
6
+ default: 'text-[hsl(var(--senren-primary))] hover:underline underline-offset-4',
7
+ muted: 'text-[hsl(var(--senren-muted-foreground))] hover:underline underline-offset-4',
8
+ destructive: 'text-[hsl(var(--senren-destructive))] hover:underline underline-offset-4'
9
+ }.freeze
10
+
11
+ SIZES = { md: '' }.freeze
12
+
13
+ def initialize(href:, variant: :default, external: false, class_name: nil, **html)
14
+ super(variant: variant, size: :md, class_name: class_name, **html)
15
+ @href = href
16
+ @external = external
17
+ end
18
+
19
+ attr_reader :href, :external
20
+
21
+ def external_attrs
22
+ external ? { rel: 'noopener noreferrer', target: '_blank' } : {}
23
+ end
24
+ end
25
+ end
@@ -0,0 +1 @@
1
+ <%= render(Senren::InputComponent.new(**input_args)) { content } %>
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class MaskedInputComponent < BaseComponent
5
+ VARIANTS = InputComponent::VARIANTS
6
+ SIZES = InputComponent::SIZES
7
+
8
+ def initialize(mask:, **args)
9
+ @mask = mask
10
+ @args = args
11
+ end
12
+
13
+ def input_args
14
+ data = (@args[:data] || {}).merge(controller: 'senren--masked-input', 'senren--masked-input-mask-value': @mask)
15
+ @args.merge(data: data)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ <%= tag.div(**wrapper_attrs) do %>
2
+ <%= tag.select(**select_attrs) do %>
3
+ <% if prompt %>
4
+ <option value=""><%= prompt %></option>
5
+ <% end %>
6
+ <% options.each do |opt| %>
7
+ <% value, label = Array === opt ? opt : [opt, opt] %>
8
+ <option value="<%= value %>" <%= "selected" if selected.to_s == value.to_s %>><%= label %></option>
9
+ <% end %>
10
+ <% end %>
11
+ <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[hsl(var(--senren-muted-foreground))] transition-transform duration-150 group-focus-within:rotate-180">
12
+ <path d="m5 8 5 5 5-5"></path>
13
+ </svg>
14
+ <% end %>
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class NativeSelectComponent < BaseComponent
5
+ VARIANTS = {
6
+ default: 'border-[hsl(var(--senren-input))] focus-visible:ring-[hsl(var(--senren-ring))]',
7
+ error: 'border-[hsl(var(--senren-destructive))] focus-visible:ring-[hsl(var(--senren-destructive))]'
8
+ }.freeze
9
+
10
+ SIZES = {
11
+ sm: 'h-8 text-sm px-2.5',
12
+ md: 'h-10 text-sm px-3',
13
+ lg: 'h-12 text-base px-4'
14
+ }.freeze
15
+
16
+ def initialize(name:, options:, selected: nil, id: nil, prompt: nil, variant: :default, size: :md, class_name: nil,
17
+ **html)
18
+ super(variant: variant, size: size, class_name: class_name, **html)
19
+ @name = name
20
+ @options = options
21
+ @selected = selected
22
+ @id = id || name.to_s.parameterize
23
+ @prompt = prompt
24
+ end
25
+
26
+ attr_reader :name, :options, :selected, :id, :prompt
27
+
28
+ def wrapper_attrs
29
+ { class: 'group relative w-full', data: { senren_component: senren_component_name } }
30
+ end
31
+
32
+ def select_attrs
33
+ attrs = html_attrs.except(:class)
34
+ attrs.merge(
35
+ id: id,
36
+ name: name,
37
+ class: select_classes,
38
+ 'aria-invalid': variant == :error
39
+ )
40
+ end
41
+
42
+ def select_classes
43
+ [
44
+ 'flex w-full cursor-pointer appearance-none rounded-(--senren-radius) border bg-[hsl(var(--senren-background))] pr-9 text-[hsl(var(--senren-foreground))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50',
45
+ self.class::VARIANTS[variant],
46
+ self.class::SIZES[size],
47
+ class_name,
48
+ html_attrs[:class]
49
+ ].compact.join(' ')
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,20 @@
1
+ <%= tag.header(**root_attrs("flex flex-col gap-4 border-b border-[hsl(var(--senren-border))] pb-6 sm:flex-row sm:items-end sm:justify-between")) do %>
2
+ <% if content? && title.blank? && description.blank? %>
3
+ <%= content %>
4
+ <% else %>
5
+ <div class="min-w-0">
6
+ <% if eyebrow? %>
7
+ <div class="mb-2 text-xs font-semibold uppercase tracking-[0.14em] text-[hsl(var(--senren-muted-foreground))]"><%= eyebrow %></div>
8
+ <% end %>
9
+ <% if title.present? %>
10
+ <h1 class="font-display text-3xl font-semibold tracking-tight text-[hsl(var(--senren-foreground))]"><%= title %></h1>
11
+ <% end %>
12
+ <% if description.present? %>
13
+ <p class="mt-2 max-w-2xl text-sm leading-6 text-[hsl(var(--senren-muted-foreground))]"><%= description %></p>
14
+ <% end %>
15
+ </div>
16
+ <% if actions? %>
17
+ <div class="flex shrink-0 flex-wrap items-center gap-2"><%= actions %></div>
18
+ <% end %>
19
+ <% end %>
20
+ <% end %>
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class PageHeaderComponent < BaseComponent
5
+ renders_one :actions
6
+ renders_one :eyebrow
7
+
8
+ VARIANTS = { default: '' }.freeze
9
+ SIZES = { md: '' }.freeze
10
+
11
+ def initialize(title: nil, description: nil, class_name: nil, **html)
12
+ super(variant: :default, size: :md, class_name: class_name, **html)
13
+ @title = title
14
+ @description = description
15
+ end
16
+
17
+ attr_reader :title, :description
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ <nav <%= tag.attributes(**root_attrs("flex items-center justify-center gap-1", aria: { label: label })) %>>
2
+ <% prev_page = current_page - 1 %>
3
+ <%= link_to "Previous", page_url(prev_page), class: "rounded-md border border-[hsl(var(--senren-border))] px-3 py-2 text-sm #{current_page == 1 ? "pointer-events-none opacity-50" : "hover:bg-[hsl(var(--senren-accent))]"}", aria: { disabled: current_page == 1 } %>
4
+
5
+ <% (1..total_pages).each do |page| %>
6
+ <%= link_to page, page_url(page), class: "inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium #{page == current_page ? "bg-[hsl(var(--senren-primary))] text-[hsl(var(--senren-primary-foreground))]" : "text-[hsl(var(--senren-muted-foreground))] hover:bg-[hsl(var(--senren-accent))] hover:text-[hsl(var(--senren-accent-foreground))]"}", aria: { current: (page == current_page ? "page" : nil) } %>
7
+ <% end %>
8
+
9
+ <% next_page = current_page + 1 %>
10
+ <%= link_to "Next", page_url(next_page), class: "rounded-md border border-[hsl(var(--senren-border))] px-3 py-2 text-sm #{current_page == total_pages ? "pointer-events-none opacity-50" : "hover:bg-[hsl(var(--senren-accent))]"}", aria: { disabled: current_page == total_pages } %>
11
+ </nav>
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class PaginationComponent < BaseComponent
5
+ VARIANTS = { default: '' }.freeze
6
+ SIZES = { md: '' }.freeze
7
+
8
+ def initialize(current_page: 1, total_pages: 1, path: nil, label: 'Pagination', class_name: nil, **html)
9
+ super(variant: :default, size: :md, class_name: class_name, **html)
10
+ @total_pages = [total_pages.to_i, 1].max
11
+ @current_page = current_page.to_i.clamp(1, @total_pages)
12
+ @path = path
13
+ @label = label
14
+ end
15
+
16
+ attr_reader :current_page, :total_pages, :path, :label
17
+
18
+ def page_url(page)
19
+ return '#' unless path
20
+
21
+ path.respond_to?(:call) ? path.call(page) : path.to_s.gsub(':page', page.to_s)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,9 @@
1
+ <div data-controller="senren--popover" class="relative inline-block" data-senren-component="popover">
2
+ <div data-senren--popover-target="trigger" data-action="click->senren--popover#toggle">
3
+ <%= trigger %>
4
+ </div>
5
+ <div data-senren--popover-target="panel" hidden role="dialog"
6
+ class="absolute z-40 mt-2 min-w-[12rem] rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-4 shadow-md">
7
+ <%= content_panel || content %>
8
+ </div>
9
+ </div>