phlex_kit 0.2.1 → 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 +81 -6
  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
@@ -1,12 +1,15 @@
1
1
  module PhlexKit
2
2
  class AccordionItem < BaseComponent
3
- def initialize(open: false, rotate_icon: 180, **attrs)
3
+ def initialize(open: false, disabled: false, rotate_icon: 180, **attrs)
4
4
  @open = open
5
+ @disabled = disabled
5
6
  @rotate_icon = rotate_icon
6
7
  @attrs = attrs
7
8
  end
8
9
  def view_template(&)
9
- div(**mix({ class: "pk-accordion-item", data: { controller: "phlex-kit--accordion", phlex_kit__accordion_open_value: @open, phlex_kit__accordion_rotate_icon_value: @rotate_icon } }, @attrs), &)
10
+ data = { controller: "phlex-kit--accordion", phlex_kit__accordion_open_value: @open, phlex_kit__accordion_rotate_icon_value: @rotate_icon }
11
+ data[:disabled] = "" if @disabled
12
+ div(**mix({ class: "pk-accordion-item", data: data }, @attrs), &)
10
13
  end
11
14
  end
12
15
  end
@@ -8,8 +8,8 @@
8
8
  position: fixed;
9
9
  inset: 0;
10
10
  z-index: 50;
11
- background: rgba(0, 0, 0, .8);
12
- backdrop-filter: blur(2px);
11
+ background: rgb(0 0 0 / .5);
12
+ backdrop-filter: blur(8px);
13
13
  }
14
14
  .pk-alert-dialog-panel {
15
15
  position: fixed;
@@ -28,10 +28,51 @@
28
28
  border-radius: var(--pk-radius);
29
29
  background: var(--pk-surface);
30
30
  color: var(--pk-text);
31
- padding: 1.5rem;
31
+ --pk-dialog-spacing: 1.5rem;
32
+ padding: var(--pk-dialog-spacing);
32
33
  box-shadow: 0 10px 40px rgba(0, 0, 0, .5);
33
34
  }
35
+ .pk-alert-dialog-panel.sm { max-width: 24rem; --pk-dialog-spacing: 1rem; gap: .75rem; }
36
+ .pk-alert-dialog-panel.sm .pk-alert-dialog-title { font-size: 1rem; }
37
+
34
38
  .pk-alert-dialog-header { display: flex; flex-direction: column; gap: .5rem; }
39
+
40
+ /* Media tile: a rounded-square icon chip; a header containing one becomes a
41
+ centered stack (shadcn's media layout). */
42
+ .pk-alert-dialog-media {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ flex: none;
47
+ width: 2.75rem;
48
+ height: 2.75rem;
49
+ border-radius: var(--pk-radius);
50
+ background: var(--pk-surface-2);
51
+ color: var(--pk-text);
52
+ margin-bottom: .25rem;
53
+ font-size: 1.25rem;
54
+ }
55
+ .pk-alert-dialog-media svg { width: 1.25rem; height: 1.25rem; }
56
+ .pk-alert-dialog-media img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
57
+ .pk-alert-dialog-header:has(.pk-alert-dialog-media) {
58
+ align-items: center;
59
+ text-align: center;
60
+ }
61
+ /* :sm dialogs pair the centered stack with a full-width 50/50 footer. */
62
+ .pk-alert-dialog-panel.sm .pk-alert-dialog-footer {
63
+ display: grid;
64
+ grid-template-columns: 1fr 1fr;
65
+ }
35
66
  .pk-alert-dialog-title { margin: 0; font-size: 1.125rem; font-weight: 600; }
36
67
  .pk-alert-dialog-description { margin: 0; font-size: .875rem; color: var(--pk-muted); }
37
- .pk-alert-dialog-footer { display: flex; justify-content: flex-end; gap: .5rem; flex-wrap: wrap; }
68
+ /* Footer: shadcn's full-bleed muted band (escapes the panel padding). */
69
+ .pk-alert-dialog-footer {
70
+ display: flex;
71
+ justify-content: flex-end;
72
+ gap: .5rem;
73
+ flex-wrap: wrap;
74
+ margin: 0 calc(-1 * var(--pk-dialog-spacing)) calc(-1 * var(--pk-dialog-spacing));
75
+ padding: var(--pk-dialog-spacing);
76
+ border-top: 1px solid var(--pk-border);
77
+ background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent);
78
+ }
@@ -3,12 +3,13 @@ module PhlexKit
3
3
  # non-GET destructive action, use a `button_to` in the footer instead, so it
4
4
  # submits a real form.) See alert_dialog.rb.
5
5
  class AlertDialogAction < BaseComponent
6
- def initialize(**attrs)
6
+ def initialize(variant: :primary, **attrs)
7
+ @variant = variant
7
8
  @attrs = attrs
8
9
  end
9
10
 
10
11
  def view_template(&block)
11
- render PhlexKit::Button.new(variant: :primary, **@attrs, &block)
12
+ render PhlexKit::Button.new(variant: @variant, **@attrs, &block)
12
13
  end
13
14
  end
14
15
  end
@@ -3,7 +3,11 @@ module PhlexKit
3
3
  # cloned div carries its own `phlex-kit--alert-dialog` controller so Cancel
4
4
  # (#dismiss) can remove it. Holds Header + Footer. See alert_dialog.rb.
5
5
  class AlertDialogContent < BaseComponent
6
- def initialize(**attrs)
6
+ # size => panel modifier; :sm is shadcn's compact variant.
7
+ SIZES = { default: nil, sm: "sm" }.freeze
8
+
9
+ def initialize(size: :default, **attrs)
10
+ @size = size.to_sym
7
11
  @attrs = attrs
8
12
  end
9
13
 
@@ -11,7 +15,7 @@ module PhlexKit
11
15
  template(**mix({ data: { phlex_kit__alert_dialog_target: "content" } }, @attrs)) do
12
16
  div(data: { controller: "phlex-kit--alert-dialog" }) do
13
17
  div(class: "pk-alert-dialog-overlay", "aria-hidden": "true")
14
- div(role: "alertdialog", "aria-modal": "true", class: "pk-alert-dialog-panel", &block)
18
+ div(role: "alertdialog", "aria-modal": "true", class: [ "pk-alert-dialog-panel", SIZES.fetch(@size) ].compact.join(" "), &block)
15
19
  end
16
20
  end
17
21
  end
@@ -0,0 +1,14 @@
1
+ module PhlexKit
2
+ # Icon/image tile in an AlertDialogHeader — a muted circle above the title
3
+ # (beside the text column in size :sm dialogs). Ported from shadcn/ui's
4
+ # AlertDialogMedia (newer than ruby_ui's alert dialog). See alert_dialog.rb.
5
+ class AlertDialogMedia < BaseComponent
6
+ def initialize(**attrs)
7
+ @attrs = attrs
8
+ end
9
+
10
+ def view_template(&)
11
+ div(**mix({ class: "pk-alert-dialog-media", aria: { hidden: "true" } }, @attrs), &)
12
+ end
13
+ end
14
+ end
@@ -1,6 +1,10 @@
1
1
  /* Co-located with attachment.rb — UI::Attachment, shadcn parity: bordered
2
- file chip (icon/preview tile, name + meta, trailing actions). */
2
+ file chip (icon/image media, name + meta, trailing actions), upload states
3
+ (uploading/processing shimmer the title; error goes destructive), three
4
+ sizes, vertical orientation, a full-card trigger behind the actions, and a
5
+ scrollable snapping group with an edge fade. */
3
6
  .pk-attachment {
7
+ position: relative;
4
8
  display: flex;
5
9
  align-items: center;
6
10
  gap: .5rem;
@@ -39,7 +43,14 @@
39
43
  text-overflow: ellipsis;
40
44
  }
41
45
  .pk-attachment-description { color: var(--pk-muted); font-size: .75rem; }
42
- .pk-attachment-actions { display: flex; align-items: center; gap: .25rem; flex: none; }
46
+ .pk-attachment-actions {
47
+ position: relative; /* above the full-card trigger */
48
+ z-index: 2;
49
+ display: flex;
50
+ align-items: center;
51
+ gap: .25rem;
52
+ flex: none;
53
+ }
43
54
  .pk-attachment-action {
44
55
  display: inline-flex;
45
56
  align-items: center;
@@ -60,3 +71,92 @@
60
71
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--pk-ring) 50%, transparent);
61
72
  }
62
73
  .pk-attachment-action svg { width: .875rem; height: .875rem; }
74
+
75
+ /* Sizes. */
76
+ .pk-attachment.sm { padding: .375rem; gap: .375rem; }
77
+ .pk-attachment.sm .pk-attachment-media { width: 2rem; height: 2rem; }
78
+ .pk-attachment.xs { padding: .25rem .375rem; gap: .375rem; font-size: .8125rem; }
79
+ .pk-attachment.xs .pk-attachment-media { width: 1.5rem; height: 1.5rem; border-radius: calc(var(--pk-radius) - 4px); }
80
+
81
+ /* Vertical: media stacks above the content (image-preview cards). */
82
+ .pk-attachment.vertical {
83
+ flex-direction: column;
84
+ align-items: stretch;
85
+ width: 10rem;
86
+ }
87
+ .pk-attachment.vertical .pk-attachment-media.image {
88
+ width: 100%;
89
+ height: auto;
90
+ aspect-ratio: 4 / 3;
91
+ }
92
+ .pk-attachment.vertical .pk-attachment-actions {
93
+ position: absolute;
94
+ top: .75rem;
95
+ right: .75rem;
96
+ }
97
+ .pk-attachment.vertical .pk-attachment-action {
98
+ background: color-mix(in oklab, var(--pk-bg) 60%, transparent);
99
+ color: var(--pk-text);
100
+ backdrop-filter: blur(4px);
101
+ }
102
+
103
+ /* Upload states: uploading/processing shimmer the title (same recipe as
104
+ .pk-shimmer); error goes destructive. */
105
+ .pk-attachment[data-state="uploading"] .pk-attachment-title,
106
+ .pk-attachment[data-state="processing"] .pk-attachment-title {
107
+ --pk-shimmer-color: oklch(from currentColor calc(l + .25) c h);
108
+ background-image:
109
+ linear-gradient(110deg,
110
+ transparent calc(50% - (3ch + 40px) / 2),
111
+ var(--pk-shimmer-color) 50%,
112
+ transparent calc(50% + (3ch + 40px) / 2)),
113
+ linear-gradient(currentColor, currentColor);
114
+ background-size: 250% 100%, auto;
115
+ background-repeat: no-repeat;
116
+ -webkit-background-clip: text;
117
+ background-clip: text;
118
+ -webkit-text-fill-color: transparent;
119
+ animation: pk-shimmer 2s linear infinite;
120
+ }
121
+ .pk-attachment[data-state="error"] { border-color: color-mix(in oklab, var(--pk-red) 40%, transparent); }
122
+ .pk-attachment[data-state="error"] .pk-attachment-media { color: var(--pk-red); background: color-mix(in oklab, var(--pk-red) 12%, transparent); }
123
+ .pk-attachment[data-state="error"] .pk-attachment-description { color: var(--pk-red); }
124
+
125
+ /* Full-card trigger — behind the actions, focus ring on the card. */
126
+ .pk-attachment-trigger {
127
+ position: absolute;
128
+ inset: 0;
129
+ z-index: 1;
130
+ border: 0;
131
+ border-radius: inherit;
132
+ background: transparent;
133
+ cursor: pointer;
134
+ padding: 0;
135
+ }
136
+ .pk-attachment-trigger:focus-visible { outline: none; }
137
+ .pk-attachment:has(.pk-attachment-trigger:hover) { background: color-mix(in oklab, var(--pk-surface-2) 40%, var(--pk-surface)); }
138
+ .pk-attachment:has(.pk-attachment-trigger:focus-visible) {
139
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
140
+ }
141
+
142
+ /* Scrollable, snapping row with an edge fade (horizontal scroll-fade). */
143
+ .pk-attachment-group {
144
+ --pk-scroll-fade-size: 2rem;
145
+ display: flex;
146
+ gap: .5rem;
147
+ overflow-x: auto;
148
+ scroll-snap-type: x mandatory;
149
+ padding: 2px; /* room for card rings */
150
+ mask-image: linear-gradient(to right,
151
+ transparent 0, black var(--pk-scroll-fade-size),
152
+ black calc(100% - var(--pk-scroll-fade-size)), transparent 100%);
153
+ }
154
+ .pk-attachment-group[data-at-start] {
155
+ mask-image: linear-gradient(to right,
156
+ black calc(100% - var(--pk-scroll-fade-size)), transparent 100%);
157
+ }
158
+ .pk-attachment-group[data-at-end] {
159
+ mask-image: linear-gradient(to right, transparent 0, black var(--pk-scroll-fade-size));
160
+ }
161
+ .pk-attachment-group[data-at-start][data-at-end] { mask-image: none; }
162
+ .pk-attachment-group > .pk-attachment { flex: none; scroll-snap-align: start; }
@@ -1,16 +1,28 @@
1
1
  module PhlexKit
2
- # File chip, ported from shadcn/ui's Attachment (an AI-chat-era addition, not
3
- # in ruby_ui): a bordered row of AttachmentMedia (icon or image preview) +
4
- # AttachmentContent(AttachmentTitle + AttachmentDescription) +
5
- # AttachmentActions(AttachmentAction e.g. a remove ×).
2
+ # File chip, ported from shadcn/ui's Attachment: media (icon or image),
3
+ # name + metadata, optional actions, upload state and a full-card trigger.
4
+ # `state:` drives styling — :uploading/:processing shimmer the title, :error
5
+ # goes destructive; `size:` (:default/:sm/:xs) and `orientation:`
6
+ # (:horizontal/:vertical) shape the card. Lay several out in an
7
+ # AttachmentGroup (scrollable, snapping, edge-faded row).
6
8
  # `.pk-attachment*` (attachment.css).
7
9
  class Attachment < BaseComponent
8
- def initialize(**attrs)
10
+ STATES = { idle: "idle", uploading: "uploading", processing: "processing", error: "error", done: "done" }.freeze
11
+ SIZES = { default: nil, sm: "sm", xs: "xs" }.freeze
12
+ ORIENTATIONS = { horizontal: nil, vertical: "vertical" }.freeze
13
+
14
+ def initialize(state: :done, size: :default, orientation: :horizontal, **attrs)
15
+ @state = state.to_sym
16
+ @size = size.to_sym
17
+ @orientation = orientation.to_sym
9
18
  @attrs = attrs
10
19
  end
11
20
 
12
21
  def view_template(&)
13
- div(**mix({ class: "pk-attachment" }, @attrs), &)
22
+ div(**mix({
23
+ class: [ "pk-attachment", SIZES.fetch(@size), ORIENTATIONS.fetch(@orientation) ].compact.join(" "),
24
+ data: { state: STATES.fetch(@state) }
25
+ }, @attrs), &)
14
26
  end
15
27
  end
16
28
  end
@@ -11,12 +11,7 @@ module PhlexKit
11
11
  if block
12
12
  yield
13
13
  else
14
- svg(xmlns: "http://www.w3.org/2000/svg", viewbox: "0 0 24 24", fill: "none",
15
- stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round",
16
- "stroke-linejoin": "round", "aria-hidden": "true") do |s|
17
- s.path(d: "M18 6 6 18")
18
- s.path(d: "m6 6 12 12")
19
- end
14
+ render Icon.new(:x, size: nil)
20
15
  end
21
16
  end
22
17
  end
@@ -0,0 +1,21 @@
1
+ module PhlexKit
2
+ # Horizontally scrollable, snapping row of Attachments with an edge fade
3
+ # (the phlex-kit--scroll-fade controller in horizontal mode).
4
+ # See attachment.rb.
5
+ class AttachmentGroup < BaseComponent
6
+ def initialize(**attrs)
7
+ @attrs = attrs
8
+ end
9
+
10
+ def view_template(&)
11
+ div(**mix({
12
+ class: "pk-attachment-group",
13
+ role: "group",
14
+ data: {
15
+ controller: "phlex-kit--scroll-fade",
16
+ phlex_kit__scroll_fade_axis_value: "x"
17
+ }
18
+ }, @attrs), &)
19
+ end
20
+ end
21
+ end
@@ -1,8 +1,17 @@
1
1
  module PhlexKit
2
- # Square preview tile of an Attachment a file-type icon or an <img>.
3
- # See attachment.rb.
2
+ # Media slot of an Attachment: `variant: :icon` (default, a muted tile) or
3
+ # `:image` (render an <img> inside; fills the slot, full-width when the
4
+ # attachment is vertical). See attachment.rb.
4
5
  class AttachmentMedia < BaseComponent
5
- def initialize(**attrs) = (@attrs = attrs)
6
- def view_template(&) = div(**mix({ class: "pk-attachment-media" }, @attrs), &)
6
+ VARIANTS = { icon: nil, image: "image" }.freeze
7
+
8
+ def initialize(variant: :icon, **attrs)
9
+ @variant = variant.to_sym
10
+ @attrs = attrs
11
+ end
12
+
13
+ def view_template(&)
14
+ div(**mix({ class: [ "pk-attachment-media", VARIANTS.fetch(@variant) ].compact.join(" ") }, @attrs), &)
15
+ end
7
16
  end
8
17
  end
@@ -0,0 +1,22 @@
1
+ module PhlexKit
2
+ # Full-card overlay that activates the attachment (a preview link or dialog
3
+ # trigger). Sits BEHIND AttachmentActions in the stacking order, so actions
4
+ # stay independently clickable. Give it an aria-label; pass `as: :a` +
5
+ # `href:` for a link. See attachment.rb.
6
+ class AttachmentTrigger < BaseComponent
7
+ def initialize(as: :button, href: nil, **attrs)
8
+ @as = as.to_sym
9
+ @href = href
10
+ @attrs = attrs
11
+ end
12
+
13
+ def view_template
14
+ base = { class: "pk-attachment-trigger" }
15
+ if @as == :a
16
+ a(**mix(base.merge(href: @href), @attrs))
17
+ else
18
+ button(**mix(base.merge(type: :button), @attrs))
19
+ end
20
+ end
21
+ end
22
+ end
@@ -3,11 +3,7 @@ module PhlexKit
3
3
  def initialize(**attrs) = (@attrs = attrs)
4
4
  def view_template
5
5
  span(**mix({ class: "pk-breadcrumb-ellipsis", role: "presentation", aria: { hidden: true } }, @attrs)) do
6
- 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|
7
- s.circle(cx: "12", cy: "12", r: "1")
8
- s.circle(cx: "19", cy: "12", r: "1")
9
- s.circle(cx: "5", cy: "12", r: "1")
10
- end
6
+ render Icon.new(:ellipsis, size: nil)
11
7
  span(class: "pk-sr-only") { "More" }
12
8
  end
13
9
  end
@@ -3,7 +3,7 @@ module PhlexKit
3
3
  def initialize(**attrs) = (@attrs = attrs)
4
4
  def view_template(&block)
5
5
  li(**mix({ class: "pk-breadcrumb-separator", role: "presentation", aria: { hidden: true } }, @attrs)) do
6
- if block then yield else 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") { |s| s.path(d: "m9 18 6-6-6-6") } end
6
+ if block then yield else render(Icon.new(:chevron_right, size: nil)) end
7
7
  end
8
8
  end
9
9
  end
@@ -20,21 +20,7 @@ module PhlexKit
20
20
  private
21
21
 
22
22
  def icon
23
- svg(
24
- xmlns: "http://www.w3.org/2000/svg",
25
- width: "15",
26
- height: "15",
27
- viewbox: "0 0 15 15",
28
- fill: "none",
29
- "aria-hidden": "true"
30
- ) do |s|
31
- s.path(
32
- d: "M6.1584 3.13508C6.35985 2.94621 6.67627 2.95642 6.86514 3.15788L10.6151 7.15788C10.7954 7.3502 10.7954 7.64949 10.6151 7.84182L6.86514 11.8418C6.67627 12.0433 6.35985 12.0535 6.1584 11.8646C5.95694 11.6757 5.94673 11.3593 6.1356 11.1579L9.565 7.49985L6.1356 3.84182C5.94673 3.64036 5.95694 3.32394 6.1584 3.13508Z",
33
- fill: "currentColor",
34
- fill_rule: "evenodd",
35
- clip_rule: "evenodd"
36
- )
37
- end
23
+ render Icon.new(:chevron_right, size: 15)
38
24
  end
39
25
  end
40
26
  end
@@ -20,21 +20,7 @@ module PhlexKit
20
20
  private
21
21
 
22
22
  def icon
23
- svg(
24
- xmlns: "http://www.w3.org/2000/svg",
25
- width: "15",
26
- height: "15",
27
- viewbox: "0 0 15 15",
28
- fill: "none",
29
- "aria-hidden": "true"
30
- ) do |s|
31
- s.path(
32
- d: "M8.84182 3.13514C9.04327 3.32401 9.05348 3.64042 8.86462 3.84188L5.43521 7.49991L8.86462 11.1579C9.05348 11.3594 9.04327 11.6758 8.84182 11.8647C8.64036 12.0535 8.32394 12.0433 8.13508 11.8419L4.38508 7.84188C4.20477 7.64955 4.20477 7.35027 4.38508 7.15794L8.13508 3.15794C8.32394 2.95648 8.64036 2.94628 8.84182 3.13514Z",
33
- fill: "currentColor",
34
- fill_rule: "evenodd",
35
- clip_rule: "evenodd"
36
- )
37
- end
23
+ render Icon.new(:chevron_left, size: 15)
38
24
  end
39
25
  end
40
26
  end
@@ -1,15 +1,51 @@
1
1
  /* Co-located with card.rb — the UI::Card container kit (Card + Header / Title /
2
- Description / Content / Footer). Namespaced .pk-card* to avoid colliding with
3
- the dashboard stat-tile .card in application.css. Theme tokens come from the
4
- global stylesheet; translated from ruby_ui's Tailwind:
5
- rounded-xl border bg-background shadow → border + radius + surface
6
- flex flex-col space-y-1.5 p-6 → header column + gap + pad
7
- font-semibold leading-none tracking-tight → title
8
- text-sm text-muted-foreground → description
9
- p-6 pt-0 / items-center p-6 pt-0 → content / footer (top pad sits in header) */
10
- .pk-card { background: var(--pk-surface); border: 1px solid var(--pk-border); border-radius: .75rem; box-shadow: 0 1px 3px rgba(0, 0, 0, .3); }
11
- .pk-card-header { display: flex; flex-direction: column; gap: .375rem; padding: 1.5rem; }
12
- .pk-card-title { margin: 0; font-weight: 600; line-height: 1.1; letter-spacing: -.01em; }
13
- .pk-card-description { margin: 0; font-size: .85rem; color: var(--pk-muted); }
14
- .pk-card-content { padding: 0 1.5rem 1.5rem; }
15
- .pk-card-footer { display: flex; align-items: center; gap: .5rem; padding: 0 1.5rem 1.5rem; }
2
+ Description / Action / Content / Footer), matching shadcn/ui's current card:
3
+ every gap and inset derives from --pk-card-spacing (1rem; .75rem for
4
+ size :sm or set it inline per card), the header becomes a 1fr/auto grid
5
+ when a CardAction is present, the footer is a full-bleed muted band, and a
6
+ leading <img> child runs edge-to-edge. Theme tokens come from the global
7
+ stylesheet. */
8
+ .pk-card {
9
+ --pk-card-spacing: 1rem;
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: var(--pk-card-spacing);
13
+ overflow: hidden;
14
+ border: 1px solid var(--pk-border);
15
+ border-radius: calc(var(--pk-radius) + 4px);
16
+ background: var(--pk-surface);
17
+ color: var(--pk-text);
18
+ padding-block: var(--pk-card-spacing);
19
+ font-size: .875rem;
20
+ }
21
+ .pk-card.sm { --pk-card-spacing: .75rem; }
22
+ /* the footer band and a leading image supply their own inset */
23
+ .pk-card:has(> .pk-card-footer:last-child) { padding-bottom: 0; }
24
+ .pk-card:has(> img:first-child) { padding-top: 0; }
25
+ .pk-card > img { width: 100%; object-fit: cover; }
26
+
27
+ .pk-card-header {
28
+ display: grid;
29
+ grid-auto-rows: min-content;
30
+ align-items: start;
31
+ gap: .25rem;
32
+ padding-inline: var(--pk-card-spacing);
33
+ }
34
+ .pk-card-header:has(.pk-card-action) { grid-template-columns: 1fr auto; }
35
+ .pk-card-title { margin: 0; font-weight: 600; line-height: 1.25; }
36
+ .pk-card-description { margin: 0; font-size: .875rem; color: var(--pk-muted); }
37
+ .pk-card-action {
38
+ grid-row: 1 / span 2;
39
+ grid-column: 2;
40
+ justify-self: end;
41
+ align-self: start;
42
+ }
43
+ .pk-card-content { padding-inline: var(--pk-card-spacing); }
44
+ .pk-card-footer {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: .5rem;
48
+ border-top: 1px solid var(--pk-border);
49
+ background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent);
50
+ padding: var(--pk-card-spacing);
51
+ }
@@ -18,12 +18,16 @@ module PhlexKit
18
18
  # render PhlexKit::CardFooter.new { render PhlexKit::Button.new { "Action" } }
19
19
  # end
20
20
  class Card < BaseComponent
21
- def initialize(**attrs)
21
+ # size => modifier class; :sm tightens --pk-card-spacing (shadcn's size prop).
22
+ SIZES = { default: nil, sm: "sm" }.freeze
23
+
24
+ def initialize(size: :default, **attrs)
25
+ @size = size.to_sym
22
26
  @attrs = attrs
23
27
  end
24
28
 
25
29
  def view_template(&block)
26
- div(**mix({ class: "pk-card" }, @attrs), &block)
30
+ div(**mix({ class: [ "pk-card", SIZES.fetch(@size) ].compact.join(" ") }, @attrs), &block)
27
31
  end
28
32
  end
29
33
  end
@@ -0,0 +1,14 @@
1
+ module PhlexKit
2
+ # Top-right slot of a CardHeader (a button, link or badge) — the header
3
+ # becomes a 1fr/auto grid when present. Ported from shadcn/ui's CardAction
4
+ # (newer than ruby_ui's card). See card.rb.
5
+ class CardAction < BaseComponent
6
+ def initialize(**attrs)
7
+ @attrs = attrs
8
+ end
9
+
10
+ def view_template(&)
11
+ div(**mix({ class: "pk-card-action" }, @attrs), &)
12
+ end
13
+ end
14
+ end
@@ -19,21 +19,7 @@ module PhlexKit
19
19
  private
20
20
 
21
21
  def icon
22
- svg(
23
- xmlns: "http://www.w3.org/2000/svg",
24
- width: "24",
25
- height: "24",
26
- viewbox: "0 0 24 24",
27
- fill: "none",
28
- stroke: "currentColor",
29
- "stroke-width": "2",
30
- "stroke-linecap": "round",
31
- "stroke-linejoin": "round",
32
- "aria-hidden": "true"
33
- ) do |s|
34
- s.path(d: "M5 12h14")
35
- s.path(d: "m12 5 7 7-7 7")
36
- end
22
+ render Icon.new(:arrow_right, size: 24)
37
23
  end
38
24
  end
39
25
  end
@@ -20,21 +20,7 @@ module PhlexKit
20
20
  private
21
21
 
22
22
  def icon
23
- svg(
24
- xmlns: "http://www.w3.org/2000/svg",
25
- width: "24",
26
- height: "24",
27
- viewbox: "0 0 24 24",
28
- fill: "none",
29
- stroke: "currentColor",
30
- "stroke-width": "2",
31
- "stroke-linecap": "round",
32
- "stroke-linejoin": "round",
33
- "aria-hidden": "true"
34
- ) do |s|
35
- s.path(d: "m12 19-7-7 7-7")
36
- s.path(d: "M19 12H5")
37
- end
23
+ render Icon.new(:arrow_left, size: 24)
38
24
  end
39
25
  end
40
26
  end
@@ -4,4 +4,4 @@
4
4
  background: var(--pk-surface-2); font-size: .875rem;
5
5
  }
6
6
  .pk-codeblock pre { margin: 0; padding: 1rem; }
7
- .pk-codeblock code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--pk-text); }
7
+ .pk-codeblock code { font-family: var(--pk-font-mono, ui-monospace, monospace); color: var(--pk-text); }
@@ -18,21 +18,7 @@ module PhlexKit
18
18
  action: "phlex-kit--combobox#clearAll"
19
19
  }
20
20
  }, @attrs)) do
21
- svg(
22
- xmlns: "http://www.w3.org/2000/svg",
23
- width: "24",
24
- height: "24",
25
- viewbox: "0 0 24 24",
26
- fill: "none",
27
- stroke: "currentColor",
28
- "stroke-width": "2",
29
- "stroke-linecap": "round",
30
- "stroke-linejoin": "round",
31
- "aria-hidden": "true"
32
- ) do |s|
33
- s.path(d: "M18 6 6 18")
34
- s.path(d: "m6 6 12 12")
35
- end
21
+ render Icon.new(:x, size: 24)
36
22
  end
37
23
  end
38
24
  end
@@ -51,18 +51,7 @@ module PhlexKit
51
51
 
52
52
  def chevron_icon
53
53
  span(class: "pk-combobox-input-trigger-icon") do
54
- svg(
55
- xmlns: "http://www.w3.org/2000/svg",
56
- width: "24",
57
- height: "24",
58
- viewbox: "0 0 24 24",
59
- fill: "none",
60
- stroke: "currentColor",
61
- "stroke-width": "2",
62
- "stroke-linecap": "round",
63
- "stroke-linejoin": "round",
64
- "aria-hidden": "true"
65
- ) { |s| s.path(d: "m6 9 6 6 6-6") }
54
+ render Icon.new(:chevron_down, size: 24)
66
55
  end
67
56
  end
68
57
  end