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,18 +1,28 @@
1
1
  module PhlexKit
2
+ # A menu row in a PhlexKit::ContextMenuContent. Matches the dropdown-menu
3
+ # grammar: gap-based layout (no forced inset), `variant: :destructive`,
4
+ # `checked:` renders a leading ✓, `shortcut:` a trailing kbd hint (or
5
+ # compose ContextMenuShortcut yourself). Closes the menu on click.
6
+ # See context_menu.rb.
2
7
  class ContextMenuItem < BaseComponent
3
- def initialize(href: "#", checked: false, shortcut: nil, disabled: false, **attrs)
8
+ VARIANTS = { default: nil, destructive: "destructive" }.freeze
9
+
10
+ def initialize(href: "#", checked: false, shortcut: nil, disabled: false, variant: :default, **attrs)
4
11
  @href = href
5
12
  @checked = checked
6
13
  @shortcut = shortcut
7
14
  @disabled = disabled
15
+ @variant = variant.to_sym
8
16
  @attrs = attrs
9
17
  end
18
+
10
19
  def view_template(&block)
11
- a(**mix({ href: @href, role: "menuitem", tabindex: "-1", class: "pk-context-menu-item",
20
+ classes = [ "pk-context-menu-item", VARIANTS.fetch(@variant) ].compact.join(" ")
21
+ a(**mix({ href: @href, role: "menuitem", tabindex: "-1", class: classes,
12
22
  data: { action: "click->phlex-kit--context-menu#close", phlex_kit__context_menu_target: "menuItem", disabled: @disabled } }, @attrs)) do
13
23
  if @checked
14
24
  span(class: "pk-context-menu-check") do
15
- render Icon.new(:check, size: 15)
25
+ render Icon.new(:check, size: nil)
16
26
  end
17
27
  end
18
28
  yield
@@ -0,0 +1,10 @@
1
+ module PhlexKit
2
+ # Exclusive-choice group of ContextMenuRadioItems (share the same `name:`).
3
+ # Mirrors shadcn/ui's ContextMenuRadioGroup. See context_menu.rb.
4
+ class ContextMenuRadioGroup < BaseComponent
5
+ def initialize(**attrs) = (@attrs = attrs)
6
+ def view_template(&)
7
+ div(**mix({ class: "pk-context-menu-group", role: "radiogroup" }, @attrs), &)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,30 @@
1
+ module PhlexKit
2
+ # One choice in a ContextMenuRadioGroup — a <label> around a real (hidden)
3
+ # radio; the ● shows via CSS and picking doesn't close the menu (matching
4
+ # Radix). See context_menu.rb.
5
+ class ContextMenuRadioItem < 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-context-menu-item pk-context-menu-radio-item",
16
+ role: "menuitemradio"
17
+ }, @attrs)) do
18
+ input(type: :radio, class: "pk-context-menu-item-input", name: @name, value: @value, checked: @checked)
19
+ span(class: "pk-context-menu-item-indicator", aria: { hidden: "true" }) do
20
+ # A filled selection dot is geometry, not icon-library vocabulary —
21
+ # identical across icon_library settings (matches the dropdown).
22
+ svg(xmlns: "http://www.w3.org/2000/svg", viewbox: "0 0 24 24", fill: "currentColor") do |s|
23
+ s.circle(cx: "12", cy: "12", r: "5")
24
+ end
25
+ end
26
+ yield if block
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,11 @@
1
+ module PhlexKit
2
+ # Right-aligned keyboard hint on a ContextMenuItem, mirroring shadcn/ui's
3
+ # ContextMenuShortcut (the item's `shortcut:` param renders the same span).
4
+ # See context_menu.rb.
5
+ class ContextMenuShortcut < BaseComponent
6
+ def initialize(**attrs) = (@attrs = attrs)
7
+ def view_template(&block)
8
+ span(**mix({ class: "pk-context-menu-shortcut" }, @attrs), &block)
9
+ end
10
+ end
11
+ 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). Mirrors shadcn/ui's
4
+ # ContextMenuSub. See context_menu.rb.
5
+ class ContextMenuSub < BaseComponent
6
+ def initialize(**attrs) = (@attrs = attrs)
7
+ def view_template(&)
8
+ div(**mix({ class: "pk-context-menu-sub" }, @attrs), &)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module PhlexKit
2
+ # The nested panel of a ContextMenuSub — opens beside the trigger.
3
+ # See context_menu.rb.
4
+ class ContextMenuSubContent < BaseComponent
5
+ def initialize(**attrs) = (@attrs = attrs)
6
+ def view_template(&)
7
+ div(**mix({ class: "pk-context-menu-sub-content", role: "menu" }, @attrs)) do
8
+ div(class: "pk-context-menu-sub-viewport", &)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ module PhlexKit
2
+ # The row that opens a ContextMenuSub (trailing ▸ chevron). See context_menu.rb.
3
+ class ContextMenuSubTrigger < BaseComponent
4
+ def initialize(**attrs) = (@attrs = attrs)
5
+ def view_template(&block)
6
+ div(**mix({
7
+ class: "pk-context-menu-item pk-context-menu-sub-trigger",
8
+ role: "menuitem",
9
+ tabindex: "0",
10
+ aria: { haspopup: "menu" }
11
+ }, @attrs)) do
12
+ block&.call
13
+ render Icon.new(:chevron_right, size: nil, class: "pk-context-menu-sub-chevron")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,38 +1,56 @@
1
- /* Co-located with dialog.rb. Native <dialog> centered by the UA; we style the box
2
- + ::backdrop. Theme tokens from the global stylesheet. */
1
+ /* Co-located with dialog.rb. Native <dialog> centered by the UA; we style the
2
+ box + ::backdrop. Matched to shadcn/ui's current dialog (nova): popover
3
+ surface with a foreground-tint ring, rounded-xl, --pk-dialog-spacing: 1rem
4
+ driving padding + the full-bleed footer band, text-sm body, close pinned at
5
+ top-2/right-2. Their default width is max-w-sm (24rem) = our md.
6
+ Overlays are kit-unified at nova's black/10 + blur-xs (darkening to /50
7
+ where backdrop-filter is unsupported) — changed across dialog/
8
+ alert-dialog/sheet/drawer/command in one pass. Tokens global. */
3
9
  .pk-dialog-trigger { display: inline-block; }
4
10
  .pk-dialog {
5
- position: fixed; z-index: 50; width: 100%; max-width: 32rem; max-height: 100vh;
6
- overflow-y: auto; gap: 1rem; border: 1px solid var(--pk-border);
7
- border-radius: var(--pk-radius); background: var(--pk-surface); color: var(--pk-text);
8
- padding: 1.5rem; box-shadow: 0 10px 30px rgb(0 0 0 / .35);
11
+ --pk-dialog-spacing: 1rem;
12
+ position: fixed; z-index: 50; width: 100%;
13
+ max-width: min(24rem, calc(100% - 2rem));
14
+ max-height: 100vh;
15
+ overflow-y: auto; gap: 1rem;
16
+ border: 1px solid color-mix(in oklab, var(--pk-text) 10%, transparent);
17
+ border-radius: .75rem; background: var(--pk-surface); color: var(--pk-text);
18
+ padding: var(--pk-dialog-spacing);
19
+ font-size: .875rem;
20
+ box-shadow: 0 10px 30px rgb(0 0 0 / .35);
9
21
  }
10
22
  .pk-dialog[open] { display: flex; flex-direction: column; }
11
- .pk-dialog::backdrop { background: rgb(0 0 0 / .5); backdrop-filter: blur(8px); }
12
- .pk-dialog.xs { max-width: 24rem; }
13
- .pk-dialog.sm { max-width: 28rem; }
14
- .pk-dialog.lg { max-width: 42rem; }
15
- .pk-dialog.xl { max-width: 56rem; }
16
- .pk-dialog.full { max-width: 100%; }
17
- .pk-dialog-header { display: flex; flex-direction: column; gap: .375rem; text-align: center; }
23
+ .pk-dialog::backdrop { background: rgb(0 0 0 / .1); backdrop-filter: blur(4px); }
24
+ @supports not (backdrop-filter: blur(1px)) {
25
+ .pk-dialog::backdrop { background: rgb(0 0 0 / .5); }
26
+ }
27
+ .pk-dialog.xs { max-width: min(16rem, calc(100% - 2rem)); }
28
+ .pk-dialog.sm { max-width: min(20rem, calc(100% - 2rem)); }
29
+ .pk-dialog.lg { max-width: min(32rem, calc(100% - 2rem)); }
30
+ .pk-dialog.xl { max-width: min(42rem, calc(100% - 2rem)); }
31
+ .pk-dialog.full { max-width: calc(100% - 2rem); }
32
+ .pk-dialog-header { display: flex; flex-direction: column; gap: .5rem; text-align: center; }
18
33
  .pk-dialog-footer {
19
34
  display: flex; flex-direction: column-reverse; gap: .5rem;
20
35
  margin: 0 calc(-1 * var(--pk-dialog-spacing)) calc(-1 * var(--pk-dialog-spacing));
21
36
  padding: var(--pk-dialog-spacing);
22
37
  border-top: 1px solid var(--pk-border);
38
+ border-radius: 0 0 .75rem .75rem;
23
39
  background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent);
24
40
  }
25
- .pk-dialog-title { font-size: 1.125rem; font-weight: 600; line-height: 1; letter-spacing: -.01em; }
41
+ .pk-dialog-title { font-size: 1rem; font-weight: 500; line-height: 1; }
26
42
  .pk-dialog-description { font-size: .875rem; color: var(--pk-muted); }
43
+ .pk-dialog-description a { text-decoration: underline; text-underline-offset: 3px; color: inherit; }
44
+ .pk-dialog-description a:hover { color: var(--pk-text); }
27
45
  .pk-dialog-middle { padding-block: 1rem; }
28
46
  .pk-overlay-close {
29
- position: absolute; inset-inline-end: 1rem; top: 1rem; border: 0; background: none;
47
+ position: absolute; inset-inline-end: .5rem; top: .5rem; border: 0; background: none;
30
48
  cursor: pointer; border-radius: calc(var(--pk-radius) - 4px); opacity: .7; color: var(--pk-text);
49
+ padding: .25rem; display: inline-flex;
31
50
  transition: opacity .15s ease;
32
51
  }
33
52
  .pk-overlay-close:hover { opacity: 1; }
34
53
  @media (min-width: 640px) {
35
54
  .pk-dialog-header { text-align: left; }
36
55
  .pk-dialog-footer { flex-direction: row; justify-content: flex-end; }
37
- .pk-dialog-footer > * + * { margin-inline-start: .5rem; }
38
56
  }
@@ -0,0 +1,13 @@
1
+ module PhlexKit
2
+ # Wraps any element (typically a Button) so clicking it dismisses the
3
+ # enclosing dialog, mirroring shadcn/ui's DialogClose. See dialog.rb.
4
+ class DialogClose < BaseComponent
5
+ def initialize(**attrs)
6
+ @attrs = attrs
7
+ end
8
+
9
+ def view_template(&)
10
+ span(**mix({ style: "display: contents", data: { action: "click->phlex-kit--dialog#dismiss" } }, @attrs), &)
11
+ end
12
+ end
13
+ end
@@ -1,17 +1,25 @@
1
1
  module PhlexKit
2
+ # The <dialog> box. `size:` picks the width tier (md = shadcn's default
3
+ # max-w-sm); `show_close_button: false` drops the top-right ✕ (their
4
+ # showCloseButton). See dialog.rb.
2
5
  class DialogContent < BaseComponent
3
6
  SIZES = { xs: "xs", sm: "sm", md: nil, lg: "lg", xl: "xl", full: "full" }.freeze
4
- def initialize(size: :md, **attrs)
7
+
8
+ def initialize(size: :md, show_close_button: true, **attrs)
5
9
  @size = size.to_sym
10
+ @show_close_button = show_close_button
6
11
  @attrs = attrs
7
12
  end
13
+
8
14
  def view_template(&block)
9
- cls = ["pk-dialog", SIZES.fetch(@size)].compact.join(" ")
15
+ cls = [ "pk-dialog", SIZES.fetch(@size) ].compact.join(" ")
10
16
  dialog(**mix({ class: cls, data: { phlex_kit__dialog_target: "dialog", action: "click->phlex-kit--dialog#backdropClick" } }, @attrs)) do
11
17
  yield
12
- button(type: "button", class: "pk-overlay-close", data: { action: "click->phlex-kit--dialog#dismiss" }) do
13
- render Icon.new(:x, size: 15)
14
- span(class: "pk-sr-only") { "Close" }
18
+ if @show_close_button
19
+ button(type: "button", class: "pk-overlay-close", data: { action: "click->phlex-kit--dialog#dismiss" }) do
20
+ render Icon.new(:x, size: 15)
21
+ span(class: "pk-sr-only") { "Close" }
22
+ end
15
23
  end
16
24
  end
17
25
  end
@@ -1,47 +1,83 @@
1
- /* Co-located with drawer.rb — UI::Drawer, shadcn parity (vaul replaced by the
2
- kit's sheet clone machinery): dimmed overlay, bottom sheet with grab handle
3
- sliding up. */
1
+ /* Co-located with drawer.rb — PhlexKit::Drawer, shadcn parity (vaul replaced
2
+ by the kit's sheet clone machinery). Matched to nova: popover surface,
3
+ text-sm body, rounded-xl on the attached edge only, all four sides
4
+ (bottom default; top/left/right via DrawerContent side:). nova hides the
5
+ classic grab handle (.cn-drawer-handle is `hidden`) — ours matches, and a
6
+ host can re-show it with `.pk-drawer-handle { display: block }`. */
4
7
  .pk-drawer-trigger, .pk-drawer-close { display: inline-block; }
5
8
  .pk-drawer-overlay {
6
9
  position: fixed;
7
10
  inset: 0;
8
11
  z-index: 50;
9
- background: rgb(0 0 0 / .5);
10
- backdrop-filter: blur(8px);
12
+ background: rgb(0 0 0 / .1);
13
+ backdrop-filter: blur(4px);
11
14
  }
12
15
  .pk-drawer {
13
16
  position: fixed;
14
- inset-inline: 0;
15
- bottom: 0;
16
17
  z-index: 50;
17
18
  display: flex;
18
19
  flex-direction: column;
19
- max-height: 85vh;
20
- border: 1px solid var(--pk-border);
21
- border-bottom: 0;
22
- border-top-left-radius: calc(var(--pk-radius) + 4px);
23
- border-top-right-radius: calc(var(--pk-radius) + 4px);
24
20
  background: var(--pk-surface);
25
21
  color: var(--pk-text);
22
+ font-size: .875rem;
23
+ }
24
+ /* bottom (default) */
25
+ .pk-drawer.bottom {
26
+ inset-inline: 0;
27
+ bottom: 0;
28
+ max-height: 85vh;
29
+ border-top: 1px solid var(--pk-border);
30
+ border-radius: .75rem .75rem 0 0;
26
31
  animation: pk-drawer-up .3s ease;
27
32
  }
33
+ .pk-drawer.top {
34
+ inset-inline: 0;
35
+ top: 0;
36
+ max-height: 85vh;
37
+ border-bottom: 1px solid var(--pk-border);
38
+ border-radius: 0 0 .75rem .75rem;
39
+ animation: pk-drawer-down .3s ease;
40
+ }
41
+ .pk-drawer.left,
42
+ .pk-drawer.right {
43
+ inset-block: 0;
44
+ width: 75%;
45
+ max-width: 24rem;
46
+ max-height: 100vh;
47
+ }
48
+ .pk-drawer.left {
49
+ left: 0;
50
+ border-right: 1px solid var(--pk-border);
51
+ border-radius: 0 .75rem .75rem 0;
52
+ animation: pk-drawer-from-left .3s ease;
53
+ }
54
+ .pk-drawer.right {
55
+ right: 0;
56
+ border-left: 1px solid var(--pk-border);
57
+ border-radius: .75rem 0 0 .75rem;
58
+ animation: pk-drawer-from-right .3s ease;
59
+ }
28
60
  @keyframes pk-drawer-up { from { transform: translateY(100%); } to { transform: translateY(0); } }
61
+ @keyframes pk-drawer-down { from { transform: translateY(-100%); } to { transform: translateY(0); } }
62
+ @keyframes pk-drawer-from-left { from { transform: translateX(-100%); } to { transform: translateX(0); } }
63
+ @keyframes pk-drawer-from-right { from { transform: translateX(100%); } to { transform: translateX(0); } }
29
64
  .pk-drawer-handle {
65
+ display: none;
30
66
  flex: none;
31
67
  width: 100px;
32
- height: .5rem;
33
- margin: .75rem auto 0;
68
+ height: .25rem;
69
+ margin: 1rem auto 0;
34
70
  border-radius: 9999px;
35
71
  background: var(--pk-surface-2);
36
72
  }
37
73
  .pk-drawer-header {
38
74
  display: flex;
39
75
  flex-direction: column;
40
- gap: .375rem;
76
+ gap: .125rem;
41
77
  padding: 1rem;
42
78
  text-align: center;
43
79
  }
44
- .pk-drawer-title { margin: 0; font-size: 1.125rem; font-weight: 600; }
80
+ .pk-drawer-title { margin: 0; font-size: 1rem; font-weight: 500; color: var(--pk-text); }
45
81
  .pk-drawer-description { margin: 0; font-size: .875rem; color: var(--pk-muted); }
46
82
  .pk-drawer-footer {
47
83
  display: flex;
@@ -50,6 +86,9 @@
50
86
  margin-top: auto;
51
87
  padding: 1rem;
52
88
  }
53
- @media (min-width: 640px) {
89
+ @media (min-width: 768px) {
54
90
  .pk-drawer-header { text-align: left; }
55
91
  }
92
+ @supports not (backdrop-filter: blur(1px)) {
93
+ .pk-drawer-overlay { background: rgb(0 0 0 / .5); }
94
+ }
@@ -1,8 +1,12 @@
1
1
  module PhlexKit
2
- # <template> holding the drawer overlay + bottom panel (cloned into <body>
3
- # on open, like SheetContent). See drawer.rb.
2
+ # <template> holding the drawer overlay + panel (cloned into <body> on open,
3
+ # like SheetContent). `side:` picks the attached edge — :bottom (default),
4
+ # :top, :left or :right, matching shadcn's direction prop. See drawer.rb.
4
5
  class DrawerContent < BaseComponent
5
- def initialize(**attrs)
6
+ SIDES = { bottom: "bottom", top: "top", left: "left", right: "right" }.freeze
7
+
8
+ def initialize(side: :bottom, **attrs)
9
+ @side = side.to_sym
6
10
  @attrs = attrs
7
11
  end
8
12
 
@@ -10,7 +14,7 @@ module PhlexKit
10
14
  template(data: { phlex_kit__sheet_target: "content" }) do
11
15
  div(data: { controller: "phlex-kit--sheet-content" }) do
12
16
  div(class: "pk-drawer-overlay", data: { action: "click->phlex-kit--sheet-content#close" })
13
- div(**mix({ class: "pk-drawer", role: "dialog", aria: { modal: "true" } }, @attrs)) do
17
+ div(**mix({ class: "pk-drawer #{SIDES.fetch(@side)}", role: "dialog", aria: { modal: "true" } }, @attrs)) do
14
18
  div(class: "pk-drawer-handle", aria: { hidden: "true" })
15
19
  yield if block
16
20
  end
@@ -1,25 +1,30 @@
1
- /* Co-located with empty.rb. Theme tokens from the global stylesheet. */
1
+ /* Co-located with empty.rb. Matched to shadcn/ui's current empty (nova):
2
+ borderless by default (their border-dashed sets only the STYLE — the
3
+ Outline example opts into a visible dashed border via class/style),
4
+ rounded-xl, p-6, text-sm title. Theme tokens from the global stylesheet. */
2
5
  .pk-empty {
3
6
  display: flex; width: 100%; min-width: 0; flex: 1; flex-direction: column;
4
7
  align-items: center; justify-content: center; gap: 1rem;
5
- border: 1px dashed var(--pk-border); border-radius: 1.5rem;
6
- padding: 3rem; text-align: center;
8
+ border: 0 dashed var(--pk-border); border-radius: .75rem;
9
+ padding: 1.5rem; text-align: center; text-wrap: balance;
7
10
  }
11
+ .pk-empty.outline { border-width: 1px; }
8
12
  .pk-empty-header { display: flex; max-width: 24rem; flex-direction: column; align-items: center; gap: .5rem; }
9
13
  .pk-empty-media {
10
14
  margin-bottom: .5rem; display: flex; flex: none;
11
15
  align-items: center; justify-content: center;
12
16
  }
17
+ .pk-empty-media svg { pointer-events: none; flex: none; }
13
18
  .pk-empty-media.icon {
14
- height: 2.5rem; width: 2.5rem; border-radius: .75rem;
19
+ height: 2rem; width: 2rem; border-radius: var(--pk-radius);
15
20
  background: var(--pk-surface-2); color: var(--pk-text);
16
21
  }
17
- .pk-empty-media.icon > svg:not([class*="size-"]) { width: 1.25rem; height: 1.25rem; }
18
- .pk-empty-title { font-size: 1.125rem; font-weight: 500; letter-spacing: -.01em; }
22
+ .pk-empty-media.icon > svg:not([class*="size-"]) { width: 1rem; height: 1rem; }
23
+ .pk-empty-title { font-size: .875rem; font-weight: 500; letter-spacing: -.025em; }
19
24
  .pk-empty-description { font-size: .875rem; line-height: 1.625; color: var(--pk-muted); }
20
- .pk-empty-description a { text-decoration: underline; text-underline-offset: 4px; }
25
+ .pk-empty-description a { text-decoration: underline; text-underline-offset: 4px; color: inherit; }
21
26
  .pk-empty-description a:hover { color: var(--pk-brand); }
22
27
  .pk-empty-content {
23
28
  display: flex; width: 100%; max-width: 24rem; min-width: 0;
24
- flex-direction: column; align-items: center; gap: 1rem; font-size: .875rem;
29
+ flex-direction: column; align-items: center; gap: .625rem; font-size: .875rem;
25
30
  }
@@ -0,0 +1,152 @@
1
+ /* Co-located with field.rb — the PhlexKit::Field system (Set / Legend /
2
+ Group / Field / Content / Label / Title / Description / Separator /
3
+ Error), shadcn parity. Field is presentational layout; FormField remains
4
+ the Stimulus live-validation wrapper — they nest freely. FieldGroup is a
5
+ size container so orientation: :responsive fields can flip horizontal at
6
+ ≥28rem of group width. The choice-card recipe is a FieldLabel wrapping a
7
+ whole Field: it grows a border and highlights while its control is
8
+ checked (:has(input:checked)). */
9
+ .pk-field-set {
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: 1rem;
13
+ min-width: 0;
14
+ margin: 0;
15
+ padding: 0;
16
+ border: 0;
17
+ }
18
+ .pk-field-set:has(> .pk-radio-group) { gap: .75rem; }
19
+ .pk-field-legend {
20
+ margin-bottom: .375rem;
21
+ padding: 0;
22
+ font-weight: 500;
23
+ }
24
+ .pk-field-legend.legend { font-size: 1rem; }
25
+ .pk-field-legend.label { font-size: .875rem; }
26
+ .pk-field-group {
27
+ display: flex;
28
+ width: 100%;
29
+ flex-direction: column;
30
+ gap: 1.25rem;
31
+ container-type: inline-size;
32
+ }
33
+ .pk-field-group .pk-field-group { gap: 1rem; container-type: normal; }
34
+
35
+ .pk-field { display: flex; width: 100%; gap: .5rem; }
36
+ .pk-field[data-invalid="true"] { color: var(--pk-red); }
37
+ .pk-field[data-orientation="vertical"] { flex-direction: column; }
38
+ .pk-field[data-orientation="vertical"] > * { width: 100%; }
39
+ .pk-field[data-orientation="vertical"] > .pk-sr-only { width: auto; }
40
+ .pk-field[data-orientation="horizontal"] { flex-direction: row; align-items: center; }
41
+ .pk-field[data-orientation="horizontal"] > .pk-field-label,
42
+ .pk-field[data-orientation="horizontal"] > .pk-field-content { flex: 1 1 auto; }
43
+ .pk-field[data-orientation="horizontal"]:has(> .pk-field-content) { align-items: flex-start; }
44
+ .pk-field[data-orientation="horizontal"]:has(> .pk-field-content) > .pk-checkbox,
45
+ .pk-field[data-orientation="horizontal"]:has(> .pk-field-content) > .pk-radio { margin-top: 1px; }
46
+ .pk-field[data-orientation="responsive"] { flex-direction: column; }
47
+ .pk-field[data-orientation="responsive"] > * { width: 100%; }
48
+ .pk-field[data-orientation="responsive"] > .pk-sr-only { width: auto; }
49
+ @container (min-width: 28rem) {
50
+ .pk-field[data-orientation="responsive"] { flex-direction: row; align-items: center; }
51
+ .pk-field[data-orientation="responsive"] > * { width: auto; }
52
+ .pk-field[data-orientation="responsive"] > .pk-field-label,
53
+ .pk-field[data-orientation="responsive"] > .pk-field-content { flex: 1 1 auto; }
54
+ .pk-field[data-orientation="responsive"]:has(> .pk-field-content) { align-items: flex-start; }
55
+ }
56
+
57
+ .pk-field-content {
58
+ display: flex;
59
+ flex: 1;
60
+ flex-direction: column;
61
+ gap: .125rem;
62
+ line-height: 1.375;
63
+ }
64
+ .pk-field-label {
65
+ gap: .5rem;
66
+ line-height: 1.375;
67
+ width: fit-content;
68
+ }
69
+ .pk-field[data-disabled="true"] .pk-field-label,
70
+ .pk-field[data-disabled="true"] .pk-field-title { opacity: .5; }
71
+
72
+ /* Choice card: a FieldLabel wrapping a whole Field. */
73
+ .pk-field-label:has(> .pk-field) {
74
+ width: 100%;
75
+ flex-direction: column;
76
+ align-items: stretch;
77
+ border: 1px solid var(--pk-border);
78
+ border-radius: var(--pk-radius);
79
+ font-weight: 400;
80
+ transition: background-color .15s ease, border-color .15s ease;
81
+ }
82
+ .pk-field-label > .pk-field { padding: .625rem; }
83
+ .pk-field-label:has(> .pk-field):has(input:checked) {
84
+ background: color-mix(in oklab, var(--pk-brand) 10%, transparent);
85
+ border-color: color-mix(in oklab, var(--pk-brand) 20%, transparent);
86
+ }
87
+ :root[data-theme="light"] .pk-field-label:has(> .pk-field):has(input:checked) {
88
+ background: color-mix(in oklab, var(--pk-brand) 5%, transparent);
89
+ border-color: color-mix(in oklab, var(--pk-brand) 30%, transparent);
90
+ }
91
+ @media (prefers-color-scheme: light) {
92
+ :root[data-theme="system"] .pk-field-label:has(> .pk-field):has(input:checked) {
93
+ background: color-mix(in oklab, var(--pk-brand) 5%, transparent);
94
+ border-color: color-mix(in oklab, var(--pk-brand) 30%, transparent);
95
+ }
96
+ }
97
+
98
+ .pk-field-title {
99
+ display: flex;
100
+ width: fit-content;
101
+ align-items: center;
102
+ gap: .5rem;
103
+ font-size: .875rem;
104
+ font-weight: 500;
105
+ line-height: 1.375;
106
+ }
107
+ .pk-field-description {
108
+ margin: 0;
109
+ color: var(--pk-muted);
110
+ font-size: .875rem;
111
+ font-weight: 400;
112
+ line-height: 1.5;
113
+ text-align: left;
114
+ }
115
+ .pk-field-description a { text-decoration: underline; text-underline-offset: 4px; color: inherit; }
116
+ .pk-field-description a:hover { color: var(--pk-brand); }
117
+ .pk-field-legend.legend + .pk-field-description { margin-top: -.375rem; }
118
+
119
+ .pk-field-separator {
120
+ position: relative;
121
+ height: 1.25rem;
122
+ margin-block: -.5rem;
123
+ font-size: .875rem;
124
+ }
125
+ .pk-field-separator .pk-field-separator-line {
126
+ position: absolute;
127
+ inset-inline: 0;
128
+ top: 50%;
129
+ }
130
+ .pk-field-separator-content {
131
+ position: relative;
132
+ display: block;
133
+ width: fit-content;
134
+ margin-inline: auto;
135
+ padding-inline: .5rem;
136
+ background: var(--pk-bg);
137
+ color: var(--pk-muted);
138
+ }
139
+
140
+ .pk-field-error {
141
+ color: var(--pk-red);
142
+ font-size: .875rem;
143
+ font-weight: 400;
144
+ }
145
+ .pk-field-error-list {
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: .25rem;
149
+ margin: 0 0 0 1rem;
150
+ padding: 0;
151
+ list-style: disc;
152
+ }
@@ -0,0 +1,35 @@
1
+ module PhlexKit
2
+ # One labeled control row/column, ported from shadcn/ui's Field. PRESENTATIONAL
3
+ # layout only — pair with PhlexKit::FormField when you want the kit's Stimulus
4
+ # live-validation (FormField wraps the control; Field arranges label, control,
5
+ # description and error around it — they nest without conflict).
6
+ #
7
+ # render PhlexKit::Field.new do
8
+ # render PhlexKit::FieldLabel.new(for: "email") { "Email" }
9
+ # render PhlexKit::Input.new(id: "email", type: :email)
10
+ # render PhlexKit::FieldDescription.new { "We never share it." }
11
+ # end
12
+ #
13
+ # orientation: :vertical (default) stacks; :horizontal puts the label beside
14
+ # the control (checkbox/switch rows); :responsive is vertical that goes
15
+ # horizontal when the enclosing FieldGroup is ≥28rem wide (container query).
16
+ # `invalid: true` tints the whole field; `disabled: true` dims labels.
17
+ # `.pk-field*` (field.css).
18
+ class Field < BaseComponent
19
+ ORIENTATIONS = { vertical: "vertical", horizontal: "horizontal", responsive: "responsive" }.freeze
20
+
21
+ def initialize(orientation: :vertical, invalid: false, disabled: false, **attrs)
22
+ @orientation = orientation.to_sym
23
+ @invalid = invalid
24
+ @disabled = disabled
25
+ @attrs = attrs
26
+ end
27
+
28
+ def view_template(&)
29
+ data = { slot: "field", orientation: ORIENTATIONS.fetch(@orientation) }
30
+ data[:invalid] = "true" if @invalid
31
+ data[:disabled] = "true" if @disabled
32
+ div(**mix({ class: "pk-field", role: "group", data: data }, @attrs), &)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,11 @@
1
+ module PhlexKit
2
+ # Stacks a FieldTitle/FieldLabel + FieldDescription beside a control in a
3
+ # horizontal Field (e.g. checkbox rows with helper text), ported from
4
+ # shadcn/ui's FieldContent. See field.rb.
5
+ class FieldContent < BaseComponent
6
+ def initialize(**attrs) = (@attrs = attrs)
7
+ def view_template(&)
8
+ div(**mix({ class: "pk-field-content", data: { slot: "field-content" } }, @attrs), &)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module PhlexKit
2
+ # Muted helper text under (or beside) a Field's control, ported from
3
+ # shadcn/ui's FieldDescription. See field.rb.
4
+ class FieldDescription < BaseComponent
5
+ def initialize(**attrs) = (@attrs = attrs)
6
+ def view_template(&)
7
+ p(**mix({ class: "pk-field-description", data: { slot: "field-description" } }, @attrs), &)
8
+ end
9
+ end
10
+ end