panda-core 0.2.4 → 0.6.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/tailwind/application.css +199 -7
  3. data/app/assets/tailwind/tailwind.config.js +8 -0
  4. data/app/components/panda/core/UI/badge.rb +107 -0
  5. data/app/components/panda/core/UI/button.rb +110 -0
  6. data/app/components/panda/core/UI/card.rb +88 -0
  7. data/app/components/panda/core/admin/breadcrumb_component.rb +133 -0
  8. data/app/components/panda/core/admin/button_component.rb +46 -28
  9. data/app/components/panda/core/admin/container_component.rb +75 -4
  10. data/app/components/panda/core/admin/file_gallery_component.rb +157 -0
  11. data/app/components/panda/core/admin/flash_message_component.rb +98 -15
  12. data/app/components/panda/core/admin/form_error_component.rb +48 -0
  13. data/app/components/panda/core/admin/form_input_component.rb +50 -0
  14. data/app/components/panda/core/admin/form_select_component.rb +68 -0
  15. data/app/components/panda/core/admin/heading_component.rb +53 -24
  16. data/app/components/panda/core/admin/page_header_component.rb +107 -0
  17. data/app/components/panda/core/admin/panel_component.rb +33 -4
  18. data/app/components/panda/core/admin/slideover_component.rb +66 -4
  19. data/app/components/panda/core/admin/statistics_component.rb +19 -0
  20. data/app/components/panda/core/admin/tab_bar_component.rb +101 -0
  21. data/app/components/panda/core/admin/table_component.rb +92 -11
  22. data/app/components/panda/core/admin/tag_component.rb +58 -16
  23. data/app/components/panda/core/admin/user_activity_component.rb +43 -0
  24. data/app/components/panda/core/admin/user_display_component.rb +77 -0
  25. data/app/components/panda/core/base.rb +122 -0
  26. data/app/controllers/panda/core/admin/base_controller.rb +68 -0
  27. data/app/controllers/panda/core/admin/dashboard_controller.rb +5 -3
  28. data/app/controllers/panda/core/admin/my_profile_controller.rb +4 -4
  29. data/app/controllers/panda/core/admin/sessions_controller.rb +15 -8
  30. data/app/controllers/panda/core/admin/test_sessions_controller.rb +60 -0
  31. data/app/helpers/panda/core/asset_helper.rb +31 -5
  32. data/app/helpers/panda/core/sessions_helper.rb +27 -2
  33. data/app/javascript/panda/core/application.js +8 -1
  34. data/app/javascript/panda/core/controllers/alert_controller.js +38 -0
  35. data/app/javascript/panda/core/controllers/index.js +3 -3
  36. data/app/javascript/panda/core/controllers/toggle_controller.js +41 -0
  37. data/app/javascript/panda/core/tailwindplus-elements.js +31 -0
  38. data/app/javascript/panda/core/vendor/@hotwired--stimulus.js +4 -0
  39. data/app/javascript/panda/core/vendor/@hotwired--turbo.js +160 -0
  40. data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +4 -0
  41. data/app/models/panda/core/user.rb +61 -14
  42. data/app/services/panda/core/attach_avatar_service.rb +67 -0
  43. data/app/views/layouts/panda/core/admin.html.erb +40 -3
  44. data/app/views/layouts/panda/core/admin_simple.html.erb +6 -0
  45. data/app/views/panda/core/admin/dashboard/_default_content.html.erb +4 -4
  46. data/app/views/panda/core/admin/dashboard/show.html.erb +2 -2
  47. data/app/views/panda/core/admin/my_profile/edit.html.erb +36 -25
  48. data/app/views/panda/core/admin/sessions/new.html.erb +9 -10
  49. data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +27 -34
  50. data/app/views/panda/core/admin/shared/_flash.html.erb +4 -30
  51. data/app/views/panda/core/admin/shared/_sidebar.html.erb +41 -20
  52. data/app/views/panda/core/shared/_header.html.erb +13 -5
  53. data/config/importmap.rb +19 -6
  54. data/config/routes.rb +10 -3
  55. data/db/migrate/20250810120000_add_current_theme_to_panda_core_users.rb +7 -0
  56. data/db/migrate/20250811120000_add_oauth_avatar_url_to_panda_core_users.rb +7 -0
  57. data/lib/panda/core/asset_loader.rb +23 -8
  58. data/lib/panda/core/configuration.rb +12 -9
  59. data/lib/panda/core/debug.rb +47 -0
  60. data/lib/panda/core/engine.rb +55 -9
  61. data/lib/panda/core/services/base_service.rb +19 -4
  62. data/lib/panda/core/version.rb +1 -1
  63. data/lib/panda/core.rb +2 -0
  64. data/lib/tasks/panda_core_users.rake +158 -0
  65. metadata +103 -14
  66. data/app/components/panda/core/admin/container_component.html.erb +0 -12
  67. data/app/components/panda/core/admin/flash_message_component.html.erb +0 -31
  68. data/app/components/panda/core/admin/panel_component.html.erb +0 -7
  69. data/app/components/panda/core/admin/slideover_component.html.erb +0 -9
  70. data/app/components/panda/core/admin/table_component.html.erb +0 -29
  71. data/app/controllers/panda/core/admin_controller.rb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea69195a180331f2758bbae8cfc22ccae44cf8b8ac0133b0642c8e4fb7e11e41
4
- data.tar.gz: 495cd12223dc77a2c3893568732ae8ae73374bc0bdaf940cf4498227eae082dc
3
+ metadata.gz: aafa859a0f30c0eac8666376380b18608a5429f760a1f30f2db283504458533d
4
+ data.tar.gz: 8e63ccaa5fd464c9a7e403a6fa2d3f789bfb0106ce824c31e59f30ffc0bc3dae
5
5
  SHA512:
6
- metadata.gz: eb217e7a4b3f7ddfee0e9e13e252c5fe31a66535b1b239badf9b0b1bfcd71906effd222b0eb2457b6bd4cd309ba69ac8f7edca6db675f93661b7706969728267
7
- data.tar.gz: 7b39637eb2c83328f84f349ff0006b550741d9c91afacdbdc5c9f11102dc25baf58ae24dd514b87891ec1a2c1d0bfa3fee7b623d919a0113ef4644ea2d3d2a62
6
+ metadata.gz: 8508b00f4c356dfceb223952d34dec3d38eec94078043ab40f08ebcdf2f1e484c2eefbe8534f3c1b86de5b62b71b0cc62dc0c63168e6053f68cf7ed9bf879198
7
+ data.tar.gz: 8c2c61ff18769f7ee23defaaae9f6d24703328a7e8269079af53508a811ce71dedd8948a0c0906f5927ec8f66e65e04319f4649541e3b928291712f49559088c
@@ -1,6 +1,15 @@
1
- @import "tailwindcss";
1
+ @import 'tailwindcss';
2
+
3
+ @source "../../app/views/**/*.html.erb";
4
+ @source "../../app/components/**/*.html.erb";
5
+ @source "../../app/components/**/*.rb";
6
+ @source "../../app/helpers/**/*.rb";
7
+ @source "../../../cms/app/views/**/*.html.erb";
8
+ @source "../../../cms/app/components/**/*.html.erb";
9
+ @source "../../../cms/app/components/**/*.rb";
2
10
 
3
11
  @theme {
12
+ /* Legacy colors */
4
13
  --color-white: var(--color-white);
5
14
  --color-black: var(--color-black);
6
15
  --color-light: var(--color-light);
@@ -11,13 +20,53 @@
11
20
  --color-inactive: var(--color-inactive);
12
21
  --color-warning: var(--color-warning);
13
22
  --color-error: var(--color-error);
23
+
24
+ /* Tailwind UI compatible primary scale */
25
+ --color-primary-50: var(--color-primary-50);
26
+ --color-primary-100: var(--color-primary-100);
27
+ --color-primary-200: var(--color-primary-200);
28
+ --color-primary-300: var(--color-primary-300);
29
+ --color-primary-400: var(--color-primary-400);
30
+ --color-primary-500: var(--color-primary-500);
31
+ --color-primary-600: var(--color-primary-600);
32
+ --color-primary-700: var(--color-primary-700);
33
+ --color-primary-800: var(--color-primary-800);
34
+ --color-primary-900: var(--color-primary-900);
35
+ --color-primary-950: var(--color-primary-950);
36
+
37
+ /* Map indigo to primary for Tailwind UI compatibility */
38
+ --color-indigo-50: var(--color-primary-50);
39
+ --color-indigo-100: var(--color-primary-100);
40
+ --color-indigo-200: var(--color-primary-200);
41
+ --color-indigo-300: var(--color-primary-300);
42
+ --color-indigo-400: var(--color-primary-400);
43
+ --color-indigo-500: var(--color-primary-500);
44
+ --color-indigo-600: var(--color-primary-600);
45
+ --color-indigo-700: var(--color-primary-700);
46
+ --color-indigo-800: var(--color-primary-800);
47
+ --color-indigo-900: var(--color-primary-900);
48
+ --color-indigo-950: var(--color-primary-950);
49
+
50
+ /* Gray scale */
51
+ --color-gray-50: var(--color-gray-50);
52
+ --color-gray-100: var(--color-gray-100);
53
+ --color-gray-200: var(--color-gray-200);
54
+ --color-gray-300: var(--color-gray-300);
55
+ --color-gray-400: var(--color-gray-400);
56
+ --color-gray-500: var(--color-gray-500);
57
+ --color-gray-600: var(--color-gray-600);
58
+ --color-gray-700: var(--color-gray-700);
59
+ --color-gray-800: var(--color-gray-800);
60
+ --color-gray-900: var(--color-gray-900);
61
+ --color-gray-950: var(--color-gray-950);
14
62
  }
15
63
 
16
64
  @layer base {
17
- html[data-theme="default"] {
65
+ html[data-theme='default'] {
18
66
  --color-white: rgb(249, 249, 249); /* #F9F9F9 */
19
67
  --color-black: rgb(26, 22, 29); /* #1A161D */
20
68
 
69
+ /* Legacy color variables (kept for backwards compatibility) */
21
70
  --color-light: rgb(238, 206, 230); /* #EECEE6 */
22
71
  --color-mid: rgb(141, 94, 183); /* #8D5EB7 */
23
72
  --color-dark: rgb(33, 29, 73); /* #211D49 */
@@ -28,11 +77,39 @@
28
77
  --color-warning: rgb(250, 207, 142); /* #FACF8E */
29
78
  --color-inactive: rgb(216, 247, 245); /* #d6e4f7 */
30
79
  --color-error: rgb(245, 129, 129); /* #F58181 */
80
+
81
+ /* Full Tailwind UI compatible color scale - Primary (Purple palette) */
82
+ --color-primary-50: rgb(250, 245, 255); /* Very light purple */
83
+ --color-primary-100: rgb(243, 232, 255); /* Lighter purple */
84
+ --color-primary-200: rgb(233, 213, 255); /* Light purple */
85
+ --color-primary-300: rgb(216, 180, 254); /* Medium light purple */
86
+ --color-primary-400: rgb(192, 132, 252); /* Medium purple */
87
+ --color-primary-500: rgb(141, 94, 183); /* Main brand color (mid) */
88
+ --color-primary-600: rgb(120, 80, 160); /* Darker purple */
89
+ --color-primary-700: rgb(100, 65, 140); /* Dark purple */
90
+ --color-primary-800: rgb(60, 45, 100); /* Very dark purple */
91
+ --color-primary-900: rgb(33, 29, 73); /* Darkest (dark) */
92
+ --color-primary-950: rgb(24, 20, 50); /* Ultra dark */
93
+
94
+ /* Gray scale for UI elements */
95
+ --color-gray-50: rgb(249, 250, 251);
96
+ --color-gray-100: rgb(243, 244, 246);
97
+ --color-gray-200: rgb(229, 231, 235);
98
+ --color-gray-300: rgb(209, 213, 219);
99
+ --color-gray-400: rgb(156, 163, 175);
100
+ --color-gray-500: rgb(107, 114, 128);
101
+ --color-gray-600: rgb(75, 85, 99);
102
+ --color-gray-700: rgb(55, 65, 81);
103
+ --color-gray-800: rgb(31, 41, 55);
104
+ --color-gray-900: rgb(17, 24, 39);
105
+ --color-gray-950: rgb(3, 7, 18);
31
106
  }
32
107
 
33
- html[data-theme="sky"] {
108
+ html[data-theme='sky'] {
34
109
  --color-white: rgb(249, 249, 249); /* #F9F9F9 */
35
110
  --color-black: rgb(26, 22, 29); /* #1A161D */
111
+
112
+ /* Legacy color variables (kept for backwards compatibility) */
36
113
  --color-light: rgb(204, 238, 242); /* #CCEEF2 */
37
114
  --color-mid: rgb(42, 102, 159); /* #2A669F */
38
115
  --color-dark: rgb(20, 32, 74); /* #14204A */
@@ -42,24 +119,139 @@
42
119
  --color-warning: rgb(244, 190, 102); /* #F4BE66 */
43
120
  --color-inactive: rgb(216, 247, 245); /* #d6e4f7 */
44
121
  --color-error: rgb(208, 64, 20); /* #D04014 */
122
+
123
+ /* Full Tailwind UI compatible color scale - Primary (Blue palette) */
124
+ --color-primary-50: rgb(240, 249, 255); /* Very light blue */
125
+ --color-primary-100: rgb(224, 242, 254); /* Lighter blue */
126
+ --color-primary-200: rgb(186, 230, 253); /* Light blue */
127
+ --color-primary-300: rgb(125, 211, 252); /* Medium light blue */
128
+ --color-primary-400: rgb(56, 189, 248); /* Medium blue */
129
+ --color-primary-500: rgb(42, 102, 159); /* Main brand color (mid) */
130
+ --color-primary-600: rgb(35, 85, 132); /* Darker blue */
131
+ --color-primary-700: rgb(28, 68, 106); /* Dark blue */
132
+ --color-primary-800: rgb(22, 52, 82); /* Very dark blue */
133
+ --color-primary-900: rgb(20, 32, 74); /* Darkest (dark) */
134
+ --color-primary-950: rgb(15, 24, 55); /* Ultra dark */
135
+
136
+ /* Gray scale for UI elements (same across themes) */
137
+ --color-gray-50: rgb(249, 250, 251);
138
+ --color-gray-100: rgb(243, 244, 246);
139
+ --color-gray-200: rgb(229, 231, 235);
140
+ --color-gray-300: rgb(209, 213, 219);
141
+ --color-gray-400: rgb(156, 163, 175);
142
+ --color-gray-500: rgb(107, 114, 128);
143
+ --color-gray-600: rgb(75, 85, 99);
144
+ --color-gray-700: rgb(55, 65, 81);
145
+ --color-gray-800: rgb(31, 41, 55);
146
+ --color-gray-900: rgb(17, 24, 39);
147
+ --color-gray-950: rgb(3, 7, 18);
45
148
  }
46
149
 
47
150
  a.block-link:after {
48
151
  position: absolute;
49
- content: "";
152
+ content: '';
50
153
  inset: 0;
51
154
  }
52
155
 
53
156
  /* Admin gradient backgrounds */
54
- html[data-theme="default"] .bg-gradient-admin {
157
+ html[data-theme='default'] .bg-gradient-admin {
55
158
  background: linear-gradient(to bottom right, rgb(33, 29, 73), rgb(141, 94, 183));
56
159
  }
57
160
 
58
- html[data-theme="sky"] .bg-gradient-admin {
161
+ html[data-theme='sky'] .bg-gradient-admin {
59
162
  background: linear-gradient(to bottom right, rgb(20, 32, 74), rgb(42, 102, 159));
60
163
  }
61
164
  }
62
165
 
166
+ /* Form input styles */
167
+ @layer components {
168
+ /* Base form field styles - matches panda-cms admin pattern */
169
+ input[type='text'],
170
+ input[type='email'],
171
+ input[type='password'],
172
+ input[type='url'],
173
+ input[type='tel'],
174
+ input[type='number'],
175
+ input[type='date'],
176
+ input[type='datetime-local'],
177
+ input[type='month'],
178
+ input[type='week'],
179
+ input[type='time'],
180
+ input[type='search'],
181
+ textarea {
182
+ @apply block w-full rounded-md border-0 p-2 text-gray-900 bg-white;
183
+ @apply ring-1 ring-inset ring-mid placeholder:text-gray-300;
184
+ @apply focus:ring-1 focus:ring-inset focus:ring-dark;
185
+ @apply hover:cursor-pointer sm:leading-6;
186
+ @apply disabled:ring-gray-300 disabled:focus:ring-gray-300 disabled:bg-gray-50 disabled:cursor-not-allowed;
187
+ }
188
+
189
+ /* Select specific styling - matches panda-cms admin pattern */
190
+ select {
191
+ @apply block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 bg-white;
192
+ @apply ring-1 ring-inset ring-mid;
193
+ @apply focus:ring-1 focus:ring-inset focus:ring-dark;
194
+ @apply hover:cursor-pointer sm:leading-6;
195
+ @apply disabled:ring-gray-300 disabled:focus:ring-gray-300 disabled:bg-gray-50 disabled:cursor-not-allowed;
196
+ @apply appearance-none bg-right bg-no-repeat;
197
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
198
+ background-position: right 0.5rem center;
199
+ background-size: 1.5em 1.5em;
200
+ }
201
+
202
+ /* Checkbox and radio styling */
203
+ input[type='checkbox'],
204
+ input[type='radio'] {
205
+ @apply w-4 h-4 text-indigo-600 bg-white border-gray-300 rounded transition-colors duration-200;
206
+ @apply focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2;
207
+ @apply disabled:bg-gray-100 disabled:cursor-not-allowed;
208
+ }
209
+
210
+ input[type='radio'] {
211
+ @apply rounded-full;
212
+ }
213
+
214
+ /* Label styling */
215
+ label {
216
+ @apply block text-sm font-medium text-gray-700 mb-1;
217
+ }
218
+
219
+ /* Field wrapper styling */
220
+ .field {
221
+ @apply mb-4;
222
+ }
223
+
224
+ /* Error state styling */
225
+ input.error,
226
+ textarea.error,
227
+ select.error {
228
+ @apply ring-red-500 focus:ring-red-500;
229
+ }
230
+
231
+ .field-error {
232
+ @apply text-sm text-red-600 mt-1;
233
+ }
234
+
235
+ /* Button styling */
236
+ .btn {
237
+ @apply inline-flex items-center justify-center px-6 py-3 text-base font-semibold rounded-md transition-colors duration-200;
238
+ @apply focus:outline-none focus:ring-2 focus:ring-offset-2;
239
+ @apply disabled:opacity-50 disabled:cursor-not-allowed;
240
+ }
241
+
242
+ .btn-primary {
243
+ @apply bg-mid text-white hover:opacity-90 focus:ring-mid;
244
+ }
245
+
246
+ .btn-secondary {
247
+ @apply bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500;
248
+ }
249
+
250
+ .btn-danger {
251
+ @apply bg-red-600 text-white hover:bg-red-700 focus:ring-red-500;
252
+ }
253
+ }
254
+
63
255
  /* EditorJS content styles */
64
256
  @layer components {
65
257
  .codex-editor__redactor .ce-block .ce-block__content {
@@ -103,7 +295,7 @@
103
295
  }
104
296
 
105
297
  .cdx-quote__text {
106
- quotes: "\201C" "\201D" "\2018" "\2019";
298
+ quotes: '\201C' '\201D' '\2018' '\2019';
107
299
  @apply pl-6;
108
300
 
109
301
  &:before {
@@ -18,4 +18,12 @@ module.exports = {
18
18
  "../../../cms/vendor/javascript/**/*.js",
19
19
  ],
20
20
  },
21
+ safelist: [
22
+ // Tree indentation classes used in pages/index
23
+ 'ml-4',
24
+ 'ml-8',
25
+ 'ml-12',
26
+ 'ml-16',
27
+ 'ml-24',
28
+ ],
21
29
  };
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module UI
6
+ # Badge component for status indicators, labels, and counts.
7
+ #
8
+ # Badges are small, inline elements that highlight an item's status
9
+ # or provide additional metadata at a glance.
10
+ #
11
+ # @example Basic badge
12
+ # render Panda::Core::UI::Badge.new(text: "New")
13
+ #
14
+ # @example Status badges
15
+ # render Panda::Core::UI::Badge.new(text: "Active", variant: :success)
16
+ # render Panda::Core::UI::Badge.new(text: "Pending", variant: :warning)
17
+ # render Panda::Core::UI::Badge.new(text: "Error", variant: :danger)
18
+ #
19
+ # @example With count
20
+ # render Panda::Core::UI::Badge.new(text: "99+", variant: :primary, size: :small)
21
+ #
22
+ # @example Removable badge
23
+ # render Panda::Core::UI::Badge.new(
24
+ # text: "Tag",
25
+ # removable: true,
26
+ # data: { action: "click->tags#remove" }
27
+ # )
28
+ #
29
+ class Badge < Panda::Core::Base
30
+ prop :text, String
31
+ prop :variant, Symbol, default: :default
32
+ prop :size, Symbol, default: :medium
33
+ prop :removable, _Boolean, default: false
34
+ prop :rounded, _Boolean, default: false
35
+
36
+ def view_template
37
+ span(**@attrs) do
38
+ plain text
39
+ if removable
40
+ whitespace
41
+ button(
42
+ type: "button",
43
+ class: "inline-flex items-center ml-1 hover:opacity-70",
44
+ aria: {label: "Remove"}
45
+ ) do
46
+ svg(
47
+ class: "h-3 w-3",
48
+ xmlns: "http://www.w3.org/2000/svg",
49
+ viewBox: "0 0 20 20",
50
+ fill: "currentColor"
51
+ ) do |s|
52
+ s.path(
53
+ d: "M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ def default_attrs
62
+ {
63
+ class: badge_classes
64
+ }
65
+ end
66
+
67
+ private
68
+
69
+ def badge_classes
70
+ base = "inline-flex items-center font-medium"
71
+ base += " #{size_classes}"
72
+ base += " #{variant_classes}"
73
+ base += rounded ? " rounded-full" : " rounded"
74
+ base
75
+ end
76
+
77
+ def size_classes
78
+ case size
79
+ when :small, :sm
80
+ "px-2 py-0.5 text-xs"
81
+ when :large, :lg
82
+ "px-3 py-1 text-base"
83
+ else # :medium, :md
84
+ "px-2.5 py-0.5 text-sm"
85
+ end
86
+ end
87
+
88
+ def variant_classes
89
+ case variant
90
+ when :primary
91
+ "bg-blue-50 text-blue-700 ring-1 ring-inset ring-blue-700/10"
92
+ when :success
93
+ "bg-green-50 text-green-700 ring-1 ring-inset ring-green-600/20"
94
+ when :warning
95
+ "bg-yellow-50 text-yellow-800 ring-1 ring-inset ring-yellow-600/20"
96
+ when :danger
97
+ "bg-red-50 text-red-700 ring-1 ring-inset ring-red-600/10"
98
+ when :info
99
+ "bg-sky-50 text-sky-700 ring-1 ring-inset ring-sky-700/10"
100
+ else # :default
101
+ "bg-gray-50 text-gray-600 ring-1 ring-inset ring-gray-500/10"
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module UI
6
+ # Modern Phlex-based button component with type-safe props.
7
+ #
8
+ # Supports both <button> and <a> elements based on whether an href is provided.
9
+ # Follows Tailwind UI Plus styling patterns with dark mode support.
10
+ #
11
+ # @example Basic button
12
+ # render Panda::Core::UI::Button.new(text: "Click me")
13
+ #
14
+ # @example Button as link
15
+ # render Panda::Core::UI::Button.new(
16
+ # text: "Edit",
17
+ # variant: :secondary,
18
+ # href: "/admin/posts/1/edit"
19
+ # )
20
+ #
21
+ # @example Primary action button
22
+ # render Panda::Core::UI::Button.new(
23
+ # text: "Publish",
24
+ # variant: :primary,
25
+ # href: "/admin/posts/1/publish"
26
+ # )
27
+ #
28
+ # @example With custom attributes
29
+ # render Panda::Core::UI::Button.new(
30
+ # text: "Submit",
31
+ # variant: :primary,
32
+ # class: "mt-4",
33
+ # data: { turbo_method: :post }
34
+ # )
35
+ #
36
+ class Button < Panda::Core::Base
37
+ # Type-safe properties using Literal
38
+ prop :text, String
39
+ prop :variant, Symbol, default: :default
40
+ prop :size, Symbol, default: :medium
41
+ prop :disabled, _Boolean, default: false
42
+ prop :type, String, default: "button"
43
+ prop :href, _Nilable(String), default: -> {}
44
+
45
+ def view_template
46
+ if @href
47
+ a(**@attrs) { @text }
48
+ else
49
+ button(**@attrs) { @text }
50
+ end
51
+ end
52
+
53
+ def default_attrs
54
+ base = {
55
+ class: button_classes
56
+ }
57
+
58
+ if @href
59
+ base[:href] = @href
60
+ else
61
+ base[:type] = @type
62
+ base[:disabled] = @disabled if @disabled
63
+ end
64
+
65
+ base
66
+ end
67
+
68
+ private
69
+
70
+ def button_classes
71
+ base = "inline-flex items-center rounded-md font-semibold"
72
+ base += " #{size_classes}"
73
+ base += " #{variant_classes}"
74
+ base += " disabled:opacity-50 disabled:cursor-not-allowed" if @disabled
75
+ base
76
+ end
77
+
78
+ def size_classes
79
+ case @size
80
+ when :small, :sm
81
+ "px-2.5 py-1.5 text-sm"
82
+ when :large, :lg
83
+ "px-3.5 py-2.5 text-lg"
84
+ else # :medium, :md
85
+ "px-3 py-2 text-sm"
86
+ end
87
+ end
88
+
89
+ def variant_classes
90
+ case @variant
91
+ when :primary
92
+ # Blue primary button with dark mode support
93
+ "bg-blue-600 text-white shadow-xs hover:bg-blue-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:bg-blue-500 dark:shadow-none dark:hover:bg-blue-400 dark:focus-visible:outline-blue-500"
94
+ when :secondary
95
+ # White/gray secondary button with ring and dark mode support
96
+ "bg-white text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20"
97
+ when :success
98
+ "bg-green-600 text-white shadow-xs hover:bg-green-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600 dark:bg-green-500 dark:shadow-none dark:hover:bg-green-400 dark:focus-visible:outline-green-500"
99
+ when :danger
100
+ "bg-red-600 text-white shadow-xs hover:bg-red-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 dark:bg-red-500 dark:shadow-none dark:hover:bg-red-400 dark:focus-visible:outline-red-500"
101
+ when :ghost
102
+ "bg-transparent text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
103
+ else # :default
104
+ "bg-gray-700 text-white shadow-xs hover:bg-gray-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-700 dark:bg-gray-600 dark:shadow-none dark:hover:bg-gray-500 dark:focus-visible:outline-gray-600"
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module UI
6
+ # Card component for containing related content.
7
+ #
8
+ # Cards are flexible containers that can hold any content,
9
+ # with optional padding, shadows, and border variations.
10
+ #
11
+ # @example Basic card
12
+ # render Panda::Core::UI::Card.new do
13
+ # "Card content here"
14
+ # end
15
+ #
16
+ # @example Card with header and footer
17
+ # render Panda::Core::UI::Card.new(padding: :large) do |card|
18
+ # card.with_header { h3 { "Card Title" } }
19
+ # card.with_body { p { "Main content" } }
20
+ # card.with_footer { "Footer content" }
21
+ # end
22
+ #
23
+ # @example Elevated card with no padding
24
+ # render Panda::Core::UI::Card.new(
25
+ # elevation: :high,
26
+ # padding: :none
27
+ # ) do
28
+ # img(src: "/image.jpg", alt: "Card image")
29
+ # end
30
+ #
31
+ class Card < Panda::Core::Base
32
+ prop :padding, Symbol, default: :medium
33
+ prop :elevation, Symbol, default: :low
34
+ prop :border, _Boolean, default: true
35
+
36
+ def view_template(&block)
37
+ div(**@attrs, &block)
38
+ end
39
+
40
+ def default_attrs
41
+ {
42
+ class: card_classes
43
+ }
44
+ end
45
+
46
+ private
47
+
48
+ def card_classes
49
+ base = "bg-white rounded-lg overflow-hidden"
50
+ base += " #{padding_classes}"
51
+ base += " #{elevation_classes}"
52
+ base += " #{border_classes}"
53
+ base
54
+ end
55
+
56
+ def padding_classes
57
+ case padding
58
+ when :none
59
+ ""
60
+ when :small, :sm
61
+ "p-4"
62
+ when :large, :lg
63
+ "p-8"
64
+ else # :medium, :md
65
+ "p-6"
66
+ end
67
+ end
68
+
69
+ def elevation_classes
70
+ case elevation
71
+ when :none
72
+ ""
73
+ when :medium, :md
74
+ "shadow-md"
75
+ when :high, :lg
76
+ "shadow-lg"
77
+ else # :low, :sm
78
+ "shadow-sm"
79
+ end
80
+ end
81
+
82
+ def border_classes
83
+ border ? "border border-gray-200" : ""
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end