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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dabd58e18372455c16ef319dc2f6270469ea2bbacfa42f2418f1e82f127d57cb
4
- data.tar.gz: 04b6f68484015c542bfb65d24322c9342ec029d4d3d322f80dca0f571bbdcba0
3
+ metadata.gz: 71cbb9b8727b67b80fe0bcbd69fb4eb3dd298529cd3ec43248c770df56c1554b
4
+ data.tar.gz: fa3abdc391af56d26491645a262a8d56657bf2ae21996370e82a7e0cc4704776
5
5
  SHA512:
6
- metadata.gz: d7ffa228298fa3583da83586982aa28404e1c87979b5c839123c6f493da0e6ce8502b9df6a40b58ba73cd11276a06c3e2c96a081bd82057b903dfd5f9d4dddab
7
- data.tar.gz: fee2a8edf8f4ad0da4ab8ee42da82316ee0a6674f689f9f1c6025b7efd097c4a557e788dbe725683c5ecdbe49635735cab086a798c39d104550bc17dbb017b70
6
+ metadata.gz: 064253644752dac989e4585356a8896ce35a4ca8ec79eee58c603c6b807e3203ae5c64bd3ccc6d2e9340007af49a85488e9f4acf3147d1f7fd76680afd54103e
7
+ data.tar.gz: 6687d41814334c237479b7bdfbaa6e47f61c06a6c00505efeb218b62a7f2e1926b2b7441bd95b5d5216fe56491a2b03db82ff9c8cf613b9954954ca89d269c6b
@@ -63,6 +63,22 @@
63
63
  mask-image: linear-gradient(to bottom, transparent 0, black var(--pk-scroll-fade-size));
64
64
  }
65
65
  .pk-scroll-fade[data-at-start][data-at-end] { mask-image: none; }
66
+ /* Horizontal mode (pair with the controller's axis value "x"). */
67
+ .pk-scroll-fade.horizontal {
68
+ overflow-y: visible;
69
+ overflow-x: auto;
70
+ mask-image: linear-gradient(to right,
71
+ transparent 0, black var(--pk-scroll-fade-size),
72
+ black calc(100% - var(--pk-scroll-fade-size)), transparent 100%);
73
+ }
74
+ .pk-scroll-fade.horizontal[data-at-start] {
75
+ mask-image: linear-gradient(to right,
76
+ black calc(100% - var(--pk-scroll-fade-size)), transparent 100%);
77
+ }
78
+ .pk-scroll-fade.horizontal[data-at-end] {
79
+ mask-image: linear-gradient(to right, transparent 0, black var(--pk-scroll-fade-size));
80
+ }
81
+ .pk-scroll-fade.horizontal[data-at-start][data-at-end] { mask-image: none; }
66
82
 
67
83
  /* Kit elements size predictably: width includes padding + border, so
68
84
  `width: 100%` controls (inputs, triggers, option rows) never overflow their
@@ -40,6 +40,7 @@
40
40
  @import url("drawer/drawer.css");
41
41
  @import url("dropdown_menu/dropdown_menu.css");
42
42
  @import url("empty/empty.css");
43
+ @import url("field/field.css");
43
44
  @import url("form/form.css");
44
45
  @import url("form_field/form_field.css");
45
46
  @import url("hover_card/hover_card.css");
@@ -1,32 +1,70 @@
1
- /* Co-located with alert.rb — UI::Alert (+ Title / Description). ruby_ui's
2
- structure kept; Tailwind replaced with vanilla CSS on the palette tokens.
3
- Geometry from ruby_ui (ring-1 ring-inset border, rounded-lg, px-4 py-4,
4
- text-sm). The variant tints reuse the flash colours (success/destructive/
5
- warning) the admin already used. Theme tokens come from the global stylesheet. */
1
+ /* Co-located with alert.rb — PhlexKit::Alert (+ Title / Description / Action).
2
+ Matched to shadcn/ui's current alert (nova): a grid callout on the card
3
+ surface. A leading `> svg` child switches on the two-column icon layout
4
+ (icon spans both rows); an AlertAction pins to the top-right corner and
5
+ reserves right padding on the box. Variants tint the TEXT only the
6
+ background stays the card surface (shadcn dropped the tinted-fill look).
7
+ success/warning are kit extras (ruby_ui heritage) in the same grammar. */
6
8
  .pk-alert {
7
- position: relative;
9
+ display: grid;
10
+ gap: .125rem;
8
11
  width: 100%;
9
12
  border: 1px solid var(--pk-border);
10
13
  border-radius: var(--pk-radius);
11
- padding: .85rem 1rem;
14
+ padding: .5rem .625rem;
12
15
  font-size: .875rem;
13
- background: color-mix(in oklab, var(--pk-surface) 60%, transparent);
16
+ line-height: 1.25rem;
17
+ text-align: left;
18
+ background: var(--pk-surface);
14
19
  color: var(--pk-text);
15
20
  }
16
- .pk-alert.success {
17
- border-color: color-mix(in oklab, var(--pk-green) 40%, var(--pk-border));
18
- background: color-mix(in oklab, var(--pk-green) 8%, transparent);
19
- color: var(--pk-green);
21
+ .pk-alert:has(> .pk-alert-action) {
22
+ position: relative;
23
+ padding-right: 4.5rem;
24
+ }
25
+ .pk-alert:has(> svg) {
26
+ grid-template-columns: auto 1fr;
27
+ column-gap: .5rem;
28
+ }
29
+ .pk-alert > svg {
30
+ grid-row: span 2;
31
+ translate: 0 .125rem;
32
+ width: 1rem;
33
+ height: 1rem;
34
+ color: currentColor;
35
+ }
36
+ .pk-alert.destructive { color: var(--pk-red); }
37
+ .pk-alert.destructive .pk-alert-description {
38
+ color: color-mix(in oklab, var(--pk-red) 90%, transparent);
39
+ }
40
+ .pk-alert.success { color: var(--pk-green); }
41
+ .pk-alert.success .pk-alert-description {
42
+ color: color-mix(in oklab, var(--pk-green) 90%, transparent);
43
+ }
44
+ .pk-alert.warning { color: var(--pk-amber); }
45
+ .pk-alert.warning .pk-alert-description {
46
+ color: color-mix(in oklab, var(--pk-amber) 90%, transparent);
47
+ }
48
+ .pk-alert-title { font-weight: 500; }
49
+ .pk-alert:has(> svg) .pk-alert-title { grid-column-start: 2; }
50
+ .pk-alert-description {
51
+ color: var(--pk-muted);
52
+ font-size: .875rem;
53
+ text-wrap: balance;
54
+ }
55
+ @media (min-width: 768px) {
56
+ .pk-alert-description { text-wrap: pretty; }
20
57
  }
21
- .pk-alert.destructive {
22
- border-color: color-mix(in oklab, var(--pk-red) 40%, var(--pk-border));
23
- background: color-mix(in oklab, var(--pk-red) 8%, transparent);
24
- color: var(--pk-red);
58
+ .pk-alert-description p:not(:last-child) { margin-bottom: 1rem; }
59
+ .pk-alert-title a,
60
+ .pk-alert-description a {
61
+ text-decoration: underline;
62
+ text-underline-offset: 3px;
25
63
  }
26
- .pk-alert.warning {
27
- border-color: color-mix(in oklab, var(--pk-amber) 40%, var(--pk-border));
28
- background: color-mix(in oklab, var(--pk-amber) 8%, transparent);
29
- color: var(--pk-amber);
64
+ .pk-alert-title a:hover,
65
+ .pk-alert-description a:hover { color: var(--pk-text); }
66
+ .pk-alert-action {
67
+ position: absolute;
68
+ top: .5rem;
69
+ right: .5rem;
30
70
  }
31
- .pk-alert-title { margin: 0 0 .25rem; font-weight: 500; line-height: 1.1; letter-spacing: -.01em; }
32
- .pk-alert-description { font-size: .875rem; }
@@ -1,15 +1,19 @@
1
1
  module PhlexKit
2
- # Callout box, ported from ruby_ui's RubyUI::Alert. Presentational, no JS. A
3
- # `variant:` selector (same shape as PhlexKit::Button) tints the box; compose with
4
- # AlertTitle + AlertDescription:
2
+ # Callout box, ported from ruby_ui's RubyUI::Alert and matched to shadcn/ui's
3
+ # current alert. Presentational, no JS. A `variant:` selector (same shape as
4
+ # PhlexKit::Button) tints the text; a leading svg child becomes the icon column
5
+ # (CSS :has switches the grid); compose with AlertTitle + AlertDescription
6
+ # (+ optional AlertAction pinned top-right):
5
7
  #
6
- # render PhlexKit::Alert.new(variant: :success) do
8
+ # render PhlexKit::Alert.new do
9
+ # svg(...) { ... } # optional icon
7
10
  # render PhlexKit::AlertTitle.new { "Saved" }
8
11
  # render PhlexKit::AlertDescription.new { "Your changes are live." }
12
+ # render PhlexKit::AlertAction.new { render PhlexKit::Button.new(size: :sm) { "Undo" } }
9
13
  # end
10
14
  #
11
- # ruby_ui's variants (nil / warning / success / destructive) map onto the
12
- # palette tokens in vanilla CSS (alert.css). `VARIANTS.fetch` fails loud.
15
+ # shadcn ships default/destructive; success/warning are kit extras kept from
16
+ # ruby_ui, restyled to the same text-tint grammar. `VARIANTS.fetch` fails loud.
13
17
  class Alert < BaseComponent
14
18
  # variant => extra class appended after the base `ui-alert` (nil = neutral).
15
19
  VARIANTS = {
@@ -25,7 +29,7 @@ module PhlexKit
25
29
  end
26
30
 
27
31
  def view_template(&block)
28
- div(**mix({ class: classes }, @attrs), &block)
32
+ div(**mix({ class: classes, role: "alert" }, @attrs), &block)
29
33
  end
30
34
 
31
35
  private
@@ -0,0 +1,14 @@
1
+ module PhlexKit
2
+ # Top-right action slot of a PhlexKit::Alert, ported from shadcn/ui's
3
+ # AlertAction. Pins its content (typically a small Button) to the alert's
4
+ # corner; the alert reserves right padding via :has(). See alert.rb.
5
+ class AlertAction < BaseComponent
6
+ def initialize(**attrs)
7
+ @attrs = attrs
8
+ end
9
+
10
+ def view_template(&block)
11
+ div(**mix({ class: "pk-alert-action" }, @attrs), &block)
12
+ end
13
+ end
14
+ end
@@ -1,12 +1,13 @@
1
1
  module PhlexKit
2
- # Heading line of a PhlexKit::Alert. See alert.rb.
2
+ # Heading line of a PhlexKit::Alert. A div, matching shadcn/ui's markup
3
+ # (role="alert" on the box carries the semantics). See alert.rb.
3
4
  class AlertTitle < BaseComponent
4
5
  def initialize(**attrs)
5
6
  @attrs = attrs
6
7
  end
7
8
 
8
9
  def view_template(&block)
9
- h5(**mix({ class: "pk-alert-title" }, @attrs), &block)
10
+ div(**mix({ class: "pk-alert-title" }, @attrs), &block)
10
11
  end
11
12
  end
12
13
  end
@@ -8,8 +8,8 @@
8
8
  position: fixed;
9
9
  inset: 0;
10
10
  z-index: 50;
11
- background: rgb(0 0 0 / .5);
12
- backdrop-filter: blur(8px);
11
+ background: rgb(0 0 0 / .1);
12
+ backdrop-filter: blur(4px);
13
13
  }
14
14
  .pk-alert-dialog-panel {
15
15
  position: fixed;
@@ -76,3 +76,6 @@
76
76
  border-top: 1px solid var(--pk-border);
77
77
  background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent);
78
78
  }
79
+ @supports not (backdrop-filter: blur(1px)) {
80
+ .pk-alert-dialog-overlay { background: rgb(0 0 0 / .5); }
81
+ }
@@ -1,22 +1,45 @@
1
- /* Co-located with avatar.rb — UI::Avatar (+ Image / Fallback). ruby_ui's Tailwind
2
- vanilla CSS on the palette tokens. The controller toggles `.hidden` between
3
- image and fallback. Theme tokens come from the global stylesheet. */
1
+ /* Co-located with avatar.rb — PhlexKit::Avatar (+ Image / Fallback / Badge /
2
+ Group / GroupCount). Matched to shadcn/ui's current avatar (nova): the root
3
+ is NOT overflow-clipped (a Badge hangs over the edge) image and fallback
4
+ round themselves, and a ::after overlay draws the hairline border, blended
5
+ darken (light mode) / lighten (dark mode) so it hugs any photo. The
6
+ controller toggles `.hidden` between image and fallback. shadcn ships
7
+ sm/default/lg; xs/xl are kit extras. */
4
8
  .pk-avatar {
5
9
  position: relative;
6
10
  display: inline-flex;
7
11
  flex-shrink: 0;
8
- overflow: hidden;
12
+ user-select: none;
9
13
  border-radius: 999px;
10
14
  height: 2rem;
11
15
  width: 2rem;
12
16
  font-size: .875rem;
13
17
  }
18
+ .pk-avatar::after {
19
+ content: "";
20
+ position: absolute;
21
+ inset: 0;
22
+ border: 1px solid var(--pk-border);
23
+ border-radius: 999px;
24
+ mix-blend-mode: lighten;
25
+ pointer-events: none;
26
+ }
27
+ :root[data-theme="light"] .pk-avatar::after { mix-blend-mode: darken; }
28
+ @media (prefers-color-scheme: light) {
29
+ :root[data-theme="system"] .pk-avatar::after { mix-blend-mode: darken; }
30
+ }
14
31
  .pk-avatar.xs { height: 1rem; width: 1rem; font-size: .5rem; }
15
32
  .pk-avatar.sm { height: 1.5rem; width: 1.5rem; font-size: .75rem; }
16
33
  .pk-avatar.lg { height: 2.5rem; width: 2.5rem; font-size: 1rem; }
17
34
  .pk-avatar.xl { height: 5rem; width: 5rem; font-size: 1.875rem; }
18
35
 
19
- .pk-avatar-image { aspect-ratio: 1 / 1; height: 100%; width: 100%; object-fit: cover; }
36
+ .pk-avatar-image {
37
+ aspect-ratio: 1 / 1;
38
+ height: 100%;
39
+ width: 100%;
40
+ object-fit: cover;
41
+ border-radius: 999px;
42
+ }
20
43
  .pk-avatar-fallback {
21
44
  display: flex;
22
45
  height: 100%;
@@ -25,11 +48,59 @@
25
48
  justify-content: center;
26
49
  border-radius: 999px;
27
50
  background: var(--pk-surface-2);
28
- font-weight: 500;
51
+ color: var(--pk-muted);
29
52
  }
30
53
  .pk-avatar-image.hidden, .pk-avatar-fallback.hidden { display: none; }
31
54
 
32
- /* Overlapping row (shadcn's -space-x-2 grouped avatars). */
55
+ /* Status dot pinned to the bottom-right corner; sized by the avatar's size
56
+ modifier, glyph hidden when there's no room (shadcn's AvatarBadge). */
57
+ .pk-avatar-badge {
58
+ position: absolute;
59
+ right: 0;
60
+ bottom: 0;
61
+ z-index: 10;
62
+ display: inline-flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ user-select: none;
66
+ border-radius: 999px;
67
+ background: var(--pk-brand);
68
+ color: var(--pk-brand-ink);
69
+ box-shadow: 0 0 0 2px var(--pk-bg);
70
+ width: .625rem;
71
+ height: .625rem;
72
+ }
73
+ .pk-avatar-badge > svg { width: .5rem; height: .5rem; }
74
+ .pk-avatar.xs .pk-avatar-badge { width: .375rem; height: .375rem; }
75
+ .pk-avatar.sm .pk-avatar-badge { width: .5rem; height: .5rem; }
76
+ .pk-avatar.xs .pk-avatar-badge > svg,
77
+ .pk-avatar.sm .pk-avatar-badge > svg { display: none; }
78
+ .pk-avatar.lg .pk-avatar-badge { width: .75rem; height: .75rem; }
79
+ .pk-avatar.xl .pk-avatar-badge { width: 1rem; height: 1rem; }
80
+ .pk-avatar.xl .pk-avatar-badge > svg { width: .625rem; height: .625rem; }
81
+
82
+ /* Overlapping row (shadcn's -space-x-2 grouped avatars); a trailing
83
+ GroupCount pill follows the avatars' size via :has(). */
33
84
  .pk-avatar-group { display: flex; }
34
- .pk-avatar-group .pk-avatar { box-shadow: 0 0 0 2px var(--pk-bg); }
35
- .pk-avatar-group .pk-avatar:not(:first-child) { margin-left: -.5rem; }
85
+ .pk-avatar-group .pk-avatar,
86
+ .pk-avatar-group .pk-avatar-group-count { box-shadow: 0 0 0 2px var(--pk-bg); }
87
+ .pk-avatar-group > :not(:first-child) { margin-left: -.5rem; }
88
+
89
+ .pk-avatar-group-count {
90
+ position: relative;
91
+ display: flex;
92
+ flex-shrink: 0;
93
+ align-items: center;
94
+ justify-content: center;
95
+ border-radius: 999px;
96
+ background: var(--pk-surface-2);
97
+ color: var(--pk-muted);
98
+ width: 2rem;
99
+ height: 2rem;
100
+ font-size: .875rem;
101
+ }
102
+ .pk-avatar-group-count > svg { width: 1rem; height: 1rem; }
103
+ .pk-avatar-group:has(.pk-avatar.sm) .pk-avatar-group-count { width: 1.5rem; height: 1.5rem; font-size: .75rem; }
104
+ .pk-avatar-group:has(.pk-avatar.sm) .pk-avatar-group-count > svg { width: .75rem; height: .75rem; }
105
+ .pk-avatar-group:has(.pk-avatar.lg) .pk-avatar-group-count { width: 2.5rem; height: 2.5rem; font-size: 1rem; }
106
+ .pk-avatar-group:has(.pk-avatar.lg) .pk-avatar-group-count > svg { width: 1.25rem; height: 1.25rem; }
@@ -1,9 +1,11 @@
1
1
  module PhlexKit
2
2
  # Avatar, ported from ruby_ui's RubyUI::Avatar — a round image with a fallback
3
- # (initials). Keeps ruby_ui's `phlex-kit--avatar` Stimulus controller (shows the
4
- # image once it loads, else the fallback). Compose AvatarImage + AvatarFallback;
5
- # fallback-only is fine (the controller no-ops without an image). `SIZES.fetch`
6
- # fails loud. Tailwind vanilla `.pk-avatar*` (avatar.css).
3
+ # (initials), matched to shadcn/ui's current avatar. Keeps ruby_ui's
4
+ # `phlex-kit--avatar` Stimulus controller (shows the image once it loads, else
5
+ # the fallback). Compose AvatarImage + AvatarFallback (+ optional AvatarBadge
6
+ # status dot); fallback-only is fine (the controller no-ops without an image).
7
+ # Overlap several in an AvatarGroup, with AvatarGroupCount as the "+3" pill.
8
+ # shadcn ships sm/default/lg; xs/xl are kit extras. `SIZES.fetch` fails loud.
7
9
  class Avatar < BaseComponent
8
10
  SIZES = { xs: "xs", sm: "sm", md: nil, lg: "lg", xl: "xl" }.freeze
9
11
 
@@ -0,0 +1,15 @@
1
+ module PhlexKit
2
+ # Status dot pinned to a PhlexKit::Avatar's bottom-right corner, ported from
3
+ # shadcn/ui's AvatarBadge. Empty for a plain presence dot, or wrap a small
4
+ # svg glyph (hidden automatically on xs/sm avatars). Recolor via style/class
5
+ # — defaults to the brand fill. See avatar.rb.
6
+ class AvatarBadge < BaseComponent
7
+ def initialize(**attrs)
8
+ @attrs = attrs
9
+ end
10
+
11
+ def view_template(&block)
12
+ span(**mix({ class: "pk-avatar-badge" }, @attrs), &block)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ module PhlexKit
2
+ # Trailing overflow pill in a PhlexKit::AvatarGroup ("+3", or a small svg),
3
+ # ported from shadcn/ui's AvatarGroupCount. Sizes itself to match the
4
+ # group's avatars via :has() in avatar.css. See avatar.rb.
5
+ class AvatarGroupCount < BaseComponent
6
+ def initialize(**attrs)
7
+ @attrs = attrs
8
+ end
9
+
10
+ def view_template(&block)
11
+ div(**mix({ class: "pk-avatar-group-count" }, @attrs), &block)
12
+ end
13
+ end
14
+ end
@@ -1,50 +1,72 @@
1
- /* Co-located with badge.rb — UI::Badge. ruby_ui's Tailwind replaced with vanilla
2
- CSS on the palette tokens, at variant/size parity. Geometry from ruby_ui
3
- (inline-flex, rounded-md, ring-1 ring-inset 1px border, bg tint, px-2 py-1
4
- text-xs). Theme tokens come from the global stylesheet. */
1
+ /* Co-located with badge.rb — PhlexKit::Badge. Matched to shadcn/ui's current
2
+ badge (nova): a fixed-height solid pill (h-5, full-round), gap for inline
3
+ glyphs, near-side padding tightened when a child carries
4
+ data-icon="inline-start/end". Hover fills apply to LINK badges only
5
+ (href:), except ghost which hovers everywhere, like theirs. destructive's
6
+ tint is stronger in dark mode (their dark:bg-destructive/20).
7
+ success/warning + sm/lg sizes are kit extras. */
5
8
  .pk-badge {
6
9
  display: inline-flex;
7
10
  align-items: center;
8
- border-radius: calc(var(--pk-radius) - 2px);
9
- padding: .25rem .5rem;
11
+ justify-content: center;
12
+ width: fit-content;
13
+ flex-shrink: 0;
14
+ gap: .25rem;
15
+ overflow: hidden;
16
+ white-space: nowrap;
17
+ height: 1.25rem;
18
+ border-radius: 2rem;
19
+ border: 1px solid transparent;
20
+ padding: .125rem .5rem;
10
21
  font-size: .75rem;
11
22
  font-weight: 500;
12
23
  line-height: 1;
13
- border: 1px solid transparent;
24
+ text-decoration: none;
25
+ transition: color .15s ease, background-color .15s ease, border-color .15s ease, box-shadow .15s ease;
26
+ }
27
+ /* shadcn forces glyphs to size-3 with `!` — override Spinner's own sizing. */
28
+ .pk-badge > svg { width: .75rem !important; height: .75rem !important; pointer-events: none; }
29
+ .pk-badge:has(> [data-icon="inline-start"]) { padding-left: .375rem; }
30
+ .pk-badge:has(> [data-icon="inline-end"]) { padding-right: .375rem; }
31
+ .pk-badge:focus-visible {
32
+ outline: none;
33
+ border-color: var(--pk-ring);
34
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
14
35
  }
15
36
 
16
- /* Sizes (md is the base above). */
17
- .pk-badge.sm { padding: .125rem .375rem; font-size: .7rem; }
18
- .pk-badge.lg { padding: .25rem .75rem; font-size: .875rem; }
37
+ /* Sizes (md is the base above) — kit extras. */
38
+ .pk-badge.sm { height: 1rem; padding: 0 .375rem; font-size: .7rem; }
39
+ .pk-badge.sm:has(> [data-icon="inline-start"]) { padding-left: .25rem; }
40
+ .pk-badge.sm:has(> [data-icon="inline-end"]) { padding-right: .25rem; }
41
+ .pk-badge.lg { height: 1.5rem; padding: .125rem .75rem; font-size: .875rem; }
19
42
 
20
- /* Variants colored text over a faint tint of the same hue + matching ring. */
21
- .pk-badge.primary {
22
- color: var(--pk-brand);
23
- background: color-mix(in oklab, var(--pk-brand) 8%, transparent);
24
- border-color: color-mix(in oklab, var(--pk-brand) 25%, transparent);
25
- }
26
- .pk-badge.secondary {
27
- color: var(--pk-muted);
28
- background: color-mix(in oklab, var(--pk-muted) 12%, transparent);
29
- border-color: color-mix(in oklab, var(--pk-muted) 25%, transparent);
30
- }
31
- .pk-badge.outline {
32
- color: var(--pk-text);
33
- background: transparent;
34
- border-color: var(--pk-border);
35
- }
36
- .pk-badge.destructive {
37
- color: var(--pk-red);
38
- background: color-mix(in oklab, var(--pk-red) 10%, transparent);
39
- border-color: color-mix(in oklab, var(--pk-red) 25%, transparent);
40
- }
41
- .pk-badge.success {
42
- color: var(--pk-green);
43
- background: color-mix(in oklab, var(--pk-green) 10%, transparent);
44
- border-color: color-mix(in oklab, var(--pk-green) 25%, transparent);
43
+ .pk-badge.primary { background: var(--pk-brand); color: var(--pk-brand-ink); }
44
+ a.pk-badge.primary:hover { background: color-mix(in oklab, var(--pk-brand) 80%, transparent); }
45
+ .pk-badge.secondary { background: var(--pk-surface-2); color: var(--pk-text); }
46
+ a.pk-badge.secondary:hover { background: color-mix(in oklab, var(--pk-surface-2) 80%, transparent); }
47
+ .pk-badge.outline { border-color: var(--pk-border); color: var(--pk-text); }
48
+ a.pk-badge.outline:hover { background: var(--pk-surface-2); color: var(--pk-muted); }
49
+ .pk-badge.ghost { color: var(--pk-text); }
50
+ .pk-badge.ghost:hover { background: var(--pk-surface-2); color: var(--pk-muted); }
51
+ .pk-badge.link { color: var(--pk-brand); }
52
+ .pk-badge.link:hover { text-decoration: underline; text-underline-offset: 4px; }
53
+
54
+ /* Tinted text variants — 20% fill in dark, 10% in light (their /20 dark, /10). */
55
+ .pk-badge.destructive { color: var(--pk-red); background: color-mix(in oklab, var(--pk-red) 20%, transparent); }
56
+ a.pk-badge.destructive:hover { background: color-mix(in oklab, var(--pk-red) 30%, transparent); }
57
+ .pk-badge.destructive:focus-visible {
58
+ border-color: var(--pk-red);
59
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 40%, transparent);
45
60
  }
46
- .pk-badge.warning {
47
- color: var(--pk-amber);
48
- background: color-mix(in oklab, var(--pk-amber) 10%, transparent);
49
- border-color: color-mix(in oklab, var(--pk-amber) 25%, transparent);
61
+ .pk-badge.success { color: var(--pk-green); background: color-mix(in oklab, var(--pk-green) 20%, transparent); }
62
+ .pk-badge.warning { color: var(--pk-amber); background: color-mix(in oklab, var(--pk-amber) 20%, transparent); }
63
+ :root[data-theme="light"] .pk-badge.destructive { background: color-mix(in oklab, var(--pk-red) 10%, transparent); }
64
+ :root[data-theme="light"] .pk-badge.destructive:focus-visible { box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent); }
65
+ :root[data-theme="light"] .pk-badge.success { background: color-mix(in oklab, var(--pk-green) 10%, transparent); }
66
+ :root[data-theme="light"] .pk-badge.warning { background: color-mix(in oklab, var(--pk-amber) 10%, transparent); }
67
+ @media (prefers-color-scheme: light) {
68
+ :root[data-theme="system"] .pk-badge.destructive { background: color-mix(in oklab, var(--pk-red) 10%, transparent); }
69
+ :root[data-theme="system"] .pk-badge.destructive:focus-visible { box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent); }
70
+ :root[data-theme="system"] .pk-badge.success { background: color-mix(in oklab, var(--pk-green) 10%, transparent); }
71
+ :root[data-theme="system"] .pk-badge.warning { background: color-mix(in oklab, var(--pk-amber) 10%, transparent); }
50
72
  }
@@ -1,15 +1,27 @@
1
1
  module PhlexKit
2
- # Pill/label badge, ported from ruby_ui's RubyUI::Badge to variant/size parity.
3
- # Same kit shape as PhlexKit::Button — a `variant:`/`size:` selector plus attrs/block
4
- # passed through to the <span>. ruby_ui's semantic variants are ported; its long
5
- # raw-colour palette (red/blue/teal/…) is omitted as unused. Tailwind vanilla
6
- # `.pk-badge` CSS (badge.css). `VARIANTS.fetch` fails loud.
2
+ # Pill/label badge, ported from ruby_ui's RubyUI::Badge and matched to
3
+ # shadcn/ui's current badge. Same kit shape as PhlexKit::Button — a
4
+ # `variant:`/`size:` selector plus attrs/block passed through. Pass `href:`
5
+ # to render an <a> (shadcn's asChild link badge hover fills only apply to
6
+ # links). Mark leading/trailing svg glyphs with data-icon: "inline-start" /
7
+ # "inline-end" to tighten the near-side padding, like theirs:
8
+ #
9
+ # render PhlexKit::Badge.new(variant: :secondary) do
10
+ # render PhlexKit::Icon.new(:circle_check, size: nil, data: { icon: "inline-start" })
11
+ # plain "Verified"
12
+ # end
13
+ #
14
+ # shadcn ships default/secondary/destructive/outline/ghost/link (:primary is
15
+ # their :default); success/warning and the sm/lg sizes are kit extras.
16
+ # `VARIANTS.fetch` fails loud.
7
17
  class Badge < BaseComponent
8
18
  VARIANTS = {
9
19
  primary: "primary",
10
20
  secondary: "secondary",
11
21
  outline: "outline",
12
22
  destructive: "destructive",
23
+ ghost: "ghost",
24
+ link: "link",
13
25
  success: "success",
14
26
  warning: "warning"
15
27
  }.freeze
@@ -20,14 +32,19 @@ module PhlexKit
20
32
  lg: "lg"
21
33
  }.freeze
22
34
 
23
- def initialize(variant: :primary, size: :md, **attrs)
35
+ def initialize(variant: :primary, size: :md, href: nil, **attrs)
24
36
  @variant = variant.to_sym
25
37
  @size = size.to_sym
38
+ @href = href
26
39
  @attrs = attrs
27
40
  end
28
41
 
29
42
  def view_template(&block)
30
- span(**mix({ class: classes }, @attrs), &block)
43
+ if @href
44
+ a(**mix({ class: classes, href: @href }, @attrs), &block)
45
+ else
46
+ span(**mix({ class: classes }, @attrs), &block)
47
+ end
31
48
  end
32
49
 
33
50
  private
@@ -4,13 +4,13 @@
4
4
  gap: .375rem; word-break: break-word;
5
5
  font-size: .875rem; color: var(--pk-muted);
6
6
  }
7
- .pk-breadcrumb-item { display: inline-flex; align-items: center; gap: .375rem; }
7
+ .pk-breadcrumb-item { display: inline-flex; align-items: center; gap: .25rem; }
8
8
  .pk-breadcrumb-link { color: inherit; text-decoration: none; transition: color .15s ease; }
9
9
  .pk-breadcrumb-link:hover { color: var(--pk-text); }
10
10
  .pk-breadcrumb-page { font-weight: 400; color: var(--pk-text); }
11
11
  .pk-breadcrumb-separator { display: inline-flex; }
12
12
  .pk-breadcrumb-separator > svg { width: .875rem; height: .875rem; }
13
- .pk-breadcrumb-ellipsis { display: flex; height: 2.25rem; width: 2.25rem; align-items: center; justify-content: center; }
13
+ .pk-breadcrumb-ellipsis { display: flex; height: 1.25rem; width: 1.25rem; align-items: center; justify-content: center; }
14
14
  .pk-breadcrumb-ellipsis > svg { width: 1rem; height: 1rem; }
15
15
  .pk-sr-only {
16
16
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;