active_canvas 0.0.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 (80) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +318 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/javascripts/active_canvas/editor/ai_panel.js +1607 -0
  6. data/app/assets/javascripts/active_canvas/editor/asset_manager.js +498 -0
  7. data/app/assets/javascripts/active_canvas/editor/blocks.js +1083 -0
  8. data/app/assets/javascripts/active_canvas/editor/code_panel.js +572 -0
  9. data/app/assets/javascripts/active_canvas/editor/component_toolbar.js +394 -0
  10. data/app/assets/javascripts/active_canvas/editor/panels.js +460 -0
  11. data/app/assets/javascripts/active_canvas/editor/utils.js +56 -0
  12. data/app/assets/javascripts/active_canvas/editor.js +295 -0
  13. data/app/assets/stylesheets/active_canvas/application.css +15 -0
  14. data/app/assets/stylesheets/active_canvas/editor.css +2929 -0
  15. data/app/controllers/active_canvas/admin/ai_controller.rb +181 -0
  16. data/app/controllers/active_canvas/admin/application_controller.rb +56 -0
  17. data/app/controllers/active_canvas/admin/media_controller.rb +61 -0
  18. data/app/controllers/active_canvas/admin/page_types_controller.rb +57 -0
  19. data/app/controllers/active_canvas/admin/page_versions_controller.rb +23 -0
  20. data/app/controllers/active_canvas/admin/pages_controller.rb +133 -0
  21. data/app/controllers/active_canvas/admin/partials_controller.rb +88 -0
  22. data/app/controllers/active_canvas/admin/settings_controller.rb +256 -0
  23. data/app/controllers/active_canvas/application_controller.rb +20 -0
  24. data/app/controllers/active_canvas/pages_controller.rb +18 -0
  25. data/app/controllers/concerns/active_canvas/current_user.rb +12 -0
  26. data/app/controllers/concerns/active_canvas/rate_limitable.rb +75 -0
  27. data/app/controllers/concerns/active_canvas/tailwind_compilation.rb +39 -0
  28. data/app/helpers/active_canvas/application_helper.rb +4 -0
  29. data/app/jobs/active_canvas/application_job.rb +4 -0
  30. data/app/jobs/active_canvas/compile_tailwind_job.rb +64 -0
  31. data/app/mailers/active_canvas/application_mailer.rb +6 -0
  32. data/app/models/active_canvas/ai_model.rb +136 -0
  33. data/app/models/active_canvas/application_record.rb +5 -0
  34. data/app/models/active_canvas/media.rb +141 -0
  35. data/app/models/active_canvas/page.rb +85 -0
  36. data/app/models/active_canvas/page_type.rb +22 -0
  37. data/app/models/active_canvas/page_version.rb +80 -0
  38. data/app/models/active_canvas/partial.rb +73 -0
  39. data/app/models/active_canvas/setting.rb +292 -0
  40. data/app/services/active_canvas/ai_configuration.rb +40 -0
  41. data/app/services/active_canvas/ai_models.rb +128 -0
  42. data/app/services/active_canvas/ai_service.rb +289 -0
  43. data/app/services/active_canvas/content_sanitizer.rb +112 -0
  44. data/app/services/active_canvas/tailwind_compiler.rb +156 -0
  45. data/app/views/active_canvas/admin/media/index.html.erb +401 -0
  46. data/app/views/active_canvas/admin/media/show.html.erb +297 -0
  47. data/app/views/active_canvas/admin/page_types/_form.html.erb +25 -0
  48. data/app/views/active_canvas/admin/page_types/edit.html.erb +13 -0
  49. data/app/views/active_canvas/admin/page_types/index.html.erb +29 -0
  50. data/app/views/active_canvas/admin/page_types/new.html.erb +9 -0
  51. data/app/views/active_canvas/admin/page_types/show.html.erb +18 -0
  52. data/app/views/active_canvas/admin/page_versions/show.html.erb +469 -0
  53. data/app/views/active_canvas/admin/pages/_form.html.erb +62 -0
  54. data/app/views/active_canvas/admin/pages/content.html.erb +139 -0
  55. data/app/views/active_canvas/admin/pages/edit.html.erb +335 -0
  56. data/app/views/active_canvas/admin/pages/editor.html.erb +710 -0
  57. data/app/views/active_canvas/admin/pages/index.html.erb +149 -0
  58. data/app/views/active_canvas/admin/pages/new.html.erb +19 -0
  59. data/app/views/active_canvas/admin/pages/show.html.erb +258 -0
  60. data/app/views/active_canvas/admin/pages/versions.html.erb +333 -0
  61. data/app/views/active_canvas/admin/partials/edit.html.erb +182 -0
  62. data/app/views/active_canvas/admin/partials/editor.html.erb +703 -0
  63. data/app/views/active_canvas/admin/partials/index.html.erb +131 -0
  64. data/app/views/active_canvas/admin/settings/show.html.erb +1864 -0
  65. data/app/views/active_canvas/pages/no_homepage.html.erb +45 -0
  66. data/app/views/active_canvas/pages/show.html.erb +113 -0
  67. data/app/views/layouts/active_canvas/admin/application.html.erb +960 -0
  68. data/app/views/layouts/active_canvas/admin/editor.html.erb +826 -0
  69. data/app/views/layouts/active_canvas/application.html.erb +55 -0
  70. data/config/routes.rb +48 -0
  71. data/db/migrate/20260202000001_create_active_canvas_tables.rb +113 -0
  72. data/db/migrate/20260202000002_create_active_canvas_ai_models.rb +26 -0
  73. data/lib/active_canvas/configuration.rb +232 -0
  74. data/lib/active_canvas/engine.rb +44 -0
  75. data/lib/active_canvas/version.rb +3 -0
  76. data/lib/active_canvas.rb +26 -0
  77. data/lib/generators/active_canvas/install/install_generator.rb +263 -0
  78. data/lib/generators/active_canvas/install/templates/initializer.rb.tt +163 -0
  79. data/lib/tasks/active_canvas_tasks.rake +69 -0
  80. metadata +150 -0
@@ -0,0 +1,297 @@
1
+ <% content_for :page_title, @medium.filename %>
2
+
3
+ <!-- Page Header -->
4
+ <div class="page-header">
5
+ <div class="page-header-left">
6
+ <h2><%= truncate(@medium.filename, length: 40) %></h2>
7
+ <p class="page-header-subtitle">
8
+ Uploaded <%= time_ago_in_words(@medium.created_at) %> ago
9
+ </p>
10
+ </div>
11
+ <div class="page-header-actions">
12
+ <button type="button" class="btn btn-secondary" onclick="copyMediaUrl('<%= @medium.url %>')">
13
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
14
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
15
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
16
+ </svg>
17
+ Copy URL
18
+ </button>
19
+ <%= link_to @medium.url, target: "_blank", class: "btn btn-secondary" do %>
20
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
21
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
22
+ <polyline points="15 3 21 3 21 9"/>
23
+ <line x1="10" y1="14" x2="21" y2="3"/>
24
+ </svg>
25
+ Open Original
26
+ <% end %>
27
+ <%= link_to admin_media_path, class: "btn btn-ghost" do %>
28
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
29
+ <line x1="19" y1="12" x2="5" y2="12"/>
30
+ <polyline points="12 19 5 12 12 5"/>
31
+ </svg>
32
+ Back to Library
33
+ <% end %>
34
+ </div>
35
+ </div>
36
+
37
+ <div style="display: grid; grid-template-columns: 1fr 380px; gap: 1.5rem;">
38
+ <!-- Preview -->
39
+ <div class="card">
40
+ <div class="card-header">
41
+ <span class="card-title">Preview</span>
42
+ </div>
43
+ <div class="media-preview-container">
44
+ <% if @medium.file.attached? %>
45
+ <%= image_tag @medium.url, alt: @medium.filename, class: "media-preview-image" %>
46
+ <% end %>
47
+ </div>
48
+ </div>
49
+
50
+ <!-- Details Sidebar -->
51
+ <div>
52
+ <!-- File Information -->
53
+ <div class="card" style="margin-bottom: 1.5rem;">
54
+ <div class="card-header">
55
+ <span class="card-title">File Information</span>
56
+ </div>
57
+ <div class="card-body" style="padding: 0;">
58
+ <div class="detail-row">
59
+ <div class="detail-label">Filename</div>
60
+ <div class="detail-value" style="word-break: break-all;"><%= @medium.filename %></div>
61
+ </div>
62
+ <div class="detail-row">
63
+ <div class="detail-label">Type</div>
64
+ <div class="detail-value">
65
+ <span class="badge badge-gray"><%= @medium.content_type %></span>
66
+ </div>
67
+ </div>
68
+ <div class="detail-row">
69
+ <div class="detail-label">Size</div>
70
+ <div class="detail-value"><%= number_to_human_size(@medium.byte_size) %></div>
71
+ </div>
72
+ <% if @medium.metadata["width"].present? && @medium.metadata["height"].present? %>
73
+ <div class="detail-row">
74
+ <div class="detail-label">Dimensions</div>
75
+ <div class="detail-value"><%= @medium.metadata["width"] %> × <%= @medium.metadata["height"] %> px</div>
76
+ </div>
77
+ <% end %>
78
+ <div class="detail-row">
79
+ <div class="detail-label">Uploaded</div>
80
+ <div class="detail-value"><%= @medium.created_at.strftime("%B %d, %Y at %l:%M %p") %></div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+
85
+ <!-- URL -->
86
+ <div class="card" style="margin-bottom: 1.5rem;">
87
+ <div class="card-header">
88
+ <span class="card-title">URL</span>
89
+ </div>
90
+ <div class="card-body">
91
+ <div class="url-display">
92
+ <code id="media-url"><%= @medium.url %></code>
93
+ <button type="button" class="btn btn-sm btn-secondary" onclick="copyMediaUrl('<%= @medium.url %>')">
94
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
95
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
96
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
97
+ </svg>
98
+ Copy
99
+ </button>
100
+ </div>
101
+ <p class="help-text" style="margin-top: 0.75rem;">Use this URL in your pages or external applications.</p>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- Usage (placeholder for future) -->
106
+ <div class="card" style="margin-bottom: 1.5rem;">
107
+ <div class="card-header">
108
+ <span class="card-title">Embed Code</span>
109
+ </div>
110
+ <div class="card-body">
111
+ <div class="embed-code">
112
+ <pre><code>&lt;img src="<%= @medium.url %>" alt="<%= @medium.filename %>" /&gt;</code></pre>
113
+ </div>
114
+ <button type="button" class="btn btn-sm btn-secondary" style="margin-top: 0.75rem;" onclick="copyEmbedCode()">
115
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
116
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
117
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
118
+ </svg>
119
+ Copy Embed Code
120
+ </button>
121
+ </div>
122
+ </div>
123
+
124
+ <!-- Danger Zone -->
125
+ <div class="card" style="border-color: #fecaca;">
126
+ <div class="card-header" style="background: var(--danger-light);">
127
+ <span class="card-title" style="color: var(--danger);">Danger Zone</span>
128
+ </div>
129
+ <div class="card-body">
130
+ <p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem;">
131
+ Deleting this file will remove it permanently. Any pages using this image will show a broken image.
132
+ </p>
133
+ <%= button_to admin_medium_path(@medium), method: :delete, class: "btn btn-danger", style: "width: 100%;", data: { confirm: "Are you sure you want to delete this file? This action cannot be undone." } do %>
134
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
135
+ <polyline points="3 6 5 6 21 6"/>
136
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
137
+ <line x1="10" y1="11" x2="10" y2="17"/>
138
+ <line x1="14" y1="11" x2="14" y2="17"/>
139
+ </svg>
140
+ Delete File
141
+ <% end %>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+
147
+ <style>
148
+ .media-preview-container {
149
+ background: var(--sidebar-bg);
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: center;
153
+ min-height: 400px;
154
+ padding: 2rem;
155
+ }
156
+
157
+ .media-preview-image {
158
+ max-width: 100%;
159
+ max-height: 500px;
160
+ object-fit: contain;
161
+ border-radius: var(--radius);
162
+ box-shadow: var(--shadow-md);
163
+ }
164
+
165
+ .detail-row {
166
+ padding: 0.875rem 1.25rem;
167
+ border-bottom: 1px solid var(--border);
168
+ }
169
+
170
+ .detail-row:last-child {
171
+ border-bottom: none;
172
+ }
173
+
174
+ .detail-row .detail-label {
175
+ font-size: 0.6875rem;
176
+ text-transform: uppercase;
177
+ letter-spacing: 0.05em;
178
+ color: var(--text-muted);
179
+ font-weight: 600;
180
+ margin-bottom: 0.25rem;
181
+ }
182
+
183
+ .detail-row .detail-value {
184
+ font-size: 0.875rem;
185
+ color: var(--text);
186
+ }
187
+
188
+ .url-display {
189
+ display: flex;
190
+ gap: 0.75rem;
191
+ align-items: flex-start;
192
+ }
193
+
194
+ .url-display code {
195
+ flex: 1;
196
+ padding: 0.625rem 0.875rem;
197
+ background: var(--bg-main);
198
+ border: 1px solid var(--border);
199
+ border-radius: var(--radius);
200
+ font-size: 0.75rem;
201
+ word-break: break-all;
202
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
203
+ }
204
+
205
+ .embed-code {
206
+ background: var(--sidebar-bg);
207
+ border-radius: var(--radius);
208
+ padding: 0.875rem;
209
+ overflow-x: auto;
210
+ }
211
+
212
+ .embed-code pre {
213
+ margin: 0;
214
+ }
215
+
216
+ .embed-code code {
217
+ color: #e2e8f0;
218
+ font-size: 0.75rem;
219
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
220
+ white-space: pre-wrap;
221
+ word-break: break-all;
222
+ }
223
+
224
+ /* Toast notification */
225
+ .toast {
226
+ position: fixed;
227
+ bottom: 2rem;
228
+ right: 2rem;
229
+ background: var(--sidebar-bg);
230
+ color: white;
231
+ padding: 0.875rem 1.25rem;
232
+ border-radius: var(--radius);
233
+ font-size: 0.875rem;
234
+ box-shadow: var(--shadow-md);
235
+ z-index: 1000;
236
+ animation: slideIn 0.3s ease;
237
+ }
238
+
239
+ .toast.success {
240
+ background: var(--success);
241
+ }
242
+
243
+ @keyframes slideIn {
244
+ from {
245
+ transform: translateY(1rem);
246
+ opacity: 0;
247
+ }
248
+ to {
249
+ transform: translateY(0);
250
+ opacity: 1;
251
+ }
252
+ }
253
+
254
+ @media (max-width: 900px) {
255
+ div[style*="grid-template-columns: 1fr 380px"] {
256
+ grid-template-columns: 1fr !important;
257
+ }
258
+ }
259
+ </style>
260
+
261
+ <script>
262
+ function copyMediaUrl(url) {
263
+ const fullUrl = window.location.origin + url;
264
+ navigator.clipboard.writeText(fullUrl).then(() => {
265
+ showToast('URL copied to clipboard!');
266
+ }).catch(() => {
267
+ // Fallback
268
+ const input = document.createElement('input');
269
+ input.value = fullUrl;
270
+ document.body.appendChild(input);
271
+ input.select();
272
+ document.execCommand('copy');
273
+ document.body.removeChild(input);
274
+ showToast('URL copied to clipboard!');
275
+ });
276
+ }
277
+
278
+ function copyEmbedCode() {
279
+ const code = document.querySelector('.embed-code code').textContent;
280
+ navigator.clipboard.writeText(code).then(() => {
281
+ showToast('Embed code copied!');
282
+ }).catch(() => {
283
+ showToast('Failed to copy');
284
+ });
285
+ }
286
+
287
+ function showToast(message) {
288
+ const toast = document.createElement('div');
289
+ toast.className = 'toast success';
290
+ toast.textContent = message;
291
+ document.body.appendChild(toast);
292
+
293
+ setTimeout(() => {
294
+ toast.remove();
295
+ }, 3000);
296
+ }
297
+ </script>
@@ -0,0 +1,25 @@
1
+ <% if page_type.errors.any? %>
2
+ <div class="error-messages">
3
+ <strong><%= pluralize(page_type.errors.count, "error") %> prohibited this page type from being saved:</strong>
4
+ <ul>
5
+ <% page_type.errors.full_messages.each do |message| %>
6
+ <li><%= message %></li>
7
+ <% end %>
8
+ </ul>
9
+ </div>
10
+ <% end %>
11
+
12
+ <div class="form-group">
13
+ <%= form.label :name %>
14
+ <%= form.text_field :name, class: "form-control" %>
15
+ </div>
16
+
17
+ <div class="form-group">
18
+ <%= form.label :key %>
19
+ <%= form.text_field :key, class: "form-control", placeholder: "Leave blank for auto-generated key" %>
20
+ </div>
21
+
22
+ <div class="form-actions">
23
+ <%= form.submit class: "btn btn-primary" %>
24
+ <%= link_to "Cancel", admin_page_types_path, class: "btn btn-secondary" %>
25
+ </div>
@@ -0,0 +1,13 @@
1
+ <div class="page-header">
2
+ <h2>Edit Page Type</h2>
3
+ </div>
4
+
5
+ <div class="card" style="padding: 1.5rem;">
6
+ <%= form_with model: @page_type, url: admin_page_type_path(@page_type) do |form| %>
7
+ <%= render "form", form: form, page_type: @page_type %>
8
+ <% end %>
9
+ </div>
10
+
11
+ <div style="margin-top: 1rem;">
12
+ <%= button_to "Delete Page Type", admin_page_type_path(@page_type), method: :delete, class: "btn btn-danger", data: { turbo_confirm: "Are you sure you want to delete this page type?" } %>
13
+ </div>
@@ -0,0 +1,29 @@
1
+ <div class="page-header">
2
+ <h2>Page Types</h2>
3
+ <%= link_to "New Page Type", new_admin_page_type_path, class: "btn btn-primary" %>
4
+ </div>
5
+
6
+ <div class="card">
7
+ <table>
8
+ <thead>
9
+ <tr>
10
+ <th>Name</th>
11
+ <th>Key</th>
12
+ <th>Pages</th>
13
+ <th></th>
14
+ </tr>
15
+ </thead>
16
+ <tbody>
17
+ <% @page_types.each do |page_type| %>
18
+ <tr>
19
+ <td><%= link_to page_type.name, admin_page_type_path(page_type) %></td>
20
+ <td><code><%= page_type.key %></code></td>
21
+ <td><%= page_type.pages.count %></td>
22
+ <td class="actions">
23
+ <%= link_to "Edit", edit_admin_page_type_path(page_type), class: "btn btn-secondary" %>
24
+ </td>
25
+ </tr>
26
+ <% end %>
27
+ </tbody>
28
+ </table>
29
+ </div>
@@ -0,0 +1,9 @@
1
+ <div class="page-header">
2
+ <h2>New Page Type</h2>
3
+ </div>
4
+
5
+ <div class="card" style="padding: 1.5rem;">
6
+ <%= form_with model: @page_type, url: admin_page_types_path do |form| %>
7
+ <%= render "form", form: form, page_type: @page_type %>
8
+ <% end %>
9
+ </div>
@@ -0,0 +1,18 @@
1
+ <div class="page-header">
2
+ <h2><%= @page_type.name %></h2>
3
+ <div class="actions">
4
+ <%= link_to "Edit", edit_admin_page_type_path(@page_type), class: "btn btn-secondary" %>
5
+ <%= link_to "Back", admin_page_types_path, class: "btn btn-secondary" %>
6
+ </div>
7
+ </div>
8
+
9
+ <div class="card">
10
+ <div class="detail-row">
11
+ <div class="detail-label">Key</div>
12
+ <div class="detail-value"><code><%= @page_type.key %></code></div>
13
+ </div>
14
+ <div class="detail-row">
15
+ <div class="detail-label">Pages using this type</div>
16
+ <div class="detail-value"><%= @page_type.pages.count %></div>
17
+ </div>
18
+ </div>