senren-ui 0.3.0 → 0.4.1
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 +4 -4
- data/CHANGELOG.md +147 -0
- data/CONTRIBUTING.md +10 -0
- data/README.md +36 -0
- data/docs/i18n.md +194 -0
- data/lib/commands/senren/add/add_command.rb +4 -1
- data/lib/commands/senren/locales/locales_command.rb +38 -0
- data/lib/generators/senren/install/install_generator.rb +36 -0
- data/lib/generators/senren/install/templates/base_component.rb.tt +39 -0
- data/lib/generators/senren/install/templates/conventions.md.tt +16 -0
- data/lib/generators/senren/install/templates/installed_components.yml.tt +1 -1
- data/lib/senren/rails/agent_rules_writer.rb +4 -0
- data/lib/senren/rails/base_component_patch.rb +24 -0
- data/lib/senren/rails/component_copier.rb +21 -0
- data/lib/senren/rails/doctor.rb +5 -1
- data/lib/senren/rails/host_paths.rb +5 -0
- data/lib/senren/rails/locale_installer.rb +90 -0
- data/lib/senren/rails/version.rb +1 -1
- data/lib/senren/rails.rb +1 -0
- data/lib/tasks/senren.rake +9 -0
- data/templates/components/accordion/accordion_component.html.erb +22 -4
- data/templates/components/accordion/accordion_component.rb +2 -1
- data/templates/components/alert_dialog/alert_dialog_component.html.erb +1 -1
- data/templates/components/api_key_field/api_key_field_component.html.erb +1 -1
- data/templates/components/api_key_field/api_key_field_component.rb +8 -1
- data/templates/components/app_shell/app_shell_component.rb +2 -1
- data/templates/components/breadcrumb/breadcrumb_component.rb +2 -1
- data/templates/components/bulk_action_bar/bulk_action_bar_component.html.erb +1 -1
- data/templates/components/bulk_action_bar/bulk_action_bar_component.rb +4 -2
- data/templates/components/calendar/calendar_component.rb +37 -2
- data/templates/components/carousel/carousel_component.html.erb +5 -5
- data/templates/components/carousel/carousel_component.rb +12 -1
- data/templates/components/cart/cart_component.html.erb +6 -6
- data/templates/components/cart/cart_component.rb +11 -2
- data/templates/components/clipboard/clipboard_component.html.erb +1 -1
- data/templates/components/clipboard/clipboard_component.rb +6 -1
- data/templates/components/collapsible/collapsible_component.rb +1 -1
- data/templates/components/combobox/combobox_component.rb +2 -1
- data/templates/components/command/command_component.rb +3 -1
- data/templates/components/data_table/data_table_component.html.erb +3 -3
- data/templates/components/data_table/data_table_component.rb +16 -1
- data/templates/components/date_picker/date_picker_component.html.erb +2 -2
- data/templates/components/date_picker/date_picker_component.rb +2 -1
- data/templates/components/dialog/dialog_component.html.erb +2 -2
- data/templates/components/filter_bar/filter_bar_component.rb +1 -1
- data/templates/components/form/form_component.html.erb +1 -1
- data/templates/components/form/form_component.rb +21 -0
- data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +9 -8
- data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +11 -3
- data/templates/components/native_select/native_select_component.rb +12 -3
- data/templates/components/pagination/pagination_component.html.erb +2 -2
- data/templates/components/pagination/pagination_component.rb +2 -1
- data/templates/components/product_card/product_card_component.html.erb +1 -1
- data/templates/components/product_card/product_card_component.rb +2 -2
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.html.erb +2 -16
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.rb +32 -1
- data/templates/components/search_input/search_input_component.rb +2 -1
- data/templates/components/sheet/sheet_component.html.erb +2 -2
- data/templates/components/sidebar/sidebar_component.html.erb +2 -2
- data/templates/components/sidebar/sidebar_component.rb +4 -1
- data/templates/components/table/table_component.html.erb +1 -1
- data/templates/components/tabs/tabs_component.rb +4 -2
- data/templates/components/theme_toggle/theme_toggle_component.html.erb +17 -3
- data/templates/components/theme_toggle/theme_toggle_component.rb +6 -1
- data/templates/components/top_nav/top_nav_component.html.erb +1 -1
- data/templates/components/top_nav/top_nav_component.rb +3 -2
- data/templates/controllers/accordion_controller.js +68 -3
- data/templates/controllers/api_key_field_controller.js +3 -2
- data/templates/controllers/carousel_controller.js +6 -1
- data/templates/controllers/clipboard_controller.js +3 -3
- data/templates/controllers/rich_text_editor_lite_controller.js +2 -2
- data/templates/controllers/theme_toggle_controller.js +13 -3
- data/templates/locales/senren.en.yml +151 -0
- data/templates/locales/senren.vi.yml +151 -0
- metadata +6 -1
|
@@ -1,4 +1,4 @@
|
|
|
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 %>
|
|
1
|
+
<%= tag.div(**root_attrs("relative 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
2
|
<% if toolbar? %>
|
|
3
3
|
<div class="border-b border-[hsl(var(--senren-border))] p-3"><%= toolbar %></div>
|
|
4
4
|
<% end %>
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<% if sortable? %>
|
|
19
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
20
|
<%= column_label(column) %>
|
|
21
|
-
<span aria-hidden="true"
|
|
21
|
+
<span aria-hidden="true"><%= senren_t('data_table.sort', default: 'Sort') %></span>
|
|
22
22
|
</button>
|
|
23
23
|
<% else %>
|
|
24
24
|
<%= column_label(column) %>
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<tr class="transition-colors hover:bg-[hsl(var(--senren-muted)/0.35)]" data-senren--data-table-target="row">
|
|
33
33
|
<% columns.each do |column| %>
|
|
34
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>
|
|
35
|
+
<td class="px-4 py-3" data-sort-key="<%= column_sort_key(column) %>" data-sort-value="<%= sort_value(value) %>"><%= value %></td>
|
|
36
36
|
<% end %>
|
|
37
37
|
</tr>
|
|
38
38
|
<% end %>
|
|
@@ -6,7 +6,8 @@ module Senren
|
|
|
6
6
|
VARIANTS = { default: '' }.freeze
|
|
7
7
|
SIZES = { md: '' }.freeze
|
|
8
8
|
|
|
9
|
-
def initialize(columns: [], rows: [], caption: nil,
|
|
9
|
+
def initialize(columns: [], rows: [], caption: nil,
|
|
10
|
+
empty_text: senren_t('data_table.empty', default: 'No records found.'), sortable: true,
|
|
10
11
|
variant: :default, class_name: nil, **html)
|
|
11
12
|
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
12
13
|
@columns = Array(columns)
|
|
@@ -33,6 +34,20 @@ module Senren
|
|
|
33
34
|
column_key(column).to_s
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
# The text a cell shows, for its data-sort-value. A cell holding markup (a
|
|
38
|
+
# badge, a link) is html_safe, which ERB does not escape, so writing it
|
|
39
|
+
# into the attribute as-is pasted the markup inside the quotes and broke
|
|
40
|
+
# the <td>. Its decoded text is a plain String, which ERB escapes once.
|
|
41
|
+
#
|
|
42
|
+
# Asks the string, not the value: ERB decides on `to_s.html_safe?`, so an
|
|
43
|
+
# object whose to_s returns markup breaks the attribute just the same.
|
|
44
|
+
def sort_value(value)
|
|
45
|
+
text = value.to_s
|
|
46
|
+
return text unless text.html_safe?
|
|
47
|
+
|
|
48
|
+
Nokogiri::HTML5.fragment(text.to_str).text
|
|
49
|
+
end
|
|
50
|
+
|
|
36
51
|
private
|
|
37
52
|
|
|
38
53
|
def column_key(column)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<%= tag.div(**root_attrs("flex w-full items-center gap-2", data: { controller: "senren--date-picker" })) do %>
|
|
2
2
|
<input type="date" autocomplete="off" id="<%= id %>" name="<%= name %>" value="<%= value %>" placeholder="<%= placeholder %>" data-senren--date-picker-target="input" class="h-10 w-full max-w-56 cursor-pointer rounded-(--senren-radius) border bg-[hsl(var(--senren-background))] px-3 text-sm text-[hsl(var(--senren-foreground))] outline-none transition-colors focus:ring-2 focus:ring-[hsl(var(--senren-ring))] [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-60 [&::-webkit-calendar-picker-indicator]:hover:opacity-100 <%= self.class::VARIANTS[variant] %>">
|
|
3
|
-
<button type="button" data-action="click->senren--date-picker#today" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) border border-[hsl(var(--senren-border))] px-3 text-sm font-medium hover:bg-[hsl(var(--senren-accent))]"
|
|
4
|
-
<button type="button" data-action="click->senren--date-picker#clear" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) px-3 text-sm text-[hsl(var(--senren-muted-foreground))] hover:bg-[hsl(var(--senren-accent))]"
|
|
3
|
+
<button type="button" data-action="click->senren--date-picker#today" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) border border-[hsl(var(--senren-border))] px-3 text-sm font-medium hover:bg-[hsl(var(--senren-accent))]"><%= senren_t('date_picker.today', default: 'Today') %></button>
|
|
4
|
+
<button type="button" data-action="click->senren--date-picker#clear" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) px-3 text-sm text-[hsl(var(--senren-muted-foreground))] hover:bg-[hsl(var(--senren-accent))]"><%= senren_t('date_picker.clear', default: 'Clear') %></button>
|
|
5
5
|
<% end %>
|
|
@@ -8,7 +8,8 @@ module Senren
|
|
|
8
8
|
}.freeze
|
|
9
9
|
SIZES = { md: '' }.freeze
|
|
10
10
|
|
|
11
|
-
def initialize(name:, value: nil, id: nil, placeholder: '
|
|
11
|
+
def initialize(name:, value: nil, id: nil, placeholder: senren_t('date_picker.placeholder', default: 'Select date'),
|
|
12
|
+
variant: :default, class_name: nil, **html)
|
|
12
13
|
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
13
14
|
@name = name
|
|
14
15
|
@value = value
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
</span>
|
|
9
9
|
<% else %>
|
|
10
10
|
<button type="button" data-action="click->senren--dialog#open"
|
|
11
|
-
class="hidden" data-senren--dialog-target="trigger" aria-controls="<%= dom_id %>-panel"
|
|
11
|
+
class="hidden" data-senren--dialog-target="trigger" aria-controls="<%= dom_id %>-panel"><%= senren_t('shared.open', default: 'Open') %></button>
|
|
12
12
|
<% end %>
|
|
13
13
|
|
|
14
14
|
<div data-senren--dialog-target="overlay" hidden
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
<% end %>
|
|
35
35
|
|
|
36
36
|
<button type="button" data-action="click->senren--dialog#close"
|
|
37
|
-
aria-label="Close"
|
|
37
|
+
aria-label="<%= senren_t('shared.close', default: 'Close') %>"
|
|
38
38
|
class="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
|
|
39
39
|
<span aria-hidden="true">×</span>
|
|
40
40
|
</button>
|
|
@@ -5,7 +5,7 @@ module Senren
|
|
|
5
5
|
VARIANTS = { default: '' }.freeze
|
|
6
6
|
SIZES = { md: '' }.freeze
|
|
7
7
|
|
|
8
|
-
def initialize(label: 'Filters', class_name: nil, **html)
|
|
8
|
+
def initialize(label: senren_t('filter_bar.label', default: 'Filters'), class_name: nil, **html)
|
|
9
9
|
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
10
10
|
@label = label
|
|
11
11
|
end
|
|
@@ -17,6 +17,27 @@ module Senren
|
|
|
17
17
|
|
|
18
18
|
attr_reader :model, :method, :multipart
|
|
19
19
|
|
|
20
|
+
# ViewComponent's `content` calls the render block with the component as
|
|
21
|
+
# its argument, so `|f|` would be this component, not the form builder.
|
|
22
|
+
# And because a component is an ActionView::Base, `f.text_field :title,
|
|
23
|
+
# class: "x"` would not raise: it would reach the template-level helper and
|
|
24
|
+
# render `name="title[{class: "x"}]"`. Keep the block and call it with the
|
|
25
|
+
# builder form_with yields instead.
|
|
26
|
+
def render_in(view_context, **, &block)
|
|
27
|
+
@form_block = block
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Mirrors ViewComponent's own `content`: the block runs under the caller's
|
|
32
|
+
# virtual path, so relative `t('.key')` lookups inside it resolve against
|
|
33
|
+
# the caller's template. (A block and with_content together never get
|
|
34
|
+
# here: ViewComponent raises DuplicateContentError first.)
|
|
35
|
+
def form_body(builder)
|
|
36
|
+
return content unless @form_block
|
|
37
|
+
|
|
38
|
+
with_captured_virtual_path(@old_virtual_path) { view_context.capture(builder, &@form_block) }
|
|
39
|
+
end
|
|
40
|
+
|
|
20
41
|
# This one reaches form_with's `action`, which makes it the highest-value
|
|
21
42
|
# URL sink in the library: `//evil.example` is a protocol-relative absolute
|
|
22
43
|
# URL, so a form built from user-controlled input POSTs every field —
|
|
@@ -19,18 +19,19 @@
|
|
|
19
19
|
<% if content? %>
|
|
20
20
|
<%= content %>
|
|
21
21
|
<% else %>
|
|
22
|
-
<label class="block text-sm font-medium text-[hsl(var(--senren-foreground))]" for="<%= dom_id %>-email"
|
|
23
|
-
<input id="<%= dom_id %>-email" name="<%= email_name %>" type="email" autocomplete="email" placeholder="teammate@company.com" class="h-10 w-full rounded-(--senren-radius) border border-[hsl(var(--senren-input))] bg-[hsl(var(--senren-background))] px-3 text-sm outline-none focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
|
|
24
|
-
<label class="block text-sm font-medium text-[hsl(var(--senren-foreground))]" for="<%= dom_id %>-role"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
<label class="block text-sm font-medium text-[hsl(var(--senren-foreground))]" for="<%= dom_id %>-email"><%= senren_t('invite_member_dialog.email', default: 'Email') %></label>
|
|
23
|
+
<input id="<%= dom_id %>-email" name="<%= email_name %>" type="email" autocomplete="email" placeholder="<%= senren_t('invite_member_dialog.email_placeholder', default: 'teammate@company.com') %>" class="h-10 w-full rounded-(--senren-radius) border border-[hsl(var(--senren-input))] bg-[hsl(var(--senren-background))] px-3 text-sm outline-none focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
|
|
24
|
+
<label class="block text-sm font-medium text-[hsl(var(--senren-foreground))]" for="<%= dom_id %>-role"><%= senren_t('invite_member_dialog.role', default: 'Role') %></label>
|
|
25
|
+
<%# Was a hand-written <select> with the styling copied out of %>
|
|
26
|
+
<%# NativeSelectComponent, which meant it silently missed anything that %>
|
|
27
|
+
<%# component gained later -- the chevron and its rotation among them. %>
|
|
28
|
+
<%= render Senren::NativeSelectComponent.new(name: role_name, id: "#{dom_id}-role", options: roles) %>
|
|
28
29
|
<% end %>
|
|
29
30
|
</div>
|
|
30
31
|
|
|
31
32
|
<div class="mt-6 flex justify-end gap-2">
|
|
32
|
-
<button type="button" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) px-4 text-sm font-medium hover:bg-[hsl(var(--senren-accent))]" data-action="click->senren--invite-member-dialog#close"
|
|
33
|
-
<% if footer? %><%= footer %><% else %><button type="submit" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) bg-[hsl(var(--senren-primary))] px-4 text-sm font-medium text-[hsl(var(--senren-primary-foreground))] hover:opacity-90"
|
|
33
|
+
<button type="button" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) px-4 text-sm font-medium hover:bg-[hsl(var(--senren-accent))]" data-action="click->senren--invite-member-dialog#close"><%= senren_t('invite_member_dialog.cancel', default: 'Cancel') %></button>
|
|
34
|
+
<% if footer? %><%= footer %><% else %><button type="submit" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) bg-[hsl(var(--senren-primary))] px-4 text-sm font-medium text-[hsl(var(--senren-primary-foreground))] hover:opacity-90"><%= senren_t('invite_member_dialog.send', default: 'Send invite') %></button><% end %>
|
|
34
35
|
</div>
|
|
35
36
|
</div>
|
|
36
37
|
<% end %>
|
|
@@ -8,9 +8,17 @@ module Senren
|
|
|
8
8
|
VARIANTS = { default: '' }.freeze
|
|
9
9
|
SIZES = { md: '' }.freeze
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
# The role option values stay `Member` and `Admin`, because that is what the
|
|
12
|
+
# form submits and what the host's code compares against. Only the label a
|
|
13
|
+
# person reads is translated.
|
|
14
|
+
def initialize(title: senren_t('invite_member_dialog.title', default: 'Invite teammate'),
|
|
15
|
+
description: senren_t('invite_member_dialog.description',
|
|
16
|
+
default: 'Send an invitation to join this workspace.'),
|
|
17
|
+
email_name: 'email', role_name: 'role',
|
|
18
|
+
roles: [['Member', senren_t('invite_member_dialog.role_member', default: 'Member')],
|
|
19
|
+
['Admin', senren_t('invite_member_dialog.role_admin', default: 'Admin')]],
|
|
20
|
+
button_label: senren_t('invite_member_dialog.button', default: 'Invite member'),
|
|
21
|
+
id: nil, class_name: nil, **html)
|
|
14
22
|
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
15
23
|
@title = title
|
|
16
24
|
@description = description
|
|
@@ -13,9 +13,18 @@ module Senren
|
|
|
13
13
|
lg: 'h-12 text-base px-4'
|
|
14
14
|
}.freeze
|
|
15
15
|
|
|
16
|
-
# native_arrow:
|
|
17
|
-
#
|
|
18
|
-
|
|
16
|
+
# native_arrow: false → custom SVG chevron that rotates while the select has
|
|
17
|
+
# focus (appearance-none + positioned SVG). The default.
|
|
18
|
+
# native_arrow: true → the browser's own arrow (appearance-auto), which
|
|
19
|
+
# matches the OS but cannot be styled or animated.
|
|
20
|
+
#
|
|
21
|
+
# The default was `true`, so out of the box a Senren select was the one
|
|
22
|
+
# control in the library with no state to see: the arrow never moved, and
|
|
23
|
+
# nothing distinguished an open select from a closed one. The custom arrow
|
|
24
|
+
# was already written and had to be asked for by name, which meant nobody
|
|
25
|
+
# got it. Pass native_arrow: true to opt back in -- worth doing on mobile,
|
|
26
|
+
# where the OS renders its own picker anyway.
|
|
27
|
+
def initialize(name:, options:, selected: nil, id: nil, prompt: nil, native_arrow: false,
|
|
19
28
|
variant: :default, size: :md, class_name: nil, **html)
|
|
20
29
|
super(variant: variant, size: size, class_name: class_name, **html)
|
|
21
30
|
@name = name
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<%= tag.nav(**root_attrs("flex flex-wrap items-center justify-center gap-1", aria: { label: label })) do %>
|
|
2
2
|
<% prev_page = current_page - 1 %>
|
|
3
|
-
<%= link_to
|
|
3
|
+
<%= link_to senren_t('pagination.previous', default: 'Previous'), page_url(prev_page), class: "rounded-md border border-[hsl(var(--senren-border))] px-3 py-2 text-sm #{current_page == 1 ? "pointer-events-none opacity-50" : "hover:bg-[hsl(var(--senren-accent))]"}", aria: { disabled: current_page == 1 } %>
|
|
4
4
|
|
|
5
5
|
<% (1..total_pages).each do |page| %>
|
|
6
6
|
<%= link_to page, page_url(page), class: "inline-flex h-9 min-w-9 items-center justify-center rounded-md px-3 text-sm font-medium #{page == current_page ? "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))]"}", aria: { current: (page == current_page ? "page" : nil) } %>
|
|
7
7
|
<% end %>
|
|
8
8
|
|
|
9
9
|
<% next_page = current_page + 1 %>
|
|
10
|
-
<%= link_to
|
|
10
|
+
<%= link_to senren_t('pagination.next', default: 'Next'), page_url(next_page), class: "rounded-md border border-[hsl(var(--senren-border))] px-3 py-2 text-sm #{current_page == total_pages ? "pointer-events-none opacity-50" : "hover:bg-[hsl(var(--senren-accent))]"}", aria: { disabled: current_page == total_pages } %>
|
|
11
11
|
<% end %>
|
|
@@ -5,7 +5,8 @@ module Senren
|
|
|
5
5
|
VARIANTS = { default: '' }.freeze
|
|
6
6
|
SIZES = { md: '' }.freeze
|
|
7
7
|
|
|
8
|
-
def initialize(current_page: 1, total_pages: 1, path: nil, label: '
|
|
8
|
+
def initialize(current_page: 1, total_pages: 1, path: nil, label: senren_t('pagination.label', default: 'Pagination'),
|
|
9
|
+
class_name: nil, **html)
|
|
9
10
|
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
10
11
|
@total_pages = [total_pages.to_i, 1].max
|
|
11
12
|
@current_page = current_page.to_i.clamp(1, @total_pages)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<%= image_tag safe_image_url, alt: title, loading: "lazy", class: "h-full w-full object-cover" %>
|
|
5
5
|
<% else %>
|
|
6
6
|
<div class="flex h-full w-full items-center justify-center text-sm text-[hsl(var(--senren-muted-foreground))]" aria-hidden="true">
|
|
7
|
-
No image
|
|
7
|
+
<%= senren_t('product_card.no_image', default: 'No image') %>
|
|
8
8
|
</div>
|
|
9
9
|
<% end %>
|
|
10
10
|
|
|
@@ -20,7 +20,7 @@ module Senren
|
|
|
20
20
|
SIZES = { md: '' }.freeze
|
|
21
21
|
|
|
22
22
|
def initialize(title:, price:, url:, image_url: nil, description: nil, badge: nil,
|
|
23
|
-
action_label: 'Add to cart', available: true, method: :post,
|
|
23
|
+
action_label: senren_t('product_card.add_to_cart', default: 'Add to cart'), available: true, method: :post,
|
|
24
24
|
variant: :default, id: nil, class_name: nil, **html)
|
|
25
25
|
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
26
26
|
@title = title
|
|
@@ -44,6 +44,6 @@ module Senren
|
|
|
44
44
|
def safe_action_url = safe_url(@url)
|
|
45
45
|
def safe_image_url = safe_media_url(@image_url)
|
|
46
46
|
|
|
47
|
-
def button_label = available? ? action_label : 'Out of stock'
|
|
47
|
+
def button_label = available? ? action_label : senren_t('product_card.out_of_stock', default: 'Out of stock')
|
|
48
48
|
end
|
|
49
49
|
end
|
|
@@ -1,24 +1,10 @@
|
|
|
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
|
|
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?, "senren--rich-text-editor-lite-link-prompt-value": link_prompt })) do %>
|
|
2
2
|
<label for="<%= dom_id %>-editor" class="sr-only"><%= label %></label>
|
|
3
3
|
<textarea hidden name="<%= name %>" data-senren--rich-text-editor-lite-target="input"><%= initial_content %></textarea>
|
|
4
4
|
|
|
5
5
|
<% if toolbar? %>
|
|
6
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| %>
|
|
7
|
+
<% toolbar_buttons.each do |command, label_text, aria_label| %>
|
|
22
8
|
<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
9
|
<%= label_text %>
|
|
24
10
|
</button>
|
|
@@ -5,7 +5,8 @@ module Senren
|
|
|
5
5
|
}.freeze
|
|
6
6
|
SIZES = { md: '' }.freeze
|
|
7
7
|
|
|
8
|
-
def initialize(name: 'content', value: nil, label: '
|
|
8
|
+
def initialize(name: 'content', value: nil, label: senren_t('rich_text_editor_lite.label', default: 'Content'),
|
|
9
|
+
placeholder: senren_t('rich_text_editor_lite.placeholder', default: 'Write something...'), id: nil,
|
|
9
10
|
toolbar: true, debug: ::Rails.env.development?, class_name: nil, **html)
|
|
10
11
|
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
11
12
|
@name = name
|
|
@@ -23,6 +24,36 @@ module Senren
|
|
|
23
24
|
|
|
24
25
|
def debug? = !!@debug
|
|
25
26
|
|
|
27
|
+
# Read by the controller from a Stimulus value, so the prompt it opens is in
|
|
28
|
+
# the host's language without a sentence living in JavaScript.
|
|
29
|
+
def link_prompt = senren_t('rich_text_editor_lite.link_prompt', default: 'Paste a URL')
|
|
30
|
+
|
|
31
|
+
# [command, visible text, aria-label]. `P`, `H1`-`H3`, `B`, `I` and `1.` are
|
|
32
|
+
# symbols, not words, so they are not translated; every word is.
|
|
33
|
+
def toolbar_buttons
|
|
34
|
+
[
|
|
35
|
+
['formatBlock:p', 'P', senren_t('rich_text_editor_lite.toolbar.paragraph', default: 'Paragraph')],
|
|
36
|
+
['formatBlock:h1', 'H1', senren_t('rich_text_editor_lite.toolbar.heading_1', default: 'Heading 1')],
|
|
37
|
+
['formatBlock:h2', 'H2', senren_t('rich_text_editor_lite.toolbar.heading_2', default: 'Heading 2')],
|
|
38
|
+
['formatBlock:h3', 'H3', senren_t('rich_text_editor_lite.toolbar.heading_3', default: 'Heading 3')],
|
|
39
|
+
['bold', 'B', senren_t('rich_text_editor_lite.toolbar.bold', default: 'Bold')],
|
|
40
|
+
['italic', 'I', senren_t('rich_text_editor_lite.toolbar.italic', default: 'Italic')],
|
|
41
|
+
['createLink', senren_t('rich_text_editor_lite.toolbar_short.link', default: 'Link'),
|
|
42
|
+
senren_t('rich_text_editor_lite.toolbar.create_link', default: 'Create link')],
|
|
43
|
+
['insertUnorderedList', senren_t('rich_text_editor_lite.toolbar_short.list', default: 'List'),
|
|
44
|
+
senren_t('rich_text_editor_lite.toolbar.bulleted_list', default: 'Bulleted list')],
|
|
45
|
+
['insertOrderedList', '1.', senren_t('rich_text_editor_lite.toolbar.numbered_list', default: 'Numbered list')],
|
|
46
|
+
['align:left', senren_t('rich_text_editor_lite.toolbar_short.left', default: 'Left'),
|
|
47
|
+
senren_t('rich_text_editor_lite.toolbar.align_left', default: 'Align left')],
|
|
48
|
+
['align:center', senren_t('rich_text_editor_lite.toolbar_short.center', default: 'Center'),
|
|
49
|
+
senren_t('rich_text_editor_lite.toolbar.align_center', default: 'Align center')],
|
|
50
|
+
['align:right', senren_t('rich_text_editor_lite.toolbar_short.right', default: 'Right'),
|
|
51
|
+
senren_t('rich_text_editor_lite.toolbar.align_right', default: 'Align right')],
|
|
52
|
+
['align:justify', senren_t('rich_text_editor_lite.toolbar_short.justify', default: 'Justify'),
|
|
53
|
+
senren_t('rich_text_editor_lite.toolbar.justify', default: 'Justify')]
|
|
54
|
+
]
|
|
55
|
+
end
|
|
56
|
+
|
|
26
57
|
def initial_content
|
|
27
58
|
value.presence || content.to_s.presence || "<p>#{ERB::Util.html_escape(placeholder)}</p>"
|
|
28
59
|
end
|
|
@@ -5,7 +5,8 @@ module Senren
|
|
|
5
5
|
VARIANTS = { default: '' }.freeze
|
|
6
6
|
SIZES = { md: '' }.freeze
|
|
7
7
|
|
|
8
|
-
def initialize(name: 'q', value: nil, placeholder: '
|
|
8
|
+
def initialize(name: 'q', value: nil, placeholder: senren_t('search_input.placeholder', default: 'Search...'),
|
|
9
|
+
label: senren_t('search_input.label', default: 'Search'), class_name: nil, **html)
|
|
9
10
|
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
10
11
|
@name = name
|
|
11
12
|
@value = value
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<%= trigger %>
|
|
8
8
|
</span>
|
|
9
9
|
<% else %>
|
|
10
|
-
<button type="button" data-action="click->senren--sheet#open" data-senren--sheet-target="trigger" class="hidden"
|
|
10
|
+
<button type="button" data-action="click->senren--sheet#open" data-senren--sheet-target="trigger" class="hidden"><%= senren_t('shared.open', default: 'Open') %></button>
|
|
11
11
|
<% end %>
|
|
12
12
|
|
|
13
13
|
<div data-senren--sheet-target="overlay" hidden
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
</div>
|
|
39
39
|
<% end %>
|
|
40
40
|
</div>
|
|
41
|
-
<button type="button" data-action="click->senren--sheet#close" aria-label="Close"
|
|
41
|
+
<button type="button" data-action="click->senren--sheet#close" aria-label="<%= senren_t('shared.close', default: 'Close') %>"
|
|
42
42
|
class="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100">
|
|
43
43
|
<span aria-hidden="true">×</span>
|
|
44
44
|
</button>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<%= tag.aside(**root_attrs("flex min-h-80 flex-col overflow-hidden rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] p-3 text-[hsl(var(--senren-card-foreground))] transition-[width] duration-300 ease-in-out", data: { controller: "senren--sidebar" })) do %>
|
|
2
2
|
<div class="mb-4 flex items-center justify-between gap-2 px-2">
|
|
3
3
|
<div data-senren--sidebar-target="brand" class="truncate font-display text-sm font-semibold tracking-tight"><%= brand %></div>
|
|
4
|
-
<button type="button" data-senren--sidebar-target="toggleButton" class="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-[hsl(var(--senren-muted-foreground))] transition-colors hover:bg-[hsl(var(--senren-accent))] hover:text-[hsl(var(--senren-accent-foreground))]" data-action="click->senren--sidebar#toggle" aria-label="
|
|
5
|
-
<span class="sr-only"
|
|
4
|
+
<button type="button" data-senren--sidebar-target="toggleButton" class="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-[hsl(var(--senren-muted-foreground))] transition-colors hover:bg-[hsl(var(--senren-accent))] hover:text-[hsl(var(--senren-accent-foreground))]" data-action="click->senren--sidebar#toggle" aria-label="<%= toggle_label %>" aria-expanded="<%= variant != :compact %>">
|
|
5
|
+
<span class="sr-only"><%= toggle_label %></span>
|
|
6
6
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
7
7
|
<path d="M3 6h18" />
|
|
8
8
|
<path d="M3 12h18" />
|
|
@@ -8,7 +8,8 @@ module Senren
|
|
|
8
8
|
}.freeze
|
|
9
9
|
SIZES = { md: '' }.freeze
|
|
10
10
|
|
|
11
|
-
def initialize(items: [], brand: '
|
|
11
|
+
def initialize(items: [], brand: senren_t('sidebar.brand', default: 'Senren'), variant: :default,
|
|
12
|
+
label: senren_t('sidebar.label', default: 'Primary'), class_name: nil, **html)
|
|
12
13
|
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
13
14
|
@items = normalize_items(items)
|
|
14
15
|
@brand = brand
|
|
@@ -17,6 +18,8 @@ module Senren
|
|
|
17
18
|
|
|
18
19
|
attr_reader :items, :brand, :label
|
|
19
20
|
|
|
21
|
+
def toggle_label = senren_t('sidebar.toggle', default: 'Toggle sidebar')
|
|
22
|
+
|
|
20
23
|
private
|
|
21
24
|
|
|
22
25
|
def normalize_items(items)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%= tag.div(**root_attrs("w-full overflow-hidden rounded-(--senren-radius) border border-[hsl(var(--senren-border))]")) do %>
|
|
1
|
+
<%= tag.div(**root_attrs("relative w-full overflow-hidden rounded-(--senren-radius) border border-[hsl(var(--senren-border))]")) do %>
|
|
2
2
|
<table class="w-full border-collapse text-left text-sm <%= self.class::VARIANTS[variant] %>">
|
|
3
3
|
<% if caption.present? %>
|
|
4
4
|
<caption class="sr-only"><%= caption %></caption>
|
|
@@ -8,7 +8,8 @@ module Senren
|
|
|
8
8
|
}.freeze
|
|
9
9
|
SIZES = { md: '' }.freeze
|
|
10
10
|
|
|
11
|
-
def initialize(items: [], variant: :default, active: nil, label: '
|
|
11
|
+
def initialize(items: [], variant: :default, active: nil, label: senren_t('tabs.label', default: 'Tabs'),
|
|
12
|
+
class_name: nil, **html)
|
|
12
13
|
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
13
14
|
@items = normalize_items(items)
|
|
14
15
|
@active = active&.to_s || @items.first&.fetch(:id, nil)
|
|
@@ -26,7 +27,8 @@ module Senren
|
|
|
26
27
|
def normalize_items(items)
|
|
27
28
|
Array(items).map.with_index do |item, index|
|
|
28
29
|
source = item.is_a?(Hash) ? item : { label: item.to_s }
|
|
29
|
-
label = source[:label] || source['label'] ||
|
|
30
|
+
label = source[:label] || source['label'] ||
|
|
31
|
+
senren_t('tabs.tab', default: 'Tab %{number}', number: index + 1)
|
|
30
32
|
id = (source[:id] || source['id'] || label.to_s.parameterize).to_s
|
|
31
33
|
{ id: id, label: label, content: source[:content] || source['content'] }
|
|
32
34
|
end
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
<%# Two icons, one hidden, rather than one element whose text is rewritten. %>
|
|
2
|
+
<%# The old version wrote the letters "D" and "L" into a span, which read as %>
|
|
3
|
+
<%# literal capital letters next to the label -- and to a screen reader, as %>
|
|
4
|
+
<%# "D Dark theme" if the aria-hidden ever came off. %>
|
|
5
|
+
<%= tag.button(**root_attrs("group 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))]", type: :button, data: { controller: "senren--theme-toggle", action: "click->senren--theme-toggle#toggle", "senren--theme-toggle-light-label-value": light_label, "senren--theme-toggle-dark-label-value": dark_label }, aria: { label: label, pressed: "false" })) do %>
|
|
6
|
+
<span aria-hidden="true" data-senren--theme-toggle-target="iconLight" hidden class="inline-flex">
|
|
7
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 transition-transform duration-200 group-hover:rotate-45">
|
|
8
|
+
<circle cx="12" cy="12" r="4"></circle>
|
|
9
|
+
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"></path>
|
|
10
|
+
</svg>
|
|
11
|
+
</span>
|
|
12
|
+
<span aria-hidden="true" data-senren--theme-toggle-target="iconDark" class="inline-flex">
|
|
13
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 transition-transform duration-200 group-hover:-rotate-12">
|
|
14
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z"></path>
|
|
15
|
+
</svg>
|
|
16
|
+
</span>
|
|
3
17
|
<span data-senren--theme-toggle-target="label"><%= label %></span>
|
|
4
|
-
|
|
18
|
+
<% end %>
|
|
@@ -5,11 +5,16 @@ module Senren
|
|
|
5
5
|
VARIANTS = { default: '' }.freeze
|
|
6
6
|
SIZES = { md: '' }.freeze
|
|
7
7
|
|
|
8
|
-
def initialize(label: 'Toggle theme', class_name: nil, **html)
|
|
8
|
+
def initialize(label: senren_t('theme_toggle.label', default: 'Toggle theme'), class_name: nil, **html)
|
|
9
9
|
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
10
10
|
@label = label
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
attr_reader :label
|
|
14
|
+
|
|
15
|
+
# What the label reads once the theme has switched. The controller swaps
|
|
16
|
+
# between the two, so both reach it as Stimulus values.
|
|
17
|
+
def light_label = senren_t('theme_toggle.light', default: 'Light theme')
|
|
18
|
+
def dark_label = senren_t('theme_toggle.dark', default: 'Dark theme')
|
|
14
19
|
end
|
|
15
20
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%= tag.header(**root_attrs("sticky top-0 z-30 border-b border-[hsl(var(--senren-border))] backdrop-blur-md")) do %>
|
|
2
2
|
<div class="mx-auto flex h-14 w-full max-w-[1400px] items-center gap-6 px-4 lg:px-8">
|
|
3
3
|
<div class="min-w-0 flex-none font-display text-sm font-semibold text-[hsl(var(--senren-foreground))]">
|
|
4
|
-
<% if brand? %><%= brand %><% else
|
|
4
|
+
<% if brand? %><%= brand %><% else %><%= senren_t('top_nav.brand', default: 'Senren') %><% end %>
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
7
|
<% if items.any? %>
|
|
@@ -6,12 +6,13 @@ module Senren
|
|
|
6
6
|
renders_one :actions
|
|
7
7
|
|
|
8
8
|
VARIANTS = {
|
|
9
|
-
default: 'bg-[hsl(var(--senren-background)
|
|
9
|
+
default: 'bg-[hsl(var(--senren-background)/0.88)]',
|
|
10
10
|
solid: 'bg-[hsl(var(--senren-card))]'
|
|
11
11
|
}.freeze
|
|
12
12
|
SIZES = { md: '' }.freeze
|
|
13
13
|
|
|
14
|
-
def initialize(items: [], current_path: nil, label: 'Global', variant: :default,
|
|
14
|
+
def initialize(items: [], current_path: nil, label: senren_t('top_nav.label', default: 'Global'), variant: :default,
|
|
15
|
+
class_name: nil, **html)
|
|
15
16
|
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
16
17
|
@items = normalize_items(items)
|
|
17
18
|
@current_path = current_path
|
|
@@ -6,6 +6,19 @@ export default class extends Controller {
|
|
|
6
6
|
static targets = ["trigger", "panel"]
|
|
7
7
|
static values = { multiple: Boolean }
|
|
8
8
|
|
|
9
|
+
// Every pending close timer, so disconnect() can clear them. A panel mid-close
|
|
10
|
+
// when Turbo swaps the page would otherwise leave a timer holding a reference
|
|
11
|
+
// to a detached element -- caught by bin/performance, which fails the build
|
|
12
|
+
// for a timer no disconnect() clears.
|
|
13
|
+
connect() {
|
|
14
|
+
this._timers = new Set()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
disconnect() {
|
|
18
|
+
this._timers?.forEach(clearTimeout)
|
|
19
|
+
this._timers?.clear()
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
toggle(event) {
|
|
10
23
|
const trigger = event.currentTarget
|
|
11
24
|
const panel = this._panelFor(trigger.dataset.panelId)
|
|
@@ -13,12 +26,64 @@ export default class extends Controller {
|
|
|
13
26
|
|
|
14
27
|
if (!this.multipleValue) this._closeAll()
|
|
15
28
|
trigger.setAttribute("aria-expanded", nextOpen ? "true" : "false")
|
|
16
|
-
|
|
29
|
+
this._setOpen(panel, nextOpen)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The panel animates from grid-template-rows 0fr to 1fr, and it also carries
|
|
33
|
+
// the `hidden` attribute so collapsed content is not focusable or read aloud.
|
|
34
|
+
// Those two want opposite things: `hidden` has to be off for the browser to
|
|
35
|
+
// animate anything, and on once the panel is closed.
|
|
36
|
+
//
|
|
37
|
+
// Opening: drop `hidden`, then flip the row on the next frame -- setting both
|
|
38
|
+
// in one go gives the browser no first value to animate from, and the panel
|
|
39
|
+
// snaps open.
|
|
40
|
+
//
|
|
41
|
+
// Closing: flip the row now, restore `hidden` when the transition ends.
|
|
42
|
+
_setOpen(panel, open) {
|
|
43
|
+
if (!panel) return
|
|
44
|
+
|
|
45
|
+
if (open) {
|
|
46
|
+
panel.hidden = false
|
|
47
|
+
requestAnimationFrame(() => {
|
|
48
|
+
panel.classList.remove("grid-rows-[0fr]")
|
|
49
|
+
panel.classList.add("grid-rows-[1fr]")
|
|
50
|
+
})
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
panel.classList.remove("grid-rows-[1fr]")
|
|
55
|
+
panel.classList.add("grid-rows-[0fr]")
|
|
56
|
+
this._hideAfterTransition(panel)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// `transitionend` alone is not enough: it never fires when the panel is
|
|
60
|
+
// already collapsed, when a reduced-motion setting removes the transition, or
|
|
61
|
+
// when the element is off-screen. Without the timeout those panels would stay
|
|
62
|
+
// focusable forever, so whichever arrives first wins and the other is dropped.
|
|
63
|
+
_hideAfterTransition(panel) {
|
|
64
|
+
const finish = () => {
|
|
65
|
+
clearTimeout(timer)
|
|
66
|
+
this._timers?.delete(timer)
|
|
67
|
+
panel.removeEventListener("transitionend", onEnd)
|
|
68
|
+
if (panel.classList.contains("grid-rows-[0fr]")) panel.hidden = true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const onEnd = (event) => {
|
|
72
|
+
if (event.target === panel && event.propertyName === "grid-template-rows") finish()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const timer = setTimeout(finish, 300)
|
|
76
|
+
this._timers?.add(timer)
|
|
77
|
+
panel.addEventListener("transitionend", onEnd)
|
|
17
78
|
}
|
|
18
79
|
|
|
19
80
|
_closeAll() {
|
|
20
|
-
this.triggerTargets.forEach((trigger) => {
|
|
21
|
-
|
|
81
|
+
this.triggerTargets.forEach((trigger) => {
|
|
82
|
+
trigger.setAttribute("aria-expanded", "false")
|
|
83
|
+
})
|
|
84
|
+
this.panelTargets.forEach((panel) => {
|
|
85
|
+
this._setOpen(panel, false)
|
|
86
|
+
})
|
|
22
87
|
}
|
|
23
88
|
|
|
24
89
|
_panelFor(id) {
|