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,11 @@
|
|
|
1
|
+
<div <%= tag.attributes(**root_attrs("w-full")) %>>
|
|
2
|
+
<% if label.present? %>
|
|
3
|
+
<div class="mb-2 flex items-center justify-between text-sm">
|
|
4
|
+
<span class="font-medium text-[hsl(var(--senren-foreground))]"><%= label %></span>
|
|
5
|
+
<span class="text-[hsl(var(--senren-muted-foreground))]"><%= percent %>%</span>
|
|
6
|
+
</div>
|
|
7
|
+
<% end %>
|
|
8
|
+
<div role="progressbar" aria-valuemin="0" aria-valuemax="<%= max.to_i %>" aria-valuenow="<%= value.to_i %>" class="h-2 w-full overflow-hidden rounded-full bg-[hsl(var(--senren-muted))]">
|
|
9
|
+
<div class="h-full rounded-full transition-all <%= self.class::VARIANTS[variant] %>" style="width: <%= percent %>%"></div>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class ProgressComponent < BaseComponent
|
|
5
|
+
VARIANTS = {
|
|
6
|
+
default: 'bg-[hsl(var(--senren-primary))]',
|
|
7
|
+
success: 'bg-[hsl(var(--senren-success))]',
|
|
8
|
+
warning: 'bg-[hsl(var(--senren-warning))]',
|
|
9
|
+
destructive: 'bg-[hsl(var(--senren-destructive))]'
|
|
10
|
+
}.freeze
|
|
11
|
+
SIZES = { md: '' }.freeze
|
|
12
|
+
|
|
13
|
+
def initialize(value: 0, max: 100, label: nil, variant: :default, class_name: nil, **html)
|
|
14
|
+
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
15
|
+
@value = value.to_f
|
|
16
|
+
@max = max.to_f.positive? ? max.to_f : 100.0
|
|
17
|
+
@label = label
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :value, :max, :label
|
|
21
|
+
|
|
22
|
+
def percent
|
|
23
|
+
((value / max) * 100).clamp(0, 100).round
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<label class="inline-flex items-center gap-2 cursor-pointer">
|
|
2
|
+
<%= tag.input(**root_attrs("h-4 w-4 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))]", type: "radio", 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 RadioButtonComponent < BaseComponent
|
|
5
|
+
VARIANTS = { default: '' }.freeze
|
|
6
|
+
SIZES = { md: '' }.freeze
|
|
7
|
+
|
|
8
|
+
def initialize(name:, value:, 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}-#{value.to_s.parameterize}"
|
|
14
|
+
@label = label
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :name, :value, :checked, :id, :label
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<%= tag.div(**root_attrs("overflow-hidden rounded-(--senren-radius) border shadow-sm", data: { controller: "senren--rich-text-editor-lite", "senren--rich-text-editor-lite-debug-value": debug? })) do %>
|
|
2
|
+
<label for="<%= dom_id %>-editor" class="sr-only"><%= label %></label>
|
|
3
|
+
<textarea hidden name="<%= name %>" data-senren--rich-text-editor-lite-target="input"><%= initial_content %></textarea>
|
|
4
|
+
|
|
5
|
+
<% if toolbar? %>
|
|
6
|
+
<div class="flex flex-wrap items-center gap-1 border-b border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-muted)/0.35)] p-2">
|
|
7
|
+
<% [
|
|
8
|
+
["formatBlock:p", "P", "Paragraph"],
|
|
9
|
+
["formatBlock:h1", "H1", "Heading 1"],
|
|
10
|
+
["formatBlock:h2", "H2", "Heading 2"],
|
|
11
|
+
["formatBlock:h3", "H3", "Heading 3"],
|
|
12
|
+
["bold", "B", "Bold"],
|
|
13
|
+
["italic", "I", "Italic"],
|
|
14
|
+
["createLink", "Link", "Create link"],
|
|
15
|
+
["insertUnorderedList", "List", "Bulleted list"],
|
|
16
|
+
["insertOrderedList", "1.", "Numbered list"],
|
|
17
|
+
["align:left", "Left", "Align left"],
|
|
18
|
+
["align:center", "Center", "Align center"],
|
|
19
|
+
["align:right", "Right", "Align right"],
|
|
20
|
+
["align:justify", "Justify", "Justify"]
|
|
21
|
+
].each do |command, label_text, aria_label| %>
|
|
22
|
+
<button type="button" disabled class="inline-flex h-8 cursor-pointer items-center rounded-(--senren-radius) px-2 text-sm font-medium text-[hsl(var(--senren-foreground))] hover:bg-[hsl(var(--senren-accent))] disabled:cursor-not-allowed disabled:opacity-50 aria-pressed:bg-[hsl(var(--senren-accent))]" data-senren--rich-text-editor-lite-target="button" data-command="<%= command %>" data-action="pointerdown->senren--rich-text-editor-lite#keepSelection mousedown->senren--rich-text-editor-lite#keepSelection touchstart->senren--rich-text-editor-lite#keepSelection click->senren--rich-text-editor-lite#format" aria-label="<%= aria_label %>" aria-pressed="false">
|
|
23
|
+
<%= label_text %>
|
|
24
|
+
</button>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
28
|
+
|
|
29
|
+
<div id="<%= dom_id %>-editor" role="textbox" aria-multiline="true" aria-label="<%= label %>" contenteditable="true" data-placeholder="<%= placeholder %>" data-senren--rich-text-editor-lite-target="editor" data-action="click->senren--rich-text-editor-lite#openLink input->senren--rich-text-editor-lite#sync input->senren--rich-text-editor-lite#rememberSelection keyup->senren--rich-text-editor-lite#rememberSelection keyup->senren--rich-text-editor-lite#updateToolbar mouseup->senren--rich-text-editor-lite#rememberSelection mouseup->senren--rich-text-editor-lite#updateToolbar touchend->senren--rich-text-editor-lite#rememberSelection paste->senren--rich-text-editor-lite#syncSoon" class="min-h-40 cursor-text bg-[hsl(var(--senren-background))] p-4 text-left text-sm leading-6 text-[hsl(var(--senren-foreground))] outline-none focus:ring-2 focus:ring-inset focus:ring-[hsl(var(--senren-ring))]">
|
|
30
|
+
<%= sanitize(initial_content, tags: %w[p br strong b em i a ul ol li h1 h2 h3], attributes: %w[href rel target data-align]) %>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Senren
|
|
2
|
+
class RichTextEditorLiteComponent < BaseComponent
|
|
3
|
+
VARIANTS = {
|
|
4
|
+
default: 'border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))]'
|
|
5
|
+
}.freeze
|
|
6
|
+
SIZES = { md: '' }.freeze
|
|
7
|
+
|
|
8
|
+
def initialize(name: 'content', value: nil, label: 'Content', placeholder: 'Write something...', id: nil,
|
|
9
|
+
toolbar: true, debug: ::Rails.env.development?, class_name: nil, **html)
|
|
10
|
+
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
11
|
+
@name = name
|
|
12
|
+
@value = value
|
|
13
|
+
@label = label
|
|
14
|
+
@placeholder = placeholder
|
|
15
|
+
@dom_id = id || "senren-editor-#{SecureRandom.hex(3)}"
|
|
16
|
+
@toolbar = toolbar
|
|
17
|
+
@debug = debug
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :name, :value, :label, :placeholder, :dom_id
|
|
21
|
+
|
|
22
|
+
def toolbar? = !!@toolbar
|
|
23
|
+
|
|
24
|
+
def debug? = !!@debug
|
|
25
|
+
|
|
26
|
+
def initial_content
|
|
27
|
+
value.presence || content.to_s.presence || "<p>#{ERB::Util.html_escape(placeholder)}</p>"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<%= tag.div(**root_attrs("relative")) do %>
|
|
2
|
+
<% if content? %>
|
|
3
|
+
<%= content %>
|
|
4
|
+
<% else %>
|
|
5
|
+
<label class="sr-only" for="<%= name %>"><%= label %></label>
|
|
6
|
+
<span aria-hidden="true" class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[hsl(var(--senren-muted-foreground))]">
|
|
7
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
|
|
8
|
+
<circle cx="11" cy="11" r="7"></circle>
|
|
9
|
+
<path d="m20 20-3.5-3.5"></path>
|
|
10
|
+
</svg>
|
|
11
|
+
</span>
|
|
12
|
+
<input id="<%= name %>" name="<%= name %>" type="search" value="<%= value %>" placeholder="<%= placeholder %>" aria-label="<%= label %>" class="h-10 w-full rounded-(--senren-radius) border border-[hsl(var(--senren-input))] bg-[hsl(var(--senren-background))] px-9 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))]">
|
|
13
|
+
<% end %>
|
|
14
|
+
<% end %>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class SearchInputComponent < BaseComponent
|
|
5
|
+
VARIANTS = { default: '' }.freeze
|
|
6
|
+
SIZES = { md: '' }.freeze
|
|
7
|
+
|
|
8
|
+
def initialize(name: 'q', value: nil, placeholder: 'Search...', label: 'Search', class_name: nil, **html)
|
|
9
|
+
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
10
|
+
@name = name
|
|
11
|
+
@value = value
|
|
12
|
+
@placeholder = placeholder
|
|
13
|
+
@label = label
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :name, :value, :placeholder, :label
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= render(Senren::NativeSelectComponent.new(**native_select_args)) { content } %>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
# Stimulus-driven styled select. v0.1 ships as a thin wrapper around the
|
|
5
|
+
# native_select with a Stimulus controller hook for future styling work.
|
|
6
|
+
class SelectComponent < BaseComponent
|
|
7
|
+
VARIANTS = NativeSelectComponent::VARIANTS
|
|
8
|
+
SIZES = NativeSelectComponent::SIZES
|
|
9
|
+
|
|
10
|
+
def initialize(**args)
|
|
11
|
+
@args = args
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def native_select_args
|
|
15
|
+
data = (@args[:data] || {}).merge(controller: 'senren--select')
|
|
16
|
+
@args.merge(data: data)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= tag.div "", **root_attrs("", role: "separator", "aria-orientation": aria_orientation) %>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Senren
|
|
2
|
+
class SeparatorComponent < BaseComponent
|
|
3
|
+
VARIANTS = {
|
|
4
|
+
horizontal: 'h-px w-full bg-[hsl(var(--senren-border))]',
|
|
5
|
+
vertical: 'w-px h-full bg-[hsl(var(--senren-border))]'
|
|
6
|
+
}.freeze
|
|
7
|
+
|
|
8
|
+
SIZES = { md: '' }.freeze
|
|
9
|
+
|
|
10
|
+
def aria_orientation = variant == :vertical ? 'vertical' : 'horizontal'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<%= tag.section(**root_attrs("rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] p-5 shadow-sm")) do %>
|
|
2
|
+
<% if title.present? || description.present? || actions? %>
|
|
3
|
+
<div class="flex items-start justify-between gap-4 border-b border-[hsl(var(--senren-border))] pb-4">
|
|
4
|
+
<div class="min-w-0">
|
|
5
|
+
<% if title.present? %>
|
|
6
|
+
<h2 class="font-display text-base font-semibold text-[hsl(var(--senren-foreground))]"><%= title %></h2>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% if description.present? %>
|
|
9
|
+
<p class="mt-1 text-sm leading-6 text-[hsl(var(--senren-muted-foreground))]"><%= description %></p>
|
|
10
|
+
<% end %>
|
|
11
|
+
</div>
|
|
12
|
+
<% if actions? %>
|
|
13
|
+
<div class="shrink-0"><%= actions %></div>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="pt-4"><%= content %></div>
|
|
17
|
+
<% else %>
|
|
18
|
+
<%= content %>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% end %>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class SettingsSectionComponent < BaseComponent
|
|
5
|
+
renders_one :actions
|
|
6
|
+
|
|
7
|
+
VARIANTS = { default: '' }.freeze
|
|
8
|
+
SIZES = { md: '' }.freeze
|
|
9
|
+
|
|
10
|
+
def initialize(title: nil, description: nil, class_name: nil, **html)
|
|
11
|
+
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
12
|
+
@title = title
|
|
13
|
+
@description = description
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :title, :description
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<div data-controller="senren--sheet" data-senren-component="sheet" id="<%= dom_id %>">
|
|
2
|
+
<% if trigger? %>
|
|
3
|
+
<span data-action="click->senren--sheet#open" data-senren--sheet-target="trigger">
|
|
4
|
+
<%= trigger %>
|
|
5
|
+
</span>
|
|
6
|
+
<% else %>
|
|
7
|
+
<button type="button" data-action="click->senren--sheet#open" data-senren--sheet-target="trigger" class="hidden">Open</button>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<div data-senren--sheet-target="overlay" hidden
|
|
11
|
+
class="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm"></div>
|
|
12
|
+
|
|
13
|
+
<div data-senren--sheet-target="panel" hidden role="dialog" aria-modal="true"
|
|
14
|
+
data-open="false"
|
|
15
|
+
class="fixed z-50 bg-[hsl(var(--senren-background))] text-[hsl(var(--senren-foreground))] shadow-lg transition-transform duration-200 <%= self.class::VARIANTS[variant] %>">
|
|
16
|
+
<div class="flex h-full flex-col p-6">
|
|
17
|
+
<% if title? %>
|
|
18
|
+
<h2 class="text-lg font-semibold"><%= title %></h2>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% if description? %>
|
|
21
|
+
<p class="text-sm text-[hsl(var(--senren-muted-foreground))]"><%= description %></p>
|
|
22
|
+
<% end %>
|
|
23
|
+
<div class="mt-4 flex-1 overflow-auto">
|
|
24
|
+
<%= body || content %>
|
|
25
|
+
</div>
|
|
26
|
+
<% if footer? %>
|
|
27
|
+
<div class="mt-4 border-t border-[hsl(var(--senren-border))] pt-4">
|
|
28
|
+
<%= footer %>
|
|
29
|
+
</div>
|
|
30
|
+
<% end %>
|
|
31
|
+
</div>
|
|
32
|
+
<button type="button" data-action="click->senren--sheet#close" aria-label="Close"
|
|
33
|
+
class="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100">
|
|
34
|
+
<span aria-hidden="true">×</span>
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class SheetComponent < BaseComponent
|
|
5
|
+
renders_one :trigger
|
|
6
|
+
renders_one :title
|
|
7
|
+
renders_one :description
|
|
8
|
+
renders_one :body
|
|
9
|
+
renders_one :footer
|
|
10
|
+
|
|
11
|
+
VARIANTS = {
|
|
12
|
+
right: 'right-0 top-0 h-full w-full max-w-md translate-x-full data-[open=true]:translate-x-0',
|
|
13
|
+
left: 'left-0 top-0 h-full w-full max-w-md -translate-x-full data-[open=true]:translate-x-0',
|
|
14
|
+
top: 'left-0 top-0 w-full h-1/2 -translate-y-full data-[open=true]:translate-y-0',
|
|
15
|
+
bottom: 'left-0 bottom-0 w-full h-1/2 translate-y-full data-[open=true]:translate-y-0'
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
SIZES = { md: '' }.freeze
|
|
19
|
+
|
|
20
|
+
def initialize(side: :right, id: nil, class_name: nil, **html)
|
|
21
|
+
super(variant: side, size: :md, class_name: class_name, **html)
|
|
22
|
+
@dom_id = id || "senren-sheet-#{SecureRandom.hex(3)}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
attr_reader :dom_id
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<span <%= tag.attributes(**root_attrs("inline-flex items-center gap-1 text-xs text-[hsl(var(--senren-muted-foreground))]")) %>>
|
|
2
|
+
<% (keys.presence || [content]).compact.each_with_index do |key, index| %>
|
|
3
|
+
<% if index.positive? %><span aria-hidden="true">+</span><% end %>
|
|
4
|
+
<kbd class="rounded border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-muted))] px-1.5 py-0.5 font-mono text-[11px] text-[hsl(var(--senren-foreground))] shadow-sm"><%= key %></kbd>
|
|
5
|
+
<% end %>
|
|
6
|
+
</span>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class ShortcutKeyComponent < BaseComponent
|
|
5
|
+
VARIANTS = { default: '' }.freeze
|
|
6
|
+
SIZES = { md: '' }.freeze
|
|
7
|
+
|
|
8
|
+
def initialize(keys: [], class_name: nil, **html)
|
|
9
|
+
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
10
|
+
@keys = Array(keys)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :keys
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<aside <%= tag.attributes(**root_attrs("flex min-h-80 flex-col rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] p-3 text-[hsl(var(--senren-card-foreground))] #{self.class::VARIANTS[variant]}", data: { controller: "senren--sidebar" })) %>>
|
|
2
|
+
<div class="mb-4 flex items-center justify-between gap-2 px-2">
|
|
3
|
+
<div class="truncate font-display text-sm font-semibold tracking-tight"><%= brand %></div>
|
|
4
|
+
<button type="button" class="cursor-pointer rounded-md px-2 py-1 text-xs text-[hsl(var(--senren-muted-foreground))] hover:bg-[hsl(var(--senren-accent))]" data-action="click->senren--sidebar#toggle" aria-label="Toggle sidebar">Toggle</button>
|
|
5
|
+
</div>
|
|
6
|
+
<nav aria-label="<%= label %>" class="space-y-1">
|
|
7
|
+
<% items.each do |item| %>
|
|
8
|
+
<%= link_to item[:label], item[:href], class: "block truncate rounded-md px-3 py-2 text-sm transition-colors #{item[:active] ? "bg-[hsl(var(--senren-primary))] text-[hsl(var(--senren-primary-foreground))]" : "text-[hsl(var(--senren-muted-foreground))] hover:bg-[hsl(var(--senren-accent))] hover:text-[hsl(var(--senren-accent-foreground))]"}" %>
|
|
9
|
+
<% end %>
|
|
10
|
+
</nav>
|
|
11
|
+
<% if content? %>
|
|
12
|
+
<div class="mt-auto pt-4 text-xs text-[hsl(var(--senren-muted-foreground))]"><%= content %></div>
|
|
13
|
+
<% end %>
|
|
14
|
+
</aside>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class SidebarComponent < BaseComponent
|
|
5
|
+
VARIANTS = {
|
|
6
|
+
default: 'w-64',
|
|
7
|
+
compact: 'w-20'
|
|
8
|
+
}.freeze
|
|
9
|
+
SIZES = { md: '' }.freeze
|
|
10
|
+
|
|
11
|
+
def initialize(items: [], brand: 'Senren', variant: :default, label: 'Primary', class_name: nil, **html)
|
|
12
|
+
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
13
|
+
@items = normalize_items(items)
|
|
14
|
+
@brand = brand
|
|
15
|
+
@label = label
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :items, :brand, :label
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def normalize_items(items)
|
|
23
|
+
Array(items).map do |item|
|
|
24
|
+
if item.is_a?(Hash)
|
|
25
|
+
{
|
|
26
|
+
label: item[:label] || item['label'],
|
|
27
|
+
href: item[:href] || item['href'] || '#',
|
|
28
|
+
active: item[:active] || item['active']
|
|
29
|
+
}
|
|
30
|
+
else
|
|
31
|
+
label, href = item
|
|
32
|
+
{ label: label, href: href || '#', active: false }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= tag.div "", **root_attrs("animate-pulse bg-[hsl(var(--senren-muted))]", "aria-hidden": "true") %>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<%= tag.div(**root_attrs("rounded-(--senren-radius) border bg-[hsl(var(--senren-card))] p-5 shadow-sm")) do %>
|
|
2
|
+
<% if content? && label.blank? && value.blank? %>
|
|
3
|
+
<%= content %>
|
|
4
|
+
<% else %>
|
|
5
|
+
<% if label.present? %>
|
|
6
|
+
<p class="text-xs font-semibold uppercase tracking-wide text-[hsl(var(--senren-muted-foreground))]"><%= label %></p>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% if value.present? %>
|
|
9
|
+
<p class="mt-2 font-display text-3xl font-semibold tracking-tight text-[hsl(var(--senren-foreground))]"><%= value %></p>
|
|
10
|
+
<% end %>
|
|
11
|
+
<div class="mt-3 flex items-center justify-between gap-3 text-sm">
|
|
12
|
+
<% if change.present? %>
|
|
13
|
+
<span class="font-medium <%= accent_class %>"><%= change %></span>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% if helper_text.present? %>
|
|
16
|
+
<span class="text-[hsl(var(--senren-muted-foreground))]"><%= helper_text %></span>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% end %>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class StatCardComponent < BaseComponent
|
|
5
|
+
VARIANTS = {
|
|
6
|
+
default: 'border-[hsl(var(--senren-border))]',
|
|
7
|
+
success: 'border-[hsl(var(--senren-success)/0.4)]',
|
|
8
|
+
warning: 'border-[hsl(var(--senren-warning)/0.5)]',
|
|
9
|
+
destructive: 'border-[hsl(var(--senren-destructive)/0.42)]'
|
|
10
|
+
}.freeze
|
|
11
|
+
SIZES = { md: '' }.freeze
|
|
12
|
+
|
|
13
|
+
def initialize(label: nil, value: nil, change: nil, helper_text: nil, variant: :default, class_name: nil, **html)
|
|
14
|
+
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
15
|
+
@label = label
|
|
16
|
+
@value = value
|
|
17
|
+
@change = change
|
|
18
|
+
@helper_text = helper_text
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :label, :value, :change, :helper_text
|
|
22
|
+
|
|
23
|
+
def accent_class
|
|
24
|
+
{
|
|
25
|
+
success: 'text-[hsl(var(--senren-success))]',
|
|
26
|
+
warning: 'text-[hsl(var(--senren-warning))]',
|
|
27
|
+
destructive: 'text-[hsl(var(--senren-destructive))]'
|
|
28
|
+
}.fetch(variant, 'text-[hsl(var(--senren-muted-foreground))]')
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<label class="inline-flex items-center gap-3 cursor-pointer">
|
|
2
|
+
<span class="relative inline-flex h-6 w-11 items-center rounded-full bg-[hsl(var(--senren-muted))] has-[:checked]:bg-[hsl(var(--senren-primary))] transition-colors">
|
|
3
|
+
<input type="checkbox" role="switch" id="<%= id %>" name="<%= name %>" value="<%= value %>" <%= "checked" if checked %> class="peer sr-only">
|
|
4
|
+
<span aria-hidden="true" class="absolute left-0.5 top-0.5 inline-block h-5 w-5 transform rounded-full bg-[hsl(var(--senren-background))] transition-transform peer-checked:translate-x-5"></span>
|
|
5
|
+
</span>
|
|
6
|
+
<% if label %>
|
|
7
|
+
<span class="text-sm font-medium text-[hsl(var(--senren-foreground))]"><%= label %></span>
|
|
8
|
+
<% else %>
|
|
9
|
+
<%= content %>
|
|
10
|
+
<% end %>
|
|
11
|
+
</label>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class SwitchComponent < BaseComponent
|
|
5
|
+
VARIANTS = { default: '' }.freeze
|
|
6
|
+
SIZES = { md: '' }.freeze
|
|
7
|
+
|
|
8
|
+
def initialize(name:, checked: false, value: '1', id: nil, label: nil, class_name: nil, **html)
|
|
9
|
+
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
10
|
+
@name = name
|
|
11
|
+
@checked = checked
|
|
12
|
+
@value = value
|
|
13
|
+
@id = id || name.to_s.parameterize
|
|
14
|
+
@label = label
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :name, :checked, :value, :id, :label
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<div <%= tag.attributes(**root_attrs("w-full overflow-hidden rounded-(--senren-radius) border border-[hsl(var(--senren-border))]")) %>>
|
|
2
|
+
<table class="w-full border-collapse text-left text-sm <%= self.class::VARIANTS[variant] %>">
|
|
3
|
+
<% if caption.present? %>
|
|
4
|
+
<caption class="sr-only"><%= caption %></caption>
|
|
5
|
+
<% end %>
|
|
6
|
+
<thead class="bg-[hsl(var(--senren-muted)/0.6)] text-[hsl(var(--senren-muted-foreground))]">
|
|
7
|
+
<tr>
|
|
8
|
+
<% columns.each do |column| %>
|
|
9
|
+
<th scope="col" class="px-4 py-3 font-medium"><%= column_label(column) %></th>
|
|
10
|
+
<% end %>
|
|
11
|
+
</tr>
|
|
12
|
+
</thead>
|
|
13
|
+
<tbody class="divide-y divide-[hsl(var(--senren-border))] bg-[hsl(var(--senren-background))] text-[hsl(var(--senren-foreground))]">
|
|
14
|
+
<% rows.each do |row| %>
|
|
15
|
+
<tr class="transition-colors hover:bg-[hsl(var(--senren-muted)/0.4)]">
|
|
16
|
+
<% columns.each do |column| %>
|
|
17
|
+
<td class="px-4 py-3"><%= cell_value(row, column) %></td>
|
|
18
|
+
<% end %>
|
|
19
|
+
</tr>
|
|
20
|
+
<% end %>
|
|
21
|
+
<% if rows.empty? && content? %>
|
|
22
|
+
<tr><td class="px-4 py-6" colspan="<%= columns.size.nonzero? || 1 %>"><%= content %></td></tr>
|
|
23
|
+
<% end %>
|
|
24
|
+
</tbody>
|
|
25
|
+
</table>
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class TableComponent < BaseComponent
|
|
5
|
+
VARIANTS = {
|
|
6
|
+
default: '',
|
|
7
|
+
compact: 'text-xs'
|
|
8
|
+
}.freeze
|
|
9
|
+
SIZES = { md: '' }.freeze
|
|
10
|
+
|
|
11
|
+
def initialize(columns: [], rows: [], caption: nil, variant: :default, class_name: nil, **html)
|
|
12
|
+
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
13
|
+
@columns = Array(columns)
|
|
14
|
+
@rows = Array(rows)
|
|
15
|
+
@caption = caption
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :columns, :rows, :caption
|
|
19
|
+
|
|
20
|
+
def cell_value(row, column)
|
|
21
|
+
key = column_key(column)
|
|
22
|
+
row.is_a?(Hash) ? (row[key] || row[key.to_s]) : row[key.to_i]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def column_label(column)
|
|
26
|
+
column.is_a?(Hash) ? (column[:label] || column['label']) : column.to_s.titleize
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def column_key(column)
|
|
32
|
+
column.is_a?(Hash) ? (column[:key] || column['key']) : column
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<div <%= tag.attributes(**root_attrs("w-full", data: { controller: "senren--tabs" })) %>>
|
|
2
|
+
<div role="tablist" aria-label="<%= label %>" class="<%= self.class::VARIANTS[variant] %> flex flex-wrap items-center gap-1">
|
|
3
|
+
<% items.each do |item| %>
|
|
4
|
+
<% selected = active_item?(item) %>
|
|
5
|
+
<button type="button" role="tab" id="<%= item[:id] %>-tab" aria-selected="<%= selected %>" aria-controls="<%= item[:id] %>-panel" tabindex="<%= selected ? 0 : -1 %>" data-senren--tabs-target="tab" data-panel-id="<%= item[:id] %>" data-action="click->senren--tabs#select keydown->senren--tabs#onKey" class="cursor-pointer rounded-[calc(var(--senren-radius)-2px)] px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))] <%= selected ? "bg-[hsl(var(--senren-background))] text-[hsl(var(--senren-foreground))] shadow-sm" : "text-[hsl(var(--senren-muted-foreground))] hover:text-[hsl(var(--senren-foreground))]" %>">
|
|
6
|
+
<%= item[:label] %>
|
|
7
|
+
</button>
|
|
8
|
+
<% end %>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="mt-4">
|
|
11
|
+
<% items.each do |item| %>
|
|
12
|
+
<% selected = active_item?(item) %>
|
|
13
|
+
<section id="<%= item[:id] %>-panel" role="tabpanel" aria-labelledby="<%= item[:id] %>-tab" data-senren--tabs-target="panel" data-panel-id="<%= item[:id] %>" <%= "hidden" unless selected %> class="rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] p-4 text-sm text-[hsl(var(--senren-card-foreground))]">
|
|
14
|
+
<%= item[:content].presence || content %>
|
|
15
|
+
</section>
|
|
16
|
+
<% end %>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class TabsComponent < BaseComponent
|
|
5
|
+
VARIANTS = {
|
|
6
|
+
default: 'rounded-(--senren-radius) bg-[hsl(var(--senren-muted))] p-1',
|
|
7
|
+
underline: 'border-b border-[hsl(var(--senren-border))]'
|
|
8
|
+
}.freeze
|
|
9
|
+
SIZES = { md: '' }.freeze
|
|
10
|
+
|
|
11
|
+
def initialize(items: [], variant: :default, active: nil, label: 'Tabs', class_name: nil, **html)
|
|
12
|
+
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
13
|
+
@items = normalize_items(items)
|
|
14
|
+
@active = active&.to_s || @items.first&.fetch(:id, nil)
|
|
15
|
+
@label = label
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :items, :active, :label
|
|
19
|
+
|
|
20
|
+
def active_item?(item)
|
|
21
|
+
item[:id].to_s == active.to_s
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def normalize_items(items)
|
|
27
|
+
Array(items).map.with_index do |item, index|
|
|
28
|
+
source = item.is_a?(Hash) ? item : { label: item.to_s }
|
|
29
|
+
label = source[:label] || source['label'] || "Tab #{index + 1}"
|
|
30
|
+
id = (source[:id] || source['id'] || label.to_s.parameterize).to_s
|
|
31
|
+
{ id: id, label: label, content: source[:content] || source['content'] }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|