view_primitives 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +57 -2
  4. data/lib/generators/view_primitives/add/add_generator.rb +8 -62
  5. data/lib/generators/view_primitives/add/templates/accordion/accordion_item_component.rb.tt +30 -11
  6. data/lib/generators/view_primitives/add/templates/alert/alert_component.rb.tt +1 -1
  7. data/lib/generators/view_primitives/add/templates/alert_dialog/alert_dialog_component.rb.tt +9 -9
  8. data/lib/generators/view_primitives/add/templates/aspect_ratio/aspect_ratio_component.rb.tt +1 -1
  9. data/lib/generators/view_primitives/add/templates/audio/audio_component.rb.tt +1 -1
  10. data/lib/generators/view_primitives/add/templates/avatar/avatar_component.rb.tt +8 -4
  11. data/lib/generators/view_primitives/add/templates/badge/badge_component.rb.tt +1 -1
  12. data/lib/generators/view_primitives/add/templates/banner/banner_component.rb.tt +6 -6
  13. data/lib/generators/view_primitives/add/templates/bottom_nav/bottom_nav_component.rb.tt +11 -4
  14. data/lib/generators/view_primitives/add/templates/breadcrumb/breadcrumb_component.rb.tt +2 -2
  15. data/lib/generators/view_primitives/add/templates/button/button_component.rb.tt +8 -5
  16. data/lib/generators/view_primitives/add/templates/button_group/button_group_component.rb.tt +5 -5
  17. data/lib/generators/view_primitives/add/templates/calendar/calendar_component.rb.tt +18 -16
  18. data/lib/generators/view_primitives/add/templates/card/card_component.rb.tt +1 -1
  19. data/lib/generators/view_primitives/add/templates/card/card_footer_component.rb.tt +1 -1
  20. data/lib/generators/view_primitives/add/templates/carousel/carousel_component.rb.tt +26 -13
  21. data/lib/generators/view_primitives/add/templates/chart/chart_component.rb.tt +10 -4
  22. data/lib/generators/view_primitives/add/templates/chart/chart_controller.js +26 -3
  23. data/lib/generators/view_primitives/add/templates/chat_bubble/chat_bubble_component.rb.tt +4 -4
  24. data/lib/generators/view_primitives/add/templates/checkbox/checkbox_component.rb.tt +1 -1
  25. data/lib/generators/view_primitives/add/templates/collapsible/collapsible_component.rb.tt +12 -5
  26. data/lib/generators/view_primitives/add/templates/combobox/combobox_component.rb.tt +3 -6
  27. data/lib/generators/view_primitives/add/templates/command/command_component.rb.tt +22 -18
  28. data/lib/generators/view_primitives/add/templates/command/command_controller.js +50 -0
  29. data/lib/generators/view_primitives/add/templates/context_menu/context_menu_component.rb.tt +9 -8
  30. data/lib/generators/view_primitives/add/templates/data_table/data_table_component.rb.tt +60 -29
  31. data/lib/generators/view_primitives/add/templates/data_table/data_table_controller.js +2 -2
  32. data/lib/generators/view_primitives/add/templates/date_picker/date_picker_component.rb.tt +8 -8
  33. data/lib/generators/view_primitives/add/templates/device_mockup/device_mockup_component.rb.tt +94 -21
  34. data/lib/generators/view_primitives/add/templates/dialog/dialog_component.rb.tt +13 -10
  35. data/lib/generators/view_primitives/add/templates/dialog/dialog_controller.js +52 -0
  36. data/lib/generators/view_primitives/add/templates/drawer/drawer_component.rb.tt +8 -7
  37. data/lib/generators/view_primitives/add/templates/dropdown_menu/dropdown_menu_component.rb.tt +5 -6
  38. data/lib/generators/view_primitives/add/templates/embed/embed_component.rb.tt +2 -2
  39. data/lib/generators/view_primitives/add/templates/figure/figure_component.rb.tt +1 -1
  40. data/lib/generators/view_primitives/add/templates/file_input/file_input_component.rb.tt +3 -12
  41. data/lib/generators/view_primitives/add/templates/floating_label/floating_label_component.rb.tt +1 -1
  42. data/lib/generators/view_primitives/add/templates/footer/footer_component.rb.tt +5 -4
  43. data/lib/generators/view_primitives/add/templates/form_field/form_field_component.rb.tt +18 -5
  44. data/lib/generators/view_primitives/add/templates/gallery/gallery_component.rb.tt +3 -3
  45. data/lib/generators/view_primitives/add/templates/gallery/gallery_controller.js +1 -1
  46. data/lib/generators/view_primitives/add/templates/hover_card/hover_card_component.rb.tt +6 -5
  47. data/lib/generators/view_primitives/add/templates/iframe/iframe_component.rb.tt +6 -4
  48. data/lib/generators/view_primitives/add/templates/image/image_component.rb.tt +1 -1
  49. data/lib/generators/view_primitives/add/templates/indicator/indicator_component.rb.tt +5 -4
  50. data/lib/generators/view_primitives/add/templates/input/input_component.rb.tt +2 -13
  51. data/lib/generators/view_primitives/add/templates/input_otp/input_otp_component.rb.tt +22 -10
  52. data/lib/generators/view_primitives/add/templates/kbd/kbd_component.rb.tt +3 -1
  53. data/lib/generators/view_primitives/add/templates/list_group/list_group_component.rb.tt +6 -2
  54. data/lib/generators/view_primitives/add/templates/list_group/list_group_item_component.rb.tt +6 -4
  55. data/lib/generators/view_primitives/add/templates/map_area/map_area_component.rb.tt +3 -2
  56. data/lib/generators/view_primitives/add/templates/mega_menu/mega_menu_component.rb.tt +9 -9
  57. data/lib/generators/view_primitives/add/templates/menubar/menubar_component.rb.tt +5 -5
  58. data/lib/generators/view_primitives/add/templates/menubar/menubar_menu_component.rb.tt +4 -5
  59. data/lib/generators/view_primitives/add/templates/navbar/navbar_component.rb.tt +51 -11
  60. data/lib/generators/view_primitives/add/templates/navbar/navbar_controller.js +8 -3
  61. data/lib/generators/view_primitives/add/templates/navigation_menu/navigation_menu_component.rb.tt +12 -16
  62. data/lib/generators/view_primitives/add/templates/number_input/number_input_component.rb.tt +4 -11
  63. data/lib/generators/view_primitives/add/templates/pagination/pagination_component.rb.tt +4 -3
  64. data/lib/generators/view_primitives/add/templates/picture/picture_component.rb.tt +2 -1
  65. data/lib/generators/view_primitives/add/templates/popover/popover_component.rb.tt +1 -2
  66. data/lib/generators/view_primitives/add/templates/progress/progress_component.rb.tt +3 -1
  67. data/lib/generators/view_primitives/add/templates/qr_code/qr_code_component.rb.tt +1 -1
  68. data/lib/generators/view_primitives/add/templates/radio_group/radio_group_component.rb.tt +8 -5
  69. data/lib/generators/view_primitives/add/templates/range/range_component.rb.tt +2 -3
  70. data/lib/generators/view_primitives/add/templates/rating/rating_component.rb.tt +1 -1
  71. data/lib/generators/view_primitives/add/templates/rating_input/rating_controller.js +1 -1
  72. data/lib/generators/view_primitives/add/templates/rating_input/rating_input_component.rb.tt +4 -3
  73. data/lib/generators/view_primitives/add/templates/resizable/resizable_component.rb.tt +27 -15
  74. data/lib/generators/view_primitives/add/templates/scroll_area/scroll_area_component.rb.tt +10 -11
  75. data/lib/generators/view_primitives/add/templates/search_input/search_input_component.rb.tt +2 -11
  76. data/lib/generators/view_primitives/add/templates/select/select_component.rb.tt +25 -6
  77. data/lib/generators/view_primitives/add/templates/separator/separator_component.rb.tt +6 -3
  78. data/lib/generators/view_primitives/add/templates/sheet/sheet_component.rb.tt +25 -21
  79. data/lib/generators/view_primitives/add/templates/sidebar/sidebar_component.rb.tt +27 -21
  80. data/lib/generators/view_primitives/add/templates/skeleton/skeleton_component.rb.tt +1 -1
  81. data/lib/generators/view_primitives/add/templates/speed_dial/speed_dial_component.rb.tt +8 -9
  82. data/lib/generators/view_primitives/add/templates/spinner/spinner_component.rb.tt +15 -6
  83. data/lib/generators/view_primitives/add/templates/stepper/stepper_component.rb.tt +17 -16
  84. data/lib/generators/view_primitives/add/templates/switch/switch_component.rb.tt +27 -14
  85. data/lib/generators/view_primitives/add/templates/tabs/tabs_component.html.erb +13 -7
  86. data/lib/generators/view_primitives/add/templates/tags_input/tags_input_component.rb.tt +136 -0
  87. data/lib/generators/view_primitives/add/templates/tags_input/tags_input_controller.js +90 -0
  88. data/lib/generators/view_primitives/add/templates/textarea/textarea_component.rb.tt +2 -11
  89. data/lib/generators/view_primitives/add/templates/timeline/timeline_component.rb.tt +9 -7
  90. data/lib/generators/view_primitives/add/templates/timepicker/timepicker_component.rb.tt +19 -15
  91. data/lib/generators/view_primitives/add/templates/toaster/toaster_component.rb.tt +10 -10
  92. data/lib/generators/view_primitives/add/templates/toaster/toaster_controller.js +6 -6
  93. data/lib/generators/view_primitives/add/templates/toggle/toggle_component.rb.tt +10 -3
  94. data/lib/generators/view_primitives/add/templates/toggle_group/toggle_group_component.rb.tt +6 -6
  95. data/lib/generators/view_primitives/add/templates/tooltip/tooltip_component.rb.tt +7 -6
  96. data/lib/generators/view_primitives/add/templates/video/video_component.rb.tt +1 -1
  97. data/lib/generators/view_primitives/add/templates/wysiwyg/wysiwyg_component.rb.tt +9 -3
  98. data/lib/generators/view_primitives/component_copier.rb +96 -0
  99. data/lib/generators/view_primitives/components.rb +16 -2
  100. data/lib/generators/view_primitives/install/install_generator.rb +13 -3
  101. data/lib/generators/view_primitives/install/templates/application_component.rb.tt +7 -0
  102. data/lib/generators/view_primitives/install/templates/styles.rb.tt +26 -0
  103. data/lib/generators/view_primitives/install/templates/view_primitives/themes/default.css +79 -0
  104. data/lib/generators/view_primitives/install/templates/view_primitives/themes/rose.css +57 -0
  105. data/lib/generators/view_primitives/install/templates/view_primitives/tokens.css +46 -0
  106. data/lib/generators/view_primitives/install/templates/view_primitives/utilities.css +64 -0
  107. data/lib/generators/view_primitives/install/templates/view_primitives.css +6 -66
  108. data/lib/generators/view_primitives/list/list_generator.rb +3 -1
  109. data/lib/generators/view_primitives/theme/theme_generator.rb +79 -0
  110. data/lib/generators/view_primitives/update/update_generator.rb +112 -0
  111. data/lib/view_primitives/class_helper.rb +4 -1
  112. data/lib/view_primitives/railtie.rb +1 -1
  113. data/lib/view_primitives/version.rb +1 -1
  114. metadata +12 -4
  115. data/lib/generators/view_primitives/add/templates/drawer/drawer_controller.js +0 -15
  116. data/lib/generators/view_primitives/add/templates/sheet/sheet_controller.js +0 -15
@@ -10,21 +10,27 @@ module UI
10
10
  # r.with_panel { right_content }
11
11
  # end
12
12
 
13
- WRAPPER_CLS = "flex overflow-hidden rounded-lg border border-border"
13
+ WRAPPER_CLS = "group flex w-full overflow-hidden rounded-md #{UI::Styles::BORDER} shadow-xs"
14
14
 
15
- PANEL_CLS = "overflow-auto"
15
+ PANEL_CLS = "h-full w-full min-h-0 min-w-0 overflow-auto"
16
16
 
17
- HANDLE_CLS = "group relative flex items-center justify-center " \
18
- "bg-border transition-colors hover:bg-ring/30 focus-visible:outline-none " \
19
- "focus-visible:ring-[3px] focus-visible:ring-ring/50 " \
20
- "data-[direction=horizontal]:w-px data-[direction=horizontal]:cursor-col-resize " \
21
- "data-[direction=vertical]:h-px data-[direction=vertical]:cursor-row-resize"
17
+ HANDLE_GRIP_BASE = "z-10 flex shrink-0 items-center justify-center rounded-xs #{UI::Styles::BORDER} bg-border"
22
18
 
23
- HANDLE_GRIP = "z-10 flex h-4 w-3 items-center justify-center rounded-sm border border-border bg-border"
19
+ HANDLE_GRIP_HORIZONTAL = "h-4 w-3"
20
+ HANDLE_GRIP_VERTICAL = "h-3 w-4"
21
+
22
+ HANDLE_HORIZONTAL = "relative flex w-px shrink-0 items-center justify-center bg-border " \
23
+ "after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 " \
24
+ "cursor-col-resize focus-visible:ring-1 focus-visible:ring-ring " \
25
+ "focus-visible:ring-offset-1 focus-visible:outline-hidden"
26
+
27
+ HANDLE_VERTICAL = "relative flex h-px w-full shrink-0 items-center justify-center bg-border " \
28
+ "after:absolute after:inset-x-0 after:top-1/2 after:h-1 after:w-full after:-translate-y-1/2 " \
29
+ "cursor-row-resize focus-visible:ring-1 focus-visible:ring-ring " \
30
+ "focus-visible:ring-offset-1 focus-visible:outline-hidden"
24
31
 
25
32
  renders_many :panels, "UI::ResizableComponent::PanelComponent"
26
33
 
27
- # direction: :horizontal (default) | :vertical
28
34
  def initialize(direction: :horizontal, **html_attrs)
29
35
  @direction = direction.to_sym
30
36
  @extra_class = html_attrs.delete(:class)
@@ -37,6 +43,9 @@ module UI
37
43
 
38
44
  content_tag(:div,
39
45
  class: cn(WRAPPER_CLS, flex_dir, @extra_class),
46
+ "data-slot": "resizable-panel-group",
47
+ "data-direction": @direction,
48
+ "aria-orientation": @direction == :horizontal ? "horizontal" : "vertical",
40
49
  data: {
41
50
  controller: "resizable",
42
51
  resizable_direction_value: @direction
@@ -52,18 +61,20 @@ module UI
52
61
  private
53
62
 
54
63
  def handle
64
+ grip_cls = @direction == :vertical ? HANDLE_GRIP_VERTICAL : HANDLE_GRIP_HORIZONTAL
65
+ handle_cls = @direction == :vertical ? HANDLE_VERTICAL : HANDLE_HORIZONTAL
66
+
55
67
  content_tag(:div,
56
- class: HANDLE_CLS,
57
- "data-direction": @direction,
68
+ content_tag(:div, nil, class: cn(HANDLE_GRIP_BASE, grip_cls)),
69
+ class: handle_cls,
70
+ "data-slot": "resizable-handle",
58
71
  tabindex: "0",
59
72
  role: "separator",
60
73
  "aria-orientation": @direction == :horizontal ? "vertical" : "horizontal",
61
74
  data: {
62
75
  resizable_target: "handle",
63
76
  action: "mousedown->resizable#startDrag touchstart->resizable#startDrag"
64
- }) do
65
- content_tag(:div, nil, class: HANDLE_GRIP)
66
- end
77
+ })
67
78
  end
68
79
 
69
80
  class PanelComponent < ApplicationComponent
@@ -75,9 +86,10 @@ module UI
75
86
  end
76
87
 
77
88
  def call
78
- style = @default ? "flex: 0 0 #{@default}%" : "flex: 1"
89
+ style = @default ? "flex: 0 0 #{@default}%" : "flex: 1 1 0%"
79
90
  content_tag(:div, content,
80
91
  class: ResizableComponent::PANEL_CLS,
92
+ "data-slot": "resizable-panel",
81
93
  style: style,
82
94
  data: {
83
95
  resizable_target: "panel",
@@ -2,8 +2,7 @@
2
2
 
3
3
  module UI
4
4
  class ScrollAreaComponent < ApplicationComponent
5
- # Custom-styled scrollbar container using CSS pseudo-elements.
6
- # Works without a plugin in Tailwind v4 via arbitrary property syntax.
5
+ ROOT = "relative"
7
6
 
8
7
  ORIENTATIONS = {
9
8
  vertical: "overflow-y-auto",
@@ -11,17 +10,16 @@ module UI
11
10
  both: "overflow-auto"
12
11
  }.freeze
13
12
 
14
- # Thin, themed scrollbar applied to the viewport
13
+ VIEWPORT = "size-full rounded-[inherit] transition-[color,box-shadow] outline-none " \
14
+ "#{UI::Styles::FOCUS_RING} focus-visible:outline-1"
15
+
15
16
  SCROLLBAR_CLS = "[scrollbar-width:thin] " \
16
17
  "[scrollbar-color:var(--color-border)_transparent] " \
17
- "[&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar]:h-1.5 " \
18
+ "[&::-webkit-scrollbar]:w-2.5 [&::-webkit-scrollbar]:h-2.5 " \
18
19
  "[&::-webkit-scrollbar-track]:bg-transparent " \
19
20
  "[&::-webkit-scrollbar-thumb]:rounded-full " \
20
21
  "[&::-webkit-scrollbar-thumb]:bg-border"
21
22
 
22
- # orientation: :vertical (default) | :horizontal | :both
23
- # max_h: Tailwind max-height class, e.g. "max-h-72" (vertical / both)
24
- # max_w: Tailwind max-width class, e.g. "max-w-sm" (horizontal / both)
25
23
  def initialize(orientation: :vertical, max_h: "max-h-72", max_w: nil, **html_attrs)
26
24
  @orientation = orientation.to_sym
27
25
  @max_h = max_h
@@ -32,10 +30,11 @@ module UI
32
30
 
33
31
  def call
34
32
  overflow = ORIENTATIONS.fetch(@orientation, ORIENTATIONS[:vertical])
35
- content_tag(:div,
36
- content,
37
- class: cn(overflow, SCROLLBAR_CLS, @max_h, @max_w, @extra_class),
38
- **@html_attrs)
33
+ content_tag(:div, class: cn(ROOT, @extra_class), **@html_attrs) do
34
+ content_tag(:div,
35
+ content,
36
+ class: cn(overflow, VIEWPORT, SCROLLBAR_CLS, @max_h, @max_w))
37
+ end
39
38
  end
40
39
  end
41
40
  end
@@ -5,20 +5,11 @@ module UI
5
5
  WRAPPER = "relative w-full"
6
6
  ICON_WRAP = "pointer-events-none absolute inset-y-0 left-3 flex items-center text-muted-foreground"
7
7
  SEARCH_PATH = "m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
8
- INPUT_BASE = "h-9 w-full min-w-0 rounded-md border border-input bg-transparent py-1 pl-9 pr-3 text-base shadow-xs " \
9
- "transition-[color,box-shadow] outline-none " \
10
- "placeholder:text-muted-foreground " \
11
- "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " \
12
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " \
13
- "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 " \
14
- "md:text-sm dark:bg-input/30"
15
-
16
8
  # placeholder: default "Search…"
17
9
  # name / id / value: passed through as html_attrs
18
10
  def initialize(placeholder: "Search…", **html_attrs)
19
11
  @placeholder = placeholder
20
- @extra_class = html_attrs.delete(:class)
21
- @html_attrs = html_attrs
12
+ extract_html_attrs(**html_attrs)
22
13
  end
23
14
 
24
15
  def call
@@ -27,7 +18,7 @@ module UI
27
18
  concat content_tag(:input, nil,
28
19
  type: "search",
29
20
  placeholder: @placeholder,
30
- class: cn(INPUT_BASE, @extra_class),
21
+ class: cn(UI::Styles::INPUT, "pl-9 pr-3", @extra_class),
31
22
  **@html_attrs)
32
23
  end
33
24
  end
@@ -2,27 +2,46 @@
2
2
 
3
3
  module UI
4
4
  class SelectComponent < ApplicationComponent
5
- BASE = "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm " \
6
- "focus:outline-none focus:ring-1 focus:ring-ring " \
7
- "disabled:cursor-not-allowed disabled:opacity-50"
5
+ WRAPPER = "group/native-select relative w-fit has-[select:disabled]:opacity-50"
6
+
7
+ BASE = UI::Styles::SELECT
8
8
 
9
9
  # options: array of strings, or [value, label] pairs, or { value: label } hash
10
- def initialize(options: [], selected: nil, include_blank: false, **html_attrs)
10
+ def initialize(options: [], selected: nil, include_blank: false, size: :default, **html_attrs)
11
11
  @options = options
12
12
  @selected = selected
13
13
  @include_blank = include_blank
14
+ @size = size.to_sym
14
15
  @extra_class = html_attrs.delete(:class)
15
16
  @html_attrs = html_attrs
16
17
  end
17
18
 
18
19
  def call
19
- content_tag(:select, class: cn(BASE, @extra_class), **@html_attrs) do
20
- safe_join(option_tags)
20
+ content_tag(:div, class: WRAPPER) do
21
+ concat content_tag(:select,
22
+ safe_join(option_tags),
23
+ class: cn(BASE, @extra_class),
24
+ "data-size": (@size == :sm ? "sm" : "default"),
25
+ **@html_attrs)
26
+ concat chevron_icon
21
27
  end
22
28
  end
23
29
 
24
30
  private
25
31
 
32
+ def chevron_icon
33
+ svg = content_tag(:svg,
34
+ content_tag(:path, nil, d: "m6 9 6 6 6-6", fill: "none", stroke: "currentColor",
35
+ "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2"),
36
+ xmlns: "http://www.w3.org/2000/svg",
37
+ viewBox: "0 0 24 24",
38
+ fill: "none",
39
+ class: "size-4",
40
+ "aria-hidden": "true")
41
+ content_tag(:span, svg,
42
+ class: "pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 text-muted-foreground opacity-50 select-none")
43
+ end
44
+
26
45
  def option_tags
27
46
  tags = []
28
47
  tags << content_tag(:option, "", value: "") if @include_blank
@@ -2,9 +2,11 @@
2
2
 
3
3
  module UI
4
4
  class SeparatorComponent < ApplicationComponent
5
+ BASE = "shrink-0 bg-border"
6
+
5
7
  ORIENTATIONS = {
6
- horizontal: "bg-border h-px w-full shrink-0",
7
- vertical: "bg-border h-full w-px shrink-0"
8
+ horizontal: "h-px w-full",
9
+ vertical: "h-full w-px"
8
10
  }.freeze
9
11
 
10
12
  def initialize(orientation: :horizontal, decorative: true, **html_attrs)
@@ -18,7 +20,8 @@ module UI
18
20
  content_tag(:div, nil,
19
21
  role: (@decorative ? "none" : "separator"),
20
22
  "aria-orientation": @orientation.to_s,
21
- class: cn(ORIENTATIONS[@orientation], @extra_class),
23
+ "data-orientation": @orientation.to_s,
24
+ class: cn(BASE, ORIENTATIONS[@orientation], @extra_class),
22
25
  **@html_attrs)
23
26
  end
24
27
  end
@@ -5,15 +5,21 @@ module UI
5
5
  renders_one :trigger
6
6
  renders_one :footer
7
7
 
8
- OVERLAY = "fixed inset-0 z-50 bg-black/50"
8
+ OVERLAY = UI::Styles::OVERLAY
9
9
 
10
10
  SIDES = {
11
- right: "fixed inset-y-0 right-0 h-full w-3/4 max-w-sm border-l",
12
- left: "fixed inset-y-0 left-0 h-full w-3/4 max-w-sm border-r",
13
- top: "fixed inset-x-0 top-0 h-auto max-h-[60vh] border-b",
14
- bottom: "fixed inset-x-0 bottom-0 h-auto max-h-[60vh] border-t"
11
+ right: "inset-y-0 right-0 h-full w-3/4 border-l border-border sm:max-w-sm",
12
+ left: "inset-y-0 left-0 h-full w-3/4 border-r border-border sm:max-w-sm",
13
+ top: "inset-x-0 top-0 h-auto border-b border-border",
14
+ bottom: "inset-x-0 bottom-0 h-auto border-t border-border"
15
15
  }.freeze
16
16
 
17
+ PANEL_BASE = "fixed z-[51] flex flex-col gap-4 bg-background p-6 shadow-lg transition ease-in-out overflow-y-auto"
18
+
19
+ CLOSE_BTN = "absolute top-4 right-4 z-10 rounded-xs opacity-70 ring-offset-background transition-opacity " \
20
+ "hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden " \
21
+ "disabled:pointer-events-none"
22
+
17
23
  def initialize(title: nil, description: nil, side: :right, **html_attrs)
18
24
  @title = title
19
25
  @description = description
@@ -23,8 +29,8 @@ module UI
23
29
  end
24
30
 
25
31
  def call
26
- content_tag(:div, data: { controller: "sheet" }, **@html_attrs) do
27
- concat content_tag(:span, trigger, data: { action: "click->sheet#open" }, class: "contents") if trigger
32
+ content_tag(:div, data: { controller: "dialog" }, **@html_attrs) do
33
+ concat content_tag(:span, trigger, data: { action: "click->dialog#open" }, class: "contents") if trigger
28
34
  concat panel
29
35
  end
30
36
  end
@@ -32,23 +38,21 @@ module UI
32
38
  private
33
39
 
34
40
  def panel
35
- content_tag(:div, data: { sheet_target: "panel" }, hidden: true) do
41
+ content_tag(:div, data: { dialog_target: "panel" }, hidden: true) do
36
42
  concat content_tag(:div, nil,
37
43
  class: OVERLAY,
38
- data: { action: "click->sheet#close" },
44
+ data: { action: "click->dialog#close" },
39
45
  "aria-hidden": "true")
40
46
  concat content_tag(:div,
41
- class: cn("z-50 bg-background p-6 shadow-xl overflow-y-auto",
42
- SIDES.fetch(@side, SIDES[:right]),
43
- @extra_class),
47
+ class: cn(PANEL_BASE, SIDES.fetch(@side, SIDES[:right]), @extra_class),
44
48
  role: "dialog",
45
49
  "aria-modal": "true",
46
50
  "aria-label": @title,
47
- data: { action: "keydown.escape@window->sheet#close" }) {
51
+ data: { action: "keydown.escape@window->dialog#close" }) {
48
52
  concat close_button
49
53
  concat header_area
50
- concat content_tag(:div, content, class: "flex-1 text-sm")
51
- concat content_tag(:div, footer, class: "mt-6 flex justify-end gap-2") if footer
54
+ concat content_tag(:div, content, class: "flex-1") unless content.blank?
55
+ concat content_tag(:div, footer, class: "mt-auto flex flex-col gap-2") if footer
52
56
  }
53
57
  end
54
58
  end
@@ -56,9 +60,9 @@ module UI
56
60
  def header_area
57
61
  return "" if @title.nil? && @description.nil?
58
62
 
59
- content_tag(:div, class: "mb-4 pr-6") do
60
- concat content_tag(:h2, @title, class: "text-lg font-semibold leading-none tracking-tight") if @title
61
- concat content_tag(:p, @description, class: "mt-2 text-sm text-muted-foreground") if @description
63
+ content_tag(:div, class: "flex flex-col gap-1.5 pr-6") do
64
+ concat content_tag(:h2, @title, class: "font-semibold text-foreground") if @title
65
+ concat content_tag(:p, @description, class: "text-sm text-muted-foreground") if @description
62
66
  end
63
67
  end
64
68
 
@@ -66,13 +70,13 @@ module UI
66
70
  content_tag(:button,
67
71
  close_svg,
68
72
  type: "button",
69
- class: "absolute right-4 top-4 rounded-sm p-1 opacity-70 hover:opacity-100 transition-opacity",
70
- data: { action: "click->sheet#close" },
73
+ class: CLOSE_BTN,
74
+ data: { action: "click->dialog#close" },
71
75
  "aria-label": "Close")
72
76
  end
73
77
 
74
78
  def close_svg
75
- raw('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>')
79
+ raw('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>')
76
80
  end
77
81
  end
78
82
  end
@@ -13,31 +13,37 @@ module UI
13
13
  # end
14
14
 
15
15
  RAIL_CLS = "group peer fixed inset-y-0 left-0 z-30 flex h-full flex-col " \
16
- "border-r border-border bg-background transition-[width] duration-300 " \
16
+ "border-r border-sidebar-border bg-sidebar text-sidebar-foreground " \
17
+ "transition-[width] duration-300 " \
17
18
  "data-[collapsed=true]:w-16 data-[collapsed=false]:w-64"
18
19
 
19
- HEADER_CLS = "flex h-14 items-center justify-between border-b border-border px-4"
20
+ HEADER_CLS = "flex h-14 shrink-0 items-center justify-between border-b border-sidebar-border px-4 " \
21
+ "group-data-[collapsed=true]:justify-center group-data-[collapsed=true]:px-2"
20
22
 
21
- TOGGLE_CLS = "inline-flex size-8 items-center justify-center rounded-md " \
22
- "text-muted-foreground hover:bg-accent hover:text-accent-foreground " \
23
- "focus-visible:ring-[3px] focus-visible:ring-ring/50 outline-none transition"
23
+ TOGGLE_CLS = "inline-flex size-7 shrink-0 items-center justify-center rounded-md " \
24
+ "text-sidebar-foreground ring-sidebar-ring outline-hidden " \
25
+ "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground " \
26
+ "focus-visible:ring-2 transition"
24
27
 
25
- NAV_CLS = "flex-1 overflow-y-auto px-2 py-3"
28
+ NAV_CLS = "flex flex-1 flex-col gap-1 overflow-y-auto px-2 py-3"
26
29
 
27
- GROUP_LABEL = "mb-1 px-3 text-xs font-medium uppercase tracking-wide text-muted-foreground " \
28
- "transition-opacity group-data-[collapsed=true]:opacity-0 group-data-[collapsed=true]:h-0 " \
29
- "group-data-[collapsed=true]:overflow-hidden group-data-[collapsed=true]:mb-0"
30
+ GROUP_WRAP = "mb-4 last:mb-0 group-data-[collapsed=true]:mb-0"
30
31
 
31
- ITEM_CLS = "flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors " \
32
- "overflow-hidden " \
33
- "group-data-[collapsed=true]:justify-center group-data-[collapsed=true]:gap-0 " \
34
- "hover:bg-accent hover:text-accent-foreground " \
35
- "aria-[current]:bg-accent aria-[current]:text-foreground aria-[current]:font-semibold " \
36
- "focus-visible:ring-[3px] focus-visible:ring-ring/50 outline-none"
32
+ GROUP_LABEL = "mb-1 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium " \
33
+ "text-sidebar-foreground/70 group-data-[collapsed=true]:hidden"
37
34
 
38
- ITEM_LABEL = "transition-[opacity,width] group-data-[collapsed=true]:w-0 " \
39
- "group-data-[collapsed=true]:opacity-0 group-data-[collapsed=true]:overflow-hidden " \
40
- "whitespace-nowrap"
35
+ ITEM_CLS = "flex w-full items-center gap-2 overflow-hidden rounded-md p-2 " \
36
+ "text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] " \
37
+ "group-data-[collapsed=true]:size-8 group-data-[collapsed=true]:justify-center " \
38
+ "group-data-[collapsed=true]:gap-0 group-data-[collapsed=true]:p-2 " \
39
+ "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground " \
40
+ "focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground " \
41
+ "aria-[current]:bg-sidebar-accent aria-[current]:font-medium " \
42
+ "aria-[current]:text-sidebar-accent-foreground [&>span:last-child]:truncate " \
43
+ "[&_svg]:size-4 [&_svg]:shrink-0"
44
+
45
+ ITEM_LABEL = "whitespace-nowrap transition-[opacity,width] " \
46
+ "group-data-[collapsed=true]:hidden"
41
47
 
42
48
  renders_many :groups, "UI::SidebarComponent::GroupComponent"
43
49
  renders_many :items, "UI::SidebarComponent::ItemComponent"
@@ -82,7 +88,7 @@ module UI
82
88
  def nav_body
83
89
  content_tag(:nav, class: NAV_CLS) do
84
90
  concat safe_join(groups) if groups.any?
85
- concat content_tag(:div, safe_join(items), class: "space-y-0.5") if items.any?
91
+ concat content_tag(:div, safe_join(items), class: "flex flex-col gap-0.5") if items.any?
86
92
  concat content if content?
87
93
  end
88
94
  end
@@ -105,9 +111,9 @@ module UI
105
111
  end
106
112
 
107
113
  def call
108
- content_tag(:div, class: "mb-4", **@html_attrs) do
114
+ content_tag(:div, class: SidebarComponent::GROUP_WRAP, **@html_attrs) do
109
115
  concat content_tag(:p, @label, class: SidebarComponent::GROUP_LABEL) if @label
110
- concat content_tag(:div, safe_join(items), class: "space-y-0.5")
116
+ concat content_tag(:div, safe_join(items), class: "flex flex-col gap-0.5")
111
117
  concat content if content?
112
118
  end
113
119
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module UI
4
4
  class SkeletonComponent < ApplicationComponent
5
- BASE = "bg-accent animate-pulse rounded-md"
5
+ BASE = "animate-pulse rounded-md bg-accent"
6
6
 
7
7
  def initialize(**html_attrs)
8
8
  @extra_class = html_attrs.delete(:class)
@@ -10,18 +10,17 @@ module UI
10
10
  # dial.with_action(label: "Upload", icon: :upload, data: { action: "..." })
11
11
  # end
12
12
 
13
- FAB_CLS = "relative z-50 inline-flex size-14 items-center justify-center rounded-full " \
14
- "bg-primary text-primary-foreground shadow-lg transition-transform " \
15
- "hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-[3px] " \
16
- "focus-visible:ring-ring/50 active:scale-95"
13
+ FAB_CLS = "relative z-50 inline-flex size-12 items-center justify-center rounded-full " \
14
+ "bg-primary text-primary-foreground shadow-xs transition-all outline-none " \
15
+ "#{UI::Styles::FOCUS_RING} active:scale-95"
17
16
 
18
- PANEL_CLS = "absolute bottom-16 right-0 flex flex-col-reverse items-end gap-2"
17
+ PANEL_CLS = "absolute bottom-14 right-0 flex flex-col-reverse items-end gap-2"
19
18
 
20
- ACTION_CLS = "flex items-center gap-2 rounded-full bg-background px-4 py-2 text-sm font-medium " \
21
- "shadow-md border border-border transition-all " \
19
+ ACTION_CLS = "flex items-center gap-2 rounded-full border border-input bg-background px-4 py-2 text-sm font-medium " \
20
+ "whitespace-nowrap shadow-xs transition-all outline-none " \
22
21
  "hover:bg-accent hover:text-accent-foreground " \
23
- "focus-visible:ring-[3px] focus-visible:ring-ring/50 outline-none " \
24
- "whitespace-nowrap"
22
+ "#{UI::Styles::FOCUS_RING} " \
23
+ "dark:border-input dark:bg-input/30 dark:hover:bg-input/50"
25
24
 
26
25
  PLUS_PATH = "M12 5v14M5 12h14"
27
26
 
@@ -2,12 +2,12 @@
2
2
 
3
3
  module UI
4
4
  class SpinnerComponent < ApplicationComponent
5
- BASE = "inline-block animate-spin rounded-full border-2 border-current border-t-transparent"
5
+ LOADER_PATH = "M21 12a9 9 0 1 1-6.219-8.56"
6
6
 
7
7
  SIZES = {
8
8
  sm: "size-4",
9
- default: "size-6",
10
- lg: "size-10"
9
+ default: "size-4",
10
+ lg: "size-6"
11
11
  }.freeze
12
12
 
13
13
  def initialize(size: :default, **html_attrs)
@@ -17,10 +17,19 @@ module UI
17
17
  end
18
18
 
19
19
  def call
20
- content_tag(:span,
21
- content_tag(:span, "Loading...", class: "sr-only"),
22
- class: cn(BASE, SIZES.fetch(@size, SIZES[:default]), @extra_class),
20
+ content_tag(:svg,
21
+ content_tag(:path, nil,
22
+ d: LOADER_PATH,
23
+ "stroke-linecap": "round",
24
+ "stroke-linejoin": "round"),
25
+ xmlns: "http://www.w3.org/2000/svg",
26
+ viewBox: "0 0 24 24",
27
+ fill: "none",
28
+ stroke: "currentColor",
29
+ "stroke-width": "2",
30
+ class: cn("animate-spin text-muted-foreground", SIZES.fetch(@size, SIZES[:default]), @extra_class),
23
31
  role: "status",
32
+ "aria-label": "Loading",
24
33
  **@html_attrs)
25
34
  end
26
35
  end
@@ -18,6 +18,7 @@ module UI
18
18
  content_tag(:ol,
19
19
  class: cn(wrapper_class, @extra_class),
20
20
  "aria-label": "Progress",
21
+ data: { slot: "stepper" },
21
22
  **@html_attrs) do
22
23
  safe_join(@steps.each_with_index.map { |step, i| step_item(step, i) })
23
24
  end
@@ -30,47 +31,47 @@ module UI
30
31
  status = step.fetch(:status, :pending).to_sym
31
32
 
32
33
  if @orientation == :vertical
33
- vertical_item(step, status, is_last)
34
+ vertical_item(step, status, is_last, index)
34
35
  else
35
- horizontal_item(step, status, is_last)
36
+ horizontal_item(step, status, is_last, index)
36
37
  end
37
38
  end
38
39
 
39
- def horizontal_item(step, status, is_last)
40
+ def horizontal_item(step, status, is_last, index)
40
41
  content_tag(:li, class: "flex items-center #{is_last ? '' : 'flex-1'}") do
41
- concat step_circle(status, step[:label])
42
- concat content_tag(:p, step[:label], class: cn("ml-2 text-sm font-medium whitespace-nowrap", label_color(status))) unless step[:label].nil?
42
+ concat step_circle(status, index + 1)
43
+ concat content_tag(:p, step[:label], class: cn("ml-3 text-sm font-medium whitespace-nowrap", label_color(status))) unless step[:label].nil?
43
44
  concat connector(:horizontal, status) unless is_last
44
45
  end
45
46
  end
46
47
 
47
- def vertical_item(step, status, is_last)
48
+ def vertical_item(step, status, is_last, index)
48
49
  content_tag(:li, class: "relative flex gap-4") do
49
50
  concat content_tag(:div, class: "flex flex-col items-center") {
50
- concat step_circle(status, step[:label])
51
+ concat step_circle(status, index + 1)
51
52
  concat connector(:vertical, status) unless is_last
52
53
  }
53
- concat content_tag(:div, class: "pb-6 pt-0.5 min-w-0") {
54
+ concat content_tag(:div, class: "min-w-0 pb-8 pt-0.5") {
54
55
  concat content_tag(:p, step[:label], class: cn("text-sm font-medium", label_color(status)))
55
56
  concat content_tag(:p, step[:description], class: "mt-0.5 text-xs text-muted-foreground") if step[:description]
56
57
  }
57
58
  end
58
59
  end
59
60
 
60
- def step_circle(status, label)
61
- base = "flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-semibold border-2"
61
+ def step_circle(status, number)
62
+ base = "flex size-8 shrink-0 items-center justify-center rounded-full border-2 text-xs font-medium transition-colors"
62
63
  case status
63
64
  when :complete
64
65
  content_tag(:span, check_svg,
65
66
  class: cn(base, "border-primary bg-primary text-primary-foreground"),
66
67
  "aria-label": "Completed")
67
68
  when :current
68
- content_tag(:span, "●",
69
- class: cn(base, "border-primary text-primary"),
69
+ content_tag(:span, number.to_s,
70
+ class: cn(base, "border-primary bg-background text-primary ring-[3px] ring-ring/50"),
70
71
  "aria-current": "step")
71
72
  else
72
- content_tag(:span, "○",
73
- class: cn(base, "border-muted-foreground text-muted-foreground"),
73
+ content_tag(:span, number.to_s,
74
+ class: cn(base, "border-border bg-muted text-muted-foreground"),
74
75
  "aria-label": "Pending")
75
76
  end
76
77
  end
@@ -78,9 +79,9 @@ module UI
78
79
  def connector(direction, status)
79
80
  filled = status == :complete
80
81
  if direction == :horizontal
81
- content_tag(:div, nil, class: cn("h-0.5 flex-1 mx-2", filled ? "bg-primary" : "bg-border"))
82
+ content_tag(:div, nil, class: cn("mx-3 h-0.5 min-w-8 flex-1", filled ? "bg-primary" : "bg-border"))
82
83
  else
83
- content_tag(:div, nil, class: cn("w-0.5 flex-1 my-1", filled ? "bg-primary" : "bg-border"))
84
+ content_tag(:div, nil, class: cn("my-1 w-0.5 flex-1", filled ? "bg-primary" : "bg-border"))
84
85
  end
85
86
  end
86
87