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,22 @@
|
|
|
1
|
+
<%= tag.div(**root_attrs("rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] shadow-sm")) do %>
|
|
2
|
+
<% if content? && members.empty? %>
|
|
3
|
+
<div class="p-4"><%= content %></div>
|
|
4
|
+
<% else %>
|
|
5
|
+
<ul class="divide-y divide-[hsl(var(--senren-border))]">
|
|
6
|
+
<% members.each do |member| %>
|
|
7
|
+
<li class="flex items-center justify-between gap-4 p-4">
|
|
8
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
9
|
+
<span class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[hsl(var(--senren-muted))] text-sm font-semibold text-[hsl(var(--senren-muted-foreground))]"><%= initials_for(member) %></span>
|
|
10
|
+
<div class="min-w-0">
|
|
11
|
+
<p class="truncate text-sm font-medium text-[hsl(var(--senren-foreground))]"><%= member_value(member, :name) %></p>
|
|
12
|
+
<p class="truncate text-xs text-[hsl(var(--senren-muted-foreground))]"><%= member_value(member, :role) || member_value(member, :email) %></p>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<% if member_value(member, :status).present? %>
|
|
16
|
+
<span class="rounded-full bg-[hsl(var(--senren-muted))] px-2 py-1 text-xs font-medium text-[hsl(var(--senren-muted-foreground))]"><%= member_value(member, :status) %></span>
|
|
17
|
+
<% end %>
|
|
18
|
+
</li>
|
|
19
|
+
<% end %>
|
|
20
|
+
</ul>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% end %>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class TeamMemberListComponent < BaseComponent
|
|
5
|
+
VARIANTS = { default: '' }.freeze
|
|
6
|
+
SIZES = { md: '' }.freeze
|
|
7
|
+
|
|
8
|
+
def initialize(members: [], class_name: nil, **html)
|
|
9
|
+
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
10
|
+
@members = Array(members)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :members
|
|
14
|
+
|
|
15
|
+
def member_value(member, key)
|
|
16
|
+
member.is_a?(Hash) ? (member[key] || member[key.to_s]) : nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initials_for(member)
|
|
20
|
+
explicit = member_value(member, :initials)
|
|
21
|
+
return explicit if explicit.present?
|
|
22
|
+
|
|
23
|
+
member_value(member, :name).to_s.split.map { |part| part[0] }.join.first(2).upcase
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= tag.textarea(value, **root_attrs("flex w-full rounded-(--senren-radius) border bg-[hsl(var(--senren-background))] text-[hsl(var(--senren-foreground))] placeholder:text-[hsl(var(--senren-muted-foreground))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50", id: id, name: name, placeholder: placeholder, rows: rows, "aria-invalid": variant == :error)) %>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class TextareaComponent < BaseComponent
|
|
5
|
+
VARIANTS = {
|
|
6
|
+
default: 'border-[hsl(var(--senren-input))] focus-visible:ring-[hsl(var(--senren-ring))]',
|
|
7
|
+
error: 'border-[hsl(var(--senren-destructive))] focus-visible:ring-[hsl(var(--senren-destructive))]'
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
SIZES = { md: 'min-h-[80px] text-sm px-3 py-2' }.freeze
|
|
11
|
+
|
|
12
|
+
def initialize(name:, value: nil, placeholder: nil, id: nil, rows: 4, variant: :default, class_name: nil, **html)
|
|
13
|
+
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
14
|
+
@name = name
|
|
15
|
+
@value = value
|
|
16
|
+
@placeholder = placeholder
|
|
17
|
+
@id = id || name.to_s.parameterize
|
|
18
|
+
@rows = rows
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :name, :value, :placeholder, :id, :rows
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<button type="button" <%= tag.attributes(**root_attrs("inline-flex h-10 cursor-pointer items-center justify-center gap-2 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-background))] px-3 text-sm font-medium text-[hsl(var(--senren-foreground))] transition-colors hover:bg-[hsl(var(--senren-accent))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))]", data: { controller: "senren--theme-toggle", action: "click->senren--theme-toggle#toggle" }, aria: { label: label, pressed: "false" })) %>>
|
|
2
|
+
<span aria-hidden="true" data-senren--theme-toggle-target="icon">O</span>
|
|
3
|
+
<span data-senren--theme-toggle-target="label"><%= label %></span>
|
|
4
|
+
</button>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class ThemeToggleComponent < BaseComponent
|
|
5
|
+
VARIANTS = { default: '' }.freeze
|
|
6
|
+
SIZES = { md: '' }.freeze
|
|
7
|
+
|
|
8
|
+
def initialize(label: 'Toggle theme', class_name: nil, **html)
|
|
9
|
+
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
10
|
+
@label = label
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :label
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<span data-controller="senren--tooltip" class="relative inline-block" data-senren-component="tooltip">
|
|
2
|
+
<span tabindex="0"
|
|
3
|
+
aria-describedby="<%= id %>"
|
|
4
|
+
data-action="mouseenter->senren--tooltip#show mouseleave->senren--tooltip#hide focus->senren--tooltip#show blur->senren--tooltip#hide">
|
|
5
|
+
<%= content %>
|
|
6
|
+
</span>
|
|
7
|
+
<span id="<%= id %>" role="tooltip" hidden data-senren--tooltip-target="bubble"
|
|
8
|
+
class="absolute z-50 mt-1 whitespace-nowrap rounded-md bg-[hsl(var(--senren-foreground))] text-[hsl(var(--senren-background))] px-2 py-1 text-xs shadow"><%= text %></span>
|
|
9
|
+
</span>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class TooltipComponent < BaseComponent
|
|
5
|
+
VARIANTS = { default: '' }.freeze
|
|
6
|
+
SIZES = { md: '' }.freeze
|
|
7
|
+
|
|
8
|
+
def initialize(text:, class_name: nil, **html)
|
|
9
|
+
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
10
|
+
@text = text
|
|
11
|
+
@id = "senren-tooltip-#{SecureRandom.hex(3)}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :text, :id
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<header <%= tag.attributes(**root_attrs("sticky top-0 z-30 border-b border-[hsl(var(--senren-border))] backdrop-blur-md")) %>>
|
|
2
|
+
<div class="mx-auto flex h-14 w-full max-w-[1400px] items-center gap-6 px-4 lg:px-8">
|
|
3
|
+
<div class="min-w-0 flex-none font-display text-sm font-semibold text-[hsl(var(--senren-foreground))]">
|
|
4
|
+
<% if brand? %><%= brand %><% else %>Senren<% end %>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<% if items.any? %>
|
|
8
|
+
<nav aria-label="<%= label %>" class="hidden min-w-0 items-center gap-1 md:flex">
|
|
9
|
+
<% items.each do |item| %>
|
|
10
|
+
<%= link_to item[:label], item[:href], class: "rounded-(--senren-radius) px-3 py-2 text-sm font-medium transition-colors #{active_item?(item) ? 'bg-[hsl(var(--senren-accent))] text-[hsl(var(--senren-accent-foreground))]' : 'text-[hsl(var(--senren-muted-foreground))] hover:bg-[hsl(var(--senren-accent))] hover:text-[hsl(var(--senren-accent-foreground))]'}" %>
|
|
11
|
+
<% end %>
|
|
12
|
+
</nav>
|
|
13
|
+
<% elsif content? %>
|
|
14
|
+
<div class="min-w-0 flex-1"><%= content %></div>
|
|
15
|
+
<% end %>
|
|
16
|
+
|
|
17
|
+
<% if actions? %>
|
|
18
|
+
<div class="ml-auto flex items-center gap-2"><%= actions %></div>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
</header>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
class TopNavComponent < BaseComponent
|
|
5
|
+
renders_one :brand
|
|
6
|
+
renders_one :actions
|
|
7
|
+
|
|
8
|
+
VARIANTS = {
|
|
9
|
+
default: 'bg-[hsl(var(--senren-background))/0.88]',
|
|
10
|
+
solid: 'bg-[hsl(var(--senren-card))]'
|
|
11
|
+
}.freeze
|
|
12
|
+
SIZES = { md: '' }.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(items: [], current_path: nil, label: 'Global', variant: :default, class_name: nil, **html)
|
|
15
|
+
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
16
|
+
@items = normalize_items(items)
|
|
17
|
+
@current_path = current_path
|
|
18
|
+
@label = label
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :items, :current_path, :label
|
|
22
|
+
|
|
23
|
+
def active_item?(item)
|
|
24
|
+
item[:active] || (current_path && item[:href] == current_path)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def normalize_items(items)
|
|
30
|
+
Array(items).map do |item|
|
|
31
|
+
if item.is_a?(Hash)
|
|
32
|
+
{
|
|
33
|
+
label: item[:label] || item['label'],
|
|
34
|
+
href: item[:href] || item['href'] || '#',
|
|
35
|
+
active: item[:active] || item['active']
|
|
36
|
+
}
|
|
37
|
+
else
|
|
38
|
+
label, href = item
|
|
39
|
+
{ label: label, href: href || '#', active: false }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= content_tag(html_tag, content, **root_attrs("")) %>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Senren
|
|
2
|
+
class TypographyComponent < BaseComponent
|
|
3
|
+
VARIANTS = {
|
|
4
|
+
h1: 'scroll-m-20 text-4xl font-bold tracking-tight',
|
|
5
|
+
h2: 'scroll-m-20 text-3xl font-semibold tracking-tight',
|
|
6
|
+
h3: 'scroll-m-20 text-2xl font-semibold tracking-tight',
|
|
7
|
+
h4: 'scroll-m-20 text-xl font-semibold tracking-tight',
|
|
8
|
+
p: 'leading-7',
|
|
9
|
+
lead: 'text-xl text-[hsl(var(--senren-muted-foreground))]',
|
|
10
|
+
large: 'text-lg font-semibold',
|
|
11
|
+
small: 'text-sm font-medium leading-none',
|
|
12
|
+
muted: 'text-sm text-[hsl(var(--senren-muted-foreground))]'
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
SIZES = { md: '' }.freeze
|
|
16
|
+
|
|
17
|
+
TAG_FOR = {
|
|
18
|
+
h1: :h1, h2: :h2, h3: :h3, h4: :h4,
|
|
19
|
+
p: :p, lead: :p, large: :p, small: :small, muted: :p
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
def html_tag = TAG_FOR[variant]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// senren--accordion
|
|
4
|
+
// Local UI: expand/collapse one or multiple panels.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["trigger", "panel"]
|
|
7
|
+
static values = { multiple: Boolean }
|
|
8
|
+
|
|
9
|
+
toggle(event) {
|
|
10
|
+
const trigger = event.currentTarget
|
|
11
|
+
const panel = this._panelFor(trigger.dataset.panelId)
|
|
12
|
+
const nextOpen = trigger.getAttribute("aria-expanded") !== "true"
|
|
13
|
+
|
|
14
|
+
if (!this.multipleValue) this._closeAll()
|
|
15
|
+
trigger.setAttribute("aria-expanded", nextOpen ? "true" : "false")
|
|
16
|
+
if (panel) panel.hidden = !nextOpen
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_closeAll() {
|
|
20
|
+
this.triggerTargets.forEach((trigger) => trigger.setAttribute("aria-expanded", "false"))
|
|
21
|
+
this.panelTargets.forEach((panel) => { panel.hidden = true })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_panelFor(id) {
|
|
25
|
+
return this.panelTargets.find((panel) => panel.dataset.panelId === id)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// senren--alert-dialog
|
|
4
|
+
// Same shape as dialog, but role=alertdialog. Triggered by external buttons that include
|
|
5
|
+
// data-action="click->senren--alert-dialog#open" pointing at the controller scope.
|
|
6
|
+
export default class extends Controller {
|
|
7
|
+
static targets = ["overlay", "panel", "trigger"]
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
this._onKey = this._onKey.bind(this)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
disconnect() {
|
|
14
|
+
document.removeEventListener("keydown", this._onKey)
|
|
15
|
+
document.body.style.overflow = ""
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
open(event) {
|
|
19
|
+
event?.preventDefault()
|
|
20
|
+
this.overlayTarget.hidden = false
|
|
21
|
+
this.panelTarget.hidden = false
|
|
22
|
+
document.addEventListener("keydown", this._onKey)
|
|
23
|
+
document.body.style.overflow = "hidden"
|
|
24
|
+
queueMicrotask(() => this.panelTarget?.focus())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
close(event) {
|
|
28
|
+
event?.preventDefault()
|
|
29
|
+
this.overlayTarget.hidden = true
|
|
30
|
+
this.panelTarget.hidden = true
|
|
31
|
+
document.removeEventListener("keydown", this._onKey)
|
|
32
|
+
document.body.style.overflow = ""
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_onKey(event) {
|
|
36
|
+
if (event.key === "Escape") this.close()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// senren--api-key-field
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = ["input", "revealButton", "status"]
|
|
6
|
+
static values = {
|
|
7
|
+
revealLabel: String,
|
|
8
|
+
hideLabel: String,
|
|
9
|
+
copyLabel: String
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
connect() {
|
|
13
|
+
this.hidden = true
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
toggle() {
|
|
17
|
+
this.hidden = !this.hidden
|
|
18
|
+
this.inputTarget.type = this.hidden ? "password" : "text"
|
|
19
|
+
this.revealButtonTarget.textContent = this.hidden ? this.revealLabelValue : this.hideLabelValue
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async copy() {
|
|
23
|
+
const value = this.inputTarget.value
|
|
24
|
+
if (!value) return
|
|
25
|
+
|
|
26
|
+
if (navigator.clipboard) {
|
|
27
|
+
await navigator.clipboard.writeText(value)
|
|
28
|
+
} else {
|
|
29
|
+
this.inputTarget.select()
|
|
30
|
+
try { document.execCommand("copy") } catch (_) {}
|
|
31
|
+
this.inputTarget.setSelectionRange(0, 0)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (this.hasStatusTarget) this.statusTarget.textContent = `${this.copyLabelValue} complete`
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// senren--calendar
|
|
4
|
+
// Local UI: select a visible day and update the optional hidden field.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["day", "value"]
|
|
7
|
+
|
|
8
|
+
select(event) {
|
|
9
|
+
const day = event.currentTarget
|
|
10
|
+
this.dayTargets.forEach((target) => {
|
|
11
|
+
target.classList.remove("bg-[hsl(var(--senren-primary))]", "text-[hsl(var(--senren-primary-foreground))]")
|
|
12
|
+
})
|
|
13
|
+
day.classList.add("bg-[hsl(var(--senren-primary))]", "text-[hsl(var(--senren-primary-foreground))]")
|
|
14
|
+
if (this.hasValueTarget) this.valueTarget.value = day.dataset.date
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// senren--carousel
|
|
4
|
+
// Local UI: previous/next controls, dots, and arrow-key navigation.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["slide", "dot", "status"]
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
this.index = 0
|
|
10
|
+
this.show(0)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
previous() {
|
|
14
|
+
this.show(this.index - 1)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
next() {
|
|
18
|
+
this.show(this.index + 1)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
goTo(event) {
|
|
22
|
+
this.show(Number(event.currentTarget.dataset.index || 0))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
onKey(event) {
|
|
26
|
+
if (event.key === "ArrowLeft") {
|
|
27
|
+
event.preventDefault()
|
|
28
|
+
this.previous()
|
|
29
|
+
}
|
|
30
|
+
if (event.key === "ArrowRight") {
|
|
31
|
+
event.preventDefault()
|
|
32
|
+
this.next()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
show(index) {
|
|
37
|
+
const count = this.slideTargets.length
|
|
38
|
+
if (count === 0) return
|
|
39
|
+
|
|
40
|
+
this.index = (index + count) % count
|
|
41
|
+
this.slideTargets.forEach((slide, slideIndex) => {
|
|
42
|
+
slide.hidden = slideIndex !== this.index
|
|
43
|
+
slide.classList.toggle("hidden", slideIndex !== this.index)
|
|
44
|
+
})
|
|
45
|
+
this.dotTargets.forEach((dot, dotIndex) => {
|
|
46
|
+
dot.setAttribute("aria-current", dotIndex === this.index ? "true" : "false")
|
|
47
|
+
})
|
|
48
|
+
if (this.hasStatusTarget) this.statusTarget.textContent = `Slide ${this.index + 1} of ${count}`
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// senren--clipboard
|
|
4
|
+
// Local UI: copy text to the clipboard and announce success.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["source", "button", "status"]
|
|
7
|
+
static values = { copiedLabel: String }
|
|
8
|
+
|
|
9
|
+
async copy() {
|
|
10
|
+
const value = this.sourceTarget.value || this.sourceTarget.textContent
|
|
11
|
+
await navigator.clipboard.writeText(value)
|
|
12
|
+
const original = this.buttonTarget.textContent
|
|
13
|
+
this.buttonTarget.textContent = this.copiedLabelValue || "Copied"
|
|
14
|
+
if (this.hasStatusTarget) this.statusTarget.textContent = "Copied to clipboard"
|
|
15
|
+
window.setTimeout(() => { this.buttonTarget.textContent = original }, 1200)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// senren--collapsible
|
|
4
|
+
// Local UI: toggle one content panel.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["trigger", "panel"]
|
|
7
|
+
|
|
8
|
+
toggle() {
|
|
9
|
+
const nextOpen = this.triggerTarget.getAttribute("aria-expanded") !== "true"
|
|
10
|
+
this.triggerTarget.setAttribute("aria-expanded", nextOpen ? "true" : "false")
|
|
11
|
+
this.panelTarget.hidden = !nextOpen
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// senren--combobox
|
|
4
|
+
// Local UI: open, filter options, and write the selected value to a hidden input.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["button", "panel", "search", "option", "value", "label", "chevron"]
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
this._onDocClick = this._onDocClick.bind(this)
|
|
10
|
+
this.setOpenState(false)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
disconnect() {
|
|
14
|
+
document.removeEventListener("click", this._onDocClick)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toggle() {
|
|
18
|
+
this.panelTarget.hidden ? this.open() : this.close()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
open() {
|
|
22
|
+
this.setOpenState(true)
|
|
23
|
+
document.addEventListener("click", this._onDocClick)
|
|
24
|
+
queueMicrotask(() => this.searchTarget.focus())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
close() {
|
|
28
|
+
this.setOpenState(false)
|
|
29
|
+
document.removeEventListener("click", this._onDocClick)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
filter() {
|
|
33
|
+
const query = this.searchTarget.value.toLowerCase()
|
|
34
|
+
this.optionTargets.forEach((option) => {
|
|
35
|
+
option.hidden = !option.dataset.label.toLowerCase().includes(query)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
choose(event) {
|
|
40
|
+
const option = event.currentTarget
|
|
41
|
+
this.valueTarget.value = option.dataset.value
|
|
42
|
+
this.labelTarget.textContent = option.dataset.label
|
|
43
|
+
this.close()
|
|
44
|
+
this.buttonTarget.focus()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
onKey(event) {
|
|
48
|
+
if (event.key === "Escape") {
|
|
49
|
+
this.close()
|
|
50
|
+
this.buttonTarget.focus()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setOpenState(open) {
|
|
55
|
+
this.panelTarget.hidden = !open
|
|
56
|
+
this.buttonTarget.setAttribute("aria-expanded", open ? "true" : "false")
|
|
57
|
+
this.buttonTarget.dataset.state = open ? "open" : "closed"
|
|
58
|
+
if (this.hasChevronTarget) this.chevronTarget.dataset.state = open ? "open" : "closed"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_onDocClick(event) {
|
|
62
|
+
if (!this.element.contains(event.target)) this.close()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// senren--command
|
|
4
|
+
// Local UI: filter command options and support keyboard navigation.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["input", "list", "option", "empty"]
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
this.activeIndex = 0
|
|
10
|
+
this.filter()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
filter() {
|
|
14
|
+
const query = this.normalize(this.inputTarget.value)
|
|
15
|
+
this.optionTargets.forEach((option) => {
|
|
16
|
+
const label = this.normalize(option.dataset.label || option.textContent)
|
|
17
|
+
option.hidden = query.length > 0 && !query.split(/\s+/).every((part) => label.includes(part))
|
|
18
|
+
})
|
|
19
|
+
this.activeIndex = 0
|
|
20
|
+
this.updateActive()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
onKey(event) {
|
|
24
|
+
if (event.key === "ArrowDown") {
|
|
25
|
+
event.preventDefault()
|
|
26
|
+
this.move(1)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (event.key === "ArrowUp") {
|
|
31
|
+
event.preventDefault()
|
|
32
|
+
this.move(-1)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (event.key === "Enter") {
|
|
37
|
+
const option = this.visibleOptions[this.activeIndex]
|
|
38
|
+
if (option) {
|
|
39
|
+
event.preventDefault()
|
|
40
|
+
option.click()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
choose(event) {
|
|
46
|
+
this.element.dispatchEvent(new CustomEvent("senren:command-select", {
|
|
47
|
+
bubbles: true,
|
|
48
|
+
detail: { label: event.currentTarget.textContent.trim(), href: event.currentTarget.getAttribute("href") }
|
|
49
|
+
}))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
move(delta) {
|
|
53
|
+
const options = this.visibleOptions
|
|
54
|
+
if (options.length === 0) return
|
|
55
|
+
this.activeIndex = (this.activeIndex + delta + options.length) % options.length
|
|
56
|
+
this.updateActive()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
updateActive() {
|
|
60
|
+
const options = this.visibleOptions
|
|
61
|
+
this.emptyTarget.hidden = options.length > 0
|
|
62
|
+
this.optionTargets.forEach((option) => option.setAttribute("aria-selected", "false"))
|
|
63
|
+
const active = options[this.activeIndex]
|
|
64
|
+
if (!active) {
|
|
65
|
+
this.inputTarget.removeAttribute("aria-activedescendant")
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
active.setAttribute("aria-selected", "true")
|
|
69
|
+
this.inputTarget.setAttribute("aria-activedescendant", active.id)
|
|
70
|
+
active.scrollIntoView({ block: "nearest" })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
normalize(value) {
|
|
74
|
+
return String(value || "").toLowerCase().replace(/[_-]+/g, " ").trim()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get visibleOptions() {
|
|
78
|
+
return this.optionTargets.filter((option) => !option.hidden)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// senren--context-menu
|
|
4
|
+
// Opens on right-click within the trigger; closes on outside click or Escape.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["trigger", "menu"]
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
this._onDocClick = this._onDocClick.bind(this)
|
|
10
|
+
this._onKey = this._onKey.bind(this)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
disconnect() {
|
|
14
|
+
document.removeEventListener("click", this._onDocClick)
|
|
15
|
+
document.removeEventListener("keydown", this._onKey)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
open(event) {
|
|
19
|
+
event.preventDefault()
|
|
20
|
+
const rect = this.element.getBoundingClientRect()
|
|
21
|
+
this.menuTarget.style.top = (event.clientY - rect.top) + "px"
|
|
22
|
+
this.menuTarget.style.left = (event.clientX - rect.left) + "px"
|
|
23
|
+
this.menuTarget.hidden = false
|
|
24
|
+
document.addEventListener("click", this._onDocClick)
|
|
25
|
+
document.addEventListener("keydown", this._onKey)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
close() {
|
|
29
|
+
this.menuTarget.hidden = true
|
|
30
|
+
document.removeEventListener("click", this._onDocClick)
|
|
31
|
+
document.removeEventListener("keydown", this._onKey)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_onDocClick(event) { if (!this.menuTarget.contains(event.target)) this.close() }
|
|
35
|
+
_onKey(event) { if (event.key === "Escape") this.close() }
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// senren--data-table
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = ["body", "row"]
|
|
6
|
+
|
|
7
|
+
connect() {
|
|
8
|
+
this.directions = {}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
sort(event) {
|
|
12
|
+
const key = event.currentTarget.dataset.sortKey
|
|
13
|
+
if (!key || !this.hasBodyTarget) return
|
|
14
|
+
|
|
15
|
+
const direction = this.directions[key] === "asc" ? "desc" : "asc"
|
|
16
|
+
this.directions[key] = direction
|
|
17
|
+
|
|
18
|
+
const rows = [...this.rowTargets]
|
|
19
|
+
rows.sort((a, b) => this.compare(this.valueFor(a, key), this.valueFor(b, key), direction))
|
|
20
|
+
rows.forEach((row) => this.bodyTarget.appendChild(row))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
valueFor(row, key) {
|
|
24
|
+
const cell = row.querySelector(`[data-sort-key="${CSS.escape(key)}"]`)
|
|
25
|
+
return (cell?.dataset.sortValue || cell?.textContent || "").trim().toLowerCase()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
compare(a, b, direction) {
|
|
29
|
+
const left = Number(a)
|
|
30
|
+
const right = Number(b)
|
|
31
|
+
const result = Number.isNaN(left) || Number.isNaN(right) ? a.localeCompare(b) : left - right
|
|
32
|
+
return direction === "asc" ? result : -result
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// senren--date-picker
|
|
4
|
+
// Local UI: small helpers around native date input.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["input"]
|
|
7
|
+
|
|
8
|
+
today() {
|
|
9
|
+
this.inputTarget.value = new Date().toISOString().slice(0, 10)
|
|
10
|
+
this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
clear() {
|
|
14
|
+
this.inputTarget.value = ""
|
|
15
|
+
this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }))
|
|
16
|
+
}
|
|
17
|
+
}
|