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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +215 -0
  3. data/CONTRIBUTING.md +41 -3
  4. data/README.md +138 -33
  5. data/Rakefile +14 -1
  6. data/docs/components.md +10 -10
  7. data/docs/hot_reload.md +103 -0
  8. data/docs/performance_testing.md +7 -3
  9. data/docs/visual_style.md +80 -0
  10. data/lib/generators/senren/component/templates/controller.js.tt +7 -4
  11. data/lib/generators/senren/install/install_generator.rb +77 -0
  12. data/lib/generators/senren/install/templates/base_component.rb.tt +142 -4
  13. data/lib/generators/senren/install/templates/conventions.md.tt +57 -1
  14. data/lib/generators/senren/install/templates/senren_themes.css.tt +339 -0
  15. data/lib/senren/rails/agent_rules_writer.rb +65 -19
  16. data/lib/senren/rails/asset_path_guard.rb +128 -0
  17. data/lib/senren/rails/base_component_patch.rb +64 -0
  18. data/lib/senren/rails/component_copier.rb +101 -50
  19. data/lib/senren/rails/component_installer.rb +26 -0
  20. data/lib/senren/rails/doctor.rb +7 -4
  21. data/lib/senren/rails/engine.rb +23 -0
  22. data/lib/senren/rails/host_paths.rb +11 -2
  23. data/lib/senren/rails/marker_block.rb +81 -0
  24. data/lib/senren/rails/registry.rb +10 -4
  25. data/lib/senren/rails/safe_write.rb +169 -0
  26. data/lib/senren/rails/skill_writer.rb +47 -11
  27. data/lib/senren/rails/version.rb +1 -1
  28. data/lib/senren/rails.rb +1 -1
  29. data/lib/senren-ui.rb +15 -0
  30. data/lib/tasks/senren.rake +43 -17
  31. data/registry/components.yml +45 -0
  32. data/registry/recipes.yml +12 -0
  33. data/templates/components/accordion/accordion_component.html.erb +3 -3
  34. data/templates/components/alert_dialog/alert_dialog_component.html.erb +6 -3
  35. data/templates/components/alert_dialog/alert_dialog_component.rb +5 -1
  36. data/templates/components/api_key_field/api_key_field_component.html.erb +1 -1
  37. data/templates/components/aspect_ratio/aspect_ratio_component.rb +7 -0
  38. data/templates/components/avatar/avatar_component.rb +8 -1
  39. data/templates/components/breadcrumb/breadcrumb_component.html.erb +8 -6
  40. data/templates/components/button/button_component.html.erb +1 -1
  41. data/templates/components/button/button_component.rb +27 -1
  42. data/templates/components/calendar/calendar_component.html.erb +2 -2
  43. data/templates/components/card/card_component.html.erb +4 -6
  44. data/templates/components/carousel/carousel_component.html.erb +1 -1
  45. data/templates/components/cart/cart_component.html.erb +67 -0
  46. data/templates/components/cart/cart_component.rb +71 -0
  47. data/templates/components/checkbox/checkbox_component.rb +1 -1
  48. data/templates/components/clipboard/clipboard_component.html.erb +3 -3
  49. data/templates/components/collapsible/collapsible_component.html.erb +3 -3
  50. data/templates/components/combobox/combobox_component.html.erb +3 -3
  51. data/templates/components/command/command_component.html.erb +1 -1
  52. data/templates/components/command/command_component.rb +1 -1
  53. data/templates/components/context_menu/context_menu_component.html.erb +6 -3
  54. data/templates/components/date_picker/date_picker_component.html.erb +3 -3
  55. data/templates/components/dialog/dialog_component.html.erb +7 -3
  56. data/templates/components/dialog/dialog_component.rb +12 -1
  57. data/templates/components/dropdown_menu/dropdown_menu_component.html.erb +6 -3
  58. data/templates/components/dropdown_menu/dropdown_menu_component.rb +57 -5
  59. data/templates/components/form/form_component.rb +9 -1
  60. data/templates/components/hover_card/hover_card_component.html.erb +6 -3
  61. data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +3 -2
  62. data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +1 -1
  63. data/templates/components/link/link_component.html.erb +1 -1
  64. data/templates/components/native_select/native_select_component.html.erb +2 -2
  65. data/templates/components/pagination/pagination_component.html.erb +2 -2
  66. data/templates/components/popover/popover_component.html.erb +6 -3
  67. data/templates/components/product_card/product_card_component.html.erb +38 -0
  68. data/templates/components/product_card/product_card_component.rb +49 -0
  69. data/templates/components/progress/progress_component.html.erb +2 -2
  70. data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.html.erb +1 -1
  71. data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.rb +1 -1
  72. data/templates/components/search_input/search_input_component.html.erb +2 -2
  73. data/templates/components/separator/separator_component.rb +7 -0
  74. data/templates/components/sheet/sheet_component.html.erb +13 -4
  75. data/templates/components/sheet/sheet_component.rb +12 -1
  76. data/templates/components/shortcut_key/shortcut_key_component.html.erb +2 -2
  77. data/templates/components/sidebar/sidebar_component.html.erb +2 -2
  78. data/templates/components/switch/switch_component.html.erb +1 -1
  79. data/templates/components/table/table_component.html.erb +2 -2
  80. data/templates/components/tabs/tabs_component.html.erb +3 -3
  81. data/templates/components/tooltip/tooltip_component.html.erb +5 -3
  82. data/templates/components/tooltip/tooltip_component.rb +2 -2
  83. data/templates/components/top_nav/top_nav_component.html.erb +2 -2
  84. data/templates/components/typography/typography_component.rb +7 -0
  85. data/templates/controllers/accordion_controller.js +1 -1
  86. data/templates/controllers/alert_dialog_controller.js +31 -7
  87. data/templates/controllers/cart_controller.js +85 -0
  88. data/templates/controllers/clipboard_controller.js +12 -1
  89. data/templates/controllers/command_controller.js +3 -4
  90. data/templates/controllers/context_menu_controller.js +38 -11
  91. data/templates/controllers/data_table_controller.js +8 -3
  92. data/templates/controllers/dialog_controller.js +47 -21
  93. data/templates/controllers/dropdown_menu_controller.js +40 -27
  94. data/templates/controllers/hover_card_controller.js +8 -0
  95. data/templates/controllers/invite_member_dialog_controller.js +6 -0
  96. data/templates/controllers/masked_input_controller.js +8 -1
  97. data/templates/controllers/popover_controller.js +25 -10
  98. data/templates/controllers/rich_text_editor_lite_controller.js +165 -28
  99. data/templates/controllers/sheet_controller.js +41 -11
  100. metadata +14 -17
  101. 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
@@ -10,7 +10,7 @@ module Senren
10
10
  @name = name
11
11
  @value = value
12
12
  @checked = checked
13
- @id = id || "#{name.to_s.parameterize}-#{SecureRandom.hex(2)}"
13
+ @id = id || senren_dom_id(name, value)
14
14
  @label = label
15
15
  end
16
16
 
@@ -1,7 +1,7 @@
1
- <div <%= tag.attributes(**root_attrs("inline-flex items-center gap-2", data: { controller: "senren--clipboard", "senren--clipboard-copied-label-value": copied_label })) %>>
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
- </div>
7
+ <% end %>
@@ -1,9 +1,9 @@
1
- <div <%= tag.attributes(**root_attrs("rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))]", data: { controller: "senren--collapsible" })) %>>
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" <%= "hidden" unless open? %> class="border-t border-[hsl(var(--senren-border))] px-4 py-3 text-sm text-[hsl(var(--senren-muted-foreground))]">
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
- </div>
9
+ <% end %>
@@ -1,4 +1,4 @@
1
- <div <%= tag.attributes(**root_attrs("relative w-full", data: { controller: "senren--combobox" })) %>>
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
- </div>
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" aria-activedescendant="" 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))]">
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| %>
@@ -11,7 +11,7 @@ module Senren
11
11
  @placeholder = placeholder
12
12
  @label = label
13
13
  @empty_text = empty_text
14
- @dom_id = id || "senren-command-#{SecureRandom.hex(3)}"
14
+ @dom_id = id || senren_dom_id(label)
15
15
  @items = normalize_items(items)
16
16
  end
17
17
 
@@ -1,11 +1,14 @@
1
- <div data-controller="senren--context-menu" class="relative" data-senren-component="context_menu">
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
- </div>
14
+ <% end %>
@@ -1,5 +1,5 @@
1
- <div <%= tag.attributes(**root_attrs("flex w-full items-center gap-2", data: { controller: "senren--date-picker" })) %>>
2
- <input type="date" id="<%= id %>" name="<%= name %>" value="<%= value %>" placeholder="<%= placeholder %>" data-senren--date-picker-target="input" class="hover:cursor-pointerh-10 w-full 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))] <%= self.class::VARIANTS[variant] %>">
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
- </div>
5
+ <% end %>
@@ -1,4 +1,7 @@
1
- <div data-controller="senren--dialog" data-senren-component="dialog" data-senren--dialog-open-value="<%= open %>" id="<%= dom_id %>">
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
- </div>
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 || "senren-dialog-#{SecureRandom.hex(3)}"
28
+ @dom_id = id || senren_dom_id
18
29
  end
19
30
 
20
31
  attr_reader :open, :dom_id
@@ -1,12 +1,15 @@
1
- <div data-controller="senren--dropdown-menu" class="relative inline-block" data-state="closed" data-senren-component="dropdown_menu">
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
- </div>
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', method: @method, class: klass, data: { action: ITEM_ACTION }, **html_attrs)
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: klass, data: { action: ITEM_ACTION }, **html_attrs)
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, :url, :method, :multipart
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
- <span data-controller="senren--hover-card" class="relative inline-block" data-senren-component="hover_card">
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
- </span>
13
+ <% end %>
@@ -7,7 +7,8 @@
7
7
  </button>
8
8
  <% end %>
9
9
 
10
- <div hidden data-senren--invite-member-dialog-target="overlay" class="fixed inset-0 z-40 bg-[hsl(var(--senren-foreground)/0.42)] backdrop-blur-sm"></div>
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 || "senren-invite-member-#{SecureRandom.hex(3)}"
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", href: safe_url(href), **external_attrs) %>
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 %>" <%= "selected" if selected.to_s == value.to_s %>><%= label %></option>
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 %>" <%= "selected" if selected.to_s == value.to_s %>><%= label %></option>
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
- <nav <%= tag.attributes(**root_attrs("flex items-center justify-center gap-1", aria: { label: label })) %>>
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
- </nav>
11
+ <% end %>
@@ -1,9 +1,12 @@
1
- <div data-controller="senren--popover" class="relative inline-block" data-senren-component="popover">
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
- </div>
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
- <div <%= tag.attributes(**root_attrs("w-full")) %>>
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
- </div>
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#syncSoon" class="min-h-40 cursor-text bg-[hsl(var(--senren-background))] p-4 text-left text-sm leading-6 text-[hsl(var(--senren-foreground))] outline-none focus:ring-2 focus:ring-inset focus:ring-[hsl(var(--senren-ring))]">
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 %>
@@ -12,7 +12,7 @@ module Senren
12
12
  @value = value
13
13
  @label = label
14
14
  @placeholder = placeholder
15
- @dom_id = id || "senren-editor-#{SecureRandom.hex(3)}"
15
+ @dom_id = id || senren_dom_id(name)
16
16
  @toolbar = toolbar
17
17
  @debug = debug
18
18
  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 %>