phlex_kit 0.2.1 → 0.2.2

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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -6
  3. data/THIRD_PARTY_LICENSES +104 -0
  4. data/app/assets/stylesheets/phlex_kit/_tokens.css +72 -0
  5. data/app/assets/stylesheets/phlex_kit/phlex_kit.css +1 -1
  6. data/app/assets/stylesheets/phlex_kit/themes/claude.css +86 -0
  7. data/app/assets/stylesheets/phlex_kit/themes/neutral.css +95 -0
  8. data/app/assets/stylesheets/phlex_kit/themes/zinc.css +85 -0
  9. data/app/components/phlex_kit/accordion/accordion.css +13 -0
  10. data/app/components/phlex_kit/accordion/accordion.rb +11 -2
  11. data/app/components/phlex_kit/accordion/accordion_icon.rb +1 -3
  12. data/app/components/phlex_kit/accordion/accordion_item.rb +5 -2
  13. data/app/components/phlex_kit/alert_dialog/alert_dialog.css +45 -4
  14. data/app/components/phlex_kit/alert_dialog/alert_dialog_action.rb +3 -2
  15. data/app/components/phlex_kit/alert_dialog/alert_dialog_content.rb +6 -2
  16. data/app/components/phlex_kit/alert_dialog/alert_dialog_media.rb +14 -0
  17. data/app/components/phlex_kit/attachment/attachment.css +102 -2
  18. data/app/components/phlex_kit/attachment/attachment.rb +18 -6
  19. data/app/components/phlex_kit/attachment/attachment_action.rb +1 -6
  20. data/app/components/phlex_kit/attachment/attachment_group.rb +21 -0
  21. data/app/components/phlex_kit/attachment/attachment_media.rb +13 -4
  22. data/app/components/phlex_kit/attachment/attachment_trigger.rb +22 -0
  23. data/app/components/phlex_kit/breadcrumb/breadcrumb_ellipsis.rb +1 -5
  24. data/app/components/phlex_kit/breadcrumb/breadcrumb_separator.rb +1 -1
  25. data/app/components/phlex_kit/calendar/calendar_next.rb +1 -15
  26. data/app/components/phlex_kit/calendar/calendar_prev.rb +1 -15
  27. data/app/components/phlex_kit/card/card.css +50 -14
  28. data/app/components/phlex_kit/card/card.rb +6 -2
  29. data/app/components/phlex_kit/card/card_action.rb +14 -0
  30. data/app/components/phlex_kit/carousel/carousel_next.rb +1 -15
  31. data/app/components/phlex_kit/carousel/carousel_previous.rb +1 -15
  32. data/app/components/phlex_kit/codeblock/codeblock.css +1 -1
  33. data/app/components/phlex_kit/combobox/combobox_clear_button.rb +1 -15
  34. data/app/components/phlex_kit/combobox/combobox_input_trigger.rb +1 -12
  35. data/app/components/phlex_kit/combobox/combobox_item_indicator.rb +1 -15
  36. data/app/components/phlex_kit/combobox/combobox_search_input.rb +1 -14
  37. data/app/components/phlex_kit/combobox/combobox_trigger.rb +1 -14
  38. data/app/components/phlex_kit/command/command.css +2 -2
  39. data/app/components/phlex_kit/command/command_input.rb +1 -13
  40. data/app/components/phlex_kit/context_menu/context_menu_item.rb +1 -3
  41. data/app/components/phlex_kit/data_table/data_table_column_toggle.rb +1 -13
  42. data/app/components/phlex_kit/data_table/data_table_expand_toggle.rb +1 -12
  43. data/app/components/phlex_kit/data_table/data_table_sort_head.rb +2 -28
  44. data/app/components/phlex_kit/dialog/dialog.css +8 -2
  45. data/app/components/phlex_kit/dialog/dialog_content.rb +1 -3
  46. data/app/components/phlex_kit/drawer/drawer.css +1 -0
  47. data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +64 -0
  48. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_checkbox_item.rb +31 -0
  49. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_group.rb +11 -0
  50. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_item.rb +5 -2
  51. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_radio_group.rb +10 -0
  52. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_radio_item.rb +31 -0
  53. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_shortcut.rb +10 -0
  54. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub.rb +11 -0
  55. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_content.rb +12 -0
  56. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_trigger.rb +18 -0
  57. data/app/components/phlex_kit/icon/icon.rb +51 -0
  58. data/app/components/phlex_kit/marker/marker.css +25 -0
  59. data/app/components/phlex_kit/marker/marker.rb +18 -0
  60. data/app/components/phlex_kit/marker/marker_content.rb +7 -0
  61. data/app/components/phlex_kit/marker/marker_icon.rb +7 -0
  62. data/app/components/phlex_kit/native_select/native_select_icon.rb +1 -10
  63. data/app/components/phlex_kit/navigation_menu/navigation_menu_trigger.rb +1 -5
  64. data/app/components/phlex_kit/pagination/pagination_ellipsis.rb +1 -5
  65. data/app/components/phlex_kit/select/select_item.rb +1 -11
  66. data/app/components/phlex_kit/select/select_trigger.rb +1 -14
  67. data/app/components/phlex_kit/sheet/sheet.css +1 -1
  68. data/app/components/phlex_kit/sheet/sheet_content.rb +1 -3
  69. data/app/components/phlex_kit/sidebar/sidebar.css +129 -11
  70. data/app/components/phlex_kit/sidebar/sidebar.rb +19 -6
  71. data/app/components/phlex_kit/sidebar/sidebar_group_action.rb +14 -0
  72. data/app/components/phlex_kit/sidebar/sidebar_group_content.rb +13 -0
  73. data/app/components/phlex_kit/sidebar/sidebar_group_label.rb +13 -0
  74. data/app/components/phlex_kit/sidebar/sidebar_input.rb +14 -0
  75. data/app/components/phlex_kit/sidebar/sidebar_menu_action.rb +13 -0
  76. data/app/components/phlex_kit/sidebar/sidebar_menu_badge.rb +13 -0
  77. data/app/components/phlex_kit/sidebar/sidebar_menu_skeleton.rb +19 -0
  78. data/app/components/phlex_kit/sidebar/sidebar_menu_sub.rb +13 -0
  79. data/app/components/phlex_kit/sidebar/sidebar_menu_sub_button.rb +18 -0
  80. data/app/components/phlex_kit/sidebar/sidebar_menu_sub_item.rb +13 -0
  81. data/app/components/phlex_kit/sidebar/sidebar_separator.rb +14 -0
  82. data/app/components/phlex_kit/spinner/spinner.rb +5 -10
  83. data/app/components/phlex_kit/toast/toast_close.rb +1 -15
  84. data/app/components/phlex_kit/toast/toast_icon.rb +12 -36
  85. data/app/components/phlex_kit/typography/heading.rb +1 -1
  86. data/app/components/phlex_kit/typography/typography.css +7 -3
  87. data/app/javascript/phlex_kit/controllers/accordion_controller.js +17 -1
  88. data/app/javascript/phlex_kit/controllers/index.js +2 -0
  89. data/app/javascript/phlex_kit/controllers/scroll_fade_controller.js +31 -0
  90. data/lib/phlex_kit/configuration.rb +7 -0
  91. data/lib/phlex_kit/icons/lucide.rb +88 -0
  92. data/lib/phlex_kit/icons/phosphor.rb +88 -0
  93. data/lib/phlex_kit/icons/remix.rb +88 -0
  94. data/lib/phlex_kit/icons/tabler.rb +88 -0
  95. data/lib/phlex_kit/icons.rb +40 -0
  96. data/lib/phlex_kit/version.rb +1 -1
  97. data/lib/phlex_kit.rb +1 -0
  98. metadata +45 -8
  99. data/app/components/phlex_kit/stars/stars.css +0 -4
  100. data/app/components/phlex_kit/stars/stars.rb +0 -19
@@ -7,21 +7,7 @@ module PhlexKit
7
7
  end
8
8
 
9
9
  def view_template
10
- svg(**mix({
11
- xmlns: "http://www.w3.org/2000/svg",
12
- width: "24",
13
- height: "24",
14
- viewbox: "0 0 24 24",
15
- fill: "none",
16
- stroke: "currentColor",
17
- "stroke-width": "2",
18
- "stroke-linecap": "round",
19
- "stroke-linejoin": "round",
20
- class: "pk-combobox-item-indicator",
21
- "aria-hidden": "true"
22
- }, @attrs)) do |s|
23
- s.path(d: "M20 6 9 17l-5-5")
24
- end
10
+ render Icon.new(:check, size: 24, **mix({ class: "pk-combobox-item-indicator" }, @attrs))
25
11
  end
26
12
  end
27
13
  end
@@ -30,20 +30,7 @@ module PhlexKit
30
30
  private
31
31
 
32
32
  def icon
33
- svg(
34
- xmlns: "http://www.w3.org/2000/svg",
35
- viewbox: "0 0 24 24",
36
- fill: "none",
37
- stroke: "currentColor",
38
- "stroke-width": "2",
39
- "stroke-linecap": "round",
40
- "stroke-linejoin": "round",
41
- class: "pk-combobox-search-icon",
42
- "aria-hidden": "true"
43
- ) do |s|
44
- s.circle(cx: "11", cy: "11", r: "8")
45
- s.path(d: "m21 21-4.3-4.3")
46
- end
33
+ render Icon.new(:search, size: nil, class: "pk-combobox-search-icon")
47
34
  end
48
35
  end
49
36
  end
@@ -29,20 +29,7 @@ module PhlexKit
29
29
  private
30
30
 
31
31
  def icon
32
- svg(
33
- xmlns: "http://www.w3.org/2000/svg",
34
- viewbox: "0 0 24 24",
35
- fill: "none",
36
- stroke: "currentColor",
37
- "stroke-width": "2",
38
- "stroke-linecap": "round",
39
- "stroke-linejoin": "round",
40
- class: "pk-combobox-trigger-icon",
41
- "aria-hidden": "true"
42
- ) do |s|
43
- s.path(d: "m7 15 5 5 5-5")
44
- s.path(d: "m7 9 5-5 5 5")
45
- end
32
+ render Icon.new(:chevrons_up_down, size: nil, class: "pk-combobox-trigger-icon")
46
33
  end
47
34
  end
48
35
  end
@@ -12,8 +12,8 @@
12
12
  inset: 0;
13
13
  z-index: 50;
14
14
  pointer-events: auto;
15
- background: color-mix(in oklab, var(--pk-bg) 80%, transparent);
16
- backdrop-filter: blur(4px);
15
+ background: rgb(0 0 0 / .5);
16
+ backdrop-filter: blur(8px);
17
17
  }
18
18
 
19
19
  /* Centered palette panel. */
@@ -38,19 +38,7 @@ module PhlexKit
38
38
  private
39
39
 
40
40
  def search_icon
41
- svg(
42
- xmlns: "http://www.w3.org/2000/svg",
43
- viewbox: "0 0 20 20",
44
- fill: "currentColor",
45
- class: "pk-command-input-icon",
46
- "aria-hidden": "true"
47
- ) do |s|
48
- s.path(
49
- fill_rule: "evenodd",
50
- d: "M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z",
51
- clip_rule: "evenodd"
52
- )
53
- end
41
+ render Icon.new(:search, size: nil, class: "pk-command-input-icon")
54
42
  end
55
43
  end
56
44
  end
@@ -12,9 +12,7 @@ module PhlexKit
12
12
  data: { action: "click->phlex-kit--context-menu#close", phlex_kit__context_menu_target: "menuItem", disabled: @disabled } }, @attrs)) do
13
13
  if @checked
14
14
  span(class: "pk-context-menu-check") do
15
- svg(width: "15", height: "15", viewbox: "0 0 15 15", fill: "none", xmlns: "http://www.w3.org/2000/svg") do |s|
16
- s.path(fill_rule: "evenodd", clip_rule: "evenodd", fill: "currentColor", d: "M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.55529 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z")
17
- end
15
+ render Icon.new(:check, size: 15)
18
16
  end
19
17
  end
20
18
  yield
@@ -43,19 +43,7 @@ module PhlexKit
43
43
  private
44
44
 
45
45
  def chevron_icon
46
- svg(
47
- xmlns: "http://www.w3.org/2000/svg",
48
- width: "16",
49
- height: "16",
50
- viewbox: "0 0 24 24",
51
- fill: "none",
52
- stroke: "currentColor",
53
- "stroke-width": "2",
54
- "stroke-linecap": "round",
55
- "stroke-linejoin": "round",
56
- class: "pk-data-table-column-toggle-icon",
57
- "aria-hidden": "true"
58
- ) { |s| s.polyline(points: "6 9 12 15 18 9") }
46
+ render Icon.new(:chevron_down, class: "pk-data-table-column-toggle-icon")
59
47
  end
60
48
  end
61
49
  end
@@ -23,18 +23,7 @@ module PhlexKit
23
23
  private
24
24
 
25
25
  def icon
26
- svg(
27
- xmlns: "http://www.w3.org/2000/svg",
28
- width: "16",
29
- height: "16",
30
- viewbox: "0 0 24 24",
31
- fill: "none",
32
- stroke: "currentColor",
33
- "stroke-width": "2",
34
- "stroke-linecap": "round",
35
- "stroke-linejoin": "round",
36
- "aria-hidden": "true"
37
- ) { |s| s.polyline(points: "9 18 15 12 9 6") }
26
+ render Icon.new(:chevron_right)
38
27
  end
39
28
  end
40
29
  end
@@ -49,34 +49,8 @@ module PhlexKit
49
49
 
50
50
  def sort_icon
51
51
  icon_class = [ "pk-data-table-sort-icon", (current_direction ? nil : "unsorted") ].compact.join(" ")
52
- case current_direction
53
- when "asc"
54
- sort_svg(icon_class) { |s| s.polyline(points: "18 15 12 9 6 15") }
55
- when "desc"
56
- sort_svg(icon_class) { |s| s.polyline(points: "6 9 12 15 18 9") }
57
- else
58
- sort_svg(icon_class) do |s|
59
- s.polyline(points: "8 15 12 19 16 15")
60
- s.polyline(points: "8 9 12 5 16 9")
61
- end
62
- end
63
- end
64
-
65
- def sort_svg(icon_class, &)
66
- svg(
67
- xmlns: "http://www.w3.org/2000/svg",
68
- width: "12",
69
- height: "12",
70
- viewbox: "0 0 24 24",
71
- fill: "none",
72
- stroke: "currentColor",
73
- "stroke-width": "2",
74
- "stroke-linecap": "round",
75
- "stroke-linejoin": "round",
76
- class: icon_class,
77
- "aria-hidden": "true",
78
- &
79
- )
52
+ glyph = { "asc" => :chevron_up, "desc" => :chevron_down }.fetch(current_direction, :chevrons_up_down)
53
+ render Icon.new(glyph, size: 12, class: icon_class)
80
54
  end
81
55
  end
82
56
  end
@@ -8,14 +8,20 @@
8
8
  padding: 1.5rem; box-shadow: 0 10px 30px rgb(0 0 0 / .35);
9
9
  }
10
10
  .pk-dialog[open] { display: flex; flex-direction: column; }
11
- .pk-dialog::backdrop { background: rgb(0 0 0 / .5); backdrop-filter: blur(2px); }
11
+ .pk-dialog::backdrop { background: rgb(0 0 0 / .5); backdrop-filter: blur(8px); }
12
12
  .pk-dialog.xs { max-width: 24rem; }
13
13
  .pk-dialog.sm { max-width: 28rem; }
14
14
  .pk-dialog.lg { max-width: 42rem; }
15
15
  .pk-dialog.xl { max-width: 56rem; }
16
16
  .pk-dialog.full { max-width: 100%; }
17
17
  .pk-dialog-header { display: flex; flex-direction: column; gap: .375rem; text-align: center; }
18
- .pk-dialog-footer { display: flex; flex-direction: column-reverse; gap: .5rem; }
18
+ .pk-dialog-footer {
19
+ display: flex; flex-direction: column-reverse; gap: .5rem;
20
+ margin: 0 calc(-1 * var(--pk-dialog-spacing)) calc(-1 * var(--pk-dialog-spacing));
21
+ padding: var(--pk-dialog-spacing);
22
+ border-top: 1px solid var(--pk-border);
23
+ background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent);
24
+ }
19
25
  .pk-dialog-title { font-size: 1.125rem; font-weight: 600; line-height: 1; letter-spacing: -.01em; }
20
26
  .pk-dialog-description { font-size: .875rem; color: var(--pk-muted); }
21
27
  .pk-dialog-middle { padding-block: 1rem; }
@@ -10,9 +10,7 @@ module PhlexKit
10
10
  dialog(**mix({ class: cls, data: { phlex_kit__dialog_target: "dialog", action: "click->phlex-kit--dialog#backdropClick" } }, @attrs)) do
11
11
  yield
12
12
  button(type: "button", class: "pk-overlay-close", data: { action: "click->phlex-kit--dialog#dismiss" }) do
13
- svg(width: "15", height: "15", viewbox: "0 0 15 15", fill: "none", xmlns: "http://www.w3.org/2000/svg") do |s|
14
- s.path(fill_rule: "evenodd", clip_rule: "evenodd", fill: "currentColor", d: "M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z")
15
- end
13
+ render Icon.new(:x, size: 15)
16
14
  span(class: "pk-sr-only") { "Close" }
17
15
  end
18
16
  end
@@ -7,6 +7,7 @@
7
7
  inset: 0;
8
8
  z-index: 50;
9
9
  background: rgb(0 0 0 / .5);
10
+ backdrop-filter: blur(8px);
10
11
  }
11
12
  .pk-drawer {
12
13
  position: fixed;
@@ -21,6 +21,7 @@
21
21
  .pk-dropdown-menu-item {
22
22
  display: flex;
23
23
  align-items: center;
24
+ gap: .5rem;
24
25
  width: 100%;
25
26
  cursor: pointer;
26
27
  user-select: none;
@@ -36,3 +37,66 @@
36
37
 
37
38
  .pk-dropdown-menu-label { margin: 0; padding: .375rem .5rem; font-size: .875rem; font-weight: 600; }
38
39
  .pk-dropdown-menu-separator { height: 1px; margin: .25rem -.25rem; background: var(--pk-border); }
40
+
41
+ .pk-dropdown-menu-item > svg { flex: none; width: 1rem; height: 1rem; color: var(--pk-muted); }
42
+ .pk-dropdown-menu-group { display: contents; }
43
+
44
+ /* Destructive rows (irreversible actions). */
45
+ .pk-dropdown-menu-item.destructive { color: var(--pk-red); }
46
+ .pk-dropdown-menu-item.destructive > svg { color: var(--pk-red); }
47
+ .pk-dropdown-menu-item.destructive:hover,
48
+ .pk-dropdown-menu-item.destructive:focus {
49
+ background: color-mix(in oklab, var(--pk-red) 12%, transparent);
50
+ color: var(--pk-red);
51
+ }
52
+
53
+ /* Keyboard hint, pushed to the row's end. */
54
+ .pk-dropdown-menu-shortcut {
55
+ margin-left: auto;
56
+ font-size: .75rem;
57
+ letter-spacing: .1em;
58
+ color: var(--pk-muted);
59
+ }
60
+
61
+ /* Checkbox / radio rows: real (hidden) inputs; indicator shows via :has. */
62
+ .pk-dropdown-menu-item-input {
63
+ position: absolute;
64
+ width: 1px;
65
+ height: 1px;
66
+ opacity: 0;
67
+ pointer-events: none;
68
+ }
69
+ .pk-dropdown-menu-checkbox-item,
70
+ .pk-dropdown-menu-radio-item { position: relative; }
71
+ .pk-dropdown-menu-item-indicator {
72
+ display: inline-flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ flex: none;
76
+ width: 1rem;
77
+ height: 1rem;
78
+ visibility: hidden;
79
+ }
80
+ .pk-dropdown-menu-item-indicator svg { width: .875rem; height: .875rem; }
81
+ .pk-dropdown-menu-radio-item .pk-dropdown-menu-item-indicator svg { width: .5rem; height: .5rem; }
82
+ .pk-dropdown-menu-item:has(.pk-dropdown-menu-item-input:checked) .pk-dropdown-menu-item-indicator { visibility: visible; }
83
+ .pk-dropdown-menu-item:has(.pk-dropdown-menu-item-input:focus-visible) {
84
+ box-shadow: 0 0 0 2px color-mix(in oklab, var(--pk-ring) 50%, transparent);
85
+ }
86
+
87
+ /* Submenu: trigger row + side panel, revealed on hover/focus — pure CSS. */
88
+ .pk-dropdown-menu-sub { position: relative; }
89
+ .pk-dropdown-menu-sub-chevron { margin-left: auto; width: 1rem; height: 1rem; }
90
+ .pk-dropdown-menu-sub-content {
91
+ position: absolute;
92
+ top: -0.25rem;
93
+ left: 100%;
94
+ z-index: 60;
95
+ min-width: 10rem;
96
+ padding-left: .25rem; /* hover bridge over the gap */
97
+ display: none;
98
+ }
99
+ .pk-dropdown-menu-sub:hover > .pk-dropdown-menu-sub-content,
100
+ .pk-dropdown-menu-sub:focus-within > .pk-dropdown-menu-sub-content { display: block; }
101
+ .pk-dropdown-menu-sub:hover > .pk-dropdown-menu-sub-trigger,
102
+ .pk-dropdown-menu-sub:focus-within > .pk-dropdown-menu-sub-trigger { background: var(--pk-accent); }
@@ -0,0 +1,31 @@
1
+ module PhlexKit
2
+ # Toggleable menu row, ported from shadcn/ui's DropdownMenuCheckboxItem: a
3
+ # <label> around a real (hidden) checkbox — clicking toggles it natively and
4
+ # the ✓ shows via CSS, without closing the menu (matching Radix). Pass
5
+ # `name:`/`value:` to submit the state with a form. See dropdown_menu.rb.
6
+ class DropdownMenuCheckboxItem < 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-dropdown-menu-item pk-dropdown-menu-checkbox-item",
17
+ role: "menuitemcheckbox"
18
+ }, @attrs)) do
19
+ input(type: :checkbox, class: "pk-dropdown-menu-item-input", name: @name, value: @value, checked: @checked)
20
+ span(class: "pk-dropdown-menu-item-indicator", aria: { hidden: "true" }) { check_icon }
21
+ yield if block
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def check_icon
28
+ render Icon.new(:check, size: nil)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,11 @@
1
+ module PhlexKit
2
+ # Grouping wrapper for related items (with an optional leading
3
+ # DropdownMenuLabel). Ported from shadcn/ui's DropdownMenuGroup.
4
+ # See dropdown_menu.rb.
5
+ class DropdownMenuGroup < BaseComponent
6
+ def initialize(**attrs) = (@attrs = attrs)
7
+ def view_template(&)
8
+ div(**mix({ class: "pk-dropdown-menu-group", role: "group" }, @attrs), &)
9
+ end
10
+ end
11
+ end
@@ -2,9 +2,12 @@ module PhlexKit
2
2
  # A menu row in a PhlexKit::DropdownMenuContent. `as: :a` (default, pass `href:`) or
3
3
  # `:div` (e.g. to wrap a button_to). Closes the menu on click. See dropdown_menu.rb.
4
4
  class DropdownMenuItem < BaseComponent
5
- def initialize(as: :a, href: "#", **attrs)
5
+ VARIANTS = { default: nil, destructive: "destructive" }.freeze
6
+
7
+ def initialize(as: :a, href: "#", variant: :default, **attrs)
6
8
  @as = as.to_sym
7
9
  @href = href
10
+ @variant = variant.to_sym
8
11
  @attrs = attrs
9
12
  end
10
13
 
@@ -12,7 +15,7 @@ module PhlexKit
12
15
  base = {
13
16
  role: "menuitem",
14
17
  tabindex: "-1",
15
- class: "pk-dropdown-menu-item",
18
+ class: [ "pk-dropdown-menu-item", VARIANTS.fetch(@variant) ].compact.join(" "),
16
19
  data: { phlex_kit__dropdown_menu_target: "menuItem", action: "click->phlex-kit--dropdown-menu#close" }
17
20
  }
18
21
  base[:href] = @href unless @as == :div
@@ -0,0 +1,10 @@
1
+ module PhlexKit
2
+ # Exclusive-choice group of DropdownMenuRadioItems (share the same `name:`).
3
+ # Ported from shadcn/ui's DropdownMenuRadioGroup. See dropdown_menu.rb.
4
+ class DropdownMenuRadioGroup < BaseComponent
5
+ def initialize(**attrs) = (@attrs = attrs)
6
+ def view_template(&)
7
+ div(**mix({ class: "pk-dropdown-menu-group", role: "radiogroup" }, @attrs), &)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,31 @@
1
+ module PhlexKit
2
+ # One choice in a DropdownMenuRadioGroup — a <label> around a real (hidden)
3
+ # radio; the ● shows via CSS and picking doesn't close the menu (matching
4
+ # Radix). See dropdown_menu.rb.
5
+ class DropdownMenuRadioItem < BaseComponent
6
+ def initialize(name:, value:, checked: false, **attrs)
7
+ @name = name
8
+ @value = value
9
+ @checked = checked
10
+ @attrs = attrs
11
+ end
12
+
13
+ def view_template(&block)
14
+ label(**mix({
15
+ class: "pk-dropdown-menu-item pk-dropdown-menu-radio-item",
16
+ role: "menuitemradio"
17
+ }, @attrs)) do
18
+ input(type: :radio, class: "pk-dropdown-menu-item-input", name: @name, value: @value, checked: @checked)
19
+ span(class: "pk-dropdown-menu-item-indicator", aria: { hidden: "true" }) do
20
+ # Deliberately NOT a PhlexKit::Icon: a filled selection dot is
21
+ # geometry, not icon-library vocabulary — it stays identical across
22
+ # icon_library settings (shadcn fills its Circle icon via CSS too).
23
+ svg(xmlns: "http://www.w3.org/2000/svg", viewbox: "0 0 24 24", fill: "currentColor") do |s|
24
+ s.circle(cx: "12", cy: "12", r: "5")
25
+ end
26
+ end
27
+ yield if block
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,10 @@
1
+ module PhlexKit
2
+ # Right-aligned keyboard hint in a menu row. Ported from shadcn/ui's
3
+ # DropdownMenuShortcut. See dropdown_menu.rb.
4
+ class DropdownMenuShortcut < BaseComponent
5
+ def initialize(**attrs) = (@attrs = attrs)
6
+ def view_template(&)
7
+ span(**mix({ class: "pk-dropdown-menu-shortcut" }, @attrs), &)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ module PhlexKit
2
+ # Nested submenu wrapper: SubTrigger row + SubContent panel, revealed on
3
+ # hover/focus with pure CSS (no extra controller). Ported from shadcn/ui's
4
+ # DropdownMenuSub. See dropdown_menu.rb.
5
+ class DropdownMenuSub < BaseComponent
6
+ def initialize(**attrs) = (@attrs = attrs)
7
+ def view_template(&)
8
+ div(**mix({ class: "pk-dropdown-menu-sub" }, @attrs), &)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module PhlexKit
2
+ # The nested panel of a DropdownMenuSub — opens beside the trigger.
3
+ # See dropdown_menu.rb.
4
+ class DropdownMenuSubContent < BaseComponent
5
+ def initialize(**attrs) = (@attrs = attrs)
6
+ def view_template(&)
7
+ div(**mix({ class: "pk-dropdown-menu-sub-content", role: "menu" }, @attrs)) do
8
+ div(class: "pk-dropdown-menu-viewport", &)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ module PhlexKit
2
+ # The row that opens a DropdownMenuSub (trailing ▸ chevron). See
3
+ # dropdown_menu.rb.
4
+ class DropdownMenuSubTrigger < BaseComponent
5
+ def initialize(**attrs) = (@attrs = attrs)
6
+ def view_template(&block)
7
+ div(**mix({
8
+ class: "pk-dropdown-menu-item pk-dropdown-menu-sub-trigger",
9
+ role: "menuitem",
10
+ tabindex: "0",
11
+ aria: { haspopup: "menu" }
12
+ }, @attrs)) do
13
+ block&.call
14
+ render Icon.new(:chevron_right, size: nil, class: "pk-dropdown-menu-sub-chevron")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,51 @@
1
+ module PhlexKit
2
+ # A glyph from the configured icon library (ui.shadcn.com/create's "Icon
3
+ # Library" option): :lucide (default), :tabler, :phosphor, or :remix — set
4
+ # kit-wide via PhlexKit.config.icon_library or per-instance with `library:`.
5
+ # Names are PhlexKit's canonical semantic set (PhlexKit::Icons.names);
6
+ # unknown names/libraries fail loud (KeyError).
7
+ #
8
+ # `size:` emits width/height attributes (default 16, shadcn's size-4); pass
9
+ # `size: nil` to defer sizing to CSS (existing `.pk-x svg { … }` rules).
10
+ # Stroke libraries (lucide/tabler) render stroke="currentColor"; fill
11
+ # libraries (phosphor/remix) render fill="currentColor" — both follow text
12
+ # color. Attrs pass through via mix.
13
+ class Icon < BaseComponent
14
+ def initialize(name, library: nil, size: 16, **attrs)
15
+ @name = name
16
+ @library = library
17
+ @size = size
18
+ @attrs = attrs
19
+ end
20
+
21
+ def view_template
22
+ icon = Icons.fetch(@name, library: @library || PhlexKit.config.icon_library)
23
+ svg(**mix(base_attrs(icon), @attrs)) do |s|
24
+ icon[:elements].each { |tag, attrs| s.public_send(tag, **attrs) }
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def base_attrs(icon)
31
+ base = {
32
+ xmlns: "http://www.w3.org/2000/svg",
33
+ viewbox: icon[:view_box],
34
+ class: "pk-icon",
35
+ "aria-hidden": "true"
36
+ }
37
+ base[:width] = base[:height] = @size.to_s if @size
38
+ if icon[:mode] == :stroke
39
+ base.merge(
40
+ fill: "none",
41
+ stroke: "currentColor",
42
+ "stroke-width": icon[:stroke_width].to_s,
43
+ "stroke-linecap": "round",
44
+ "stroke-linejoin": "round"
45
+ )
46
+ else
47
+ base.merge(fill: "currentColor")
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,25 @@
1
+ /* Co-located with marker.rb — UI::Marker, shadcn parity: muted inline status,
2
+ bordered system-note row, or labeled separator in a conversation. */
3
+ .pk-marker {
4
+ display: flex;
5
+ align-items: center;
6
+ gap: .5rem;
7
+ min-height: 1.25rem;
8
+ color: var(--pk-muted);
9
+ font-size: .875rem;
10
+ }
11
+ .pk-marker-icon { display: inline-flex; flex: none; }
12
+ .pk-marker-icon svg { width: 1rem; height: 1rem; }
13
+ .pk-marker-content { min-width: 0; }
14
+ .pk-marker.border {
15
+ border: 1px solid var(--pk-border);
16
+ border-radius: var(--pk-radius);
17
+ padding: .5rem .625rem;
18
+ }
19
+ .pk-marker.separator { justify-content: center; }
20
+ .pk-marker.separator::before, .pk-marker.separator::after {
21
+ content: "";
22
+ flex: 1;
23
+ height: 1px;
24
+ background: var(--pk-border);
25
+ }
@@ -0,0 +1,18 @@
1
+ module PhlexKit
2
+ # Conversation annotation, ported from shadcn/ui's Marker (an AI-chat-era
3
+ # addition): an inline status line, bordered system-note row, or labeled
4
+ # separator. Compose Marker(variant:) > [MarkerIcon +] MarkerContent.
5
+ # `.pk-marker*` (marker.css).
6
+ class Marker < BaseComponent
7
+ VARIANTS = { default: nil, border: "border", separator: "separator" }.freeze
8
+
9
+ def initialize(variant: :default, **attrs)
10
+ @variant = variant.to_sym
11
+ @attrs = attrs
12
+ end
13
+
14
+ def view_template(&)
15
+ div(**mix({ class: [ "pk-marker", VARIANTS.fetch(@variant) ].compact.join(" ") }, @attrs), &)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ module PhlexKit
2
+ # Text of a Marker. See marker.rb.
3
+ class MarkerContent < BaseComponent
4
+ def initialize(**attrs) = (@attrs = attrs)
5
+ def view_template(&) = span(**mix({ class: "pk-marker-content" }, @attrs), &)
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module PhlexKit
2
+ # 16px muted icon slot of a Marker. See marker.rb.
3
+ class MarkerIcon < BaseComponent
4
+ def initialize(**attrs) = (@attrs = attrs)
5
+ def view_template(&) = span(**mix({ class: "pk-marker-icon" }, @attrs), &)
6
+ end
7
+ end
@@ -17,16 +17,7 @@ module PhlexKit
17
17
  private
18
18
 
19
19
  def chevron
20
- svg(
21
- xmlns: "http://www.w3.org/2000/svg",
22
- viewbox: "0 0 24 24",
23
- fill: "none",
24
- stroke: "currentColor",
25
- "stroke-width": "2",
26
- "stroke-linecap": "round",
27
- "stroke-linejoin": "round",
28
- "aria-hidden": "true"
29
- ) { |s| s.path(d: "m6 9 6 6 6-6") }
20
+ render Icon.new(:chevron_down, size: nil)
30
21
  end
31
22
  end
32
23
  end
@@ -17,11 +17,7 @@ module PhlexKit
17
17
  private
18
18
 
19
19
  def chevron
20
- svg(xmlns: "http://www.w3.org/2000/svg", viewbox: "0 0 24 24", fill: "none",
21
- stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round",
22
- "stroke-linejoin": "round", class: "pk-navigation-menu-chevron", "aria-hidden": "true") do |s|
23
- s.path(d: "m6 9 6 6 6-6")
24
- end
20
+ render Icon.new(:chevron_down, size: nil, class: "pk-navigation-menu-chevron")
25
21
  end
26
22
  end
27
23
  end
@@ -4,11 +4,7 @@ module PhlexKit
4
4
  def view_template
5
5
  li do
6
6
  span(**mix({ class: "pk-pagination-ellipsis", aria: { hidden: true } }, @attrs)) do
7
- svg(xmlns: "http://www.w3.org/2000/svg", viewbox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_linejoin: "round") do |s|
8
- s.circle(cx: "12", cy: "12", r: "1")
9
- s.circle(cx: "19", cy: "12", r: "1")
10
- s.circle(cx: "5", cy: "12", r: "1")
11
- end
7
+ render Icon.new(:ellipsis, size: nil)
12
8
  span(class: "pk-sr-only") { "More pages" }
13
9
  end
14
10
  end