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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +33 -0
- data/CONTRIBUTING.md +63 -0
- data/LICENSE +21 -0
- data/README.md +135 -0
- data/Rakefile +22 -0
- data/docs/visual_style.md +51 -0
- data/lib/generators/senren/component/component_generator.rb +62 -0
- data/lib/generators/senren/component/templates/component.html.erb.tt +3 -0
- data/lib/generators/senren/component/templates/component.rb.tt +13 -0
- data/lib/generators/senren/component/templates/component_test.rb.tt +16 -0
- data/lib/generators/senren/component/templates/controller.js.tt +23 -0
- data/lib/generators/senren/component/templates/system_test.rb.tt +7 -0
- data/lib/generators/senren/install/install_generator.rb +67 -0
- data/lib/generators/senren/install/templates/base_component.rb.tt +45 -0
- data/lib/generators/senren/install/templates/conventions.md.tt +66 -0
- data/lib/generators/senren/install/templates/installed_components.yml.tt +4 -0
- data/lib/generators/senren/install/templates/senren.css.tt +164 -0
- data/lib/senren/rails/component_copier.rb +111 -0
- data/lib/senren/rails/doctor.rb +86 -0
- data/lib/senren/rails/engine.rb +16 -0
- data/lib/senren/rails/host_paths.rb +36 -0
- data/lib/senren/rails/installer.rb +83 -0
- data/lib/senren/rails/llms_writer.rb +149 -0
- data/lib/senren/rails/registry.rb +161 -0
- data/lib/senren/rails/skill_writer.rb +166 -0
- data/lib/senren/rails/version.rb +7 -0
- data/lib/senren/rails.rb +39 -0
- data/lib/tasks/senren.rake +74 -0
- data/registry/components.yml +1053 -0
- data/registry/groups.yml +25 -0
- data/registry/recipes.yml +79 -0
- data/templates/components/accordion/accordion_component.html.erb +16 -0
- data/templates/components/accordion/accordion_component.rb +31 -0
- data/templates/components/activity_feed/activity_feed_component.html.erb +22 -0
- data/templates/components/activity_feed/activity_feed_component.rb +19 -0
- data/templates/components/alert/alert_component.html.erb +9 -0
- data/templates/components/alert/alert_component.rb +18 -0
- data/templates/components/alert_dialog/alert_dialog_component.html.erb +34 -0
- data/templates/components/alert_dialog/alert_dialog_component.rb +21 -0
- data/templates/components/api_key_field/api_key_field_component.html.erb +13 -0
- data/templates/components/api_key_field/api_key_field_component.rb +20 -0
- data/templates/components/app_shell/app_shell_component.html.erb +28 -0
- data/templates/components/app_shell/app_shell_component.rb +24 -0
- data/templates/components/aspect_ratio/aspect_ratio_component.html.erb +3 -0
- data/templates/components/aspect_ratio/aspect_ratio_component.rb +14 -0
- data/templates/components/avatar/avatar_component.html.erb +27 -0
- data/templates/components/avatar/avatar_component.rb +30 -0
- data/templates/components/badge/badge_component.html.erb +1 -0
- data/templates/components/badge/badge_component.rb +16 -0
- data/templates/components/billing_plan_card/billing_plan_card_component.html.erb +28 -0
- data/templates/components/billing_plan_card/billing_plan_card_component.rb +27 -0
- data/templates/components/breadcrumb/breadcrumb_component.html.erb +23 -0
- data/templates/components/breadcrumb/breadcrumb_component.rb +30 -0
- data/templates/components/bulk_action_bar/bulk_action_bar_component.html.erb +12 -0
- data/templates/components/bulk_action_bar/bulk_action_bar_component.rb +24 -0
- data/templates/components/button/button_component.html.erb +6 -0
- data/templates/components/button/button_component.rb +29 -0
- data/templates/components/calendar/calendar_component.html.erb +21 -0
- data/templates/components/calendar/calendar_component.rb +30 -0
- data/templates/components/card/card_component.html.erb +13 -0
- data/templates/components/card/card_component.rb +17 -0
- data/templates/components/carousel/carousel_component.html.erb +68 -0
- data/templates/components/carousel/carousel_component.rb +34 -0
- data/templates/components/checkbox/checkbox_component.html.erb +8 -0
- data/templates/components/checkbox/checkbox_component.rb +19 -0
- data/templates/components/checkbox_group/checkbox_group_component.html.erb +10 -0
- data/templates/components/checkbox_group/checkbox_group_component.rb +30 -0
- data/templates/components/clipboard/clipboard_component.html.erb +7 -0
- data/templates/components/clipboard/clipboard_component.rb +17 -0
- data/templates/components/codeblock/codeblock_component.html.erb +11 -0
- data/templates/components/codeblock/codeblock_component.rb +31 -0
- data/templates/components/collapsible/collapsible_component.html.erb +9 -0
- data/templates/components/collapsible/collapsible_component.rb +19 -0
- data/templates/components/combobox/combobox_component.html.erb +19 -0
- data/templates/components/combobox/combobox_component.rb +38 -0
- data/templates/components/command/command_component.html.erb +22 -0
- data/templates/components/command/command_component.rb +38 -0
- data/templates/components/context_menu/context_menu_component.html.erb +11 -0
- data/templates/components/context_menu/context_menu_component.rb +11 -0
- data/templates/components/data_table/data_table_component.html.erb +50 -0
- data/templates/components/data_table/data_table_component.rb +42 -0
- data/templates/components/date_picker/date_picker_component.html.erb +5 -0
- data/templates/components/date_picker/date_picker_component.rb +21 -0
- data/templates/components/dialog/dialog_component.html.erb +38 -0
- data/templates/components/dialog/dialog_component.rb +22 -0
- data/templates/components/dropdown_menu/dropdown_menu_component.html.erb +12 -0
- data/templates/components/dropdown_menu/dropdown_menu_component.rb +36 -0
- data/templates/components/empty_state/empty_state_component.html.erb +18 -0
- data/templates/components/empty_state/empty_state_component.rb +22 -0
- data/templates/components/filter_bar/filter_bar_component.html.erb +5 -0
- data/templates/components/filter_bar/filter_bar_component.rb +15 -0
- data/templates/components/form/form_component.html.erb +3 -0
- data/templates/components/form/form_component.rb +18 -0
- data/templates/components/hover_card/hover_card_component.html.erb +10 -0
- data/templates/components/hover_card/hover_card_component.rb +11 -0
- data/templates/components/input/input_component.html.erb +1 -0
- data/templates/components/input/input_component.rb +28 -0
- data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +35 -0
- data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +26 -0
- data/templates/components/label/label_component.html.erb +4 -0
- data/templates/components/label/label_component.rb +19 -0
- data/templates/components/link/link_component.html.erb +1 -0
- data/templates/components/link/link_component.rb +25 -0
- data/templates/components/masked_input/masked_input_component.html.erb +1 -0
- data/templates/components/masked_input/masked_input_component.rb +18 -0
- data/templates/components/native_select/native_select_component.html.erb +14 -0
- data/templates/components/native_select/native_select_component.rb +52 -0
- data/templates/components/page_header/page_header_component.html.erb +20 -0
- data/templates/components/page_header/page_header_component.rb +19 -0
- data/templates/components/pagination/pagination_component.html.erb +11 -0
- data/templates/components/pagination/pagination_component.rb +24 -0
- data/templates/components/popover/popover_component.html.erb +9 -0
- data/templates/components/popover/popover_component.rb +11 -0
- data/templates/components/progress/progress_component.html.erb +11 -0
- data/templates/components/progress/progress_component.rb +26 -0
- data/templates/components/radio_button/radio_button_component.html.erb +8 -0
- data/templates/components/radio_button/radio_button_component.rb +19 -0
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.html.erb +32 -0
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.rb +30 -0
- data/templates/components/search_input/search_input_component.html.erb +14 -0
- data/templates/components/search_input/search_input_component.rb +18 -0
- data/templates/components/select/select_component.html.erb +1 -0
- data/templates/components/select/select_component.rb +19 -0
- data/templates/components/separator/separator_component.html.erb +1 -0
- data/templates/components/separator/separator_component.rb +12 -0
- data/templates/components/settings_section/settings_section_component.html.erb +20 -0
- data/templates/components/settings_section/settings_section_component.rb +18 -0
- data/templates/components/sheet/sheet_component.html.erb +37 -0
- data/templates/components/sheet/sheet_component.rb +27 -0
- data/templates/components/shortcut_key/shortcut_key_component.html.erb +6 -0
- data/templates/components/shortcut_key/shortcut_key_component.rb +15 -0
- data/templates/components/sidebar/sidebar_component.html.erb +14 -0
- data/templates/components/sidebar/sidebar_component.rb +37 -0
- data/templates/components/skeleton/skeleton_component.html.erb +1 -0
- data/templates/components/skeleton/skeleton_component.rb +13 -0
- data/templates/components/stat_card/stat_card_component.html.erb +20 -0
- data/templates/components/stat_card/stat_card_component.rb +31 -0
- data/templates/components/switch/switch_component.html.erb +11 -0
- data/templates/components/switch/switch_component.rb +19 -0
- data/templates/components/table/table_component.html.erb +26 -0
- data/templates/components/table/table_component.rb +35 -0
- data/templates/components/tabs/tabs_component.html.erb +18 -0
- data/templates/components/tabs/tabs_component.rb +35 -0
- data/templates/components/team_member_list/team_member_list_component.html.erb +22 -0
- data/templates/components/team_member_list/team_member_list_component.rb +26 -0
- data/templates/components/textarea/textarea_component.html.erb +1 -0
- data/templates/components/textarea/textarea_component.rb +23 -0
- data/templates/components/theme_toggle/theme_toggle_component.html.erb +4 -0
- data/templates/components/theme_toggle/theme_toggle_component.rb +15 -0
- data/templates/components/tooltip/tooltip_component.html.erb +9 -0
- data/templates/components/tooltip/tooltip_component.rb +16 -0
- data/templates/components/top_nav/top_nav_component.html.erb +21 -0
- data/templates/components/top_nav/top_nav_component.rb +44 -0
- data/templates/components/typography/typography_component.html.erb +1 -0
- data/templates/components/typography/typography_component.rb +24 -0
- data/templates/controllers/accordion_controller.js +27 -0
- data/templates/controllers/alert_dialog_controller.js +38 -0
- data/templates/controllers/api_key_field_controller.js +36 -0
- data/templates/controllers/calendar_controller.js +16 -0
- data/templates/controllers/carousel_controller.js +50 -0
- data/templates/controllers/clipboard_controller.js +17 -0
- data/templates/controllers/collapsible_controller.js +13 -0
- data/templates/controllers/combobox_controller.js +64 -0
- data/templates/controllers/command_controller.js +80 -0
- data/templates/controllers/context_menu_controller.js +36 -0
- data/templates/controllers/data_table_controller.js +34 -0
- data/templates/controllers/date_picker_controller.js +17 -0
- data/templates/controllers/dialog_controller.js +50 -0
- data/templates/controllers/dropdown_menu_controller.js +92 -0
- data/templates/controllers/hover_card_controller.js +17 -0
- data/templates/controllers/invite_member_dialog_controller.js +28 -0
- data/templates/controllers/masked_input_controller.js +30 -0
- data/templates/controllers/popover_controller.js +42 -0
- data/templates/controllers/rich_text_editor_lite_controller.js +443 -0
- data/templates/controllers/select_controller.js +10 -0
- data/templates/controllers/sheet_controller.js +34 -0
- data/templates/controllers/sidebar_controller.js +10 -0
- data/templates/controllers/tabs_controller.js +41 -0
- data/templates/controllers/theme_toggle_controller.js +24 -0
- data/templates/controllers/tooltip_controller.js +10 -0
- metadata +257 -0
data/registry/groups.yml
ADDED
|
@@ -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,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 %>
|