better_ui 0.1.0 → 0.1.1

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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +225 -119
  4. data/app/assets/stylesheets/better_ui/application.css +0 -356
  5. data/app/components/better_ui/application/card/component.html.erb +20 -0
  6. data/app/components/better_ui/application/card/component.rb +214 -0
  7. data/app/components/better_ui/application/main/component.html.erb +9 -0
  8. data/app/components/better_ui/application/main/component.rb +123 -0
  9. data/app/components/better_ui/application/navbar/component.html.erb +92 -0
  10. data/app/components/better_ui/application/navbar/component.rb +136 -0
  11. data/app/components/better_ui/application/sidebar/component.html.erb +190 -0
  12. data/app/components/better_ui/application/sidebar/component.rb +129 -0
  13. data/app/components/better_ui/general/alert/component.html.erb +32 -0
  14. data/app/components/better_ui/general/alert/component.rb +242 -0
  15. data/app/components/better_ui/general/avatar/component.html.erb +20 -0
  16. data/app/components/better_ui/general/avatar/component.rb +301 -0
  17. data/app/components/better_ui/general/badge/component.html.erb +23 -0
  18. data/app/components/better_ui/general/badge/component.rb +248 -0
  19. data/app/components/better_ui/general/breadcrumb/component.html.erb +15 -0
  20. data/app/components/better_ui/general/breadcrumb/component.rb +187 -0
  21. data/app/components/better_ui/general/button/component.html.erb +34 -0
  22. data/app/components/better_ui/general/button/component.rb +214 -0
  23. data/app/components/better_ui/general/divider/component.html.erb +10 -0
  24. data/app/components/better_ui/general/divider/component.rb +226 -0
  25. data/app/components/better_ui/general/field/component.html.erb +27 -0
  26. data/app/components/better_ui/general/field/component.rb +37 -0
  27. data/app/components/better_ui/general/heading/component.html.erb +22 -0
  28. data/app/components/better_ui/general/heading/component.rb +257 -0
  29. data/app/components/better_ui/general/icon/component.html.erb +7 -0
  30. data/app/components/better_ui/general/icon/component.rb +239 -0
  31. data/app/components/better_ui/general/input/checkbox/component.html.erb +5 -0
  32. data/app/components/better_ui/general/input/checkbox/component.rb +238 -0
  33. data/app/components/better_ui/general/input/datetime/component.html.erb +5 -0
  34. data/app/components/better_ui/general/input/datetime/component.rb +223 -0
  35. data/app/components/better_ui/general/input/radio/component.html.erb +5 -0
  36. data/app/components/better_ui/general/input/radio/component.rb +230 -0
  37. data/app/components/better_ui/general/input/select/component.html.erb +16 -0
  38. data/app/components/better_ui/general/input/select/component.rb +184 -0
  39. data/app/components/better_ui/general/input/select/select_component.html.erb +5 -0
  40. data/app/components/better_ui/general/input/select/select_component.rb +37 -0
  41. data/app/components/better_ui/general/input/text/component.html.erb +5 -0
  42. data/app/components/better_ui/general/input/text/component.rb +171 -0
  43. data/app/components/better_ui/general/input/textarea/component.html.erb +5 -0
  44. data/app/components/better_ui/general/input/textarea/component.rb +166 -0
  45. data/app/components/better_ui/general/link/component.html.erb +18 -0
  46. data/app/components/better_ui/general/link/component.rb +258 -0
  47. data/app/components/better_ui/general/panel/component.html.erb +28 -0
  48. data/app/components/better_ui/general/panel/component.rb +249 -0
  49. data/app/components/better_ui/general/progress/component.html.erb +11 -0
  50. data/app/components/better_ui/general/progress/component.rb +160 -0
  51. data/app/components/better_ui/general/spinner/component.html.erb +35 -0
  52. data/app/components/better_ui/general/spinner/component.rb +93 -0
  53. data/app/components/better_ui/general/table/component.html.erb +5 -0
  54. data/app/components/better_ui/general/table/component.rb +217 -0
  55. data/app/components/better_ui/general/table/tbody_component.html.erb +3 -0
  56. data/app/components/better_ui/general/table/tbody_component.rb +30 -0
  57. data/app/components/better_ui/general/table/td_component.html.erb +3 -0
  58. data/app/components/better_ui/general/table/td_component.rb +44 -0
  59. data/app/components/better_ui/general/table/tfoot_component.html.erb +3 -0
  60. data/app/components/better_ui/general/table/tfoot_component.rb +28 -0
  61. data/app/components/better_ui/general/table/th_component.html.erb +6 -0
  62. data/app/components/better_ui/general/table/th_component.rb +51 -0
  63. data/app/components/better_ui/general/table/thead_component.html.erb +3 -0
  64. data/app/components/better_ui/general/table/thead_component.rb +28 -0
  65. data/app/components/better_ui/general/table/tr_component.html.erb +3 -0
  66. data/app/components/better_ui/general/table/tr_component.rb +30 -0
  67. data/app/components/better_ui/general/tag/component.html.erb +3 -0
  68. data/app/components/better_ui/general/tag/component.rb +104 -0
  69. data/app/components/better_ui/general/tooltip/component.html.erb +7 -0
  70. data/app/components/better_ui/general/tooltip/component.rb +239 -0
  71. data/app/helpers/better_ui/application/components/card/card_helper.rb +96 -0
  72. data/app/helpers/better_ui/application/components/card.rb +11 -0
  73. data/app/helpers/better_ui/application/components/main/main_helper.rb +64 -0
  74. data/app/helpers/better_ui/application/components/navbar/navbar_helper.rb +77 -0
  75. data/app/helpers/better_ui/application/components/sidebar/sidebar_helper.rb +51 -0
  76. data/app/helpers/better_ui/application_helper.rb +42 -179
  77. data/app/helpers/better_ui/general/components/alert/alert_helper.rb +57 -0
  78. data/app/helpers/better_ui/general/components/avatar/avatar_helper.rb +29 -0
  79. data/app/helpers/better_ui/general/components/badge/badge_helper.rb +53 -0
  80. data/app/helpers/better_ui/general/components/breadcrumb/breadcrumb_helper.rb +37 -0
  81. data/app/helpers/better_ui/general/components/button/button_helper.rb +65 -0
  82. data/app/helpers/better_ui/general/components/container/container_helper.rb +60 -0
  83. data/app/helpers/better_ui/general/components/divider/divider_helper.rb +63 -0
  84. data/app/helpers/better_ui/general/components/field/field_helper.rb +26 -0
  85. data/app/helpers/better_ui/general/components/heading/heading_helper.rb +72 -0
  86. data/app/helpers/better_ui/general/components/icon/icon_helper.rb +16 -0
  87. data/app/helpers/better_ui/general/components/input/checkbox/checkbox_helper.rb +81 -0
  88. data/app/helpers/better_ui/general/components/input/datetime/datetime_helper.rb +91 -0
  89. data/app/helpers/better_ui/general/components/input/radio/radio_helper.rb +79 -0
  90. data/app/helpers/better_ui/general/components/input/radio_group/radio_group_helper.rb +124 -0
  91. data/app/helpers/better_ui/general/components/input/select/select_helper.rb +70 -0
  92. data/app/helpers/better_ui/general/components/input/text/text_helper.rb +138 -0
  93. data/app/helpers/better_ui/general/components/input/textarea/textarea_helper.rb +73 -0
  94. data/app/helpers/better_ui/general/components/link/link_helper.rb +89 -0
  95. data/app/helpers/better_ui/general/components/panel/panel_helper.rb +83 -0
  96. data/app/helpers/better_ui/general/components/progress/progress_helper.rb +53 -0
  97. data/app/helpers/better_ui/general/components/spinner/spinner_helper.rb +19 -0
  98. data/app/helpers/better_ui/general/components/table/table_helper.rb +53 -0
  99. data/app/helpers/better_ui/general/components/table/tbody_helper.rb +13 -0
  100. data/app/helpers/better_ui/general/components/table/td_helper.rb +19 -0
  101. data/app/helpers/better_ui/general/components/table/tfoot_helper.rb +13 -0
  102. data/app/helpers/better_ui/general/components/table/th_helper.rb +19 -0
  103. data/app/helpers/better_ui/general/components/table/thead_helper.rb +13 -0
  104. data/app/helpers/better_ui/general/components/table/tr_helper.rb +13 -0
  105. data/app/helpers/better_ui/general/components/tag/tag_helper.rb +26 -0
  106. data/app/helpers/better_ui/general/components/tooltip/tooltip_helper.rb +60 -0
  107. data/app/views/layouts/better_ui/application.html.erb +6 -124
  108. data/config/initializers/lookbook.rb +23 -0
  109. data/config/routes.rb +0 -8
  110. data/lib/better_ui/engine.rb +5 -19
  111. data/lib/better_ui/railtie.rb +20 -0
  112. data/lib/better_ui/version.rb +1 -1
  113. data/lib/better_ui.rb +4 -20
  114. metadata +131 -28
  115. data/app/controllers/better_ui/docs_controller.rb +0 -41
  116. data/app/views/better_ui/docs/component.html.erb +0 -365
  117. data/app/views/better_ui/docs/index.html.erb +0 -100
  118. data/app/views/better_ui/docs/show.html.erb +0 -60
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Application
5
+ module Main
6
+ class Component < ViewComponent::Base
7
+ attr_reader :padding, :inner_padding, :rounded, :shadow, :with_sidebar, :sidebar_width, :classes
8
+
9
+ # Arrotondamento bordi con classi Tailwind dirette
10
+ MAIN_ROUNDED = {
11
+ none: "",
12
+ small: "rounded-lg",
13
+ medium: "rounded-xl",
14
+ large: "rounded-2xl",
15
+ full: "rounded-full"
16
+ }.freeze
17
+
18
+ # Ombre con classi Tailwind dirette
19
+ MAIN_SHADOW = {
20
+ none: "",
21
+ small: "shadow-sm",
22
+ medium: "shadow-md",
23
+ large: "shadow-lg"
24
+ }.freeze
25
+
26
+ # Padding con classi Tailwind dirette
27
+ MAIN_PADDING = {
28
+ true: "p-6",
29
+ false: "p-0"
30
+ }.freeze
31
+
32
+ # Classi layout per sidebar
33
+ MAIN_LAYOUT = {
34
+ sidebar: { sm: "pl-48", md: "pl-64", lg: "pl-72", xl: "pl-80" }, # Con Sidebar
35
+ none: "" # Senza sidebar
36
+ }.freeze
37
+
38
+ # Larghezza sidebar (deve corrispondere a SIDEBAR_WIDTHS del componente Sidebar)
39
+ MAIN_SIDEBAR_WIDTH = {
40
+ sm: 48,
41
+ md: 64,
42
+ lg: 72,
43
+ xl: 80
44
+ }.freeze
45
+
46
+ # @param padding [Boolean] Se applicare il padding al contenitore principale
47
+ # @param inner_padding [Boolean] Se applicare il padding al contenitore interno
48
+ # @param rounded [Symbol] Tipo di border-radius del contenitore interno (:none, :small, :medium, :large, :full), default :small
49
+ # @param shadow [Symbol] Tipo di ombra del contenitore interno (:none, :small, :medium, :large), default :medium
50
+ # @param with_sidebar [Boolean] Se lasciare lo spazio per la sidebar
51
+ # @param sidebar_width [Symbol] Larghezza della sidebar (:sm, :md, :lg, :xl), default :md
52
+ # @param classes [String] Classi CSS aggiuntive per il contenitore principale
53
+ def initialize(
54
+ padding: true,
55
+ inner_padding: true,
56
+ rounded: :small,
57
+ shadow: :medium,
58
+ with_sidebar: true,
59
+ sidebar_width: :md,
60
+ classes: nil
61
+ )
62
+ @padding = padding
63
+ @inner_padding = inner_padding
64
+ @rounded = rounded.to_sym
65
+ @shadow = shadow.to_sym
66
+ @with_sidebar = with_sidebar
67
+ @sidebar_width = sidebar_width.to_sym
68
+ @classes = classes
69
+
70
+ validate_params
71
+ end
72
+
73
+
74
+
75
+ private
76
+
77
+ def get_layout_class
78
+ if with_sidebar
79
+ MAIN_LAYOUT[:sidebar][@sidebar_width] || MAIN_LAYOUT[:sidebar][:md]
80
+ else
81
+ MAIN_LAYOUT[:none]
82
+ end
83
+ end
84
+
85
+ def get_padding_class(enabled)
86
+ MAIN_PADDING[enabled] || MAIN_PADDING[false]
87
+ end
88
+
89
+ def get_rounded_class
90
+ MAIN_ROUNDED[@rounded] || MAIN_ROUNDED[:small]
91
+ end
92
+
93
+ def get_shadow_class
94
+ MAIN_SHADOW[@shadow] || MAIN_SHADOW[:medium]
95
+ end
96
+
97
+ def validate_params
98
+ validate_rounded
99
+ validate_shadow
100
+ validate_sidebar_width if with_sidebar
101
+ end
102
+
103
+ def validate_rounded
104
+ unless MAIN_ROUNDED.keys.include?(@rounded)
105
+ raise ArgumentError, "L'arrotondamento deve essere uno tra: #{MAIN_ROUNDED.keys.join(', ')}"
106
+ end
107
+ end
108
+
109
+ def validate_shadow
110
+ unless MAIN_SHADOW.keys.include?(@shadow)
111
+ raise ArgumentError, "L'ombra deve essere una tra: #{MAIN_SHADOW.keys.join(', ')}"
112
+ end
113
+ end
114
+
115
+ def validate_sidebar_width
116
+ unless MAIN_SIDEBAR_WIDTH.keys.include?(@sidebar_width)
117
+ raise ArgumentError, "La larghezza della sidebar deve essere una tra: #{MAIN_SIDEBAR_WIDTH.keys.join(', ')}"
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,92 @@
1
+ <header class="<%= container_classes %>">
2
+ <div class="flex items-center justify-between">
3
+ <!-- Left side - custom content-->
4
+ <div class="flex items-center">
5
+ <%= content %>
6
+ </div>
7
+
8
+ <!-- User actions -->
9
+ <div class="flex items-center space-x-4">
10
+ <% if has_actions? %>
11
+ <% actions.each do |action| %>
12
+ <div class="flex-shrink-0">
13
+ <% if action[:type] == :button %>
14
+ <% if action[:href].present? %>
15
+ <a
16
+ href="<%= action[:href] %>"
17
+ class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors duration-150 <%= action[:theme] == :primary ? 'bg-gray-900 text-white hover:bg-gray-700' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' %>"
18
+ >
19
+ <% if action[:icon].present? %>
20
+ <%= bui_icon(action[:icon], size: :small) %>
21
+ <% if action[:label].present? %>
22
+ <span class="ml-2"><%= action[:label] %></span>
23
+ <% end %>
24
+ <% else %>
25
+ <%= action[:label] %>
26
+ <% end %>
27
+ </a>
28
+ <% else %>
29
+ <button
30
+ type="button"
31
+ class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors duration-150 <%= action[:theme] == :primary ? 'bg-gray-900 text-white hover:bg-gray-700' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' %>"
32
+ <% if action[:data].present? %>
33
+ <% action[:data].each do |key, value| %>
34
+ data-<%= key %>="<%= value %>"
35
+ <% end %>
36
+ <% end %>
37
+ >
38
+ <% if action[:icon].present? %>
39
+ <%= bui_icon(action[:icon], size: :small) %>
40
+ <% if action[:label].present? %>
41
+ <span class="ml-2"><%= action[:label] %></span>
42
+ <% end %>
43
+ <% else %>
44
+ <%= action[:label] %>
45
+ <% end %>
46
+ </button>
47
+ <% end %>
48
+ <% elsif action[:type] == :avatar %>
49
+ <% if action[:href].present? %>
50
+ <a href="<%= action[:href] %>" class="flex-shrink-0 hover:opacity-75 transition-opacity duration-150">
51
+ <% end %>
52
+
53
+ <% if action[:avatar].is_a?(Hash) %>
54
+ <%= bui_avatar(**action[:avatar]) %>
55
+ <% else %>
56
+ <%= action[:avatar].html_safe %>
57
+ <% end %>
58
+
59
+ <% if action[:href].present? %>
60
+ </a>
61
+ <% end %>
62
+ <% elsif action[:type] == :icon %>
63
+ <% if action[:href].present? %>
64
+ <a
65
+ href="<%= action[:href] %>"
66
+ class="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-md transition-colors duration-150"
67
+ >
68
+ <%= bui_icon(action[:icon], size: :medium) %>
69
+ </a>
70
+ <% else %>
71
+ <button
72
+ type="button"
73
+ class="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-md transition-colors duration-150"
74
+ <% if action[:data].present? %>
75
+ <% action[:data].each do |key, value| %>
76
+ data-<%= key %>="<%= value %>"
77
+ <% end %>
78
+ <% end %>
79
+ >
80
+ <%= bui_icon(action[:icon], size: :medium) %>
81
+ </button>
82
+ <% end %>
83
+ <% else %>
84
+ <!-- Custom content -->
85
+ <%= action[:content].html_safe if action[:content].present? %>
86
+ <% end %>
87
+ </div>
88
+ <% end %>
89
+ <% end %>
90
+ </div>
91
+ </div>
92
+ </header>
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Application
5
+ module Navbar
6
+ class Component < ViewComponent::Base
7
+ # Include degli helper per utilizzare bui_icon e bui_avatar
8
+ include BetterUi::General::Components::Icon::IconHelper
9
+ include BetterUi::General::Components::Avatar::AvatarHelper
10
+
11
+ attr_reader :theme, :shadow, :border, :actions, :classes, :with_sidebar, :sidebar_width
12
+
13
+ # Temi navbar con classi Tailwind dirette
14
+ NAVBAR_THEME = {
15
+ default: "bg-white text-gray-900 border-gray-200",
16
+ white: "bg-white text-gray-900 border-gray-200",
17
+ red: "bg-red-50 text-red-900 border-red-200",
18
+ rose: "bg-rose-50 text-rose-900 border-rose-200",
19
+ orange: "bg-orange-50 text-orange-900 border-orange-200",
20
+ green: "bg-green-50 text-green-900 border-green-200",
21
+ blue: "bg-blue-50 text-blue-900 border-blue-200",
22
+ yellow: "bg-yellow-50 text-yellow-900 border-yellow-200",
23
+ violet: "bg-violet-50 text-violet-900 border-violet-200"
24
+ }.freeze
25
+
26
+ # Ombre navbar con classi Tailwind dirette
27
+ NAVBAR_SHADOW = {
28
+ none: "",
29
+ small: "shadow-sm",
30
+ medium: "shadow-md",
31
+ large: "shadow-lg"
32
+ }.freeze
33
+
34
+ # Larghezze sidebar con valori numerici per utilizzo in classi custom
35
+ NAVBAR_SIDEBAR_WIDTH = {
36
+ sm: 48,
37
+ md: 64,
38
+ lg: 72,
39
+ xl: 80
40
+ }.freeze
41
+
42
+ # @param theme [Symbol] Tema colori (default, white, red, rose, orange, green, blue, yellow, violet), default :default
43
+ # @param shadow [Symbol] Tipo di ombra (none, small, medium, large), default :small
44
+ # @param border [Boolean] Se mostrare il bordo inferiore, default true
45
+ # @param actions [Array] Array di azioni/pulsanti a destra
46
+ # @param classes [String] Classi CSS aggiuntive
47
+ # @param with_sidebar [Boolean] Se la navbar è affiancata a una sidebar, default false
48
+ # @param sidebar_width [Symbol] Larghezza della sidebar affiancata (:sm, :md, :lg, :xl), default :md
49
+ def initialize(
50
+ theme: :default,
51
+ shadow: :small,
52
+ border: false,
53
+ actions: [],
54
+ classes: nil,
55
+ with_sidebar: false,
56
+ sidebar_width: :md
57
+ )
58
+ @theme = theme.to_sym
59
+ @shadow = shadow.to_sym
60
+ @border = border
61
+ @actions = actions || []
62
+ @classes = classes
63
+ @with_sidebar = with_sidebar
64
+ @sidebar_width = sidebar_width.to_sym
65
+
66
+ validate_params
67
+ end
68
+
69
+ def container_classes
70
+ base_classes = %w[h-[81px] px-6 py-4]
71
+
72
+ # Width
73
+ if with_sidebar
74
+ sidebar_width_value = NAVBAR_SIDEBAR_WIDTH[@sidebar_width] || NAVBAR_SIDEBAR_WIDTH[:md]
75
+ base_classes << "pl-#{sidebar_width_value}"
76
+ base_classes << "ml-auto"
77
+ else
78
+ base_classes << "w-full"
79
+ end
80
+
81
+ # Tema
82
+ base_classes.concat(theme_classes.split)
83
+
84
+ # Shadow
85
+ base_classes << shadow_class if shadow != :none
86
+
87
+ # Border
88
+ base_classes << "border-b" if border
89
+
90
+ # Classi aggiuntive
91
+ base_classes << classes if classes.present?
92
+
93
+ base_classes.compact.join(" ")
94
+ end
95
+
96
+ def has_actions?
97
+ actions.present? && actions.any?
98
+ end
99
+
100
+ private
101
+
102
+ def theme_classes
103
+ NAVBAR_THEME[@theme] || NAVBAR_THEME[:default]
104
+ end
105
+
106
+ def shadow_class
107
+ NAVBAR_SHADOW[@shadow] || NAVBAR_SHADOW[:small]
108
+ end
109
+
110
+ def validate_params
111
+ validate_theme
112
+ validate_shadow
113
+ validate_sidebar_width if with_sidebar
114
+ end
115
+
116
+ def validate_theme
117
+ unless NAVBAR_THEME.keys.include?(@theme)
118
+ raise ArgumentError, "Il tema deve essere uno tra: #{NAVBAR_THEME.keys.join(', ')}"
119
+ end
120
+ end
121
+
122
+ def validate_shadow
123
+ unless NAVBAR_SHADOW.keys.include?(@shadow)
124
+ raise ArgumentError, "L'ombra deve essere una tra: #{NAVBAR_SHADOW.keys.join(', ')}"
125
+ end
126
+ end
127
+
128
+ def validate_sidebar_width
129
+ unless NAVBAR_SIDEBAR_WIDTH.keys.include?(@sidebar_width)
130
+ raise ArgumentError, "La larghezza della sidebar deve essere una tra: #{NAVBAR_SIDEBAR_WIDTH.keys.join(', ')}"
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,190 @@
1
+ <aside class="<%= container_classes %>">
2
+ <!-- Header Section -->
3
+ <% if has_header? %>
4
+ <div class="px-6 py-4 border-b border-gray-200">
5
+ <% if header[:logo].present? %>
6
+ <div class="flex items-center">
7
+ <div class="flex-shrink-0">
8
+ <% if header[:logo].is_a?(Hash) %>
9
+ <%= bui_avatar(**header[:logo]) %>
10
+ <% else %>
11
+ <%= header[:logo].html_safe %>
12
+ <% end %>
13
+ </div>
14
+ <% if header[:title].present? %>
15
+ <div class="ml-3">
16
+ <h2 class="text-lg font-semibold text-gray-900"><%= header[:title] %></h2>
17
+ <% if header[:subtitle].present? %>
18
+ <p class="text-sm text-gray-500"><%= header[:subtitle] %></p>
19
+ <% end %>
20
+ </div>
21
+ <% end %>
22
+ </div>
23
+ <% elsif header[:title].present? %>
24
+ <div>
25
+ <h2 class="text-lg font-semibold text-gray-900"><%= header[:title] %></h2>
26
+ <% if header[:subtitle].present? %>
27
+ <p class="text-sm text-gray-500"><%= header[:subtitle] %></p>
28
+ <% end %>
29
+ </div>
30
+ <% end %>
31
+ </div>
32
+ <% end %>
33
+
34
+ <!-- Navigation Section -->
35
+ <nav class="flex-1 px-4 py-6 space-y-6">
36
+ <% navigation_sections.each do |section| %>
37
+ <div class="space-y-2">
38
+ <!-- Section Title -->
39
+ <% if section[:title].present? %>
40
+ <% if section[:href] %>
41
+ <%= link_to section[:href], class: "px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider" do %>
42
+ <%= section[:title] %>
43
+ <% end %>
44
+ <% else %>
45
+ <h3 class="px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">
46
+ <%= section[:title] %>
47
+ </h3>
48
+ <% end %>
49
+ <% end %>
50
+
51
+ <!-- Navigation Items -->
52
+ <div class="space-y-1">
53
+ <% (section[:items] || []).each do |item| %>
54
+ <% if item[:type] == :collapsible && collapsible %>
55
+ <!-- Collapsible Section -->
56
+ <div>
57
+ <button
58
+ type="button"
59
+ class="group flex items-center justify-between w-full px-3 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-50 hover:text-gray-900 transition-colors duration-150"
60
+ data-collapse-target="#<%= item[:id] %>"
61
+ aria-expanded="<%= item[:expanded] || false %>"
62
+ >
63
+ <div class="flex items-center">
64
+ <% if item[:icon].present? %>
65
+ <span class="mr-3 text-gray-400 group-hover:text-gray-500">
66
+ <%= bui_icon(item[:icon], size: :medium) %>
67
+ </span>
68
+ <% end %>
69
+ <span><%= item[:label] %></span>
70
+ </div>
71
+ <span class="ml-3 transform transition-transform duration-150 <%= 'rotate-90' if item[:expanded] %>">
72
+ <%= bui_icon("chevron-right", size: :small) %>
73
+ </span>
74
+ </button>
75
+
76
+ <div
77
+ id="<%= item[:id] %>"
78
+ class="<%= item[:expanded] ? 'block' : 'hidden' %> mt-1 space-y-1"
79
+ >
80
+ <% (item[:children] || []).each do |child| %>
81
+ <% if child[:href].present? %>
82
+ <a
83
+ href="<%= child[:href] %>"
84
+ class="<%= (child[:active] || false) ? 'group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors duration-150 bg-gray-100 text-gray-900' : 'group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors duration-150 text-gray-700 hover:bg-gray-50 hover:text-gray-900' %> pl-11"
85
+ >
86
+ <% if child[:icon].present? %>
87
+ <span class="mr-3">
88
+ <%= bui_icon(child[:icon], size: :small) %>
89
+ </span>
90
+ <% end %>
91
+ <%= child[:label] %>
92
+ </a>
93
+ <% else %>
94
+ <div class="<%= (child[:active] || false) ? 'group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors duration-150 bg-gray-100 text-gray-900' : 'group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors duration-150 text-gray-700 hover:bg-gray-50 hover:text-gray-900' %> pl-11 cursor-default">
95
+ <% if child[:icon].present? %>
96
+ <span class="mr-3">
97
+ <%= bui_icon(child[:icon], size: :small) %>
98
+ </span>
99
+ <% end %>
100
+ <%= child[:label] %>
101
+ </div>
102
+ <% end %>
103
+ <% end %>
104
+ </div>
105
+ </div>
106
+ <% else %>
107
+ <!-- Regular Navigation Item -->
108
+ <% if item[:href].present? %>
109
+ <a
110
+ href="<%= item[:href] %>"
111
+ class="<%= (item[:active] || false) ? 'group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors duration-150 bg-gray-100 text-gray-900' : 'group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors duration-150 text-gray-700 hover:bg-gray-50 hover:text-gray-900' %>"
112
+ >
113
+ <% if item[:icon].present? %>
114
+ <span class="mr-3 text-gray-400 group-hover:text-gray-500">
115
+ <%= bui_icon(item[:icon], size: :medium) %>
116
+ </span>
117
+ <% end %>
118
+ <%= item[:label] %>
119
+ <% if item[:badge].present? %>
120
+ <span class="ml-auto inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
121
+ <%= item[:badge] %>
122
+ </span>
123
+ <% end %>
124
+ </a>
125
+ <% else %>
126
+ <div class="<%= (item[:active] || false) ? 'group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors duration-150 bg-gray-100 text-gray-900' : 'group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors duration-150 text-gray-700 hover:bg-gray-50 hover:text-gray-900' %> cursor-default">
127
+ <% if item[:icon].present? %>
128
+ <span class="mr-3 text-gray-400">
129
+ <%= bui_icon(item[:icon], size: :medium) %>
130
+ </span>
131
+ <% end %>
132
+ <%= item[:label] %>
133
+ <% if item[:badge].present? %>
134
+ <span class="ml-auto inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
135
+ <%= item[:badge] %>
136
+ </span>
137
+ <% end %>
138
+ </div>
139
+ <% end %>
140
+ <% end %>
141
+ <% end %>
142
+ </div>
143
+ </div>
144
+ <% end %>
145
+
146
+ <%= content if content.present? %>
147
+ </nav>
148
+
149
+ <!-- Footer Section -->
150
+ <% if has_footer? %>
151
+ <div class="px-6 py-4 border-t border-gray-200">
152
+ <% if footer[:user_info].present? %>
153
+ <div class="flex items-center">
154
+ <% if footer[:user_info][:avatar].present? %>
155
+ <div class="flex-shrink-0">
156
+ <% if footer[:user_info][:avatar].is_a?(Hash) %>
157
+ <%= bui_avatar(**footer[:user_info][:avatar]) %>
158
+ <% else %>
159
+ <%= footer[:user_info][:avatar].html_safe %>
160
+ <% end %>
161
+ </div>
162
+ <% end %>
163
+ <div class="<%= footer[:user_info][:avatar].present? ? 'ml-3' : '' %>">
164
+ <% if footer[:user_info][:name].present? %>
165
+ <p class="text-sm font-medium text-gray-700">
166
+ <%= footer[:user_info][:name] %>
167
+ </p>
168
+ <% end %>
169
+ <% if footer[:user_info][:email].present? %>
170
+ <p class="text-xs text-gray-500">
171
+ <%= footer[:user_info][:email] %>
172
+ </p>
173
+ <% end %>
174
+ </div>
175
+ <% if footer[:user_info][:menu_button].present? %>
176
+ <div class="ml-auto">
177
+ <%= footer[:user_info][:menu_button] %>
178
+ </div>
179
+ <% end %>
180
+ </div>
181
+ <% end %>
182
+
183
+ <% if footer[:content].present? %>
184
+ <div class="<%= footer[:user_info].present? ? 'mt-4' : '' %>">
185
+ <%= footer[:content] %>
186
+ </div>
187
+ <% end %>
188
+ </div>
189
+ <% end %>
190
+ </aside>
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Application
5
+ module Sidebar
6
+ class Component < ViewComponent::Base
7
+ # Include degli helper per utilizzare bui_icon e bui_avatar
8
+ include BetterUi::General::Components::Icon::IconHelper
9
+ include BetterUi::General::Components::Avatar::AvatarHelper
10
+ attr_reader :width, :position, :theme, :shadow, :border, :header, :footer, :navigation_sections, :collapsible, :classes
11
+
12
+ # Larghezze sidebar con classi Tailwind dirette
13
+ SIDEBAR_WIDTHS = {
14
+ sm: "w-48",
15
+ md: "w-64",
16
+ lg: "w-72",
17
+ xl: "w-80"
18
+ }
19
+
20
+ # Temi sidebar con classi Tailwind dirette
21
+ SIDEBAR_THEMES = {
22
+ default: "bg-white text-gray-900",
23
+ dark: "bg-gray-900 text-white",
24
+ light: "bg-white text-gray-900"
25
+ }
26
+
27
+ # Ombre sidebar con classi Tailwind dirette
28
+ SIDEBAR_SHADOWS = {
29
+ none: "",
30
+ sm: "shadow-sm",
31
+ md: "shadow-md",
32
+ lg: "shadow-lg",
33
+ xl: "shadow-xl"
34
+ }
35
+
36
+ # Bordi sidebar con classi Tailwind dirette
37
+ SIDEBAR_BORDERS = {
38
+ left: "border-r border-gray-200",
39
+ right: "border-l border-gray-200"
40
+ }
41
+
42
+ # @param width [Symbol] Larghezza della sidebar (:sm, :md, :lg, :xl), default :md (w-64)
43
+ # @param position [Symbol] Posizione della sidebar (:left, :right), default :left
44
+ # @param theme [Symbol] Tema colori (:default, :dark, :light), default :default
45
+ # @param shadow [Symbol] Tipo di ombra (:none, :sm, :md, :lg), default :lg
46
+ # @param border [Boolean] Se mostrare il bordo destro/sinistro, default true
47
+ # @param header [Hash] Configurazione header (logo, title, subtitle)
48
+ # @param footer [Hash] Configurazione footer (content, user_info)
49
+ # @param navigation_sections [Array] Array di sezioni di navigazione
50
+ # @param collapsible [Boolean] Se abilitare sezioni collassabili, default true
51
+ # @param classes [String] Classi CSS aggiuntive
52
+ def initialize(
53
+ width: :md,
54
+ position: :left,
55
+ theme: :default,
56
+ shadow: :lg,
57
+ border: true,
58
+ header: {},
59
+ footer: {},
60
+ navigation_sections: [],
61
+ collapsible: true,
62
+ classes: nil
63
+ )
64
+ @width = width.to_sym
65
+ @position = position.to_sym
66
+ @theme = theme.to_sym
67
+ @shadow = shadow.to_sym
68
+ @border = border
69
+ @header = header || {}
70
+ @footer = footer || {}
71
+ @navigation_sections = navigation_sections || []
72
+ @collapsible = collapsible
73
+ @classes = classes
74
+ end
75
+
76
+ def container_classes
77
+ base_classes = %w[fixed inset-y-0 z-50 flex flex-col overflow-y-auto]
78
+
79
+ # Posizione
80
+ base_classes << (position == :right ? "right-0" : "left-0")
81
+
82
+ # Larghezza
83
+ base_classes << width_class
84
+
85
+ # Tema
86
+ base_classes.concat(theme_classes)
87
+
88
+ # Shadow
89
+ base_classes << shadow_class if shadow != :none
90
+
91
+ # Border
92
+ base_classes << border_class if border
93
+
94
+ # Classi aggiuntive
95
+ base_classes << classes if classes.present?
96
+
97
+ base_classes.compact.join(" ")
98
+ end
99
+
100
+
101
+ def has_header?
102
+ header.present? && (header[:title].present? || header[:logo].present?)
103
+ end
104
+
105
+ def has_footer?
106
+ footer.present? && (footer[:content].present? || footer[:user_info].present?)
107
+ end
108
+
109
+ private
110
+
111
+ def width_class
112
+ SIDEBAR_WIDTHS[@width] || SIDEBAR_WIDTHS[:md]
113
+ end
114
+
115
+ def theme_classes
116
+ (SIDEBAR_THEMES[@theme] || SIDEBAR_THEMES[:default]).split
117
+ end
118
+
119
+ def shadow_class
120
+ SIDEBAR_SHADOWS[@shadow] || SIDEBAR_SHADOWS[:none]
121
+ end
122
+
123
+ def border_class
124
+ SIDEBAR_BORDERS[@position] || SIDEBAR_BORDERS[:left]
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end