phlex_kit 0.4.0 → 0.5.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 (138) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/phlex_kit/_tokens.css +16 -0
  3. data/app/assets/stylesheets/phlex_kit/phlex_kit.css +1 -0
  4. data/app/components/phlex_kit/alert/alert.css +60 -22
  5. data/app/components/phlex_kit/alert/alert.rb +11 -7
  6. data/app/components/phlex_kit/alert/alert_action.rb +14 -0
  7. data/app/components/phlex_kit/alert/alert_title.rb +3 -2
  8. data/app/components/phlex_kit/alert_dialog/alert_dialog.css +5 -2
  9. data/app/components/phlex_kit/avatar/avatar.css +80 -9
  10. data/app/components/phlex_kit/avatar/avatar.rb +6 -4
  11. data/app/components/phlex_kit/avatar/avatar_badge.rb +15 -0
  12. data/app/components/phlex_kit/avatar/avatar_group_count.rb +14 -0
  13. data/app/components/phlex_kit/badge/badge.css +61 -39
  14. data/app/components/phlex_kit/badge/badge.rb +24 -7
  15. data/app/components/phlex_kit/breadcrumb/breadcrumb.css +2 -2
  16. data/app/components/phlex_kit/bubble/bubble.css +49 -8
  17. data/app/components/phlex_kit/button/button.css +71 -26
  18. data/app/components/phlex_kit/button/button.rb +20 -13
  19. data/app/components/phlex_kit/button_group/button_group.css +59 -8
  20. data/app/components/phlex_kit/button_group/button_group.rb +11 -5
  21. data/app/components/phlex_kit/button_group/button_group_separator.rb +14 -0
  22. data/app/components/phlex_kit/button_group/button_group_text.rb +14 -0
  23. data/app/components/phlex_kit/calendar/calendar.css +66 -15
  24. data/app/components/phlex_kit/calendar/calendar.rb +87 -19
  25. data/app/components/phlex_kit/calendar/calendar_controller.js +226 -75
  26. data/app/components/phlex_kit/calendar/calendar_days.rb +7 -6
  27. data/app/components/phlex_kit/calendar/calendar_weekdays.rb +3 -1
  28. data/app/components/phlex_kit/checkbox/checkbox.css +52 -7
  29. data/app/components/phlex_kit/collapsible/collapsible.css +10 -1
  30. data/app/components/phlex_kit/collapsible/collapsible_controller.js +9 -2
  31. data/app/components/phlex_kit/combobox/combobox.css +37 -3
  32. data/app/components/phlex_kit/combobox/combobox.rb +3 -1
  33. data/app/components/phlex_kit/combobox/combobox_controller.js +9 -1
  34. data/app/components/phlex_kit/combobox/combobox_input_trigger.rb +7 -2
  35. data/app/components/phlex_kit/command/command.css +58 -22
  36. data/app/components/phlex_kit/command/command_controller.js +3 -1
  37. data/app/components/phlex_kit/command/command_input.rb +29 -25
  38. data/app/components/phlex_kit/command/command_separator.rb +18 -0
  39. data/app/components/phlex_kit/command/command_shortcut.rb +13 -0
  40. data/app/components/phlex_kit/context_menu/context_menu.css +78 -6
  41. data/app/components/phlex_kit/context_menu/context_menu_checkbox_item.rb +27 -0
  42. data/app/components/phlex_kit/context_menu/context_menu_group.rb +10 -0
  43. data/app/components/phlex_kit/context_menu/context_menu_item.rb +13 -3
  44. data/app/components/phlex_kit/context_menu/context_menu_radio_group.rb +10 -0
  45. data/app/components/phlex_kit/context_menu/context_menu_radio_item.rb +30 -0
  46. data/app/components/phlex_kit/context_menu/context_menu_shortcut.rb +11 -0
  47. data/app/components/phlex_kit/context_menu/context_menu_sub.rb +11 -0
  48. data/app/components/phlex_kit/context_menu/context_menu_sub_content.rb +12 -0
  49. data/app/components/phlex_kit/context_menu/context_menu_sub_trigger.rb +17 -0
  50. data/app/components/phlex_kit/dialog/dialog.css +34 -16
  51. data/app/components/phlex_kit/dialog/dialog_close.rb +13 -0
  52. data/app/components/phlex_kit/dialog/dialog_content.rb +13 -5
  53. data/app/components/phlex_kit/drawer/drawer.css +56 -17
  54. data/app/components/phlex_kit/drawer/drawer_content.rb +8 -4
  55. data/app/components/phlex_kit/empty/empty.css +13 -8
  56. data/app/components/phlex_kit/field/field.css +152 -0
  57. data/app/components/phlex_kit/field/field.rb +35 -0
  58. data/app/components/phlex_kit/field/field_content.rb +11 -0
  59. data/app/components/phlex_kit/field/field_description.rb +10 -0
  60. data/app/components/phlex_kit/field/field_error.rb +29 -0
  61. data/app/components/phlex_kit/field/field_group.rb +12 -0
  62. data/app/components/phlex_kit/field/field_label.rb +12 -0
  63. data/app/components/phlex_kit/field/field_legend.rb +17 -0
  64. data/app/components/phlex_kit/field/field_separator.rb +15 -0
  65. data/app/components/phlex_kit/field/field_set.rb +10 -0
  66. data/app/components/phlex_kit/field/field_title.rb +11 -0
  67. data/app/components/phlex_kit/hover_card/hover_card.css +12 -4
  68. data/app/components/phlex_kit/hover_card/hover_card_content.rb +11 -2
  69. data/app/components/phlex_kit/input/input.css +45 -11
  70. data/app/components/phlex_kit/input_group/input_group.css +88 -12
  71. data/app/components/phlex_kit/input_group/input_group_addon.rb +11 -4
  72. data/app/components/phlex_kit/input_group/input_group_button.rb +18 -0
  73. data/app/components/phlex_kit/input_otp/input_otp.css +29 -5
  74. data/app/components/phlex_kit/item/item.css +56 -8
  75. data/app/components/phlex_kit/item/item.rb +13 -3
  76. data/app/components/phlex_kit/item/item_footer.rb +8 -0
  77. data/app/components/phlex_kit/item/item_header.rb +8 -0
  78. data/app/components/phlex_kit/item/item_media.rb +13 -3
  79. data/app/components/phlex_kit/item/item_separator.rb +10 -0
  80. data/app/components/phlex_kit/kbd/kbd.css +10 -0
  81. data/app/components/phlex_kit/marker/marker.css +29 -9
  82. data/app/components/phlex_kit/marker/marker.rb +13 -3
  83. data/app/components/phlex_kit/menubar/menubar.css +88 -17
  84. data/app/components/phlex_kit/menubar/menubar_checkbox_item.rb +26 -0
  85. data/app/components/phlex_kit/menubar/menubar_group.rb +10 -0
  86. data/app/components/phlex_kit/menubar/menubar_item.rb +9 -4
  87. data/app/components/phlex_kit/menubar/menubar_label.rb +10 -0
  88. data/app/components/phlex_kit/menubar/menubar_radio_group.rb +10 -0
  89. data/app/components/phlex_kit/menubar/menubar_radio_item.rb +29 -0
  90. data/app/components/phlex_kit/menubar/menubar_shortcut.rb +10 -0
  91. data/app/components/phlex_kit/menubar/menubar_sub.rb +10 -0
  92. data/app/components/phlex_kit/menubar/menubar_sub_content.rb +11 -0
  93. data/app/components/phlex_kit/menubar/menubar_sub_trigger.rb +17 -0
  94. data/app/components/phlex_kit/message/message.css +8 -1
  95. data/app/components/phlex_kit/message_scroller/message_scroller.css +18 -0
  96. data/app/components/phlex_kit/native_select/native_select.css +25 -5
  97. data/app/components/phlex_kit/navigation_menu/navigation_menu.css +9 -6
  98. data/app/components/phlex_kit/pagination/pagination.css +10 -3
  99. data/app/components/phlex_kit/pagination/pagination_link.rb +21 -0
  100. data/app/components/phlex_kit/pagination/pagination_next.rb +20 -0
  101. data/app/components/phlex_kit/pagination/pagination_previous.rb +21 -0
  102. data/app/components/phlex_kit/popover/popover.css +15 -4
  103. data/app/components/phlex_kit/popover/popover_content.rb +11 -2
  104. data/app/components/phlex_kit/popover/popover_description.rb +10 -0
  105. data/app/components/phlex_kit/popover/popover_header.rb +10 -0
  106. data/app/components/phlex_kit/popover/popover_title.rb +10 -0
  107. data/app/components/phlex_kit/progress/progress.css +5 -5
  108. data/app/components/phlex_kit/radio_button/radio_button.css +46 -2
  109. data/app/components/phlex_kit/radio_group/radio_group.css +2 -2
  110. data/app/components/phlex_kit/resizable/resizable.css +17 -0
  111. data/app/components/phlex_kit/resizable/resizable_handle.rb +7 -3
  112. data/app/components/phlex_kit/select/select.css +46 -10
  113. data/app/components/phlex_kit/select/select_separator.rb +10 -0
  114. data/app/components/phlex_kit/select/select_trigger.rb +5 -2
  115. data/app/components/phlex_kit/sheet/sheet.css +18 -9
  116. data/app/components/phlex_kit/sheet/sheet_close.rb +10 -0
  117. data/app/components/phlex_kit/sheet/sheet_content.rb +8 -4
  118. data/app/components/phlex_kit/sidebar/sidebar.css +76 -9
  119. data/app/components/phlex_kit/sidebar/sidebar_controller.js +22 -7
  120. data/app/components/phlex_kit/sidebar/sidebar_menu_button.rb +8 -3
  121. data/app/components/phlex_kit/sidebar/sidebar_rail.rb +21 -0
  122. data/app/components/phlex_kit/sidebar/sidebar_wrapper.rb +25 -8
  123. data/app/components/phlex_kit/skeleton/skeleton.css +1 -1
  124. data/app/components/phlex_kit/slider/slider.css +33 -12
  125. data/app/components/phlex_kit/switch/switch.css +50 -13
  126. data/app/components/phlex_kit/switch/switch.rb +5 -2
  127. data/app/components/phlex_kit/table/table.css +8 -4
  128. data/app/components/phlex_kit/tabs/tabs.css +72 -8
  129. data/app/components/phlex_kit/tabs/tabs.rb +7 -2
  130. data/app/components/phlex_kit/tabs/tabs_list.rb +11 -2
  131. data/app/components/phlex_kit/textarea/textarea.css +33 -13
  132. data/app/components/phlex_kit/toast/toast.css +1 -1
  133. data/app/components/phlex_kit/toggle/toggle.css +25 -10
  134. data/app/components/phlex_kit/toggle_group/toggle_group.css +7 -6
  135. data/app/components/phlex_kit/tooltip/tooltip.css +37 -10
  136. data/app/components/phlex_kit/tooltip/tooltip_content.rb +9 -4
  137. data/lib/phlex_kit/version.rb +1 -1
  138. metadata +50 -1
@@ -1,20 +1,65 @@
1
- /* Co-located with checkbox.rb — UI::Checkbox. ruby_ui draws a fully custom box
2
- with peer/checked Tailwind utilities; we instead let the native control render
3
- and tint it with the brand token via accent-color (themes correctly on the
4
- dark background, no SVG). Vanilla CSS, our own look, redesign later. Theme
5
- tokens come from the global stylesheet. */
1
+ /* Co-located with checkbox.rb — PhlexKit::Checkbox. Matched to shadcn/ui's
2
+ current checkbox: appearance:none custom box (size-4, 4px radius,
3
+ input-border, translucent input fill in dark), solid brand fill when
4
+ checked with a masked check glyph (currentColor-free the mask carries
5
+ the shape, the fill is brand-ink), an :indeterminate minus, an extended
6
+ touch target (::before inset bleed, their after:-inset-x-3/-inset-y-2),
7
+ focus + invalid rings. Native input semantics kept — no JS. */
6
8
  .pk-checkbox {
9
+ appearance: none;
10
+ -webkit-appearance: none;
11
+ position: relative;
12
+ display: grid;
13
+ place-content: center;
14
+ flex-shrink: 0;
7
15
  width: 1rem;
8
16
  height: 1rem;
9
- border-radius: calc(var(--pk-radius) - 4px);
10
- accent-color: var(--pk-brand);
17
+ margin: 0;
18
+ border: 1px solid var(--pk-input);
19
+ border-radius: 4px;
20
+ background: color-mix(in oklab, var(--pk-input) 30%, transparent);
11
21
  cursor: pointer;
22
+ transition: border-color .15s ease, background-color .15s ease, box-shadow .15s ease;
23
+ }
24
+ /* Extended touch target. */
25
+ .pk-checkbox::before { content: ""; position: absolute; inset: -.5rem -.75rem; }
26
+ .pk-checkbox:checked, .pk-checkbox:indeterminate {
27
+ background: var(--pk-brand);
28
+ border-color: var(--pk-brand);
29
+ }
30
+ .pk-checkbox:checked::after, .pk-checkbox:indeterminate::after {
31
+ content: "";
32
+ width: .875rem;
33
+ height: .875rem;
34
+ background: var(--pk-brand-ink);
35
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat;
36
+ }
37
+ .pk-checkbox:indeterminate::after {
38
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E") center / contain no-repeat;
12
39
  }
13
40
  .pk-checkbox:focus-visible {
14
41
  outline: none;
42
+ border-color: var(--pk-ring);
15
43
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
16
44
  }
45
+ .pk-checkbox[aria-invalid="true"] {
46
+ border-color: color-mix(in oklab, var(--pk-red) 50%, transparent);
47
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 40%, transparent);
48
+ }
49
+ .pk-checkbox[aria-invalid="true"]:checked { border-color: var(--pk-brand); }
17
50
  .pk-checkbox:disabled { cursor: not-allowed; opacity: .5; }
51
+ :root[data-theme="light"] .pk-checkbox:not(:checked):not(:indeterminate) { background: transparent; }
52
+ :root[data-theme="light"] .pk-checkbox[aria-invalid="true"] {
53
+ border-color: var(--pk-red);
54
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
55
+ }
56
+ @media (prefers-color-scheme: light) {
57
+ :root[data-theme="system"] .pk-checkbox:not(:checked):not(:indeterminate) { background: transparent; }
58
+ :root[data-theme="system"] .pk-checkbox[aria-invalid="true"] {
59
+ border-color: var(--pk-red);
60
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
61
+ }
62
+ }
18
63
 
19
64
  /* A checkbox paired inline with its label (ruby_ui's `flex items-center
20
65
  space-x-3` row). Used for checkbox lists like store-assignment; vertical
@@ -1,3 +1,12 @@
1
- /* Co-located with collapsible.rb. Theme tokens from the global stylesheet. */
1
+ /* Co-located with collapsible.rb. Theme tokens from the global stylesheet.
2
+ The controller stamps data-state="open|closed" on the root (shadcn's
3
+ styling hook). Give a trigger glyph .pk-collapsible-chevron to rotate it
4
+ when open — 180deg by default, override with --pk-collapsible-rotate
5
+ (file trees use 90deg on a chevron-right). Scoped to the nearest root so
6
+ nested collapsibles rotate independently. */
2
7
  .pk-collapsible-trigger { cursor: pointer; }
3
8
  .pk-collapsible-content { overflow-y: hidden; }
9
+ .pk-collapsible-chevron { transition: transform .15s ease; }
10
+ .pk-collapsible[data-state="open"] > .pk-collapsible-trigger .pk-collapsible-chevron {
11
+ transform: rotate(var(--pk-collapsible-rotate, 180deg));
12
+ }
@@ -9,6 +9,13 @@ export default class extends Controller {
9
9
  toggle() { this.openValue = !this.openValue }
10
10
  openValueChanged(isOpen) { isOpen ? this.open() : this.close() }
11
11
 
12
- open() { if (this.hasContentTarget) { this.contentTarget.classList.remove("pk-hidden"); this.openValue = true } }
13
- close() { if (this.hasContentTarget) { this.contentTarget.classList.add("pk-hidden"); this.openValue = false } }
12
+ open() {
13
+ if (this.hasContentTarget) { this.contentTarget.classList.remove("pk-hidden"); this.openValue = true }
14
+ this.element.dataset.state = "open";
15
+ }
16
+
17
+ close() {
18
+ if (this.hasContentTarget) { this.contentTarget.classList.add("pk-hidden"); this.openValue = false }
19
+ this.element.dataset.state = "closed";
20
+ }
14
21
  }
@@ -24,10 +24,15 @@
24
24
  cursor: pointer;
25
25
  transition: background-color .15s ease, border-color .15s ease, box-shadow .15s ease;
26
26
  }
27
- .pk-combobox-trigger:hover { background: var(--pk-accent); }
27
+ .pk-combobox-trigger { background: color-mix(in oklab, var(--pk-input) 30%, transparent); }
28
+ .pk-combobox-trigger:hover { background: color-mix(in oklab, var(--pk-input) 50%, transparent); }
29
+ .pk-combobox-trigger[aria-invalid="true"] {
30
+ border-color: color-mix(in oklab, var(--pk-red) 50%, transparent);
31
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 40%, transparent);
32
+ }
28
33
  .pk-combobox-trigger:focus-visible {
29
34
  outline: none;
30
- border-color: var(--pk-brand);
35
+ border-color: var(--pk-ring);
31
36
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
32
37
  }
33
38
  .pk-combobox-trigger:disabled,
@@ -177,8 +182,18 @@
177
182
  cursor: text;
178
183
  transition: border-color .15s ease, box-shadow .15s ease;
179
184
  }
185
+ .pk-combobox-input-trigger { background: color-mix(in oklab, var(--pk-input) 30%, transparent); }
186
+ .pk-combobox-input-trigger[aria-invalid="true"] {
187
+ border-color: color-mix(in oklab, var(--pk-red) 50%, transparent);
188
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 40%, transparent);
189
+ }
190
+ .pk-combobox-input-trigger:has(.pk-combobox-input-trigger-field:disabled) {
191
+ pointer-events: none;
192
+ opacity: .5;
193
+ background: color-mix(in oklab, var(--pk-input) 80%, transparent);
194
+ }
180
195
  .pk-combobox-input-trigger:focus-within {
181
- border-color: var(--pk-brand);
196
+ border-color: var(--pk-ring);
182
197
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
183
198
  }
184
199
  .pk-combobox-input-trigger-field {
@@ -308,3 +323,22 @@
308
323
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--pk-ring) 50%, transparent);
309
324
  }
310
325
  .pk-combobox-clear-button svg { width: .875rem; height: .875rem; }
326
+
327
+ :root[data-theme="light"] .pk-combobox-trigger,
328
+ :root[data-theme="light"] .pk-combobox-input-trigger { background: transparent; }
329
+ :root[data-theme="light"] .pk-combobox-trigger:hover { background: transparent; }
330
+ :root[data-theme="light"] .pk-combobox-trigger[aria-invalid="true"],
331
+ :root[data-theme="light"] .pk-combobox-input-trigger[aria-invalid="true"] {
332
+ border-color: var(--pk-red);
333
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
334
+ }
335
+ @media (prefers-color-scheme: light) {
336
+ :root[data-theme="system"] .pk-combobox-trigger,
337
+ :root[data-theme="system"] .pk-combobox-input-trigger { background: transparent; }
338
+ :root[data-theme="system"] .pk-combobox-trigger:hover { background: transparent; }
339
+ :root[data-theme="system"] .pk-combobox-trigger[aria-invalid="true"],
340
+ :root[data-theme="system"] .pk-combobox-input-trigger[aria-invalid="true"] {
341
+ border-color: var(--pk-red);
342
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
343
+ }
344
+ }
@@ -13,8 +13,9 @@ module PhlexKit
13
13
  # latter two shipped unfinished in ruby_ui (no controller support) and are
14
14
  # completed here. Tailwind → vanilla `.pk-combobox*` (combobox.css).
15
15
  class Combobox < BaseComponent
16
- def initialize(term: nil, **attrs)
16
+ def initialize(term: nil, auto_highlight: false, **attrs)
17
17
  @term = term
18
+ @auto_highlight = auto_highlight
18
19
  @attrs = attrs
19
20
  end
20
21
 
@@ -25,6 +26,7 @@ module PhlexKit
25
26
  data: {
26
27
  controller: "phlex-kit--combobox",
27
28
  phlex_kit__combobox_term_value: @term,
29
+ phlex_kit__combobox_auto_highlight_value: (@auto_highlight ? "true" : nil),
28
30
  action: "turbo:morph@window->phlex-kit--combobox#updateTriggerContent click@window->phlex-kit--combobox#onClickOutside"
29
31
  }
30
32
  }, @attrs), &)
@@ -14,7 +14,8 @@ import { Controller } from "@hotwired/stimulus";
14
14
  export default class extends Controller {
15
15
  static values = {
16
16
  term: String,
17
- minPopoverWidth: { type: Number, default: 240 }
17
+ minPopoverWidth: { type: Number, default: 240 },
18
+ autoHighlight: { type: Boolean, default: false }
18
19
  }
19
20
 
20
21
  static targets = [
@@ -241,6 +242,13 @@ export default class extends Controller {
241
242
  if (this.hasEmptyStateTarget) {
242
243
  this.emptyStateTarget.classList.toggle("pk-hidden", resultCount !== 0)
243
244
  }
245
+
246
+ // autoHighlight: keep the first visible option marked while typing so
247
+ // Enter picks it immediately (their autoHighlight prop).
248
+ if (this.autoHighlightValue && filterTerm && resultCount > 0) {
249
+ this.selectedItemIndex = 0
250
+ this.focusSelectedInput()
251
+ }
244
252
  }
245
253
 
246
254
  keyDownPressed(e) {
@@ -9,15 +9,19 @@ module PhlexKit
9
9
  # popover's own bindings only fire for the button+search layout.
10
10
  # See combobox.rb.
11
11
  class ComboboxInputTrigger < BaseComponent
12
- def initialize(placeholder: "", **attrs)
12
+ def initialize(placeholder: "", disabled: false, invalid: false, **attrs)
13
13
  @placeholder = placeholder
14
+ @disabled = disabled
15
+ @invalid = invalid
14
16
  @attrs = attrs
15
17
  end
16
18
 
17
19
  def view_template
20
+ wrapper_aria = { haspopup: "listbox", expanded: "false" }
21
+ wrapper_aria[:invalid] = "true" if @invalid
18
22
  div(**mix({
19
23
  class: "pk-combobox-input-trigger",
20
- aria: { haspopup: "listbox", expanded: "false" },
24
+ aria: wrapper_aria,
21
25
  data: {
22
26
  placeholder: @placeholder,
23
27
  phlex_kit__combobox_target: "trigger",
@@ -34,6 +38,7 @@ module PhlexKit
34
38
  input(
35
39
  type: :text,
36
40
  placeholder: @placeholder,
41
+ disabled: @disabled,
37
42
  autocomplete: "off",
38
43
  autocorrect: "off",
39
44
  spellcheck: "false",
@@ -1,9 +1,16 @@
1
- /* Co-located with command.rb — UI::Command (+ CommandDialog overlay). ruby_ui's
2
- structure kept (template cloned into <body>, custom backdrop + centered panel,
3
- like the shipped sheet); fuse.js replaced with a substring filter in the
4
- controller, Tailwind with vanilla CSS on the palette tokens. Geometry from
5
- ruby_ui (h-10 input, p-1 groups, px-2 py-1.5 rows, divide-y list). Theme
6
- tokens come from the global stylesheet. */
1
+ /* Co-located with command.rb — PhlexKit::Command (+ CommandDialog overlay).
2
+ Matched to shadcn/ui's current command (nova): the palette is a padded
3
+ rounded-xl popover surface; the search field is a bordered PILL inside it
4
+ (input-wrapper > input-group) rather than a full-width divider row; groups
5
+ are no longer divide-y an explicit CommandSeparator (hidden while
6
+ filtering) draws the line; selected rows fill with the muted surface.
7
+ fuse.js stays replaced by the controller's substring scorer. */
8
+ .pk-command {
9
+ background: var(--pk-surface);
10
+ color: var(--pk-text);
11
+ border-radius: .75rem;
12
+ padding: .25rem;
13
+ }
7
14
  .pk-command-dialog-trigger { display: inline-block; }
8
15
 
9
16
  /* Backdrop of the cloned overlay. */
@@ -12,8 +19,8 @@
12
19
  inset: 0;
13
20
  z-index: 50;
14
21
  pointer-events: auto;
15
- background: rgb(0 0 0 / .5);
16
- backdrop-filter: blur(8px);
22
+ background: rgb(0 0 0 / .1);
23
+ backdrop-filter: blur(4px);
17
24
  }
18
25
 
19
26
  /* Centered palette panel. */
@@ -24,12 +31,10 @@
24
31
  transform: translate(-50%, -50%);
25
32
  z-index: 50;
26
33
  pointer-events: auto;
27
- display: grid;
28
- gap: 1rem;
29
34
  width: 100%;
30
35
  max-width: 32rem;
31
36
  border: 1px solid var(--pk-border);
32
- border-radius: var(--pk-radius);
37
+ border-radius: .75rem;
33
38
  background: var(--pk-surface);
34
39
  color: var(--pk-text);
35
40
  box-shadow: 0 10px 30px rgb(0 0 0 / .35);
@@ -40,18 +45,22 @@
40
45
  .pk-command-dialog.xl { max-width: 56rem; }
41
46
  .pk-command-dialog.full { max-width: 100%; }
42
47
 
43
- /* Search row. */
44
- .pk-command-input-row {
48
+ /* Search pill (their input-wrapper > input-group). */
49
+ .pk-command-input-wrapper { padding: .25rem .25rem 0; }
50
+ .pk-command-input-pill {
45
51
  display: flex;
46
52
  align-items: center;
47
- padding: 0 .75rem;
48
- border-bottom: 1px solid var(--pk-border);
53
+ gap: .5rem;
54
+ height: 2rem;
55
+ padding: 0 .5rem;
56
+ border: 1px solid color-mix(in oklab, var(--pk-input) 30%, transparent);
57
+ border-radius: var(--pk-radius);
58
+ background: color-mix(in oklab, var(--pk-input) 30%, transparent);
49
59
  }
50
- .pk-command-input-icon { flex: none; width: 1rem; height: 1rem; margin-right: .375rem; }
60
+ .pk-command-input-icon { flex: none; width: 1rem; height: 1rem; opacity: .5; }
51
61
  .pk-command-input {
52
62
  width: 100%;
53
- height: 2.5rem;
54
- padding: .75rem 0;
63
+ height: 100%;
55
64
  border: none;
56
65
  outline: none;
57
66
  background: transparent;
@@ -62,8 +71,16 @@
62
71
  .pk-command-input::placeholder { color: var(--pk-muted); }
63
72
  .pk-command-input:disabled { cursor: not-allowed; opacity: .5; }
64
73
 
65
- /* Groups, divided. */
66
- .pk-command-list > * + * { border-top: 1px solid var(--pk-border); }
74
+ /* Scrollable results (their max-h-72, hidden scrollbar). */
75
+ .pk-command-list {
76
+ max-height: 18rem;
77
+ overflow-y: auto;
78
+ scroll-padding-block: .25rem;
79
+ outline: none;
80
+ scrollbar-width: none;
81
+ }
82
+ .pk-command-list::-webkit-scrollbar { display: none; }
83
+
67
84
  .pk-command-group {
68
85
  overflow: hidden;
69
86
  padding: .25rem;
@@ -76,6 +93,12 @@
76
93
  color: var(--pk-muted);
77
94
  }
78
95
 
96
+ .pk-command-separator {
97
+ height: 1px;
98
+ margin: 0 -.25rem;
99
+ background: var(--pk-border);
100
+ }
101
+
79
102
  /* One result row. */
80
103
  .pk-command-item {
81
104
  position: relative;
@@ -87,18 +110,31 @@
87
110
  font-size: .875rem;
88
111
  color: var(--pk-text);
89
112
  text-decoration: none;
90
- cursor: pointer;
113
+ cursor: default;
91
114
  user-select: none;
92
115
  outline: none;
93
116
  transition: background-color .12s ease;
94
117
  }
118
+ .pk-command-item svg:not([width]) { width: 1rem; height: 1rem; }
95
119
  .pk-command-item:hover,
96
- .pk-command-item[aria-selected="true"] { background: var(--pk-accent); }
120
+ .pk-command-item[aria-selected="true"] { background: var(--pk-surface-2); color: var(--pk-text); }
97
121
  .pk-command-item[data-disabled] { pointer-events: none; opacity: .5; }
98
122
 
123
+ .pk-command-shortcut {
124
+ margin-left: auto;
125
+ font-size: .75rem;
126
+ letter-spacing: .1em;
127
+ color: var(--pk-muted);
128
+ }
129
+ .pk-command-item:hover .pk-command-shortcut,
130
+ .pk-command-item[aria-selected="true"] .pk-command-shortcut { color: var(--pk-text); }
131
+
99
132
  /* Filter empty state (hidden/shown by the controller). */
100
133
  .pk-command-empty {
101
134
  padding: 1.5rem 0;
102
135
  text-align: center;
103
136
  font-size: .875rem;
104
137
  }
138
+ @supports not (backdrop-filter: blur(1px)) {
139
+ .pk-command-overlay { background: rgb(0 0 0 / .5); }
140
+ }
@@ -33,7 +33,7 @@ function fuzzyScore(query, value) {
33
33
  // nav, dismiss) is unchanged.
34
34
  // Connects to data-controller="phlex-kit--command"
35
35
  export default class extends Controller {
36
- static targets = ["input", "group", "item", "empty"];
36
+ static targets = ["input", "group", "item", "empty", "separator"];
37
37
 
38
38
  connect() {
39
39
  this.selectedIndex = -1;
@@ -76,6 +76,7 @@ export default class extends Controller {
76
76
  );
77
77
 
78
78
  this.toggleVisibility(this.emptyTargets, results.length === 0);
79
+ this.toggleVisibility(this.separatorTargets, false);
79
80
  this.updateGroupVisibility();
80
81
  }
81
82
 
@@ -96,6 +97,7 @@ export default class extends Controller {
96
97
  resetVisibility() {
97
98
  this.toggleVisibility(this.itemTargets, true);
98
99
  this.toggleVisibility(this.groupTargets, true);
100
+ this.toggleVisibility(this.separatorTargets, true);
99
101
  this.toggleVisibility(this.emptyTargets, false);
100
102
  }
101
103
 
@@ -1,6 +1,8 @@
1
1
  module PhlexKit
2
2
  # Search field at the top of a PhlexKit::Command — filters the items as you
3
- # type, arrows/enter drive selection, esc dismisses. See command.rb.
3
+ # type, arrows/enter drive selection, esc dismisses. Renders as shadcn's
4
+ # current bordered pill inside the padded palette (input-wrapper >
5
+ # input-group). See command.rb.
4
6
  class CommandInput < BaseComponent
5
7
  def initialize(placeholder: "Type a command or search...", **attrs)
6
8
  @placeholder = placeholder
@@ -8,30 +10,32 @@ module PhlexKit
8
10
  end
9
11
 
10
12
  def view_template
11
- div(class: "pk-command-input-row") do
12
- search_icon
13
- input(**mix({
14
- type: :text,
15
- class: "pk-command-input",
16
- placeholder: @placeholder,
17
- autocomplete: "off",
18
- autocorrect: "off",
19
- spellcheck: "false",
20
- autofocus: true,
21
- role: "combobox",
22
- value: "",
23
- aria: { autocomplete: "list", expanded: "true" },
24
- data: {
25
- phlex_kit__command_target: "input",
26
- action: [
27
- "input->phlex-kit--command#filter",
28
- "keydown.down->phlex-kit--command#handleKeydown",
29
- "keydown.up->phlex-kit--command#handleKeydown",
30
- "keydown.enter->phlex-kit--command#handleKeydown",
31
- "keydown.esc->phlex-kit--command#dismiss"
32
- ].join(" ")
33
- }
34
- }, @attrs))
13
+ div(class: "pk-command-input-wrapper") do
14
+ div(class: "pk-command-input-pill") do
15
+ search_icon
16
+ input(**mix({
17
+ type: :text,
18
+ class: "pk-command-input",
19
+ placeholder: @placeholder,
20
+ autocomplete: "off",
21
+ autocorrect: "off",
22
+ spellcheck: "false",
23
+ autofocus: true,
24
+ role: "combobox",
25
+ value: "",
26
+ aria: { autocomplete: "list", expanded: "true" },
27
+ data: {
28
+ phlex_kit__command_target: "input",
29
+ action: [
30
+ "input->phlex-kit--command#filter",
31
+ "keydown.down->phlex-kit--command#handleKeydown",
32
+ "keydown.up->phlex-kit--command#handleKeydown",
33
+ "keydown.enter->phlex-kit--command#handleKeydown",
34
+ "keydown.esc->phlex-kit--command#dismiss"
35
+ ].join(" ")
36
+ }
37
+ }, @attrs))
38
+ end
35
39
  end
36
40
  end
37
41
 
@@ -0,0 +1,18 @@
1
+ module PhlexKit
2
+ # Hairline between CommandGroups, ported from shadcn/ui's CommandSeparator.
3
+ # Hidden while a filter query is active (like cmdk). See command.rb.
4
+ class CommandSeparator < BaseComponent
5
+ def initialize(**attrs)
6
+ @attrs = attrs
7
+ end
8
+
9
+ def view_template
10
+ div(**mix({
11
+ class: "pk-command-separator",
12
+ role: "separator",
13
+ aria: { hidden: true },
14
+ data: { phlex_kit__command_target: "separator" }
15
+ }, @attrs))
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ module PhlexKit
2
+ # Right-aligned keyboard hint on a CommandItem, ported from shadcn/ui's
3
+ # CommandShortcut. See command.rb.
4
+ class CommandShortcut < BaseComponent
5
+ def initialize(**attrs)
6
+ @attrs = attrs
7
+ end
8
+
9
+ def view_template(&block)
10
+ span(**mix({ class: "pk-command-shortcut" }, @attrs), &block)
11
+ end
12
+ end
13
+ end
@@ -1,19 +1,91 @@
1
- /* Co-located with context_menu.rb. CSS-positioned at the cursor. Tokens global. */
1
+ /* Co-located with context_menu.rb. CSS-positioned at the cursor. Matches the
2
+ dropdown-menu grammar (shadcn's context menu shares the same part set):
3
+ gap-based rows, destructive variant, hidden-input checkbox/radio rows with
4
+ :has() indicators, pure-CSS hover submenus. Tokens global. */
2
5
  .pk-context-menu { position: relative; }
3
6
  .pk-context-menu-content {
4
- position: absolute; z-index: 50; min-width: 8rem; overflow: hidden;
7
+ position: absolute; z-index: 50; min-width: 8rem; overflow: visible;
5
8
  border: 1px solid var(--pk-border); border-radius: calc(var(--pk-radius) - 2px); background: var(--pk-surface);
6
9
  color: var(--pk-text); padding: .25rem; box-shadow: 0 8px 24px rgb(0 0 0 / .3); outline: none;
7
10
  }
8
11
  .pk-context-menu-item {
9
- position: relative; display: flex; cursor: pointer; user-select: none; align-items: center;
10
- border-radius: .125rem; padding: .375rem .5rem .375rem 2rem; font-size: .875rem;
11
- color: inherit; text-decoration: none; outline: none;
12
+ position: relative; display: flex; align-items: center; gap: .5rem;
13
+ width: 100%; cursor: pointer; user-select: none;
14
+ border-radius: calc(var(--pk-radius) - 4px); padding: .375rem .5rem; font-size: .875rem;
15
+ color: var(--pk-text); text-decoration: none; outline: none;
12
16
  }
17
+ .pk-context-menu-item > svg { flex: none; width: 1rem; height: 1rem; color: var(--pk-muted); }
13
18
  .pk-context-menu-item:hover, .pk-context-menu-item:focus { background: var(--pk-accent); color: var(--pk-text); }
14
19
  .pk-context-menu-item[data-disabled="true"] { pointer-events: none; opacity: .5; }
15
- .pk-context-menu-check { position: absolute; left: .5rem; display: inline-flex; height: .875rem; width: .875rem; align-items: center; justify-content: center; }
20
+ .pk-context-menu-item.inset { padding-left: 2rem; }
21
+ .pk-context-menu-check {
22
+ display: inline-flex; align-items: center; justify-content: center;
23
+ flex: none; width: 1rem; height: 1rem;
24
+ }
25
+ .pk-context-menu-check svg { width: .875rem; height: .875rem; }
16
26
  .pk-context-menu-shortcut { margin-left: auto; font-size: .75rem; letter-spacing: .1em; color: var(--pk-muted); }
17
27
  .pk-context-menu-label { padding: .375rem .5rem; font-size: .875rem; font-weight: 600; color: var(--pk-text); }
18
28
  .pk-context-menu-label.inset { padding-left: 2rem; }
19
29
  .pk-context-menu-separator { height: 1px; margin: .25rem -.25rem; background: var(--pk-border); }
30
+ .pk-context-menu-group { display: contents; }
31
+
32
+ /* Destructive rows (irreversible actions). */
33
+ .pk-context-menu-item.destructive { color: var(--pk-red); }
34
+ .pk-context-menu-item.destructive > svg { color: var(--pk-red); }
35
+ .pk-context-menu-item.destructive:hover,
36
+ .pk-context-menu-item.destructive:focus {
37
+ background: color-mix(in oklab, var(--pk-red) 12%, transparent);
38
+ color: var(--pk-red);
39
+ }
40
+
41
+ /* Checkbox / radio rows: real (hidden) inputs; indicator shows via :has. */
42
+ .pk-context-menu-item-input {
43
+ position: absolute;
44
+ width: 1px;
45
+ height: 1px;
46
+ opacity: 0;
47
+ pointer-events: none;
48
+ }
49
+ .pk-context-menu-checkbox-item,
50
+ .pk-context-menu-radio-item { position: relative; }
51
+ .pk-context-menu-item-indicator {
52
+ display: inline-flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ flex: none;
56
+ width: 1rem;
57
+ height: 1rem;
58
+ visibility: hidden;
59
+ }
60
+ .pk-context-menu-item-indicator svg { width: .875rem; height: .875rem; }
61
+ .pk-context-menu-radio-item .pk-context-menu-item-indicator svg { width: .5rem; height: .5rem; }
62
+ .pk-context-menu-item:has(.pk-context-menu-item-input:checked) .pk-context-menu-item-indicator { visibility: visible; }
63
+ .pk-context-menu-item:has(.pk-context-menu-item-input:focus-visible) {
64
+ box-shadow: 0 0 0 2px color-mix(in oklab, var(--pk-ring) 50%, transparent);
65
+ }
66
+
67
+ /* Submenu: trigger row + side panel, revealed on hover/focus — pure CSS. */
68
+ .pk-context-menu-sub { position: relative; }
69
+ .pk-context-menu-sub-chevron { margin-left: auto; width: 1rem; height: 1rem; }
70
+ .pk-context-menu-sub-content {
71
+ position: absolute;
72
+ top: -0.25rem;
73
+ left: 100%;
74
+ z-index: 60;
75
+ min-width: 10rem;
76
+ padding-left: .25rem; /* hover bridge over the gap */
77
+ display: none;
78
+ }
79
+ .pk-context-menu-sub-viewport {
80
+ min-width: 10rem;
81
+ border-radius: calc(var(--pk-radius) - 2px);
82
+ border: 1px solid var(--pk-border);
83
+ background: var(--pk-surface);
84
+ color: var(--pk-text);
85
+ padding: .25rem;
86
+ box-shadow: 0 8px 24px rgb(0 0 0 / .3);
87
+ }
88
+ .pk-context-menu-sub:hover > .pk-context-menu-sub-content,
89
+ .pk-context-menu-sub:focus-within > .pk-context-menu-sub-content { display: block; }
90
+ .pk-context-menu-sub:hover > .pk-context-menu-sub-trigger,
91
+ .pk-context-menu-sub:focus-within > .pk-context-menu-sub-trigger { background: var(--pk-accent); }
@@ -0,0 +1,27 @@
1
+ module PhlexKit
2
+ # Toggleable row, mirroring shadcn/ui's ContextMenuCheckboxItem: a <label>
3
+ # around a real (hidden) checkbox — clicking toggles it natively and the ✓
4
+ # shows via CSS, without closing the menu (matching Radix). Pass `name:`/
5
+ # `value:` to submit the state with a form. See context_menu.rb.
6
+ class ContextMenuCheckboxItem < BaseComponent
7
+ def initialize(checked: false, name: nil, value: "1", **attrs)
8
+ @checked = checked
9
+ @name = name
10
+ @value = value
11
+ @attrs = attrs
12
+ end
13
+
14
+ def view_template(&block)
15
+ label(**mix({
16
+ class: "pk-context-menu-item pk-context-menu-checkbox-item",
17
+ role: "menuitemcheckbox"
18
+ }, @attrs)) do
19
+ input(type: :checkbox, class: "pk-context-menu-item-input", name: @name, value: @value, checked: @checked)
20
+ span(class: "pk-context-menu-item-indicator", aria: { hidden: "true" }) do
21
+ render Icon.new(:check, size: nil)
22
+ end
23
+ yield if block
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,10 @@
1
+ module PhlexKit
2
+ # Grouping wrapper for related rows (with an optional leading
3
+ # ContextMenuLabel). Mirrors shadcn/ui's ContextMenuGroup. See context_menu.rb.
4
+ class ContextMenuGroup < BaseComponent
5
+ def initialize(**attrs) = (@attrs = attrs)
6
+ def view_template(&)
7
+ div(**mix({ class: "pk-context-menu-group", role: "group" }, @attrs), &)
8
+ end
9
+ end
10
+ end