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,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aeno::Primitives::InputTextAreaAi
4
+ class Component < ::Aeno::FormBuilder::BaseComponent
5
+ prop :rows, description: "Number of visible rows", default: -> { 6 }
6
+ prop :ai_url, description: "AI endpoint URL"
7
+ prop :ai_prompt, description: "AI prompt template", optional: true
8
+ prop :system_prompts, description: "System prompts array", default: -> { [] }
9
+ prop :model, description: "AI model to use", optional: true
10
+ prop :ai_button_label, description: "AI button label", default: -> { "AI Assist" }
11
+ prop :ai_button_position, description: "AI button position", default: -> { :bottom_right }
12
+
13
+ examples("Input Text Area AI", description: "Text area with AI assistance") do |b|
14
+ b.example(:default, title: "Default") do |e|
15
+ e.preview name: "content", ai_url: "/api/ai/complete"
16
+ end
17
+
18
+ b.example(:custom_label, title: "Custom Button") do |e|
19
+ e.preview name: "description", ai_url: "/api/ai", ai_button_label: "Generate"
20
+ end
21
+ end
22
+
23
+ def stimulus_values
24
+ values = {
25
+ "#{controller_name}-ai-url-value" => ai_url,
26
+ "#{controller_name}-ai-button-label-value" => ai_button_label
27
+ }
28
+ values["#{controller_name}-ai-prompt-value"] = ai_prompt if ai_prompt.present?
29
+ values["#{controller_name}-system-prompts-value"] = system_prompts.to_json if system_prompts.any?
30
+ values["#{controller_name}-model-value"] = model if model.present?
31
+ values
32
+ end
33
+
34
+ def button_position_classes
35
+ case ai_button_position.to_sym
36
+ when :top_right
37
+ "top-2 right-2"
38
+ when :top_left
39
+ "top-2 left-2"
40
+ when :bottom_left
41
+ "bottom-2 left-2"
42
+ else
43
+ "bottom-2 right-2"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,198 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = [
5
+ "textarea",
6
+ "aiButton",
7
+ "buttonIcon",
8
+ "buttonSpinner",
9
+ "buttonLabel",
10
+ "streamingIndicator"
11
+ ];
12
+
13
+ static values = {
14
+ aiUrl: String,
15
+ aiPrompt: String,
16
+ systemPrompts: { type: Array, default: [] },
17
+ model: String,
18
+ aiButtonLabel: { type: String, default: "AI Assist" },
19
+ generating: { type: Boolean, default: false }
20
+ };
21
+
22
+ connect() {
23
+ this.abortController = null;
24
+ }
25
+
26
+ disconnect() {
27
+ this.cancelGeneration();
28
+ }
29
+
30
+ async generateAi() {
31
+ if (this.generatingValue) {
32
+ this.cancelGeneration();
33
+ return;
34
+ }
35
+
36
+ const currentText = this.textareaTarget.value;
37
+
38
+ const prompt = this.aiPromptValue
39
+ ? this.aiPromptValue.replace("{{content}}", currentText)
40
+ : currentText;
41
+
42
+ if (!prompt.trim()) {
43
+ this.textareaTarget.focus();
44
+ return;
45
+ }
46
+
47
+ this.startGenerating();
48
+
49
+ try {
50
+ this.abortController = new AbortController();
51
+
52
+ const body = {
53
+ prompt: prompt
54
+ };
55
+
56
+ if (this.systemPromptsValue.length > 0) {
57
+ body.system_prompts = this.systemPromptsValue;
58
+ }
59
+
60
+ if (this.modelValue) {
61
+ body.model = this.modelValue;
62
+ }
63
+
64
+ const response = await fetch(this.aiUrlValue, {
65
+ method: "POST",
66
+ headers: {
67
+ "Content-Type": "application/json",
68
+ "Accept": "text/event-stream",
69
+ "X-CSRF-Token": this.csrfToken()
70
+ },
71
+ body: JSON.stringify(body),
72
+ signal: this.abortController.signal
73
+ });
74
+
75
+ if (!response.ok) {
76
+ throw new Error(`HTTP error! status: ${response.status}`);
77
+ }
78
+
79
+ const contentType = response.headers.get("content-type");
80
+
81
+ if (contentType?.includes("text/event-stream") || contentType?.includes("application/x-ndjson")) {
82
+ await this.handleStreamingResponse(response);
83
+ } else {
84
+ await this.handleJsonResponse(response);
85
+ }
86
+
87
+ } catch (error) {
88
+ if (error.name === "AbortError") {
89
+ console.log("Generation cancelled");
90
+ } else {
91
+ console.error("AI generation failed:", error);
92
+ this.dispatch("error", { detail: { message: error.message } });
93
+ }
94
+ } finally {
95
+ this.stopGenerating();
96
+ }
97
+ }
98
+
99
+ async handleStreamingResponse(response) {
100
+ const reader = response.body.getReader();
101
+ const decoder = new TextDecoder();
102
+
103
+ const originalContent = this.textareaTarget.value;
104
+ const separator = originalContent.trim() ? "\n\n" : "";
105
+ let generatedContent = "";
106
+
107
+ try {
108
+ while (true) {
109
+ const { done, value } = await reader.read();
110
+ if (done) break;
111
+
112
+ const chunk = decoder.decode(value, { stream: true });
113
+ const lines = chunk.split("\n");
114
+
115
+ for (const line of lines) {
116
+ if (line.startsWith("data: ")) {
117
+ const data = line.slice(6);
118
+
119
+ if (data === "[DONE]") {
120
+ continue;
121
+ }
122
+
123
+ try {
124
+ const parsed = JSON.parse(data);
125
+ const content = parsed.content || parsed.text || parsed.delta?.content || "";
126
+
127
+ if (content) {
128
+ generatedContent += content;
129
+ this.textareaTarget.value = originalContent + separator + generatedContent;
130
+ this.scrollTextareaToBottom();
131
+ }
132
+ } catch {
133
+ if (data.trim() && data !== "[DONE]") {
134
+ generatedContent += data;
135
+ this.textareaTarget.value = originalContent + separator + generatedContent;
136
+ this.scrollTextareaToBottom();
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ } catch (error) {
143
+ if (error.name !== "AbortError") {
144
+ throw error;
145
+ }
146
+ }
147
+
148
+ this.dispatch("complete", { detail: { content: generatedContent } });
149
+ }
150
+
151
+ async handleJsonResponse(response) {
152
+ const data = await response.json();
153
+ const content = data.content || data.text || data.response || "";
154
+
155
+ if (content) {
156
+ const originalContent = this.textareaTarget.value;
157
+ const separator = originalContent.trim() ? "\n\n" : "";
158
+ this.textareaTarget.value = originalContent + separator + content;
159
+ this.dispatch("complete", { detail: { content } });
160
+ }
161
+ }
162
+
163
+ cancelGeneration() {
164
+ if (this.abortController) {
165
+ this.abortController.abort();
166
+ this.abortController = null;
167
+ }
168
+ }
169
+
170
+ startGenerating() {
171
+ this.generatingValue = true;
172
+ this.aiButtonTarget.disabled = false;
173
+ this.buttonIconTarget.classList.add("hidden");
174
+ this.buttonSpinnerTarget.classList.remove("hidden");
175
+ this.buttonLabelTarget.textContent = "Cancel";
176
+ this.streamingIndicatorTarget.classList.remove("hidden");
177
+ this.textareaTarget.readOnly = true;
178
+ }
179
+
180
+ stopGenerating() {
181
+ this.generatingValue = false;
182
+ this.aiButtonTarget.disabled = false;
183
+ this.buttonIconTarget.classList.remove("hidden");
184
+ this.buttonSpinnerTarget.classList.add("hidden");
185
+ this.buttonLabelTarget.textContent = this.aiButtonLabelValue;
186
+ this.streamingIndicatorTarget.classList.add("hidden");
187
+ this.textareaTarget.readOnly = false;
188
+ this.abortController = null;
189
+ }
190
+
191
+ scrollTextareaToBottom() {
192
+ this.textareaTarget.scrollTop = this.textareaTarget.scrollHeight;
193
+ }
194
+
195
+ csrfToken() {
196
+ return document.querySelector('meta[name="csrf-token"]')?.content || "";
197
+ }
198
+ }
@@ -0,0 +1,91 @@
1
+ /* Input Text Area AI */
2
+ .cp-input-text-area-ai {
3
+ }
4
+
5
+ .cp-input-text-area-ai__container {
6
+ position: relative;
7
+ }
8
+
9
+ .cp-input-text-area-ai__textarea {
10
+ display: block;
11
+ width: 100%;
12
+ padding: 0.5rem 0.75rem;
13
+ padding-right: 6rem;
14
+ font-size: 0.875rem;
15
+ color: var(--ui-input-fg);
16
+ background: var(--ui-input-bg);
17
+ border: 1px solid var(--ui-input-border);
18
+ border-radius: var(--ui-input-radius, 0.375rem);
19
+ outline: none;
20
+ resize: vertical;
21
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
22
+ }
23
+
24
+ .cp-input-text-area-ai__textarea::placeholder {
25
+ color: var(--ui-input-placeholder);
26
+ }
27
+
28
+ .cp-input-text-area-ai__textarea:focus {
29
+ border-color: var(--ui-input-border-focus);
30
+ box-shadow: 0 0 0 1px var(--ui-input-ring);
31
+ }
32
+
33
+ .cp-input-text-area-ai__textarea:disabled {
34
+ opacity: 0.5;
35
+ cursor: not-allowed;
36
+ }
37
+
38
+ .cp-input-text-area-ai__actions {
39
+ position: absolute;
40
+ bottom: 0.5rem;
41
+ right: 0.5rem;
42
+ }
43
+
44
+ .cp-input-text-area-ai__ai-btn {
45
+ display: inline-flex;
46
+ align-items: center;
47
+ gap: 0.375rem;
48
+ padding: 0.375rem 0.75rem;
49
+ font-size: 0.75rem;
50
+ font-weight: 500;
51
+ color: var(--ui-primary);
52
+ background: var(--ui-accent);
53
+ border: none;
54
+ border-radius: var(--ui-radius-sm);
55
+ cursor: pointer;
56
+ transition: background-color 0.15s ease;
57
+ }
58
+
59
+ .cp-input-text-area-ai__ai-btn:hover {
60
+ background: var(--ui-accent-foreground);
61
+ color: var(--ui-accent);
62
+ }
63
+
64
+ .cp-input-text-area-ai__ai-btn:disabled {
65
+ opacity: 0.5;
66
+ cursor: not-allowed;
67
+ }
68
+
69
+ .cp-input-text-area-ai__ai-icon {
70
+ width: 1rem;
71
+ height: 1rem;
72
+ }
73
+
74
+ .cp-input-text-area-ai__spinner--hidden {
75
+ display: none;
76
+ }
77
+
78
+ .cp-input-text-area-ai__streaming {
79
+ position: absolute;
80
+ bottom: 0.5rem;
81
+ left: 0.5rem;
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 0.25rem;
85
+ font-size: 0.75rem;
86
+ color: var(--ui-muted-foreground);
87
+ }
88
+
89
+ .cp-input-text-area-ai__streaming--hidden {
90
+ display: none;
91
+ }
@@ -0,0 +1,20 @@
1
+ <div class="cp-input-wrapper <%= 'cp-input-wrapper--disabled' if disabled %>">
2
+ <% if label %>
3
+ <label for="<%= id || name %>" class="cp-input-wrapper__label">
4
+ <%= label %>
5
+ <% if required %>
6
+ <span class="cp-input-wrapper__required">*</span>
7
+ <% end %>
8
+ </label>
9
+ <% end %>
10
+
11
+ <div class="cp-input-wrapper__field">
12
+ <%= content %>
13
+ </div>
14
+
15
+ <% if error_text %>
16
+ <p class="cp-input-wrapper__error"><%= error_text %></p>
17
+ <% elsif helper_text %>
18
+ <p class="cp-input-wrapper__helper"><%= helper_text %></p>
19
+ <% end %>
20
+ </div>
@@ -0,0 +1,31 @@
1
+ module Aeno::Primitives::InputWrapper
2
+ class Component < ::Aeno::ApplicationViewComponent
3
+ prop :label, description: "Input label", optional: true
4
+ prop :name, description: "Input name attribute"
5
+ prop :id, description: "Input id attribute", optional: true
6
+ prop :helper_text, description: "Helper text below input", optional: true
7
+ prop :error_text, description: "Error message", optional: true
8
+ prop :disabled, description: "Disabled state", default: -> { false }
9
+ prop :required, description: "Required field", default: -> { false }
10
+
11
+ option(:data, default: proc { {} })
12
+
13
+ examples("Input Wrapper", description: "Wrapper for form inputs with label and messages") do |b|
14
+ b.example(:default, title: "Default") do |e|
15
+ e.preview name: "field"
16
+ end
17
+
18
+ b.example(:with_label, title: "With Label") do |e|
19
+ e.preview name: "email", label: "Email Address"
20
+ end
21
+
22
+ b.example(:with_helper, title: "With Helper Text") do |e|
23
+ e.preview name: "username", label: "Username", helper_text: "Choose a unique username"
24
+ end
25
+
26
+ b.example(:with_error, title: "With Error") do |e|
27
+ e.preview name: "email", label: "Email", error_text: "Email is invalid"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,72 @@
1
+ /* Input Wrapper */
2
+ .cp-input-wrapper {
3
+ }
4
+
5
+ .cp-input-wrapper--disabled {
6
+ opacity: 0.5;
7
+ pointer-events: none;
8
+ }
9
+
10
+ .cp-input-wrapper__label {
11
+ display: block;
12
+ font-size: 0.875rem;
13
+ font-weight: 500;
14
+ color: var(--ui-foreground);
15
+ margin-bottom: 0.25rem;
16
+ }
17
+
18
+ .cp-input-wrapper__required {
19
+ color: var(--ui-destructive);
20
+ }
21
+
22
+ .cp-input-wrapper__field {
23
+ position: relative;
24
+ }
25
+
26
+ .cp-input-wrapper__helper {
27
+ margin-top: 0.25rem;
28
+ font-size: 0.875rem;
29
+ color: var(--ui-muted-foreground);
30
+ }
31
+
32
+ .cp-input-wrapper__error {
33
+ margin-top: 0.25rem;
34
+ font-size: 0.875rem;
35
+ color: var(--ui-destructive);
36
+ }
37
+
38
+ /* Shared input styles */
39
+ .cp-input {
40
+ display: block;
41
+ width: 100%;
42
+ padding: 0.5rem 0.75rem;
43
+ font-size: 0.875rem;
44
+ color: var(--ui-input-fg);
45
+ background: var(--ui-input-bg);
46
+ border: 1px solid var(--ui-input-border);
47
+ border-radius: var(--ui-input-radius, 0.375rem);
48
+ outline: none;
49
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
50
+ }
51
+
52
+ .cp-input::placeholder {
53
+ color: var(--ui-input-placeholder);
54
+ }
55
+
56
+ .cp-input:focus {
57
+ border-color: var(--ui-input-border-focus);
58
+ box-shadow: 0 0 0 1px var(--ui-input-ring);
59
+ }
60
+
61
+ .cp-input:disabled {
62
+ opacity: 0.5;
63
+ cursor: not-allowed;
64
+ }
65
+
66
+ .cp-input--error {
67
+ border-color: var(--ui-destructive);
68
+ }
69
+
70
+ .cp-input--error:focus {
71
+ box-shadow: 0 0 0 1px var(--ui-destructive);
72
+ }
@@ -0,0 +1,4 @@
1
+ <div class="<%= component_classes %>">
2
+ <div class="cp-layouts-agentic__sidebar"><%= sidebar %></div>
3
+ <div class="cp-layouts-agentic__content"><%= content %></div>
4
+ </div>
@@ -0,0 +1,9 @@
1
+ module Aeno::Primitives::Layouts::Agentic
2
+ class Component < Aeno::ApplicationViewComponent
3
+ renders_one :sidebar, Aeno::Primitives::Sidebar::Component
4
+
5
+ def component_classes
6
+ classes
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,23 @@
1
+ .cp-layouts-agentic {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+ bottom: 0;
7
+ width: 100vw;
8
+ height: 100vh;
9
+ display: grid;
10
+ grid-template-columns: 1fr 5fr;
11
+
12
+ &__sidebar {
13
+ height: 100vh;
14
+ min-width: 0;
15
+ overflow: hidden;
16
+ border-right: 1px solid var(--ui-border);
17
+ }
18
+
19
+ &__content {
20
+ height: 100vh;
21
+ min-width: 0;
22
+ }
23
+ }
@@ -0,0 +1,9 @@
1
+ module Aeno::Primitives::Layouts::App
2
+ class Aside < Aeno::ApplicationViewComponent
3
+ erb_template <<~ERB
4
+ <aside class="cp-layout-app__aside" style="<%= merged_style %>">
5
+ <%= content %>
6
+ </aside>
7
+ ERB
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ <div class="cp-layout-app<%= " cp-layout-app--with-aside" if aside? %>">
2
+ <%= sidebar %>
3
+ <main class="cp-layout-app__main">
4
+ <% if header? %>
5
+ <header class="cp-layout-app__header">
6
+ <%= header %>
7
+ </header>
8
+ <% end %>
9
+ <div class="cp-layout-app__body">
10
+ <%= content %>
11
+ </div>
12
+ </main>
13
+ <%= aside if aside? %>
14
+ </div>
@@ -0,0 +1,11 @@
1
+ module Aeno::Primitives::Layouts::App
2
+ class Component < Aeno::ApplicationViewComponent
3
+ renders_one :sidebar, ->(style: nil, &block) {
4
+ Aeno::Primitives::Layouts::App::Sidebar.new(style: style, &block)
5
+ }
6
+ renders_one :header
7
+ renders_one :aside, ->(style: nil, &block) {
8
+ Aeno::Primitives::Layouts::App::Aside.new(style: style, &block)
9
+ }
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module Aeno::Primitives::Layouts::App
2
+ class Sidebar < Aeno::ApplicationViewComponent
3
+ erb_template <<~ERB
4
+ <aside class="cp-layout-app__sidebar" style="<%= merged_style %>">
5
+ <%= content %>
6
+ </aside>
7
+ ERB
8
+ end
9
+ end
@@ -0,0 +1,46 @@
1
+ /* App Layout */
2
+ .cp-layout-app {
3
+ position: fixed;
4
+ inset: 0;
5
+ display: grid;
6
+ grid-template-columns: auto 1fr;
7
+ width: 100vw;
8
+ height: 100vh;
9
+
10
+ &--with-aside {
11
+ grid-template-columns: auto 1fr auto;
12
+ }
13
+
14
+ &__sidebar {
15
+ height: 100vh;
16
+ flex-shrink: 0;
17
+ overflow: hidden;
18
+ }
19
+
20
+ &__main {
21
+ display: flex;
22
+ flex-direction: column;
23
+ height: 100vh;
24
+ min-width: 0;
25
+ overflow: hidden;
26
+ }
27
+
28
+ &__header {
29
+ flex-shrink: 0;
30
+ border-bottom: 1px solid var(--ui-sidebar-border, #e5e5e5);
31
+ padding: 1rem 2rem;
32
+ }
33
+
34
+ &__body {
35
+ flex: 1;
36
+ overflow-y: auto;
37
+ padding: 2rem;
38
+ }
39
+
40
+ &__aside {
41
+ height: 100vh;
42
+ flex-shrink: 0;
43
+ overflow: hidden;
44
+ border-left: 1px solid var(--ui-sidebar-border, #e5e5e5);
45
+ }
46
+ }
@@ -0,0 +1,24 @@
1
+ <div class="cp-page">
2
+ <div class="cp-page__header">
3
+ <div class="cp-page__title-area">
4
+ <h1 class="cp-page__title">
5
+ <span class="cp-page__title-text"><%= title %></span>
6
+ <% if subtitle %>
7
+ <span class="cp-page__subtitle"><%= subtitle %></span>
8
+ <% end %>
9
+ </h1>
10
+ <% if description %>
11
+ <p class="cp-page__description"><%= description %></p>
12
+ <% end %>
13
+ </div>
14
+ <% if actions_area %>
15
+ <div class="cp-page__actions">
16
+ <%= actions_area %>
17
+ </div>
18
+ <% end %>
19
+ </div>
20
+
21
+ <div class="cp-page__content">
22
+ <%= content %>
23
+ </div>
24
+ </div>
@@ -0,0 +1,23 @@
1
+ module Aeno::Primitives::Page
2
+ class Component < Aeno::ApplicationViewComponent
3
+ prop :title, description: "Page title"
4
+ prop :subtitle, description: "Optional subtitle", optional: true
5
+ prop :description, description: "Page description", optional: true
6
+
7
+ renders_one(:actions_area)
8
+
9
+ examples("Page", description: "Page layout with title and content") do |b|
10
+ b.example(:default, title: "Default") do |e|
11
+ e.preview title: "Dashboard"
12
+ end
13
+
14
+ b.example(:with_subtitle, title: "With Subtitle") do |e|
15
+ e.preview title: "Users", subtitle: "(123)"
16
+ end
17
+
18
+ b.example(:with_description, title: "With Description") do |e|
19
+ e.preview title: "Settings", description: "Manage your account settings"
20
+ end
21
+ end
22
+ end
23
+ end