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,24 +1,64 @@
1
- /* Co-located with bubble.rb. Theme tokens from the global stylesheet. The variant
2
- colours the child [data-slot=bubble-content]. */
1
+ /* Co-located with bubble.rb PhlexKit::Bubble (+ Content / Reactions / Group).
2
+ Matched to shadcn/ui's current bubble (nova). The variant colours the child
3
+ [data-slot=bubble-content]; hover fills apply only when the content itself
4
+ is a <button>/<a> (BubbleContent as: :button / :a). tinted derives its fill
5
+ from the brand color via relative oklch — lightness pinned per theme.
6
+ ghost is frameless and full-width (assistant prose). */
3
7
  .pk-bubble {
4
8
  position: relative; display: flex; width: fit-content; min-width: 0;
5
9
  flex-direction: column; gap: .25rem; max-width: 80%;
6
10
  }
7
11
  .pk-bubble[data-align="end"] { align-self: flex-end; }
12
+ .pk-bubble[data-variant="ghost"] { max-width: 100%; border: none; }
13
+ /* Inside a Message row aligned to the end, bubbles follow. */
14
+ .pk-message[data-align="end"] .pk-bubble { align-self: flex-end; }
8
15
  .pk-bubble-content {
9
16
  width: fit-content; max-width: 100%; min-width: 0; overflow: hidden;
10
- overflow-wrap: break-word; border: 1px solid transparent; border-radius: 1.5rem;
11
- padding: .625rem .75rem; font-size: .875rem; line-height: 1.625;
17
+ overflow-wrap: break-word; border: 1px solid transparent; border-radius: .75rem;
18
+ padding: .5rem .75rem; font-size: .875rem; line-height: 1.625;
12
19
  }
13
20
  .pk-bubble[data-align="end"] > .pk-bubble-content { align-self: flex-end; }
14
- /* variants */
21
+ button.pk-bubble-content { text-align: left; font: inherit; }
22
+ a.pk-bubble-content { color: inherit; text-decoration: none; }
23
+ button.pk-bubble-content, a.pk-bubble-content {
24
+ cursor: pointer; outline: none;
25
+ transition: color .15s ease, background-color .15s ease;
26
+ }
27
+ :is(button, a).pk-bubble-content:focus-visible {
28
+ border-color: var(--pk-ring);
29
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
30
+ }
31
+ /* variants (dark-first; light overrides below) */
15
32
  .pk-bubble[data-variant="default"] > .pk-bubble-content { background: var(--pk-brand); color: var(--pk-brand-ink); }
33
+ .pk-bubble[data-variant="default"] > :is(button, a).pk-bubble-content:hover { background: color-mix(in oklab, var(--pk-brand) 80%, transparent); }
16
34
  .pk-bubble[data-variant="secondary"] > .pk-bubble-content { background: var(--pk-surface-2); color: var(--pk-text); }
17
- .pk-bubble[data-variant="muted"] > .pk-bubble-content { background: var(--pk-surface-2); color: var(--pk-text); }
18
- .pk-bubble[data-variant="tinted"] > .pk-bubble-content { background: color-mix(in oklab, var(--pk-brand) 18%, transparent); color: var(--pk-text); }
35
+ .pk-bubble[data-variant="secondary"] > :is(button, a).pk-bubble-content:hover { background: color-mix(in oklch, var(--pk-surface-2), var(--pk-text) 5%); }
36
+ .pk-bubble[data-variant="muted"] > .pk-bubble-content { background: var(--pk-surface-2); }
37
+ .pk-bubble[data-variant="muted"] > :is(button, a).pk-bubble-content:hover { background: color-mix(in oklch, var(--pk-surface-2), var(--pk-text) 5%); }
38
+ .pk-bubble[data-variant="tinted"] > .pk-bubble-content { background: oklch(from var(--pk-brand) 0.3 calc(c * 0.4) h); color: var(--pk-text); }
39
+ .pk-bubble[data-variant="tinted"] > :is(button, a).pk-bubble-content:hover { background: oklch(from var(--pk-brand) 0.35 calc(c * 0.5) h); }
19
40
  .pk-bubble[data-variant="outline"] > .pk-bubble-content { background: var(--pk-bg); border-color: var(--pk-border); color: var(--pk-text); }
41
+ .pk-bubble[data-variant="outline"] > :is(button, a).pk-bubble-content:hover { background: color-mix(in oklab, var(--pk-input) 30%, transparent); color: var(--pk-text); }
20
42
  .pk-bubble[data-variant="ghost"] > .pk-bubble-content { background: transparent; border-radius: 0; padding: 0; }
21
- .pk-bubble[data-variant="destructive"] > .pk-bubble-content { background: color-mix(in oklab, var(--pk-red) 12%, transparent); color: var(--pk-red); }
43
+ .pk-bubble[data-variant="ghost"] > :is(button, a).pk-bubble-content:hover { background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent); color: var(--pk-text); }
44
+ .pk-bubble[data-variant="destructive"] > .pk-bubble-content { background: color-mix(in oklab, var(--pk-red) 20%, transparent); color: var(--pk-red); }
45
+ .pk-bubble[data-variant="destructive"] > :is(button, a).pk-bubble-content:hover { background: color-mix(in oklab, var(--pk-red) 30%, transparent); }
46
+
47
+ :root[data-theme="light"] .pk-bubble[data-variant="tinted"] > .pk-bubble-content { background: oklch(from var(--pk-brand) 0.93 calc(c * 0.4) h); }
48
+ :root[data-theme="light"] .pk-bubble[data-variant="tinted"] > :is(button, a).pk-bubble-content:hover { background: oklch(from var(--pk-brand) 0.88 calc(c * 0.5) h); }
49
+ :root[data-theme="light"] .pk-bubble[data-variant="outline"] > :is(button, a).pk-bubble-content:hover { background: var(--pk-surface-2); }
50
+ :root[data-theme="light"] .pk-bubble[data-variant="ghost"] > :is(button, a).pk-bubble-content:hover { background: var(--pk-surface-2); }
51
+ :root[data-theme="light"] .pk-bubble[data-variant="destructive"] > .pk-bubble-content { background: color-mix(in oklab, var(--pk-red) 10%, transparent); }
52
+ :root[data-theme="light"] .pk-bubble[data-variant="destructive"] > :is(button, a).pk-bubble-content:hover { background: color-mix(in oklab, var(--pk-red) 20%, transparent); }
53
+ @media (prefers-color-scheme: light) {
54
+ :root[data-theme="system"] .pk-bubble[data-variant="tinted"] > .pk-bubble-content { background: oklch(from var(--pk-brand) 0.93 calc(c * 0.4) h); }
55
+ :root[data-theme="system"] .pk-bubble[data-variant="tinted"] > :is(button, a).pk-bubble-content:hover { background: oklch(from var(--pk-brand) 0.88 calc(c * 0.5) h); }
56
+ :root[data-theme="system"] .pk-bubble[data-variant="outline"] > :is(button, a).pk-bubble-content:hover { background: var(--pk-surface-2); }
57
+ :root[data-theme="system"] .pk-bubble[data-variant="ghost"] > :is(button, a).pk-bubble-content:hover { background: var(--pk-surface-2); }
58
+ :root[data-theme="system"] .pk-bubble[data-variant="destructive"] > .pk-bubble-content { background: color-mix(in oklab, var(--pk-red) 10%, transparent); }
59
+ :root[data-theme="system"] .pk-bubble[data-variant="destructive"] > :is(button, a).pk-bubble-content:hover { background: color-mix(in oklab, var(--pk-red) 20%, transparent); }
60
+ }
61
+
22
62
  .pk-bubble-group { display: flex; min-width: 0; flex-direction: column; gap: .5rem; }
23
63
  .pk-bubble-reactions {
24
64
  position: absolute; z-index: 10; display: flex; width: fit-content;
@@ -26,6 +66,7 @@
26
66
  box-shadow: 0 0 0 3px var(--pk-surface); background: var(--pk-surface-2);
27
67
  flex: none; gap: .25rem; padding: .125rem .375rem; font-size: .875rem;
28
68
  }
69
+ .pk-bubble-reactions:has(button) { padding: 0; }
29
70
  .pk-bubble-reactions[data-side="bottom"] { bottom: 0; transform: translateY(75%); }
30
71
  .pk-bubble-reactions[data-side="top"] { top: 0; transform: translateY(-75%); }
31
72
  .pk-bubble-reactions[data-align="end"] { right: .75rem; }
@@ -1,66 +1,96 @@
1
- /* Co-located with button.rb — UI::Button. ruby_ui's Tailwind replaced with
2
- vanilla CSS on the palette tokens, at full variant/size parity. Geometry from
3
- ruby_ui (inline-flex centred, rounded-md, h-9 default, font-medium, focus ring,
4
- filled variants). Theme tokens come from the global stylesheet. */
1
+ /* Co-located with button.rb — PhlexKit::Button. Matched to shadcn/ui's current
2
+ button (nova): h-8 default, tinted destructive (no more solid red), theme-
3
+ forked outline (translucent input fill in dark, plain bordered in light),
4
+ active nudge, per-size radius caps, data-icon inset tightening, svg-4
5
+ glyphs. Hover fills; aria-expanded holds the open state for menu triggers.
6
+ xl is a kit extra. Dark-first; light overrides at the bottom. */
5
7
  .pk-button {
6
8
  display: inline-flex;
7
9
  align-items: center;
8
10
  justify-content: center;
11
+ flex-shrink: 0;
9
12
  white-space: nowrap;
13
+ user-select: none;
10
14
  gap: .375rem;
11
15
  height: 2rem;
12
16
  padding: .25rem .625rem;
13
17
  border: 1px solid transparent;
14
18
  border-radius: var(--pk-radius);
19
+ background-clip: padding-box;
15
20
  font: inherit;
16
21
  font-size: .875rem;
17
22
  font-weight: 500;
18
23
  cursor: pointer;
19
- transition: background-color .15s ease, color .15s ease, border-color .15s ease, box-shadow .15s ease;
24
+ text-decoration: none;
25
+ transition: background-color .15s ease, color .15s ease, border-color .15s ease, box-shadow .15s ease, transform .15s ease;
20
26
  }
21
27
  .pk-button:focus-visible {
22
28
  outline: none;
29
+ border-color: var(--pk-ring);
23
30
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
24
31
  }
32
+ .pk-button:active:not([aria-haspopup]) { transform: translateY(1px); }
25
33
  .pk-button:disabled, .pk-button[disabled], .pk-button[aria-disabled="true"] {
26
34
  pointer-events: none;
27
35
  opacity: .5;
28
36
  }
37
+ /* Glyphs default to size-4; explicit width attrs (Icon size:) win. */
38
+ .pk-button svg { flex-shrink: 0; pointer-events: none; }
39
+ .pk-button svg:not([width]) { width: 1rem; height: 1rem; }
40
+ .pk-button:has(> [data-icon="inline-start"]) { padding-left: .5rem; }
41
+ .pk-button:has(> [data-icon="inline-end"]) { padding-right: .5rem; }
29
42
 
30
43
  /* Sizes (md is the base above). */
31
- .pk-button.sm { height: 1.75rem; padding: .25rem .5rem; font-size: .75rem; }
32
- .pk-button.lg { height: 2.5rem; padding: .375rem 1rem; font-size: .875rem; }
44
+ .pk-button.xs {
45
+ height: 1.5rem; gap: .25rem; padding: .125rem .5rem; font-size: .75rem;
46
+ border-radius: min(calc(var(--pk-radius) - 2px), 10px);
47
+ }
48
+ .pk-button.xs svg:not([width]) { width: .75rem; height: .75rem; }
49
+ .pk-button.xs:has(> [data-icon="inline-start"]) { padding-left: .375rem; }
50
+ .pk-button.xs:has(> [data-icon="inline-end"]) { padding-right: .375rem; }
51
+ .pk-button.sm {
52
+ height: 1.75rem; gap: .25rem; padding: .25rem .625rem; font-size: .8rem;
53
+ border-radius: min(calc(var(--pk-radius) - 2px), 12px);
54
+ }
55
+ .pk-button.sm svg:not([width]) { width: .875rem; height: .875rem; }
56
+ .pk-button.sm:has(> [data-icon="inline-start"]) { padding-left: .375rem; }
57
+ .pk-button.sm:has(> [data-icon="inline-end"]) { padding-right: .375rem; }
58
+ .pk-button.lg { height: 2.25rem; padding: .375rem .625rem; }
33
59
  .pk-button.xl { height: 3rem; padding: .625rem 1.5rem; font-size: 1rem; }
34
60
 
35
- /* Icon-only square buttons. */
61
+ /* Icon-only square buttons (their icon / icon-xs / icon-sm / icon-lg). */
36
62
  .pk-button.icon { width: 2rem; padding: 0; }
63
+ .pk-button.icon.xs { width: 1.5rem; }
37
64
  .pk-button.icon.sm { width: 1.75rem; }
38
- .pk-button.icon.lg { width: 2.5rem; }
65
+ .pk-button.icon.lg { width: 2.25rem; }
39
66
  .pk-button.icon.xl { width: 3rem; }
40
67
 
41
- /* Variants. */
42
- .pk-button.primary {
43
- background: var(--pk-brand);
44
- color: var(--pk-brand-ink);
45
- border-color: var(--pk-brand);
46
- }
47
- .pk-button.primary:hover { background: color-mix(in oklab, var(--pk-brand) 90%, transparent); border-color: transparent; }
68
+ /* Variants (dark-first; light overrides below). */
69
+ .pk-button.primary { background: var(--pk-brand); color: var(--pk-brand-ink); }
70
+ .pk-button.primary:hover { background: color-mix(in oklab, var(--pk-brand) 80%, transparent); }
48
71
 
49
72
  .pk-button.secondary { background: var(--pk-surface-2); color: var(--pk-text); }
50
- .pk-button.secondary:hover { background: color-mix(in oklab, var(--pk-surface-2) 80%, transparent); }
73
+ .pk-button.secondary:hover { background: color-mix(in oklch, var(--pk-surface-2), var(--pk-text) 5%); }
74
+ .pk-button.secondary[aria-expanded="true"] { background: var(--pk-surface-2); color: var(--pk-text); }
51
75
 
52
- .pk-button.destructive {
53
- background: var(--pk-red);
54
- color: var(--pk-brand-ink);
55
- border-color: var(--pk-red);
76
+ .pk-button.destructive { background: color-mix(in oklab, var(--pk-red) 20%, transparent); color: var(--pk-red); }
77
+ .pk-button.destructive:hover { background: color-mix(in oklab, var(--pk-red) 30%, transparent); }
78
+ .pk-button.destructive:focus-visible {
79
+ border-color: color-mix(in oklab, var(--pk-red) 40%, transparent);
80
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 40%, transparent);
56
81
  }
57
- .pk-button.destructive:hover { background: color-mix(in oklab, var(--pk-red) 88%, black); }
58
82
 
59
- .pk-button.outline { background: color-mix(in oklab, var(--pk-input) 30%, transparent); color: var(--pk-text); border-color: var(--pk-input); }
60
- .pk-button.outline:hover { background: var(--pk-accent); }
83
+ .pk-button.outline {
84
+ background: color-mix(in oklab, var(--pk-input) 30%, transparent);
85
+ color: var(--pk-text);
86
+ border-color: var(--pk-input);
87
+ }
88
+ .pk-button.outline:hover { background: color-mix(in oklab, var(--pk-input) 50%, transparent); }
89
+ .pk-button.outline[aria-expanded="true"] { background: var(--pk-surface-2); color: var(--pk-text); }
61
90
 
62
91
  .pk-button.ghost { background: transparent; border-color: transparent; color: var(--pk-text); }
63
- .pk-button.ghost:hover { background: var(--pk-accent); }
92
+ .pk-button.ghost:hover { background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent); color: var(--pk-text); }
93
+ .pk-button.ghost[aria-expanded="true"] { background: var(--pk-surface-2); color: var(--pk-text); }
64
94
 
65
95
  .pk-button.link {
66
96
  height: auto;
@@ -69,4 +99,19 @@
69
99
  border-color: transparent;
70
100
  color: var(--pk-brand);
71
101
  }
72
- .pk-button.link:hover { text-decoration: underline; }
102
+ .pk-button.link:hover { text-decoration: underline; text-underline-offset: 4px; }
103
+
104
+ :root[data-theme="light"] .pk-button.outline { background: var(--pk-bg); border-color: var(--pk-border); }
105
+ :root[data-theme="light"] .pk-button.outline:hover { background: var(--pk-surface-2); color: var(--pk-text); }
106
+ :root[data-theme="light"] .pk-button.ghost:hover { background: var(--pk-surface-2); }
107
+ :root[data-theme="light"] .pk-button.destructive { background: color-mix(in oklab, var(--pk-red) 10%, transparent); }
108
+ :root[data-theme="light"] .pk-button.destructive:hover { background: color-mix(in oklab, var(--pk-red) 20%, transparent); }
109
+ :root[data-theme="light"] .pk-button.destructive:focus-visible { box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent); }
110
+ @media (prefers-color-scheme: light) {
111
+ :root[data-theme="system"] .pk-button.outline { background: var(--pk-bg); border-color: var(--pk-border); }
112
+ :root[data-theme="system"] .pk-button.outline:hover { background: var(--pk-surface-2); color: var(--pk-text); }
113
+ :root[data-theme="system"] .pk-button.ghost:hover { background: var(--pk-surface-2); }
114
+ :root[data-theme="system"] .pk-button.destructive { background: color-mix(in oklab, var(--pk-red) 10%, transparent); }
115
+ :root[data-theme="system"] .pk-button.destructive:hover { background: color-mix(in oklab, var(--pk-red) 20%, transparent); }
116
+ :root[data-theme="system"] .pk-button.destructive:focus-visible { box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent); }
117
+ }
@@ -1,19 +1,20 @@
1
1
  module PhlexKit
2
- # Button, ported from ruby_ui's RubyUI::Button (https://ruby-ui.com) to full
3
- # variant/size parity. Renders a <button>; the `mix` pass-through means a
4
- # caller's `class:` augments ours and a phlex-reactive `**on(:event)` bundle
5
- # flows straight onto the element:
2
+ # Button, ported from ruby_ui's RubyUI::Button and matched to shadcn/ui's
3
+ # current button. Renders a <button>, or an <a> when `href:` is given
4
+ # (their asChild link button). The `mix` pass-through means a caller's
5
+ # `class:` augments ours and a phlex-reactive `**on(:event)` bundle flows
6
+ # straight onto the element:
6
7
  #
7
8
  # render PhlexKit::Button.new(variant: :primary, **on(:publish)) { "Approve" }
9
+ # render PhlexKit::Button.new(href: "/login") { "Login" }
8
10
  #
9
- # For a button-styled link, put the classes on an <a> directly
10
- # (`class: "pk-button outline sm"`) that's ruby_ui's Link approach.
11
- #
12
- # Variants/sizes mirror ruby_ui; Tailwind is replaced with vanilla `.pk-button`
13
- # CSS (button.css). `VARIANTS.fetch`/`SIZES.fetch` fail loud on a bad value.
11
+ # Mark leading/trailing glyphs with data-icon: "inline-start"/"inline-end"
12
+ # to tighten the near-side padding like theirs. `icon: true` makes the
13
+ # button square (their icon/icon-xs/… sizes — compose with `size:`).
14
+ # :primary is their :default; :xl is a kit extra. `VARIANTS.fetch`/
15
+ # `SIZES.fetch` fail loud on a bad value.
14
16
  class Button < BaseComponent
15
- # variant => modifier class. Default is :primary (the filled brand button),
16
- # matching ruby_ui.
17
+ # variant => modifier class. Default is :primary (the filled brand button).
17
18
  VARIANTS = {
18
19
  primary: "primary",
19
20
  secondary: "secondary",
@@ -24,22 +25,28 @@ module PhlexKit
24
25
  }.freeze
25
26
 
26
27
  SIZES = {
28
+ xs: "xs",
27
29
  sm: "sm",
28
30
  md: nil,
29
31
  lg: "lg",
30
32
  xl: "xl"
31
33
  }.freeze
32
34
 
33
- def initialize(variant: :primary, size: :md, type: :button, icon: false, **attrs)
35
+ def initialize(variant: :primary, size: :md, type: :button, icon: false, href: nil, **attrs)
34
36
  @variant = variant.to_sym
35
37
  @size = size.to_sym
36
38
  @type = type
37
39
  @icon = icon
40
+ @href = href
38
41
  @attrs = attrs
39
42
  end
40
43
 
41
44
  def view_template(&block)
42
- button(**mix({ type: @type, class: classes }, @attrs), &block)
45
+ if @href
46
+ a(**mix({ href: @href, class: classes }, @attrs), &block)
47
+ else
48
+ button(**mix({ type: @type, class: classes }, @attrs), &block)
49
+ end
43
50
  end
44
51
 
45
52
  private
@@ -1,8 +1,59 @@
1
- /* Co-located with button_group.rb — UI::ButtonGroup, shadcn parity: children
2
- join into one segmented control (shared borders, inner corners squared). */
3
- .pk-button-group { display: inline-flex; align-items: stretch; }
4
- .pk-button-group > * { border-radius: 0; }
5
- .pk-button-group > *:not(:first-child) { margin-left: -1px; }
6
- .pk-button-group > *:first-child { border-top-left-radius: var(--pk-radius); border-bottom-left-radius: var(--pk-radius); }
7
- .pk-button-group > *:last-child { border-top-right-radius: var(--pk-radius); border-bottom-right-radius: var(--pk-radius); }
8
- .pk-button-group > *:focus-visible { position: relative; z-index: 1; }
1
+ /* Co-located with button_group.rb — PhlexKit::ButtonGroup (+ Text /
2
+ Separator). Matched to shadcn/ui's current button-group: children keep
3
+ their OWN outer corners and only lose the inner ones (so sm buttons keep
4
+ their smaller radius), shared edges drop the trailing border. Nested
5
+ groups separate with a gap and stay independently rounded. The separator
6
+ is a hairline for same-fill segments (secondary/primary) where no border
7
+ exists to divide them. */
8
+ .pk-button-group { display: flex; width: fit-content; align-items: stretch; }
9
+ .pk-button-group:has(> .pk-button-group) { gap: .5rem; }
10
+ .pk-button-group > :focus-visible { position: relative; z-index: 10; }
11
+ .pk-button-group > input { flex: 1; }
12
+
13
+ /* horizontal (default) */
14
+ .pk-button-group:not(.vertical) > :not(:first-child) {
15
+ border-top-left-radius: 0;
16
+ border-bottom-left-radius: 0;
17
+ border-left: 0;
18
+ }
19
+ .pk-button-group:not(.vertical) > :not(:last-child) {
20
+ border-top-right-radius: 0;
21
+ border-bottom-right-radius: 0;
22
+ }
23
+
24
+ /* vertical */
25
+ .pk-button-group.vertical { flex-direction: column; }
26
+ .pk-button-group.vertical > :not(:first-child) {
27
+ border-top-left-radius: 0;
28
+ border-top-right-radius: 0;
29
+ border-top: 0;
30
+ }
31
+ .pk-button-group.vertical > :not(:last-child) {
32
+ border-bottom-left-radius: 0;
33
+ border-bottom-right-radius: 0;
34
+ }
35
+
36
+ /* Nested groups keep their own corners — undo the squaring the parent applied. */
37
+ .pk-button-group > .pk-button-group { border-radius: 0; border: 0; }
38
+
39
+ .pk-button-group-text {
40
+ display: flex;
41
+ align-items: center;
42
+ gap: .5rem;
43
+ background: var(--pk-surface-2);
44
+ border: 1px solid var(--pk-border);
45
+ border-radius: var(--pk-radius);
46
+ padding: 0 .625rem;
47
+ font-size: .875rem;
48
+ font-weight: 500;
49
+ }
50
+ .pk-button-group-text svg { pointer-events: none; }
51
+ .pk-button-group-text svg:not([width]) { width: 1rem; height: 1rem; }
52
+
53
+ .pk-button-group-separator {
54
+ position: relative;
55
+ align-self: stretch;
56
+ background: var(--pk-input);
57
+ }
58
+ .pk-button-group:not(.vertical) > .pk-button-group-separator { width: 1px; margin: 0 1px; }
59
+ .pk-button-group.vertical > .pk-button-group-separator { height: 1px; margin: 1px 0; }
@@ -1,14 +1,20 @@
1
1
  module PhlexKit
2
- # Joined row of buttons, ported from shadcn/ui's ButtonGroup: children lose
3
- # their inner corners/double borders and read as one segmented control.
4
- # `.pk-button-group` (button_group.css).
2
+ # Joined row (or column) of buttons, ported from shadcn/ui's ButtonGroup:
3
+ # children lose their inner corners/double borders and read as one segmented
4
+ # control. `orientation: :vertical` stacks. Nest groups to separate segments
5
+ # with a gap; add ButtonGroupText for static labels and ButtonGroupSeparator
6
+ # between same-fill buttons. `.pk-button-group` (button_group.css).
5
7
  class ButtonGroup < BaseComponent
6
- def initialize(**attrs)
8
+ ORIENTATIONS = { horizontal: nil, vertical: "vertical" }.freeze
9
+
10
+ def initialize(orientation: :horizontal, **attrs)
11
+ @orientation = orientation.to_sym
7
12
  @attrs = attrs
8
13
  end
9
14
 
10
15
  def view_template(&)
11
- div(**mix({ class: "pk-button-group", role: "group" }, @attrs), &)
16
+ classes = [ "pk-button-group", ORIENTATIONS.fetch(@orientation) ].compact.join(" ")
17
+ div(**mix({ class: classes, role: "group" }, @attrs), &)
12
18
  end
13
19
  end
14
20
  end
@@ -0,0 +1,14 @@
1
+ module PhlexKit
2
+ # Hairline divider between same-fill segments of a PhlexKit::ButtonGroup,
3
+ # ported from shadcn/ui's ButtonGroupSeparator. Runs perpendicular to the
4
+ # group's orientation automatically (button_group.css). See button_group.rb.
5
+ class ButtonGroupSeparator < BaseComponent
6
+ def initialize(**attrs)
7
+ @attrs = attrs
8
+ end
9
+
10
+ def view_template
11
+ div(**mix({ class: "pk-button-group-separator", role: "separator", aria: { hidden: true } }, @attrs))
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module PhlexKit
2
+ # Static text segment inside a PhlexKit::ButtonGroup, ported from shadcn/ui's
3
+ # ButtonGroupText — a muted, bordered cell that joins the group like a
4
+ # button but isn't interactive. See button_group.rb.
5
+ class ButtonGroupText < BaseComponent
6
+ def initialize(**attrs)
7
+ @attrs = attrs
8
+ end
9
+
10
+ def view_template(&block)
11
+ div(**mix({ class: "pk-button-group-text" }, @attrs), &block)
12
+ end
13
+ end
14
+ end
@@ -1,10 +1,15 @@
1
- /* Co-located with calendar.rb — UI::Calendar. ruby_ui's structure + Stimulus
2
- kept (the controller renders week rows from <template>s; its Mustache
3
- dependency replaced with a tiny interpolator); Tailwind replaced with vanilla
4
- CSS on the palette tokens. Geometry from ruby_ui (p-3 card, h-7 nav buttons,
5
- 2rem day cells, flex week rows). Theme tokens come from the global
6
- stylesheet. */
1
+ /* Co-located with calendar.rb — PhlexKit::Calendar. ruby_ui's structure +
2
+ Stimulus kept (the controller renders week rows from <template>s; its
3
+ Mustache dependency replaced with a tiny interpolator); extended to
4
+ shadcn's current surface. Sizing rides nova's variable system:
5
+ --pk-cell-size (default 2rem) and --pk-cell-radius drive every day cell
6
+ the Custom Cell Size example just overrides --pk-cell-size inline. Range
7
+ days paint a contiguous accent band (in-range cells square off; the
8
+ start/end caps keep their outer radius). Booked (explicitly disabled)
9
+ days strike through at full opacity, like theirs. */
7
10
  .pk-calendar {
11
+ --pk-cell-size: 2rem;
12
+ --pk-cell-radius: calc(var(--pk-radius) - 2px);
8
13
  display: inline-flex;
9
14
  flex-direction: column;
10
15
  gap: 1rem;
@@ -20,6 +25,18 @@
20
25
  }
21
26
  .pk-calendar-title { font-size: .875rem; font-weight: 500; }
22
27
 
28
+ /* Month/year dropdown caption (caption_layout: :dropdown) — native selects
29
+ restyled to their compact h-6 caption controls. */
30
+ .pk-calendar-dropdowns { display: flex; align-items: center; gap: .25rem; }
31
+ .pk-native-select-field.pk-calendar-dropdown {
32
+ width: auto;
33
+ height: 1.5rem;
34
+ padding: 0 .25rem 0 .375rem;
35
+ font-size: .875rem;
36
+ font-weight: 500;
37
+ border-color: var(--pk-input);
38
+ }
39
+
23
40
  .pk-calendar-prev, .pk-calendar-next {
24
41
  position: absolute;
25
42
  display: inline-flex;
@@ -50,23 +67,39 @@
50
67
  .pk-calendar-weekdays-row { display: flex; }
51
68
  .pk-calendar-week { display: flex; width: 100%; margin-top: .5rem; }
52
69
  .pk-calendar-weekday {
53
- width: 2rem;
54
- border-radius: calc(var(--pk-radius) - 2px);
70
+ flex: none;
71
+ width: var(--pk-cell-size);
72
+ border-radius: var(--pk-cell-radius);
55
73
  font-size: .8rem;
56
74
  font-weight: 400;
57
75
  color: var(--pk-muted);
58
76
  }
77
+ .pk-calendar-weeknumber,
78
+ .pk-calendar-weeknumber-head {
79
+ flex: none;
80
+ display: inline-flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ width: var(--pk-cell-size);
84
+ height: var(--pk-cell-size);
85
+ font-size: .75rem;
86
+ font-weight: 400;
87
+ color: var(--pk-muted);
88
+ }
89
+ .pk-calendar-weeknumber-head { height: auto; }
59
90
 
60
91
  .pk-calendar-cell {
61
92
  position: relative;
93
+ display: inline-flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ flex: none;
97
+ width: var(--pk-cell-size);
98
+ height: var(--pk-cell-size);
62
99
  padding: 0;
63
100
  text-align: center;
64
101
  font-size: .875rem;
65
102
  }
66
- .pk-calendar-cell:has([aria-selected]) {
67
- background: var(--pk-accent);
68
- border-radius: calc(var(--pk-radius) - 2px);
69
- }
70
103
  .pk-calendar-cell:focus-within { position: relative; z-index: 20; }
71
104
 
72
105
  /* Day button states (base = current month). */
@@ -74,11 +107,11 @@
74
107
  display: inline-flex;
75
108
  align-items: center;
76
109
  justify-content: center;
77
- width: 2rem;
78
- height: 2rem;
110
+ width: var(--pk-cell-size);
111
+ height: var(--pk-cell-size);
79
112
  padding: 0;
80
113
  border: 0;
81
- border-radius: calc(var(--pk-radius) - 2px);
114
+ border-radius: var(--pk-cell-radius);
82
115
  background: transparent;
83
116
  color: var(--pk-text);
84
117
  font: inherit;
@@ -107,3 +140,21 @@
107
140
  color: var(--pk-muted);
108
141
  opacity: .5;
109
142
  }
143
+ /* Booked: explicitly blocked dates keep full opacity, struck through. */
144
+ .pk-calendar-day.booked {
145
+ text-decoration: line-through;
146
+ opacity: 1;
147
+ }
148
+
149
+ /* Range selection: a contiguous accent band between the brand-filled caps. */
150
+ .pk-calendar-cell:has(> .in-range) { background: var(--pk-accent); }
151
+ .pk-calendar-day.in-range {
152
+ width: 100%;
153
+ height: 100%;
154
+ border-radius: 0;
155
+ background: var(--pk-accent);
156
+ }
157
+ .pk-calendar-cell:has(> .range-cap) { background: var(--pk-accent); }
158
+ .pk-calendar-cell:has(> .range-start.range-cap) { border-radius: var(--pk-cell-radius) 0 0 var(--pk-cell-radius); }
159
+ .pk-calendar-cell:has(> .range-end.range-cap) { border-radius: 0 var(--pk-cell-radius) var(--pk-cell-radius) 0; }
160
+ .pk-calendar-cell:has(> .range-start.range-end) { border-radius: var(--pk-cell-radius); }