aeno 0.0.3

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 (140) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +230 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/stylesheets/aeno/application.css +1 -0
  6. data/app/assets/stylesheets/aeno/base.css +43 -0
  7. data/app/assets/stylesheets/aeno/reset.css +397 -0
  8. data/app/assets/stylesheets/aeno/source.css +15 -0
  9. data/app/assets/stylesheets/aeno/theme.css +6 -0
  10. data/app/assets/stylesheets/aeno/themes/slate.css +163 -0
  11. data/app/assets/stylesheets/aeno/themes/zinc.css +163 -0
  12. data/app/assets/stylesheets/aeno/utilities.css +23 -0
  13. data/app/components/aeno/application_view_component.rb +219 -0
  14. data/app/components/aeno/blocks/component_preview/component.html.erb +7 -0
  15. data/app/components/aeno/blocks/component_preview/component.rb +10 -0
  16. data/app/components/aeno/blocks/component_preview/styles.css +10 -0
  17. data/app/components/aeno/form_builder.rb +87 -0
  18. data/app/components/aeno/pages/showcase/index/component.html.erb +53 -0
  19. data/app/components/aeno/pages/showcase/index/component.rb +7 -0
  20. data/app/components/aeno/pages/showcase/index/styles.css +27 -0
  21. data/app/components/aeno/pages/showcase/placeholder/component.html.erb +7 -0
  22. data/app/components/aeno/pages/showcase/placeholder/component.rb +10 -0
  23. data/app/components/aeno/pages/showcase/show/component.html.erb +38 -0
  24. data/app/components/aeno/pages/showcase/show/component.rb +48 -0
  25. data/app/components/aeno/primitives/button/component.rb +66 -0
  26. data/app/components/aeno/primitives/button/controller.js +7 -0
  27. data/app/components/aeno/primitives/button/styles.css +153 -0
  28. data/app/components/aeno/primitives/card/component.html.erb +3 -0
  29. data/app/components/aeno/primitives/card/component.rb +42 -0
  30. data/app/components/aeno/primitives/card/styles.css +28 -0
  31. data/app/components/aeno/primitives/conversation/component.html.erb +28 -0
  32. data/app/components/aeno/primitives/conversation/component.rb +15 -0
  33. data/app/components/aeno/primitives/conversation/controller.js +18 -0
  34. data/app/components/aeno/primitives/conversation/message/component.html.erb +24 -0
  35. data/app/components/aeno/primitives/conversation/message/component.rb +35 -0
  36. data/app/components/aeno/primitives/conversation/streaming_indicator/component.html.erb +21 -0
  37. data/app/components/aeno/primitives/conversation/streaming_indicator/component.rb +18 -0
  38. data/app/components/aeno/primitives/conversation/styles.css +221 -0
  39. data/app/components/aeno/primitives/conversation/user_message_box/component.html.erb +1 -0
  40. data/app/components/aeno/primitives/conversation/user_message_box/component.rb +4 -0
  41. data/app/components/aeno/primitives/drawer/component.html.erb +43 -0
  42. data/app/components/aeno/primitives/drawer/component.rb +33 -0
  43. data/app/components/aeno/primitives/drawer/controller.js +104 -0
  44. data/app/components/aeno/primitives/drawer/styles.css +90 -0
  45. data/app/components/aeno/primitives/dropdown/checkbox.rb +22 -0
  46. data/app/components/aeno/primitives/dropdown/component.html.erb +38 -0
  47. data/app/components/aeno/primitives/dropdown/component.rb +53 -0
  48. data/app/components/aeno/primitives/dropdown/controller.js +153 -0
  49. data/app/components/aeno/primitives/dropdown/item.rb +29 -0
  50. data/app/components/aeno/primitives/dropdown/label.rb +7 -0
  51. data/app/components/aeno/primitives/dropdown/radio_group.rb +16 -0
  52. data/app/components/aeno/primitives/dropdown/radio_item.rb +24 -0
  53. data/app/components/aeno/primitives/dropdown/separator.rb +7 -0
  54. data/app/components/aeno/primitives/dropdown/styles.css +155 -0
  55. data/app/components/aeno/primitives/empty/component.html.erb +15 -0
  56. data/app/components/aeno/primitives/empty/component.rb +18 -0
  57. data/app/components/aeno/primitives/empty/styles.css +40 -0
  58. data/app/components/aeno/primitives/input_attachments/component.html.erb +60 -0
  59. data/app/components/aeno/primitives/input_attachments/component.rb +52 -0
  60. data/app/components/aeno/primitives/input_attachments/controller.js +357 -0
  61. data/app/components/aeno/primitives/input_attachments/styles.css +102 -0
  62. data/app/components/aeno/primitives/input_color/component.html.erb +24 -0
  63. data/app/components/aeno/primitives/input_color/component.rb +42 -0
  64. data/app/components/aeno/primitives/input_color/styles.css +64 -0
  65. data/app/components/aeno/primitives/input_password/component.html.erb +43 -0
  66. data/app/components/aeno/primitives/input_password/component.rb +20 -0
  67. data/app/components/aeno/primitives/input_password/controller.js +17 -0
  68. data/app/components/aeno/primitives/input_password/styles.css +61 -0
  69. data/app/components/aeno/primitives/input_select/component.html.erb +43 -0
  70. data/app/components/aeno/primitives/input_select/component.rb +21 -0
  71. data/app/components/aeno/primitives/input_select/option.rb +14 -0
  72. data/app/components/aeno/primitives/input_select/styles.css +30 -0
  73. data/app/components/aeno/primitives/input_slider/component.html.erb +33 -0
  74. data/app/components/aeno/primitives/input_slider/component.rb +35 -0
  75. data/app/components/aeno/primitives/input_slider/styles.css +74 -0
  76. data/app/components/aeno/primitives/input_tagging/component.html.erb +73 -0
  77. data/app/components/aeno/primitives/input_tagging/component.rb +40 -0
  78. data/app/components/aeno/primitives/input_tagging/controller.js +326 -0
  79. data/app/components/aeno/primitives/input_tagging/styles.css +148 -0
  80. data/app/components/aeno/primitives/input_text/component.html.erb +25 -0
  81. data/app/components/aeno/primitives/input_text/component.rb +20 -0
  82. data/app/components/aeno/primitives/input_text/styles.css +38 -0
  83. data/app/components/aeno/primitives/input_text_area/component.html.erb +23 -0
  84. data/app/components/aeno/primitives/input_text_area/component.rb +19 -0
  85. data/app/components/aeno/primitives/input_text_area/styles.css +30 -0
  86. data/app/components/aeno/primitives/input_text_area_ai/component.html.erb +51 -0
  87. data/app/components/aeno/primitives/input_text_area_ai/component.rb +47 -0
  88. data/app/components/aeno/primitives/input_text_area_ai/controller.js +198 -0
  89. data/app/components/aeno/primitives/input_text_area_ai/styles.css +91 -0
  90. data/app/components/aeno/primitives/input_wrapper/component.html.erb +20 -0
  91. data/app/components/aeno/primitives/input_wrapper/component.rb +31 -0
  92. data/app/components/aeno/primitives/input_wrapper/styles.css +72 -0
  93. data/app/components/aeno/primitives/layouts/agentic/component.html.erb +4 -0
  94. data/app/components/aeno/primitives/layouts/agentic/component.rb +9 -0
  95. data/app/components/aeno/primitives/layouts/agentic/styles.css +23 -0
  96. data/app/components/aeno/primitives/layouts/app/aside.rb +9 -0
  97. data/app/components/aeno/primitives/layouts/app/component.html.erb +14 -0
  98. data/app/components/aeno/primitives/layouts/app/component.rb +11 -0
  99. data/app/components/aeno/primitives/layouts/app/sidebar.rb +9 -0
  100. data/app/components/aeno/primitives/layouts/app/styles.css +46 -0
  101. data/app/components/aeno/primitives/page/component.html.erb +24 -0
  102. data/app/components/aeno/primitives/page/component.rb +23 -0
  103. data/app/components/aeno/primitives/page/styles.css +55 -0
  104. data/app/components/aeno/primitives/sidebar/component.html.erb +25 -0
  105. data/app/components/aeno/primitives/sidebar/component.rb +14 -0
  106. data/app/components/aeno/primitives/sidebar/footer.rb +7 -0
  107. data/app/components/aeno/primitives/sidebar/group.rb +18 -0
  108. data/app/components/aeno/primitives/sidebar/header.rb +7 -0
  109. data/app/components/aeno/primitives/sidebar/item.rb +19 -0
  110. data/app/components/aeno/primitives/sidebar/styles.css +95 -0
  111. data/app/components/aeno/primitives/spinner/component.rb +36 -0
  112. data/app/components/aeno/primitives/spinner/styles.css +81 -0
  113. data/app/components/aeno/primitives/table/cell.rb +7 -0
  114. data/app/components/aeno/primitives/table/column.rb +7 -0
  115. data/app/components/aeno/primitives/table/component.html.erb +8 -0
  116. data/app/components/aeno/primitives/table/component.rb +14 -0
  117. data/app/components/aeno/primitives/table/header.rb +13 -0
  118. data/app/components/aeno/primitives/table/row.rb +11 -0
  119. data/app/components/aeno/primitives/table/styles.css +39 -0
  120. data/app/controllers/aeno/application_controller.rb +15 -0
  121. data/app/controllers/aeno/showcase_controller.rb +40 -0
  122. data/app/controllers/aeno/theme_controller.rb +10 -0
  123. data/app/helpers/aeno/application_helper.rb +28 -0
  124. data/app/javascript/aeno/application.js +3 -0
  125. data/app/javascript/aeno/controllers/application.js +5 -0
  126. data/app/javascript/aeno/controllers/index.js +5 -0
  127. data/app/javascript/aeno/controllers/loader.js +62 -0
  128. data/app/jobs/aeno/application_job.rb +4 -0
  129. data/app/models/aeno/application_record.rb +5 -0
  130. data/app/views/layouts/aeno/application.html.erb +55 -0
  131. data/config/importmap.rb +20 -0
  132. data/config/routes.rb +5 -0
  133. data/lib/aeno/configuration.rb +56 -0
  134. data/lib/aeno/engine.rb +43 -0
  135. data/lib/aeno/engine_helpers.rb +44 -0
  136. data/lib/aeno/theme.rb +326 -0
  137. data/lib/aeno/version.rb +3 -0
  138. data/lib/aeno.rb +11 -0
  139. data/lib/tasks/aeno_tasks.rake +39 -0
  140. metadata +310 -0
@@ -0,0 +1,357 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = [
5
+ "dropzone",
6
+ "fileInput",
7
+ "previewsContainer",
8
+ "hiddenInputs",
9
+ "progressContainer",
10
+ "progressBar",
11
+ "progressText"
12
+ ];
13
+
14
+ static values = {
15
+ accept: { type: String, default: "*/*" },
16
+ maxFiles: Number,
17
+ maxSize: Number,
18
+ attachments: { type: Array, default: [] },
19
+ directUploadUrl: String
20
+ };
21
+
22
+ connect() {
23
+ this.dragCounter = 0;
24
+ this.renderPreviews();
25
+ this.renderHiddenInputs();
26
+ }
27
+
28
+ triggerFileInput() {
29
+ this.fileInputTarget.click();
30
+ }
31
+
32
+ onDragOver(event) {
33
+ event.preventDefault();
34
+ event.stopPropagation();
35
+ this.dragCounter++;
36
+ this.dropzoneTarget.classList.add("border-indigo-400", "bg-indigo-50");
37
+ }
38
+
39
+ onDragLeave(event) {
40
+ event.preventDefault();
41
+ event.stopPropagation();
42
+ this.dragCounter--;
43
+ if (this.dragCounter === 0) {
44
+ this.dropzoneTarget.classList.remove("border-indigo-400", "bg-indigo-50");
45
+ }
46
+ }
47
+
48
+ onDrop(event) {
49
+ event.preventDefault();
50
+ event.stopPropagation();
51
+ this.dragCounter = 0;
52
+ this.dropzoneTarget.classList.remove("border-indigo-400", "bg-indigo-50");
53
+
54
+ const files = Array.from(event.dataTransfer.files);
55
+ this.handleFiles(files);
56
+ }
57
+
58
+ onFileSelect(event) {
59
+ const files = Array.from(event.target.files);
60
+ this.handleFiles(files);
61
+ event.target.value = "";
62
+ }
63
+
64
+ handleFiles(files) {
65
+ const validFiles = files.filter(file => this.validateFile(file));
66
+
67
+ if (this.maxFilesValue) {
68
+ const remaining = this.maxFilesValue - this.attachmentsValue.length;
69
+ if (validFiles.length > remaining) {
70
+ alert(`You can only add ${remaining} more file(s).`);
71
+ validFiles.splice(remaining);
72
+ }
73
+ }
74
+
75
+ if (validFiles.length === 0) return;
76
+
77
+ if (this.directUploadUrlValue) {
78
+ this.uploadFiles(validFiles);
79
+ } else {
80
+ this.addFilesLocally(validFiles);
81
+ }
82
+ }
83
+
84
+ validateFile(file) {
85
+ if (this.maxSizeValue && file.size > this.maxSizeValue) {
86
+ alert(`File "${file.name}" is too large. Maximum size is ${this.formatBytes(this.maxSizeValue)}.`);
87
+ return false;
88
+ }
89
+
90
+ if (this.acceptValue !== "*/*") {
91
+ const acceptedTypes = this.acceptValue.split(",").map(t => t.trim());
92
+ const fileType = file.type;
93
+ const fileExt = "." + file.name.split(".").pop().toLowerCase();
94
+
95
+ const isAccepted = acceptedTypes.some(accepted => {
96
+ if (accepted.startsWith(".")) {
97
+ return fileExt === accepted.toLowerCase();
98
+ }
99
+ if (accepted.endsWith("/*")) {
100
+ return fileType.startsWith(accepted.replace("/*", "/"));
101
+ }
102
+ return fileType === accepted;
103
+ });
104
+
105
+ if (!isAccepted) {
106
+ alert(`File "${file.name}" is not an accepted file type.`);
107
+ return false;
108
+ }
109
+ }
110
+
111
+ return true;
112
+ }
113
+
114
+ async uploadFiles(files) {
115
+ this.showProgress();
116
+
117
+ for (let i = 0; i < files.length; i++) {
118
+ const file = files[i];
119
+ const progress = ((i / files.length) * 100).toFixed(0);
120
+ this.updateProgress(progress);
121
+
122
+ try {
123
+ const signedId = await this.uploadFile(file);
124
+ const url = URL.createObjectURL(file);
125
+
126
+ this.attachmentsValue = [
127
+ ...this.attachmentsValue,
128
+ {
129
+ id: signedId,
130
+ url: url,
131
+ filename: file.name,
132
+ position: this.attachmentsValue.length,
133
+ isNew: true
134
+ }
135
+ ];
136
+ } catch (error) {
137
+ console.error(`Failed to upload ${file.name}:`, error);
138
+ alert(`Failed to upload "${file.name}".`);
139
+ }
140
+ }
141
+
142
+ this.hideProgress();
143
+ this.renderPreviews();
144
+ this.renderHiddenInputs();
145
+ this.dispatch("change", { detail: { attachments: this.attachmentsValue } });
146
+ }
147
+
148
+ async uploadFile(file) {
149
+ return new Promise((resolve, reject) => {
150
+ const formData = new FormData();
151
+ formData.append("file", file);
152
+
153
+ const xhr = new XMLHttpRequest();
154
+ xhr.open("POST", this.directUploadUrlValue);
155
+ xhr.setRequestHeader("Accept", "application/json");
156
+ xhr.setRequestHeader("X-CSRF-Token", this.csrfToken());
157
+
158
+ xhr.upload.addEventListener("progress", (event) => {
159
+ if (event.lengthComputable) {
160
+ const progress = ((event.loaded / event.total) * 100).toFixed(0);
161
+ this.updateProgress(progress);
162
+ }
163
+ });
164
+
165
+ xhr.addEventListener("load", () => {
166
+ if (xhr.status >= 200 && xhr.status < 300) {
167
+ const response = JSON.parse(xhr.responseText);
168
+ resolve(response.signed_id || response.id);
169
+ } else {
170
+ reject(new Error(`Upload failed: ${xhr.status}`));
171
+ }
172
+ });
173
+
174
+ xhr.addEventListener("error", () => reject(new Error("Upload failed")));
175
+ xhr.send(formData);
176
+ });
177
+ }
178
+
179
+ addFilesLocally(files) {
180
+ files.forEach(file => {
181
+ const url = URL.createObjectURL(file);
182
+ const tempId = `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
183
+
184
+ this.attachmentsValue = [
185
+ ...this.attachmentsValue,
186
+ {
187
+ id: tempId,
188
+ url: url,
189
+ filename: file.name,
190
+ position: this.attachmentsValue.length,
191
+ file: file,
192
+ isNew: true
193
+ }
194
+ ];
195
+ });
196
+
197
+ this.renderPreviews();
198
+ this.renderHiddenInputs();
199
+ this.dispatch("change", { detail: { attachments: this.attachmentsValue } });
200
+ }
201
+
202
+ removeAttachment(event) {
203
+ const index = parseInt(event.currentTarget.dataset.index, 10);
204
+ const attachment = this.attachmentsValue[index];
205
+
206
+ if (attachment?.url?.startsWith("blob:")) {
207
+ URL.revokeObjectURL(attachment.url);
208
+ }
209
+
210
+ this.attachmentsValue = this.attachmentsValue.filter((_, i) => i !== index);
211
+ this.reorderPositions();
212
+ this.renderPreviews();
213
+ this.renderHiddenInputs();
214
+ this.dispatch("change", { detail: { attachments: this.attachmentsValue } });
215
+ }
216
+
217
+ reorderPositions() {
218
+ this.attachmentsValue = this.attachmentsValue.map((att, idx) => ({
219
+ ...att,
220
+ position: idx
221
+ }));
222
+ }
223
+
224
+ onDragStartPreview(event) {
225
+ event.dataTransfer.effectAllowed = "move";
226
+ event.dataTransfer.setData("text/plain", event.currentTarget.dataset.index);
227
+ event.currentTarget.classList.add("opacity-50");
228
+ }
229
+
230
+ onDragEndPreview(event) {
231
+ event.currentTarget.classList.remove("opacity-50");
232
+ }
233
+
234
+ onDragOverPreview(event) {
235
+ event.preventDefault();
236
+ event.dataTransfer.dropEffect = "move";
237
+ }
238
+
239
+ onDropPreview(event) {
240
+ event.preventDefault();
241
+ const fromIndex = parseInt(event.dataTransfer.getData("text/plain"), 10);
242
+ const toIndex = parseInt(event.currentTarget.dataset.index, 10);
243
+
244
+ if (fromIndex === toIndex) return;
245
+
246
+ const attachments = [...this.attachmentsValue];
247
+ const [moved] = attachments.splice(fromIndex, 1);
248
+ attachments.splice(toIndex, 0, moved);
249
+
250
+ this.attachmentsValue = attachments.map((att, idx) => ({
251
+ ...att,
252
+ position: idx
253
+ }));
254
+
255
+ this.renderPreviews();
256
+ this.renderHiddenInputs();
257
+ this.dispatch("change", { detail: { attachments: this.attachmentsValue } });
258
+ }
259
+
260
+ renderPreviews() {
261
+ const html = this.attachmentsValue.map((attachment, index) => {
262
+ const isImage = this.isImageFile(attachment.filename || attachment.url);
263
+
264
+ return `
265
+ <div class="relative group rounded-lg border border-gray-200 overflow-hidden bg-gray-50 aspect-square cursor-move"
266
+ draggable="true"
267
+ data-index="${index}"
268
+ data-action="dragstart->${this.identifier}#onDragStartPreview dragend->${this.identifier}#onDragEndPreview dragover->${this.identifier}#onDragOverPreview drop->${this.identifier}#onDropPreview">
269
+ ${isImage
270
+ ? `<img src="${attachment.url}" alt="${this.escapeHtml(attachment.filename)}" class="w-full h-full object-cover">`
271
+ : `<div class="w-full h-full flex flex-col items-center justify-center p-2">
272
+ <svg class="h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
273
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
274
+ </svg>
275
+ <span class="mt-1 text-xs text-gray-500 truncate max-w-full">${this.escapeHtml(attachment.filename)}</span>
276
+ </div>`
277
+ }
278
+ <button type="button"
279
+ class="absolute top-1 right-1 p-1 bg-white rounded-full shadow-sm opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-50"
280
+ data-index="${index}"
281
+ data-action="click->${this.identifier}#removeAttachment">
282
+ <svg class="h-4 w-4 text-gray-500 hover:text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
283
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
284
+ </svg>
285
+ </button>
286
+ <div class="absolute bottom-1 left-1 px-1.5 py-0.5 bg-black/50 rounded text-[10px] text-white">
287
+ ${index + 1}
288
+ </div>
289
+ </div>
290
+ `;
291
+ }).join("");
292
+
293
+ this.previewsContainerTarget.innerHTML = html;
294
+ }
295
+
296
+ renderHiddenInputs() {
297
+ const baseName = this.element.dataset[`${this.identifier.replace(/-/g, "")}Name`] ||
298
+ this.element.querySelector("[name]")?.name?.replace(/\[\]$/, "") ||
299
+ "attachments";
300
+
301
+ const html = this.attachmentsValue.map((attachment, index) => {
302
+ if (attachment.file) {
303
+ return "";
304
+ }
305
+ return `<input type="hidden" name="${baseName}[${index}][id]" value="${attachment.id}">
306
+ <input type="hidden" name="${baseName}[${index}][position]" value="${index}">`;
307
+ }).join("");
308
+
309
+ this.hiddenInputsTarget.innerHTML = html;
310
+ }
311
+
312
+ isImageFile(filename) {
313
+ if (!filename) return false;
314
+ const ext = filename.split(".").pop().toLowerCase();
315
+ return ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp"].includes(ext);
316
+ }
317
+
318
+ showProgress() {
319
+ if (this.hasProgressContainerTarget) {
320
+ this.progressContainerTarget.classList.remove("hidden");
321
+ }
322
+ }
323
+
324
+ hideProgress() {
325
+ if (this.hasProgressContainerTarget) {
326
+ this.progressContainerTarget.classList.add("hidden");
327
+ }
328
+ }
329
+
330
+ updateProgress(percent) {
331
+ if (this.hasProgressBarTarget) {
332
+ this.progressBarTarget.style.width = `${percent}%`;
333
+ }
334
+ if (this.hasProgressTextTarget) {
335
+ this.progressTextTarget.textContent = `${percent}%`;
336
+ }
337
+ }
338
+
339
+ formatBytes(bytes) {
340
+ if (bytes === 0) return "0 Bytes";
341
+ const k = 1024;
342
+ const sizes = ["Bytes", "KB", "MB", "GB"];
343
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
344
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
345
+ }
346
+
347
+ escapeHtml(text) {
348
+ if (!text) return "";
349
+ const div = document.createElement("div");
350
+ div.textContent = text;
351
+ return div.innerHTML;
352
+ }
353
+
354
+ csrfToken() {
355
+ return document.querySelector('meta[name="csrf-token"]')?.content || "";
356
+ }
357
+ }
@@ -0,0 +1,102 @@
1
+ /* Input Attachments */
2
+ .cp-input-attachments {
3
+ }
4
+
5
+ .cp-input-attachments__container {
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: 0.75rem;
9
+ }
10
+
11
+ .cp-input-attachments__dropzone {
12
+ display: flex;
13
+ flex-direction: column;
14
+ align-items: center;
15
+ justify-content: center;
16
+ padding: 1.5rem;
17
+ border: 2px dashed var(--ui-input-border);
18
+ border-radius: var(--ui-input-radius, 0.375rem);
19
+ background: var(--ui-input-bg);
20
+ cursor: pointer;
21
+ transition: border-color 0.15s ease, background-color 0.15s ease;
22
+ }
23
+
24
+ .cp-input-attachments__dropzone:hover {
25
+ border-color: var(--ui-input-ring);
26
+ background: var(--ui-muted);
27
+ }
28
+
29
+ .cp-input-attachments__dropzone--dragging {
30
+ border-color: var(--ui-input-ring);
31
+ background: var(--ui-muted);
32
+ }
33
+
34
+ .cp-input-attachments__dropzone-content {
35
+ display: flex;
36
+ flex-direction: column;
37
+ align-items: center;
38
+ gap: 0.5rem;
39
+ text-align: center;
40
+ }
41
+
42
+ .cp-input-attachments__icon {
43
+ width: 2rem;
44
+ height: 2rem;
45
+ color: var(--ui-muted-foreground);
46
+ }
47
+
48
+ .cp-input-attachments__text {
49
+ font-size: 0.875rem;
50
+ color: var(--ui-muted-foreground);
51
+ }
52
+
53
+ .cp-input-attachments__link {
54
+ font-weight: 500;
55
+ color: var(--ui-primary);
56
+ }
57
+
58
+ .cp-input-attachments__hint {
59
+ font-size: 0.75rem;
60
+ color: var(--ui-muted-foreground);
61
+ }
62
+
63
+ .cp-input-attachments__file-input {
64
+ display: none;
65
+ }
66
+
67
+ .cp-input-attachments__progress {
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: 0.25rem;
71
+ }
72
+
73
+ .cp-input-attachments__progress--hidden {
74
+ display: none;
75
+ }
76
+
77
+ .cp-input-attachments__progress-info {
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 0.5rem;
81
+ font-size: 0.875rem;
82
+ color: var(--ui-muted-foreground);
83
+ }
84
+
85
+ .cp-input-attachments__progress-track {
86
+ height: 0.375rem;
87
+ background: var(--ui-accent);
88
+ border-radius: 9999px;
89
+ overflow: hidden;
90
+ }
91
+
92
+ .cp-input-attachments__progress-bar {
93
+ height: 100%;
94
+ background: var(--ui-primary);
95
+ transition: width 0.3s ease;
96
+ }
97
+
98
+ .cp-input-attachments__previews {
99
+ display: grid;
100
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
101
+ gap: 0.75rem;
102
+ }
@@ -0,0 +1,24 @@
1
+ <div class="cp-input-color <%= 'cp-input-color--disabled' if disabled %>">
2
+ <div class="cp-input-color__container">
3
+ <input
4
+ type="color"
5
+ id="<%= input_id %>"
6
+ name="<%= name %>"
7
+ value="<%= value %>"
8
+ class="cp-input-color__input <%= size_class %>"
9
+ <%= 'disabled' if disabled %>
10
+ <% data.each do |key, val| %>
11
+ data-<%= key.to_s.dasherize %>="<%= val %>"
12
+ <% end %>
13
+ />
14
+ <% if label %>
15
+ <label for="<%= input_id %>" class="cp-input-color__label">
16
+ <%= label %>
17
+ </label>
18
+ <% end %>
19
+ </div>
20
+
21
+ <% if helper_text %>
22
+ <p class="cp-input-color__helper"><%= helper_text %></p>
23
+ <% end %>
24
+ </div>
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aeno::Primitives::InputColor
4
+ class Component < ::Aeno::ApplicationViewComponent
5
+ prop :name, description: "Input name attribute"
6
+ prop :id, description: "Input id", optional: true
7
+ prop :value, description: "Current color value", default: -> { "#000000" }
8
+ prop :label, description: "Label text", optional: true
9
+ prop :helper_text, description: "Helper text", optional: true
10
+ prop :disabled, description: "Disabled state", default: -> { false }
11
+ prop :data, description: "Data attributes", default: -> { {} }
12
+ prop :size, description: "Size variant", values: [:small, :default, :large], default: -> { :default }
13
+
14
+ examples("Input Color", description: "Color picker input") do |b|
15
+ b.example(:default, title: "Default") do |e|
16
+ e.preview name: "color"
17
+ end
18
+
19
+ b.example(:with_label, title: "With Label") do |e|
20
+ e.preview name: "brand_color", label: "Brand Color", value: "#6366f1"
21
+ end
22
+
23
+ b.example(:sizes, title: "Sizes") do |e|
24
+ e.preview name: "small", size: :small
25
+ e.preview name: "default", size: :default
26
+ e.preview name: "large", size: :large
27
+ end
28
+ end
29
+
30
+ def input_id
31
+ id || name
32
+ end
33
+
34
+ def size_class
35
+ case size.to_sym
36
+ when :small then "cp-input-color--small"
37
+ when :large then "cp-input-color--large"
38
+ else ""
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,64 @@
1
+ /* Input Color */
2
+ .cp-input-color {
3
+ }
4
+
5
+ .cp-input-color--disabled {
6
+ opacity: 0.5;
7
+ pointer-events: none;
8
+ }
9
+
10
+ .cp-input-color__container {
11
+ display: flex;
12
+ align-items: center;
13
+ gap: 0.75rem;
14
+ }
15
+
16
+ .cp-input-color__input {
17
+ width: 2.5rem;
18
+ height: 2.5rem;
19
+ padding: 0;
20
+ border: 1px solid var(--ui-input-border);
21
+ border-radius: var(--ui-input-radius, 0.375rem);
22
+ cursor: pointer;
23
+ overflow: hidden;
24
+ }
25
+
26
+ .cp-input-color__input::-webkit-color-swatch-wrapper {
27
+ padding: 0;
28
+ }
29
+
30
+ .cp-input-color__input::-webkit-color-swatch {
31
+ border: none;
32
+ border-radius: calc(var(--ui-input-radius, 0.375rem) - 1px);
33
+ }
34
+
35
+ .cp-input-color__input::-moz-color-swatch {
36
+ border: none;
37
+ border-radius: calc(var(--ui-input-radius, 0.375rem) - 1px);
38
+ }
39
+
40
+ .cp-input-color__input:focus {
41
+ outline: none;
42
+ box-shadow: 0 0 0 2px var(--ui-background), 0 0 0 4px var(--ui-ring);
43
+ }
44
+
45
+ .cp-input-color__input--small {
46
+ width: 2rem;
47
+ height: 2rem;
48
+ }
49
+
50
+ .cp-input-color__input--large {
51
+ width: 3rem;
52
+ height: 3rem;
53
+ }
54
+
55
+ .cp-input-color__label {
56
+ font-size: 0.875rem;
57
+ color: var(--ui-foreground);
58
+ }
59
+
60
+ .cp-input-color__helper {
61
+ margin-top: 0.25rem;
62
+ font-size: 0.875rem;
63
+ color: var(--ui-muted-foreground);
64
+ }
@@ -0,0 +1,43 @@
1
+ <div class="cp-input-password">
2
+ <%= render Aeno::Primitives::InputWrapper::Component.new(
3
+ label: label,
4
+ helper_text: helper_text,
5
+ error_text: error_text,
6
+ name: name,
7
+ id: id,
8
+ disabled: disabled,
9
+ required: required) do %>
10
+ <div class="cp-input-password__field" data-controller="<%= controller_name %>">
11
+ <input
12
+ type="password"
13
+ id="<%= id %>"
14
+ name="<%= name %>"
15
+ value="<%= value %>"
16
+ placeholder="<%= placeholder %>"
17
+ autocomplete="<%= autocomplete %>"
18
+ class="cp-input-password__input"
19
+ data-<%= controller_name %>-target="input"
20
+ <%= 'disabled' if disabled %>
21
+ <%= 'required' if required %>
22
+ <% data.each do |key, val| %>
23
+ data-<%= key.to_s.dasherize %>="<%= val %>"
24
+ <% end %>
25
+ />
26
+ <% if show_toggle %>
27
+ <button
28
+ type="button"
29
+ class="cp-input-password__toggle"
30
+ data-action="click-><%= controller_name %>#toggle"
31
+ tabindex="-1"
32
+ >
33
+ <span data-<%= controller_name %>-target="showIcon">
34
+ <%= lucide_icon("eye", class: "cp-input-password__toggle-icon") %>
35
+ </span>
36
+ <span data-<%= controller_name %>-target="hideIcon" class="cp-input-password__toggle-icon--hidden">
37
+ <%= lucide_icon("eye-off", class: "cp-input-password__toggle-icon") %>
38
+ </span>
39
+ </button>
40
+ <% end %>
41
+ </div>
42
+ <% end %>
43
+ </div>
@@ -0,0 +1,20 @@
1
+ module Aeno::Primitives::InputPassword
2
+ class Component < ::Aeno::FormBuilder::BaseComponent
3
+ prop :autocomplete, description: "Autocomplete attribute", default: -> { "current-password" }
4
+ prop :show_toggle, description: "Show password visibility toggle", default: -> { true }
5
+
6
+ examples("Input Password", description: "Password input with visibility toggle") do |b|
7
+ b.example(:default, title: "Default") do |e|
8
+ e.preview name: "password"
9
+ end
10
+
11
+ b.example(:with_label, title: "With Label") do |e|
12
+ e.preview name: "password", label: "Password"
13
+ end
14
+
15
+ b.example(:no_toggle, title: "Without Toggle") do |e|
16
+ e.preview name: "password", show_toggle: false
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["input", "showIcon", "hideIcon"];
5
+
6
+ toggle() {
7
+ if (this.inputTarget.type === "password") {
8
+ this.inputTarget.type = "text";
9
+ this.showIconTarget.classList.add("hidden");
10
+ this.hideIconTarget.classList.remove("hidden");
11
+ } else {
12
+ this.inputTarget.type = "password";
13
+ this.showIconTarget.classList.remove("hidden");
14
+ this.hideIconTarget.classList.add("hidden");
15
+ }
16
+ }
17
+ }