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,95 @@
1
+ /* PhlexKit theme: Neutral — translated from a ui.shadcn.com/create export
2
+ (shadcn's stock neutral palette: pure grayscale, zero chroma throughout).
3
+ Pure token override: load AFTER the phlex_kit.css manifest and the
4
+ cascade re-themes every component.
5
+
6
+ Translation notes (the recipe for any shadcn/create export):
7
+ - background → bg; card/popover → surface; secondary/muted/accent →
8
+ surface-2/accent; foreground → text; secondary-foreground → text-2;
9
+ muted-foreground → muted; primary/primary-foreground → brand/brand-ink;
10
+ destructive → red; border/input/ring/chart-1..5/radius map 1:1.
11
+ - shadcn is light-default with a .dark class; PhlexKit themes are
12
+ dark-default with data-theme="light" opt-in — blocks swapped.
13
+ - sidebar-* maps onto the kit's optional --pk-sidebar-* overrides, set
14
+ only where the export differs from the shared tokens: here that's the
15
+ dark sidebar-primary (a distinct blue) + its ink. Like the shadcn create
16
+ page, the blue only shows on active items under Sidebar's menu: :solid —
17
+ the :default menu highlights with the accent role. shadcn/create exports
18
+ no green/amber — those fall through to _tokens.css defaults.
19
+ - dark text-2 has no shadcn source (every dark *-foreground is 0.985);
20
+ chart-1's 0.87 step (neutral-300) is used, matching the kit's own dark
21
+ text-2 weight. */
22
+ :root {
23
+ --pk-bg: oklch(0.145 0 0);
24
+ --pk-surface: oklch(0.205 0 0);
25
+ --pk-surface-2: oklch(0.269 0 0);
26
+ --pk-accent: oklch(0.269 0 0);
27
+ --pk-border: oklch(1 0 0 / 10%);
28
+ --pk-input: oklch(1 0 0 / 15%);
29
+ --pk-ring: oklch(0.556 0 0);
30
+ --pk-text: oklch(0.985 0 0);
31
+ --pk-text-2: oklch(0.87 0 0);
32
+ --pk-muted: oklch(0.708 0 0);
33
+ --pk-brand: oklch(0.922 0 0);
34
+ --pk-brand-ink: oklch(0.205 0 0);
35
+ --pk-red: oklch(0.704 0.191 22.216);
36
+ --pk-chart-1: oklch(0.87 0 0);
37
+ --pk-chart-2: oklch(0.556 0 0);
38
+ --pk-chart-3: oklch(0.439 0 0);
39
+ --pk-chart-4: oklch(0.371 0 0);
40
+ --pk-chart-5: oklch(0.269 0 0);
41
+ --pk-sidebar-primary: oklch(0.488 0.243 264.376);
42
+ --pk-sidebar-primary-ink: oklch(0.985 0 0);
43
+ --pk-radius: 0.625rem;
44
+ color-scheme: dark;
45
+ }
46
+
47
+ :root[data-theme="light"] {
48
+ --pk-bg: oklch(1 0 0);
49
+ --pk-sidebar-primary: oklch(0.205 0 0);
50
+ --pk-sidebar-primary-ink: oklch(0.985 0 0);
51
+ --pk-surface: oklch(1 0 0);
52
+ --pk-surface-2: oklch(0.97 0 0);
53
+ --pk-accent: oklch(0.97 0 0);
54
+ --pk-border: oklch(0.922 0 0);
55
+ --pk-input: oklch(0.922 0 0);
56
+ --pk-ring: oklch(0.708 0 0);
57
+ --pk-text: oklch(0.145 0 0);
58
+ --pk-text-2: oklch(0.205 0 0);
59
+ --pk-muted: oklch(0.556 0 0);
60
+ --pk-brand: oklch(0.205 0 0);
61
+ --pk-brand-ink: oklch(0.985 0 0);
62
+ --pk-red: oklch(0.577 0.245 27.325);
63
+ --pk-chart-1: oklch(0.87 0 0);
64
+ --pk-chart-2: oklch(0.556 0 0);
65
+ --pk-chart-3: oklch(0.439 0 0);
66
+ --pk-chart-4: oklch(0.371 0 0);
67
+ --pk-chart-5: oklch(0.269 0 0);
68
+ color-scheme: light;
69
+ }
70
+
71
+ @media (prefers-color-scheme: light) {
72
+ :root[data-theme="system"] {
73
+ --pk-bg: oklch(1 0 0);
74
+ --pk-sidebar-primary: oklch(0.205 0 0);
75
+ --pk-sidebar-primary-ink: oklch(0.985 0 0);
76
+ --pk-surface: oklch(1 0 0);
77
+ --pk-surface-2: oklch(0.97 0 0);
78
+ --pk-accent: oklch(0.97 0 0);
79
+ --pk-border: oklch(0.922 0 0);
80
+ --pk-input: oklch(0.922 0 0);
81
+ --pk-ring: oklch(0.708 0 0);
82
+ --pk-text: oklch(0.145 0 0);
83
+ --pk-text-2: oklch(0.205 0 0);
84
+ --pk-muted: oklch(0.556 0 0);
85
+ --pk-brand: oklch(0.205 0 0);
86
+ --pk-brand-ink: oklch(0.985 0 0);
87
+ --pk-red: oklch(0.577 0.245 27.325);
88
+ --pk-chart-1: oklch(0.87 0 0);
89
+ --pk-chart-2: oklch(0.556 0 0);
90
+ --pk-chart-3: oklch(0.439 0 0);
91
+ --pk-chart-4: oklch(0.371 0 0);
92
+ --pk-chart-5: oklch(0.269 0 0);
93
+ color-scheme: light;
94
+ }
95
+ }
@@ -0,0 +1,85 @@
1
+ /* PhlexKit theme: Zinc — translated from a ui.shadcn.com/create export
2
+ (preset b6mHLWLL2u, shadcn's stock zinc palette). Pure token override:
3
+ load AFTER the phlex_kit.css manifest and the cascade re-themes every
4
+ component.
5
+
6
+ Translation notes (the recipe for any shadcn/create export):
7
+ - background → bg; card/popover → surface; secondary/muted/accent →
8
+ surface-2/accent; foreground → text; secondary-foreground → text-2;
9
+ muted-foreground → muted; primary/primary-foreground → brand/brand-ink;
10
+ destructive → red; border/input/ring/chart-1..5/radius map 1:1.
11
+ - shadcn is light-default with a .dark class; PhlexKit themes are
12
+ dark-default with data-theme="light" opt-in — blocks swapped.
13
+ - sidebar-* has no --pk-* slots (the kit's sidebar reads the shared
14
+ surface tokens) and shadcn/create exports no green/amber — both fall
15
+ through to _tokens.css defaults.
16
+ - dark text-2 has no shadcn source (every dark *-foreground is 0.985);
17
+ zinc-300 is used, matching the kit's own dark text-2 weight. */
18
+ :root {
19
+ --pk-bg: oklch(0.141 0.005 285.823);
20
+ --pk-surface: oklch(0.21 0.006 285.885);
21
+ --pk-surface-2: oklch(0.274 0.006 286.033);
22
+ --pk-accent: oklch(0.274 0.006 286.033);
23
+ --pk-border: oklch(1 0 0 / 10%);
24
+ --pk-input: oklch(1 0 0 / 15%);
25
+ --pk-ring: oklch(0.552 0.016 285.938);
26
+ --pk-text: oklch(0.985 0 0);
27
+ --pk-text-2: oklch(0.871 0.006 286.286);
28
+ --pk-muted: oklch(0.705 0.015 286.067);
29
+ --pk-brand: oklch(0.92 0.004 286.32);
30
+ --pk-brand-ink: oklch(0.21 0.006 285.885);
31
+ --pk-red: oklch(0.704 0.191 22.216);
32
+ --pk-chart-1: oklch(0.871 0.006 286.286);
33
+ --pk-chart-2: oklch(0.552 0.016 285.938);
34
+ --pk-chart-3: oklch(0.442 0.017 285.786);
35
+ --pk-chart-4: oklch(0.37 0.013 285.805);
36
+ --pk-chart-5: oklch(0.274 0.006 286.033);
37
+ --pk-radius: 0.625rem;
38
+ color-scheme: dark;
39
+ }
40
+
41
+ :root[data-theme="light"] {
42
+ --pk-bg: oklch(1 0 0);
43
+ --pk-surface: oklch(1 0 0);
44
+ --pk-surface-2: oklch(0.967 0.001 286.375);
45
+ --pk-accent: oklch(0.967 0.001 286.375);
46
+ --pk-border: oklch(0.92 0.004 286.32);
47
+ --pk-input: oklch(0.92 0.004 286.32);
48
+ --pk-ring: oklch(0.705 0.015 286.067);
49
+ --pk-text: oklch(0.141 0.005 285.823);
50
+ --pk-text-2: oklch(0.21 0.006 285.885);
51
+ --pk-muted: oklch(0.552 0.016 285.938);
52
+ --pk-brand: oklch(0.21 0.006 285.885);
53
+ --pk-brand-ink: oklch(0.985 0 0);
54
+ --pk-red: oklch(0.577 0.245 27.325);
55
+ --pk-chart-1: oklch(0.871 0.006 286.286);
56
+ --pk-chart-2: oklch(0.552 0.016 285.938);
57
+ --pk-chart-3: oklch(0.442 0.017 285.786);
58
+ --pk-chart-4: oklch(0.37 0.013 285.805);
59
+ --pk-chart-5: oklch(0.274 0.006 286.033);
60
+ color-scheme: light;
61
+ }
62
+
63
+ @media (prefers-color-scheme: light) {
64
+ :root[data-theme="system"] {
65
+ --pk-bg: oklch(1 0 0);
66
+ --pk-surface: oklch(1 0 0);
67
+ --pk-surface-2: oklch(0.967 0.001 286.375);
68
+ --pk-accent: oklch(0.967 0.001 286.375);
69
+ --pk-border: oklch(0.92 0.004 286.32);
70
+ --pk-input: oklch(0.92 0.004 286.32);
71
+ --pk-ring: oklch(0.705 0.015 286.067);
72
+ --pk-text: oklch(0.141 0.005 285.823);
73
+ --pk-text-2: oklch(0.21 0.006 285.885);
74
+ --pk-muted: oklch(0.552 0.016 285.938);
75
+ --pk-brand: oklch(0.21 0.006 285.885);
76
+ --pk-brand-ink: oklch(0.985 0 0);
77
+ --pk-red: oklch(0.577 0.245 27.325);
78
+ --pk-chart-1: oklch(0.871 0.006 286.286);
79
+ --pk-chart-2: oklch(0.552 0.016 285.938);
80
+ --pk-chart-3: oklch(0.442 0.017 285.786);
81
+ --pk-chart-4: oklch(0.37 0.013 285.805);
82
+ --pk-chart-5: oklch(0.274 0.006 286.033);
83
+ color-scheme: light;
84
+ }
85
+ }
@@ -12,3 +12,16 @@
12
12
  .pk-accordion-icon svg { width: 1rem; height: 1rem; }
13
13
  .pk-accordion-content { overflow-y: hidden; }
14
14
  .pk-accordion-default-content { padding-bottom: 1rem; padding-top: 0; font-size: .875rem; }
15
+
16
+ /* Disabled items: inert trigger, dimmed. */
17
+ .pk-accordion-item[data-disabled] .pk-accordion-trigger {
18
+ pointer-events: none;
19
+ opacity: .5;
20
+ }
21
+
22
+ /* Bordered flavour (shadcn's "Borders" example): outer border, inset rows. */
23
+ .pk-accordion.bordered {
24
+ border: 1px solid var(--pk-border);
25
+ border-radius: var(--pk-radius);
26
+ padding-inline: 1rem;
27
+ }
@@ -4,7 +4,16 @@ module PhlexKit
4
4
  # AccordionContent). Animated open/close via the phlex-kit--accordion controller
5
5
  # (native Web Animations API — no `motion` dependency).
6
6
  class Accordion < BaseComponent
7
- def initialize(**attrs) = (@attrs = attrs)
8
- def view_template(&) = div(**mix({ class: "pk-accordion" }, @attrs), &)
7
+ # single (default, shadcn's): opening an item closes the others.
8
+ TYPES = { single: "single", multiple: "multiple" }.freeze
9
+
10
+ def initialize(type: :single, **attrs)
11
+ @type = type.to_sym
12
+ @attrs = attrs
13
+ end
14
+
15
+ def view_template(&)
16
+ div(**mix({ class: "pk-accordion", data: { type: TYPES.fetch(@type) } }, @attrs), &)
17
+ end
9
18
  end
10
19
  end
@@ -6,9 +6,7 @@ module PhlexKit
6
6
  if block
7
7
  yield
8
8
  else
9
- svg(xmlns: "http://www.w3.org/2000/svg", viewbox: "0 0 20 20", fill: "currentColor") do |s|
10
- s.path(fill_rule: "evenodd", clip_rule: "evenodd", d: "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z")
11
- end
9
+ render Icon.new(:chevron_down, size: nil)
12
10
  end
13
11
  end
14
12
  end
@@ -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