senren-ui 0.1.6 → 0.3.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 +215 -0
- data/CONTRIBUTING.md +41 -3
- data/README.md +138 -33
- data/Rakefile +14 -1
- data/docs/components.md +10 -10
- data/docs/hot_reload.md +103 -0
- data/docs/performance_testing.md +7 -3
- data/docs/visual_style.md +80 -0
- data/lib/generators/senren/component/templates/controller.js.tt +7 -4
- data/lib/generators/senren/install/install_generator.rb +77 -0
- data/lib/generators/senren/install/templates/base_component.rb.tt +142 -4
- data/lib/generators/senren/install/templates/conventions.md.tt +57 -1
- data/lib/generators/senren/install/templates/senren_themes.css.tt +339 -0
- data/lib/senren/rails/agent_rules_writer.rb +65 -19
- data/lib/senren/rails/asset_path_guard.rb +128 -0
- data/lib/senren/rails/base_component_patch.rb +64 -0
- data/lib/senren/rails/component_copier.rb +101 -50
- data/lib/senren/rails/component_installer.rb +26 -0
- data/lib/senren/rails/doctor.rb +7 -4
- data/lib/senren/rails/engine.rb +23 -0
- data/lib/senren/rails/host_paths.rb +11 -2
- data/lib/senren/rails/marker_block.rb +81 -0
- data/lib/senren/rails/registry.rb +10 -4
- data/lib/senren/rails/safe_write.rb +169 -0
- data/lib/senren/rails/skill_writer.rb +47 -11
- data/lib/senren/rails/version.rb +1 -1
- data/lib/senren/rails.rb +1 -1
- data/lib/senren-ui.rb +15 -0
- data/lib/tasks/senren.rake +43 -17
- data/registry/components.yml +45 -0
- data/registry/recipes.yml +12 -0
- data/templates/components/accordion/accordion_component.html.erb +3 -3
- data/templates/components/alert_dialog/alert_dialog_component.html.erb +6 -3
- data/templates/components/alert_dialog/alert_dialog_component.rb +5 -1
- data/templates/components/api_key_field/api_key_field_component.html.erb +1 -1
- data/templates/components/aspect_ratio/aspect_ratio_component.rb +7 -0
- data/templates/components/avatar/avatar_component.rb +8 -1
- data/templates/components/breadcrumb/breadcrumb_component.html.erb +8 -6
- 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/card/card_component.html.erb +4 -6
- data/templates/components/carousel/carousel_component.html.erb +1 -1
- data/templates/components/cart/cart_component.html.erb +67 -0
- data/templates/components/cart/cart_component.rb +71 -0
- data/templates/components/checkbox/checkbox_component.rb +1 -1
- data/templates/components/clipboard/clipboard_component.html.erb +3 -3
- data/templates/components/collapsible/collapsible_component.html.erb +3 -3
- data/templates/components/combobox/combobox_component.html.erb +3 -3
- data/templates/components/command/command_component.html.erb +1 -1
- data/templates/components/command/command_component.rb +1 -1
- data/templates/components/context_menu/context_menu_component.html.erb +6 -3
- data/templates/components/date_picker/date_picker_component.html.erb +3 -3
- data/templates/components/dialog/dialog_component.html.erb +7 -3
- data/templates/components/dialog/dialog_component.rb +12 -1
- 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/form/form_component.rb +9 -1
- data/templates/components/hover_card/hover_card_component.html.erb +6 -3
- data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +3 -2
- data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +1 -1
- 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/pagination/pagination_component.html.erb +2 -2
- data/templates/components/popover/popover_component.html.erb +6 -3
- data/templates/components/product_card/product_card_component.html.erb +38 -0
- data/templates/components/product_card/product_card_component.rb +49 -0
- data/templates/components/progress/progress_component.html.erb +2 -2
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.html.erb +1 -1
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.rb +1 -1
- data/templates/components/search_input/search_input_component.html.erb +2 -2
- data/templates/components/separator/separator_component.rb +7 -0
- data/templates/components/sheet/sheet_component.html.erb +13 -4
- data/templates/components/sheet/sheet_component.rb +12 -1
- data/templates/components/shortcut_key/shortcut_key_component.html.erb +2 -2
- data/templates/components/sidebar/sidebar_component.html.erb +2 -2
- 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/tooltip/tooltip_component.html.erb +5 -3
- data/templates/components/tooltip/tooltip_component.rb +2 -2
- data/templates/components/top_nav/top_nav_component.html.erb +2 -2
- data/templates/components/typography/typography_component.rb +7 -0
- data/templates/controllers/accordion_controller.js +1 -1
- data/templates/controllers/alert_dialog_controller.js +31 -7
- data/templates/controllers/cart_controller.js +85 -0
- data/templates/controllers/clipboard_controller.js +12 -1
- data/templates/controllers/command_controller.js +3 -4
- data/templates/controllers/context_menu_controller.js +38 -11
- data/templates/controllers/data_table_controller.js +8 -3
- data/templates/controllers/dialog_controller.js +47 -21
- data/templates/controllers/dropdown_menu_controller.js +40 -27
- data/templates/controllers/hover_card_controller.js +8 -0
- data/templates/controllers/invite_member_dialog_controller.js +6 -0
- data/templates/controllers/masked_input_controller.js +8 -1
- data/templates/controllers/popover_controller.js +25 -10
- data/templates/controllers/rich_text_editor_lite_controller.js +165 -28
- data/templates/controllers/sheet_controller.js +41 -11
- metadata +14 -17
- data/lib/senren/rails/installer.rb +0 -85
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
# A shopping cart: line items with quantity steppers, a live subtotal, and
|
|
5
|
+
# removal.
|
|
6
|
+
#
|
|
7
|
+
# The server owns the cart. This component owns the widget. The Stimulus
|
|
8
|
+
# controller only does what does not round-trip pleasantly — stepping a
|
|
9
|
+
# quantity and recomputing the displayed subtotal while the user clicks — and
|
|
10
|
+
# announces the result with senren--cart:changed so a host app can update a
|
|
11
|
+
# header badge without reaching inside.
|
|
12
|
+
#
|
|
13
|
+
# Money arrives twice, and on purpose: `price` already formatted for display,
|
|
14
|
+
# and `price_cents` as an integer for arithmetic. Formatting is locale- and
|
|
15
|
+
# money-library-specific and belongs to the host app. Doing arithmetic on a
|
|
16
|
+
# formatted string is how currency bugs start.
|
|
17
|
+
class CartComponent < BaseComponent
|
|
18
|
+
VARIANTS = {
|
|
19
|
+
default: 'border-[hsl(var(--senren-border))]',
|
|
20
|
+
flush: 'border-transparent shadow-none'
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
SIZES = { md: '' }.freeze
|
|
24
|
+
|
|
25
|
+
def initialize(items: [], currency: '$', title: 'Cart', empty_text: 'Your cart is empty.',
|
|
26
|
+
checkout_url: nil, checkout_label: 'Checkout', variant: :default,
|
|
27
|
+
id: nil, class_name: nil, **html)
|
|
28
|
+
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
29
|
+
@items = Array(items).map { |item| normalize_item(item) }
|
|
30
|
+
@currency = currency
|
|
31
|
+
@title = title
|
|
32
|
+
@empty_text = empty_text
|
|
33
|
+
@checkout_url = checkout_url
|
|
34
|
+
@checkout_label = checkout_label
|
|
35
|
+
@dom_id = id || senren_dom_id(title)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
attr_reader :items, :currency, :title, :empty_text, :checkout_label, :dom_id
|
|
39
|
+
|
|
40
|
+
def empty? = items.empty?
|
|
41
|
+
def total_quantity = items.sum { |item| item[:quantity] }
|
|
42
|
+
def subtotal_cents = items.sum { |item| item[:price_cents] * item[:quantity] }
|
|
43
|
+
def subtotal = format_cents(subtotal_cents)
|
|
44
|
+
def line_total(item) = format_cents(item[:price_cents] * item[:quantity])
|
|
45
|
+
def safe_checkout_url = @checkout_url && safe_url(@checkout_url)
|
|
46
|
+
|
|
47
|
+
# Kept public so the same rendering is available to a host app updating a
|
|
48
|
+
# line through a Turbo Stream.
|
|
49
|
+
# Kernel.format explicitly: ViewComponent::Base defines its own `format`
|
|
50
|
+
# (the current template format, taking no arguments), which shadows
|
|
51
|
+
# Kernel#format inside a component. On Rails 8.x this happened to resolve;
|
|
52
|
+
# on 7.1 it raised "wrong number of arguments (given 2, expected 0)" as soon
|
|
53
|
+
# as the call ran inside a tag block. Caught by the version matrix.
|
|
54
|
+
def format_cents(cents)
|
|
55
|
+
"#{currency}#{Kernel.format('%.2f', cents.to_i / 100.0)}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def normalize_item(item)
|
|
61
|
+
{
|
|
62
|
+
id: item[:id] || item['id'],
|
|
63
|
+
name: item[:name] || item['name'],
|
|
64
|
+
price_cents: (item[:price_cents] || item['price_cents']).to_i,
|
|
65
|
+
quantity: [(item[:quantity] || item['quantity'] || 1).to_i, 1].max,
|
|
66
|
+
image_url: item[:image_url] || item['image_url'],
|
|
67
|
+
remove_url: item[:remove_url] || item['remove_url']
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
<input type="text" readonly value="<%= value %>" data-senren--clipboard-target="source" class="h-10 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-muted)/0.4)] px-3 font-mono text-sm text-[hsl(var(--senren-foreground))]">
|
|
1
|
+
<%= tag.div(**root_attrs("inline-flex items-center gap-2", data: { controller: "senren--clipboard", "senren--clipboard-copied-label-value": copied_label })) 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 %>
|
|
@@ -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 %>
|
|
@@ -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 %>
|
|
@@ -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| %>
|
|
@@ -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,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
<input type="date" id="<%= id %>" name="<%= name %>" value="<%= value %>" placeholder="<%= placeholder %>" data-senren--date-picker-target="input" class="
|
|
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
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))]">Today</button>
|
|
4
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))]">Clear</button>
|
|
5
|
-
|
|
5
|
+
<% end %>
|
|
@@ -1,4 +1,7 @@
|
|
|
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 %>
|
|
@@ -9,13 +12,14 @@
|
|
|
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 %>
|
|
@@ -35,4 +39,4 @@
|
|
|
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
|
|
@@ -14,7 +25,7 @@ module Senren
|
|
|
14
25
|
def initialize(open: false, id: nil, class_name: nil, **html)
|
|
15
26
|
super(variant: :default, size: :md, class_name: class_name, **html)
|
|
16
27
|
@open = open
|
|
17
|
-
@dom_id = id ||
|
|
28
|
+
@dom_id = id || senren_dom_id
|
|
18
29
|
end
|
|
19
30
|
|
|
20
31
|
attr_reader :open, :dom_id
|
|
@@ -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
|
|
@@ -15,6 +15,14 @@ module Senren
|
|
|
15
15
|
@multipart = multipart
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
attr_reader :model, :
|
|
18
|
+
attr_reader :model, :method, :multipart
|
|
19
|
+
|
|
20
|
+
# This one reaches form_with's `action`, which makes it the highest-value
|
|
21
|
+
# URL sink in the library: `//evil.example` is a protocol-relative absolute
|
|
22
|
+
# URL, so a form built from user-controlled input POSTs every field —
|
|
23
|
+
# including the CSRF token — off-origin. Same policy as every other href in
|
|
24
|
+
# the library; it was simply missed because the security test enumerated
|
|
25
|
+
# known components rather than asserting a property over all of them.
|
|
26
|
+
def url = @url && safe_url(@url)
|
|
19
27
|
end
|
|
20
28
|
end
|
|
@@ -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? %>
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
<%= content %>
|
|
20
21
|
<% else %>
|
|
21
22
|
<label class="block text-sm font-medium text-[hsl(var(--senren-foreground))]" for="<%= dom_id %>-email">Email</label>
|
|
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
|
+
<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))]">
|
|
23
24
|
<label class="block text-sm font-medium text-[hsl(var(--senren-foreground))]" for="<%= dom_id %>-role">Role</label>
|
|
24
25
|
<select id="<%= dom_id %>-role" name="<%= role_name %>" 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))]">
|
|
25
26
|
<% roles.each do |role| %><option><%= role %></option><% end %>
|
|
@@ -18,7 +18,7 @@ module Senren
|
|
|
18
18
|
@role_name = role_name
|
|
19
19
|
@roles = Array(roles)
|
|
20
20
|
@button_label = button_label
|
|
21
|
-
@dom_id = id ||
|
|
21
|
+
@dom_id = id || senren_dom_id(title)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
attr_reader :title, :description, :email_name, :role_name, :roles, :button_label, :dom_id
|
|
@@ -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) %>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<% end %>
|
|
7
7
|
<% options.each do |opt| %>
|
|
8
8
|
<% value, label = Array === opt ? opt : [opt, opt] %>
|
|
9
|
-
<option value="<%= value %>"
|
|
9
|
+
<option value="<%= value %>" <% if selected.to_s == value.to_s %>selected<% end %>><%= label %></option>
|
|
10
10
|
<% end %>
|
|
11
11
|
<% end %>
|
|
12
12
|
<% else %>
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<% end %>
|
|
19
19
|
<% options.each do |opt| %>
|
|
20
20
|
<% value, label = Array === opt ? opt : [opt, opt] %>
|
|
21
|
-
<option value="<%= value %>"
|
|
21
|
+
<option value="<%= value %>" <% if selected.to_s == value.to_s %>selected<% end %>><%= label %></option>
|
|
22
22
|
<% end %>
|
|
23
23
|
<% end %>
|
|
24
24
|
<svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[hsl(var(--senren-muted-foreground))] transition-transform duration-150 group-focus-within:rotate-180">
|
|
@@ -1,4 +1,4 @@
|
|
|
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
3
|
<%= link_to "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
|
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
|
|
9
9
|
<% next_page = current_page + 1 %>
|
|
10
10
|
<%= link_to "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 %>
|
|
@@ -1,9 +1,12 @@
|
|
|
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--popover" })) do %>
|
|
2
5
|
<div data-senren--popover-target="trigger" data-action="click->senren--popover#toggle">
|
|
3
6
|
<%= trigger %>
|
|
4
7
|
</div>
|
|
5
8
|
<div data-senren--popover-target="panel" hidden role="dialog"
|
|
6
|
-
class="absolute z-40 mt-2 min-w-[12rem] rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-4 shadow-md">
|
|
9
|
+
class="<%= panel_class("absolute z-40 mt-2 min-w-[12rem] rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-4 shadow-md") %>">
|
|
7
10
|
<%= content_panel || content %>
|
|
8
11
|
</div>
|
|
9
|
-
|
|
12
|
+
<% end %>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<%= tag.article(**root_attrs("flex flex-col overflow-hidden rounded-(--senren-radius) border bg-[hsl(var(--senren-card))] shadow-sm", id: dom_id)) do %>
|
|
2
|
+
<div class="relative aspect-square w-full bg-[hsl(var(--senren-muted)/0.4)]">
|
|
3
|
+
<% if safe_image_url %>
|
|
4
|
+
<%= image_tag safe_image_url, alt: title, loading: "lazy", class: "h-full w-full object-cover" %>
|
|
5
|
+
<% else %>
|
|
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
|
|
8
|
+
</div>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<% if badge %>
|
|
12
|
+
<span class="absolute left-2 top-2 inline-flex items-center rounded-(--senren-radius) bg-[hsl(var(--senren-primary))] px-2 py-0.5 text-xs font-medium text-[hsl(var(--senren-primary-foreground))]">
|
|
13
|
+
<%= badge %>
|
|
14
|
+
</span>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="flex flex-1 flex-col gap-2 p-4">
|
|
19
|
+
<h3 class="text-sm font-medium leading-tight text-[hsl(var(--senren-foreground))]">
|
|
20
|
+
<%= title %>
|
|
21
|
+
</h3>
|
|
22
|
+
|
|
23
|
+
<% if description %>
|
|
24
|
+
<p class="line-clamp-2 text-sm text-[hsl(var(--senren-muted-foreground))]"><%= description %></p>
|
|
25
|
+
<% end %>
|
|
26
|
+
|
|
27
|
+
<p class="mt-auto text-base font-semibold text-[hsl(var(--senren-foreground))]"><%= price %></p>
|
|
28
|
+
|
|
29
|
+
<%# A plain form: the server owns the cart, and Turbo handles the response. %>
|
|
30
|
+
<%= form_with url: safe_action_url, method: method, class: "mt-1" do %>
|
|
31
|
+
<button type="submit"
|
|
32
|
+
<% unless available? %>disabled<% end %>
|
|
33
|
+
class="inline-flex h-10 w-full cursor-pointer items-center justify-center rounded-(--senren-radius) bg-[hsl(var(--senren-primary))] px-4 text-sm font-medium text-[hsl(var(--senren-primary-foreground))] transition-colors hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))] disabled:cursor-not-allowed disabled:opacity-50">
|
|
34
|
+
<%= button_label %>
|
|
35
|
+
</button>
|
|
36
|
+
<% end %>
|
|
37
|
+
</div>
|
|
38
|
+
<% end %>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Senren
|
|
4
|
+
# A product tile with an add-to-cart action.
|
|
5
|
+
#
|
|
6
|
+
# Adding to a cart is an ordinary form submission, so this component ships no
|
|
7
|
+
# Stimulus controller. That is deliberate: a listing page renders this tile
|
|
8
|
+
# many times, and a controller here would download JavaScript on a page that
|
|
9
|
+
# does not need any.
|
|
10
|
+
#
|
|
11
|
+
# Price is passed in already formatted. Currency formatting is locale- and
|
|
12
|
+
# money-library-specific and belongs to the host app; a UI library that
|
|
13
|
+
# guesses gets it wrong in another country.
|
|
14
|
+
class ProductCardComponent < BaseComponent
|
|
15
|
+
VARIANTS = {
|
|
16
|
+
default: 'border-[hsl(var(--senren-border))]',
|
|
17
|
+
featured: 'border-[hsl(var(--senren-primary))] ring-1 ring-[hsl(var(--senren-primary)/0.35)]'
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
SIZES = { md: '' }.freeze
|
|
21
|
+
|
|
22
|
+
def initialize(title:, price:, url:, image_url: nil, description: nil, badge: nil,
|
|
23
|
+
action_label: 'Add to cart', available: true, method: :post,
|
|
24
|
+
variant: :default, id: nil, class_name: nil, **html)
|
|
25
|
+
super(variant: variant, size: :md, class_name: class_name, **html)
|
|
26
|
+
@title = title
|
|
27
|
+
@price = price
|
|
28
|
+
@url = url
|
|
29
|
+
@image_url = image_url
|
|
30
|
+
@description = description
|
|
31
|
+
@badge = badge
|
|
32
|
+
@action_label = action_label
|
|
33
|
+
@available = available
|
|
34
|
+
@method = method
|
|
35
|
+
@dom_id = id || senren_dom_id(title)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
attr_reader :title, :price, :image_url, :description, :badge, :action_label, :method, :dom_id
|
|
39
|
+
|
|
40
|
+
def available? = @available == true
|
|
41
|
+
|
|
42
|
+
# Untrusted values reach href/src attributes, so both go through the shared
|
|
43
|
+
# guards rather than being interpolated directly.
|
|
44
|
+
def safe_action_url = safe_url(@url)
|
|
45
|
+
def safe_image_url = safe_media_url(@image_url)
|
|
46
|
+
|
|
47
|
+
def button_label = available? ? action_label : 'Out of stock'
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
<%= tag.div(**root_attrs("w-full")) do %>
|
|
2
2
|
<% if label.present? %>
|
|
3
3
|
<div class="mb-2 flex items-center justify-between text-sm">
|
|
4
4
|
<span class="font-medium text-[hsl(var(--senren-foreground))]"><%= label %></span>
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
<div role="progressbar" aria-valuemin="0" aria-valuemax="<%= max.to_i %>" aria-valuenow="<%= value.to_i %>" class="h-2.5 w-full overflow-hidden rounded-full bg-[hsl(var(--senren-muted))]">
|
|
9
9
|
<div class="h-full rounded-full transition-[width] duration-300 ease-out <%= indicator_class %>" style="width: <%= percent %>%"></div>
|
|
10
10
|
</div>
|
|
11
|
-
|
|
11
|
+
<% end %>
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
</div>
|
|
27
27
|
<% end %>
|
|
28
28
|
|
|
29
|
-
<div id="<%= dom_id %>-editor" role="textbox" aria-multiline="true" aria-label="<%= label %>" contenteditable="true" data-placeholder="<%= placeholder %>" data-senren--rich-text-editor-lite-target="editor" data-action="click->senren--rich-text-editor-lite#openLink input->senren--rich-text-editor-lite#sync input->senren--rich-text-editor-lite#rememberSelection keyup->senren--rich-text-editor-lite#rememberSelection keyup->senren--rich-text-editor-lite#updateToolbar mouseup->senren--rich-text-editor-lite#rememberSelection mouseup->senren--rich-text-editor-lite#updateToolbar touchend->senren--rich-text-editor-lite#rememberSelection paste->senren--rich-text-editor-lite#
|
|
29
|
+
<div id="<%= dom_id %>-editor" role="textbox" aria-multiline="true" aria-label="<%= label %>" contenteditable="true" data-placeholder="<%= placeholder %>" data-senren--rich-text-editor-lite-target="editor" data-action="click->senren--rich-text-editor-lite#openLink input->senren--rich-text-editor-lite#sync input->senren--rich-text-editor-lite#rememberSelection keyup->senren--rich-text-editor-lite#rememberSelection keyup->senren--rich-text-editor-lite#updateToolbar mouseup->senren--rich-text-editor-lite#rememberSelection mouseup->senren--rich-text-editor-lite#updateToolbar touchend->senren--rich-text-editor-lite#rememberSelection paste->senren--rich-text-editor-lite#paste drop->senren--rich-text-editor-lite#paste" class="min-h-40 cursor-text bg-[hsl(var(--senren-background))] p-4 text-left text-sm leading-6 text-[hsl(var(--senren-foreground))] outline-none focus:ring-2 focus:ring-inset focus:ring-[hsl(var(--senren-ring))]">
|
|
30
30
|
<%= sanitize(initial_content, tags: %w[p br strong b em i a ul ol li h1 h2 h3], attributes: %w[href rel target data-align]) %>
|
|
31
31
|
</div>
|
|
32
32
|
<% end %>
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
<% else %>
|
|
5
5
|
<label class="sr-only" for="<%= name %>"><%= label %></label>
|
|
6
6
|
<span aria-hidden="true" class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[hsl(var(--senren-muted-foreground))]">
|
|
7
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
|
|
7
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
|
|
8
8
|
<circle cx="11" cy="11" r="7"></circle>
|
|
9
9
|
<path d="m20 20-3.5-3.5"></path>
|
|
10
10
|
</svg>
|
|
11
11
|
</span>
|
|
12
|
-
<input id="<%= name %>" name="<%= name %>" type="search" value="<%= value %>" placeholder="<%= placeholder %>" aria-label="<%= label %>" class="h-10 w-full rounded-(--senren-radius) border border-[hsl(var(--senren-input))] bg-[hsl(var(--senren-background))] px-9 text-sm text-[hsl(var(--senren-foreground))] outline-none placeholder:text-[hsl(var(--senren-muted-foreground))] focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
|
|
12
|
+
<input id="<%= name %>" name="<%= name %>" type="search" autocomplete="off" value="<%= value %>" placeholder="<%= placeholder %>" aria-label="<%= label %>" class="h-10 w-full rounded-(--senren-radius) border border-[hsl(var(--senren-input))] bg-[hsl(var(--senren-background))] px-9 text-sm text-[hsl(var(--senren-foreground))] outline-none placeholder:text-[hsl(var(--senren-muted-foreground))] focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
|
|
13
13
|
<% end %>
|
|
14
14
|
<% end %>
|