phlex_kit 0.3.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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -2
  3. data/app/assets/stylesheets/phlex_kit/_tokens.css +16 -0
  4. data/app/assets/stylesheets/phlex_kit/phlex_kit.css +1 -0
  5. data/app/components/phlex_kit/alert/alert.css +60 -22
  6. data/app/components/phlex_kit/alert/alert.rb +11 -7
  7. data/app/components/phlex_kit/alert/alert_action.rb +14 -0
  8. data/app/components/phlex_kit/alert/alert_title.rb +3 -2
  9. data/app/components/phlex_kit/alert_dialog/alert_dialog.css +5 -2
  10. data/app/components/phlex_kit/avatar/avatar.css +80 -9
  11. data/app/components/phlex_kit/avatar/avatar.rb +6 -4
  12. data/app/components/phlex_kit/avatar/avatar_badge.rb +15 -0
  13. data/app/components/phlex_kit/avatar/avatar_group_count.rb +14 -0
  14. data/app/components/phlex_kit/badge/badge.css +61 -39
  15. data/app/components/phlex_kit/badge/badge.rb +24 -7
  16. data/app/components/phlex_kit/breadcrumb/breadcrumb.css +2 -2
  17. data/app/components/phlex_kit/bubble/bubble.css +49 -8
  18. data/app/components/phlex_kit/button/button.css +71 -26
  19. data/app/components/phlex_kit/button/button.rb +20 -13
  20. data/app/components/phlex_kit/button_group/button_group.css +59 -8
  21. data/app/components/phlex_kit/button_group/button_group.rb +11 -5
  22. data/app/components/phlex_kit/button_group/button_group_separator.rb +14 -0
  23. data/app/components/phlex_kit/button_group/button_group_text.rb +14 -0
  24. data/app/components/phlex_kit/calendar/calendar.css +66 -15
  25. data/app/components/phlex_kit/calendar/calendar.rb +87 -19
  26. data/app/components/phlex_kit/calendar/calendar_controller.js +226 -75
  27. data/app/components/phlex_kit/calendar/calendar_days.rb +7 -6
  28. data/app/components/phlex_kit/calendar/calendar_weekdays.rb +3 -1
  29. data/app/components/phlex_kit/checkbox/checkbox.css +52 -7
  30. data/app/components/phlex_kit/collapsible/collapsible.css +10 -1
  31. data/app/components/phlex_kit/collapsible/collapsible_controller.js +9 -2
  32. data/app/components/phlex_kit/combobox/combobox.css +37 -3
  33. data/app/components/phlex_kit/combobox/combobox.rb +3 -1
  34. data/app/components/phlex_kit/combobox/combobox_controller.js +9 -1
  35. data/app/components/phlex_kit/combobox/combobox_input_trigger.rb +7 -2
  36. data/app/components/phlex_kit/command/command.css +58 -22
  37. data/app/components/phlex_kit/command/command_controller.js +3 -1
  38. data/app/components/phlex_kit/command/command_input.rb +29 -25
  39. data/app/components/phlex_kit/command/command_separator.rb +18 -0
  40. data/app/components/phlex_kit/command/command_shortcut.rb +13 -0
  41. data/app/components/phlex_kit/context_menu/context_menu.css +78 -6
  42. data/app/components/phlex_kit/context_menu/context_menu_checkbox_item.rb +27 -0
  43. data/app/components/phlex_kit/context_menu/context_menu_group.rb +10 -0
  44. data/app/components/phlex_kit/context_menu/context_menu_item.rb +13 -3
  45. data/app/components/phlex_kit/context_menu/context_menu_radio_group.rb +10 -0
  46. data/app/components/phlex_kit/context_menu/context_menu_radio_item.rb +30 -0
  47. data/app/components/phlex_kit/context_menu/context_menu_shortcut.rb +11 -0
  48. data/app/components/phlex_kit/context_menu/context_menu_sub.rb +11 -0
  49. data/app/components/phlex_kit/context_menu/context_menu_sub_content.rb +12 -0
  50. data/app/components/phlex_kit/context_menu/context_menu_sub_trigger.rb +17 -0
  51. data/app/components/phlex_kit/dialog/dialog.css +34 -16
  52. data/app/components/phlex_kit/dialog/dialog_close.rb +13 -0
  53. data/app/components/phlex_kit/dialog/dialog_content.rb +13 -5
  54. data/app/components/phlex_kit/drawer/drawer.css +56 -17
  55. data/app/components/phlex_kit/drawer/drawer_content.rb +8 -4
  56. data/app/components/phlex_kit/empty/empty.css +13 -8
  57. data/app/components/phlex_kit/field/field.css +152 -0
  58. data/app/components/phlex_kit/field/field.rb +35 -0
  59. data/app/components/phlex_kit/field/field_content.rb +11 -0
  60. data/app/components/phlex_kit/field/field_description.rb +10 -0
  61. data/app/components/phlex_kit/field/field_error.rb +29 -0
  62. data/app/components/phlex_kit/field/field_group.rb +12 -0
  63. data/app/components/phlex_kit/field/field_label.rb +12 -0
  64. data/app/components/phlex_kit/field/field_legend.rb +17 -0
  65. data/app/components/phlex_kit/field/field_separator.rb +15 -0
  66. data/app/components/phlex_kit/field/field_set.rb +10 -0
  67. data/app/components/phlex_kit/field/field_title.rb +11 -0
  68. data/app/components/phlex_kit/hover_card/hover_card.css +12 -4
  69. data/app/components/phlex_kit/hover_card/hover_card_content.rb +11 -2
  70. data/app/components/phlex_kit/input/input.css +45 -11
  71. data/app/components/phlex_kit/input_group/input_group.css +88 -12
  72. data/app/components/phlex_kit/input_group/input_group_addon.rb +11 -4
  73. data/app/components/phlex_kit/input_group/input_group_button.rb +18 -0
  74. data/app/components/phlex_kit/input_otp/input_otp.css +29 -5
  75. data/app/components/phlex_kit/item/item.css +56 -8
  76. data/app/components/phlex_kit/item/item.rb +13 -3
  77. data/app/components/phlex_kit/item/item_footer.rb +8 -0
  78. data/app/components/phlex_kit/item/item_header.rb +8 -0
  79. data/app/components/phlex_kit/item/item_media.rb +13 -3
  80. data/app/components/phlex_kit/item/item_separator.rb +10 -0
  81. data/app/components/phlex_kit/kbd/kbd.css +10 -0
  82. data/app/components/phlex_kit/marker/marker.css +29 -9
  83. data/app/components/phlex_kit/marker/marker.rb +13 -3
  84. data/app/components/phlex_kit/menubar/menubar.css +88 -17
  85. data/app/components/phlex_kit/menubar/menubar_checkbox_item.rb +26 -0
  86. data/app/components/phlex_kit/menubar/menubar_group.rb +10 -0
  87. data/app/components/phlex_kit/menubar/menubar_item.rb +9 -4
  88. data/app/components/phlex_kit/menubar/menubar_label.rb +10 -0
  89. data/app/components/phlex_kit/menubar/menubar_radio_group.rb +10 -0
  90. data/app/components/phlex_kit/menubar/menubar_radio_item.rb +29 -0
  91. data/app/components/phlex_kit/menubar/menubar_shortcut.rb +10 -0
  92. data/app/components/phlex_kit/menubar/menubar_sub.rb +10 -0
  93. data/app/components/phlex_kit/menubar/menubar_sub_content.rb +11 -0
  94. data/app/components/phlex_kit/menubar/menubar_sub_trigger.rb +17 -0
  95. data/app/components/phlex_kit/message/message.css +8 -1
  96. data/app/components/phlex_kit/message_scroller/message_scroller.css +18 -0
  97. data/app/components/phlex_kit/native_select/native_select.css +25 -5
  98. data/app/components/phlex_kit/navigation_menu/navigation_menu.css +9 -6
  99. data/app/components/phlex_kit/pagination/pagination.css +10 -3
  100. data/app/components/phlex_kit/pagination/pagination_link.rb +21 -0
  101. data/app/components/phlex_kit/pagination/pagination_next.rb +20 -0
  102. data/app/components/phlex_kit/pagination/pagination_previous.rb +21 -0
  103. data/app/components/phlex_kit/popover/popover.css +15 -4
  104. data/app/components/phlex_kit/popover/popover_content.rb +11 -2
  105. data/app/components/phlex_kit/popover/popover_description.rb +10 -0
  106. data/app/components/phlex_kit/popover/popover_header.rb +10 -0
  107. data/app/components/phlex_kit/popover/popover_title.rb +10 -0
  108. data/app/components/phlex_kit/progress/progress.css +5 -5
  109. data/app/components/phlex_kit/radio_button/radio_button.css +46 -2
  110. data/app/components/phlex_kit/radio_group/radio_group.css +2 -2
  111. data/app/components/phlex_kit/resizable/resizable.css +17 -0
  112. data/app/components/phlex_kit/resizable/resizable_handle.rb +7 -3
  113. data/app/components/phlex_kit/select/select.css +46 -10
  114. data/app/components/phlex_kit/select/select_separator.rb +10 -0
  115. data/app/components/phlex_kit/select/select_trigger.rb +5 -2
  116. data/app/components/phlex_kit/sheet/sheet.css +18 -9
  117. data/app/components/phlex_kit/sheet/sheet_close.rb +10 -0
  118. data/app/components/phlex_kit/sheet/sheet_content.rb +8 -4
  119. data/app/components/phlex_kit/sidebar/sidebar.css +126 -2
  120. data/app/components/phlex_kit/sidebar/sidebar.rb +5 -5
  121. data/app/components/phlex_kit/sidebar/sidebar_controller.js +39 -0
  122. data/app/components/phlex_kit/sidebar/sidebar_menu_button.rb +8 -3
  123. data/app/components/phlex_kit/sidebar/sidebar_rail.rb +21 -0
  124. data/app/components/phlex_kit/sidebar/sidebar_trigger.rb +23 -0
  125. data/app/components/phlex_kit/sidebar/sidebar_wrapper.rb +54 -2
  126. data/app/components/phlex_kit/skeleton/skeleton.css +1 -1
  127. data/app/components/phlex_kit/slider/slider.css +33 -12
  128. data/app/components/phlex_kit/switch/switch.css +50 -13
  129. data/app/components/phlex_kit/switch/switch.rb +5 -2
  130. data/app/components/phlex_kit/table/table.css +8 -4
  131. data/app/components/phlex_kit/tabs/tabs.css +72 -8
  132. data/app/components/phlex_kit/tabs/tabs.rb +7 -2
  133. data/app/components/phlex_kit/tabs/tabs_list.rb +11 -2
  134. data/app/components/phlex_kit/textarea/textarea.css +33 -13
  135. data/app/components/phlex_kit/toast/toast.css +1 -1
  136. data/app/components/phlex_kit/toggle/toggle.css +25 -10
  137. data/app/components/phlex_kit/toggle_group/toggle_group.css +7 -6
  138. data/app/components/phlex_kit/tooltip/tooltip.css +37 -10
  139. data/app/components/phlex_kit/tooltip/tooltip_content.rb +9 -4
  140. data/app/javascript/phlex_kit/controllers/index.js +2 -0
  141. data/lib/phlex_kit/version.rb +1 -1
  142. metadata +52 -1
@@ -0,0 +1,21 @@
1
+ module PhlexKit
2
+ # Invisible grab strip along the sidebar's trailing edge — clicking it
3
+ # toggles the collapse, like shadcn/ui's SidebarRail. Place it as the last
4
+ # child of a Sidebar inside a collapsible wrapper. See sidebar.rb.
5
+ class SidebarRail < BaseComponent
6
+ def initialize(**attrs)
7
+ @attrs = attrs
8
+ end
9
+
10
+ def view_template
11
+ button(**mix({
12
+ type: :button,
13
+ class: "pk-sidebar-rail",
14
+ aria_label: "Toggle sidebar",
15
+ tabindex: "-1",
16
+ title: "Toggle sidebar",
17
+ data: { action: "click->phlex-kit--sidebar#toggle" }
18
+ }, @attrs))
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ module PhlexKit
2
+ # The hamburger that toggles an offcanvas sidebar, ported from shadcn/ui's
3
+ # SidebarTrigger. Place it anywhere inside a
4
+ # SidebarWrapper(collapsible: :offcanvas) — typically at the top of the
5
+ # SidebarInset. Renders the :menu icon unless a block supplies its own
6
+ # content. Hidden by CSS unless the wrapper is offcanvas. See sidebar.rb.
7
+ class SidebarTrigger < BaseComponent
8
+ def initialize(**attrs)
9
+ @attrs = attrs
10
+ end
11
+
12
+ def view_template(&block)
13
+ button(**mix({
14
+ type: :button,
15
+ class: "pk-sidebar-trigger",
16
+ aria_label: "Toggle sidebar",
17
+ data: { action: "click->phlex-kit--sidebar#toggle" }
18
+ }, @attrs)) do
19
+ block ? yield : render(Icon.new(:menu, size: nil))
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,13 +1,65 @@
1
1
  module PhlexKit
2
2
  # The page-level flex row that holds a PhlexKit::Sidebar + a PhlexKit::SidebarInset.
3
+ #
4
+ # `collapsible:` (shadcn/ui's Sidebar `collapsible` prop, held here because the
5
+ # wrapper is the Stimulus scope shared by rail, trigger and scrim):
6
+ # :none — static rail (default).
7
+ # :offcanvas — the rail hides/reveals via a PhlexKit::SidebarTrigger. On
8
+ # desktop it slides out of the layout; below 768px it overlays
9
+ # as a fixed drawer behind a click-to-close scrim, closed by
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.
3
23
  # See sidebar.rb.
4
24
  class SidebarWrapper < BaseComponent
5
- def initialize(**attrs)
25
+ COLLAPSIBLE = { none: nil, offcanvas: "collapsible-offcanvas", icon: "collapsible-icon" }.freeze
26
+
27
+ def initialize(collapsible: :none, default_collapsed: false, **attrs)
28
+ @collapsible = collapsible.to_sym
29
+ @default_collapsed = default_collapsed
6
30
  @attrs = attrs
7
31
  end
8
32
 
9
33
  def view_template(&block)
10
- div(**mix({ class: "pk-sidebar-wrapper" }, @attrs), &block)
34
+ unless collapsible?
35
+ return div(**mix({ class: classes }, @attrs), &block)
36
+ end
37
+
38
+ base = {
39
+ class: classes,
40
+ data: {
41
+ controller: "phlex-kit--sidebar",
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 " \
45
+ "turbo:before-cache@window->phlex-kit--sidebar#closeMobile"
46
+ }
47
+ }
48
+ base["data-collapsed"] = "" if @default_collapsed
49
+
50
+ div(**mix(base, @attrs)) do
51
+ yield if block
52
+ button(type: :button, class: "pk-sidebar-scrim", aria_label: "Close sidebar",
53
+ tabindex: "-1", data: { action: "click->phlex-kit--sidebar#closeMobile" })
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def collapsible? = @collapsible != :none
60
+
61
+ def classes
62
+ [ "pk-sidebar-wrapper", COLLAPSIBLE.fetch(@collapsible) ].compact.join(" ")
11
63
  end
12
64
  end
13
65
  end
@@ -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; }