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
@@ -7,30 +7,47 @@ module PhlexKit
7
7
  # :offcanvas — the rail hides/reveals via a PhlexKit::SidebarTrigger. On
8
8
  # desktop it slides out of the layout; below 768px it overlays
9
9
  # as a fixed drawer behind a click-to-close scrim, closed by
10
- # Escape and before Turbo caches the page. State is DOM-only:
11
- # `data-collapsed` (desktop) / `data-open` (mobile drawer).
10
+ # Escape and before Turbo caches the page.
11
+ # :icon — like :offcanvas, but on desktop the rail collapses to a
12
+ # 3rem icon strip instead of leaving: labels hide, menu
13
+ # buttons become icon squares (add `tooltip:` to
14
+ # SidebarMenuButton for hover labels), groups/badges/subs
15
+ # disappear. Mobile behaves like :offcanvas.
16
+ #
17
+ # State is DOM-only: `data-collapsed` (desktop) / `data-open` (mobile
18
+ # drawer). ⌘B/Ctrl+B toggles. The desktop state persists in a
19
+ # `pk_sidebar_state` cookie ("collapsed"/"expanded") — pass
20
+ # `default_collapsed: cookies[:pk_sidebar_state] == "collapsed"` from the
21
+ # host layout to render the collapsed rail server-side with no flash.
22
+ # Add a PhlexKit::SidebarRail inside the Sidebar for the edge grab-strip.
12
23
  # See sidebar.rb.
13
24
  class SidebarWrapper < BaseComponent
14
- COLLAPSIBLE = { none: nil, offcanvas: "collapsible-offcanvas" }.freeze
25
+ COLLAPSIBLE = { none: nil, offcanvas: "collapsible-offcanvas", icon: "collapsible-icon" }.freeze
15
26
 
16
- def initialize(collapsible: :none, **attrs)
27
+ def initialize(collapsible: :none, default_collapsed: false, **attrs)
17
28
  @collapsible = collapsible.to_sym
29
+ @default_collapsed = default_collapsed
18
30
  @attrs = attrs
19
31
  end
20
32
 
21
33
  def view_template(&block)
22
- unless offcanvas?
34
+ unless collapsible?
23
35
  return div(**mix({ class: classes }, @attrs), &block)
24
36
  end
25
37
 
26
- div(**mix({
38
+ base = {
27
39
  class: classes,
28
40
  data: {
29
41
  controller: "phlex-kit--sidebar",
30
42
  action: "keydown.esc@window->phlex-kit--sidebar#closeMobile " \
43
+ "keydown.meta+b@window->phlex-kit--sidebar#toggle:prevent " \
44
+ "keydown.ctrl+b@window->phlex-kit--sidebar#toggle:prevent " \
31
45
  "turbo:before-cache@window->phlex-kit--sidebar#closeMobile"
32
46
  }
33
- }, @attrs)) do
47
+ }
48
+ base["data-collapsed"] = "" if @default_collapsed
49
+
50
+ div(**mix(base, @attrs)) do
34
51
  yield if block
35
52
  button(type: :button, class: "pk-sidebar-scrim", aria_label: "Close sidebar",
36
53
  tabindex: "-1", data: { action: "click->phlex-kit--sidebar#closeMobile" })
@@ -39,7 +56,7 @@ module PhlexKit
39
56
 
40
57
  private
41
58
 
42
- def offcanvas? = @collapsible == :offcanvas
59
+ def collapsible? = @collapsible != :none
43
60
 
44
61
  def classes
45
62
  [ "pk-sidebar-wrapper", COLLAPSIBLE.fetch(@collapsible) ].compact.join(" ")
@@ -1,7 +1,7 @@
1
1
  /* Co-located with skeleton.rb. Theme tokens come from the global stylesheet. */
2
2
  .pk-skeleton {
3
3
  border-radius: calc(var(--pk-radius) - 2px);
4
- background: color-mix(in oklab, var(--pk-brand, #6c8cff) 10%, transparent);
4
+ background: var(--pk-surface-2);
5
5
  animation: pk-pulse 1.5s cubic-bezier(.4, 0, .6, 1) infinite;
6
6
  }
7
7
  @keyframes pk-pulse { 50% { opacity: .5; } }
@@ -12,7 +12,7 @@
12
12
  }
13
13
  .pk-slider:focus-visible { outline: none; }
14
14
  .pk-slider::-webkit-slider-runnable-track {
15
- height: .375rem;
15
+ height: .25rem;
16
16
  border-radius: 9999px;
17
17
  background: linear-gradient(to right,
18
18
  var(--pk-brand) var(--pk-slider-progress, 50%),
@@ -20,33 +20,54 @@
20
20
  }
21
21
  .pk-slider::-webkit-slider-thumb {
22
22
  -webkit-appearance: none;
23
- width: 1rem;
24
- height: 1rem;
25
- margin-top: -0.3125rem;
23
+ width: .75rem;
24
+ height: .75rem;
25
+ margin-top: -0.25rem;
26
26
  border-radius: 9999px;
27
- border: 1px solid var(--pk-brand);
28
- background: var(--pk-bg);
27
+ border: 1px solid var(--pk-ring);
28
+ background: #fff;
29
29
  box-shadow: 0 1px 2px rgb(0 0 0 / .25);
30
30
  transition: box-shadow .15s ease;
31
31
  }
32
+ .pk-slider:hover::-webkit-slider-thumb,
33
+ .pk-slider:active::-webkit-slider-thumb {
34
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
35
+ }
32
36
  .pk-slider:focus-visible::-webkit-slider-thumb {
33
37
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
34
38
  }
35
39
  .pk-slider::-moz-range-track {
36
- height: .375rem;
40
+ height: .25rem;
37
41
  border-radius: 9999px;
38
42
  background: var(--pk-surface-2);
39
43
  }
40
44
  .pk-slider::-moz-range-progress {
41
- height: .375rem;
45
+ height: .25rem;
42
46
  border-radius: 9999px;
43
47
  background: var(--pk-brand);
44
48
  }
45
49
  .pk-slider::-moz-range-thumb {
46
- width: 1rem;
47
- height: 1rem;
50
+ width: .75rem;
51
+ height: .75rem;
48
52
  border-radius: 9999px;
49
- border: 1px solid var(--pk-brand);
50
- background: var(--pk-bg);
53
+ border: 1px solid var(--pk-ring);
54
+ background: #fff;
55
+ }
56
+ .pk-slider:hover::-moz-range-thumb,
57
+ .pk-slider:active::-moz-range-thumb {
58
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
51
59
  }
52
60
  .pk-slider:disabled { opacity: .5; cursor: not-allowed; }
61
+ /* Vertical: modern writing-mode flip (min-h-40 like theirs). */
62
+ .pk-slider.vertical {
63
+ writing-mode: vertical-lr;
64
+ direction: rtl;
65
+ width: 1rem;
66
+ height: auto;
67
+ min-height: 10rem;
68
+ }
69
+ .pk-slider.vertical::-webkit-slider-runnable-track {
70
+ background: linear-gradient(to top,
71
+ var(--pk-brand) var(--pk-slider-progress, 50%),
72
+ var(--pk-surface-2) var(--pk-slider-progress, 50%));
73
+ }
@@ -1,28 +1,65 @@
1
- /* Co-located with switch.rb. Theme tokens from the global stylesheet. */
1
+ /* Co-located with switch.rb. Matched to shadcn/ui's current switch (nova):
2
+ 32×18.4px track (sm 24×14) with a size-4 thumb sliding calc(100%-2px);
3
+ unchecked fills with the input tone (stronger in dark), checked with the
4
+ brand; the thumb is the page background in light, foreground in dark
5
+ (brand-ink when checked). Pure CSS (:has(:checked)), no Stimulus. */
2
6
  .pk-switch {
7
+ position: relative;
3
8
  display: inline-flex;
4
- height: 1.5rem;
5
- width: 2.75rem;
9
+ height: 18.4px;
10
+ width: 32px;
6
11
  flex: none;
7
12
  cursor: pointer;
8
13
  align-items: center;
9
- border: 2px solid transparent;
14
+ border: 1px solid transparent;
10
15
  border-radius: 9999px;
11
- background: var(--pk-border);
12
- transition: background-color .15s ease;
16
+ background: color-mix(in oklab, var(--pk-input) 80%, transparent);
17
+ transition: background-color .15s ease, box-shadow .15s ease;
13
18
  }
19
+ .pk-switch.sm { height: 14px; width: 24px; }
14
20
  .pk-switch:has(.pk-switch-input:checked) { background: var(--pk-brand); }
15
21
  .pk-switch:has(.pk-switch-input:disabled) { cursor: not-allowed; opacity: .5; }
16
- .pk-switch-input { display: none; }
22
+ .pk-switch:has(.pk-switch-input:focus-visible) {
23
+ border-color: var(--pk-ring);
24
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
25
+ }
26
+ .pk-switch:has(.pk-switch-input[aria-invalid="true"]) {
27
+ border-color: var(--pk-red);
28
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 40%, transparent);
29
+ }
30
+ .pk-switch-input {
31
+ position: absolute;
32
+ width: 1px;
33
+ height: 1px;
34
+ opacity: 0;
35
+ pointer-events: none;
36
+ }
17
37
  .pk-switch-thumb {
18
38
  pointer-events: none;
19
39
  display: block;
20
- height: 1.25rem;
21
- width: 1.25rem;
40
+ height: 1rem;
41
+ width: 1rem;
22
42
  border-radius: 9999px;
23
- background: var(--pk-surface);
24
- box-shadow: 0 1px 3px rgb(0 0 0 / .3);
25
- transition: transform .15s ease;
43
+ background: var(--pk-text);
44
+ transition: transform .15s ease, background-color .15s ease;
26
45
  transform: translateX(0);
27
46
  }
28
- .pk-switch:has(.pk-switch-input:checked) .pk-switch-thumb { transform: translateX(1.25rem); }
47
+ .pk-switch.sm .pk-switch-thumb { height: .75rem; width: .75rem; }
48
+ .pk-switch:has(.pk-switch-input:checked) .pk-switch-thumb {
49
+ transform: translateX(calc(100% - 2px));
50
+ background: var(--pk-brand-ink);
51
+ }
52
+ :root[data-theme="light"] .pk-switch { background: var(--pk-input); }
53
+ :root[data-theme="light"] .pk-switch:has(.pk-switch-input:checked) { background: var(--pk-brand); }
54
+ :root[data-theme="light"] .pk-switch-thumb { background: var(--pk-bg) !important; }
55
+ :root[data-theme="light"] .pk-switch:has(.pk-switch-input[aria-invalid="true"]) {
56
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
57
+ }
58
+ @media (prefers-color-scheme: light) {
59
+ :root[data-theme="system"] .pk-switch { background: var(--pk-input); }
60
+ :root[data-theme="system"] .pk-switch:has(.pk-switch-input:checked) { background: var(--pk-brand); }
61
+ :root[data-theme="system"] .pk-switch-thumb { background: var(--pk-bg) !important; }
62
+ :root[data-theme="system"] .pk-switch:has(.pk-switch-input[aria-invalid="true"]) {
63
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
64
+ }
65
+ }
@@ -3,15 +3,18 @@ module PhlexKit
3
3
  # ruby_ui's RubyUI::Switch — pure CSS (`:has(:checked)`), no Stimulus. Emits an
4
4
  # optional hidden field so an unchecked box still posts a value (Rails idiom).
5
5
  class Switch < BaseComponent
6
- def initialize(include_hidden: true, checked_value: "1", unchecked_value: "0", **attrs)
6
+ SIZES = { md: nil, sm: "sm" }.freeze
7
+
8
+ def initialize(include_hidden: true, checked_value: "1", unchecked_value: "0", size: :md, **attrs)
7
9
  @include_hidden = include_hidden
8
10
  @checked_value = checked_value
9
11
  @unchecked_value = unchecked_value
12
+ @size = size.to_sym
10
13
  @attrs = attrs
11
14
  end
12
15
 
13
16
  def view_template
14
- label(**mix({ role: "switch", class: "pk-switch" }, {})) do
17
+ label(**mix({ role: "switch", class: [ "pk-switch", SIZES.fetch(@size) ].compact.join(" ") }, {})) do
15
18
  input(type: "hidden", name: @attrs[:name], value: @unchecked_value) if @include_hidden
16
19
  input(**mix({ class: "pk-switch-input" }, @attrs).merge(type: "checkbox", value: @checked_value))
17
20
  span(class: "pk-switch-thumb")
@@ -7,10 +7,11 @@
7
7
 
8
8
  .pk-table-header .pk-table-row { border-bottom: 1px solid var(--pk-border); }
9
9
  .pk-table-body .pk-table-row:last-child { border-bottom: 0; }
10
- .pk-table-footer { border-top: 1px solid var(--pk-border); font-weight: 500; }
10
+ .pk-table-footer { border-top: 1px solid var(--pk-border); font-weight: 500; background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent); }
11
11
 
12
12
  .pk-table-row { border-bottom: 1px solid var(--pk-border); transition: background-color .12s ease; }
13
- .pk-table-row:hover { background: color-mix(in oklab, var(--pk-surface-2) 60%, transparent); }
13
+ .pk-table-row:hover { background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent); }
14
+ .pk-table-row[data-state="selected"] { background: var(--pk-surface-2); }
14
15
 
15
16
  .pk-table-head {
16
17
  height: 2.5rem;
@@ -18,7 +19,10 @@
18
19
  text-align: left;
19
20
  vertical-align: middle;
20
21
  font-weight: 500;
21
- color: var(--pk-muted);
22
+ white-space: nowrap;
23
+ color: var(--pk-text);
22
24
  }
23
- .pk-table-cell { padding: .5rem; vertical-align: middle; }
25
+ .pk-table-head:has([role="checkbox"]),
26
+ .pk-table-cell:has([role="checkbox"]) { padding-right: 0; }
27
+ .pk-table-cell { padding: .5rem; vertical-align: middle; white-space: nowrap; }
24
28
  .pk-table-caption { margin-top: 1rem; font-size: .875rem; color: var(--pk-muted); }
@@ -1,13 +1,77 @@
1
- /* Co-located with tabs.rb. Theme tokens from the global stylesheet. */
1
+ /* Co-located with tabs.rb. Matched to nova: h-8 list with 3px padding,
2
+ rounded-md px-1.5 triggers with data-icon insets and svg-4 glyphs, active
3
+ fill (translucent input in dark) with a soft shadow; the `line` variant is
4
+ transparent with an animated underline; `orientation: :vertical` stacks
5
+ the list (underline moves to the right edge). Tokens global. */
6
+ .pk-tabs { display: flex; flex-direction: column; gap: .5rem; }
7
+ .pk-tabs.vertical { flex-direction: row; align-items: flex-start; }
2
8
  .pk-tabs-list {
3
- display: inline-flex; height: 2.25rem; align-items: center; justify-content: center;
4
- border-radius: var(--pk-radius); background: var(--pk-surface-2); padding: .25rem; color: var(--pk-muted);
9
+ display: inline-flex; width: fit-content; height: 2rem; align-items: center; justify-content: center;
10
+ border-radius: var(--pk-radius); background: var(--pk-surface-2); padding: 3px; color: var(--pk-muted);
5
11
  }
12
+ .pk-tabs.vertical .pk-tabs-list { height: fit-content; flex-direction: column; align-items: stretch; }
6
13
  .pk-tabs-trigger {
7
- display: inline-flex; align-items: center; justify-content: center; white-space: nowrap;
8
- border-radius: calc(var(--pk-radius) - 2px); padding: .25rem .75rem; font-size: .875rem; font-weight: 500;
9
- background: none; border: 0; cursor: pointer; color: inherit; transition: all .15s ease;
14
+ position: relative;
15
+ display: inline-flex; align-items: center; justify-content: center; gap: .375rem;
16
+ white-space: nowrap; flex: 1;
17
+ border: 1px solid transparent;
18
+ border-radius: calc(var(--pk-radius) - 2px); padding: 2px .375rem; font-size: .875rem; font-weight: 500;
19
+ background: none; cursor: pointer; color: inherit; transition: all .15s ease;
20
+ }
21
+ .pk-tabs.vertical .pk-tabs-trigger { width: 100%; justify-content: flex-start; }
22
+ .pk-tabs-trigger:hover { color: var(--pk-text); }
23
+ .pk-tabs-trigger svg { pointer-events: none; flex-shrink: 0; }
24
+ .pk-tabs-trigger svg:not([class*="size-"]) { width: 1rem; height: 1rem; }
25
+ .pk-tabs-trigger:has(> [data-icon="inline-start"]) { padding-left: .25rem; }
26
+ .pk-tabs-trigger:has(> [data-icon="inline-end"]) { padding-right: .25rem; }
27
+ .pk-tabs-trigger:focus-visible {
28
+ outline: none;
29
+ border-color: var(--pk-ring);
30
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
31
+ }
32
+ .pk-tabs-trigger[data-state="active"] {
33
+ background: color-mix(in oklab, var(--pk-input) 30%, transparent);
34
+ border-color: var(--pk-input);
35
+ color: var(--pk-text);
36
+ box-shadow: 0 1px 2px rgb(0 0 0 / .1);
10
37
  }
11
- .pk-tabs-trigger[data-state="active"] { background: var(--pk-bg); color: var(--pk-text); box-shadow: 0 1px 2px rgb(0 0 0 / .1); }
12
38
  .pk-tabs-trigger:disabled { pointer-events: none; opacity: .5; }
13
- .pk-tabs-content { margin-top: .5rem; }
39
+
40
+ /* line variant: transparent list, underline on the active trigger. */
41
+ .pk-tabs-list.line { background: transparent; border-radius: 0; gap: .25rem; padding: 0; }
42
+ .pk-tabs-list.line .pk-tabs-trigger[data-state="active"] {
43
+ background: transparent;
44
+ border-color: transparent;
45
+ box-shadow: none;
46
+ }
47
+ .pk-tabs-trigger::after {
48
+ content: "";
49
+ position: absolute;
50
+ inset-inline: 0;
51
+ bottom: -5px;
52
+ height: 2px;
53
+ background: var(--pk-text);
54
+ opacity: 0;
55
+ transition: opacity .15s ease;
56
+ }
57
+ .pk-tabs.vertical .pk-tabs-trigger::after {
58
+ inset-inline: auto;
59
+ inset-block: 0;
60
+ right: -.25rem;
61
+ width: 2px;
62
+ height: auto;
63
+ }
64
+ .pk-tabs-list.line .pk-tabs-trigger[data-state="active"]::after { opacity: 1; }
65
+
66
+ .pk-tabs-content { flex: 1; font-size: .875rem; }
67
+
68
+ :root[data-theme="light"] .pk-tabs-trigger[data-state="active"] {
69
+ background: var(--pk-bg);
70
+ border-color: transparent;
71
+ }
72
+ @media (prefers-color-scheme: light) {
73
+ :root[data-theme="system"] .pk-tabs-trigger[data-state="active"] {
74
+ background: var(--pk-bg);
75
+ border-color: transparent;
76
+ }
77
+ }
@@ -2,12 +2,17 @@ module PhlexKit
2
2
  # Tabbed panels. Ported from ruby_ui's RubyUI::Tabs. Compose Tabs(default:) >
3
3
  # (TabsList > TabsTrigger(value:)) + TabsContent(value:). phlex-kit--tabs.
4
4
  class Tabs < BaseComponent
5
- def initialize(default: nil, **attrs)
5
+ ORIENTATIONS = { horizontal: nil, vertical: "vertical" }.freeze
6
+
7
+ def initialize(default: nil, orientation: :horizontal, **attrs)
6
8
  @default = default
9
+ @orientation = orientation.to_sym
7
10
  @attrs = attrs
8
11
  end
12
+
9
13
  def view_template(&)
10
- div(**mix({ class: "pk-tabs", data: { controller: "phlex-kit--tabs", phlex_kit__tabs_active_value: @default } }, @attrs), &)
14
+ classes = [ "pk-tabs", ORIENTATIONS.fetch(@orientation) ].compact.join(" ")
15
+ div(**mix({ class: classes, data: { controller: "phlex-kit--tabs", phlex_kit__tabs_active_value: @default } }, @attrs), &)
11
16
  end
12
17
  end
13
18
  end
@@ -1,6 +1,15 @@
1
1
  module PhlexKit
2
2
  class TabsList < BaseComponent
3
- def initialize(**attrs) = (@attrs = attrs)
4
- def view_template(&) = div(**mix({ class: "pk-tabs-list", role: "tablist" }, @attrs), &)
3
+ VARIANTS = { default: nil, line: "line" }.freeze
4
+
5
+ def initialize(variant: :default, **attrs)
6
+ @variant = variant.to_sym
7
+ @attrs = attrs
8
+ end
9
+
10
+ def view_template(&)
11
+ classes = [ "pk-tabs-list", VARIANTS.fetch(@variant) ].compact.join(" ")
12
+ div(**mix({ class: classes, role: "tablist" }, @attrs), &)
13
+ end
5
14
  end
6
15
  end
@@ -1,27 +1,47 @@
1
- /* Co-located with textarea.rb — UI::Textarea. Same vanilla treatment as
2
- .pk-input (input.css): ruby_ui's geometry (rounded-md / px-3 / text-sm /
3
- shadow / ring-on-focus), our palette tokens, height left to the `rows`
4
- attribute exactly as ruby_ui does. Theme tokens come from the global
5
- stylesheet. */
1
+ /* Co-located with textarea.rb — PhlexKit::Textarea. Same nova treatment as
2
+ .pk-input (input.css): rounded-lg, translucent dark fill / transparent
3
+ light, ring focus, invalid + disabled states, text-base on small screens
4
+ (iOS zoom guard) dropping to text-sm from md. Height stays with `rows`. */
6
5
  .pk-textarea {
7
6
  width: 100%;
8
- border-radius: calc(var(--pk-radius) - 2px);
7
+ border-radius: var(--pk-radius);
9
8
  border: 1px solid var(--pk-input);
10
- background: var(--pk-bg);
9
+ background: color-mix(in oklab, var(--pk-input) 30%, transparent);
11
10
  color: var(--pk-text);
12
- padding: .5rem .75rem;
11
+ padding: .5rem .625rem;
13
12
  font: inherit;
14
- font-size: .875rem;
15
- transition: color .15s ease, border-color .15s ease, box-shadow .15s ease;
13
+ font-size: 1rem;
14
+ transition: color .15s ease, border-color .15s ease, box-shadow .15s ease, background-color .15s ease;
15
+ }
16
+ @media (min-width: 768px) {
17
+ .pk-textarea { font-size: .875rem; }
16
18
  }
17
19
  .pk-textarea::placeholder { color: var(--pk-muted); }
18
20
  .pk-textarea:focus-visible {
19
21
  outline: none;
20
- border-color: var(--pk-brand);
22
+ border-color: var(--pk-ring);
21
23
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
22
24
  }
23
- .pk-textarea:disabled { cursor: not-allowed; opacity: .5; }
25
+ .pk-textarea:disabled {
26
+ cursor: not-allowed;
27
+ opacity: .5;
28
+ background: color-mix(in oklab, var(--pk-input) 80%, transparent);
29
+ }
24
30
  .pk-textarea[aria-invalid="true"] {
31
+ border-color: color-mix(in oklab, var(--pk-red) 50%, transparent);
32
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 40%, transparent);
33
+ }
34
+ :root[data-theme="light"] .pk-textarea { background: transparent; }
35
+ :root[data-theme="light"] .pk-textarea:disabled { background: color-mix(in oklab, var(--pk-input) 50%, transparent); }
36
+ :root[data-theme="light"] .pk-textarea[aria-invalid="true"] {
25
37
  border-color: var(--pk-red);
26
- box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 30%, transparent);
38
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
39
+ }
40
+ @media (prefers-color-scheme: light) {
41
+ :root[data-theme="system"] .pk-textarea { background: transparent; }
42
+ :root[data-theme="system"] .pk-textarea:disabled { background: color-mix(in oklab, var(--pk-input) 50%, transparent); }
43
+ :root[data-theme="system"] .pk-textarea[aria-invalid="true"] {
44
+ border-color: var(--pk-red);
45
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
46
+ }
27
47
  }
@@ -40,7 +40,7 @@
40
40
  width: 356px;
41
41
  max-width: 100%;
42
42
  border: 1px solid var(--pk-border);
43
- border-radius: var(--pk-radius);
43
+ border-radius: 1rem;
44
44
  background: var(--pk-surface);
45
45
  color: var(--pk-text);
46
46
  padding: 1rem;
@@ -1,16 +1,31 @@
1
- /* Co-located with toggle.rb. Theme tokens from the global stylesheet. */
1
+ /* Co-located with toggle.rb. Matched to nova: h-8/h-7/h-9 sizes with min-w,
2
+ rounded-lg (sm capped), muted hover + on-state fills, gap-1, data-icon
3
+ insets, svg-4 (3.5 at sm). Theme tokens from the global stylesheet. */
2
4
  .pk-contents { display: contents; }
3
5
  .pk-toggle {
4
- display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
5
- height: 2.25rem; min-width: 2.25rem; padding-inline: .5rem;
6
- border-radius: calc(var(--pk-radius) - 2px); border: 1px solid transparent; background: transparent;
6
+ display: inline-flex; align-items: center; justify-content: center; gap: .25rem;
7
+ height: 2rem; min-width: 2rem; padding-inline: .625rem;
8
+ border-radius: var(--pk-radius); border: 1px solid transparent; background: transparent;
7
9
  font-size: .875rem; font-weight: 500; white-space: nowrap; color: var(--pk-text);
8
10
  cursor: pointer; outline: none; transition: color .15s ease, background-color .15s ease, box-shadow .15s ease;
9
11
  }
10
- .pk-toggle:hover { background: var(--pk-accent); color: var(--pk-muted); }
12
+ .pk-toggle:hover { background: var(--pk-surface-2); color: var(--pk-text); }
11
13
  .pk-toggle:disabled { pointer-events: none; opacity: .5; }
12
- .pk-toggle[data-state="on"] { background: var(--pk-surface-2); color: var(--pk-text); }
13
- .pk-toggle.outline { border-color: var(--pk-border); }
14
- .pk-toggle.sm { height: 2rem; min-width: 2rem; padding-inline: .375rem; }
15
- .pk-toggle.lg { height: 2.5rem; min-width: 2.5rem; padding-inline: .625rem; }
16
- .pk-toggle svg { pointer-events: none; flex-shrink: 0; width: 1rem; height: 1rem; }
14
+ .pk-toggle[data-state="on"], .pk-toggle[aria-pressed="true"] { background: var(--pk-surface-2); color: var(--pk-text); }
15
+ .pk-toggle:focus-visible {
16
+ border-color: var(--pk-ring);
17
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
18
+ }
19
+ .pk-toggle.outline { border-color: var(--pk-input); }
20
+ .pk-toggle.sm {
21
+ height: 1.75rem; min-width: 1.75rem; font-size: .8rem;
22
+ border-radius: min(calc(var(--pk-radius) - 2px), 12px);
23
+ }
24
+ .pk-toggle.lg { height: 2.25rem; min-width: 2.25rem; }
25
+ .pk-toggle svg { pointer-events: none; flex-shrink: 0; }
26
+ .pk-toggle svg:not([class*="size-"]) { width: 1rem; height: 1rem; }
27
+ .pk-toggle.sm svg:not([class*="size-"]) { width: .875rem; height: .875rem; }
28
+ .pk-toggle:has(> [data-icon="inline-start"]) { padding-left: .5rem; }
29
+ .pk-toggle:has(> [data-icon="inline-end"]) { padding-right: .5rem; }
30
+ .pk-toggle.sm:has(> [data-icon="inline-start"]) { padding-left: .375rem; }
31
+ .pk-toggle.sm:has(> [data-icon="inline-end"]) { padding-right: .375rem; }
@@ -1,10 +1,11 @@
1
1
  /* Co-located with toggle_group.rb. Theme tokens from the global stylesheet. */
2
- .pk-toggle-group { display: inline-flex; width: fit-content; align-items: center; border-radius: calc(var(--pk-radius) - 2px); }
2
+ .pk-toggle-group { display: inline-flex; width: fit-content; align-items: center; border-radius: var(--pk-radius); }
3
3
  .pk-toggle-group.vertical { flex-direction: column; align-items: stretch; }
4
4
  .pk-toggle-group.spaced { gap: .25rem; }
5
- /* Joined (default, unspaced): square the inner corners. */
5
+ /* Joined (default, unspaced): square the inner corners, tighten padding. */
6
+ .pk-toggle-group:not(.spaced) .pk-toggle-group-item { padding-inline: .5rem; }
6
7
  .pk-toggle-group:not(.spaced) .pk-toggle-group-item { border-radius: 0; }
7
- .pk-toggle-group:not(.spaced):not(.vertical) .pk-toggle-group-item:first-of-type { border-top-left-radius: .375rem; border-bottom-left-radius: .375rem; }
8
- .pk-toggle-group:not(.spaced):not(.vertical) .pk-toggle-group-item:last-of-type { border-top-right-radius: .375rem; border-bottom-right-radius: .375rem; }
9
- .pk-toggle-group.vertical:not(.spaced) .pk-toggle-group-item:first-of-type { border-top-left-radius: .375rem; border-top-right-radius: .375rem; }
10
- .pk-toggle-group.vertical:not(.spaced) .pk-toggle-group-item:last-of-type { border-bottom-left-radius: .375rem; border-bottom-right-radius: .375rem; }
8
+ .pk-toggle-group:not(.spaced):not(.vertical) .pk-toggle-group-item:first-of-type { border-top-left-radius: var(--pk-radius); border-bottom-left-radius: var(--pk-radius); }
9
+ .pk-toggle-group:not(.spaced):not(.vertical) .pk-toggle-group-item:last-of-type { border-top-right-radius: var(--pk-radius); border-bottom-right-radius: var(--pk-radius); }
10
+ .pk-toggle-group.vertical:not(.spaced) .pk-toggle-group-item:first-of-type { border-top-left-radius: var(--pk-radius); border-top-right-radius: var(--pk-radius); }
11
+ .pk-toggle-group.vertical:not(.spaced) .pk-toggle-group-item:last-of-type { border-bottom-left-radius: var(--pk-radius); border-bottom-right-radius: var(--pk-radius); }
@@ -1,7 +1,8 @@
1
1
  /* Co-located with tooltip.rb — UI::Tooltip (+ Trigger / Content). CSS-only:
2
- the bubble is absolutely positioned above the trigger and revealed on the
3
- wrapper's :hover / :focus-within (no JS, no @floating-ui). Theme tokens come
4
- from the global stylesheet. */
2
+ the bubble is absolutely positioned and revealed on the wrapper's :hover /
3
+ :focus-within (no JS, no @floating-ui). Matched to nova: INVERTED surface
4
+ (foreground bg, background text) with a rotated-square arrow, rounded-md,
5
+ px-3 py-1.5 text-xs, gap for inline kbd chips (kbd.css tints them). */
5
6
  .pk-tooltip { position: relative; display: inline-flex; }
6
7
  .pk-tooltip-trigger { display: inline-flex; }
7
8
  .pk-tooltip-content {
@@ -10,19 +11,45 @@
10
11
  left: 50%;
11
12
  transform: translateX(-50%);
12
13
  z-index: 50;
14
+ display: inline-flex;
15
+ align-items: center;
16
+ gap: .375rem;
13
17
  width: max-content;
14
- max-width: min(16rem, calc(100vw - 2rem));
15
- padding: .35rem .6rem;
18
+ max-width: min(20rem, calc(100vw - 2rem));
19
+ padding: .375rem .75rem;
16
20
  border-radius: calc(var(--pk-radius) - 2px);
17
- border: 1px solid var(--pk-border);
18
- background: var(--pk-surface-2);
19
- color: var(--pk-text);
20
- font-size: .8rem;
21
- box-shadow: 0 4px 16px rgba(0, 0, 0, .4);
21
+ background: var(--pk-text);
22
+ color: var(--pk-bg);
23
+ font-size: .75rem;
22
24
  opacity: 0;
23
25
  visibility: hidden;
24
26
  pointer-events: none;
25
27
  transition: opacity .12s ease;
26
28
  }
29
+ .pk-tooltip-content:has(kbd) { padding-right: .375rem; }
30
+ .pk-tooltip-content::after {
31
+ content: "";
32
+ position: absolute;
33
+ z-index: -1;
34
+ width: .625rem;
35
+ height: .625rem;
36
+ border-radius: 2px;
37
+ background: inherit;
38
+ top: 100%;
39
+ left: 50%;
40
+ transform: translate(-50%, calc(-50% - 2px)) rotate(45deg);
41
+ }
42
+ .pk-tooltip-content.bottom { bottom: auto; top: calc(100% + 6px); }
43
+ .pk-tooltip-content.bottom::after { top: 0; transform: translate(-50%, calc(-50% + 2px)) rotate(45deg); }
44
+ .pk-tooltip-content.left {
45
+ bottom: auto; left: auto; right: calc(100% + 6px); top: 50%;
46
+ transform: translateY(-50%);
47
+ }
48
+ .pk-tooltip-content.left::after { top: 50%; left: 100%; transform: translate(calc(-50% - 2px), -50%) rotate(45deg); }
49
+ .pk-tooltip-content.right {
50
+ bottom: auto; right: auto; left: calc(100% + 6px); top: 50%;
51
+ transform: translateY(-50%);
52
+ }
53
+ .pk-tooltip-content.right::after { top: 50%; left: 0; transform: translate(calc(-50% + 2px), -50%) rotate(45deg); }
27
54
  .pk-tooltip:hover .pk-tooltip-content,
28
55
  .pk-tooltip:focus-within .pk-tooltip-content { opacity: 1; visibility: visible; }
@@ -1,12 +1,17 @@
1
1
  module PhlexKit
2
- # The tooltip bubble, revealed on hover/focus of its PhlexKit::Tooltip. See tooltip.rb.
2
+ # The tooltip bubble. `side:` picks the edge it opens from (:top default,
3
+ # :bottom/:left/:right — shadcn's side prop). See tooltip.rb.
3
4
  class TooltipContent < BaseComponent
4
- def initialize(**attrs)
5
+ SIDES = { top: nil, bottom: "bottom", left: "left", right: "right" }.freeze
6
+
7
+ def initialize(side: :top, **attrs)
8
+ @side = side.to_sym
5
9
  @attrs = attrs
6
10
  end
7
11
 
8
- def view_template(&block)
9
- div(**mix({ class: "pk-tooltip-content", role: "tooltip" }, @attrs), &block)
12
+ def view_template(&)
13
+ classes = [ "pk-tooltip-content", SIDES.fetch(@side) ].compact.join(" ")
14
+ div(**mix({ class: classes, role: "tooltip" }, @attrs), &)
10
15
  end
11
16
  end
12
17
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PhlexKit
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end