phlex_kit 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/phlex_kit/_tokens.css +10 -2
  3. data/app/assets/stylesheets/phlex_kit/themes/claude.css +1 -1
  4. data/app/assets/stylesheets/phlex_kit/themes/neutral.css +1 -1
  5. data/app/assets/stylesheets/phlex_kit/themes/zinc.css +1 -1
  6. data/app/components/phlex_kit/accordion/accordion_content.rb +11 -2
  7. data/app/components/phlex_kit/accordion/accordion_controller.js +15 -2
  8. data/app/components/phlex_kit/alert_dialog/alert_dialog_content.rb +5 -2
  9. data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +80 -6
  10. data/app/components/phlex_kit/aspect_ratio/aspect_ratio.rb +8 -4
  11. data/app/components/phlex_kit/calendar/calendar_body.rb +9 -1
  12. data/app/components/phlex_kit/calendar/calendar_controller.js +63 -1
  13. data/app/components/phlex_kit/chart/chart_controller.js +5 -1
  14. data/app/components/phlex_kit/clipboard/clipboard_popover.rb +3 -1
  15. data/app/components/phlex_kit/combobox/combobox_controller.js +15 -2
  16. data/app/components/phlex_kit/context_menu/context_menu_checkbox_item.rb +6 -2
  17. data/app/components/phlex_kit/context_menu/context_menu_controller.js +51 -1
  18. data/app/components/phlex_kit/context_menu/context_menu_radio_item.rb +6 -2
  19. data/app/components/phlex_kit/context_menu/context_menu_sub_trigger.rb +5 -2
  20. data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +3 -2
  21. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_checkbox_item.rb +6 -2
  22. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_content.rb +3 -1
  23. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +71 -48
  24. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_radio_item.rb +6 -2
  25. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_trigger.rb +5 -2
  26. data/app/components/phlex_kit/form_field/form_field_controller.js +14 -1
  27. data/app/components/phlex_kit/form_field/form_field_error.rb +3 -0
  28. data/app/components/phlex_kit/input_otp/input_otp_controller.js +30 -4
  29. data/app/components/phlex_kit/masked_input/masked_input.rb +1 -1
  30. data/app/components/phlex_kit/masked_input/masked_input_controller.js +24 -5
  31. data/app/components/phlex_kit/menubar/menubar_controller.js +56 -11
  32. data/app/components/phlex_kit/menubar/menubar_sub_trigger.rb +3 -1
  33. data/app/components/phlex_kit/navigation_menu/navigation_menu.css +4 -1
  34. data/app/components/phlex_kit/navigation_menu/navigation_menu_trigger.rb +1 -1
  35. data/app/components/phlex_kit/popover/popover.css +3 -3
  36. data/app/components/phlex_kit/popover/popover_controller.js +29 -6
  37. data/app/components/phlex_kit/popover/popover_trigger.rb +4 -1
  38. data/app/components/phlex_kit/resizable/resizable_controller.js +65 -3
  39. data/app/components/phlex_kit/resizable/resizable_handle.rb +3 -1
  40. data/app/components/phlex_kit/select/select.rb +8 -7
  41. data/app/components/phlex_kit/select/select_controller.js +29 -14
  42. data/app/components/phlex_kit/select/select_item.rb +0 -1
  43. data/app/components/phlex_kit/sidebar/sidebar.css +19 -7
  44. data/app/components/phlex_kit/slider/slider.css +2 -2
  45. data/app/components/phlex_kit/theme_toggle/theme_toggle.rb +11 -0
  46. data/app/components/phlex_kit/theme_toggle/theme_toggle_controller.js +6 -0
  47. data/app/components/phlex_kit/toast/toast_region.rb +6 -2
  48. data/app/components/phlex_kit/toast/toaster_controller.js +7 -3
  49. data/app/components/phlex_kit/toggle/toggle.rb +3 -1
  50. data/app/javascript/phlex_kit/controllers/index.js +0 -2
  51. data/lib/phlex_kit/version.rb +1 -1
  52. metadata +1 -2
  53. data/app/components/phlex_kit/select/select_item_controller.js +0 -14
@@ -1,13 +1,25 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
 
3
3
  // PhlexKit's stand-in for react-resizable-panels behind shadcn's Resizable:
4
- // dragging a handle rebalances the flex-grow of the two panels around it.
4
+ // dragging (or arrow-keying) a handle rebalances the flex-grow of the two
5
+ // panels around it. The handle is a focusable role="separator", so it also
6
+ // carries the ARIA value contract: aria-valuenow tracks the leading panel's
7
+ // share of the pair, and aria-orientation follows the group direction (a
8
+ // handle in a horizontal group is a vertical separator).
5
9
  // Connects to data-controller="phlex-kit--resizable"
6
10
  export default class extends Controller {
7
11
  static targets = ["panel", "handle"]
8
12
  static values = { direction: { type: String, default: "horizontal" } }
9
13
 
14
+ handleTargetConnected(handle) {
15
+ handle.setAttribute("aria-orientation", this.directionValue === "horizontal" ? "vertical" : "horizontal")
16
+ handle.setAttribute("aria-valuemin", "0")
17
+ handle.setAttribute("aria-valuemax", "100")
18
+ this.syncValuenow(handle)
19
+ }
20
+
10
21
  start(e) {
22
+ if (e.button !== 0 || !e.isPrimary) return
11
23
  const handle = e.currentTarget
12
24
  const prev = handle.previousElementSibling
13
25
  const next = handle.nextElementSibling
@@ -20,6 +32,10 @@ export default class extends Controller {
20
32
  startPos: horizontal ? e.clientX : e.clientY,
21
33
  prevSize: sizeOf(prev),
22
34
  nextSize: sizeOf(next),
35
+ // The drag redistributes the PAIR's combined flex-grow, not a fixed
36
+ // constant: normalizing to 2 rescaled the pair against untouched
37
+ // siblings, so in a 25/25/50 group the 50 panel ballooned on first drag.
38
+ pairGrow: this.growOf(prev) + this.growOf(next) || 2,
23
39
  }
24
40
  try { handle.setPointerCapture(e.pointerId) } catch {}
25
41
 
@@ -27,8 +43,9 @@ export default class extends Controller {
27
43
  const delta = (horizontal ? ev.clientX : ev.clientY) - drag.startPos
28
44
  const total = drag.prevSize + drag.nextSize
29
45
  const prevSize = Math.min(Math.max(drag.prevSize + delta, 0), total)
30
- prev.style.flexGrow = (prevSize / total) * 2
31
- next.style.flexGrow = ((total - prevSize) / total) * 2
46
+ prev.style.flexGrow = (prevSize / total) * drag.pairGrow
47
+ next.style.flexGrow = ((total - prevSize) / total) * drag.pairGrow
48
+ this.syncValuenow(handle)
32
49
  }
33
50
  const onUp = () => {
34
51
  handle.removeEventListener("pointermove", onMove)
@@ -39,4 +56,49 @@ export default class extends Controller {
39
56
  handle.addEventListener("pointerup", onUp)
40
57
  handle.addEventListener("pointercancel", onUp)
41
58
  }
59
+
60
+ // Arrow keys resize by 5% of the pair per press (matching the drag axis);
61
+ // Home/End collapse the leading panel to its min/max.
62
+ keydown(e) {
63
+ const handle = e.currentTarget
64
+ const prev = handle.previousElementSibling
65
+ const next = handle.nextElementSibling
66
+ if (!prev || !next) return
67
+
68
+ const horizontal = this.directionValue === "horizontal"
69
+ const steps = horizontal
70
+ ? { ArrowLeft: -0.05, ArrowRight: 0.05 }
71
+ : { ArrowUp: -0.05, ArrowDown: 0.05 }
72
+
73
+ const pairGrow = this.growOf(prev) + this.growOf(next) || 2
74
+ let share = this.growOf(prev) / (pairGrow || 1)
75
+ if (e.key in steps) {
76
+ share = Math.min(1, Math.max(0, share + steps[e.key]))
77
+ } else if (e.key === "Home") {
78
+ share = 0
79
+ } else if (e.key === "End") {
80
+ share = 1
81
+ } else {
82
+ return
83
+ }
84
+ e.preventDefault()
85
+
86
+ prev.style.flexGrow = share * pairGrow
87
+ next.style.flexGrow = (1 - share) * pairGrow
88
+ this.syncValuenow(handle)
89
+ }
90
+
91
+ growOf(el) {
92
+ const g = parseFloat(getComputedStyle(el).flexGrow)
93
+ return Number.isFinite(g) ? g : 1
94
+ }
95
+
96
+ syncValuenow(handle) {
97
+ const prev = handle.previousElementSibling
98
+ const next = handle.nextElementSibling
99
+ if (!prev || !next) return
100
+ const pair = this.growOf(prev) + this.growOf(next)
101
+ const share = pair > 0 ? this.growOf(prev) / pair : 0.5
102
+ handle.setAttribute("aria-valuenow", String(Math.round(share * 100)))
103
+ }
42
104
  }
@@ -12,10 +12,12 @@ module PhlexKit
12
12
  class: "pk-resizable-handle",
13
13
  role: "separator",
14
14
  tabindex: "0",
15
+ # Correct for the default horizontal group; the controller re-stamps
16
+ # orientation (and aria-valuenow/min/max) per group direction on connect.
15
17
  aria: { orientation: "vertical" },
16
18
  data: {
17
19
  phlex_kit__resizable_target: "handle",
18
- action: "pointerdown->phlex-kit--resizable#start"
20
+ action: "pointerdown->phlex-kit--resizable#start keydown->phlex-kit--resizable#keydown"
19
21
  }
20
22
  }, @attrs)) do
21
23
  span(class: "pk-resizable-handle-grip", aria: { hidden: "true" }) if @with_handle
@@ -2,11 +2,13 @@ module PhlexKit
2
2
  # Custom dropdown select, ported from ruby_ui's RubyUI::Select — the styled
3
3
  # popover (Image #2), NOT the native <select> (that's PhlexKit::NativeSelect). Unlike
4
4
  # the rest of the kit this component IS JS-driven: it keeps ruby_ui's Stimulus
5
- # wiring (`phlex-kit--select` / `phlex-kit--select-item`, in
6
- # app/javascript/controllers/ruby_ui/), since the open/close, selection, and
7
- # keyboard nav are the point. The one change from upstream: the controller drops
8
- # the `@floating-ui/dom` dependency and positions the panel with plain CSS
9
- # (opens directly below the trigger). Tailwind → vanilla `.pk-select-*` (select.css).
5
+ # wiring (`phlex-kit--select`), since the open/close, selection, and keyboard
6
+ # nav are the point. Two changes from upstream: the controller drops the
7
+ # `@floating-ui/dom` dependency and positions the panel with plain CSS (opens
8
+ # directly below the trigger), and selection flips aria-selected via the
9
+ # instance-scoped itemTargets rather than upstream's document-scoped
10
+ # `.pk-select-item` outlet (which clobbered every other Select on the page).
11
+ # Tailwind → vanilla `.pk-select-*` (select.css).
10
12
  #
11
13
  # Multi-part. The hidden SelectInput carries the form value/name; SelectTrigger +
12
14
  # SelectValue are the closed-state button; SelectContent > SelectGroup >
@@ -34,8 +36,7 @@ module PhlexKit
34
36
  data: {
35
37
  controller: "phlex-kit--select",
36
38
  phlex_kit__select_open_value: "false",
37
- action: "click@window->phlex-kit--select#clickOutside",
38
- phlex_kit__select_phlex_kit__select_item_outlet: ".pk-select-item"
39
+ action: "click@window->phlex-kit--select#clickOutside"
39
40
  }
40
41
  }, @attrs), &block)
41
42
  end
@@ -8,7 +8,6 @@ import { Controller } from "@hotwired/stimulus";
8
8
  export default class extends Controller {
9
9
  static targets = ["trigger", "content", "input", "value", "item"];
10
10
  static values = { open: Boolean };
11
- static outlets = ["phlex-kit--select-item"];
12
11
 
13
12
  connect() {
14
13
  this.generateItemsIds();
@@ -17,13 +16,20 @@ export default class extends Controller {
17
16
  selectItem(event) {
18
17
  event.preventDefault();
19
18
 
20
- this.phlexKitSelectItemOutlets.forEach((item) => item.handleSelectItem(event));
19
+ // currentTarget, not target: the click may land on a child of the item
20
+ // (target.dataset.value would be undefined). itemTargets, not a
21
+ // document-scoped outlet: outlets match `.pk-select-item` page-wide and
22
+ // clobbered every other Select's aria-selected.
23
+ const item = event.currentTarget;
24
+ this.itemTargets.forEach((el) => {
25
+ el.setAttribute("aria-selected", el === item ? "true" : "false");
26
+ });
21
27
 
22
28
  const oldValue = this.inputTarget.value;
23
- const newValue = event.target.dataset.value;
29
+ const newValue = item.dataset.value;
24
30
 
25
31
  this.inputTarget.value = newValue;
26
- this.valueTarget.innerText = event.target.innerText;
32
+ this.valueTarget.innerText = item.innerText;
27
33
 
28
34
  this.dispatchOnChange(oldValue, newValue);
29
35
  this.closeContent();
@@ -89,21 +95,30 @@ export default class extends Controller {
89
95
  }
90
96
 
91
97
  clickOutside(event) {
92
- if (!this.openValue) return;
98
+ if (!this.contentTarget.matches(":popover-open")) return;
93
99
  if (this.element.contains(event.target)) return;
94
100
 
95
101
  event.preventDefault();
96
- this.toogleContent();
102
+ this.#hide();
97
103
  }
98
104
 
105
+ // Open/close derive from the live :popover-open state, never a stored
106
+ // flag — a stale flag is how a close on an already-closed panel becomes
107
+ // an open (bit the popover's keyboard toggle).
99
108
  toogleContent() {
100
- this.openValue = !this.openValue;
101
- if (this.openValue) {
102
- this.contentTarget.showPopover();
103
- } else if (this.contentTarget.matches(":popover-open")) {
104
- this.contentTarget.hidePopover();
105
- }
106
- this.triggerTarget.setAttribute("aria-expanded", this.openValue);
109
+ this.contentTarget.matches(":popover-open") ? this.#hide() : this.#show();
110
+ }
111
+
112
+ #show() {
113
+ this.openValue = true;
114
+ this.contentTarget.showPopover();
115
+ this.triggerTarget.setAttribute("aria-expanded", "true");
116
+ }
117
+
118
+ #hide() {
119
+ this.openValue = false;
120
+ if (this.contentTarget.matches(":popover-open")) this.contentTarget.hidePopover();
121
+ this.triggerTarget.setAttribute("aria-expanded", "false");
107
122
  }
108
123
 
109
124
  generateItemsIds() {
@@ -123,7 +138,7 @@ export default class extends Controller {
123
138
  }
124
139
 
125
140
  closeContent() {
126
- this.toogleContent();
141
+ this.#hide();
127
142
  this.resetCurrent();
128
143
 
129
144
  // aria-activedescendant holds an element id; on close it must be removed,
@@ -21,7 +21,6 @@ module PhlexKit
21
21
  "aria-selected": (@selected ? "true" : "false"),
22
22
  data: {
23
23
  value: @value,
24
- controller: "phlex-kit--select-item",
25
24
  action: "click->phlex-kit--select#selectItem keydown.enter->phlex-kit--select#selectItem " \
26
25
  "keydown.down->phlex-kit--select#handleKeyDown keydown.up->phlex-kit--select#handleKeyUp " \
27
26
  "keydown.esc->phlex-kit--select#handleEsc",
@@ -169,14 +169,23 @@
169
169
  .pk-sidebar-wrapper.collapsible-offcanvas .pk-sidebar,
170
170
  .pk-sidebar-wrapper.collapsible-icon .pk-sidebar {
171
171
  position: relative;
172
- transition: margin-left .2s ease, transform .2s ease, width .2s ease;
172
+ /* visibility rides the transition: it stays visible while sliding out and
173
+ flips hidden at the end — hidden also drops the off-screen links from
174
+ the tab order (they stayed focusable while invisible before). */
175
+ transition: margin-left .2s ease, transform .2s ease, width .2s ease, visibility .2s;
173
176
  }
174
- .pk-sidebar-wrapper.collapsible-offcanvas[data-collapsed] .pk-sidebar { margin-left: -16rem; }
177
+ .pk-sidebar-wrapper.collapsible-offcanvas[data-collapsed] .pk-sidebar { margin-left: -16rem; visibility: hidden; }
175
178
 
176
179
  /* Icon mode: the rail shrinks to a 3rem strip — labels and text hide, menu
177
180
  buttons become centered icon squares, hover shows a data-tooltip bubble. */
178
181
  .pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar { width: 3rem; }
179
- .pk-sidebar-wrapper.collapsible-icon[data-collapsed] :is(.pk-sidebar-group-label, .pk-sidebar-group-action, .pk-sidebar-menu-badge, .pk-sidebar-menu-action, .pk-sidebar-menu-sub, .pk-sidebar-input, .pk-sidebar-separator) { display: none; }
182
+ /* Desktop-only: at mobile widths icon mode becomes the full drawer, and the
183
+ labels/subs must keep their AUTHOR display values (flex). The old approach
184
+ un-hid them with `display: revert`, but revert rolls back to the USER-AGENT
185
+ origin — group labels lost their flex centering, sub lists their gap. */
186
+ @media (min-width: 768px) {
187
+ .pk-sidebar-wrapper.collapsible-icon[data-collapsed] :is(.pk-sidebar-group-label, .pk-sidebar-group-action, .pk-sidebar-menu-badge, .pk-sidebar-menu-action, .pk-sidebar-menu-sub, .pk-sidebar-input, .pk-sidebar-separator) { display: none; }
188
+ }
180
189
  .pk-sidebar-wrapper.collapsible-icon[data-collapsed] :is(.pk-sidebar-header, .pk-sidebar-content, .pk-sidebar-footer) {
181
190
  padding-inline: .5rem;
182
191
  align-items: center;
@@ -187,7 +196,9 @@
187
196
  padding: .5rem;
188
197
  justify-content: center;
189
198
  }
190
- .pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button > :not(svg) { display: none; }
199
+ @media (min-width: 768px) {
200
+ .pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button > :not(svg) { display: none; }
201
+ }
191
202
  .pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button[data-tooltip] { position: relative; }
192
203
  .pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button[data-tooltip]:hover::after {
193
204
  content: attr(data-tooltip);
@@ -252,9 +263,8 @@
252
263
  @media (max-width: 767px) {
253
264
  /* Below the cutoff both collapsible modes become the overlay drawer. */
254
265
  .pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar { width: 16rem; }
255
- .pk-sidebar-wrapper.collapsible-icon[data-collapsed] :is(.pk-sidebar-group-label, .pk-sidebar-group-action, .pk-sidebar-menu-badge, .pk-sidebar-menu-action, .pk-sidebar-menu-sub, .pk-sidebar-input, .pk-sidebar-separator) { display: revert; }
266
+ /* No un-hiding needed: the icon-mode hide rules are scoped to >=768px. */
256
267
  .pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button { width: 100%; height: auto; padding: .5rem; justify-content: flex-start; }
257
- .pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button > :not(svg) { display: revert; }
258
268
  .pk-sidebar-wrapper.collapsible-icon .pk-sidebar-rail { display: none; }
259
269
  .pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon) .pk-sidebar {
260
270
  position: fixed;
@@ -264,9 +274,11 @@
264
274
  height: 100dvh;
265
275
  margin-left: 0;
266
276
  transform: translateX(-100%);
277
+ /* closed drawer: invisible AND out of the tab order */
278
+ visibility: hidden;
267
279
  box-shadow: 0 8px 30px rgb(0 0 0 / .35);
268
280
  }
269
- .pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon)[data-open] .pk-sidebar { transform: none; }
281
+ .pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon)[data-open] .pk-sidebar { transform: none; visibility: visible; }
270
282
  .pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon)[data-open] .pk-sidebar-scrim {
271
283
  display: block;
272
284
  position: fixed;
@@ -25,7 +25,7 @@
25
25
  margin-top: -0.25rem;
26
26
  border-radius: 9999px;
27
27
  border: 1px solid var(--pk-ring);
28
- background: #fff;
28
+ background: var(--pk-bg);
29
29
  box-shadow: 0 1px 2px rgb(0 0 0 / .25);
30
30
  transition: box-shadow .15s ease;
31
31
  }
@@ -51,7 +51,7 @@
51
51
  height: .75rem;
52
52
  border-radius: 9999px;
53
53
  border: 1px solid var(--pk-ring);
54
- background: #fff;
54
+ background: var(--pk-bg);
55
55
  }
56
56
  .pk-slider:hover::-moz-range-thumb,
57
57
  .pk-slider:active::-moz-range-thumb {
@@ -2,6 +2,17 @@ module PhlexKit
2
2
  # Light/dark theme switch built on Toggle. Ported from ruby_ui's RubyUI::
3
3
  # ThemeToggle. The sibling phlex-kit--theme-toggle controller listens for the
4
4
  # toggle's change event and flips :root[data-theme] (matching the token system).
5
+ # Only an actual user toggle is persisted to localStorage — with no stored
6
+ # choice the OS preference is followed live on every load.
7
+ #
8
+ # The controller applies the stored theme at Stimulus connect, which is late
9
+ # enough to flash light-mode for returning dark-theme users. To avoid the
10
+ # FOUC, inline this in <head> before any stylesheets:
11
+ #
12
+ # <script>
13
+ # const t = localStorage.theme;
14
+ # if (t === "dark" || t === "light") document.documentElement.dataset.theme = t;
15
+ # </script>
5
16
  class ThemeToggle < BaseComponent
6
17
  def initialize(wrapper: {}, aria: {}, **attrs)
7
18
  @wrapper = wrapper
@@ -7,6 +7,12 @@ export default class extends Controller {
7
7
  connect() { this.applyTheme(this.currentTheme()) }
8
8
  apply(e) {
9
9
  const theme = e.detail?.pressed ? "dark" : "light"
10
+ // connect()'s own pressed-state sync re-enters here via the toggle's
11
+ // change event (Stimulus value observation is async, so a flag can't
12
+ // gate it). If the incoming theme already matches the applied one this
13
+ // is that echo, not a user toggle — persisting it would silently pin
14
+ // the visitor's OS preference in localStorage on first visit.
15
+ if (theme === document.documentElement.getAttribute("data-theme")) return
10
16
  localStorage.theme = theme
11
17
  this.applyTheme(theme)
12
18
  }
@@ -68,7 +68,9 @@ module PhlexKit
68
68
  @flash.each do |key, message|
69
69
  next if message.nil? || message.to_s.empty?
70
70
  variant = Toast.flash_variant(key)
71
- render ToastItem.new(variant: variant, id: "flash-#{key}") do
71
+ # Stamp the region's duration — without it the toast controller's
72
+ # 4000ms default wins and ToastRegion(duration:) is dead config.
73
+ render ToastItem.new(variant: variant, id: "flash-#{key}", duration: @duration) do
72
74
  render ToastIcon.new(variant: variant)
73
75
  render ToastTitle.new { message.to_s }
74
76
  end
@@ -77,7 +79,9 @@ module PhlexKit
77
79
 
78
80
  def skeleton(variant)
79
81
  template(data: { phlex_kit__toaster_target: "skeleton", variant: variant.to_s }) do
80
- render ToastItem.new(variant: variant) do
82
+ # Region duration baked into the skeleton; _spawn overrides it only
83
+ # when the caller passes a per-toast duration.
84
+ render ToastItem.new(variant: variant, duration: @duration) do
81
85
  render ToastIcon.new(variant: variant)
82
86
  div(class: "pk-toast-body") do
83
87
  render ToastTitle.new
@@ -189,7 +189,10 @@ export default class extends Controller {
189
189
  if (!this._listEl) return
190
190
  const isBottom = this.positionValue.startsWith("bottom")
191
191
  const items = this.toastTargets
192
- const order = isBottom ? items.slice().reverse() : items.slice()
192
+ // Newest toast fronts the stack at BOTH edges (index 0 = front); only
193
+ // the offset direction flips with the position. Using DOM order at
194
+ // top-* put the oldest toast in front and stacked new ones behind it.
195
+ const order = items.slice().reverse()
193
196
  const heights = order.map(el => this._heights.get(el) || el.offsetHeight || 64)
194
197
  const gap = this.gapValue
195
198
  const peekOffset = 16
@@ -237,9 +240,10 @@ export default class extends Controller {
237
240
 
238
241
  _enforceMax(items) {
239
242
  if (items.length <= this.maxValue) return
240
- const isBottom = this.positionValue.startsWith("bottom")
241
243
  const dropping = items.length - this.maxValue
242
- const candidates = isBottom ? items.slice(0, dropping) : items.slice(-dropping)
244
+ // Evict the OLDEST toasts (DOM order = append order) regardless of edge;
245
+ // slicing from the tail at top-* dropped the newest ones instead.
246
+ const candidates = items.slice(0, dropping)
243
247
  candidates.forEach(el => {
244
248
  if (el.dataset.state !== "closing") {
245
249
  el.dispatchEvent(new CustomEvent("phlex-kit:toast:force-dismiss", { bubbles: true }))
@@ -7,7 +7,9 @@ module PhlexKit
7
7
  SIZES = { sm: "sm", default: nil, lg: "lg" }.freeze
8
8
 
9
9
  def self.modifier_classes(variant:, size:)
10
- [ VARIANTS.fetch(variant, nil), SIZES.fetch(size, nil) ].compact
10
+ # Plain fetch an unknown variant/size raises KeyError (kit-wide
11
+ # fail-loud rule); the nil-default form silently rendered unstyled.
12
+ [ VARIANTS.fetch(variant), SIZES.fetch(size) ].compact
11
13
  end
12
14
 
13
15
  def initialize(pressed: false, name: nil, value: "1", unpressed_value: nil,
@@ -36,7 +36,6 @@ import PopoverController from "phlex_kit/controllers/popover_controller"
36
36
  import ResizableController from "phlex_kit/controllers/resizable_controller"
37
37
  import ScrollFadeController from "phlex_kit/controllers/scroll_fade_controller"
38
38
  import SelectController from "phlex_kit/controllers/select_controller"
39
- import SelectItemController from "phlex_kit/controllers/select_item_controller"
40
39
  import SheetController from "phlex_kit/controllers/sheet_controller"
41
40
  import SheetContentController from "phlex_kit/controllers/sheet_content_controller"
42
41
  import SidebarController from "phlex_kit/controllers/sidebar_controller"
@@ -78,7 +77,6 @@ export function registerPhlexKitControllers(application) {
78
77
  application.register("phlex-kit--resizable", ResizableController)
79
78
  application.register("phlex-kit--scroll-fade", ScrollFadeController)
80
79
  application.register("phlex-kit--select", SelectController)
81
- application.register("phlex-kit--select-item", SelectItemController)
82
80
  application.register("phlex-kit--sheet", SheetController)
83
81
  application.register("phlex-kit--sheet-content", SheetContentController)
84
82
  application.register("phlex-kit--sidebar", SidebarController)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PhlexKit
4
- VERSION = "0.7.0"
4
+ VERSION = "0.8.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Kennedy
@@ -486,7 +486,6 @@ files:
486
486
  - app/components/phlex_kit/select/select_group.rb
487
487
  - app/components/phlex_kit/select/select_input.rb
488
488
  - app/components/phlex_kit/select/select_item.rb
489
- - app/components/phlex_kit/select/select_item_controller.js
490
489
  - app/components/phlex_kit/select/select_label.rb
491
490
  - app/components/phlex_kit/select/select_separator.rb
492
491
  - app/components/phlex_kit/select/select_trigger.rb
@@ -1,14 +0,0 @@
1
- import { Controller } from "@hotwired/stimulus";
2
-
3
- // Ported verbatim from ruby_ui's phlex-kit--select-item controller. Flips its own
4
- // element's aria-selected when an item is chosen (the select controller invokes
5
- // this on every item via the outlet), which drives the checkmark in select.css.
6
- export default class extends Controller {
7
- handleSelectItem({ target }) {
8
- if (this.element.dataset.value == target.dataset.value) {
9
- this.element.setAttribute("aria-selected", true);
10
- } else {
11
- this.element.removeAttribute("aria-selected");
12
- }
13
- }
14
- }