phlex_kit 0.5.1 → 0.6.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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -8
  3. data/app/components/phlex_kit/accordion/accordion.css +2 -0
  4. data/app/components/phlex_kit/accordion/accordion.rb +1 -1
  5. data/app/components/phlex_kit/accordion/accordion_content.rb +3 -1
  6. data/app/components/phlex_kit/accordion/accordion_controller.js +26 -2
  7. data/app/components/phlex_kit/accordion/accordion_default_trigger.rb +20 -4
  8. data/app/components/phlex_kit/accordion/accordion_trigger.rb +18 -2
  9. data/app/components/phlex_kit/alert/alert.rb +1 -1
  10. data/app/components/phlex_kit/alert_dialog/alert_dialog_cancel.rb +1 -1
  11. data/app/components/phlex_kit/alert_dialog/alert_dialog_content.rb +1 -1
  12. data/app/components/phlex_kit/attachment/attachment.rb +2 -2
  13. data/app/components/phlex_kit/attachment/attachment_media.rb +1 -1
  14. data/app/components/phlex_kit/avatar/avatar.rb +1 -1
  15. data/app/components/phlex_kit/badge/badge.rb +1 -1
  16. data/app/components/phlex_kit/button/button.rb +1 -1
  17. data/app/components/phlex_kit/button_group/button_group.rb +1 -1
  18. data/app/components/phlex_kit/calendar/calendar_controller.js +12 -8
  19. data/app/components/phlex_kit/card/card.rb +1 -1
  20. data/app/components/phlex_kit/carousel/carousel.rb +1 -1
  21. data/app/components/phlex_kit/clipboard/clipboard_popover.rb +1 -1
  22. data/app/components/phlex_kit/combobox/combobox.css +21 -12
  23. data/app/components/phlex_kit/combobox/combobox.rb +19 -3
  24. data/app/components/phlex_kit/combobox/combobox_badge_trigger.rb +7 -2
  25. data/app/components/phlex_kit/combobox/combobox_checkbox.rb +3 -0
  26. data/app/components/phlex_kit/combobox/combobox_controller.js +85 -20
  27. data/app/components/phlex_kit/combobox/combobox_input_trigger.rb +11 -2
  28. data/app/components/phlex_kit/combobox/combobox_item.rb +4 -1
  29. data/app/components/phlex_kit/combobox/combobox_list.rb +9 -2
  30. data/app/components/phlex_kit/combobox/combobox_popover.rb +8 -7
  31. data/app/components/phlex_kit/combobox/combobox_radio.rb +3 -0
  32. data/app/components/phlex_kit/combobox/combobox_search_input.rb +8 -3
  33. data/app/components/phlex_kit/command/command.rb +17 -2
  34. data/app/components/phlex_kit/command/command_controller.js +93 -3
  35. data/app/components/phlex_kit/command/command_dialog_content.rb +16 -4
  36. data/app/components/phlex_kit/command/command_input.rb +7 -2
  37. data/app/components/phlex_kit/command/command_item.rb +12 -3
  38. data/app/components/phlex_kit/command/command_list.rb +9 -2
  39. data/app/components/phlex_kit/context_menu/context_menu.css +25 -13
  40. data/app/components/phlex_kit/context_menu/context_menu_content.rb +1 -1
  41. data/app/components/phlex_kit/context_menu/context_menu_controller.js +71 -12
  42. data/app/components/phlex_kit/context_menu/context_menu_item.rb +7 -3
  43. data/app/components/phlex_kit/context_menu/context_menu_label.rb +1 -1
  44. data/app/components/phlex_kit/context_menu/context_menu_trigger.rb +3 -1
  45. data/app/components/phlex_kit/data_table/data_table_column_visibility_controller.js +1 -1
  46. data/app/components/phlex_kit/data_table/data_table_controller.js +12 -1
  47. data/app/components/phlex_kit/data_table/data_table_pagination.rb +12 -10
  48. data/app/components/phlex_kit/data_table/data_table_selection_summary.rb +1 -0
  49. data/app/components/phlex_kit/data_table/data_table_sort_head.rb +5 -1
  50. data/app/components/phlex_kit/dialog/dialog_content.rb +12 -4
  51. data/app/components/phlex_kit/dialog/dialog_controller.js +13 -0
  52. data/app/components/phlex_kit/drawer/drawer_content.rb +2 -2
  53. data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +36 -15
  54. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_content.rb +4 -3
  55. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +8 -11
  56. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_item.rb +1 -1
  57. data/app/components/phlex_kit/empty/empty_media.rb +1 -1
  58. data/app/components/phlex_kit/field/field.rb +1 -1
  59. data/app/components/phlex_kit/field/field_legend.rb +1 -1
  60. data/app/components/phlex_kit/hover_card/hover_card.css +27 -8
  61. data/app/components/phlex_kit/hover_card/hover_card.rb +2 -1
  62. data/app/components/phlex_kit/hover_card/hover_card_content.rb +2 -2
  63. data/app/components/phlex_kit/hover_card/hover_card_controller.js +19 -5
  64. data/app/components/phlex_kit/input_group/input_group_addon.rb +1 -1
  65. data/app/components/phlex_kit/input_otp/input_otp.rb +4 -1
  66. data/app/components/phlex_kit/input_otp/input_otp_controller.js +12 -1
  67. data/app/components/phlex_kit/item/item.rb +1 -1
  68. data/app/components/phlex_kit/item/item_media.rb +1 -1
  69. data/app/components/phlex_kit/link/link.rb +1 -1
  70. data/app/components/phlex_kit/marker/marker.rb +1 -1
  71. data/app/components/phlex_kit/menubar/menubar.css +36 -14
  72. data/app/components/phlex_kit/menubar/menubar.rb +1 -1
  73. data/app/components/phlex_kit/menubar/menubar_checkbox_item.rb +5 -2
  74. data/app/components/phlex_kit/menubar/menubar_content.rb +1 -1
  75. data/app/components/phlex_kit/menubar/menubar_controller.js +100 -12
  76. data/app/components/phlex_kit/menubar/menubar_item.rb +8 -5
  77. data/app/components/phlex_kit/menubar/menubar_radio_item.rb +5 -2
  78. data/app/components/phlex_kit/menubar/menubar_trigger.rb +1 -0
  79. data/app/components/phlex_kit/native_select/native_select.rb +1 -1
  80. data/app/components/phlex_kit/navigation_menu/navigation_menu.css +24 -7
  81. data/app/components/phlex_kit/navigation_menu/navigation_menu_content.rb +3 -1
  82. data/app/components/phlex_kit/popover/popover.css +24 -9
  83. data/app/components/phlex_kit/popover/popover.rb +2 -1
  84. data/app/components/phlex_kit/popover/popover_content.rb +6 -4
  85. data/app/components/phlex_kit/popover/popover_controller.js +12 -8
  86. data/app/components/phlex_kit/popover/popover_trigger.rb +1 -1
  87. data/app/components/phlex_kit/resizable/resizable_panel.rb +4 -2
  88. data/app/components/phlex_kit/select/select.css +24 -12
  89. data/app/components/phlex_kit/select/select_content.rb +6 -4
  90. data/app/components/phlex_kit/select/select_controller.js +21 -17
  91. data/app/components/phlex_kit/select/select_trigger.rb +1 -1
  92. data/app/components/phlex_kit/sheet/sheet_content.rb +2 -2
  93. data/app/components/phlex_kit/sheet/sheet_content_controller.js +68 -2
  94. data/app/components/phlex_kit/sheet/sheet_controller.js +28 -3
  95. data/app/components/phlex_kit/sidebar/sidebar.rb +1 -1
  96. data/app/components/phlex_kit/sidebar/sidebar_wrapper.rb +1 -1
  97. data/app/components/phlex_kit/spinner/spinner.rb +1 -1
  98. data/app/components/phlex_kit/switch/switch.rb +7 -4
  99. data/app/components/phlex_kit/tabs/tabs.rb +1 -1
  100. data/app/components/phlex_kit/tabs/tabs_content.rb +4 -1
  101. data/app/components/phlex_kit/tabs/tabs_controller.js +60 -7
  102. data/app/components/phlex_kit/tabs/tabs_list.rb +2 -2
  103. data/app/components/phlex_kit/tabs/tabs_trigger.rb +14 -2
  104. data/app/components/phlex_kit/theme_toggle/theme_toggle.rb +11 -3
  105. data/app/components/phlex_kit/toast/toast_controller.js +17 -0
  106. data/app/components/phlex_kit/toast/toast_icon.rb +1 -1
  107. data/app/components/phlex_kit/toast/toast_item.rb +1 -1
  108. data/app/components/phlex_kit/toggle/toggle.rb +2 -2
  109. data/app/components/phlex_kit/toggle_group/toggle_group.rb +2 -2
  110. data/app/components/phlex_kit/toggle_group/toggle_group_item.rb +1 -1
  111. data/app/components/phlex_kit/tooltip/tooltip.css +40 -22
  112. data/app/components/phlex_kit/tooltip/tooltip.rb +3 -1
  113. data/app/components/phlex_kit/tooltip/tooltip_content.rb +1 -1
  114. data/app/components/phlex_kit/tooltip/tooltip_controller.js +32 -0
  115. data/app/components/phlex_kit/tooltip/tooltip_trigger.rb +8 -3
  116. data/app/components/phlex_kit/typography/text.rb +1 -1
  117. data/app/javascript/phlex_kit/controllers/index.js +2 -0
  118. data/lib/generators/phlex_kit/component/component_generator.rb +28 -0
  119. data/lib/generators/phlex_kit/install/templates/phlex_kit.rb +3 -0
  120. data/lib/phlex_kit/base_component.rb +12 -0
  121. data/lib/phlex_kit/engine.rb +11 -1
  122. data/lib/phlex_kit/icons/lucide.rb +1 -1
  123. data/lib/phlex_kit/icons/phosphor.rb +1 -1
  124. data/lib/phlex_kit/icons/remix.rb +1 -1
  125. data/lib/phlex_kit/icons/tabler.rb +1 -1
  126. data/lib/phlex_kit/version.rb +1 -1
  127. data/lib/phlex_kit.rb +5 -0
  128. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8140f5bead7563457fe7da7998915db03b3011c72bf82163236b75aff4ce15d
4
- data.tar.gz: '08fe9d1bd2e68091e4586e10823a98d5d7226dd57fff7d8050cf073511f483d8'
3
+ metadata.gz: 74f3d49a1a6adfab950d193db75d0cce12c02308c10fe69566d7a62ea0a835e3
4
+ data.tar.gz: 0b673ba98ab594e4f2783a32c83b792e849605cd43fb1033d2bd258dece4c789
5
5
  SHA512:
6
- metadata.gz: 250d307d76eacb729f7672be2a8e00cc7e0057b3fca3079e544f518eec8417615843a1e2ef2d2043d99cde927993d6383f5269cae06e164f6d68d9e4a4705c10
7
- data.tar.gz: 5fb5b1a04c2483fb63c9ef761ba05fd3ebd01ef45f05a0be2bee40b979ae4da240d610d0ae555a6470dcd11be74d08475d488ed5861b9fba7472bcd5efe764c7
6
+ metadata.gz: d0e680e98ac74f15fa085a2828e7aa86041d67fa58ec82dd07d9f2912bc06c063ef58b5fcdcba5c44b037cc7b4070b01869b891da449ad8bcbb9ce4eb62facf4
7
+ data.tar.gz: ddea978e8b26eaf08dcb373df2c59bec15405f0c043ca349c8c6f6e1c20194f75712281d526f70bc0fc234fcb2aeb41a79f42386182494a6a597af3bb9868fe3
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  A [shadcn/ui](https://ui.shadcn.com)-grade component kit for
7
7
  [Phlex](https://phlex.fun) + Rails, styled with **vanilla CSS + design tokens
8
8
  instead of Tailwind**. The full [ruby_ui](https://ruby-ui.com) catalog plus
9
- shadcn's own additions — 70 component families, 38 Stimulus controllers —
9
+ shadcn's own additions — 72 component families, 40 Stimulus controllers —
10
10
  with the default theme lifted verbatim from shadcn/ui's current token system.
11
11
 
12
12
  <picture>
@@ -21,7 +21,9 @@ switches, nav menu, command palette — at `/create` in [the docs site](#the-doc
21
21
 
22
22
  - **No build step.** Components are plain Ruby classes with co-located vanilla
23
23
  CSS, served precompiled-static through Propshaft. No Tailwind, no Node, no
24
- PostCSS.
24
+ PostCSS. (Outside Rails there's no Zeitwerk to autoload them — after
25
+ `require "phlex_kit"`, require the component files you use, e.g.
26
+ `Dir[File.join(gem_dir, "app/components/phlex_kit/**/*.rb")].sort.each { require it }`.)
25
27
  - **The shadcn look, themeable.** Every component reads `--pk-*` custom
26
28
  properties. The default palette, radii, and control geometry match
27
29
  ui.shadcn.com; redefine `:root` and the whole kit re-themes live — dark,
@@ -119,8 +121,8 @@ PhlexKit.configure { |c| c.define_ui_alias = true } # UI == PhlexKit
119
121
 
120
122
  The gem ships shadcn/ui's default (neutral) token set — dark by default, light
121
123
  via `<html data-theme="light">`, OS-following via `data-theme="system"`.
122
- Override any token in your own stylesheet; your app's CSS sorts ahead of the
123
- gem's, so you win:
124
+ Override any token in your own stylesheet the installer prepends the gem's
125
+ `@import`, so the gem's rules come first and yours, coming later, win:
124
126
 
125
127
  ```css
126
128
  @import url("phlex_kit/phlex_kit.css");
@@ -137,8 +139,9 @@ The full token surface: `--pk-bg`, `--pk-surface`, `--pk-surface-2`,
137
139
  `--pk-brand`, `--pk-brand-ink`, `--pk-green`, `--pk-amber`, `--pk-red`,
138
140
  `--pk-chart-1..5`, `--pk-radius`, plus `--pk-font-sans` / `--pk-font-serif` /
139
141
  `--pk-font-mono` (system stacks by default — override them after an @font-face to adopt Geist
140
- like shadcn, as the docs site does). Want a fully custom palette? Drop the
141
- `_tokens.css` import and define them all yourself. The complete reference
142
+ like shadcn, as the docs site does). Want a fully custom palette? Override
143
+ every token in your stylesheet (the `_tokens.css` import lives inside the
144
+ gem's manifest, so you re-declare rather than remove). The complete reference —
142
145
  defaults, shadcn equivalents, the optional `--pk-sidebar-*` overrides, and
143
146
  every per-component knob (`--pk-card-spacing`, `--pk-cell-size`, …) — is in
144
147
  [docs/07-TOKENS.md](docs/07-TOKENS.md).
@@ -248,7 +251,16 @@ for another component reuses the same wiring — it's injected only once.
248
251
  The dummy Rails app is a full shadcn-style docs site: a sidebar menu with one
249
252
  page per component, every use case demoed with a Preview/Code toggle (the code
250
253
  shown is extracted from the running example source, so it can never drift).
251
- A one-page kitchen-sink gallery lives at /gallery:
254
+ A one-page kitchen-sink gallery lives at /gallery, and /examples holds six
255
+ full-page admin screens (dashboard, orders, inbox, profile, reports, settings)
256
+ for a fictional commerce app — every visible element a kit component:
257
+
258
+ <picture>
259
+ <source media="(prefers-color-scheme: dark)" srcset="docs/images/examples-dashboard-dark.png">
260
+ <source media="(prefers-color-scheme: light)" srcset="docs/images/examples-dashboard-light.png">
261
+ <img alt="The /examples admin dashboard — KPI cards, a revenue chart and a live activity feed inside the kit's sidebar shell" src="docs/images/examples-dashboard-light.png">
262
+ </picture>
263
+
252
264
 
253
265
  ```bash
254
266
  git clone https://github.com/MatthewKennedy/phlex_kit && cd phlex_kit
@@ -284,7 +296,8 @@ alert dialog, aspect ratio, attachment, avatar (+ group), badge, breadcrumb,
284
296
  bubble, button, button group, calendar, card, carousel, chart, checkbox,
285
297
  clipboard, codeblock, collapsible, combobox (button/input/badge-chip
286
298
  triggers), command palette, context menu, data table, date picker, dialog,
287
- drawer, dropdown menu, empty, form + form field, hover card, input, input
299
+ drawer, dropdown menu, empty, field (set / group / label / description /
300
+ error), form + form field, hover card, input, input
288
301
  group, input OTP, item, kbd, label, link, masked input, menubar, message,
289
302
  message scroller, native select, navigation menu, pagination, popover,
290
303
  marker, progress, radio button + radio group, resizable, scroll area, select,
@@ -1,6 +1,8 @@
1
1
  /* Co-located with accordion.rb. Theme tokens from the global stylesheet. */
2
2
  .pk-accordion { width: 100%; }
3
3
  .pk-accordion-item { border-bottom: 1px solid var(--pk-border); }
4
+ /* APG: triggers live inside headings — the heading is a neutral wrapper. */
5
+ .pk-accordion-heading { margin: 0; display: flex; font-size: inherit; font-weight: inherit; line-height: inherit; }
4
6
  .pk-accordion-trigger {
5
7
  width: 100%; display: flex; flex: 1; align-items: center; justify-content: space-between;
6
8
  padding-block: 1rem; font-size: .875rem; font-weight: 500; text-align: left;
@@ -13,7 +13,7 @@ module PhlexKit
13
13
  end
14
14
 
15
15
  def view_template(&)
16
- div(**mix({ class: "pk-accordion", data: { type: TYPES.fetch(@type) } }, @attrs), &)
16
+ div(**mix({ class: "pk-accordion", data: { type: fetch_option(TYPES, @type, :type) } }, @attrs), &)
17
17
  end
18
18
  end
19
19
  end
@@ -2,7 +2,9 @@ module PhlexKit
2
2
  class AccordionContent < BaseComponent
3
3
  def initialize(**attrs) = (@attrs = attrs)
4
4
  def view_template(&)
5
- div(**mix({ class: "pk-accordion-content", data: { phlex_kit__accordion_target: "content", state: "closed" }, style: "height: 0px;", hidden: true }, @attrs), &)
5
+ # role="region" + aria-labelledby trigger id (wired by the controller
6
+ # in connect(), which also assigns the ids), per the APG accordion pattern.
7
+ div(**mix({ class: "pk-accordion-content", role: "region", data: { phlex_kit__accordion_target: "content", state: "closed" }, style: "height: 0px;", hidden: true }, @attrs), &)
6
8
  end
7
9
  end
8
10
  end
@@ -3,8 +3,10 @@ import { Controller } from "@hotwired/stimulus"
3
3
  // Connects to data-controller="phlex-kit--accordion"
4
4
  // Ported from ruby_ui's ruby-ui--accordion controller, reworked to use the native
5
5
  // Web Animations API instead of the `motion` package (no extra JS dependency).
6
+ let uid = 0
7
+
6
8
  export default class extends Controller {
7
- static targets = ["icon", "content"]
9
+ static targets = ["icon", "content", "trigger"]
8
10
  static values = {
9
11
  open: { type: Boolean, default: false },
10
12
  animationDuration: { type: Number, default: 150 },
@@ -12,6 +14,7 @@ export default class extends Controller {
12
14
  }
13
15
 
14
16
  connect() {
17
+ this.wireAria()
15
18
  const d = this.animationDurationValue
16
19
  this.animationDurationValue = 0
17
20
  this.openValue ? this.open() : this.close()
@@ -35,7 +38,28 @@ export default class extends Controller {
35
38
  if (ctrl && ctrl.openValue) ctrl.openValue = false
36
39
  })
37
40
  }
38
- openValueChanged(isOpen) { isOpen ? this.open() : this.close() }
41
+ openValueChanged(isOpen) {
42
+ if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", isOpen ? "true" : "false")
43
+ isOpen ? this.open() : this.close()
44
+ }
45
+
46
+ // Pair the trigger with its content: assign per-instance ids (render time
47
+ // has no shared item value, so the parts can't derive them), wire
48
+ // aria-controls/aria-labelledby, and propagate a disabled item onto the
49
+ // actual button so it drops out of the tab order.
50
+ wireAria() {
51
+ if (!this.hasTriggerTarget) return
52
+ const id = ++uid
53
+ const trigger = this.triggerTarget
54
+ if (!trigger.id) trigger.id = `pk-accordion-trigger-${id}`
55
+ if (this.hasContentTarget) {
56
+ const content = this.contentTarget
57
+ if (!content.id) content.id = `pk-accordion-panel-${id}`
58
+ trigger.setAttribute("aria-controls", content.id)
59
+ content.setAttribute("aria-labelledby", trigger.id)
60
+ }
61
+ if ("disabled" in this.element.dataset) trigger.disabled = true
62
+ }
39
63
 
40
64
  open() {
41
65
  if (!this.hasContentTarget) return
@@ -1,11 +1,27 @@
1
1
  module PhlexKit
2
2
  # Convenience trigger: label on the left, chevron icon on the right.
3
3
  class AccordionDefaultTrigger < BaseComponent
4
- def initialize(**attrs) = (@attrs = attrs)
4
+ def initialize(open: false, disabled: false, heading_level: 3, **attrs)
5
+ raise ArgumentError, "heading_level must be 1..6" unless (1..6).cover?(heading_level)
6
+ @open = open
7
+ @disabled = disabled
8
+ @heading_level = heading_level
9
+ @attrs = attrs
10
+ end
5
11
  def view_template(&block)
6
- button(**mix({ type: "button", class: "pk-accordion-trigger", data: { action: "click->phlex-kit--accordion#toggle" } }, @attrs)) do
7
- span(class: "pk-accordion-trigger-label", &block)
8
- render PhlexKit::AccordionIcon.new
12
+ # APG accordion: each trigger is a button wrapped in a heading.
13
+ # aria-expanded reflects the render-time open state; the controller keeps
14
+ # it in sync and wires aria-controls to the content's id in connect().
15
+ base = {
16
+ type: "button", class: "pk-accordion-trigger",
17
+ aria_expanded: @open ? "true" : "false", disabled: @disabled,
18
+ data: { phlex_kit__accordion_target: "trigger", action: "click->phlex-kit--accordion#toggle" }
19
+ }
20
+ send(:"h#{@heading_level}", class: "pk-accordion-heading") do
21
+ button(**mix(base, @attrs)) do
22
+ span(class: "pk-accordion-trigger-label", &block)
23
+ render PhlexKit::AccordionIcon.new
24
+ end
9
25
  end
10
26
  end
11
27
  end
@@ -1,8 +1,24 @@
1
1
  module PhlexKit
2
2
  class AccordionTrigger < BaseComponent
3
- def initialize(**attrs) = (@attrs = attrs)
3
+ def initialize(open: false, disabled: false, heading_level: 3, **attrs)
4
+ raise ArgumentError, "heading_level must be 1..6" unless (1..6).cover?(heading_level)
5
+ @open = open
6
+ @disabled = disabled
7
+ @heading_level = heading_level
8
+ @attrs = attrs
9
+ end
4
10
  def view_template(&)
5
- button(**mix({ type: "button", class: "pk-accordion-trigger", data: { action: "click->phlex-kit--accordion#toggle" } }, @attrs), &)
11
+ # APG accordion: each trigger is a button wrapped in a heading.
12
+ # aria-expanded reflects the render-time open state; the controller keeps
13
+ # it in sync and wires aria-controls to the content's id in connect().
14
+ base = {
15
+ type: "button", class: "pk-accordion-trigger",
16
+ aria_expanded: @open ? "true" : "false", disabled: @disabled,
17
+ data: { phlex_kit__accordion_target: "trigger", action: "click->phlex-kit--accordion#toggle" }
18
+ }
19
+ send(:"h#{@heading_level}", class: "pk-accordion-heading") do
20
+ button(**mix(base, @attrs), &)
21
+ end
6
22
  end
7
23
  end
8
24
  end
@@ -35,7 +35,7 @@ module PhlexKit
35
35
  private
36
36
 
37
37
  def classes
38
- [ "pk-alert", VARIANTS.fetch(@variant) ].compact.join(" ")
38
+ [ "pk-alert", fetch_option(VARIANTS, @variant, :variant) ].compact.join(" ")
39
39
  end
40
40
  end
41
41
  end
@@ -7,7 +7,7 @@ module PhlexKit
7
7
  end
8
8
 
9
9
  def view_template(&block)
10
- render PhlexKit::Button.new(variant: :outline, data: { action: "click->phlex-kit--alert-dialog#dismiss" }, **@attrs, &block)
10
+ render PhlexKit::Button.new(variant: :outline, **mix({ data: { action: "click->phlex-kit--alert-dialog#dismiss" } }, @attrs), &block)
11
11
  end
12
12
  end
13
13
  end
@@ -15,7 +15,7 @@ module PhlexKit
15
15
  template(**mix({ data: { phlex_kit__alert_dialog_target: "content" } }, @attrs)) do
16
16
  div(data: { controller: "phlex-kit--alert-dialog" }) do
17
17
  div(class: "pk-alert-dialog-overlay", "aria-hidden": "true")
18
- div(role: "alertdialog", "aria-modal": "true", class: [ "pk-alert-dialog-panel", SIZES.fetch(@size) ].compact.join(" "), &block)
18
+ div(role: "alertdialog", "aria-modal": "true", class: [ "pk-alert-dialog-panel", fetch_option(SIZES, @size, :size) ].compact.join(" "), &block)
19
19
  end
20
20
  end
21
21
  end
@@ -20,8 +20,8 @@ module PhlexKit
20
20
 
21
21
  def view_template(&)
22
22
  div(**mix({
23
- class: [ "pk-attachment", SIZES.fetch(@size), ORIENTATIONS.fetch(@orientation) ].compact.join(" "),
24
- data: { state: STATES.fetch(@state) }
23
+ class: [ "pk-attachment", fetch_option(SIZES, @size, :size), fetch_option(ORIENTATIONS, @orientation, :orientation) ].compact.join(" "),
24
+ data: { state: fetch_option(STATES, @state, :state) }
25
25
  }, @attrs), &)
26
26
  end
27
27
  end
@@ -11,7 +11,7 @@ module PhlexKit
11
11
  end
12
12
 
13
13
  def view_template(&)
14
- div(**mix({ class: [ "pk-attachment-media", VARIANTS.fetch(@variant) ].compact.join(" ") }, @attrs), &)
14
+ div(**mix({ class: [ "pk-attachment-media", fetch_option(VARIANTS, @variant, :variant) ].compact.join(" ") }, @attrs), &)
15
15
  end
16
16
  end
17
17
  end
@@ -21,7 +21,7 @@ module PhlexKit
21
21
  private
22
22
 
23
23
  def classes
24
- [ "pk-avatar", SIZES.fetch(@size) ].compact.join(" ")
24
+ [ "pk-avatar", fetch_option(SIZES, @size, :size) ].compact.join(" ")
25
25
  end
26
26
  end
27
27
  end
@@ -50,7 +50,7 @@ module PhlexKit
50
50
  private
51
51
 
52
52
  def classes
53
- [ "pk-badge", VARIANTS.fetch(@variant), SIZES.fetch(@size) ].compact.join(" ")
53
+ [ "pk-badge", fetch_option(VARIANTS, @variant, :variant), fetch_option(SIZES, @size, :size) ].compact.join(" ")
54
54
  end
55
55
  end
56
56
  end
@@ -52,7 +52,7 @@ module PhlexKit
52
52
  private
53
53
 
54
54
  def classes
55
- [ "pk-button", VARIANTS.fetch(@variant), SIZES.fetch(@size), ("icon" if @icon) ].compact.join(" ")
55
+ [ "pk-button", fetch_option(VARIANTS, @variant, :variant), fetch_option(SIZES, @size, :size), ("icon" if @icon) ].compact.join(" ")
56
56
  end
57
57
  end
58
58
  end
@@ -13,7 +13,7 @@ module PhlexKit
13
13
  end
14
14
 
15
15
  def view_template(&)
16
- classes = [ "pk-button-group", ORIENTATIONS.fetch(@orientation) ].compact.join(" ")
16
+ classes = [ "pk-button-group", fetch_option(ORIENTATIONS, @orientation, :orientation) ].compact.join(" ")
17
17
  div(**mix({ class: classes, role: "group" }, @attrs), &)
18
18
  end
19
19
  end
@@ -34,10 +34,10 @@ export default class extends Controller {
34
34
  maxDate: { type: String, default: null },
35
35
  disabledDates: { type: Array, default: [] },
36
36
  weekNumbers: { type: Boolean, default: false },
37
- viewDate: {
38
- type: String,
39
- default: new Date().toISOString().slice(0, 10),
40
- },
37
+ // Resolved lazily by viewDate() — a `new Date()` default here would be
38
+ // frozen at module load (stale "today" in long-lived tabs) and
39
+ // toISOString() is UTC (wrong day for non-UTC users near midnight).
40
+ viewDate: { type: String, default: "" },
41
41
  format: { type: String, default: "yyyy-MM-dd" },
42
42
  };
43
43
  static outlets = ["phlex-kit--calendar-input"];
@@ -61,14 +61,14 @@ export default class extends Controller {
61
61
  const date = this.viewDate();
62
62
  date.setDate(2);
63
63
  date.setMonth(Number(e.target.value));
64
- this.viewDateValue = date.toISOString().slice(0, 10);
64
+ this.viewDateValue = this.isoDate(date);
65
65
  }
66
66
 
67
67
  setYear(e) {
68
68
  const date = this.viewDate();
69
69
  date.setDate(2);
70
70
  date.setFullYear(Number(e.target.value));
71
- this.viewDateValue = date.toISOString().slice(0, 10);
71
+ this.viewDateValue = this.isoDate(date);
72
72
  }
73
73
 
74
74
  selectDay(e) {
@@ -132,9 +132,13 @@ export default class extends Controller {
132
132
  // update the viewDateValue to the selected date's month (this re-renders)
133
133
  const newViewDate = new Date(selectedDate);
134
134
  newViewDate.setDate(2); // avoid month-length/timezone edges
135
- this.viewDateValue = newViewDate.toISOString().slice(0, 10);
135
+ this.viewDateValue = this.isoDate(newViewDate);
136
136
 
137
137
  this.updateCalendar();
138
+
139
+ // Stimulus fires valueChanged during value initialization too — a
140
+ // server-rendered selected_date must not dispatch a change event.
141
+ if (prevValue === undefined) return;
138
142
  this.pushToOutlets();
139
143
  this.dispatchChange();
140
144
  }
@@ -180,7 +184,7 @@ export default class extends Controller {
180
184
  const date = this.viewDate();
181
185
  date.setDate(2); // avoid month-length/timezone edges
182
186
  date.setMonth(date.getMonth() + adjustment);
183
- return date.toISOString().slice(0, 10);
187
+ return this.isoDate(date);
184
188
  }
185
189
 
186
190
  updateCalendar() {
@@ -27,7 +27,7 @@ module PhlexKit
27
27
  end
28
28
 
29
29
  def view_template(&block)
30
- div(**mix({ class: [ "pk-card", SIZES.fetch(@size) ].compact.join(" ") }, @attrs), &block)
30
+ div(**mix({ class: [ "pk-card", fetch_option(SIZES, @size, :size) ].compact.join(" ") }, @attrs), &block)
31
31
  end
32
32
  end
33
33
  end
@@ -17,7 +17,7 @@ module PhlexKit
17
17
 
18
18
  def view_template(&)
19
19
  div(**mix({
20
- class: [ "pk-carousel", ORIENTATIONS.fetch(@orientation) ].join(" "),
20
+ class: [ "pk-carousel", fetch_option(ORIENTATIONS, @orientation, :orientation) ].join(" "),
21
21
  role: "region",
22
22
  aria: { roledescription: "carousel" },
23
23
  data: {
@@ -6,7 +6,7 @@ module PhlexKit
6
6
  @attrs = attrs
7
7
  end
8
8
  def view_template(&block)
9
- div(class: "pk-clipboard-popover pk-hidden", data: { phlex_kit__clipboard_target: TARGETS.fetch(@type) }) do
9
+ div(class: "pk-clipboard-popover pk-hidden", data: { phlex_kit__clipboard_target: fetch_option(TARGETS, @type, :target) }) do
10
10
  div(**mix({ class: "pk-clipboard-popover-inner" }, @attrs), &block)
11
11
  end
12
12
  end
@@ -1,10 +1,13 @@
1
1
  /* Co-located with combobox.rb — UI::Combobox. ruby_ui's structure + Stimulus
2
- kept; the @floating-ui/native-popover positioning replaced with plain CSS
3
- (panel below the trigger), and Tailwind with vanilla CSS on the palette
4
- tokens. Geometry from ruby_ui (h-9 trigger, rounded-lg panel, p-1 list,
5
- px-2 py-1 label rows, group labels via ::before content(attr(label))).
2
+ kept; like upstream the panel is a native [popover] (manual), and CSS
3
+ anchor positioning replaces @floating-ui (Baseline 2026): the panel anchors
4
+ to the combobox wrapper (all three trigger layouts fill it), gets the
5
+ trigger width as a floor via anchor-size(), and flips above at the
6
+ viewport bottom via position-try-fallbacks. Tailwind → vanilla CSS on the
7
+ palette tokens. Geometry from ruby_ui (h-9 trigger, rounded-lg panel, p-1
8
+ list, px-2 py-1 label rows, group labels via ::before content(attr(label))).
6
9
  Theme tokens come from the global stylesheet. */
7
- .pk-combobox { position: relative; width: 100%; }
10
+ .pk-combobox { width: 100%; anchor-name: --pk-combobox; anchor-scope: --pk-combobox; }
8
11
 
9
12
  /* Closed-state trigger (mirrors .pk-select-trigger geometry). */
10
13
  .pk-combobox-trigger {
@@ -40,14 +43,17 @@
40
43
  .pk-combobox-trigger-content { overflow: hidden; text-overflow: ellipsis; }
41
44
  .pk-combobox-trigger-icon { flex: none; width: 1rem; height: 1rem; margin-left: .5rem; opacity: .5; }
42
45
 
43
- /* Dropdown panel — opens below the trigger (no floating-ui); the controller
44
- toggles .pk-hidden and matches its width to the trigger. */
46
+ /* Dropdown panel — anchored below the combobox, flips above at the viewport
47
+ bottom. Width: trigger width with a 15rem floor (was JS
48
+ updatePopoverWidth, now pure CSS). */
45
49
  .pk-combobox-popover {
46
- position: absolute;
47
- top: 100%;
48
- left: 0;
49
- z-index: 50;
50
- margin-top: .25rem;
50
+ position: fixed;
51
+ position-anchor: --pk-combobox;
52
+ inset: auto;
53
+ position-area: block-end span-inline-end;
54
+ margin: .25rem 0 0;
55
+ position-try-fallbacks: flip-block;
56
+ width: max(anchor-size(width), 15rem);
51
57
  border: 1px solid var(--pk-border);
52
58
  border-radius: var(--pk-radius);
53
59
  background: var(--pk-surface);
@@ -342,3 +348,6 @@
342
348
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
343
349
  }
344
350
  }
351
+ /* Hiding must survive author display values (a host class like
352
+ display:grid would beat the UA [popover] { display: none }). */
353
+ .pk-combobox-popover:not(:popover-open) { display: none; }
@@ -19,9 +19,10 @@ module PhlexKit
19
19
  @attrs = attrs
20
20
  end
21
21
 
22
- def view_template(&)
22
+ def view_template
23
+ # role="combobox" lives on the interactive element (the trigger button /
24
+ # filter input), not this wrapper — see ComboboxTrigger and friends.
23
25
  div(**mix({
24
- role: "combobox",
25
26
  class: "pk-combobox",
26
27
  data: {
27
28
  controller: "phlex-kit--combobox",
@@ -29,7 +30,22 @@ module PhlexKit
29
30
  phlex_kit__combobox_auto_highlight_value: (@auto_highlight ? "true" : nil),
30
31
  action: "turbo:morph@window->phlex-kit--combobox#updateTriggerContent click@window->phlex-kit--combobox#onClickOutside"
31
32
  }
32
- }, @attrs), &)
33
+ }, @attrs)) do
34
+ live_region
35
+ yield
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # Screen-reader announcement of the filtered result count — the controller
42
+ # writes "N results" / "No results" into it from applyFilter.
43
+ def live_region
44
+ div(
45
+ class: "pk-sr-only",
46
+ aria: { live: "polite" },
47
+ data: { phlex_kit__combobox_target: "liveRegion" }
48
+ )
33
49
  end
34
50
  end
35
51
  end
@@ -8,16 +8,19 @@ module PhlexKit
8
8
  # PhlexKit's completion of it. Keyboard actions live here because focus stays
9
9
  # in this field (see ComboboxInputTrigger). See combobox.rb.
10
10
  class ComboboxBadgeTrigger < BaseComponent
11
- def initialize(placeholder: "", clear_button: false, **attrs)
11
+ def initialize(placeholder: "", clear_button: false, list_id: nil, **attrs)
12
12
  @placeholder = placeholder
13
13
  @clear_button = clear_button
14
+ @list_id = list_id
14
15
  @attrs = attrs
15
16
  end
16
17
 
17
18
  def view_template
19
+ # The ARIA combobox role sits on the field itself (the interactive
20
+ # element), not this wrapper. Pass `list_id:` matching the ComboboxList
21
+ # id to wire aria-controls statically — otherwise the controller wires it.
18
22
  div(**mix({
19
23
  class: "pk-combobox-badge-trigger",
20
- aria: { haspopup: "listbox", expanded: "false" },
21
24
  data: {
22
25
  placeholder: @placeholder,
23
26
  phlex_kit__combobox_target: "trigger",
@@ -34,6 +37,8 @@ module PhlexKit
34
37
  div(class: "pk-combobox-badge-container pk-hidden", data: { phlex_kit__combobox_target: "badgeContainer" })
35
38
  input(
36
39
  type: :text,
40
+ role: "combobox",
41
+ aria: { haspopup: "listbox", expanded: "false", autocomplete: "list", controls: @list_id },
37
42
  placeholder: @placeholder,
38
43
  autocomplete: "off",
39
44
  autocorrect: "off",
@@ -10,6 +10,9 @@ module PhlexKit
10
10
  def view_template
11
11
  input(**mix({
12
12
  type: :checkbox,
13
+ # The option label is the interactive surface (ARIA option); the real
14
+ # input must not be a second tab stop inside it.
15
+ tabindex: "-1",
13
16
  class: "pk-combobox-checkbox",
14
17
  data: {
15
18
  phlex_kit__combobox_target: "input",