senren-ui 0.2.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +238 -0
- data/CONTRIBUTING.md +11 -0
- data/README.md +64 -0
- data/docs/i18n.md +194 -0
- data/docs/visual_style.md +80 -0
- data/lib/generators/senren/install/install_generator.rb +39 -0
- data/lib/generators/senren/install/templates/base_component.rb.tt +136 -7
- data/lib/generators/senren/install/templates/conventions.md.tt +57 -0
- data/lib/generators/senren/install/templates/senren_themes.css.tt +339 -0
- data/lib/senren/rails/agent_rules_writer.rb +7 -0
- data/lib/senren/rails/base_component_patch.rb +24 -0
- data/lib/senren/rails/component_copier.rb +21 -0
- 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 +24 -6
- data/templates/components/accordion/accordion_component.rb +2 -1
- data/templates/components/alert_dialog/alert_dialog_component.html.erb +7 -4
- data/templates/components/alert_dialog/alert_dialog_component.rb +4 -0
- data/templates/components/api_key_field/api_key_field_component.html.erb +2 -2
- 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.html.erb +8 -6
- 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/button/button_component.html.erb +1 -1
- data/templates/components/button/button_component.rb +27 -1
- data/templates/components/calendar/calendar_component.html.erb +2 -2
- data/templates/components/calendar/calendar_component.rb +37 -2
- data/templates/components/card/card_component.html.erb +4 -6
- data/templates/components/carousel/carousel_component.html.erb +6 -6
- data/templates/components/carousel/carousel_component.rb +12 -1
- data/templates/components/cart/cart_component.html.erb +13 -7
- data/templates/components/cart/cart_component.rb +11 -2
- data/templates/components/clipboard/clipboard_component.html.erb +3 -3
- data/templates/components/clipboard/clipboard_component.rb +6 -1
- data/templates/components/collapsible/collapsible_component.html.erb +3 -3
- data/templates/components/collapsible/collapsible_component.rb +1 -1
- data/templates/components/combobox/combobox_component.html.erb +3 -3
- data/templates/components/combobox/combobox_component.rb +2 -1
- data/templates/components/command/command_component.html.erb +1 -1
- data/templates/components/command/command_component.rb +3 -1
- data/templates/components/context_menu/context_menu_component.html.erb +6 -3
- 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 +5 -5
- data/templates/components/date_picker/date_picker_component.rb +2 -1
- data/templates/components/dialog/dialog_component.html.erb +9 -5
- data/templates/components/dialog/dialog_component.rb +11 -0
- data/templates/components/dropdown_menu/dropdown_menu_component.html.erb +6 -3
- data/templates/components/dropdown_menu/dropdown_menu_component.rb +57 -5
- 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/hover_card/hover_card_component.html.erb +6 -3
- data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +11 -9
- data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +11 -3
- data/templates/components/link/link_component.html.erb +1 -1
- data/templates/components/native_select/native_select_component.html.erb +2 -2
- data/templates/components/native_select/native_select_component.rb +12 -3
- data/templates/components/pagination/pagination_component.html.erb +4 -4
- data/templates/components/pagination/pagination_component.rb +2 -1
- data/templates/components/popover/popover_component.html.erb +6 -3
- data/templates/components/product_card/product_card_component.html.erb +3 -3
- data/templates/components/product_card/product_card_component.rb +2 -2
- data/templates/components/progress/progress_component.html.erb +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.html.erb +2 -2
- data/templates/components/search_input/search_input_component.rb +2 -1
- data/templates/components/sheet/sheet_component.html.erb +15 -6
- data/templates/components/sheet/sheet_component.rb +11 -0
- data/templates/components/shortcut_key/shortcut_key_component.html.erb +2 -2
- data/templates/components/sidebar/sidebar_component.html.erb +4 -4
- data/templates/components/sidebar/sidebar_component.rb +4 -1
- data/templates/components/switch/switch_component.html.erb +1 -1
- data/templates/components/table/table_component.html.erb +2 -2
- data/templates/components/tabs/tabs_component.html.erb +3 -3
- 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/tooltip/tooltip_component.html.erb +5 -3
- data/templates/components/top_nav/top_nav_component.html.erb +3 -3
- 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/cart_controller.js +3 -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
|
@@ -12,9 +12,15 @@
|
|
|
12
12
|
<li class="flex items-center gap-3 px-4 py-3"
|
|
13
13
|
data-senren--cart-target="line"
|
|
14
14
|
data-line-id="<%= item[:id] %>"
|
|
15
|
+
<%# Emitted so the `removed` event can carry it. The component
|
|
16
|
+
accepted remove_url: and normalised it, then never wrote it
|
|
17
|
+
anywhere -- an advertised input that reached nothing, so the
|
|
18
|
+
consumer listening for the event had no way to learn where to
|
|
19
|
+
send the delete. %>
|
|
20
|
+
<% if item[:remove_url].present? %>data-remove-url="<%= safe_url(item[:remove_url]) %>"<% end %>
|
|
15
21
|
data-price-cents="<%= item[:price_cents] %>">
|
|
16
22
|
<% if (image = safe_media_url(item[:image_url])) %>
|
|
17
|
-
|
|
23
|
+
<%= image_tag image, alt: "", loading: "lazy", class: "h-12 w-12 shrink-0 rounded-(--senren-radius) object-cover" %>
|
|
18
24
|
<% else %>
|
|
19
25
|
<div class="h-12 w-12 shrink-0 rounded-(--senren-radius) bg-[hsl(var(--senren-muted)/0.5)]" aria-hidden="true"></div>
|
|
20
26
|
<% end %>
|
|
@@ -24,10 +30,10 @@
|
|
|
24
30
|
<p class="text-sm text-[hsl(var(--senren-muted-foreground))]" data-senren--cart-target="lineTotal"><%= line_total(item) %></p>
|
|
25
31
|
</div>
|
|
26
32
|
|
|
27
|
-
<div class="flex items-center gap-1" role="group" aria-label="<%= item
|
|
33
|
+
<div class="flex items-center gap-1" role="group" aria-label="<%= quantity_label(item) %>">
|
|
28
34
|
<button type="button"
|
|
29
35
|
data-action="click->senren--cart#decrement"
|
|
30
|
-
aria-label="
|
|
36
|
+
aria-label="<%= decrease_label(item) %>"
|
|
31
37
|
class="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-(--senren-radius) border border-[hsl(var(--senren-border))] text-sm hover:bg-[hsl(var(--senren-accent))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))]">−</button>
|
|
32
38
|
|
|
33
39
|
<output class="w-8 text-center text-sm tabular-nums text-[hsl(var(--senren-foreground))]"
|
|
@@ -35,21 +41,21 @@
|
|
|
35
41
|
|
|
36
42
|
<button type="button"
|
|
37
43
|
data-action="click->senren--cart#increment"
|
|
38
|
-
aria-label="
|
|
44
|
+
aria-label="<%= increase_label(item) %>"
|
|
39
45
|
class="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-(--senren-radius) border border-[hsl(var(--senren-border))] text-sm hover:bg-[hsl(var(--senren-accent))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))]">+</button>
|
|
40
46
|
</div>
|
|
41
47
|
|
|
42
48
|
<button type="button"
|
|
43
49
|
data-action="click->senren--cart#remove"
|
|
44
|
-
aria-label="
|
|
45
|
-
class="inline-flex h-8 cursor-pointer items-center rounded-(--senren-radius) px-2 text-sm text-[hsl(var(--senren-muted-foreground))] hover:bg-[hsl(var(--senren-accent))] hover:text-[hsl(var(--senren-destructive))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))]"
|
|
50
|
+
aria-label="<%= remove_item_label(item) %>"
|
|
51
|
+
class="inline-flex h-8 cursor-pointer items-center rounded-(--senren-radius) px-2 text-sm text-[hsl(var(--senren-muted-foreground))] hover:bg-[hsl(var(--senren-accent))] hover:text-[hsl(var(--senren-destructive))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))]"><%= remove_label %></button>
|
|
46
52
|
</li>
|
|
47
53
|
<% end %>
|
|
48
54
|
</ul>
|
|
49
55
|
|
|
50
56
|
<footer class="flex items-center justify-between gap-3 border-t border-[hsl(var(--senren-border))] px-4 py-3">
|
|
51
57
|
<div class="text-sm text-[hsl(var(--senren-muted-foreground))]">
|
|
52
|
-
|
|
58
|
+
<%= subtotal_label %>
|
|
53
59
|
<span class="ml-1 font-semibold text-[hsl(var(--senren-foreground))]" data-senren--cart-target="subtotal"><%= subtotal %></span>
|
|
54
60
|
</div>
|
|
55
61
|
|
|
@@ -22,8 +22,9 @@ module Senren
|
|
|
22
22
|
|
|
23
23
|
SIZES = { md: '' }.freeze
|
|
24
24
|
|
|
25
|
-
def initialize(items: [], currency: '$', title: '
|
|
26
|
-
|
|
25
|
+
def initialize(items: [], currency: '$', title: senren_t('cart.title', default: 'Cart'),
|
|
26
|
+
empty_text: senren_t('cart.empty', default: 'Your cart is empty.'),
|
|
27
|
+
checkout_url: nil, checkout_label: senren_t('cart.checkout', default: 'Checkout'), variant: :default,
|
|
27
28
|
id: nil, class_name: nil, **html)
|
|
28
29
|
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
29
30
|
@items = Array(items).map { |item| normalize_item(item) }
|
|
@@ -37,6 +38,14 @@ module Senren
|
|
|
37
38
|
|
|
38
39
|
attr_reader :items, :currency, :title, :empty_text, :checkout_label, :dom_id
|
|
39
40
|
|
|
41
|
+
def subtotal_label = senren_t('cart.subtotal', default: 'Subtotal')
|
|
42
|
+
def remove_label = senren_t('cart.remove', default: 'Remove')
|
|
43
|
+
|
|
44
|
+
def quantity_label(item) = senren_t('cart.quantity_group', default: '%{name} quantity', name: item[:name])
|
|
45
|
+
def decrease_label(item) = senren_t('cart.decrease', default: 'Decrease %{name} quantity', name: item[:name])
|
|
46
|
+
def increase_label(item) = senren_t('cart.increase', default: 'Increase %{name} quantity', name: item[:name])
|
|
47
|
+
def remove_item_label(item) = senren_t('cart.remove_item', default: 'Remove %{name}', name: item[:name])
|
|
48
|
+
|
|
40
49
|
def empty? = items.empty?
|
|
41
50
|
def total_quantity = items.sum { |item| item[:quantity] }
|
|
42
51
|
def subtotal_cents = items.sum { |item| item[:price_cents] * item[:quantity] }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
<input type="text" readonly aria-label="<%= label %>" value="<%= value %>" data-senren--clipboard-target="source" class="h-10 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-muted)/0.4)] px-3 font-mono text-sm text-[hsl(var(--senren-foreground))]">
|
|
1
|
+
<%= tag.div(**root_attrs("inline-flex items-center gap-2", data: { controller: "senren--clipboard", "senren--clipboard-copied-label-value": copied_label, "senren--clipboard-copied-status-value": copied_status })) do %>
|
|
2
|
+
<input type="text" readonly autocomplete="off" aria-label="<%= label %>" value="<%= value %>" data-senren--clipboard-target="source" class="h-10 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-muted)/0.4)] px-3 font-mono text-sm text-[hsl(var(--senren-foreground))]">
|
|
3
3
|
<button type="button" data-senren--clipboard-target="button" data-action="click->senren--clipboard#copy" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) bg-[hsl(var(--senren-primary))] px-3 text-sm font-medium text-[hsl(var(--senren-primary-foreground))] hover:opacity-90">
|
|
4
4
|
<%= label %>
|
|
5
5
|
</button>
|
|
6
6
|
<span class="sr-only" aria-live="polite" data-senren--clipboard-target="status"></span>
|
|
7
|
-
|
|
7
|
+
<% end %>
|
|
@@ -5,7 +5,8 @@ module Senren
|
|
|
5
5
|
VARIANTS = { default: '' }.freeze
|
|
6
6
|
SIZES = { md: '' }.freeze
|
|
7
7
|
|
|
8
|
-
def initialize(value:, label: '
|
|
8
|
+
def initialize(value:, label: senren_t('clipboard.copy', default: 'Copy'),
|
|
9
|
+
copied_label: senren_t('clipboard.copied', default: 'Copied'), class_name: nil, **html)
|
|
9
10
|
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
10
11
|
@label = label
|
|
11
12
|
@value = value
|
|
@@ -13,5 +14,9 @@ module Senren
|
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
attr_reader :label, :value, :copied_label
|
|
17
|
+
|
|
18
|
+
# What a screen reader is told once the copy has happened. Passed to the
|
|
19
|
+
# controller as a Stimulus value so no sentence lives in JavaScript.
|
|
20
|
+
def copied_status = senren_t('clipboard.copied_status', default: 'Copied to clipboard')
|
|
16
21
|
end
|
|
17
22
|
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
<%= tag.div(**root_attrs("rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))]", data: { controller: "senren--collapsible" })) do %>
|
|
2
2
|
<button type="button" class="flex w-full cursor-pointer items-center justify-between gap-4 px-4 py-3 text-left text-sm font-medium text-[hsl(var(--senren-card-foreground))]" aria-expanded="<%= open? %>" data-senren--collapsible-target="trigger" data-action="click->senren--collapsible#toggle">
|
|
3
3
|
<span><%= trigger || title %></span>
|
|
4
4
|
<span aria-hidden="true" class="text-[hsl(var(--senren-muted-foreground))]">+</span>
|
|
5
5
|
</button>
|
|
6
|
-
<div data-senren--collapsible-target="panel"
|
|
6
|
+
<div data-senren--collapsible-target="panel" <% unless open? %>hidden<% end %> class="border-t border-[hsl(var(--senren-border))] px-4 py-3 text-sm text-[hsl(var(--senren-muted-foreground))]">
|
|
7
7
|
<%= body || content %>
|
|
8
8
|
</div>
|
|
9
|
-
|
|
9
|
+
<% end %>
|
|
@@ -6,7 +6,7 @@ module Senren
|
|
|
6
6
|
VARIANTS = { default: '' }.freeze
|
|
7
7
|
SIZES = { md: '' }.freeze
|
|
8
8
|
|
|
9
|
-
def initialize(title: 'Details', open: false, class_name: nil, **html)
|
|
9
|
+
def initialize(title: senren_t('collapsible.details', default: 'Details'), open: false, class_name: nil, **html)
|
|
10
10
|
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
11
11
|
@title = title
|
|
12
12
|
@open = open
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
<%= tag.div(**root_attrs("relative w-full", data: { controller: "senren--combobox" })) do %>
|
|
2
2
|
<input type="hidden" name="<%= name %>" value="<%= value %>" data-senren--combobox-target="value">
|
|
3
3
|
<button type="button" class="group flex h-10 w-full cursor-pointer items-center justify-between rounded-(--senren-radius) border bg-[hsl(var(--senren-background))] px-3 text-left text-sm text-[hsl(var(--senren-foreground))] transition-colors hover:bg-[hsl(var(--senren-muted)/0.35)] <%= self.class::VARIANTS[variant] %>" aria-haspopup="listbox" aria-expanded="false" data-state="closed" data-senren--combobox-target="button" data-action="click->senren--combobox#toggle">
|
|
4
4
|
<span data-senren--combobox-target="label"><%= selected_label || placeholder %></span>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
</svg>
|
|
8
8
|
</button>
|
|
9
9
|
<div data-senren--combobox-target="panel" hidden class="absolute z-50 mt-2 w-full rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] p-2 shadow-md">
|
|
10
|
-
<input type="text" placeholder="<%= placeholder %>" data-senren--combobox-target="search" data-action="input->senren--combobox#filter keydown->senren--combobox#onKey" class="mb-2 h-9 w-full rounded-md border border-[hsl(var(--senren-input))] bg-[hsl(var(--senren-background))] px-2 text-sm outline-none focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
|
|
10
|
+
<input type="text" autocomplete="off" placeholder="<%= placeholder %>" data-senren--combobox-target="search" data-action="input->senren--combobox#filter keydown->senren--combobox#onKey" class="mb-2 h-9 w-full rounded-md border border-[hsl(var(--senren-input))] bg-[hsl(var(--senren-background))] px-2 text-sm outline-none focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
|
|
11
11
|
<div role="listbox" class="max-h-56 overflow-auto">
|
|
12
12
|
<% options.each do |option| %>
|
|
13
13
|
<button type="button" role="option" data-senren--combobox-target="option" data-value="<%= option[:value] %>" data-label="<%= option[:label] %>" data-action="click->senren--combobox#choose" class="block w-full cursor-pointer rounded-md px-2 py-2 text-left text-sm text-[hsl(var(--senren-popover-foreground))] hover:bg-[hsl(var(--senren-accent))]">
|
|
@@ -16,4 +16,4 @@
|
|
|
16
16
|
<% end %>
|
|
17
17
|
</div>
|
|
18
18
|
</div>
|
|
19
|
-
|
|
19
|
+
<% end %>
|
|
@@ -8,7 +8,8 @@ module Senren
|
|
|
8
8
|
}.freeze
|
|
9
9
|
SIZES = { md: '' }.freeze
|
|
10
10
|
|
|
11
|
-
def initialize(name:, options:, value: nil, placeholder: '
|
|
11
|
+
def initialize(name:, options:, value: nil, placeholder: senren_t('combobox.placeholder', default: 'Search...'),
|
|
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
|
@options = normalize_options(options)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<% else %>
|
|
5
5
|
<label for="<%= dom_id %>-input" class="sr-only"><%= label %></label>
|
|
6
6
|
<div class="border-b border-[hsl(var(--senren-border))] p-2">
|
|
7
|
-
<input id="<%= dom_id %>-input" type="search" autocomplete="off" placeholder="<%= placeholder %>" aria-label="<%= label %>" aria-controls="<%= dom_id %>-list"
|
|
7
|
+
<input id="<%= dom_id %>-input" type="search" autocomplete="off" placeholder="<%= placeholder %>" aria-label="<%= label %>" aria-controls="<%= dom_id %>-list" data-senren--command-target="input" data-action="input->senren--command#filter keydown->senren--command#onKey" class="h-10 w-full rounded-(--senren-radius) bg-[hsl(var(--senren-background))] px-3 text-sm text-[hsl(var(--senren-foreground))] outline-none placeholder:text-[hsl(var(--senren-muted-foreground))] focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
|
|
8
8
|
</div>
|
|
9
9
|
<div id="<%= dom_id %>-list" role="listbox" aria-label="<%= label %>" data-senren--command-target="list" class="max-h-72 overflow-y-auto p-1.5">
|
|
10
10
|
<% items.each_with_index do |item, index| %>
|
|
@@ -5,7 +5,9 @@ module Senren
|
|
|
5
5
|
VARIANTS = { default: '' }.freeze
|
|
6
6
|
SIZES = { md: '' }.freeze
|
|
7
7
|
|
|
8
|
-
def initialize(items: [], placeholder: '
|
|
8
|
+
def initialize(items: [], placeholder: senren_t('command.placeholder', default: 'Type a command...'),
|
|
9
|
+
label: senren_t('command.label', default: 'Command menu'),
|
|
10
|
+
empty_text: senren_t('command.empty', default: 'No results found.'),
|
|
9
11
|
id: nil, class_name: nil, **html)
|
|
10
12
|
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
11
13
|
@placeholder = placeholder
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
<%# Root goes through root_attrs so a caller's class: and data: reach it.
|
|
2
|
+
Hand-writing these attributes dropped both -- which is why a Stimulus
|
|
3
|
+
value could not be set from the server. %>
|
|
4
|
+
<%= tag.div(**wrapper_attrs("relative", data: { controller: "senren--context-menu" })) do %>
|
|
2
5
|
<div data-senren--context-menu-target="trigger" data-action="contextmenu->senren--context-menu#open">
|
|
3
6
|
<%= trigger %>
|
|
4
7
|
</div>
|
|
5
8
|
<div data-senren--context-menu-target="menu" role="menu" hidden
|
|
6
|
-
class="absolute z-50 w-56 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-1 shadow-md">
|
|
9
|
+
class="<%= panel_class("absolute z-50 w-56 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-1 shadow-md") %>">
|
|
7
10
|
<% items.each do |it| %>
|
|
8
11
|
<%= it %>
|
|
9
12
|
<% end %>
|
|
10
13
|
</div>
|
|
11
|
-
|
|
14
|
+
<% end %>
|
|
@@ -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
|
-
|
|
2
|
-
<input type="date" 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))]"
|
|
5
|
-
|
|
1
|
+
<%= tag.div(**root_attrs("flex w-full items-center gap-2", data: { controller: "senren--date-picker" })) do %>
|
|
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))]"><%= 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
|
+
<% 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
|
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
<%# Root goes through root_attrs so a caller's class: and data: reach it.
|
|
2
|
+
Hand-writing these attributes dropped both -- which is why a Stimulus
|
|
3
|
+
value could not be set from the server. %>
|
|
4
|
+
<%= tag.div(**wrapper_attrs(id: dom_id, data: { controller: "senren--dialog", "senren--dialog-open-value": open })) do %>
|
|
2
5
|
<% if trigger? %>
|
|
3
6
|
<span data-action="click->senren--dialog#open" data-senren--dialog-target="trigger" aria-controls="<%= dom_id %>-panel">
|
|
4
7
|
<%= trigger %>
|
|
5
8
|
</span>
|
|
6
9
|
<% else %>
|
|
7
10
|
<button type="button" data-action="click->senren--dialog#open"
|
|
8
|
-
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>
|
|
9
12
|
<% end %>
|
|
10
13
|
|
|
11
14
|
<div data-senren--dialog-target="overlay" hidden
|
|
15
|
+
data-action="click->senren--dialog#close"
|
|
12
16
|
class="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm"></div>
|
|
13
17
|
|
|
14
18
|
<div data-senren--dialog-target="panel" hidden
|
|
15
19
|
role="dialog" aria-modal="true" aria-labelledby="<%= dom_id %>-title"
|
|
16
20
|
id="<%= dom_id %>-panel"
|
|
17
21
|
tabindex="-1"
|
|
18
|
-
class="fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-6 shadow-lg focus:outline-none">
|
|
22
|
+
class="<%= panel_class("fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-6 shadow-lg focus:outline-none") %>">
|
|
19
23
|
<% if title? %>
|
|
20
24
|
<h2 id="<%= dom_id %>-title" class="text-lg font-semibold leading-none tracking-tight"><%= title %></h2>
|
|
21
25
|
<% end %>
|
|
@@ -30,9 +34,9 @@
|
|
|
30
34
|
<% end %>
|
|
31
35
|
|
|
32
36
|
<button type="button" data-action="click->senren--dialog#close"
|
|
33
|
-
aria-label="Close"
|
|
37
|
+
aria-label="<%= senren_t('shared.close', default: 'Close') %>"
|
|
34
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))]">
|
|
35
39
|
<span aria-hidden="true">×</span>
|
|
36
40
|
</button>
|
|
37
41
|
</div>
|
|
38
|
-
|
|
42
|
+
<% end %>
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Senren
|
|
4
|
+
# Two ancestor traps to know about, because neither is visible in this file
|
|
5
|
+
# and both look like a z-index bug from the outside:
|
|
6
|
+
#
|
|
7
|
+
# * A `z-index` on any positioned ancestor creates a stacking context, and
|
|
8
|
+
# a fixed overlay cannot escape one. The panel then competes only inside
|
|
9
|
+
# that context, so a sticky header with a lower z-index still covers it.
|
|
10
|
+
# `position: relative` alone is fine; it is the z-index that traps.
|
|
11
|
+
# * `transform`, `filter`, `backdrop-filter`, `will-change` and an opacity
|
|
12
|
+
# below 1 create a stacking context too, with the same effect.
|
|
13
|
+
#
|
|
14
|
+
# Symptom in both cases: raising the panel's z-index changes nothing.
|
|
4
15
|
class DialogComponent < BaseComponent
|
|
5
16
|
renders_one :trigger
|
|
6
17
|
renders_one :title
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
<%# Root goes through root_attrs so a caller's class: and data: reach it.
|
|
2
|
+
Hand-writing these attributes dropped both -- which is why a Stimulus
|
|
3
|
+
value could not be set from the server. %>
|
|
4
|
+
<%= tag.div(**wrapper_attrs("relative inline-block", data: { controller: "senren--dropdown-menu", state: "closed" })) do %>
|
|
2
5
|
<div class="inline-flex cursor-pointer" data-state="closed" aria-haspopup="menu" aria-expanded="false" data-senren--dropdown-menu-target="trigger" data-action="click->senren--dropdown-menu#toggle keydown->senren--dropdown-menu#onTriggerKey">
|
|
3
6
|
<%= trigger %>
|
|
4
7
|
</div>
|
|
5
8
|
|
|
6
9
|
<div data-senren--dropdown-menu-target="menu" role="menu" hidden
|
|
7
|
-
class="absolute right-0 z-50 mt-2 w-56 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-1 shadow-md">
|
|
10
|
+
class="<%= panel_class("absolute right-0 z-50 mt-2 w-56 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-1 shadow-md") %>">
|
|
8
11
|
<% items.each do |it| %>
|
|
9
12
|
<%= it %>
|
|
10
13
|
<% end %>
|
|
11
14
|
</div>
|
|
12
|
-
|
|
15
|
+
<% end %>
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Senren
|
|
4
|
+
# A trigger and an absolutely positioned menu.
|
|
5
|
+
#
|
|
6
|
+
# The menu is taller than the trigger and escapes it, so an ancestor with
|
|
7
|
+
# `overflow-hidden` will clip it -- usually a card, table wrapper, or list
|
|
8
|
+
# container that has the class only to make its border radius clip children.
|
|
9
|
+
# Symptom: the menu opens but is cut off at the container's edge. Remove the
|
|
10
|
+
# overflow, or round the first and last rows instead of the wrapper.
|
|
4
11
|
class DropdownMenuComponent < BaseComponent
|
|
5
12
|
renders_one :trigger
|
|
6
13
|
renders_many :items, 'ItemTag'
|
|
@@ -17,6 +24,10 @@ module Senren
|
|
|
17
24
|
SIZES = { md: '' }.freeze
|
|
18
25
|
ITEM_ACTION = 'click->senren--dropdown-menu#close keydown->senren--dropdown-menu#onItemKey'
|
|
19
26
|
|
|
27
|
+
BASE_CLASSES = 'block w-full text-left px-3 py-2 text-sm rounded-sm ' \
|
|
28
|
+
'hover:bg-[hsl(var(--senren-accent))] focus:bg-[hsl(var(--senren-accent))] ' \
|
|
29
|
+
'outline-none cursor-pointer'
|
|
30
|
+
|
|
20
31
|
def initialize(href: nil, method: nil, destructive: false, class_name: nil, **)
|
|
21
32
|
super(variant: :default, size: :md, class_name: class_name, **)
|
|
22
33
|
@href = href
|
|
@@ -25,15 +36,56 @@ module Senren
|
|
|
25
36
|
end
|
|
26
37
|
|
|
27
38
|
def call
|
|
28
|
-
klass = 'block w-full text-left px-3 py-2 text-sm rounded-sm hover:bg-[hsl(var(--senren-accent))] focus:bg-[hsl(var(--senren-accent))] outline-none cursor-pointer'
|
|
29
|
-
klass += " #{class_name}" if class_name.present?
|
|
30
|
-
klass += ' text-[hsl(var(--senren-destructive))]' if @destructive
|
|
31
39
|
if @href
|
|
32
|
-
link_to(content, safe_url(@href), role: 'menuitem',
|
|
40
|
+
link_to(content, safe_url(@href), role: 'menuitem', class: item_classes, **item_attrs)
|
|
33
41
|
else
|
|
34
|
-
tag.button(content, type: 'button', role: 'menuitem', class:
|
|
42
|
+
tag.button(content, type: 'button', role: 'menuitem', class: item_classes, **item_attrs)
|
|
35
43
|
end
|
|
36
44
|
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# `class:` is merged, not substituted.
|
|
49
|
+
#
|
|
50
|
+
# It used to reach `call` only through **html_attrs, which the tag helper
|
|
51
|
+
# splatted over the computed class -- so `class: "font-bold"` produced an
|
|
52
|
+
# item wearing nothing but font-bold. Losing the hover style is cosmetic;
|
|
53
|
+
# losing `focus:bg-` is not, because it is the only indication a keyboard
|
|
54
|
+
# user has of where they are while arrowing through the menu.
|
|
55
|
+
#
|
|
56
|
+
# Same defect `data:` had, and the fix for `data:` was not extended here.
|
|
57
|
+
def item_classes
|
|
58
|
+
[
|
|
59
|
+
BASE_CLASSES,
|
|
60
|
+
('text-[hsl(var(--senren-destructive))]' if @destructive),
|
|
61
|
+
class_name.presence,
|
|
62
|
+
caller_class.presence
|
|
63
|
+
].compact.join(' ')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Two defects lived in the old one-line version of `call`.
|
|
67
|
+
#
|
|
68
|
+
# It wrote `data: { action: ITEM_ACTION }` and then splatted html_attrs
|
|
69
|
+
# after it, so any caller passing `data:` replaced the whole hash and
|
|
70
|
+
# silently lost close-on-click and keyboard handling. This is the same
|
|
71
|
+
# defect `root_attrs` had for `data:`, fixed there and never here --
|
|
72
|
+
# merging is the fix in both places.
|
|
73
|
+
#
|
|
74
|
+
# And `method:` was passed to link_to, which was a rails-ujs option.
|
|
75
|
+
# Rails 7 dropped rails-ujs, so it rendered a `method` attribute on an
|
|
76
|
+
# `<a>` that does nothing at all: a documented parameter that had been
|
|
77
|
+
# inert since the library targeted Rails 7.1. Turbo reads
|
|
78
|
+
# data-turbo-method -- and only on a link, which is why `method:` without
|
|
79
|
+
# `href:` emits nothing rather than an attribute a <button> cannot act on.
|
|
80
|
+
#
|
|
81
|
+
# A caller's own action is appended rather than dropped: an item that
|
|
82
|
+
# tracks a click still has to close its menu.
|
|
83
|
+
def item_attrs
|
|
84
|
+
data = merge_data(caller_data, { action: ITEM_ACTION })
|
|
85
|
+
data = data.merge(turbo_method: @method) if @method && @href
|
|
86
|
+
|
|
87
|
+
html_attrs_without_class_and_data.merge(data: data)
|
|
88
|
+
end
|
|
37
89
|
end
|
|
38
90
|
end
|
|
39
91
|
end
|
|
@@ -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 —
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
<%# Root goes through root_attrs so a caller's class: and data: reach it.
|
|
2
|
+
Hand-writing these attributes dropped both -- which is why a Stimulus
|
|
3
|
+
value could not be set from the server. %>
|
|
4
|
+
<%= tag.span(**wrapper_attrs("relative inline-block", data: { controller: "senren--hover-card" })) do %>
|
|
2
5
|
<span tabindex="0"
|
|
3
6
|
data-action="mouseenter->senren--hover-card#show mouseleave->senren--hover-card#hide focus->senren--hover-card#show blur->senren--hover-card#hide">
|
|
4
7
|
<%= trigger %>
|
|
5
8
|
</span>
|
|
6
9
|
<span hidden data-senren--hover-card-target="panel"
|
|
7
|
-
class="absolute z-40 mt-2 w-64 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-4 shadow-md">
|
|
10
|
+
class="<%= panel_class("absolute z-40 mt-2 w-64 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-4 shadow-md") %>">
|
|
8
11
|
<%= content_panel || content %>
|
|
9
12
|
</span>
|
|
10
|
-
|
|
13
|
+
<% end %>
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
</button>
|
|
8
8
|
<% end %>
|
|
9
9
|
|
|
10
|
-
<div hidden data-senren--invite-member-dialog-target="overlay"
|
|
10
|
+
<div hidden data-senren--invite-member-dialog-target="overlay"
|
|
11
|
+
data-action="click->senren--invite-member-dialog#close" class="fixed inset-0 z-40 bg-[hsl(var(--senren-foreground)/0.42)] backdrop-blur-sm"></div>
|
|
11
12
|
<div hidden id="<%= dom_id %>" data-senren--invite-member-dialog-target="panel" role="dialog" aria-modal="true" aria-labelledby="<%= dom_id %>-title" tabindex="-1" class="fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] p-6 text-[hsl(var(--senren-popover-foreground))] shadow-lg">
|
|
12
13
|
<h2 id="<%= dom_id %>-title" class="font-display text-lg font-semibold"><%= title %></h2>
|
|
13
14
|
<% if description.present? %>
|
|
@@ -18,18 +19,19 @@
|
|
|
18
19
|
<% if content? %>
|
|
19
20
|
<%= content %>
|
|
20
21
|
<% else %>
|
|
21
|
-
<label class="block text-sm font-medium text-[hsl(var(--senren-foreground))]" for="<%= dom_id %>-email"
|
|
22
|
-
<input id="<%= dom_id %>-email" name="<%= email_name %>" type="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))]">
|
|
23
|
-
<label class="block text-sm font-medium text-[hsl(var(--senren-foreground))]" for="<%= dom_id %>-role"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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) %>
|
|
27
29
|
<% end %>
|
|
28
30
|
</div>
|
|
29
31
|
|
|
30
32
|
<div class="mt-6 flex justify-end gap-2">
|
|
31
|
-
<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"
|
|
32
|
-
<% 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 %>
|
|
33
35
|
</div>
|
|
34
36
|
</div>
|
|
35
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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<%= tag.a content, **root_attrs("inline-flex items-center gap-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))] rounded-sm",
|
|
1
|
+
<%= tag.a content, href: safe_url(href), **root_attrs("inline-flex items-center gap-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))] rounded-sm", **external_attrs) %>
|