senren-ui 0.2.0 → 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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -0
  3. data/CONTRIBUTING.md +1 -0
  4. data/README.md +28 -0
  5. data/docs/visual_style.md +80 -0
  6. data/lib/generators/senren/install/install_generator.rb +3 -0
  7. data/lib/generators/senren/install/templates/base_component.rb.tt +97 -7
  8. data/lib/generators/senren/install/templates/conventions.md.tt +41 -0
  9. data/lib/generators/senren/install/templates/senren_themes.css.tt +339 -0
  10. data/lib/senren/rails/agent_rules_writer.rb +3 -0
  11. data/lib/senren/rails/version.rb +1 -1
  12. data/templates/components/accordion/accordion_component.html.erb +3 -3
  13. data/templates/components/alert_dialog/alert_dialog_component.html.erb +6 -3
  14. data/templates/components/alert_dialog/alert_dialog_component.rb +4 -0
  15. data/templates/components/api_key_field/api_key_field_component.html.erb +1 -1
  16. data/templates/components/breadcrumb/breadcrumb_component.html.erb +8 -6
  17. data/templates/components/button/button_component.html.erb +1 -1
  18. data/templates/components/button/button_component.rb +27 -1
  19. data/templates/components/calendar/calendar_component.html.erb +2 -2
  20. data/templates/components/card/card_component.html.erb +4 -6
  21. data/templates/components/carousel/carousel_component.html.erb +1 -1
  22. data/templates/components/cart/cart_component.html.erb +7 -1
  23. data/templates/components/clipboard/clipboard_component.html.erb +3 -3
  24. data/templates/components/collapsible/collapsible_component.html.erb +3 -3
  25. data/templates/components/combobox/combobox_component.html.erb +3 -3
  26. data/templates/components/command/command_component.html.erb +1 -1
  27. data/templates/components/context_menu/context_menu_component.html.erb +6 -3
  28. data/templates/components/date_picker/date_picker_component.html.erb +3 -3
  29. data/templates/components/dialog/dialog_component.html.erb +7 -3
  30. data/templates/components/dialog/dialog_component.rb +11 -0
  31. data/templates/components/dropdown_menu/dropdown_menu_component.html.erb +6 -3
  32. data/templates/components/dropdown_menu/dropdown_menu_component.rb +57 -5
  33. data/templates/components/hover_card/hover_card_component.html.erb +6 -3
  34. data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +3 -2
  35. data/templates/components/link/link_component.html.erb +1 -1
  36. data/templates/components/native_select/native_select_component.html.erb +2 -2
  37. data/templates/components/pagination/pagination_component.html.erb +2 -2
  38. data/templates/components/popover/popover_component.html.erb +6 -3
  39. data/templates/components/product_card/product_card_component.html.erb +2 -2
  40. data/templates/components/progress/progress_component.html.erb +2 -2
  41. data/templates/components/search_input/search_input_component.html.erb +2 -2
  42. data/templates/components/sheet/sheet_component.html.erb +13 -4
  43. data/templates/components/sheet/sheet_component.rb +11 -0
  44. data/templates/components/shortcut_key/shortcut_key_component.html.erb +2 -2
  45. data/templates/components/sidebar/sidebar_component.html.erb +2 -2
  46. data/templates/components/switch/switch_component.html.erb +1 -1
  47. data/templates/components/table/table_component.html.erb +2 -2
  48. data/templates/components/tabs/tabs_component.html.erb +3 -3
  49. data/templates/components/tooltip/tooltip_component.html.erb +5 -3
  50. data/templates/components/top_nav/top_nav_component.html.erb +2 -2
  51. data/templates/controllers/cart_controller.js +3 -1
  52. metadata +2 -1
@@ -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="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] %>">
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
@@ -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
@@ -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 %>
@@ -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 %>
@@ -1,7 +1,7 @@
1
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
2
  <div class="relative aspect-square w-full bg-[hsl(var(--senren-muted)/0.4)]">
3
3
  <% if safe_image_url %>
4
- <img src="<%= safe_image_url %>" alt="<%= title %>" loading="lazy" class="h-full w-full object-cover">
4
+ <%= image_tag safe_image_url, alt: title, loading: "lazy", class: "h-full w-full object-cover" %>
5
5
  <% else %>
6
6
  <div class="flex h-full w-full items-center justify-center text-sm text-[hsl(var(--senren-muted-foreground))]" aria-hidden="true">
7
7
  No image
@@ -29,7 +29,7 @@
29
29
  <%# A plain form: the server owns the cart, and Turbo handles the response. %>
30
30
  <%= form_with url: safe_action_url, method: method, class: "mt-1" do %>
31
31
  <button type="submit"
32
- <%= "disabled" unless available? %>
32
+ <% unless available? %>disabled<% end %>
33
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
34
  <%= button_label %>
35
35
  </button>
@@ -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 %>
@@ -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 %>
@@ -1,4 +1,7 @@
1
- <div data-controller="senren--sheet" data-senren-component="sheet" 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--sheet" })) do %>
2
5
  <% if trigger? %>
3
6
  <span data-action="click->senren--sheet#open" data-senren--sheet-target="trigger">
4
7
  <%= trigger %>
@@ -8,11 +11,12 @@
8
11
  <% end %>
9
12
 
10
13
  <div data-senren--sheet-target="overlay" hidden
14
+ data-action="click->senren--sheet#close"
11
15
  class="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm"></div>
12
16
 
13
17
  <div data-senren--sheet-target="panel" hidden role="dialog" aria-modal="true"
14
18
  data-open="false"
15
- class="fixed z-50 bg-[hsl(var(--senren-background))] text-[hsl(var(--senren-foreground))] shadow-lg transition-transform duration-200 <%= self.class::VARIANTS[variant] %>">
19
+ class="<%= panel_class("fixed z-50 bg-[hsl(var(--senren-background))] text-[hsl(var(--senren-foreground))] shadow-lg transition-transform duration-200 #{ self.class::VARIANTS[variant] }") %>">
16
20
  <div class="flex h-full flex-col p-6">
17
21
  <% if title? %>
18
22
  <h2 class="text-lg font-semibold"><%= title %></h2>
@@ -20,7 +24,12 @@
20
24
  <% if description? %>
21
25
  <p class="text-sm text-[hsl(var(--senren-muted-foreground))]"><%= description %></p>
22
26
  <% end %>
23
- <div class="mt-4 flex-1 overflow-auto">
27
+ <%# -mx-2 px-2 gives the scroll container room for focus rings.
28
+ overflow-auto clips whatever draws outside a child's border box,
29
+ and a ring does exactly that -- a focused input sitting flush
30
+ against the edge had its ring shaved off on both sides. The
31
+ negative margin cancels the padding, so nothing moves. %>
32
+ <div class="-mx-2 mt-4 flex-1 overflow-auto px-2">
24
33
  <%= body || content %>
25
34
  </div>
26
35
  <% if footer? %>
@@ -34,4 +43,4 @@
34
43
  <span aria-hidden="true">×</span>
35
44
  </button>
36
45
  </div>
37
- </div>
46
+ <% 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 SheetComponent < BaseComponent
5
16
  renders_one :trigger
6
17
  renders_one :title
@@ -1,6 +1,6 @@
1
- <span <%= tag.attributes(**root_attrs("inline-flex items-center gap-1 text-xs text-[hsl(var(--senren-muted-foreground))]")) %>>
1
+ <%= tag.span(**root_attrs("inline-flex items-center gap-1 text-xs text-[hsl(var(--senren-muted-foreground))]")) do %>
2
2
  <% (keys.presence || [content]).compact.each_with_index do |key, index| %>
3
3
  <% if index.positive? %><span aria-hidden="true">+</span><% end %>
4
4
  <kbd class="rounded border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-muted))] px-1.5 py-0.5 font-mono text-[11px] text-[hsl(var(--senren-foreground))] shadow-sm"><%= key %></kbd>
5
5
  <% end %>
6
- </span>
6
+ <% end %>
@@ -1,4 +1,4 @@
1
- <aside <%= tag.attributes(**root_attrs("flex min-h-80 flex-col overflow-hidden rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] p-3 text-[hsl(var(--senren-card-foreground))] transition-[width] duration-300 ease-in-out", data: { controller: "senren--sidebar" })) %>>
1
+ <%= tag.aside(**root_attrs("flex min-h-80 flex-col overflow-hidden rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] p-3 text-[hsl(var(--senren-card-foreground))] transition-[width] duration-300 ease-in-out", data: { controller: "senren--sidebar" })) do %>
2
2
  <div class="mb-4 flex items-center justify-between gap-2 px-2">
3
3
  <div data-senren--sidebar-target="brand" class="truncate font-display text-sm font-semibold tracking-tight"><%= brand %></div>
4
4
  <button type="button" data-senren--sidebar-target="toggleButton" class="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-[hsl(var(--senren-muted-foreground))] transition-colors hover:bg-[hsl(var(--senren-accent))] hover:text-[hsl(var(--senren-accent-foreground))]" data-action="click->senren--sidebar#toggle" aria-label="Toggle sidebar" aria-expanded="<%= variant != :compact %>">
@@ -21,4 +21,4 @@
21
21
  <% if content? %>
22
22
  <div data-senren--sidebar-target="footer" class="mt-auto pt-4 text-xs text-[hsl(var(--senren-muted-foreground))]"><%= content %></div>
23
23
  <% end %>
24
- </aside>
24
+ <% end %>
@@ -1,6 +1,6 @@
1
1
  <%= tag.label(**root_attrs("inline-flex items-center gap-3 cursor-pointer")) do %>
2
2
  <span class="relative inline-flex h-6 w-11 items-center rounded-full bg-[hsl(var(--senren-muted))] has-[:checked]:bg-[hsl(var(--senren-primary))] transition-colors">
3
- <input type="checkbox" role="switch" id="<%= id %>" name="<%= name %>" value="<%= value %>" <%= "checked" if checked %> class="peer sr-only">
3
+ <input type="checkbox" role="switch" id="<%= id %>" name="<%= name %>" value="<%= value %>" <% if checked %>checked<% end %> class="peer sr-only">
4
4
  <span aria-hidden="true" class="absolute left-0.5 top-0.5 inline-block h-5 w-5 transform rounded-full bg-[hsl(var(--senren-background))] transition-transform peer-checked:translate-x-5"></span>
5
5
  </span>
6
6
  <% if label %>
@@ -1,4 +1,4 @@
1
- <div <%= tag.attributes(**root_attrs("w-full overflow-hidden rounded-(--senren-radius) border border-[hsl(var(--senren-border))]")) %>>
1
+ <%= tag.div(**root_attrs("w-full overflow-hidden rounded-(--senren-radius) border border-[hsl(var(--senren-border))]")) do %>
2
2
  <table class="w-full border-collapse text-left text-sm <%= self.class::VARIANTS[variant] %>">
3
3
  <% if caption.present? %>
4
4
  <caption class="sr-only"><%= caption %></caption>
@@ -23,4 +23,4 @@
23
23
  <% end %>
24
24
  </tbody>
25
25
  </table>
26
- </div>
26
+ <% end %>
@@ -1,4 +1,4 @@
1
- <div <%= tag.attributes(**root_attrs("w-full", data: { controller: "senren--tabs" })) %>>
1
+ <%= tag.div(**root_attrs("w-full", data: { controller: "senren--tabs" })) do %>
2
2
  <div role="tablist" aria-label="<%= label %>" class="<%= self.class::VARIANTS[variant] %> flex flex-wrap items-center gap-1">
3
3
  <% items.each do |item| %>
4
4
  <% selected = active_item?(item) %>
@@ -10,9 +10,9 @@
10
10
  <div class="mt-4">
11
11
  <% items.each do |item| %>
12
12
  <% selected = active_item?(item) %>
13
- <section id="<%= item[:id] %>-panel" role="tabpanel" aria-labelledby="<%= item[:id] %>-tab" data-senren--tabs-target="panel" data-panel-id="<%= item[:id] %>" data-state="<%= selected ? "active" : "inactive" %>" <%= "hidden" unless selected %> class="rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] p-4 text-sm text-[hsl(var(--senren-card-foreground))]">
13
+ <section id="<%= item[:id] %>-panel" role="tabpanel" aria-labelledby="<%= item[:id] %>-tab" data-senren--tabs-target="panel" data-panel-id="<%= item[:id] %>" data-state="<%= selected ? "active" : "inactive" %>" <% unless selected %>hidden<% end %> class="rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))] p-4 text-sm text-[hsl(var(--senren-card-foreground))]">
14
14
  <%= item[:content].presence || content %>
15
15
  </section>
16
16
  <% end %>
17
17
  </div>
18
- </div>
18
+ <% end %>
@@ -1,9 +1,11 @@
1
- <span data-controller="senren--tooltip" class="relative inline-block" data-senren-component="tooltip">
1
+ <%# Root goes through root_attrs so a caller's class: and data: reach it.
2
+ Hand-writing these attributes dropped both. %>
3
+ <%= tag.span(**wrapper_attrs("relative inline-block", data: { controller: "senren--tooltip" })) do %>
2
4
  <span tabindex="0"
3
5
  aria-describedby="<%= id %>"
4
6
  data-action="mouseenter->senren--tooltip#show mouseleave->senren--tooltip#hide focus->senren--tooltip#show blur->senren--tooltip#hide">
5
7
  <%= content %>
6
8
  </span>
7
9
  <span id="<%= id %>" role="tooltip" hidden data-senren--tooltip-target="bubble"
8
- class="absolute z-50 mt-1 whitespace-nowrap rounded-md bg-[hsl(var(--senren-foreground))] text-[hsl(var(--senren-background))] px-2 py-1 text-xs shadow"><%= text %></span>
9
- </span>
10
+ class="<%= panel_class("absolute z-50 mt-1 whitespace-nowrap rounded-md bg-[hsl(var(--senren-foreground))] text-[hsl(var(--senren-background))] px-2 py-1 text-xs shadow") %>"><%= text %></span>
11
+ <% end %>
@@ -1,4 +1,4 @@
1
- <header <%= tag.attributes(**root_attrs("sticky top-0 z-30 border-b border-[hsl(var(--senren-border))] backdrop-blur-md")) %>>
1
+ <%= tag.header(**root_attrs("sticky top-0 z-30 border-b border-[hsl(var(--senren-border))] backdrop-blur-md")) do %>
2
2
  <div class="mx-auto flex h-14 w-full max-w-[1400px] items-center gap-6 px-4 lg:px-8">
3
3
  <div class="min-w-0 flex-none font-display text-sm font-semibold text-[hsl(var(--senren-foreground))]">
4
4
  <% if brand? %><%= brand %><% else %>Senren<% end %>
@@ -18,4 +18,4 @@
18
18
  <div class="ml-auto flex items-center gap-2"><%= actions %></div>
19
19
  <% end %>
20
20
  </div>
21
- </header>
21
+ <% end %>
@@ -27,9 +27,11 @@ export default class extends Controller {
27
27
  if (!line) return
28
28
 
29
29
  const id = line.dataset.lineId
30
+ // Read before the node goes away.
31
+ const removeUrl = line.dataset.removeUrl || null
30
32
  line.remove()
31
33
  this._recalculate()
32
- this.dispatch("removed", { detail: { id, subtotalCents: this.subtotalCentsValue } })
34
+ this.dispatch("removed", { detail: { id, removeUrl, subtotalCents: this.subtotalCentsValue } })
33
35
  }
34
36
 
35
37
  // The value is the state; this renders it. Called on initialization too, so
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: senren-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - vutt
@@ -69,6 +69,7 @@ files:
69
69
  - lib/generators/senren/install/templates/conventions.md.tt
70
70
  - lib/generators/senren/install/templates/installed_components.yml.tt
71
71
  - lib/generators/senren/install/templates/senren.css.tt
72
+ - lib/generators/senren/install/templates/senren_themes.css.tt
72
73
  - lib/senren-ui.rb
73
74
  - lib/senren/rails.rb
74
75
  - lib/senren/rails/agent_rules_writer.rb