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
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class ButtonComponent < BaseComponent
|
|
5
|
+
VARIANTS = {
|
|
6
|
+
default: 'bg-[hsl(var(--senren-secondary))] text-[hsl(var(--senren-secondary-foreground))] hover:opacity-90',
|
|
7
|
+
primary: 'bg-[hsl(var(--senren-primary))] text-[hsl(var(--senren-primary-foreground))] hover:opacity-90',
|
|
8
|
+
secondary: 'bg-[hsl(var(--senren-secondary))] text-[hsl(var(--senren-secondary-foreground))] hover:opacity-90',
|
|
9
|
+
destructive: 'bg-[hsl(var(--senren-destructive))] text-[hsl(var(--senren-destructive-foreground))] hover:opacity-90',
|
|
10
|
+
ghost: 'bg-transparent text-[hsl(var(--senren-foreground))] hover:bg-[hsl(var(--senren-accent))]',
|
|
11
|
+
link: 'bg-transparent text-[hsl(var(--senren-primary))] underline-offset-4 hover:underline'
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
SIZES = {
|
|
15
|
+
sm: 'h-8 px-3 text-sm',
|
|
16
|
+
md: 'h-10 px-4 text-sm',
|
|
17
|
+
lg: 'h-12 px-6 text-base'
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def initialize(variant: :default, size: :md, type: 'button', as: :button, href: nil, class_name: nil, **html)
|
|
21
|
+
super(variant: variant, size: size, class_name: class_name, **html)
|
|
22
|
+
@type = type
|
|
23
|
+
@as = href ? :a : as
|
|
24
|
+
@href = href
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :type, :as, :href
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<div <%= tag.attributes(**root_attrs("w-72 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] p-3 text-[hsl(var(--senren-card-foreground))]", data: { controller: "senren--calendar" })) %>>
|
|
2
|
+
<% if name.present? %>
|
|
3
|
+
<input type="hidden" name="<%= name %>" value="<%= selected&.iso8601 %>" data-senren--calendar-target="value">
|
|
4
|
+
<% end %>
|
|
5
|
+
<div class="mb-3 flex items-center justify-between">
|
|
6
|
+
<p class="font-display text-sm font-semibold tracking-tight"><%= title %></p>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="grid grid-cols-7 gap-1 text-center text-xs text-[hsl(var(--senren-muted-foreground))]">
|
|
9
|
+
<% weekday_labels.each do |day| %>
|
|
10
|
+
<div class="py-1"><%= day %></div>
|
|
11
|
+
<% end %>
|
|
12
|
+
</div>
|
|
13
|
+
<div role="grid" class="mt-1 grid grid-cols-7 gap-1">
|
|
14
|
+
<% calendar_days.each do |day| %>
|
|
15
|
+
<% outside = day.month != date.month %>
|
|
16
|
+
<button type="button" role="gridcell" data-senren--calendar-target="day" data-date="<%= day.iso8601 %>" data-action="click->senren--calendar#select" class="h-9 cursor-pointer rounded-md text-sm transition-colors <%= selected?(day) ? "bg-[hsl(var(--senren-primary))] text-[hsl(var(--senren-primary-foreground))]" : "hover:bg-[hsl(var(--senren-accent))] hover:text-[hsl(var(--senren-accent-foreground))]" %> <%= outside ? "text-[hsl(var(--senren-muted-foreground)/0.45)]" : "text-[hsl(var(--senren-foreground))]" %>">
|
|
17
|
+
<%= day.day %>
|
|
18
|
+
</button>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Senren
|
|
2
|
+
class CalendarComponent < BaseComponent
|
|
3
|
+
VARIANTS = { default: '' }.freeze
|
|
4
|
+
SIZES = { md: '' }.freeze
|
|
5
|
+
|
|
6
|
+
def initialize(date: Date.current, selected: nil, name: nil, class_name: nil, **html)
|
|
7
|
+
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
8
|
+
@date = date.to_date
|
|
9
|
+
@selected = selected&.to_date
|
|
10
|
+
@name = name
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :date, :selected, :name
|
|
14
|
+
|
|
15
|
+
def month_start = date.beginning_of_month
|
|
16
|
+
def month_end = date.end_of_month
|
|
17
|
+
def title = date.strftime('%B %Y')
|
|
18
|
+
def weekday_labels = Date::ABBR_DAYNAMES
|
|
19
|
+
|
|
20
|
+
def calendar_days
|
|
21
|
+
start_day = month_start.beginning_of_week(:sunday)
|
|
22
|
+
end_day = month_end.end_of_week(:sunday)
|
|
23
|
+
(start_day..end_day).to_a
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def selected?(day)
|
|
27
|
+
selected && day == selected
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%= tag.div(**root_attrs("rounded-(--senren-radius) border shadow-sm")) do %>
|
|
2
|
+
<% if header? %>
|
|
3
|
+
<div class="flex flex-col space-y-1.5 p-6 border-b border-[hsl(var(--senren-border))]"><%= header %></div>
|
|
4
|
+
<% end %>
|
|
5
|
+
<% if body? %>
|
|
6
|
+
<div class="p-6"><%= body %></div>
|
|
7
|
+
<% else %>
|
|
8
|
+
<div class="p-6"><%= content %></div>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% if footer? %>
|
|
11
|
+
<div class="flex items-center p-6 pt-0 border-t border-[hsl(var(--senren-border))]"><%= footer %></div>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% end %>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class CardComponent < BaseComponent
|
|
5
|
+
renders_one :header
|
|
6
|
+
renders_one :body
|
|
7
|
+
renders_one :footer
|
|
8
|
+
|
|
9
|
+
VARIANTS = {
|
|
10
|
+
default: 'bg-[hsl(var(--senren-card))] text-[hsl(var(--senren-card-foreground))] border-[hsl(var(--senren-border))]',
|
|
11
|
+
muted: 'bg-[hsl(var(--senren-muted))] text-[hsl(var(--senren-muted-foreground))] border-transparent',
|
|
12
|
+
outline: 'bg-transparent text-[hsl(var(--senren-foreground))] border-[hsl(var(--senren-border))]'
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
SIZES = { md: '' }.freeze
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<%= tag.section(**root_attrs("relative overflow-hidden rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] text-[hsl(var(--senren-card-foreground))] shadow-sm", data: { controller: "senren--carousel" }, role: "region", "aria-roledescription": "carousel", "aria-label": label)) do %>
|
|
2
|
+
<% if content? && slides.empty? %>
|
|
3
|
+
<%= content %>
|
|
4
|
+
<% else %>
|
|
5
|
+
<div class="relative" data-action="keydown->senren--carousel#onKey" tabindex="0">
|
|
6
|
+
<% slides.each_with_index do |slide, index| %>
|
|
7
|
+
<article data-senren--carousel-target="slide" data-index="<%= index %>" class="<%= index.zero? ? '' : 'hidden' %>">
|
|
8
|
+
<% if slide[:image_url].present? %>
|
|
9
|
+
<img src="<%= slide[:image_url] %>" alt="<%= slide[:alt] || slide[:title] %>" class="aspect-video w-full object-cover">
|
|
10
|
+
<% else %>
|
|
11
|
+
<div class="relative flex aspect-video items-end overflow-hidden bg-[hsl(var(--senren-muted)/0.45)] p-6">
|
|
12
|
+
<svg aria-hidden="true" viewBox="0 0 960 540" preserveAspectRatio="none" class="absolute inset-0 h-full w-full">
|
|
13
|
+
<rect width="960" height="540" fill="#2397cf" />
|
|
14
|
+
<circle cx="486" cy="75" r="55" fill="#fffbea" />
|
|
15
|
+
<path d="M0 181 C72 158 111 165 156 124 C206 82 243 133 291 113 C340 92 383 159 437 127 C486 98 521 145 565 120 C611 94 649 151 704 92 C755 38 798 94 830 134 C880 127 912 153 960 137 L960 315 L0 315 Z" fill="#f4a6c8" />
|
|
16
|
+
<path d="M0 255 C72 214 124 230 193 205 C251 184 280 227 332 199 C398 165 449 215 520 190 C608 158 642 232 728 197 C817 159 867 224 960 178 L960 540 L0 540 Z" fill="#8fd04e" />
|
|
17
|
+
<path d="M118 313 C213 276 366 272 538 294 C691 314 792 288 883 318 C764 384 546 401 348 380 C235 368 160 350 118 313 Z" fill="#b9edf5" />
|
|
18
|
+
<path d="M0 350 C73 321 145 352 217 318 C287 285 336 340 413 324 C474 311 520 341 570 319 C661 279 737 343 805 313 C864 287 912 321 960 301 L960 540 L0 540 Z" fill="#79c945" />
|
|
19
|
+
<path d="M0 427 C61 391 102 415 150 383 C187 430 247 424 287 470 C216 500 102 514 0 490 Z" fill="#b59ce9" />
|
|
20
|
+
<path d="M657 394 C733 356 804 380 862 342 C901 403 934 398 960 421 L960 540 L676 540 C622 491 615 428 657 394 Z" fill="#b59ce9" opacity=".86" />
|
|
21
|
+
<path d="M0 456 C57 434 105 459 163 428 C186 482 261 469 298 517 C218 539 95 552 0 527 Z" fill="#f29bc7" opacity=".86" />
|
|
22
|
+
<path d="M752 423 C812 395 879 415 960 381 L960 540 L725 540 C706 499 715 449 752 423 Z" fill="#129b5a" opacity=".92" />
|
|
23
|
+
<g fill="#0a7f77" opacity=".48">
|
|
24
|
+
<circle cx="44" cy="93" r="1.2" /><circle cx="86" cy="143" r="1.4" /><circle cx="140" cy="238" r="1.1" /><circle cx="212" cy="168" r="1.5" />
|
|
25
|
+
<circle cx="318" cy="74" r="1.1" /><circle cx="384" cy="218" r="1.4" /><circle cx="470" cy="158" r="1.2" /><circle cx="548" cy="251" r="1.5" />
|
|
26
|
+
<circle cx="635" cy="118" r="1.1" /><circle cx="716" cy="230" r="1.4" /><circle cx="803" cy="152" r="1.2" /><circle cx="899" cy="260" r="1.5" />
|
|
27
|
+
<circle cx="110" cy="438" r="1.4" /><circle cx="248" cy="468" r="1.2" /><circle cx="406" cy="423" r="1.5" /><circle cx="592" cy="460" r="1.2" />
|
|
28
|
+
<circle cx="742" cy="445" r="1.5" /><circle cx="884" cy="420" r="1.2" />
|
|
29
|
+
</g>
|
|
30
|
+
</svg>
|
|
31
|
+
<div class="relative z-10 max-w-md rounded-(--senren-radius) bg-[hsl(var(--senren-background)/0.78)] p-4 shadow-sm ring-1 ring-[hsl(var(--senren-border)/0.7)] backdrop-blur">
|
|
32
|
+
<% if slide[:badge].present? %>
|
|
33
|
+
<span class="mb-3 inline-flex rounded-full bg-[hsl(var(--senren-accent))] px-2.5 py-1 text-xs font-semibold text-[hsl(var(--senren-accent-foreground))]"><%= slide[:badge] %></span>
|
|
34
|
+
<% end %>
|
|
35
|
+
<h3 class="font-display text-2xl font-semibold tracking-tight text-[hsl(var(--senren-foreground))]"><%= slide[:title] %></h3>
|
|
36
|
+
<% if slide[:description].present? %>
|
|
37
|
+
<p class="mt-2 text-sm leading-6 text-[hsl(var(--senren-muted-foreground))]"><%= slide[:description] %></p>
|
|
38
|
+
<% end %>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<% end %>
|
|
42
|
+
</article>
|
|
43
|
+
<% end %>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<% if slides.size > 1 %>
|
|
47
|
+
<div class="pointer-events-none absolute inset-x-0 top-1/2 z-10 flex -translate-y-1/2 justify-between px-3">
|
|
48
|
+
<button type="button" class="pointer-events-auto inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-full border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-background)/0.88)] text-[hsl(var(--senren-foreground))] shadow-sm transition hover:-translate-y-0.5 hover:bg-[hsl(var(--senren-accent))] hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))]" data-action="click->senren--carousel#previous" aria-label="Previous slide">
|
|
49
|
+
<svg aria-hidden="true" viewBox="0 0 20 20" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
50
|
+
<path d="M12.5 4.5 7 10l5.5 5.5" />
|
|
51
|
+
</svg>
|
|
52
|
+
</button>
|
|
53
|
+
<button type="button" class="pointer-events-auto inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-full border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-background)/0.88)] text-[hsl(var(--senren-foreground))] shadow-sm transition hover:-translate-y-0.5 hover:bg-[hsl(var(--senren-accent))] hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))]" data-action="click->senren--carousel#next" aria-label="Next slide">
|
|
54
|
+
<svg aria-hidden="true" viewBox="0 0 20 20" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
55
|
+
<path d="M7.5 4.5 13 10l-5.5 5.5" />
|
|
56
|
+
</svg>
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="absolute bottom-3 left-0 right-0 z-10 flex justify-center gap-1.5">
|
|
61
|
+
<% slides.each_with_index do |_slide, index| %>
|
|
62
|
+
<button type="button" class="h-2.5 w-2.5 cursor-pointer rounded-full bg-[hsl(var(--senren-background)/0.72)] ring-1 ring-[hsl(var(--senren-border))] transition-all aria-current:w-6 aria-current:bg-[hsl(var(--senren-primary))]" data-senren--carousel-target="dot" data-index="<%= index %>" data-action="click->senren--carousel#goTo" aria-label="Go to slide <%= index + 1 %>" aria-current="<%= index.zero? ? 'true' : 'false' %>"></button>
|
|
63
|
+
<% end %>
|
|
64
|
+
</div>
|
|
65
|
+
<% end %>
|
|
66
|
+
<p class="sr-only" aria-live="polite" data-senren--carousel-target="status">Slide 1 of <%= slides.size %></p>
|
|
67
|
+
<% end %>
|
|
68
|
+
<% end %>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class CarouselComponent < BaseComponent
|
|
5
|
+
VARIANTS = { default: '' }.freeze
|
|
6
|
+
SIZES = { md: '' }.freeze
|
|
7
|
+
|
|
8
|
+
def initialize(slides: [], label: 'Carousel', class_name: nil, **html)
|
|
9
|
+
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
10
|
+
@slides = normalize_slides(slides)
|
|
11
|
+
@label = label
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :slides, :label
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def normalize_slides(slides)
|
|
19
|
+
Array(slides).map do |slide|
|
|
20
|
+
if slide.is_a?(Hash)
|
|
21
|
+
{
|
|
22
|
+
title: slide[:title] || slide['title'],
|
|
23
|
+
description: slide[:description] || slide['description'],
|
|
24
|
+
image_url: slide[:image_url] || slide['image_url'],
|
|
25
|
+
alt: slide[:alt] || slide['alt'],
|
|
26
|
+
badge: slide[:badge] || slide['badge']
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
{ title: slide.to_s, description: nil, image_url: nil, alt: nil, badge: nil }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<label class="inline-flex items-center gap-2 cursor-pointer">
|
|
2
|
+
<%= tag.input(**root_attrs("peer h-4 w-4 rounded border border-[hsl(var(--senren-input))] text-[hsl(var(--senren-primary))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))] disabled:cursor-not-allowed disabled:opacity-50", type: "checkbox", id: id, name: name, value: value, checked: checked)) %>
|
|
3
|
+
<% if label %>
|
|
4
|
+
<span class="text-sm font-medium text-[hsl(var(--senren-foreground))]"><%= label %></span>
|
|
5
|
+
<% else %>
|
|
6
|
+
<%= content %>
|
|
7
|
+
<% end %>
|
|
8
|
+
</label>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class CheckboxComponent < BaseComponent
|
|
5
|
+
VARIANTS = { default: '' }.freeze
|
|
6
|
+
SIZES = { md: '' }.freeze
|
|
7
|
+
|
|
8
|
+
def initialize(name:, value: '1', checked: false, id: nil, label: nil, class_name: nil, **html)
|
|
9
|
+
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
10
|
+
@name = name
|
|
11
|
+
@value = value
|
|
12
|
+
@checked = checked
|
|
13
|
+
@id = id || "#{name.to_s.parameterize}-#{SecureRandom.hex(2)}"
|
|
14
|
+
@label = label
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :name, :value, :checked, :id, :label
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<%= tag.fieldset(**root_attrs("space-y-2")) do %>
|
|
2
|
+
<% if legend? %>
|
|
3
|
+
<legend class="text-sm font-medium text-[hsl(var(--senren-foreground))]"><%= legend %></legend>
|
|
4
|
+
<% end %>
|
|
5
|
+
<div class="flex flex-col gap-2">
|
|
6
|
+
<% options.each do |opt| %>
|
|
7
|
+
<%= opt %>
|
|
8
|
+
<% end %>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Senren
|
|
2
|
+
class CheckboxGroupComponent < BaseComponent
|
|
3
|
+
renders_one :legend
|
|
4
|
+
renders_many :options, 'OptionTag'
|
|
5
|
+
|
|
6
|
+
VARIANTS = { default: '' }.freeze
|
|
7
|
+
SIZES = { md: '' }.freeze
|
|
8
|
+
|
|
9
|
+
class OptionTag < ViewComponent::Base
|
|
10
|
+
def initialize(name:, value:, label:, checked: false)
|
|
11
|
+
@name = name
|
|
12
|
+
@value = value
|
|
13
|
+
@label = label
|
|
14
|
+
@checked = checked
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
content_tag(:label, class: 'inline-flex items-center gap-2 cursor-pointer') do
|
|
19
|
+
tag.input(type: 'checkbox', name: name, value: @value, checked: @checked,
|
|
20
|
+
class: 'h-4 w-4 rounded border border-[hsl(var(--senren-input))] text-[hsl(var(--senren-primary))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))]') +
|
|
21
|
+
content_tag(:span, @label, class: 'text-sm text-[hsl(var(--senren-foreground))]')
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :name
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<div <%= tag.attributes(**root_attrs("inline-flex items-center gap-2", data: { controller: "senren--clipboard", "senren--clipboard-copied-label-value": copied_label })) %>>
|
|
2
|
+
<input type="text" readonly value="<%= value %>" data-senren--clipboard-target="source" class="h-10 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-muted)/0.4)] px-3 font-mono text-sm text-[hsl(var(--senren-foreground))]">
|
|
3
|
+
<button type="button" data-senren--clipboard-target="button" data-action="click->senren--clipboard#copy" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) bg-[hsl(var(--senren-primary))] px-3 text-sm font-medium text-[hsl(var(--senren-primary-foreground))] hover:opacity-90">
|
|
4
|
+
<%= label %>
|
|
5
|
+
</button>
|
|
6
|
+
<span class="sr-only" aria-live="polite" data-senren--clipboard-target="status"></span>
|
|
7
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class ClipboardComponent < BaseComponent
|
|
5
|
+
VARIANTS = { default: '' }.freeze
|
|
6
|
+
SIZES = { md: '' }.freeze
|
|
7
|
+
|
|
8
|
+
def initialize(value:, label: 'Copy', copied_label: 'Copied', class_name: nil, **html)
|
|
9
|
+
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
10
|
+
@label = label
|
|
11
|
+
@value = value
|
|
12
|
+
@copied_label = copied_label
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :label, :value, :copied_label
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%= tag.figure(**root_attrs("overflow-hidden rounded-(--senren-radius) border")) do %>
|
|
2
|
+
<% if filename.present? || language_label.present? || caption.present? %>
|
|
3
|
+
<figcaption class="flex items-center justify-between gap-3 border-b border-[hsl(var(--senren-border))] px-4 py-2 text-xs text-[hsl(var(--senren-muted-foreground))]">
|
|
4
|
+
<span class="min-w-0 truncate font-medium text-[hsl(var(--senren-foreground))]"><%= filename.presence || caption %></span>
|
|
5
|
+
<% if language_label.present? %>
|
|
6
|
+
<span class="shrink-0 rounded bg-[hsl(var(--senren-muted))] px-1.5 py-0.5 font-mono uppercase tracking-wide"><%= language_label %></span>
|
|
7
|
+
<% end %>
|
|
8
|
+
</figcaption>
|
|
9
|
+
<% end %>
|
|
10
|
+
<pre class="overflow-x-auto p-4 text-left text-sm leading-6 text-[hsl(var(--senren-foreground))] <%= wrap? ? 'whitespace-pre-wrap break-words' : '' %>"><code class="font-mono"><%= code_text %></code></pre>
|
|
11
|
+
<% end %>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Senren
|
|
2
|
+
class CodeblockComponent < BaseComponent
|
|
3
|
+
VARIANTS = {
|
|
4
|
+
default: 'border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-muted)/0.35)]',
|
|
5
|
+
elevated: 'border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] shadow-sm'
|
|
6
|
+
}.freeze
|
|
7
|
+
SIZES = { md: '' }.freeze
|
|
8
|
+
|
|
9
|
+
def initialize(source: nil, language: nil, filename: nil, caption: nil, wrap: false, variant: :default,
|
|
10
|
+
class_name: nil, **html)
|
|
11
|
+
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
12
|
+
@source = source
|
|
13
|
+
@language = language
|
|
14
|
+
@filename = filename
|
|
15
|
+
@caption = caption
|
|
16
|
+
@wrap = wrap
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :source, :language, :filename, :caption
|
|
20
|
+
|
|
21
|
+
def wrap? = !!@wrap
|
|
22
|
+
|
|
23
|
+
def code_text
|
|
24
|
+
source.presence || content.to_s
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def language_label
|
|
28
|
+
language.to_s.presence
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<div <%= tag.attributes(**root_attrs("rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))]", data: { controller: "senren--collapsible" })) %>>
|
|
2
|
+
<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-card-foreground))]" aria-expanded="<%= open? %>" data-senren--collapsible-target="trigger" data-action="click->senren--collapsible#toggle">
|
|
3
|
+
<span><%= trigger || title %></span>
|
|
4
|
+
<span aria-hidden="true" class="text-[hsl(var(--senren-muted-foreground))]">+</span>
|
|
5
|
+
</button>
|
|
6
|
+
<div data-senren--collapsible-target="panel" <%= "hidden" unless open? %> class="border-t border-[hsl(var(--senren-border))] px-4 py-3 text-sm text-[hsl(var(--senren-muted-foreground))]">
|
|
7
|
+
<%= body || content %>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Senren
|
|
2
|
+
class CollapsibleComponent < BaseComponent
|
|
3
|
+
renders_one :trigger
|
|
4
|
+
renders_one :body
|
|
5
|
+
|
|
6
|
+
VARIANTS = { default: '' }.freeze
|
|
7
|
+
SIZES = { md: '' }.freeze
|
|
8
|
+
|
|
9
|
+
def initialize(title: 'Details', open: false, class_name: nil, **html)
|
|
10
|
+
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
11
|
+
@title = title
|
|
12
|
+
@open = open
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :title
|
|
16
|
+
|
|
17
|
+
def open? = @open
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<div <%= tag.attributes(**root_attrs("relative w-full", data: { controller: "senren--combobox" })) %>>
|
|
2
|
+
<input type="hidden" name="<%= name %>" value="<%= value %>" data-senren--combobox-target="value">
|
|
3
|
+
<button type="button" class="group flex h-10 w-full cursor-pointer items-center justify-between rounded-(--senren-radius) border bg-[hsl(var(--senren-background))] px-3 text-left text-sm text-[hsl(var(--senren-foreground))] transition-colors hover:bg-[hsl(var(--senren-muted)/0.35)] <%= self.class::VARIANTS[variant] %>" aria-haspopup="listbox" aria-expanded="false" data-state="closed" data-senren--combobox-target="button" data-action="click->senren--combobox#toggle">
|
|
4
|
+
<span data-senren--combobox-target="label"><%= selected_label || placeholder %></span>
|
|
5
|
+
<svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 shrink-0 text-[hsl(var(--senren-muted-foreground))] transition-transform duration-150 data-[state=open]:rotate-180" data-state="closed" data-senren--combobox-target="chevron">
|
|
6
|
+
<path d="m5 8 5 5 5-5"></path>
|
|
7
|
+
</svg>
|
|
8
|
+
</button>
|
|
9
|
+
<div data-senren--combobox-target="panel" hidden class="absolute z-50 mt-2 w-full rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] p-2 shadow-md">
|
|
10
|
+
<input type="text" placeholder="<%= placeholder %>" data-senren--combobox-target="search" data-action="input->senren--combobox#filter keydown->senren--combobox#onKey" class="mb-2 h-9 w-full rounded-md border border-[hsl(var(--senren-input))] bg-[hsl(var(--senren-background))] px-2 text-sm outline-none focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
|
|
11
|
+
<div role="listbox" class="max-h-56 overflow-auto">
|
|
12
|
+
<% options.each do |option| %>
|
|
13
|
+
<button type="button" role="option" data-senren--combobox-target="option" data-value="<%= option[:value] %>" data-label="<%= option[:label] %>" data-action="click->senren--combobox#choose" class="block w-full cursor-pointer rounded-md px-2 py-2 text-left text-sm text-[hsl(var(--senren-popover-foreground))] hover:bg-[hsl(var(--senren-accent))]">
|
|
14
|
+
<%= option[:label] %>
|
|
15
|
+
</button>
|
|
16
|
+
<% end %>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class ComboboxComponent < BaseComponent
|
|
5
|
+
VARIANTS = {
|
|
6
|
+
default: 'border-[hsl(var(--senren-border))]',
|
|
7
|
+
error: 'border-[hsl(var(--senren-destructive))]'
|
|
8
|
+
}.freeze
|
|
9
|
+
SIZES = { md: '' }.freeze
|
|
10
|
+
|
|
11
|
+
def initialize(name:, options:, value: nil, placeholder: 'Search...', variant: :default, class_name: nil, **html)
|
|
12
|
+
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
13
|
+
@name = name
|
|
14
|
+
@options = normalize_options(options)
|
|
15
|
+
@value = value
|
|
16
|
+
@placeholder = placeholder
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :name, :options, :value, :placeholder
|
|
20
|
+
|
|
21
|
+
def selected_label
|
|
22
|
+
options.find { |option| option[:value].to_s == value.to_s }&.dig(:label)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def normalize_options(options)
|
|
28
|
+
Array(options).map do |option|
|
|
29
|
+
if option.is_a?(Hash)
|
|
30
|
+
{ value: option[:value] || option['value'], label: option[:label] || option['label'] }
|
|
31
|
+
else
|
|
32
|
+
value, label = option
|
|
33
|
+
{ value: value, label: label || value }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<%= tag.div(**root_attrs("overflow-hidden rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] shadow-sm", data: { controller: "senren--command" })) do %>
|
|
2
|
+
<% if content? && items.empty? %>
|
|
3
|
+
<%= content %>
|
|
4
|
+
<% else %>
|
|
5
|
+
<label for="<%= dom_id %>-input" class="sr-only"><%= label %></label>
|
|
6
|
+
<div class="border-b border-[hsl(var(--senren-border))] p-2">
|
|
7
|
+
<input id="<%= dom_id %>-input" type="search" autocomplete="off" placeholder="<%= placeholder %>" aria-label="<%= label %>" aria-controls="<%= dom_id %>-list" aria-activedescendant="" data-senren--command-target="input" data-action="input->senren--command#filter keydown->senren--command#onKey" class="h-10 w-full rounded-(--senren-radius) bg-[hsl(var(--senren-background))] px-3 text-sm text-[hsl(var(--senren-foreground))] outline-none placeholder:text-[hsl(var(--senren-muted-foreground))] focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
|
|
8
|
+
</div>
|
|
9
|
+
<div id="<%= dom_id %>-list" role="listbox" aria-label="<%= label %>" data-senren--command-target="list" class="max-h-72 overflow-y-auto p-1.5">
|
|
10
|
+
<% items.each_with_index do |item, index| %>
|
|
11
|
+
<% tag_name = item[:href].present? ? :a : :button %>
|
|
12
|
+
<%= tag.public_send(tag_name, href: item[:href], type: (tag_name == :button ? "button" : nil), id: item[:id], role: "option", data: { "senren--command-target": "option", action: "click->senren--command#choose", label: item[:keywords] }, class: "block w-full cursor-pointer rounded-(--senren-radius) px-3 py-2 text-left text-sm transition-colors hover:bg-[hsl(var(--senren-accent))] aria-selected:bg-[hsl(var(--senren-accent))] aria-selected:text-[hsl(var(--senren-accent-foreground))]", "aria-selected": (index.zero? ? "true" : "false")) do %>
|
|
13
|
+
<span class="block font-medium"><%= item[:label] %></span>
|
|
14
|
+
<% if item[:description].present? %>
|
|
15
|
+
<span class="mt-0.5 block text-xs text-[hsl(var(--senren-muted-foreground))]"><%= item[:description] %></span>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% end %>
|
|
19
|
+
<div hidden data-senren--command-target="empty" class="px-3 py-8 text-center text-sm text-[hsl(var(--senren-muted-foreground))]"><%= empty_text %></div>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% end %>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class CommandComponent < BaseComponent
|
|
5
|
+
VARIANTS = { default: '' }.freeze
|
|
6
|
+
SIZES = { md: '' }.freeze
|
|
7
|
+
|
|
8
|
+
def initialize(items: [], placeholder: 'Type a command...', label: 'Command menu', empty_text: 'No results found.',
|
|
9
|
+
id: nil, class_name: nil, **html)
|
|
10
|
+
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
11
|
+
@placeholder = placeholder
|
|
12
|
+
@label = label
|
|
13
|
+
@empty_text = empty_text
|
|
14
|
+
@dom_id = id || "senren-command-#{SecureRandom.hex(3)}"
|
|
15
|
+
@items = normalize_items(items)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :items, :placeholder, :label, :empty_text, :dom_id
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def normalize_items(items)
|
|
23
|
+
Array(items).map.with_index do |item, index|
|
|
24
|
+
data = item.is_a?(Hash) ? item : { label: item.to_s }
|
|
25
|
+
label = data[:label] || data['label']
|
|
26
|
+
description = data[:description] || data['description']
|
|
27
|
+
keywords = data[:keywords] || data['keywords']
|
|
28
|
+
{
|
|
29
|
+
id: data[:id] || data['id'] || "#{dom_id}-option-#{index}",
|
|
30
|
+
label: label,
|
|
31
|
+
description: description,
|
|
32
|
+
href: data[:href] || data['href'],
|
|
33
|
+
keywords: [label, description, keywords].flatten.compact.join(' ')
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<div data-controller="senren--context-menu" class="relative" data-senren-component="context_menu">
|
|
2
|
+
<div data-senren--context-menu-target="trigger" data-action="contextmenu->senren--context-menu#open">
|
|
3
|
+
<%= trigger %>
|
|
4
|
+
</div>
|
|
5
|
+
<div data-senren--context-menu-target="menu" role="menu" hidden
|
|
6
|
+
class="absolute z-50 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">
|
|
7
|
+
<% items.each do |it| %>
|
|
8
|
+
<%= it %>
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<%= tag.div(**root_attrs("overflow-hidden rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] shadow-sm", data: { controller: "senren--data-table" })) do %>
|
|
2
|
+
<% if toolbar? %>
|
|
3
|
+
<div class="border-b border-[hsl(var(--senren-border))] p-3"><%= toolbar %></div>
|
|
4
|
+
<% end %>
|
|
5
|
+
|
|
6
|
+
<% if content? && columns.empty? %>
|
|
7
|
+
<div class="p-3"><%= content %></div>
|
|
8
|
+
<% else %>
|
|
9
|
+
<div class="overflow-x-auto">
|
|
10
|
+
<table class="w-full border-collapse text-left text-sm">
|
|
11
|
+
<% if caption.present? %>
|
|
12
|
+
<caption class="sr-only"><%= caption %></caption>
|
|
13
|
+
<% end %>
|
|
14
|
+
<thead class="bg-[hsl(var(--senren-muted)/0.6)] text-[hsl(var(--senren-muted-foreground))]">
|
|
15
|
+
<tr>
|
|
16
|
+
<% columns.each do |column| %>
|
|
17
|
+
<th scope="col" class="px-4 py-3 font-medium">
|
|
18
|
+
<% if sortable? %>
|
|
19
|
+
<button type="button" class="inline-flex cursor-pointer items-center gap-1 rounded-sm hover:text-[hsl(var(--senren-foreground))]" data-action="click->senren--data-table#sort" data-sort-key="<%= column_sort_key(column) %>">
|
|
20
|
+
<%= column_label(column) %>
|
|
21
|
+
<span aria-hidden="true">Sort</span>
|
|
22
|
+
</button>
|
|
23
|
+
<% else %>
|
|
24
|
+
<%= column_label(column) %>
|
|
25
|
+
<% end %>
|
|
26
|
+
</th>
|
|
27
|
+
<% end %>
|
|
28
|
+
</tr>
|
|
29
|
+
</thead>
|
|
30
|
+
<tbody class="divide-y divide-[hsl(var(--senren-border))] text-[hsl(var(--senren-foreground))]" data-senren--data-table-target="body">
|
|
31
|
+
<% rows.each do |row| %>
|
|
32
|
+
<tr class="transition-colors hover:bg-[hsl(var(--senren-muted)/0.35)]" data-senren--data-table-target="row">
|
|
33
|
+
<% columns.each do |column| %>
|
|
34
|
+
<% value = cell_value(row, column) %>
|
|
35
|
+
<td class="px-4 py-3" data-sort-key="<%= column_sort_key(column) %>" data-sort-value="<%= value %>"><%= value %></td>
|
|
36
|
+
<% end %>
|
|
37
|
+
</tr>
|
|
38
|
+
<% end %>
|
|
39
|
+
<% if rows.empty? %>
|
|
40
|
+
<tr><td class="px-4 py-8 text-center text-[hsl(var(--senren-muted-foreground))]" colspan="<%= columns.size.nonzero? || 1 %>"><%= empty_text %></td></tr>
|
|
41
|
+
<% end %>
|
|
42
|
+
</tbody>
|
|
43
|
+
</table>
|
|
44
|
+
</div>
|
|
45
|
+
<% end %>
|
|
46
|
+
|
|
47
|
+
<% if footer? %>
|
|
48
|
+
<div class="border-t border-[hsl(var(--senren-border))] p-3"><%= footer %></div>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% end %>
|