phlex_kit 0.2.0 → 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 +210 -49
  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
@@ -20,21 +20,7 @@ module PhlexKit
20
20
  private
21
21
 
22
22
  def icon
23
- svg(
24
- xmlns: "http://www.w3.org/2000/svg",
25
- width: "15",
26
- height: "15",
27
- viewbox: "0 0 15 15",
28
- fill: "none",
29
- "aria-hidden": "true"
30
- ) do |s|
31
- s.path(
32
- d: "M8.84182 3.13514C9.04327 3.32401 9.05348 3.64042 8.86462 3.84188L5.43521 7.49991L8.86462 11.1579C9.05348 11.3594 9.04327 11.6758 8.84182 11.8647C8.64036 12.0535 8.32394 12.0433 8.13508 11.8419L4.38508 7.84188C4.20477 7.64955 4.20477 7.35027 4.38508 7.15794L8.13508 3.15794C8.32394 2.95648 8.64036 2.94628 8.84182 3.13514Z",
33
- fill: "currentColor",
34
- fill_rule: "evenodd",
35
- clip_rule: "evenodd"
36
- )
37
- end
23
+ render Icon.new(:chevron_left, size: 15)
38
24
  end
39
25
  end
40
26
  end
@@ -1,15 +1,51 @@
1
1
  /* Co-located with card.rb — the UI::Card container kit (Card + Header / Title /
2
- Description / Content / Footer). Namespaced .pk-card* to avoid colliding with
3
- the dashboard stat-tile .card in application.css. Theme tokens come from the
4
- global stylesheet; translated from ruby_ui's Tailwind:
5
- rounded-xl border bg-background shadow → border + radius + surface
6
- flex flex-col space-y-1.5 p-6 → header column + gap + pad
7
- font-semibold leading-none tracking-tight → title
8
- text-sm text-muted-foreground → description
9
- p-6 pt-0 / items-center p-6 pt-0 → content / footer (top pad sits in header) */
10
- .pk-card { background: var(--pk-surface); border: 1px solid var(--pk-border); border-radius: .75rem; box-shadow: 0 1px 3px rgba(0, 0, 0, .3); }
11
- .pk-card-header { display: flex; flex-direction: column; gap: .375rem; padding: 1.5rem; }
12
- .pk-card-title { margin: 0; font-weight: 600; line-height: 1.1; letter-spacing: -.01em; }
13
- .pk-card-description { margin: 0; font-size: .85rem; color: var(--pk-muted); }
14
- .pk-card-content { padding: 0 1.5rem 1.5rem; }
15
- .pk-card-footer { display: flex; align-items: center; gap: .5rem; padding: 0 1.5rem 1.5rem; }
2
+ Description / Action / Content / Footer), matching shadcn/ui's current card:
3
+ every gap and inset derives from --pk-card-spacing (1rem; .75rem for
4
+ size :sm or set it inline per card), the header becomes a 1fr/auto grid
5
+ when a CardAction is present, the footer is a full-bleed muted band, and a
6
+ leading <img> child runs edge-to-edge. Theme tokens come from the global
7
+ stylesheet. */
8
+ .pk-card {
9
+ --pk-card-spacing: 1rem;
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: var(--pk-card-spacing);
13
+ overflow: hidden;
14
+ border: 1px solid var(--pk-border);
15
+ border-radius: calc(var(--pk-radius) + 4px);
16
+ background: var(--pk-surface);
17
+ color: var(--pk-text);
18
+ padding-block: var(--pk-card-spacing);
19
+ font-size: .875rem;
20
+ }
21
+ .pk-card.sm { --pk-card-spacing: .75rem; }
22
+ /* the footer band and a leading image supply their own inset */
23
+ .pk-card:has(> .pk-card-footer:last-child) { padding-bottom: 0; }
24
+ .pk-card:has(> img:first-child) { padding-top: 0; }
25
+ .pk-card > img { width: 100%; object-fit: cover; }
26
+
27
+ .pk-card-header {
28
+ display: grid;
29
+ grid-auto-rows: min-content;
30
+ align-items: start;
31
+ gap: .25rem;
32
+ padding-inline: var(--pk-card-spacing);
33
+ }
34
+ .pk-card-header:has(.pk-card-action) { grid-template-columns: 1fr auto; }
35
+ .pk-card-title { margin: 0; font-weight: 600; line-height: 1.25; }
36
+ .pk-card-description { margin: 0; font-size: .875rem; color: var(--pk-muted); }
37
+ .pk-card-action {
38
+ grid-row: 1 / span 2;
39
+ grid-column: 2;
40
+ justify-self: end;
41
+ align-self: start;
42
+ }
43
+ .pk-card-content { padding-inline: var(--pk-card-spacing); }
44
+ .pk-card-footer {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: .5rem;
48
+ border-top: 1px solid var(--pk-border);
49
+ background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent);
50
+ padding: var(--pk-card-spacing);
51
+ }
@@ -18,12 +18,16 @@ module PhlexKit
18
18
  # render PhlexKit::CardFooter.new { render PhlexKit::Button.new { "Action" } }
19
19
  # end
20
20
  class Card < BaseComponent
21
- def initialize(**attrs)
21
+ # size => modifier class; :sm tightens --pk-card-spacing (shadcn's size prop).
22
+ SIZES = { default: nil, sm: "sm" }.freeze
23
+
24
+ def initialize(size: :default, **attrs)
25
+ @size = size.to_sym
22
26
  @attrs = attrs
23
27
  end
24
28
 
25
29
  def view_template(&block)
26
- div(**mix({ class: "pk-card" }, @attrs), &block)
30
+ div(**mix({ class: [ "pk-card", SIZES.fetch(@size) ].compact.join(" ") }, @attrs), &block)
27
31
  end
28
32
  end
29
33
  end
@@ -0,0 +1,14 @@
1
+ module PhlexKit
2
+ # Top-right slot of a CardHeader (a button, link or badge) — the header
3
+ # becomes a 1fr/auto grid when present. Ported from shadcn/ui's CardAction
4
+ # (newer than ruby_ui's card). See card.rb.
5
+ class CardAction < BaseComponent
6
+ def initialize(**attrs)
7
+ @attrs = attrs
8
+ end
9
+
10
+ def view_template(&)
11
+ div(**mix({ class: "pk-card-action" }, @attrs), &)
12
+ end
13
+ end
14
+ end
@@ -19,21 +19,7 @@ module PhlexKit
19
19
  private
20
20
 
21
21
  def icon
22
- svg(
23
- xmlns: "http://www.w3.org/2000/svg",
24
- width: "24",
25
- height: "24",
26
- viewbox: "0 0 24 24",
27
- fill: "none",
28
- stroke: "currentColor",
29
- "stroke-width": "2",
30
- "stroke-linecap": "round",
31
- "stroke-linejoin": "round",
32
- "aria-hidden": "true"
33
- ) do |s|
34
- s.path(d: "M5 12h14")
35
- s.path(d: "m12 5 7 7-7 7")
36
- end
22
+ render Icon.new(:arrow_right, size: 24)
37
23
  end
38
24
  end
39
25
  end
@@ -20,21 +20,7 @@ module PhlexKit
20
20
  private
21
21
 
22
22
  def icon
23
- svg(
24
- xmlns: "http://www.w3.org/2000/svg",
25
- width: "24",
26
- height: "24",
27
- viewbox: "0 0 24 24",
28
- fill: "none",
29
- stroke: "currentColor",
30
- "stroke-width": "2",
31
- "stroke-linecap": "round",
32
- "stroke-linejoin": "round",
33
- "aria-hidden": "true"
34
- ) do |s|
35
- s.path(d: "m12 19-7-7 7-7")
36
- s.path(d: "M19 12H5")
37
- end
23
+ render Icon.new(:arrow_left, size: 24)
38
24
  end
39
25
  end
40
26
  end
@@ -4,4 +4,4 @@
4
4
  background: var(--pk-surface-2); font-size: .875rem;
5
5
  }
6
6
  .pk-codeblock pre { margin: 0; padding: 1rem; }
7
- .pk-codeblock code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--pk-text); }
7
+ .pk-codeblock code { font-family: var(--pk-font-mono, ui-monospace, monospace); color: var(--pk-text); }
@@ -18,21 +18,7 @@ module PhlexKit
18
18
  action: "phlex-kit--combobox#clearAll"
19
19
  }
20
20
  }, @attrs)) do
21
- svg(
22
- xmlns: "http://www.w3.org/2000/svg",
23
- width: "24",
24
- height: "24",
25
- viewbox: "0 0 24 24",
26
- fill: "none",
27
- stroke: "currentColor",
28
- "stroke-width": "2",
29
- "stroke-linecap": "round",
30
- "stroke-linejoin": "round",
31
- "aria-hidden": "true"
32
- ) do |s|
33
- s.path(d: "M18 6 6 18")
34
- s.path(d: "m6 6 12 12")
35
- end
21
+ render Icon.new(:x, size: 24)
36
22
  end
37
23
  end
38
24
  end
@@ -51,18 +51,7 @@ module PhlexKit
51
51
 
52
52
  def chevron_icon
53
53
  span(class: "pk-combobox-input-trigger-icon") do
54
- svg(
55
- xmlns: "http://www.w3.org/2000/svg",
56
- width: "24",
57
- height: "24",
58
- viewbox: "0 0 24 24",
59
- fill: "none",
60
- stroke: "currentColor",
61
- "stroke-width": "2",
62
- "stroke-linecap": "round",
63
- "stroke-linejoin": "round",
64
- "aria-hidden": "true"
65
- ) { |s| s.path(d: "m6 9 6 6 6-6") }
54
+ render Icon.new(:chevron_down, size: 24)
66
55
  end
67
56
  end
68
57
  end
@@ -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