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,25 @@
1
+ groups:
2
+ - id: actions
3
+ title: Actions
4
+ description: Buttons, links, and clickable affordances.
5
+ - id: forms
6
+ title: Forms
7
+ description: Form structure, labels, inputs, selects, validation surfaces.
8
+ - id: overlays
9
+ title: Overlays
10
+ description: Dialogs, popovers, tooltips, sheets, menus.
11
+ - id: navigation
12
+ title: Navigation
13
+ description: Tabs, breadcrumbs, sidebar, theme toggle, shortcut keys.
14
+ - id: layout
15
+ title: Layout
16
+ description: Cards, separators, app shells, page headers, aspect ratios.
17
+ - id: data
18
+ title: Data Display
19
+ description: Tables, badges, avatars, progress, skeletons, pagination.
20
+ - id: saas
21
+ title: SaaS Blocks
22
+ description: Composite blocks for SaaS apps - settings, billing, team, search, filters.
23
+ - id: rich
24
+ title: Rich Content
25
+ description: Editors, code blocks, carousels, command palettes.
@@ -0,0 +1,79 @@
1
+ recipes:
2
+ form_basics:
3
+ description: Minimal form stack - structure, primitives, and submit button.
4
+ components: [form, label, input, textarea, native_select, button, alert]
5
+
6
+ dashboard:
7
+ description: SaaS dashboard layout with shell, header, and stat cards.
8
+ components:
9
+ - app_shell
10
+ - top_nav
11
+ - sidebar
12
+ - page_header
13
+ - card
14
+ - badge
15
+ - button
16
+ - stat_card
17
+ - typography
18
+ - separator
19
+
20
+ settings_page:
21
+ description: Settings page with sectioned form and destructive action.
22
+ components:
23
+ - app_shell
24
+ - sidebar
25
+ - page_header
26
+ - card
27
+ - settings_section
28
+ - form
29
+ - input
30
+ - textarea
31
+ - switch
32
+ - button
33
+ - alert_dialog
34
+ - separator
35
+ - typography
36
+
37
+ team_management:
38
+ description: Team list with invite dialog and bulk actions.
39
+ components:
40
+ - app_shell
41
+ - page_header
42
+ - card
43
+ - team_member_list
44
+ - invite_member_dialog
45
+ - dropdown_menu
46
+ - bulk_action_bar
47
+ - search_input
48
+ - filter_bar
49
+ - badge
50
+ - avatar
51
+ - button
52
+
53
+ todolist_index:
54
+ description: Components used by the apps/todolist Todo index page.
55
+ components:
56
+ - app_shell
57
+ - top_nav
58
+ - sidebar
59
+ - page_header
60
+ - button
61
+ - card
62
+ - badge
63
+ - table
64
+ - dropdown_menu
65
+ - pagination
66
+ - empty_state
67
+ - filter_bar
68
+ - search_input
69
+
70
+ todolist_form:
71
+ description: Components used by the apps/todolist Todo new/edit pages.
72
+ components:
73
+ - form
74
+ - label
75
+ - input
76
+ - textarea
77
+ - native_select
78
+ - button
79
+ - alert
@@ -0,0 +1,16 @@
1
+ <div <%= tag.attributes(**root_attrs("divide-y divide-[hsl(var(--senren-border))] rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-background))]", data: { controller: "senren--accordion", "senren--accordion-multiple-value": multiple? })) %>>
2
+ <% items.each_with_index do |item, index| %>
3
+ <% expanded = open_item?(item, index) %>
4
+ <section>
5
+ <h3>
6
+ <button type="button" class="flex w-full cursor-pointer items-center justify-between gap-4 px-4 py-3 text-left text-sm font-medium text-[hsl(var(--senren-foreground))] transition-colors hover:bg-[hsl(var(--senren-muted)/0.5)]" aria-expanded="<%= expanded %>" aria-controls="<%= item[:id] %>-panel" data-senren--accordion-target="trigger" data-panel-id="<%= item[:id] %>" data-action="click->senren--accordion#toggle">
7
+ <span><%= item[:title] %></span>
8
+ <span aria-hidden="true" class="text-[hsl(var(--senren-muted-foreground))]">+</span>
9
+ </button>
10
+ </h3>
11
+ <div id="<%= item[:id] %>-panel" data-senren--accordion-target="panel" data-panel-id="<%= item[:id] %>" <%= "hidden" unless expanded %> class="px-4 pb-4 text-sm leading-6 text-[hsl(var(--senren-muted-foreground))]">
12
+ <%= item[:content].presence || content %>
13
+ </div>
14
+ </section>
15
+ <% end %>
16
+ </div>
@@ -0,0 +1,31 @@
1
+ module Senren
2
+ class AccordionComponent < BaseComponent
3
+ VARIANTS = { single: '', multiple: '' }.freeze
4
+ SIZES = { md: '' }.freeze
5
+
6
+ def initialize(items: [], variant: :single, open: nil, class_name: nil, **html)
7
+ super(variant: variant, size: :md, class_name: class_name, **html)
8
+ @items = normalize_items(items)
9
+ @open = Array(open).map(&:to_s)
10
+ end
11
+
12
+ attr_reader :items, :open
13
+
14
+ def multiple? = variant == :multiple
15
+
16
+ def open_item?(item, index)
17
+ open.include?(item[:id]) || (open.empty? && index.zero?)
18
+ end
19
+
20
+ private
21
+
22
+ def normalize_items(items)
23
+ Array(items).map.with_index do |item, index|
24
+ source = item.is_a?(Hash) ? item : { title: item.to_s }
25
+ title = source[:title] || source['title'] || "Section #{index + 1}"
26
+ id = (source[:id] || source['id'] || title.to_s.parameterize).to_s
27
+ { id: id, title: title, content: source[:content] || source['content'] }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ <%= tag.div(**root_attrs("rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] p-4 shadow-sm")) do %>
2
+ <% if content? && items.empty? %>
3
+ <%= content %>
4
+ <% else %>
5
+ <ol class="space-y-4">
6
+ <% items.each do |item| %>
7
+ <li class="relative flex gap-3">
8
+ <span class="mt-1 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[hsl(var(--senren-muted))] text-xs font-semibold text-[hsl(var(--senren-muted-foreground))]"><%= item_value(item, :initials) || "!" %></span>
9
+ <div class="min-w-0 flex-1">
10
+ <p class="text-sm text-[hsl(var(--senren-foreground))]"><%= item_value(item, :title) %></p>
11
+ <% if item_value(item, :description).present? %>
12
+ <p class="mt-1 text-sm leading-6 text-[hsl(var(--senren-muted-foreground))]"><%= item_value(item, :description) %></p>
13
+ <% end %>
14
+ <% if item_value(item, :time).present? %>
15
+ <time class="mt-1 block text-xs text-[hsl(var(--senren-muted-foreground))]"><%= item_value(item, :time) %></time>
16
+ <% end %>
17
+ </div>
18
+ </li>
19
+ <% end %>
20
+ </ol>
21
+ <% end %>
22
+ <% end %>
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class ActivityFeedComponent < BaseComponent
5
+ VARIANTS = { default: '' }.freeze
6
+ SIZES = { md: '' }.freeze
7
+
8
+ def initialize(items: [], class_name: nil, **html)
9
+ super(variant: :default, size: :md, class_name: class_name, **html)
10
+ @items = Array(items)
11
+ end
12
+
13
+ attr_reader :items
14
+
15
+ def item_value(item, key)
16
+ item.is_a?(Hash) ? (item[key] || item[key.to_s]) : nil
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ <%= tag.div(**root_attrs("relative w-full rounded-(--senren-radius) border p-4", role: aria_role)) do %>
2
+ <% if title? %>
3
+ <h5 class="mb-1 font-semibold leading-none tracking-tight"><%= title %></h5>
4
+ <% end %>
5
+ <% if description? %>
6
+ <div class="text-sm opacity-90"><%= description %></div>
7
+ <% end %>
8
+ <%= content if !title? && !description? %>
9
+ <% end %>
@@ -0,0 +1,18 @@
1
+ module Senren
2
+ class AlertComponent < BaseComponent
3
+ renders_one :title
4
+ renders_one :description
5
+
6
+ VARIANTS = {
7
+ default: 'bg-[hsl(var(--senren-background))] text-[hsl(var(--senren-foreground))] border-[hsl(var(--senren-border))]',
8
+ info: 'bg-[hsl(var(--senren-secondary))] text-[hsl(var(--senren-secondary-foreground))] border-transparent',
9
+ success: 'bg-[hsl(var(--senren-success))] text-[hsl(var(--senren-success-foreground))] border-transparent',
10
+ warning: 'bg-[hsl(var(--senren-warning))] text-[hsl(var(--senren-warning-foreground))] border-transparent',
11
+ destructive: 'bg-[hsl(var(--senren-destructive))] text-[hsl(var(--senren-destructive-foreground))] border-transparent'
12
+ }.freeze
13
+
14
+ SIZES = { md: '' }.freeze
15
+
16
+ def aria_role = variant == :destructive ? 'alert' : 'status'
17
+ end
18
+ end
@@ -0,0 +1,34 @@
1
+ <div data-controller="senren--alert-dialog" data-senren-component="alert_dialog" id="<%= dom_id %>">
2
+ <% if trigger? %>
3
+ <span data-action="click->senren--alert-dialog#open" data-senren--alert-dialog-target="trigger">
4
+ <%= trigger %>
5
+ </span>
6
+ <% else %>
7
+ <button type="button" data-action="click->senren--alert-dialog#open"
8
+ data-senren--alert-dialog-target="trigger"
9
+ class="hidden">Open</button>
10
+ <% end %>
11
+
12
+ <div data-senren--alert-dialog-target="overlay" hidden
13
+ class="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm"></div>
14
+
15
+ <div data-senren--alert-dialog-target="panel" hidden
16
+ role="alertdialog" aria-modal="true" aria-labelledby="<%= dom_id %>-title" aria-describedby="<%= dom_id %>-desc"
17
+ tabindex="-1"
18
+ 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))] text-[hsl(var(--senren-popover-foreground))] p-6 shadow-lg">
19
+ <% if title? %>
20
+ <h2 id="<%= dom_id %>-title" class="text-lg font-semibold"><%= title %></h2>
21
+ <% end %>
22
+ <% if description? %>
23
+ <p id="<%= dom_id %>-desc" class="mt-2 text-sm text-[hsl(var(--senren-muted-foreground))]"><%= description %></p>
24
+ <% end %>
25
+ <div class="mt-6 flex justify-end gap-2">
26
+ <% if cancel? %>
27
+ <span data-action="click->senren--alert-dialog#close"><%= cancel %></span>
28
+ <% end %>
29
+ <% if confirm? %>
30
+ <%= confirm %>
31
+ <% end %>
32
+ </div>
33
+ </div>
34
+ </div>
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class AlertDialogComponent < BaseComponent
5
+ renders_one :trigger
6
+ renders_one :title
7
+ renders_one :description
8
+ renders_one :cancel
9
+ renders_one :confirm
10
+
11
+ VARIANTS = { default: '', destructive: '' }.freeze
12
+ SIZES = { md: '' }.freeze
13
+
14
+ def initialize(variant: :default, id: nil, class_name: nil, **html)
15
+ super(variant: variant, size: :md, class_name: class_name, **html)
16
+ @dom_id = id || "senren-alert-dialog-#{SecureRandom.hex(3)}"
17
+ end
18
+
19
+ attr_reader :dom_id
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ <%= tag.div(**root_attrs("space-y-2", data: { controller: "senren--api-key-field", "senren--api-key-field-reveal-label-value": reveal_label, "senren--api-key-field-hide-label-value": hide_label, "senren--api-key-field-copy-label-value": copy_label })) do %>
2
+ <% if content? && value.blank? %>
3
+ <%= content %>
4
+ <% else %>
5
+ <label class="block text-sm font-medium text-[hsl(var(--senren-foreground))]"><%= label %></label>
6
+ <div class="flex flex-col gap-2 sm:flex-row">
7
+ <input type="password" readonly value="<%= value %>" data-senren--api-key-field-target="input" class="h-10 min-w-0 flex-1 rounded-(--senren-radius) border border-[hsl(var(--senren-input))] bg-[hsl(var(--senren-muted)/0.35)] px-3 font-mono text-sm text-[hsl(var(--senren-foreground))] outline-none focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
8
+ <button type="button" data-senren--api-key-field-target="revealButton" data-action="click->senren--api-key-field#toggle" class="inline-flex h-10 cursor-pointer items-center justify-center rounded-(--senren-radius) border border-[hsl(var(--senren-border))] px-3 text-sm font-medium hover:bg-[hsl(var(--senren-accent))]"><%= reveal_label %></button>
9
+ <button type="button" data-action="click->senren--api-key-field#copy" class="inline-flex h-10 cursor-pointer items-center justify-center rounded-(--senren-radius) bg-[hsl(var(--senren-primary))] px-3 text-sm font-medium text-[hsl(var(--senren-primary-foreground))] hover:opacity-90"><%= copy_label %></button>
10
+ </div>
11
+ <span class="sr-only" aria-live="polite" data-senren--api-key-field-target="status"></span>
12
+ <% end %>
13
+ <% end %>
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class ApiKeyFieldComponent < BaseComponent
5
+ VARIANTS = { default: '' }.freeze
6
+ SIZES = { md: '' }.freeze
7
+
8
+ def initialize(value: nil, label: 'API key', reveal_label: 'Reveal', hide_label: 'Hide', copy_label: 'Copy',
9
+ class_name: nil, **html)
10
+ super(variant: :default, size: :md, class_name: class_name, **html)
11
+ @value = value
12
+ @label = label
13
+ @reveal_label = reveal_label
14
+ @hide_label = hide_label
15
+ @copy_label = copy_label
16
+ end
17
+
18
+ attr_reader :value, :label, :reveal_label, :hide_label, :copy_label
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ <%= tag.div(**root_attrs("min-h-screen text-[hsl(var(--senren-foreground))]")) do %>
2
+ <a href="#<%= content_id %>" class="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-50 focus:rounded-(--senren-radius) focus:bg-[hsl(var(--senren-primary))] focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-[hsl(var(--senren-primary-foreground))]">
3
+ <%= skip_label %>
4
+ </a>
5
+
6
+ <% if top_nav? %>
7
+ <%= top_nav %>
8
+ <% end %>
9
+
10
+ <div class="mx-auto grid w-full max-w-[1400px] gap-6 px-4 py-6 md:grid-cols-[16rem_minmax(0,1fr)] lg:px-8">
11
+ <% if sidebar? %>
12
+ <aside class="min-w-0"><%= sidebar %></aside>
13
+ <% end %>
14
+
15
+ <main id="<%= content_id %>" class="min-w-0 <%= sidebar? ? '' : 'md:col-span-2' %>">
16
+ <% if header? %>
17
+ <div class="mb-6"><%= header %></div>
18
+ <% end %>
19
+ <%= content %>
20
+ </main>
21
+ </div>
22
+
23
+ <% if footer? %>
24
+ <footer class="border-t border-[hsl(var(--senren-border))] px-4 py-6 text-sm text-[hsl(var(--senren-muted-foreground))] lg:px-8">
25
+ <%= footer %>
26
+ </footer>
27
+ <% end %>
28
+ <% end %>
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class AppShellComponent < BaseComponent
5
+ renders_one :top_nav
6
+ renders_one :sidebar
7
+ renders_one :header
8
+ renders_one :footer
9
+
10
+ VARIANTS = {
11
+ default: 'bg-[hsl(var(--senren-background))]',
12
+ compact: 'bg-[hsl(var(--senren-muted)/0.25)]'
13
+ }.freeze
14
+ SIZES = { md: '' }.freeze
15
+
16
+ def initialize(content_id: 'senren-main', skip_label: 'Skip to content', variant: :default, class_name: nil, **html)
17
+ super(variant: variant, size: :md, class_name: class_name, **html)
18
+ @content_id = content_id
19
+ @skip_label = skip_label
20
+ end
21
+
22
+ attr_reader :content_id, :skip_label
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ <%= tag.div(**root_attrs("relative w-full overflow-hidden rounded-(--senren-radius)")) do %>
2
+ <%= content %>
3
+ <% end %>
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class AspectRatioComponent < BaseComponent
5
+ VARIANTS = {
6
+ square: 'aspect-square',
7
+ video: 'aspect-video',
8
+ portrait: 'aspect-[3/4]',
9
+ ultrawide: 'aspect-[21/9]'
10
+ }.freeze
11
+
12
+ SIZES = { md: '' }.freeze
13
+ end
14
+ end
@@ -0,0 +1,27 @@
1
+ <%= tag.span(**root_attrs("relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-[hsl(var(--senren-muted))] text-[hsl(var(--senren-foreground))] font-semibold shadow-sm ring-1 ring-[hsl(var(--senren-border))]", role: "img", "aria-label": alt.presence || fallback)) do %>
2
+ <% if src.present? %>
3
+ <%= image_tag src, alt: alt.to_s, class: "h-full w-full object-cover" %>
4
+ <% else %>
5
+ <svg aria-hidden="true" viewBox="0 0 64 64" preserveAspectRatio="none" class="absolute inset-0 h-full w-full">
6
+ <rect width="64" height="64" fill="#2b9bd7" />
7
+ <circle cx="42" cy="15" r="8" fill="#fffbea" />
8
+ <path d="M0 27 C10 20 16 22 24 17 C31 13 37 23 45 18 C54 12 58 22 64 20 L64 64 L0 64 Z" fill="#f6a4c8" />
9
+ <path d="M0 36 C10 29 19 35 29 29 C39 23 48 33 64 27 L64 64 L0 64 Z" fill="#86c84a" />
10
+ <path d="M5 46 C17 39 34 40 59 43 C50 53 23 57 5 46 Z" fill="#bdeef4" />
11
+ <path d="M0 50 C10 44 18 48 25 43 C28 51 42 48 46 55 C56 48 60 53 64 50 L64 64 L0 64 Z" fill="#7fc547" />
12
+ <path d="M2 54 C11 49 18 51 25 47 C28 55 16 60 5 61 Z" fill="#b59ce9" opacity=".9" />
13
+ <path d="M44 49 C52 45 59 47 64 44 L64 64 L45 64 C41 58 40 53 44 49 Z" fill="#16a060" opacity=".9" />
14
+ <g fill="#107e76" opacity=".45">
15
+ <circle cx="8" cy="12" r=".8" />
16
+ <circle cx="17" cy="25" r=".6" />
17
+ <circle cx="28" cy="9" r=".7" />
18
+ <circle cx="36" cy="32" r=".6" />
19
+ <circle cx="51" cy="25" r=".7" />
20
+ <circle cx="57" cy="38" r=".6" />
21
+ </g>
22
+ </svg>
23
+ <% if fallback.present? %>
24
+ <span class="relative z-10 rounded-full bg-[hsl(var(--senren-background)/0.72)] px-1.5 leading-none text-[hsl(var(--senren-foreground))] shadow-sm"><%= fallback %></span>
25
+ <% end %>
26
+ <% end %>
27
+ <% end %>
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class AvatarComponent < BaseComponent
5
+ VARIANTS = { default: '' }.freeze
6
+
7
+ SIZES = {
8
+ sm: 'h-8 w-8 text-xs',
9
+ md: 'h-10 w-10 text-sm',
10
+ lg: 'h-14 w-14 text-base'
11
+ }.freeze
12
+
13
+ def initialize(src: nil, alt: nil, fallback: nil, initials: nil, size: :md, class_name: nil, **html)
14
+ super(variant: :default, size: size, class_name: class_name, **html)
15
+ @src = src
16
+ @alt = alt
17
+ @fallback = fallback.presence || initials.presence || initials_from_alt
18
+ end
19
+
20
+ attr_reader :src, :alt, :fallback
21
+
22
+ private
23
+
24
+ def initials_from_alt
25
+ return '' if alt.blank?
26
+
27
+ alt.split.map(&:first).first(2).join.upcase
28
+ end
29
+ end
30
+ end
@@ -0,0 +1 @@
1
+ <%= tag.span content, **root_attrs("inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium") %>
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class BadgeComponent < BaseComponent
5
+ VARIANTS = {
6
+ default: 'bg-[hsl(var(--senren-secondary))] text-[hsl(var(--senren-secondary-foreground))]',
7
+ secondary: 'bg-[hsl(var(--senren-muted))] text-[hsl(var(--senren-muted-foreground))]',
8
+ success: 'bg-[hsl(var(--senren-success))] text-[hsl(var(--senren-success-foreground))]',
9
+ warning: 'bg-[hsl(var(--senren-warning))] text-[hsl(var(--senren-warning-foreground))]',
10
+ destructive: 'bg-[hsl(var(--senren-destructive))] text-[hsl(var(--senren-destructive-foreground))]',
11
+ outline: 'border border-[hsl(var(--senren-border))] text-[hsl(var(--senren-foreground))]'
12
+ }.freeze
13
+
14
+ SIZES = { md: '' }.freeze
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ <%= tag.div(**root_attrs("relative rounded-(--senren-radius) border bg-[hsl(var(--senren-card))] p-6 shadow-sm")) do %>
2
+ <% if badge.present? %>
3
+ <span class="absolute right-4 top-4 rounded-full bg-[hsl(var(--senren-accent))] px-2.5 py-1 text-xs font-semibold text-[hsl(var(--senren-accent-foreground))]"><%= badge %></span>
4
+ <% end %>
5
+
6
+ <% if content? && name.blank? %>
7
+ <%= content %>
8
+ <% else %>
9
+ <% if name.present? %><h3 class="font-display text-lg font-semibold text-[hsl(var(--senren-foreground))]"><%= name %></h3><% end %>
10
+ <% if description.present? %><p class="mt-2 text-sm leading-6 text-[hsl(var(--senren-muted-foreground))]"><%= description %></p><% end %>
11
+ <% if price.present? %>
12
+ <div class="mt-5 flex items-end gap-1">
13
+ <span class="font-display text-4xl font-semibold tracking-tight text-[hsl(var(--senren-foreground))]"><%= price %></span>
14
+ <% if interval.present? %><span class="pb-1 text-sm text-[hsl(var(--senren-muted-foreground))]"><%= interval %></span><% end %>
15
+ </div>
16
+ <% end %>
17
+ <% if features.any? %>
18
+ <ul class="mt-5 space-y-2 text-sm text-[hsl(var(--senren-muted-foreground))]">
19
+ <% features.each do |feature| %>
20
+ <li class="flex gap-2"><span class="text-[hsl(var(--senren-success))]">+</span><span><%= feature %></span></li>
21
+ <% end %>
22
+ </ul>
23
+ <% end %>
24
+ <% if cta_label.present? %>
25
+ <%= link_to cta_label, cta_href || "#", class: "mt-6 inline-flex h-10 w-full 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" %>
26
+ <% end %>
27
+ <% end %>
28
+ <% end %>
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class BillingPlanCardComponent < BaseComponent
5
+ VARIANTS = {
6
+ default: 'border-[hsl(var(--senren-border))]',
7
+ current: 'border-[hsl(var(--senren-accent))]',
8
+ recommended: 'border-[hsl(var(--senren-primary))]'
9
+ }.freeze
10
+ SIZES = { md: '' }.freeze
11
+
12
+ def initialize(name: nil, price: nil, interval: nil, description: nil, features: [], cta_label: nil, cta_href: nil,
13
+ badge: nil, variant: :default, class_name: nil, **html)
14
+ super(variant: variant, size: :md, class_name: class_name, **html)
15
+ @name = name
16
+ @price = price
17
+ @interval = interval
18
+ @description = description
19
+ @features = Array(features)
20
+ @cta_label = cta_label
21
+ @cta_href = cta_href
22
+ @badge = badge
23
+ end
24
+
25
+ attr_reader :name, :price, :interval, :description, :features, :cta_label, :cta_href, :badge
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ <% if items.any? %>
2
+ <nav <%= tag.attributes(**root_attrs("text-sm text-[hsl(var(--senren-muted-foreground))]", aria: { label: label })) %>>
3
+ <ol class="flex flex-wrap items-center gap-2">
4
+ <% items.each_with_index do |item, index| %>
5
+ <% current = index == items.length - 1 || item[:href].blank? %>
6
+ <li class="flex items-center gap-2">
7
+ <% if index.positive? %>
8
+ <span aria-hidden="true" class="select-none text-[hsl(var(--senren-muted-foreground)/0.7)]"><%= separator %></span>
9
+ <% end %>
10
+ <% if current %>
11
+ <span aria-current="page" class="font-medium text-[hsl(var(--senren-foreground))]"><%= item[:label] %></span>
12
+ <% else %>
13
+ <%= link_to item[:label], item[:href], class: "transition-colors hover:text-[hsl(var(--senren-foreground))]" %>
14
+ <% end %>
15
+ </li>
16
+ <% end %>
17
+ </ol>
18
+ </nav>
19
+ <% else %>
20
+ <nav <%= tag.attributes(**root_attrs("text-sm text-[hsl(var(--senren-muted-foreground))]", aria: { label: label })) %>>
21
+ <%= content %>
22
+ </nav>
23
+ <% end %>
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class BreadcrumbComponent < BaseComponent
5
+ VARIANTS = { default: '' }.freeze
6
+ SIZES = { md: '' }.freeze
7
+
8
+ def initialize(items: [], label: 'Breadcrumb', separator: '/', class_name: nil, **html)
9
+ super(variant: :default, size: :md, class_name: class_name, **html)
10
+ @items = normalize_items(items)
11
+ @label = label
12
+ @separator = separator
13
+ end
14
+
15
+ attr_reader :items, :label, :separator
16
+
17
+ private
18
+
19
+ def normalize_items(items)
20
+ Array(items).map do |item|
21
+ if item.is_a?(Hash)
22
+ { label: item.fetch(:label) { item.fetch('label') }, href: item[:href] || item['href'] }
23
+ else
24
+ label, href = item
25
+ { label: label, href: href }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,12 @@
1
+ <%= tag.div(**root_attrs("rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-foreground))] p-3 text-[hsl(var(--senren-background))] shadow-md", role: "status", "aria-live": "polite")) do %>
2
+ <% if content? && selected_count.nil? && !actions? %>
3
+ <%= content %>
4
+ <% else %>
5
+ <div class="flex flex-wrap items-center justify-between gap-3">
6
+ <div class="text-sm font-medium"><%= selection_text || "Selection ready" %></div>
7
+ <% if actions? %>
8
+ <div class="flex flex-wrap items-center gap-2"><%= actions %></div>
9
+ <% end %>
10
+ </div>
11
+ <% end %>
12
+ <% end %>
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ class BulkActionBarComponent < BaseComponent
5
+ renders_one :actions
6
+
7
+ VARIANTS = { default: '' }.freeze
8
+ SIZES = { md: '' }.freeze
9
+
10
+ def initialize(selected_count: nil, item_label: 'items', class_name: nil, **html)
11
+ super(variant: :default, size: :md, class_name: class_name, **html)
12
+ @selected_count = selected_count
13
+ @item_label = item_label
14
+ end
15
+
16
+ attr_reader :selected_count, :item_label
17
+
18
+ def selection_text
19
+ return nil if selected_count.nil?
20
+
21
+ "#{selected_count} #{item_label} selected"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,6 @@
1
+ <% base = "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-(--senren-radius) font-medium transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))] disabled:opacity-50 disabled:pointer-events-none disabled:cursor-not-allowed" %>
2
+ <% if as == :a %>
3
+ <%= tag.a content, **root_attrs(base, href: href) %>
4
+ <% else %>
5
+ <%= tag.button content, **root_attrs(base, type: type) %>
6
+ <% end %>