phlex_kit 0.2.0 → 0.2.2

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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +210 -49
  3. data/THIRD_PARTY_LICENSES +104 -0
  4. data/app/assets/stylesheets/phlex_kit/_tokens.css +72 -0
  5. data/app/assets/stylesheets/phlex_kit/phlex_kit.css +1 -1
  6. data/app/assets/stylesheets/phlex_kit/themes/claude.css +86 -0
  7. data/app/assets/stylesheets/phlex_kit/themes/neutral.css +95 -0
  8. data/app/assets/stylesheets/phlex_kit/themes/zinc.css +85 -0
  9. data/app/components/phlex_kit/accordion/accordion.css +13 -0
  10. data/app/components/phlex_kit/accordion/accordion.rb +11 -2
  11. data/app/components/phlex_kit/accordion/accordion_icon.rb +1 -3
  12. data/app/components/phlex_kit/accordion/accordion_item.rb +5 -2
  13. data/app/components/phlex_kit/alert_dialog/alert_dialog.css +45 -4
  14. data/app/components/phlex_kit/alert_dialog/alert_dialog_action.rb +3 -2
  15. data/app/components/phlex_kit/alert_dialog/alert_dialog_content.rb +6 -2
  16. data/app/components/phlex_kit/alert_dialog/alert_dialog_media.rb +14 -0
  17. data/app/components/phlex_kit/attachment/attachment.css +102 -2
  18. data/app/components/phlex_kit/attachment/attachment.rb +18 -6
  19. data/app/components/phlex_kit/attachment/attachment_action.rb +1 -6
  20. data/app/components/phlex_kit/attachment/attachment_group.rb +21 -0
  21. data/app/components/phlex_kit/attachment/attachment_media.rb +13 -4
  22. data/app/components/phlex_kit/attachment/attachment_trigger.rb +22 -0
  23. data/app/components/phlex_kit/breadcrumb/breadcrumb_ellipsis.rb +1 -5
  24. data/app/components/phlex_kit/breadcrumb/breadcrumb_separator.rb +1 -1
  25. data/app/components/phlex_kit/calendar/calendar_next.rb +1 -15
  26. data/app/components/phlex_kit/calendar/calendar_prev.rb +1 -15
  27. data/app/components/phlex_kit/card/card.css +50 -14
  28. data/app/components/phlex_kit/card/card.rb +6 -2
  29. data/app/components/phlex_kit/card/card_action.rb +14 -0
  30. data/app/components/phlex_kit/carousel/carousel_next.rb +1 -15
  31. data/app/components/phlex_kit/carousel/carousel_previous.rb +1 -15
  32. data/app/components/phlex_kit/codeblock/codeblock.css +1 -1
  33. data/app/components/phlex_kit/combobox/combobox_clear_button.rb +1 -15
  34. data/app/components/phlex_kit/combobox/combobox_input_trigger.rb +1 -12
  35. data/app/components/phlex_kit/combobox/combobox_item_indicator.rb +1 -15
  36. data/app/components/phlex_kit/combobox/combobox_search_input.rb +1 -14
  37. data/app/components/phlex_kit/combobox/combobox_trigger.rb +1 -14
  38. data/app/components/phlex_kit/command/command.css +2 -2
  39. data/app/components/phlex_kit/command/command_input.rb +1 -13
  40. data/app/components/phlex_kit/context_menu/context_menu_item.rb +1 -3
  41. data/app/components/phlex_kit/data_table/data_table_column_toggle.rb +1 -13
  42. data/app/components/phlex_kit/data_table/data_table_expand_toggle.rb +1 -12
  43. data/app/components/phlex_kit/data_table/data_table_sort_head.rb +2 -28
  44. data/app/components/phlex_kit/dialog/dialog.css +8 -2
  45. data/app/components/phlex_kit/dialog/dialog_content.rb +1 -3
  46. data/app/components/phlex_kit/drawer/drawer.css +1 -0
  47. data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +64 -0
  48. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_checkbox_item.rb +31 -0
  49. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_group.rb +11 -0
  50. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_item.rb +5 -2
  51. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_radio_group.rb +10 -0
  52. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_radio_item.rb +31 -0
  53. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_shortcut.rb +10 -0
  54. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub.rb +11 -0
  55. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_content.rb +12 -0
  56. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_trigger.rb +18 -0
  57. data/app/components/phlex_kit/icon/icon.rb +51 -0
  58. data/app/components/phlex_kit/marker/marker.css +25 -0
  59. data/app/components/phlex_kit/marker/marker.rb +18 -0
  60. data/app/components/phlex_kit/marker/marker_content.rb +7 -0
  61. data/app/components/phlex_kit/marker/marker_icon.rb +7 -0
  62. data/app/components/phlex_kit/native_select/native_select_icon.rb +1 -10
  63. data/app/components/phlex_kit/navigation_menu/navigation_menu_trigger.rb +1 -5
  64. data/app/components/phlex_kit/pagination/pagination_ellipsis.rb +1 -5
  65. data/app/components/phlex_kit/select/select_item.rb +1 -11
  66. data/app/components/phlex_kit/select/select_trigger.rb +1 -14
  67. data/app/components/phlex_kit/sheet/sheet.css +1 -1
  68. data/app/components/phlex_kit/sheet/sheet_content.rb +1 -3
  69. data/app/components/phlex_kit/sidebar/sidebar.css +129 -11
  70. data/app/components/phlex_kit/sidebar/sidebar.rb +19 -6
  71. data/app/components/phlex_kit/sidebar/sidebar_group_action.rb +14 -0
  72. data/app/components/phlex_kit/sidebar/sidebar_group_content.rb +13 -0
  73. data/app/components/phlex_kit/sidebar/sidebar_group_label.rb +13 -0
  74. data/app/components/phlex_kit/sidebar/sidebar_input.rb +14 -0
  75. data/app/components/phlex_kit/sidebar/sidebar_menu_action.rb +13 -0
  76. data/app/components/phlex_kit/sidebar/sidebar_menu_badge.rb +13 -0
  77. data/app/components/phlex_kit/sidebar/sidebar_menu_skeleton.rb +19 -0
  78. data/app/components/phlex_kit/sidebar/sidebar_menu_sub.rb +13 -0
  79. data/app/components/phlex_kit/sidebar/sidebar_menu_sub_button.rb +18 -0
  80. data/app/components/phlex_kit/sidebar/sidebar_menu_sub_item.rb +13 -0
  81. data/app/components/phlex_kit/sidebar/sidebar_separator.rb +14 -0
  82. data/app/components/phlex_kit/spinner/spinner.rb +5 -10
  83. data/app/components/phlex_kit/toast/toast_close.rb +1 -15
  84. data/app/components/phlex_kit/toast/toast_icon.rb +12 -36
  85. data/app/components/phlex_kit/typography/heading.rb +1 -1
  86. data/app/components/phlex_kit/typography/typography.css +7 -3
  87. data/app/javascript/phlex_kit/controllers/accordion_controller.js +17 -1
  88. data/app/javascript/phlex_kit/controllers/index.js +2 -0
  89. data/app/javascript/phlex_kit/controllers/scroll_fade_controller.js +31 -0
  90. data/lib/phlex_kit/configuration.rb +7 -0
  91. data/lib/phlex_kit/icons/lucide.rb +88 -0
  92. data/lib/phlex_kit/icons/phosphor.rb +88 -0
  93. data/lib/phlex_kit/icons/remix.rb +88 -0
  94. data/lib/phlex_kit/icons/tabler.rb +88 -0
  95. data/lib/phlex_kit/icons.rb +40 -0
  96. data/lib/phlex_kit/version.rb +1 -1
  97. data/lib/phlex_kit.rb +1 -0
  98. metadata +45 -8
  99. data/app/components/phlex_kit/stars/stars.css +0 -4
  100. data/app/components/phlex_kit/stars/stars.rb +0 -19
@@ -0,0 +1,10 @@
1
+ module PhlexKit
2
+ # Right-aligned keyboard hint in a menu row. Ported from shadcn/ui's
3
+ # DropdownMenuShortcut. See dropdown_menu.rb.
4
+ class DropdownMenuShortcut < BaseComponent
5
+ def initialize(**attrs) = (@attrs = attrs)
6
+ def view_template(&)
7
+ span(**mix({ class: "pk-dropdown-menu-shortcut" }, @attrs), &)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ module PhlexKit
2
+ # Nested submenu wrapper: SubTrigger row + SubContent panel, revealed on
3
+ # hover/focus with pure CSS (no extra controller). Ported from shadcn/ui's
4
+ # DropdownMenuSub. See dropdown_menu.rb.
5
+ class DropdownMenuSub < BaseComponent
6
+ def initialize(**attrs) = (@attrs = attrs)
7
+ def view_template(&)
8
+ div(**mix({ class: "pk-dropdown-menu-sub" }, @attrs), &)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module PhlexKit
2
+ # The nested panel of a DropdownMenuSub — opens beside the trigger.
3
+ # See dropdown_menu.rb.
4
+ class DropdownMenuSubContent < BaseComponent
5
+ def initialize(**attrs) = (@attrs = attrs)
6
+ def view_template(&)
7
+ div(**mix({ class: "pk-dropdown-menu-sub-content", role: "menu" }, @attrs)) do
8
+ div(class: "pk-dropdown-menu-viewport", &)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ module PhlexKit
2
+ # The row that opens a DropdownMenuSub (trailing ▸ chevron). See
3
+ # dropdown_menu.rb.
4
+ class DropdownMenuSubTrigger < BaseComponent
5
+ def initialize(**attrs) = (@attrs = attrs)
6
+ def view_template(&block)
7
+ div(**mix({
8
+ class: "pk-dropdown-menu-item pk-dropdown-menu-sub-trigger",
9
+ role: "menuitem",
10
+ tabindex: "0",
11
+ aria: { haspopup: "menu" }
12
+ }, @attrs)) do
13
+ block&.call
14
+ render Icon.new(:chevron_right, size: nil, class: "pk-dropdown-menu-sub-chevron")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,51 @@
1
+ module PhlexKit
2
+ # A glyph from the configured icon library (ui.shadcn.com/create's "Icon
3
+ # Library" option): :lucide (default), :tabler, :phosphor, or :remix — set
4
+ # kit-wide via PhlexKit.config.icon_library or per-instance with `library:`.
5
+ # Names are PhlexKit's canonical semantic set (PhlexKit::Icons.names);
6
+ # unknown names/libraries fail loud (KeyError).
7
+ #
8
+ # `size:` emits width/height attributes (default 16, shadcn's size-4); pass
9
+ # `size: nil` to defer sizing to CSS (existing `.pk-x svg { … }` rules).
10
+ # Stroke libraries (lucide/tabler) render stroke="currentColor"; fill
11
+ # libraries (phosphor/remix) render fill="currentColor" — both follow text
12
+ # color. Attrs pass through via mix.
13
+ class Icon < BaseComponent
14
+ def initialize(name, library: nil, size: 16, **attrs)
15
+ @name = name
16
+ @library = library
17
+ @size = size
18
+ @attrs = attrs
19
+ end
20
+
21
+ def view_template
22
+ icon = Icons.fetch(@name, library: @library || PhlexKit.config.icon_library)
23
+ svg(**mix(base_attrs(icon), @attrs)) do |s|
24
+ icon[:elements].each { |tag, attrs| s.public_send(tag, **attrs) }
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def base_attrs(icon)
31
+ base = {
32
+ xmlns: "http://www.w3.org/2000/svg",
33
+ viewbox: icon[:view_box],
34
+ class: "pk-icon",
35
+ "aria-hidden": "true"
36
+ }
37
+ base[:width] = base[:height] = @size.to_s if @size
38
+ if icon[:mode] == :stroke
39
+ base.merge(
40
+ fill: "none",
41
+ stroke: "currentColor",
42
+ "stroke-width": icon[:stroke_width].to_s,
43
+ "stroke-linecap": "round",
44
+ "stroke-linejoin": "round"
45
+ )
46
+ else
47
+ base.merge(fill: "currentColor")
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,25 @@
1
+ /* Co-located with marker.rb — UI::Marker, shadcn parity: muted inline status,
2
+ bordered system-note row, or labeled separator in a conversation. */
3
+ .pk-marker {
4
+ display: flex;
5
+ align-items: center;
6
+ gap: .5rem;
7
+ min-height: 1.25rem;
8
+ color: var(--pk-muted);
9
+ font-size: .875rem;
10
+ }
11
+ .pk-marker-icon { display: inline-flex; flex: none; }
12
+ .pk-marker-icon svg { width: 1rem; height: 1rem; }
13
+ .pk-marker-content { min-width: 0; }
14
+ .pk-marker.border {
15
+ border: 1px solid var(--pk-border);
16
+ border-radius: var(--pk-radius);
17
+ padding: .5rem .625rem;
18
+ }
19
+ .pk-marker.separator { justify-content: center; }
20
+ .pk-marker.separator::before, .pk-marker.separator::after {
21
+ content: "";
22
+ flex: 1;
23
+ height: 1px;
24
+ background: var(--pk-border);
25
+ }
@@ -0,0 +1,18 @@
1
+ module PhlexKit
2
+ # Conversation annotation, ported from shadcn/ui's Marker (an AI-chat-era
3
+ # addition): an inline status line, bordered system-note row, or labeled
4
+ # separator. Compose Marker(variant:) > [MarkerIcon +] MarkerContent.
5
+ # `.pk-marker*` (marker.css).
6
+ class Marker < BaseComponent
7
+ VARIANTS = { default: nil, border: "border", separator: "separator" }.freeze
8
+
9
+ def initialize(variant: :default, **attrs)
10
+ @variant = variant.to_sym
11
+ @attrs = attrs
12
+ end
13
+
14
+ def view_template(&)
15
+ div(**mix({ class: [ "pk-marker", VARIANTS.fetch(@variant) ].compact.join(" ") }, @attrs), &)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ module PhlexKit
2
+ # Text of a Marker. See marker.rb.
3
+ class MarkerContent < BaseComponent
4
+ def initialize(**attrs) = (@attrs = attrs)
5
+ def view_template(&) = span(**mix({ class: "pk-marker-content" }, @attrs), &)
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module PhlexKit
2
+ # 16px muted icon slot of a Marker. See marker.rb.
3
+ class MarkerIcon < BaseComponent
4
+ def initialize(**attrs) = (@attrs = attrs)
5
+ def view_template(&) = span(**mix({ class: "pk-marker-icon" }, @attrs), &)
6
+ end
7
+ end
@@ -17,16 +17,7 @@ module PhlexKit
17
17
  private
18
18
 
19
19
  def chevron
20
- svg(
21
- xmlns: "http://www.w3.org/2000/svg",
22
- viewbox: "0 0 24 24",
23
- fill: "none",
24
- stroke: "currentColor",
25
- "stroke-width": "2",
26
- "stroke-linecap": "round",
27
- "stroke-linejoin": "round",
28
- "aria-hidden": "true"
29
- ) { |s| s.path(d: "m6 9 6 6 6-6") }
20
+ render Icon.new(:chevron_down, size: nil)
30
21
  end
31
22
  end
32
23
  end
@@ -17,11 +17,7 @@ module PhlexKit
17
17
  private
18
18
 
19
19
  def chevron
20
- svg(xmlns: "http://www.w3.org/2000/svg", viewbox: "0 0 24 24", fill: "none",
21
- stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round",
22
- "stroke-linejoin": "round", class: "pk-navigation-menu-chevron", "aria-hidden": "true") do |s|
23
- s.path(d: "m6 9 6 6 6-6")
24
- end
20
+ render Icon.new(:chevron_down, size: nil, class: "pk-navigation-menu-chevron")
25
21
  end
26
22
  end
27
23
  end
@@ -4,11 +4,7 @@ module PhlexKit
4
4
  def view_template
5
5
  li do
6
6
  span(**mix({ class: "pk-pagination-ellipsis", aria: { hidden: true } }, @attrs)) do
7
- svg(xmlns: "http://www.w3.org/2000/svg", viewbox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_linejoin: "round") do |s|
8
- s.circle(cx: "12", cy: "12", r: "1")
9
- s.circle(cx: "19", cy: "12", r: "1")
10
- s.circle(cx: "5", cy: "12", r: "1")
11
- end
7
+ render Icon.new(:ellipsis, size: nil)
12
8
  span(class: "pk-sr-only") { "More pages" }
13
9
  end
14
10
  end
@@ -36,17 +36,7 @@ module PhlexKit
36
36
  private
37
37
 
38
38
  def check_icon
39
- svg(
40
- xmlns: "http://www.w3.org/2000/svg",
41
- viewbox: "0 0 24 24",
42
- fill: "none",
43
- stroke: "currentColor",
44
- "stroke-width": "2",
45
- "stroke-linecap": "round",
46
- "stroke-linejoin": "round",
47
- class: "pk-select-item-check",
48
- "aria-hidden": "true"
49
- ) { |s| s.path(d: "M20 6 9 17l-5-5") }
39
+ render Icon.new(:check, size: nil, class: "pk-select-item-check")
50
40
  end
51
41
  end
52
42
  end
@@ -23,20 +23,7 @@ module PhlexKit
23
23
  private
24
24
 
25
25
  def icon
26
- svg(
27
- xmlns: "http://www.w3.org/2000/svg",
28
- viewbox: "0 0 24 24",
29
- fill: "none",
30
- stroke: "currentColor",
31
- "stroke-width": "2",
32
- "stroke-linecap": "round",
33
- "stroke-linejoin": "round",
34
- class: "pk-select-trigger-icon",
35
- "aria-hidden": "true"
36
- ) do |s|
37
- s.path(d: "m7 15 5 5 5-5")
38
- s.path(d: "m7 9 5-5 5 5")
39
- end
26
+ render Icon.new(:chevrons_up_down, size: nil, class: "pk-select-trigger-icon")
40
27
  end
41
28
  end
42
29
  end
@@ -1,6 +1,6 @@
1
1
  /* Co-located with sheet.rb. Theme tokens from the global stylesheet. */
2
2
  .pk-sheet-trigger { display: inline-block; }
3
- .pk-sheet-backdrop { position: fixed; inset: 0; z-index: 50; background: rgb(0 0 0 / .5); backdrop-filter: blur(2px); }
3
+ .pk-sheet-backdrop { position: fixed; inset: 0; z-index: 50; background: rgb(0 0 0 / .5); backdrop-filter: blur(8px); }
4
4
  .pk-sheet-content {
5
5
  position: fixed; z-index: 50; display: flex; flex-direction: column; gap: 1rem;
6
6
  background: var(--pk-surface); color: var(--pk-text); padding: 1.5rem;
@@ -12,9 +12,7 @@ module PhlexKit
12
12
  div(**mix({ class: ["pk-sheet-content", SIDES.fetch(@side)].join(" ") }, @attrs)) do
13
13
  block&.call
14
14
  button(type: "button", class: "pk-overlay-close", data: { action: "click->phlex-kit--sheet-content#close" }) do
15
- svg(width: "15", height: "15", viewbox: "0 0 15 15", fill: "none", xmlns: "http://www.w3.org/2000/svg") do |s|
16
- s.path(fill_rule: "evenodd", clip_rule: "evenodd", fill: "currentColor", d: "M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z")
17
- end
15
+ render Icon.new(:x, size: 15)
18
16
  span(class: "pk-sr-only") { "Close" }
19
17
  end
20
18
  end
@@ -1,23 +1,59 @@
1
1
  /* Co-located with sidebar.rb — UI::Sidebar (+ Wrapper / Header / Content / Footer
2
- / Group / Menu / MenuItem / MenuButton / Inset). ruby_ui's structure at
3
- collapsible:none, Tailwind vanilla CSS on the palette tokens. The collapsible/
4
- offcanvas/icon modes (+ their Stimulus/cookie/Sheet) are deliberately not ported.
5
- Theme tokens come from the global stylesheet. */
2
+ / Group / GroupLabel / GroupContent / GroupAction / Menu / MenuItem / MenuButton
3
+ / MenuAction / MenuBadge / MenuSkeleton / MenuSub / MenuSubItem / MenuSubButton
4
+ / Separator / Input / Inset). ruby_ui's structure at collapsible:none, extended
5
+ to shadcn/ui's static part set; Tailwind → vanilla CSS on the palette tokens.
6
+ The collapsible/offcanvas/icon modes (+ their trigger/rail/Stimulus/cookie/
7
+ Sheet) are deliberately not ported.
8
+ Theme tokens come from the global stylesheet. The sidebar reads the shared
9
+ palette by default; --pk-sidebar[-text|-accent|-border|-primary|-primary-ink]
10
+ are optional per-role overrides (shadcn sidebar-* parity), undefined by
11
+ default. shadcn's sidebar-ring / sidebar-accent-foreground have no consumption
12
+ site here and stay unmapped. */
6
13
  .pk-sidebar-wrapper { display: flex; min-height: 100dvh; width: 100%; }
7
14
 
8
15
  .pk-sidebar {
9
16
  display: flex;
10
17
  flex-direction: column;
11
- width: 14rem;
18
+ width: 16rem;
12
19
  flex-shrink: 0;
13
- background: var(--pk-surface);
14
- border-right: 1px solid var(--pk-border);
20
+ background: var(--pk-sidebar, var(--pk-surface));
21
+ border-right: 1px solid var(--pk-sidebar-border, var(--pk-border));
15
22
  }
16
23
 
17
24
  .pk-sidebar-header { display: flex; flex-direction: column; gap: .5rem; padding: 1rem .75rem .25rem; }
18
25
  .pk-sidebar-content { display: flex; flex-direction: column; gap: .25rem; flex: 1; min-height: 0; overflow: auto; padding: .5rem .75rem; }
19
26
  .pk-sidebar-footer { display: flex; flex-direction: column; gap: .5rem; padding: .75rem; }
20
- .pk-sidebar-group { display: flex; flex-direction: column; gap: .125rem; }
27
+ .pk-sidebar-group { position: relative; display: flex; flex-direction: column; gap: .125rem; }
28
+ .pk-sidebar-group-label {
29
+ display: flex;
30
+ align-items: center;
31
+ flex-shrink: 0;
32
+ height: 2rem;
33
+ padding: 0 .5rem;
34
+ border-radius: calc(var(--pk-radius) - 2px);
35
+ font-size: .75rem;
36
+ font-weight: 500;
37
+ color: color-mix(in oklab, var(--pk-sidebar-text, var(--pk-text)) 70%, transparent);
38
+ }
39
+ .pk-sidebar-group-content { width: 100%; font-size: .875rem; }
40
+ .pk-sidebar-group-action {
41
+ position: absolute;
42
+ top: .375rem;
43
+ right: .25rem;
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ aspect-ratio: 1;
48
+ width: 1.25rem;
49
+ padding: 0;
50
+ border: 0;
51
+ border-radius: calc(var(--pk-radius) - 2px);
52
+ background: transparent;
53
+ color: var(--pk-sidebar-text, var(--pk-text));
54
+ cursor: pointer;
55
+ }
56
+ .pk-sidebar-group-action:hover { background: var(--pk-sidebar-accent, var(--pk-accent)); }
21
57
 
22
58
  .pk-sidebar-menu { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .125rem; width: 100%; }
23
59
  .pk-sidebar-menu-item { position: relative; }
@@ -30,13 +66,95 @@
30
66
  border: 0;
31
67
  border-radius: 8px;
32
68
  background: transparent;
33
- color: var(--pk-text);
69
+ color: var(--pk-sidebar-text, var(--pk-text));
34
70
  font: inherit;
35
71
  font-size: .9rem;
36
72
  text-align: left;
73
+ text-decoration: none;
37
74
  cursor: pointer;
38
75
  }
39
- .pk-sidebar-menu-button:hover { background: var(--pk-accent); text-decoration: none; }
40
- .pk-sidebar-menu-button[data-active="true"] { background: var(--pk-brand); color: var(--pk-brand-ink); font-weight: 600; }
76
+ .pk-sidebar-menu-button:hover { background: var(--pk-sidebar-accent, var(--pk-accent)); text-decoration: none; }
77
+ /* Active item: shadcn's stock "Default" menu look (accent tint). The create
78
+ page's "Solid" mode is the .menu-solid modifier on the sidebar root. */
79
+ .pk-sidebar-menu-button[data-active="true"] { background: var(--pk-sidebar-accent, var(--pk-accent)); color: var(--pk-sidebar-text, var(--pk-text)); font-weight: 500; }
80
+ .pk-sidebar.menu-solid .pk-sidebar-menu-button[data-active="true"] { background: var(--pk-sidebar-primary, var(--pk-brand)); color: var(--pk-sidebar-primary-ink, var(--pk-brand-ink)); font-weight: 600; }
81
+
82
+ /* Anchored to the top of the item (the button row) — the item may also
83
+ contain a submenu, so 50%-of-item would drift. */
84
+ .pk-sidebar-menu-action {
85
+ position: absolute;
86
+ top: .5rem;
87
+ right: .25rem;
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ aspect-ratio: 1;
92
+ width: 1.25rem;
93
+ padding: 0;
94
+ border: 0;
95
+ border-radius: calc(var(--pk-radius) - 2px);
96
+ background: transparent;
97
+ color: var(--pk-sidebar-text, var(--pk-text));
98
+ cursor: pointer;
99
+ }
100
+ .pk-sidebar-menu-action:hover { background: var(--pk-sidebar-accent, var(--pk-accent)); }
101
+ .pk-sidebar-menu-badge {
102
+ pointer-events: none;
103
+ user-select: none;
104
+ position: absolute;
105
+ top: .5rem;
106
+ right: .25rem;
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ height: 1.25rem;
111
+ min-width: 1.25rem;
112
+ padding: 0 .25rem;
113
+ border-radius: calc(var(--pk-radius) - 2px);
114
+ font-size: .75rem;
115
+ font-weight: 500;
116
+ font-variant-numeric: tabular-nums;
117
+ color: var(--pk-sidebar-text, var(--pk-text));
118
+ }
119
+ .pk-sidebar-menu-skeleton { display: flex; align-items: center; gap: .5rem; height: 2rem; padding: 0 .5rem; border-radius: calc(var(--pk-radius) - 2px); }
120
+ .pk-sidebar-menu-skeleton .pk-skeleton.icon { width: 1rem; height: 1rem; flex-shrink: 0; }
121
+ .pk-sidebar-menu-skeleton .pk-skeleton.text { height: 1rem; flex: 1; max-width: var(--pk-skeleton-width, 70%); }
122
+
123
+ .pk-sidebar-menu-sub {
124
+ list-style: none;
125
+ display: flex;
126
+ flex-direction: column;
127
+ gap: .25rem;
128
+ min-width: 0;
129
+ margin: 0 .875rem;
130
+ padding: .125rem .625rem;
131
+ border-left: 1px solid var(--pk-sidebar-border, var(--pk-border));
132
+ }
133
+ .pk-sidebar-menu-sub-item { position: relative; }
134
+ .pk-sidebar-menu-sub-button {
135
+ display: flex;
136
+ align-items: center;
137
+ gap: .5rem;
138
+ height: 1.75rem;
139
+ min-width: 0;
140
+ width: 100%;
141
+ padding: 0 .5rem;
142
+ overflow: hidden;
143
+ border: 0;
144
+ border-radius: calc(var(--pk-radius) - 2px);
145
+ background: transparent;
146
+ color: var(--pk-sidebar-text, var(--pk-text));
147
+ font: inherit;
148
+ font-size: .875rem;
149
+ text-align: left;
150
+ text-decoration: none;
151
+ cursor: pointer;
152
+ }
153
+ .pk-sidebar-menu-sub-button:hover { background: var(--pk-sidebar-accent, var(--pk-accent)); text-decoration: none; }
154
+ /* shadcn highlights active sub-items with the accent role, not primary. */
155
+ .pk-sidebar-menu-sub-button[data-active="true"] { background: var(--pk-sidebar-accent, var(--pk-accent)); font-weight: 600; }
156
+
157
+ .pk-separator.pk-sidebar-separator { width: auto; margin: 0 .5rem; background: var(--pk-sidebar-border, var(--pk-border)); }
158
+ .pk-input.pk-sidebar-input { height: 2rem; width: 100%; background: var(--pk-bg); box-shadow: none; }
41
159
 
42
160
  .pk-sidebar-inset { flex: 1; min-width: 0; }
@@ -3,17 +3,30 @@ module PhlexKit
3
3
  # (the NonCollapsibleSidebar path): a static flex-column rail. Deliberately
4
4
  # WITHOUT ruby_ui's collapsible/offcanvas Stimulus controller + cookie state +
5
5
  # mobile Sheet (the admin sidebar is static) — those can be added later if a
6
- # collapsible chrome is wanted. Compose with SidebarWrapper (the page root) +
7
- # SidebarHeader / SidebarContent / SidebarMenu (+ Item / Button) / SidebarFooter,
8
- # alongside a SidebarInset for the main content. Tailwind vanilla `.pk-sidebar*`
9
- # (sidebar.css).
6
+ # collapsible chrome is wanted. The static part set matches shadcn/ui's Sidebar:
7
+ # compose with SidebarWrapper (the page root) + SidebarHeader / SidebarContent /
8
+ # SidebarGroup (+ Label / Content / Action) / SidebarMenu (+ Item / Button /
9
+ # Action / Badge / Skeleton / Sub / SubItem / SubButton) / SidebarSeparator /
10
+ # SidebarInput / SidebarFooter, alongside a SidebarInset for the main content.
11
+ # Tailwind → vanilla `.pk-sidebar*` (sidebar.css).
10
12
  class Sidebar < BaseComponent
11
- def initialize(**attrs)
13
+ # Active-item treatment (ui.shadcn.com/create's "Menu" option): :default is
14
+ # shadcn's stock accent tint; :solid fills with the primary/brand role.
15
+ MENUS = { default: nil, solid: "menu-solid" }.freeze
16
+
17
+ def initialize(menu: :default, **attrs)
18
+ @menu = menu.to_sym
12
19
  @attrs = attrs
13
20
  end
14
21
 
15
22
  def view_template(&block)
16
- div(**mix({ class: "pk-sidebar" }, @attrs), &block)
23
+ div(**mix({ class: classes }, @attrs), &block)
24
+ end
25
+
26
+ private
27
+
28
+ def classes
29
+ [ "pk-sidebar", MENUS.fetch(@menu) ].compact.join(" ")
17
30
  end
18
31
  end
19
32
  end
@@ -0,0 +1,14 @@
1
+ module PhlexKit
2
+ # Action button pinned to the top-right of a SidebarGroup (the group is the
3
+ # positioning context), ported from shadcn/ui's SidebarGroupAction.
4
+ # See sidebar.rb.
5
+ class SidebarGroupAction < BaseComponent
6
+ def initialize(**attrs)
7
+ @attrs = attrs
8
+ end
9
+
10
+ def view_template(&block)
11
+ button(**mix({ class: "pk-sidebar-group-action", type: "button" }, @attrs), &block)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module PhlexKit
2
+ # Content container within a SidebarGroup, ported from shadcn/ui's
3
+ # SidebarGroupContent. See sidebar.rb.
4
+ class SidebarGroupContent < BaseComponent
5
+ def initialize(**attrs)
6
+ @attrs = attrs
7
+ end
8
+
9
+ def view_template(&block)
10
+ div(**mix({ class: "pk-sidebar-group-content" }, @attrs), &block)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module PhlexKit
2
+ # Heading for a SidebarGroup, ported from shadcn/ui's SidebarGroupLabel.
3
+ # See sidebar.rb.
4
+ class SidebarGroupLabel < BaseComponent
5
+ def initialize(**attrs)
6
+ @attrs = attrs
7
+ end
8
+
9
+ def view_template(&block)
10
+ div(**mix({ class: "pk-sidebar-group-label" }, @attrs), &block)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module PhlexKit
2
+ # Search/filter input for the sidebar, ported from shadcn/ui's SidebarInput:
3
+ # the kit Input on the page background, sized to the sidebar's control height.
4
+ # See sidebar.rb.
5
+ class SidebarInput < BaseComponent
6
+ def initialize(**attrs)
7
+ @attrs = attrs
8
+ end
9
+
10
+ def view_template
11
+ render Input.new(**mix({ class: "pk-sidebar-input" }, @attrs))
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module PhlexKit
2
+ # Secondary action pinned to the right edge of a SidebarMenuItem (the item is
3
+ # position:relative), ported from shadcn/ui's SidebarMenuAction. See sidebar.rb.
4
+ class SidebarMenuAction < BaseComponent
5
+ def initialize(**attrs)
6
+ @attrs = attrs
7
+ end
8
+
9
+ def view_template(&block)
10
+ button(**mix({ class: "pk-sidebar-menu-action", type: "button" }, @attrs), &block)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module PhlexKit
2
+ # Non-interactive count/status pinned to the right edge of a SidebarMenuItem,
3
+ # ported from shadcn/ui's SidebarMenuBadge. See sidebar.rb.
4
+ class SidebarMenuBadge < BaseComponent
5
+ def initialize(**attrs)
6
+ @attrs = attrs
7
+ end
8
+
9
+ def view_template(&block)
10
+ span(**mix({ class: "pk-sidebar-menu-badge" }, @attrs), &block)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module PhlexKit
2
+ # Loading placeholder for a menu item, ported from shadcn/ui's
3
+ # SidebarMenuSkeleton: an optional icon shimmer plus a text shimmer whose
4
+ # width randomises 50–90% per render (their --skeleton-width trick).
5
+ # `show_icon:` mirrors shadcn's showIcon. See sidebar.rb.
6
+ class SidebarMenuSkeleton < BaseComponent
7
+ def initialize(show_icon: false, **attrs)
8
+ @show_icon = show_icon
9
+ @attrs = attrs
10
+ end
11
+
12
+ def view_template
13
+ div(**mix({ class: "pk-sidebar-menu-skeleton" }, @attrs)) do
14
+ render Skeleton.new(class: "icon") if @show_icon
15
+ render Skeleton.new(class: "text", style: "--pk-skeleton-width: #{rand(50..90)}%;")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ module PhlexKit
2
+ # Nested submenu list under a SidebarMenuItem, ported from shadcn/ui's
3
+ # SidebarMenuSub. See sidebar.rb.
4
+ class SidebarMenuSub < BaseComponent
5
+ def initialize(**attrs)
6
+ @attrs = attrs
7
+ end
8
+
9
+ def view_template(&block)
10
+ ul(**mix({ class: "pk-sidebar-menu-sub" }, @attrs), &block)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ module PhlexKit
2
+ # The interactive element of a SidebarMenuSubItem, ported from shadcn/ui's
3
+ # SidebarMenuSubButton. `as:` is :a (their default — pass `href:`) or :button;
4
+ # `active: true` highlights with the accent role (shadcn subs use accent, not
5
+ # primary). Kwarg rather than attr for the same mix-merge reason as
6
+ # SidebarMenuButton. See sidebar.rb.
7
+ class SidebarMenuSubButton < BaseComponent
8
+ def initialize(as: :a, active: false, **attrs)
9
+ @as = as
10
+ @active = active
11
+ @attrs = attrs
12
+ end
13
+
14
+ def view_template(&block)
15
+ send(@as, **mix({ class: "pk-sidebar-menu-sub-button", "data-active": (@active ? "true" : "false") }, @attrs), &block)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ module PhlexKit
2
+ # An item within a SidebarMenuSub, ported from shadcn/ui's SidebarMenuSubItem.
3
+ # See sidebar.rb.
4
+ class SidebarMenuSubItem < BaseComponent
5
+ def initialize(**attrs)
6
+ @attrs = attrs
7
+ end
8
+
9
+ def view_template(&block)
10
+ li(**mix({ class: "pk-sidebar-menu-sub-item" }, @attrs), &block)
11
+ end
12
+ end
13
+ end